import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { BankAccount } from "@/payload-types"; import { createAccount, ensurePersonalAccount, getTreasuryAccountId } from "@/lib/banking"; import { getBalancesBySteamId, parseMoneyDelta, processMoneyEvent } from "@/lib/arma-bridge/money"; let payload: Payload; const RUN = `money-${Date.now().toString(36)}`; const STEAM_ID = `${RUN}-steam-001`; const UNLINKED = `${RUN}-steam-unlinked`; async function balanceOf(id: number): Promise { const acc = (await payload.findByID({ collection: "bank-accounts", id, depth: 0, overrideAccess: true, })) as unknown as BankAccount; return acc.balance ?? 0; } let eventSeq = 0; function moneyEvent(delta: number, reason: string, detail = "") { eventSeq += 1; return { eventId: `${RUN}:${eventSeq}`, payload: [STEAM_ID, delta, reason, detail] }; } describe("Arma shop money sync", () => { let userId: number; let accountId: number; let treasuryId: number; let createdTreasury = false; beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); const user = await payload.create({ collection: "users", data: { username: `${RUN}-player`, discordUsername: `${RUN}-player-discord`, displayName: `${RUN} Player`, steamId: STEAM_ID, password: "Test1234", }, overrideAccess: true, depth: 0, }); userId = user.id; const existingTreasury = await getTreasuryAccountId(payload); if (existingTreasury) { treasuryId = existingTreasury; } else { const treasury = await createAccount(payload, { name: `${RUN} Treasury`, accountType: "treasury" }); treasuryId = treasury.id; createdTreasury = true; } }); afterAll(async () => { if (accountId) { await payload.delete({ collection: "bank-accounts", id: accountId, overrideAccess: true }).catch(() => {}); } if (createdTreasury && treasuryId) { await payload.delete({ collection: "bank-accounts", id: treasuryId, overrideAccess: true }).catch(() => {}); } if (userId) { const profiles = await payload.find({ collection: "profiles", where: { user: { equals: userId } }, depth: 0, overrideAccess: true, }); for (const profile of profiles.docs) { await payload.delete({ collection: "profiles", id: profile.id, overrideAccess: true }).catch(() => {}); } await payload.delete({ collection: "users", id: userId, overrideAccess: true }).catch(() => {}); } }); describe("parseMoneyDelta", () => { it("parses [steamId, delta, reason, detail] and rounds the delta", () => { expect(parseMoneyDelta(["7656119", -250.4, "Purchase", "arifle_MX_F x1"])).toEqual({ steamId: "7656119", delta: -250, reason: "purchase", detail: "arifle_MX_F x1", }); }); it("rejects malformed or zero payloads", () => { expect(parseMoneyDelta(null)).toBeNull(); expect(parseMoneyDelta(["7656119", 100])).toBeNull(); expect(parseMoneyDelta(["", 100, "reward"])).toBeNull(); expect(parseMoneyDelta(["7656119", 0, "reward"])).toBeNull(); expect(parseMoneyDelta(["7656119", "100", "reward"])).toBeNull(); expect(parseMoneyDelta(["7656119", 100, ""])).toBeNull(); }); }); it("creates reward money in the personal account and reports it by Steam ID", async () => { await processMoneyEvent(payload, moneyEvent(500, "reward", "Dialogue test")); const account = await ensurePersonalAccount(payload, userId); accountId = account.id; expect(await balanceOf(accountId)).toBe(500); const balances = await getBalancesBySteamId(payload, [STEAM_ID, UNLINKED]); expect(balances[STEAM_ID]).toBe(500); expect(balances).not.toHaveProperty(UNLINKED); }); it("pays purchases to the treasury and refunds them from it", async () => { const treasuryBefore = await balanceOf(treasuryId); await processMoneyEvent(payload, moneyEvent(-200, "purchase", "arifle_MX_F x1")); expect(await balanceOf(accountId)).toBe(300); expect(await balanceOf(treasuryId)).toBe(treasuryBefore + 200); await processMoneyEvent(payload, moneyEvent(200, "refund", "arifle_MX_F x1")); expect(await balanceOf(accountId)).toBe(500); expect(await balanceOf(treasuryId)).toBe(treasuryBefore); }); it("does not overdraw: a debit larger than the balance is skipped without throwing", async () => { await expect(processMoneyEvent(payload, moneyEvent(-100000, "purchase"))).resolves.toBeUndefined(); expect(await balanceOf(accountId)).toBe(500); }); it("skips unlinked players and malformed events without throwing", async () => { await expect( processMoneyEvent(payload, { eventId: `${RUN}:unlinked`, payload: [UNLINKED, 100, "reward"] }), ).resolves.toBeUndefined(); await expect(processMoneyEvent(payload, { eventId: `${RUN}:bad`, payload: "nope" })).resolves.toBeUndefined(); expect(await balanceOf(accountId)).toBe(500); }); });