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, };