191 lines
5.0 KiB
JavaScript
191 lines
5.0 KiB
JavaScript
const admin = require("firebase-admin");
|
|
const { onDocumentCreated } = require("firebase-functions/firestore");
|
|
const { HttpsError, onCall } = require("firebase-functions/https");
|
|
|
|
const { REGION } = require("../index");
|
|
const {
|
|
ORDER_TYPES,
|
|
ORDER_STATUS,
|
|
ORDERS_COLLECTION,
|
|
createOrderDocument,
|
|
normalizeAmount,
|
|
} = require("./helpers/orders");
|
|
|
|
const USERS_COLLECTION = "users";
|
|
|
|
const onOrderCreated = onDocumentCreated(
|
|
`${ORDERS_COLLECTION}/{orderId}`,
|
|
async (event) => {
|
|
const orderRef = event?.data?.ref;
|
|
const orderData = event?.data?.data();
|
|
|
|
if (!orderRef || !orderData) {
|
|
return;
|
|
}
|
|
|
|
if (orderData?.processedAt) {
|
|
return;
|
|
}
|
|
|
|
const userId =
|
|
typeof orderData.userId === "string" ? orderData.userId.trim() : "";
|
|
const amount = normalizeAmount(orderData.amount);
|
|
|
|
if (!userId) {
|
|
await orderRef.set(
|
|
{
|
|
status: ORDER_STATUS.REJECTED,
|
|
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
failureReason: "USER_NOT_FOUND",
|
|
},
|
|
{ merge: true },
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (amount === null || amount === 0) {
|
|
await orderRef.set(
|
|
{
|
|
status: ORDER_STATUS.REJECTED,
|
|
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
failureReason: "INVALID_AMOUNT",
|
|
},
|
|
{ merge: true },
|
|
);
|
|
return;
|
|
}
|
|
|
|
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(userId);
|
|
|
|
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: admin.firestore.FieldValue.serverTimestamp(),
|
|
failureReason: "INSUFFICIENT_FUNDS",
|
|
balanceBefore: currentBalance,
|
|
balanceAfter: currentBalance,
|
|
},
|
|
{ merge: true },
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (userSnapshot?.exists) {
|
|
transaction.update(userRef, {
|
|
coins: nextBalance,
|
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
});
|
|
} else {
|
|
transaction.set(
|
|
userRef,
|
|
{
|
|
coins: nextBalance,
|
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
},
|
|
{ merge: true },
|
|
);
|
|
}
|
|
|
|
transaction.set(
|
|
orderRef,
|
|
{
|
|
status: ORDER_STATUS.APPLIED,
|
|
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
balanceBefore: currentBalance,
|
|
balanceAfter: nextBalance,
|
|
},
|
|
{ merge: true },
|
|
);
|
|
});
|
|
} catch (error) {
|
|
console.error(
|
|
"[orders-onOrderCreated] Failed to process order",
|
|
orderRef.id,
|
|
error,
|
|
);
|
|
|
|
await orderRef.set(
|
|
{
|
|
status: ORDER_STATUS.REJECTED,
|
|
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
failureReason: "PROCESSING_ERROR",
|
|
errorMessage: error?.message || String(error),
|
|
},
|
|
{ merge: true },
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
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.",
|
|
);
|
|
}
|
|
|
|
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;
|
|
|
|
if (currentCoins + amount < 0) {
|
|
throw new HttpsError(
|
|
"failed-precondition",
|
|
"Crédits insuffisants pour finaliser l'opération.",
|
|
);
|
|
}
|
|
|
|
const { orderId } = await createOrderDocument({
|
|
userId: auth.uid,
|
|
type: ORDER_TYPES.SONG,
|
|
amount,
|
|
songId,
|
|
createdBy: auth.uid,
|
|
metadata: {
|
|
source: data?.source || "music_generation",
|
|
requestId:
|
|
typeof data?.requestId === "string" ? data.requestId : undefined,
|
|
},
|
|
});
|
|
|
|
return { orderId };
|
|
});
|
|
|
|
module.exports = {
|
|
onOrderCreated,
|
|
createSongOrder,
|
|
};
|