import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { User } from '../../common/user' import { Txn } from '../../common/txn' import { removeUndefinedProps } from '../../common/util/object' export const transact = functions .runWith({ minInstances: 1 }) .https.onCall(async (data: Omit, context) => { const userId = context?.auth?.uid if (!userId) return { status: 'error', message: 'Not authorized' } const { amount, fromType, fromId, toId, toType, description } = data if (fromType !== 'USER') return { status: 'error', message: "From type is only implemented for type 'user'.", } if (fromId !== userId) return { status: 'error', message: 'Must be authenticated with userId equal to specified fromId.', } if (amount <= 0 || isNaN(amount) || !isFinite(amount)) return { status: 'error', message: 'Invalid amount' } // Run as transaction to prevent race conditions. return await firestore.runTransaction(async (transaction) => { const fromDoc = firestore.doc(`users/${userId}`) const fromSnap = await transaction.get(fromDoc) if (!fromSnap.exists) { return { status: 'error', message: 'User not found' } } const fromUser = fromSnap.data() as User if (fromUser.balance < amount) { return { status: 'error', message: `Insufficient balance: ${fromUser.username} needed ${amount} but only had ${fromUser.balance} `, } } if (toType === 'USER') { const toDoc = firestore.doc(`users/${toId}`) const toSnap = await transaction.get(toDoc) if (!toSnap.exists) { return { status: 'error', message: 'User not found' } } const toUser = toSnap.data() as User transaction.update(toDoc, { balance: toUser.balance + amount, totalDeposits: toUser.totalDeposits + amount, }) } const newTxnDoc = firestore.collection(`txns/`).doc() const txn: Txn = removeUndefinedProps({ id: newTxnDoc.id, createdTime: Date.now(), fromId, fromType, toId, toType, amount, // TODO: Unhardcode once we have non-donation txns token: 'M$', category: 'CHARITY', description, }) transaction.create(newTxnDoc, txn) transaction.update(fromDoc, { balance: fromUser.balance - amount, totalDeposits: fromUser.totalDeposits - amount, }) return { status: 'success', txn } }) }) const firestore = admin.firestore()