"use server"; import config from "@payload-config"; import { isPayloadUser } from "@/utils/access-control/isPayloadUser"; import { getPayload } from "payload"; import type { User } from "@/payload-types"; import { hasPermission } from "@/utils/access-control/hasPermission"; import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification"; import { emitGameEvent } from "@/utils/event-log/emit"; import { EventTypes } from "@/utils/event-log/eventTypes"; import { applyTransaction, createAccount, ensurePersonalAccount, getMainCurrencyName, } from "@/lib/banking"; import { notifyUser } from "@/lib/notifications"; interface ActionResult { success: boolean; error?: string; data?: T; } async function authenticate() { const payloadConfig = await config; const payload = await getPayload({ config: payloadConfig }); const { headers } = await import("next/headers"); const hdrs = await headers(); const { user } = await payload.auth({ headers: hdrs, canSetHeaders: false, }); if (!isPayloadUser(user)) { throw new Error("Unauthorized"); } return { payload, user }; } async function isBankingManager( payload: Awaited>, user: User, ): Promise { if (await hasPermission(payload, user, "banking:manage")) return true; return hasLogisticsQualification(payload, user); } export async function createBankAccount(input: { name: string; accountType: "treasury" | "faction" | "personal"; ownerFactionId?: number; ownerUserId?: number; }): Promise> { try { const { payload, user } = await authenticate(); const manager = await isBankingManager(payload, user); if (input.accountType === "personal") { const ownerId = input.ownerUserId ?? (user.id as number); if (!manager && ownerId !== user.id) { return { success: false, error: "You can only create a personal account for yourself.", }; } const existing = await payload.find({ collection: "bank-accounts", where: { and: [{ accountType: { equals: "personal" } }, { ownerUser: { equals: ownerId } }], }, limit: 1, depth: 0, overrideAccess: true, }); if (existing.docs.length > 0) { return { success: false, error: "This user already has a personal account.", }; } const account = await createAccount(payload, { name: input.name, accountType: "personal", ownerUserId: ownerId, }); await emitGameEvent(payload, { type: EventTypes.BankAccountCreate, message: `Created personal account "${account.name}"`, actor: user.id, targetCollection: "bank-accounts", targetId: account.id, data: { accountType: "personal", ownerUserId: ownerId }, }); return { success: true, data: account.id }; } if (!manager) { return { success: false, error: "Insufficient permissions. Logistics personnel or higher can manage unit accounts.", }; } if (input.accountType === "faction" && !input.ownerFactionId) { return { success: false, error: "A faction account requires an owner faction.", }; } const account = await createAccount(payload, { name: input.name, accountType: input.accountType, ownerFactionId: input.ownerFactionId, }); await emitGameEvent(payload, { type: EventTypes.BankAccountCreate, message: `Created ${input.accountType} account "${account.name}"`, actor: user.id, targetCollection: "bank-accounts", targetId: account.id, data: { accountType: input.accountType, ownerFactionId: input.ownerFactionId }, }); return { success: true, data: account.id }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; if (message === "Unauthorized") { return { success: false, error: "You must be logged in." }; } return { success: false, error: message }; } } export async function ensureMyAccount(): Promise> { try { const { payload, user } = await authenticate(); const account = await ensurePersonalAccount(payload, user.id as number); await emitGameEvent(payload, { type: EventTypes.BankAccountCreate, message: `Created personal account "${account.name}"`, actor: user.id, targetCollection: "bank-accounts", targetId: account.id, data: { accountType: "personal", ownerUserId: user.id }, }); return { success: true, data: account.id }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; if (message === "Unauthorized") { return { success: false, error: "You must be logged in." }; } return { success: false, error: message }; } } export async function depositFunds( accountId: number, amount: number, memo?: string, ): Promise> { return moveFunds("deposit", undefined, accountId, amount, memo); } export async function withdrawFunds( accountId: number, amount: number, memo?: string, ): Promise> { return moveFunds("withdrawal", accountId, undefined, amount, memo); } export async function transferFunds( fromAccountId: number, toAccountId: number, amount: number, memo?: string, ): Promise> { return moveFunds("transfer", fromAccountId, toAccountId, amount, memo); } async function moveFunds( type: "deposit" | "withdrawal" | "transfer", fromAccountId: number | undefined, toAccountId: number | undefined, amount: number, memo?: string, ): Promise> { try { const { payload, user } = await authenticate(); if (!(await hasPermission(payload, user, "bank-accounts:create"))) { return { success: false, error: "Insufficient permissions." }; } if (!amount || amount <= 0) { return { success: false, error: "Amount must be greater than zero." }; } const manager = await isBankingManager(payload, user); const accountChecks: { id?: number; label: string }[] = []; if (fromAccountId) accountChecks.push({ id: fromAccountId, label: "source" }); if (toAccountId) accountChecks.push({ id: toAccountId, label: "destination" }); const accountOwners = new Map(); for (const check of accountChecks) { const account = (await payload .findByID({ collection: "bank-accounts", id: check.id as number, depth: 0, overrideAccess: true, }) .catch(() => null)) as unknown as { id: number; accountType: string; ownerUser?: number | { id: number } | null; status: string; } | null; if (!account) { return { success: false, error: `${check.label} account not found.` }; } if (account.status === "closed") { return { success: false, error: `${check.label} account is closed.` }; } const ownerId = typeof account.ownerUser === "object" ? account.ownerUser?.id : account.ownerUser; accountOwners.set(check.id as number, ownerId ?? null); if (account.accountType === "personal") { if (!manager && ownerId !== user.id) { return { success: false, error: "You can only move funds on your own personal account.", }; } } else if (!manager) { return { success: false, error: "Insufficient permissions. Logistics personnel or higher can move unit funds.", }; } } if (type === "transfer" && (!fromAccountId || !toAccountId)) { return { success: false, error: "A transfer requires both a source and a destination account.", }; } if (type === "deposit" && !toAccountId) { return { success: false, error: "A deposit requires a destination account." }; } if (type === "withdrawal" && !fromAccountId) { return { success: false, error: "A withdrawal requires a source account." }; } const transaction = await applyTransaction(payload, { type, fromAccountId, toAccountId, amount, memo, actorId: user.id as number, }); const currency = await getMainCurrencyName(payload); const suffix = currency ? ` ${currency}` : ""; const amountLabel = `${amount.toLocaleString()}${suffix}`; const eventType = type === "deposit" ? EventTypes.FinanceDeposit : type === "withdrawal" ? EventTypes.FinanceWithdraw : EventTypes.FinanceTransfer; const eventMessage = type === "deposit" ? `Deposited ${amountLabel} into account #${toAccountId}` : type === "withdrawal" ? `Withdrew ${amountLabel} from account #${fromAccountId}` : `Transferred ${amountLabel} from account #${fromAccountId} to account #${toAccountId}`; await emitGameEvent(payload, { type: eventType, message: eventMessage, actor: user.id, targetCollection: "bank-transactions", targetId: transaction.id, data: { fromAccountId, toAccountId, amount, type }, }); const notifications: { userId: number; message: string; link: string }[] = []; if (type === "deposit" && toAccountId) { const ownerId = accountOwners.get(toAccountId) ?? null; if (ownerId != null) { notifications.push({ userId: ownerId, message: `Deposited ${amountLabel} into your account (#${toAccountId}).`, link: `/logistics/banking/${toAccountId}`, }); } } else if (type === "withdrawal" && fromAccountId) { const ownerId = accountOwners.get(fromAccountId) ?? null; if (ownerId != null) { notifications.push({ userId: ownerId, message: `Withdrew ${amountLabel} from your account (#${fromAccountId}).`, link: `/logistics/banking/${fromAccountId}`, }); } } else if (type === "transfer" && fromAccountId && toAccountId) { const fromOwner = accountOwners.get(fromAccountId) ?? null; const toOwner = accountOwners.get(toAccountId) ?? null; if (fromOwner != null) { notifications.push({ userId: fromOwner, message: `Transferred ${amountLabel} from your account (#${fromAccountId}) to account #${toAccountId}.`, link: `/logistics/banking/${fromAccountId}`, }); } if (toOwner != null && toOwner !== fromOwner) { notifications.push({ userId: toOwner, message: `Received ${amountLabel} in account #${toAccountId} from account #${fromAccountId}.`, link: `/logistics/banking/${toAccountId}`, }); } } const notifType = type === "deposit" ? "finance:deposit" : type === "withdrawal" ? "finance:withdraw" : "finance:transfer"; const notifTitle = type === "deposit" ? "Bank deposit" : type === "withdrawal" ? "Bank withdrawal" : "Bank transfer"; for (const n of notifications) { await notifyUser(payload, { userId: n.userId, type: notifType, title: notifTitle, message: n.message, link: n.link, }); } return { success: true, data: transaction.id }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; if (message === "Unauthorized") { return { success: false, error: "You must be logged in." }; } return { success: false, error: message }; } }