import { Timestamp, type Transaction } from "firebase-admin/firestore"; import { firestore } from "../firebase"; import { HttpError } from "../middleware/errors"; import { createLedgerEntryTx } from "../repositories/pointLedgerRepository"; import { getWalletTx, walletDocRef } from "../repositories/walletRepository"; import { OP_BY_TYPE, PointLedgerType, type PointChange, type PointLedgerEntry, type WalletDoc } from "../types/points"; const TX_ID = /^[A-Za-z0-9:_-]{1,240}$/; export function isAlreadyExistsError(err: unknown): boolean { const code = (err as { code?: unknown })?.code; return code === 6 || code === "already-exists" || code === "ALREADY_EXISTS"; } export async function applyPointChangesTx(tx: Transaction, uid: string, changes: PointChange[]): Promise { for (const c of changes) if (!TX_ID.test(c.txId) || !Number.isSafeInteger(c.amount) || c.amount <= 0) throw new HttpError(400, "invalid point change", "INVALID_POINT_CHANGE"); const existing = await getWalletTx(tx, uid); if (changes.length === 0) return existing; const now = Timestamp.now(); const wallet: WalletDoc = existing ? { ...existing } : { availableBalance: 0, totalEarned: 0, totalSpent: 0, version: 0, createdAt: now, updatedAt: now }; for (const c of changes) { const beforeA = wallet.availableBalance; const op = OP_BY_TYPE[c.type]; if (op === "credit") { wallet.availableBalance += c.amount; if (c.type === PointLedgerType.OrderRefund) wallet.totalSpent -= c.amount; else wallet.totalEarned += c.amount; } if (op === "debit") { wallet.availableBalance -= c.amount; wallet.totalSpent += c.amount; } if (wallet.availableBalance < 0) throw new HttpError(409, "insufficient balance", "INSUFFICIENT_BALANCE"); const entry: PointLedgerEntry = { ...c, uid, op, availableBefore: beforeA, availableAfter: wallet.availableBalance, createdAt: now }; createLedgerEntryTx(tx, uid, entry); } if (wallet.availableBalance !== wallet.totalEarned - wallet.totalSpent) throw new Error("wallet invariant violated"); wallet.version += 1; wallet.updatedAt = now; tx.set(walletDocRef(uid), wallet); return wallet; } export async function applyPointChanges(uid: string, changes: PointChange[]) { return firestore.runTransaction((tx) => applyPointChangesTx(tx, uid, changes)); }