feat: fixes and formatter
This commit is contained in:
+174
-205
@@ -1,86 +1,81 @@
|
||||
const admin = require("firebase-admin");
|
||||
const { FieldValue } = require("firebase-admin/firestore");
|
||||
const { onDocumentCreated } = require("firebase-functions/firestore");
|
||||
const { HttpsError, onCall } = require("firebase-functions/https");
|
||||
const admin = require('firebase-admin')
|
||||
const { FieldValue } = require('firebase-admin/firestore')
|
||||
const { onDocumentCreated } = require('firebase-functions/firestore')
|
||||
const { HttpsError, onCall } = require('firebase-functions/https')
|
||||
|
||||
const { REGION, ALERT_TYPE } = require("../index");
|
||||
const { sendNotification } = require("./notifications");
|
||||
const { REGION, ALERT_TYPE } = require('../index')
|
||||
const { sendNotification } = require('./notifications')
|
||||
const {
|
||||
ORDER_TYPES,
|
||||
ORDER_STATUS,
|
||||
ORDERS_COLLECTION,
|
||||
createOrderDocument,
|
||||
normalizeAmount,
|
||||
} = require("./helpers/orders");
|
||||
} = require('./helpers/orders')
|
||||
|
||||
const USERS_COLLECTION = "users";
|
||||
const USERS_COLLECTION = 'users'
|
||||
|
||||
const formatCoinsText = (value) => {
|
||||
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||
return null;
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const absoluteValue = Math.abs(value);
|
||||
const absoluteValue = Math.abs(value)
|
||||
if (!Number.isFinite(absoluteValue)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const formatted = Number.isInteger(absoluteValue)
|
||||
? `${absoluteValue}`
|
||||
: absoluteValue.toFixed(2);
|
||||
const suffix = absoluteValue === 1 ? "crédit" : "crédits";
|
||||
const formatted = Number.isInteger(absoluteValue) ? `${absoluteValue}` : absoluteValue.toFixed(2)
|
||||
const suffix = absoluteValue === 1 ? 'crédit' : 'crédits'
|
||||
|
||||
return `${formatted} ${suffix}`;
|
||||
};
|
||||
return `${formatted} ${suffix}`
|
||||
}
|
||||
|
||||
const buildOrderNotificationContent = ({ amount, orderType, balanceAfter }) => {
|
||||
if (typeof amount !== "number" || Number.isNaN(amount) || amount === 0) {
|
||||
return null;
|
||||
if (typeof amount !== 'number' || Number.isNaN(amount) || amount === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const coinsText = formatCoinsText(amount);
|
||||
const coinsText = formatCoinsText(amount)
|
||||
if (!coinsText) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const balanceText = formatCoinsText(balanceAfter);
|
||||
const balanceSentence = balanceText
|
||||
? ` Ton solde est maintenant de ${balanceText}.`
|
||||
: "";
|
||||
const balanceText = formatCoinsText(balanceAfter)
|
||||
const balanceSentence = balanceText ? ` Ton solde est maintenant de ${balanceText}.` : ''
|
||||
|
||||
if (amount > 0) {
|
||||
if (orderType === ORDER_TYPES.COINS) {
|
||||
return {
|
||||
title: "Crédits achetés",
|
||||
title: 'Crédits achetés',
|
||||
message: `Ton achat de ${coinsText} est confirmé.${balanceSentence}`,
|
||||
action: "PURCHASED",
|
||||
};
|
||||
action: 'PURCHASED',
|
||||
}
|
||||
}
|
||||
|
||||
if (orderType === ORDER_TYPES.GIFT) {
|
||||
return {
|
||||
title: "Crédits reçus",
|
||||
title: 'Crédits reçus',
|
||||
message: `Tu as reçu ${coinsText}.${balanceSentence}`,
|
||||
action: "EARNED",
|
||||
};
|
||||
action: 'EARNED',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: "Crédits ajoutés",
|
||||
title: 'Crédits ajoutés',
|
||||
message: `Ton solde augmente de ${coinsText}.${balanceSentence}`,
|
||||
action: "CREDITED",
|
||||
};
|
||||
action: 'CREDITED',
|
||||
}
|
||||
}
|
||||
|
||||
const reason =
|
||||
orderType === ORDER_TYPES.SONG ? " pour générer un nouveau son" : "";
|
||||
const reason = orderType === ORDER_TYPES.SONG ? ' pour générer un nouveau son' : ''
|
||||
|
||||
return {
|
||||
title: "Crédits dépensés",
|
||||
title: 'Crédits dépensés',
|
||||
message: `Tu as dépensé ${coinsText}${reason}.${balanceSentence}`,
|
||||
action: "SPENT",
|
||||
};
|
||||
};
|
||||
action: 'SPENT',
|
||||
}
|
||||
}
|
||||
|
||||
const notifyOrderApplied = async ({
|
||||
userId,
|
||||
@@ -95,226 +90,200 @@ const notifyOrderApplied = async ({
|
||||
amount,
|
||||
orderType,
|
||||
balanceAfter,
|
||||
});
|
||||
})
|
||||
|
||||
if (!content || !userId) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await sendNotification({
|
||||
sender: "SYSTEM",
|
||||
sender: 'SYSTEM',
|
||||
receiver: userId,
|
||||
receiverCollection: USERS_COLLECTION,
|
||||
title: content.title,
|
||||
message: content.message,
|
||||
data: {
|
||||
type: ALERT_TYPE?.CREDITS_UPDATED || "CREDITS_UPDATED",
|
||||
type: ALERT_TYPE?.CREDITS_UPDATED || 'CREDITS_UPDATED',
|
||||
orderId,
|
||||
orderType: orderType || null,
|
||||
amount,
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
action: content.action,
|
||||
source:
|
||||
typeof metadata?.source === "string" ? metadata.source : null,
|
||||
source: typeof metadata?.source === 'string' ? metadata.source : null,
|
||||
metadata: metadata || {},
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[orders-onOrderCreated] Failed to send notification",
|
||||
orderId,
|
||||
error,
|
||||
);
|
||||
console.error('[orders-onOrderCreated] Failed to send notification', orderId, error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onOrderCreated = onDocumentCreated(
|
||||
`${ORDERS_COLLECTION}/{orderId}`,
|
||||
async (event) => {
|
||||
const orderRef = event?.data?.ref;
|
||||
const orderData = event?.data?.data();
|
||||
const onOrderCreated = onDocumentCreated(`${ORDERS_COLLECTION}/{orderId}`, async (event) => {
|
||||
const orderRef = event?.data?.ref
|
||||
const orderData = event?.data?.data()
|
||||
|
||||
if (!orderRef || !orderData) {
|
||||
return;
|
||||
}
|
||||
if (!orderRef || !orderData) {
|
||||
return
|
||||
}
|
||||
|
||||
if (orderData?.processedAt) {
|
||||
return;
|
||||
}
|
||||
if (orderData?.processedAt) {
|
||||
return
|
||||
}
|
||||
|
||||
const userId =
|
||||
typeof orderData.userId === "string" ? orderData.userId.trim() : "";
|
||||
const amount = normalizeAmount(orderData.amount);
|
||||
const userId = typeof orderData.userId === 'string' ? orderData.userId.trim() : ''
|
||||
const amount = normalizeAmount(orderData.amount)
|
||||
|
||||
if (!userId) {
|
||||
await orderRef.set(
|
||||
{
|
||||
status: ORDER_STATUS.REJECTED,
|
||||
processedAt: FieldValue.serverTimestamp(),
|
||||
failureReason: "USER_NOT_FOUND",
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!userId) {
|
||||
await orderRef.set(
|
||||
{
|
||||
status: ORDER_STATUS.REJECTED,
|
||||
processedAt: FieldValue.serverTimestamp(),
|
||||
failureReason: 'USER_NOT_FOUND',
|
||||
},
|
||||
{ merge: true }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (amount === null || amount === 0) {
|
||||
await orderRef.set(
|
||||
{
|
||||
status: ORDER_STATUS.REJECTED,
|
||||
processedAt: FieldValue.serverTimestamp(),
|
||||
failureReason: "INVALID_AMOUNT",
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (amount === null || amount === 0) {
|
||||
await orderRef.set(
|
||||
{
|
||||
status: ORDER_STATUS.REJECTED,
|
||||
processedAt: FieldValue.serverTimestamp(),
|
||||
failureReason: 'INVALID_AMOUNT',
|
||||
},
|
||||
{ merge: true }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(userId);
|
||||
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(userId)
|
||||
|
||||
let notificationContext = null;
|
||||
let notificationContext = null
|
||||
|
||||
try {
|
||||
await admin.firestore().runTransaction(async (transaction) => {
|
||||
const userSnapshot = await transaction.get(userRef);
|
||||
const userData = userSnapshot?.data() || {};
|
||||
const currentBalanceValue = normalizeAmount(userData?.coins);
|
||||
const currentBalance =
|
||||
currentBalanceValue !== null ? currentBalanceValue : 0;
|
||||
try {
|
||||
await admin.firestore().runTransaction(async (transaction) => {
|
||||
const userSnapshot = await transaction.get(userRef)
|
||||
const userData = userSnapshot?.data() || {}
|
||||
const currentBalanceValue = normalizeAmount(userData?.coins)
|
||||
const currentBalance = currentBalanceValue !== null ? currentBalanceValue : 0
|
||||
|
||||
const nextBalance = currentBalance + amount;
|
||||
|
||||
if (
|
||||
amount < 0 &&
|
||||
nextBalance < 0 &&
|
||||
orderData?.type === ORDER_TYPES.SONG
|
||||
) {
|
||||
transaction.set(
|
||||
orderRef,
|
||||
{
|
||||
status: ORDER_STATUS.REJECTED,
|
||||
processedAt: FieldValue.serverTimestamp(),
|
||||
failureReason: "INSUFFICIENT_FUNDS",
|
||||
balanceBefore: currentBalance,
|
||||
balanceAfter: currentBalance,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userSnapshot?.exists) {
|
||||
transaction.update(userRef, {
|
||||
coins: nextBalance,
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
});
|
||||
} else {
|
||||
transaction.set(
|
||||
userRef,
|
||||
{
|
||||
coins: nextBalance,
|
||||
createdAt: FieldValue.serverTimestamp(),
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
}
|
||||
const nextBalance = currentBalance + amount
|
||||
|
||||
if (amount < 0 && nextBalance < 0 && orderData?.type === ORDER_TYPES.SONG) {
|
||||
transaction.set(
|
||||
orderRef,
|
||||
{
|
||||
status: ORDER_STATUS.APPLIED,
|
||||
status: ORDER_STATUS.REJECTED,
|
||||
processedAt: FieldValue.serverTimestamp(),
|
||||
failureReason: 'INSUFFICIENT_FUNDS',
|
||||
balanceBefore: currentBalance,
|
||||
balanceAfter: nextBalance,
|
||||
balanceAfter: currentBalance,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
{ merge: true }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
notificationContext = {
|
||||
if (userSnapshot?.exists) {
|
||||
transaction.update(userRef, {
|
||||
coins: nextBalance,
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
})
|
||||
} else {
|
||||
transaction.set(
|
||||
userRef,
|
||||
{
|
||||
coins: nextBalance,
|
||||
createdAt: FieldValue.serverTimestamp(),
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
)
|
||||
}
|
||||
|
||||
transaction.set(
|
||||
orderRef,
|
||||
{
|
||||
status: ORDER_STATUS.APPLIED,
|
||||
processedAt: FieldValue.serverTimestamp(),
|
||||
balanceBefore: currentBalance,
|
||||
balanceAfter: nextBalance,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[orders-onOrderCreated] Failed to process order",
|
||||
orderRef.id,
|
||||
error,
|
||||
);
|
||||
|
||||
await orderRef.set(
|
||||
{
|
||||
status: ORDER_STATUS.REJECTED,
|
||||
processedAt: FieldValue.serverTimestamp(),
|
||||
failureReason: "PROCESSING_ERROR",
|
||||
errorMessage: error?.message || String(error),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
{ merge: true }
|
||||
)
|
||||
|
||||
if (notificationContext) {
|
||||
await notifyOrderApplied({
|
||||
userId,
|
||||
orderId: orderRef.id,
|
||||
amount,
|
||||
orderType:
|
||||
typeof orderData?.type === "string" ? orderData.type : null,
|
||||
balanceBefore: notificationContext.balanceBefore,
|
||||
balanceAfter: notificationContext.balanceAfter,
|
||||
metadata: orderData?.metadata || {},
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
notificationContext = {
|
||||
balanceBefore: currentBalance,
|
||||
balanceAfter: nextBalance,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[orders-onOrderCreated] Failed to process order', orderRef.id, error)
|
||||
|
||||
const createSongOrder = onCall({ region: REGION }, async (request) => {
|
||||
const { auth, data } = request || {};
|
||||
|
||||
if (!auth?.uid) {
|
||||
throw new HttpsError("unauthenticated", "Authentification requise.");
|
||||
await orderRef.set(
|
||||
{
|
||||
status: ORDER_STATUS.REJECTED,
|
||||
processedAt: FieldValue.serverTimestamp(),
|
||||
failureReason: 'PROCESSING_ERROR',
|
||||
errorMessage: error?.message || String(error),
|
||||
},
|
||||
{ merge: true }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const amount = normalizeAmount(data?.amount);
|
||||
if (notificationContext) {
|
||||
await notifyOrderApplied({
|
||||
userId,
|
||||
orderId: orderRef.id,
|
||||
amount,
|
||||
orderType: typeof orderData?.type === 'string' ? orderData.type : null,
|
||||
balanceBefore: notificationContext.balanceBefore,
|
||||
balanceAfter: notificationContext.balanceAfter,
|
||||
metadata: orderData?.metadata || {},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const createSongOrder = onCall({ region: REGION }, async (request) => {
|
||||
const { auth, data } = request || {}
|
||||
|
||||
if (!auth?.uid) {
|
||||
throw new HttpsError('unauthenticated', 'Authentification requise.')
|
||||
}
|
||||
|
||||
const amount = normalizeAmount(data?.amount)
|
||||
|
||||
if (amount === null || amount >= 0) {
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
"Le montant doit être négatif pour un achat de musique.",
|
||||
);
|
||||
'invalid-argument',
|
||||
'Le montant doit être négatif pour un achat de musique.'
|
||||
)
|
||||
}
|
||||
|
||||
const songId =
|
||||
typeof data?.songId === "string" && data.songId.trim()
|
||||
? data.songId.trim()
|
||||
: null;
|
||||
const songId = typeof data?.songId === 'string' && data.songId.trim() ? data.songId.trim() : null
|
||||
|
||||
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(auth.uid);
|
||||
const userSnapshot = await userRef.get();
|
||||
const currentCoinsValue = normalizeAmount(userSnapshot?.data()?.coins);
|
||||
const currentCoins =
|
||||
currentCoinsValue !== null ? currentCoinsValue : 0;
|
||||
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(auth.uid)
|
||||
const userSnapshot = await userRef.get()
|
||||
const currentCoinsValue = normalizeAmount(userSnapshot?.data()?.coins)
|
||||
const currentCoins = currentCoinsValue !== null ? currentCoinsValue : 0
|
||||
|
||||
if (currentCoins + amount < 0) {
|
||||
throw new HttpsError(
|
||||
"failed-precondition",
|
||||
"Crédits insuffisants pour finaliser l'opération.",
|
||||
);
|
||||
throw new HttpsError('failed-precondition', "Crédits insuffisants pour finaliser l'opération.")
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
source:
|
||||
typeof data?.source === "string" && data.source.trim()
|
||||
typeof data?.source === 'string' && data.source.trim()
|
||||
? data.source.trim()
|
||||
: "music_generation",
|
||||
};
|
||||
: 'music_generation',
|
||||
}
|
||||
|
||||
if (typeof data?.requestId === "string" && data.requestId.trim()) {
|
||||
metadata.requestId = data.requestId.trim();
|
||||
if (typeof data?.requestId === 'string' && data.requestId.trim()) {
|
||||
metadata.requestId = data.requestId.trim()
|
||||
}
|
||||
|
||||
const { orderId } = await createOrderDocument({
|
||||
@@ -324,12 +293,12 @@ const createSongOrder = onCall({ region: REGION }, async (request) => {
|
||||
songId,
|
||||
createdBy: auth.uid,
|
||||
metadata,
|
||||
});
|
||||
})
|
||||
|
||||
return { orderId };
|
||||
});
|
||||
return { orderId }
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
onOrderCreated,
|
||||
createSongOrder,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user