import type { BankAccount, BankTransaction } from "@/payload-types"; export type BankAccountType = "treasury" | "faction" | "personal"; export type BankTransactionType = | "deposit" | "withdrawal" | "transfer" | "payment" | "fee" | "salary" | "adjustment"; export interface ApplyTransactionInput { type: BankTransactionType; fromAccountId?: number; toAccountId?: number; amount: number; fee?: number; memo?: string; actorId?: number; } type PayloadType = Awaited>; async function getAccountOrThrow(payload: PayloadType, id: number): Promise { const account = (await payload .findByID({ collection: "bank-accounts", id, depth: 0, overrideAccess: true }) .catch(() => null)) as unknown as BankAccount | null; if (!account) throw new Error("Account not found."); return account; } export async function getMainCurrencyId(payload: PayloadType): Promise { try { const gameRules = await payload.findGlobal({ slug: "game-rules", depth: 0 }); const currency = gameRules.mainCurrency; if (typeof currency === "number") return currency; if (currency && typeof currency === "object" && "id" in currency) { return (currency.id as number) ?? null; } return null; } catch { return null; } } export async function getMainCurrencyName(payload: PayloadType): Promise { const currencyId = await getMainCurrencyId(payload); if (!currencyId) return null; try { const resource = (await payload.findByID({ collection: "resources", id: currencyId, depth: 0, overrideAccess: true, })) as unknown as { name?: string | null; codeName?: string | null } | null; if (!resource) return null; return resource.name || resource.codeName || null; } catch { return null; } } export async function createAccount( payload: PayloadType, input: { name: string; accountType: BankAccountType; ownerFactionId?: number; ownerUserId?: number; }, ): Promise { const { name, accountType, ownerFactionId, ownerUserId } = input; const currencyId = await getMainCurrencyId(payload); if (!currencyId) { throw new Error("No main currency is set in Game Rules."); } return (await payload.create({ collection: "bank-accounts", data: { name, accountType, ownerFaction: ownerFactionId ?? null, ownerUser: ownerUserId ?? null, currency: currencyId, balance: 0, status: "open", }, overrideAccess: true, depth: 0, })) as unknown as BankAccount; } export async function ensurePersonalAccount( payload: PayloadType, userId: number, ): Promise { const existing = (await payload.find({ collection: "bank-accounts", where: { and: [{ accountType: { equals: "personal" } }, { ownerUser: { equals: userId } }], }, limit: 1, depth: 0, overrideAccess: true, })) as unknown as { docs: BankAccount[] }; if (existing.docs.length > 0) return existing.docs[0]; const user = (await payload .findByID({ collection: "users", id: userId, depth: 0, overrideAccess: true, }) .catch(() => null)) as unknown as { username?: string; displayName?: string } | null; const label = user?.displayName || user?.username || `User ${userId}`; return createAccount(payload, { name: `${label}'s Account`, accountType: "personal", ownerUserId: userId, }); } export async function applyTransaction( payload: PayloadType, input: ApplyTransactionInput, ): Promise { const { type, fromAccountId, toAccountId, amount, fee = 0, memo, actorId } = input; if (!fromAccountId && !toAccountId) { throw new Error("A transaction must involve at least one account."); } if (amount <= 0) { throw new Error("Amount must be greater than zero."); } if (type === "transfer" && (!fromAccountId || !toAccountId)) { throw new Error("A transfer requires both a source and a destination account."); } const totalDebit = amount + fee; const fromAccount = fromAccountId ? await getAccountOrThrow(payload, fromAccountId) : null; const toAccount = toAccountId ? await getAccountOrThrow(payload, toAccountId) : null; for (const account of [fromAccount, toAccount]) { if (account && account.status === "closed") { throw new Error(`Account "${account.name}" is closed.`); } if (account && account.status === "frozen") { throw new Error(`Account "${account.name}" is frozen.`); } } if (fromAccount && (fromAccount.balance ?? 0) < totalDebit) { throw new Error( `Insufficient funds in "${fromAccount.name}". Available: ${(fromAccount.balance ?? 0).toLocaleString()}, needed: ${totalDebit.toLocaleString()}.`, ); } const timestamp = new Date().toISOString(); const transaction = (await payload.create({ collection: "bank-transactions", data: { transactionNumber: "", type, fromAccount: fromAccountId ?? null, toAccount: toAccountId ?? null, amount, fee: fee ?? 0, memo: memo ?? null, actor: actorId ?? null, status: "completed", timestamp, }, overrideAccess: true, depth: 0, })) as unknown as BankTransaction; const fromNewBalance = fromAccount ? (fromAccount.balance ?? 0) - totalDebit : 0; const toNewBalance = toAccount ? (toAccount.balance ?? 0) + amount : 0; const entries: { account: number; transaction: number; type: BankTransactionType; amount: number; balanceAfter: number; memo: string; timestamp: string; }[] = []; if (fromAccount) { entries.push({ account: fromAccount.id, transaction: transaction.id, type, amount: -totalDebit, balanceAfter: fromNewBalance, memo: memo ?? `${type} — out`, timestamp, }); } if (toAccount) { entries.push({ account: toAccount.id, transaction: transaction.id, type, amount, balanceAfter: toNewBalance, memo: memo ?? `${type} — in`, timestamp, }); } for (const entry of entries) { await payload.create({ collection: "ledger-entries", data: entry, overrideAccess: true, depth: 0, }); } if (fromAccount) { await payload.update({ collection: "bank-accounts", id: fromAccount.id, data: { balance: fromNewBalance }, overrideAccess: true, }); } if (toAccount) { await payload.update({ collection: "bank-accounts", id: toAccount.id, data: { balance: toNewBalance }, overrideAccess: true, }); } return transaction; }