import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { expireOperationReservations, failOperationReservations, reserveOperationAssets, settleOperationReservation, type SettlementRecord, } from "@/lib/operations/allocation"; import { applyTransaction, createAccount, getMainCurrencyId } from "@/lib/banking"; import type { BankAccount, GameStructure, OperationReservation, Structure } from "@/payload-types"; let payload: Payload; const RUN = `alloc-${Date.now().toString(36)}`; const LEXICAL_EMPTY = { root: { children: [{ text: "" }], direction: null, format: "" as const, indent: 0, type: "text", version: 1, }, }; // --------------------------------------------------------------------------- // Fixture tracking for cleanup // --------------------------------------------------------------------------- const reservationIds: number[] = []; const syncEventIds: number[] = []; const opEventIds: number[] = []; const opEffectIds: number[] = []; const userIds: number[] = []; const roleIds: number[] = []; const accountIds: number[] = []; const gameStructureIds: number[] = []; const blueprintIds: number[] = []; const gameVehicleIds: number[] = []; const vehicleBlueprintIds: number[] = []; const resourceIds: number[] = []; const missionIds: number[] = []; const campaignIds: number[] = []; const mapIds: number[] = []; let serverId: number; let commandUserId: number; let plainUserId: number; let soldierOneId: number; let soldierTwoId: number; let mapId: number; let campaignId: number; let missionId: number; let resourceAId: number; let resourceBId: number; let normalBlueprintId: number; let restrictedBlueprintId: number; let vehicleBlueprintId: number; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function relId(value: unknown): number { return typeof value === "object" && value !== null ? (value as { id: number }).id : (value as number); } function settlementOf(doc: OperationReservation): SettlementRecord { return doc.settlement as unknown as SettlementRecord; } async function makeUser(label: string, roleDocIds?: number[]): Promise { const user = await payload.create({ collection: "users", data: { username: `${RUN}-${label}`, discordUsername: `${RUN}-${label}`, displayName: `${RUN} ${label}`, steamId: `7656119${Math.floor(Math.random() * 1e9)}`, password: "Test1234", roles: ["user"], ...(roleDocIds && roleDocIds.length > 0 ? { roleDocs: roleDocIds } : {}), }, overrideAccess: true, depth: 0, }); userIds.push(user.id); return user.id; } async function makeMission(label: string): Promise { const mission = await payload.create({ collection: "missions", data: { name: `${RUN} ${label}`, codeName: `${RUN}-${label}`, summary: "Allocation test mission", operationType: "main", classification: { map: mapId, missionType: "PvE", campaign: campaignId, startDateTime: new Date(Date.now() + 86_400_000).toISOString(), estimatedDuration: 60, }, ownershipAndStatus: { authors: [commandUserId], status: "Scheduled", visibility: "unit", }, missionRoles: { maxPlayers: 16 }, gameDetails: { serverDetails: { serverIp: "127.0.0.1", serverPort: 2302 } }, briefing: [], }, overrideAccess: true, depth: 0, }); missionIds.push(mission.id); return mission.id; } async function makeOrigin(stock: { resourceId: number; amount: number }[]): Promise { const site = await payload.create({ collection: "game-structures", data: { name: `${RUN} origin ${gameStructureIds.length}`, type: normalBlueprintId, map: mapId, coordinates: [100 + gameStructureIds.length, 100], constructionStatus: "complete", storedResources: stock.map((s, i) => ({ resource: s.resourceId, amount: s.amount, gridX: 0, gridY: i, rotated: false, })), }, overrideAccess: true, depth: 0, }); gameStructureIds.push(site.id); return site.id; } async function makeDestination(restricted = false): Promise { const site = await payload.create({ collection: "game-structures", data: { name: `${RUN} dest ${gameStructureIds.length}`, type: restricted ? restrictedBlueprintId : normalBlueprintId, map: mapId, coordinates: [500 + gameStructureIds.length, 500], constructionStatus: "complete", }, overrideAccess: true, depth: 0, }); gameStructureIds.push(site.id); return site.id; } async function makeVehicle(atId: number, status: "idle" | "damaged" = "idle"): Promise { const vehicle = await payload.create({ collection: "game-vehicles", data: { name: `${RUN} vic ${gameVehicleIds.length}`, type: vehicleBlueprintId, deployedAt: atId, status, currentFuel: 1000, currentHealth: 100, }, overrideAccess: true, depth: 0, }); gameVehicleIds.push(vehicle.id); return vehicle.id; } async function makeFundedAccount(amount: number): Promise { const account = await createAccount(payload, { name: `${RUN} Treasury ${accountIds.length}`, accountType: "treasury", }); accountIds.push(account.id); if (amount > 0) { await applyTransaction(payload, { type: "deposit", toAccountId: account.id, amount, memo: `${RUN} test funding`, }); } return account; } async function findByKey(key: string): Promise { const res = await payload.find({ collection: "operation-reservations", where: { reservationKey: { equals: key } }, limit: 1, depth: 0, overrideAccess: true, }); return (res.docs[0] as OperationReservation | undefined) ?? null; } async function storedAmountAt(siteId: number, resourceId: number): Promise { const doc = (await payload.findByID({ collection: "game-structures", id: siteId, depth: 0, overrideAccess: true, })) as unknown as GameStructure; return (doc.storedResources ?? []) .filter((r) => relId(r.resource) === resourceId) .reduce((sum, r) => sum + r.amount, 0); } // --------------------------------------------------------------------------- // Setup / teardown // --------------------------------------------------------------------------- beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); const currencyId = await getMainCurrencyId(payload); if (!currencyId) throw new Error("Test requires a main currency in Game Rules."); const superRole = await payload.create({ collection: "roles", data: { name: `${RUN} Command`, slug: `${RUN}-command`, isSuperuser: true }, overrideAccess: true, depth: 0, }); roleIds.push(superRole.id); commandUserId = await makeUser("command", [superRole.id]); plainUserId = await makeUser("plain"); soldierOneId = await makeUser("soldier1"); soldierTwoId = await makeUser("soldier2"); const map = await payload.create({ collection: "maps", data: { name: `${RUN} Map`, worldSizeWidth: 8192, worldSizeHeight: 8192, basemapMode: "image" }, overrideAccess: true, depth: 0, }); mapId = map.id; mapIds.push(map.id); const campaign = await payload.create({ collection: "campaigns", data: { name: `${RUN} Campaign`, summary: "Allocation test campaign", status: "concept", campaignMode: "custom", }, overrideAccess: true, depth: 0, }); campaignId = campaign.id; campaignIds.push(campaign.id); missionId = await makeMission("Main"); const server = await payload.create({ collection: "game-servers", data: { serverId: `${RUN}-srv`, name: `${RUN} Server`, status: "online" }, overrideAccess: true, depth: 0, }); serverId = server.id; for (const label of ["alpha", "bravo"] as const) { const resource = await payload.create({ collection: "resources", data: { name: `${RUN} ${label}`, codeName: `${RUN}-${label}`, approvalStatus: "in_progress", type: "physical", baseValue: 1, rarity: "common", unitOfMeasure: "kg", massPerUnit: 1, gridWidth: 1, gridHeight: 1, }, overrideAccess: true, depth: 0, }); resourceIds.push(resource.id); if (label === "alpha") resourceAId = resource.id; else resourceBId = resource.id; } const normalBlueprint = await payload.create({ collection: "structures", data: { name: `${RUN} Depot`, codeName: `${RUN}-depot`, approvalStatus: "in_progress", description: LEXICAL_EMPTY as unknown as Structure["description"], category: "logistics", materials: [{ resource: resourceAId, amount: 1 }], constructionDurationMinutes: 1, terrainType: "land", maxHealth: 100, }, overrideAccess: true, depth: 0, }); normalBlueprintId = normalBlueprint.id; blueprintIds.push(normalBlueprint.id); const restrictedBlueprint = await payload.create({ collection: "structures", data: { name: `${RUN} Vault`, codeName: `${RUN}-vault`, approvalStatus: "in_progress", description: LEXICAL_EMPTY as unknown as Structure["description"], category: "logistics", materials: [{ resource: resourceAId, amount: 1 }], constructionDurationMinutes: 1, terrainType: "land", maxHealth: 100, restrictToAllowed: true, allowedStorage: [], }, overrideAccess: true, depth: 0, }); restrictedBlueprintId = restrictedBlueprint.id; blueprintIds.push(restrictedBlueprint.id); const vehicleBlueprint = await payload.create({ collection: "vehicles", data: { name: `${RUN} Truck`, approvalStatus: "in_progress", transportMode: "ground", maxSpeedOnRoad: 60, fuel: { fuelType: resourceAId, fuelCapacity: 1000, fuelConsumptionRate: 0.01 }, }, overrideAccess: true, depth: 0, }); vehicleBlueprintId = vehicleBlueprint.id; vehicleBlueprintIds.push(vehicleBlueprint.id); }); afterAll(async () => { if (!payload) return; for (const id of reservationIds) { await payload.delete({ collection: "operation-reservations", id, overrideAccess: true }).catch(() => {}); } for (const id of opEffectIds) { await payload.delete({ collection: "operation-effects", id, overrideAccess: true }).catch(() => {}); } for (const id of opEventIds) { await payload.delete({ collection: "operation-events", id, overrideAccess: true }).catch(() => {}); } for (const id of syncEventIds) { await payload.delete({ collection: "arma-sync-events", id, overrideAccess: true }).catch(() => {}); } for (const id of gameVehicleIds) { await payload.delete({ collection: "game-vehicles", id, overrideAccess: true }).catch(() => {}); } for (const id of gameStructureIds) { await payload.delete({ collection: "game-structures", id, overrideAccess: true }).catch(() => {}); } for (const id of vehicleBlueprintIds) { await payload.delete({ collection: "vehicles", id, overrideAccess: true }).catch(() => {}); } for (const id of blueprintIds) { await payload.delete({ collection: "structures", id, overrideAccess: true }).catch(() => {}); } for (const id of resourceIds) { await payload.delete({ collection: "resources", id, overrideAccess: true }).catch(() => {}); } for (const id of missionIds) { await payload.delete({ collection: "missions", id, overrideAccess: true }).catch(() => {}); } for (const id of campaignIds) { await payload.delete({ collection: "campaigns", id, overrideAccess: true }).catch(() => {}); } for (const id of mapIds) { await payload.delete({ collection: "maps", id, overrideAccess: true }).catch(() => {}); } const deleteAccountChain = async (accountId: number) => { const txns = await payload.find({ collection: "bank-transactions", where: { or: [{ fromAccount: { equals: accountId } }, { toAccount: { equals: accountId } }] }, limit: 100, depth: 0, overrideAccess: true, }); for (const txn of txns.docs) { const entries = await payload.find({ collection: "ledger-entries", where: { transaction: { equals: txn.id } }, limit: 100, depth: 0, overrideAccess: true, }); for (const entry of entries.docs) { await payload.delete({ collection: "ledger-entries", id: entry.id, overrideAccess: true }).catch(() => {}); } await payload.delete({ collection: "bank-transactions", id: txn.id, overrideAccess: true }).catch(() => {}); } await payload.delete({ collection: "bank-accounts", id: accountId, overrideAccess: true }).catch(() => {}); }; for (const id of accountIds) { await deleteAccountChain(id); } for (const userId of userIds) { const profiles = await payload.find({ collection: "profiles", where: { user: { equals: userId } }, limit: 5, depth: 0, overrideAccess: true, }); for (const profile of profiles.docs) { await payload.delete({ collection: "profiles", id: profile.id, overrideAccess: true }).catch(() => {}); } const accounts = await payload.find({ collection: "bank-accounts", where: { ownerUser: { equals: userId } }, limit: 5, depth: 0, overrideAccess: true, }); for (const account of accounts.docs) { await deleteAccountChain(account.id); } await payload.delete({ collection: "users", id: userId, overrideAccess: true }).catch(() => {}); } for (const id of roleIds) { await payload.delete({ collection: "roles", id, overrideAccess: true }).catch(() => {}); } if (serverId) { await payload.delete({ collection: "game-servers", id: serverId, overrideAccess: true }).catch(() => {}); } }); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("Operation allocation", () => { describe("reserveOperationAssets", () => { it("reserves personnel, vehicles, cargo, and budget in one record", async () => { const originId = await makeOrigin([{ resourceId: resourceAId, amount: 100 }]); const destinationId = await makeDestination(); const vehicleId = await makeVehicle(originId); const account = await makeFundedAccount(1000); const key = `${RUN}-happy`; const doc = await reserveOperationAssets(payload, { reservationKey: key, operationId: `${RUN}-op-happy`, actorId: commandUserId, missionId, personnel: [{ userId: soldierOneId, slot: "Team Lead" }], vehicleIds: [vehicleId], cargo: [{ resourceId: resourceAId, amount: 40 }], budget: { accountId: account.id, amount: 200 }, originId, destinationId, expiresAt: new Date(Date.now() + 3_600_000).toISOString(), }); reservationIds.push(doc.id); expect(doc.reservationKey).toBe(key); expect(doc.status).toBe("reserved"); expect(doc.operationId).toBe(`${RUN}-op-happy`); expect(relId(doc.mission)).toBe(missionId); expect(doc.personnel).toHaveLength(1); expect(relId(doc.personnel?.[0]?.user)).toBe(soldierOneId); expect(doc.personnel?.[0]?.slot).toBe("Team Lead"); expect(doc.vehicles).toHaveLength(1); expect(relId(doc.vehicles?.[0]?.vehicle)).toBe(vehicleId); expect(doc.cargo).toHaveLength(1); expect(relId(doc.cargo?.[0]?.resource)).toBe(resourceAId); expect(doc.cargo?.[0]?.amount).toBe(40); expect(relId(doc.budget?.account)).toBe(account.id); expect(doc.budget?.amount).toBe(200); expect(relId(doc.origin)).toBe(originId); expect(relId(doc.destination)).toBe(destinationId); expect(relId(doc.createdBy)).toBe(commandUserId); }); it("is idempotent for a retry with the same reservation key", async () => { const originId = await makeOrigin([{ resourceId: resourceAId, amount: 50 }]); const key = `${RUN}-retry`; const input = { reservationKey: key, operationId: `${RUN}-op-retry`, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 10 }], originId, }; const first = await reserveOperationAssets(payload, input); const second = await reserveOperationAssets(payload, input); reservationIds.push(first.id); expect(second.id).toBe(first.id); const all = await payload.find({ collection: "operation-reservations", where: { reservationKey: { equals: key } }, limit: 10, depth: 0, overrideAccess: true, }); expect(all.docs).toHaveLength(1); }); it("rejects an unauthorized actor without writing anything", async () => { const originId = await makeOrigin([{ resourceId: resourceAId, amount: 100 }]); const key = `${RUN}-unauthorized`; await expect( reserveOperationAssets(payload, { reservationKey: key, operationId: `${RUN}-op-unauthorized`, actorId: plainUserId, cargo: [{ resourceId: resourceAId, amount: 10 }], originId, }), ).rejects.toThrow(/not authorized/i); expect(await findByKey(key)).toBeNull(); }); it("fails insufficient cargo without partial writes", async () => { const originId = await makeOrigin([{ resourceId: resourceAId, amount: 100 }]); const key = `${RUN}-insufficient`; await expect( reserveOperationAssets(payload, { reservationKey: key, operationId: `${RUN}-op-insufficient`, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 101 }], originId, }), ).rejects.toThrow(/insufficient cargo/i); expect(await findByKey(key)).toBeNull(); expect(await storedAmountAt(originId, resourceAId)).toBe(100); }); it("requires an origin structure when cargo is reserved", async () => { const key = `${RUN}-no-origin`; await expect( reserveOperationAssets(payload, { reservationKey: key, operationId: `${RUN}-op-no-origin`, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 1 }], }), ).rejects.toThrow(/origin/i); expect(await findByKey(key)).toBeNull(); }); it("accounts for cargo already reserved by other active reservations", async () => { const originId = await makeOrigin([{ resourceId: resourceAId, amount: 100 }]); const first = await reserveOperationAssets(payload, { reservationKey: `${RUN}-spend-1`, operationId: `${RUN}-op-spend`, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 60 }], originId, }); reservationIds.push(first.id); await expect( reserveOperationAssets(payload, { reservationKey: `${RUN}-spend-2`, operationId: `${RUN}-op-spend`, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 50 }], originId, }), ).rejects.toThrow(/insufficient cargo/i); expect(await findByKey(`${RUN}-spend-2`)).toBeNull(); const third = await reserveOperationAssets(payload, { reservationKey: `${RUN}-spend-3`, operationId: `${RUN}-op-spend`, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 40 }], originId, }); reservationIds.push(third.id); expect(third.status).toBe("reserved"); }); it("enforces destination storage rules at reserve time", async () => { const originId = await makeOrigin([{ resourceId: resourceAId, amount: 100 }]); const restrictedDestinationId = await makeDestination(true); const key = `${RUN}-dest-rules`; await expect( reserveOperationAssets(payload, { reservationKey: key, operationId: `${RUN}-op-dest-rules`, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 10 }], originId, destinationId: restrictedDestinationId, }), ).rejects.toThrow(/allowed storage list/i); expect(await findByKey(key)).toBeNull(); }); it("rejects unavailable vehicles", async () => { const originId = await makeOrigin([]); const otherOriginId = await makeOrigin([]); const reservedVehicleId = await makeVehicle(originId); const damagedVehicleId = await makeVehicle(originId, "damaged"); const elsewhereVehicleId = await makeVehicle(otherOriginId); const first = await reserveOperationAssets(payload, { reservationKey: `${RUN}-vic-1`, operationId: `${RUN}-op-vic`, actorId: commandUserId, vehicleIds: [reservedVehicleId], originId, }); reservationIds.push(first.id); await expect( reserveOperationAssets(payload, { reservationKey: `${RUN}-vic-2`, operationId: `${RUN}-op-vic`, actorId: commandUserId, vehicleIds: [reservedVehicleId], originId, }), ).rejects.toThrow(/already reserved/i); await expect( reserveOperationAssets(payload, { reservationKey: `${RUN}-vic-3`, operationId: `${RUN}-op-vic`, actorId: commandUserId, vehicleIds: [damagedVehicleId], originId, }), ).rejects.toThrow(/not available/i); await expect( reserveOperationAssets(payload, { reservationKey: `${RUN}-vic-4`, operationId: `${RUN}-op-vic`, actorId: commandUserId, vehicleIds: [elsewhereVehicleId], originId, }), ).rejects.toThrow(/not stationed/i); expect(await findByKey(`${RUN}-vic-2`)).toBeNull(); expect(await findByKey(`${RUN}-vic-3`)).toBeNull(); expect(await findByKey(`${RUN}-vic-4`)).toBeNull(); }); it("rejects an overdrawn treasury budget, including other reservations' earmarks", async () => { const account = await makeFundedAccount(500); await expect( reserveOperationAssets(payload, { reservationKey: `${RUN}-budget-1`, operationId: `${RUN}-op-budget`, actorId: commandUserId, budget: { accountId: account.id, amount: 600 }, }), ).rejects.toThrow(/insufficient budget/i); const ok = await reserveOperationAssets(payload, { reservationKey: `${RUN}-budget-2`, operationId: `${RUN}-op-budget`, actorId: commandUserId, budget: { accountId: account.id, amount: 500 }, }); reservationIds.push(ok.id); await expect( reserveOperationAssets(payload, { reservationKey: `${RUN}-budget-3`, operationId: `${RUN}-op-budget`, actorId: commandUserId, budget: { accountId: account.id, amount: 1 }, }), ).rejects.toThrow(/insufficient budget/i); expect(await findByKey(`${RUN}-budget-1`)).toBeNull(); expect(await findByKey(`${RUN}-budget-3`)).toBeNull(); const fresh = (await payload.findByID({ collection: "bank-accounts", id: account.id, depth: 0, overrideAccess: true, })) as unknown as BankAccount; expect(fresh.balance).toBe(500); }); it("rejects personnel already booked on the same mission", async () => { const personnelMissionId = await makeMission("Personnel"); const first = await reserveOperationAssets(payload, { reservationKey: `${RUN}-pers-1`, operationId: `${RUN}-op-pers`, actorId: commandUserId, missionId: personnelMissionId, personnel: [{ userId: soldierTwoId }], }); reservationIds.push(first.id); await expect( reserveOperationAssets(payload, { reservationKey: `${RUN}-pers-2`, operationId: `${RUN}-op-pers`, actorId: commandUserId, missionId: personnelMissionId, personnel: [{ userId: soldierTwoId }], }), ).rejects.toThrow(/already reserved/i); const otherMission = await reserveOperationAssets(payload, { reservationKey: `${RUN}-pers-3`, operationId: `${RUN}-op-pers`, actorId: commandUserId, missionId, personnel: [{ userId: soldierTwoId }], }); reservationIds.push(otherMission.id); expect(otherMission.status).toBe("reserved"); }); }); describe("settleOperationReservation", () => { it("cancels: returns everything, exactly once", async () => { const originId = await makeOrigin([{ resourceId: resourceAId, amount: 100 }]); const vehicleId = await makeVehicle(originId); const account = await makeFundedAccount(300); const doc = await reserveOperationAssets(payload, { reservationKey: `${RUN}-cancel`, operationId: `${RUN}-op-cancel`, actorId: commandUserId, personnel: [{ userId: soldierOneId }], vehicleIds: [vehicleId], cargo: [{ resourceId: resourceAId, amount: 25 }], budget: { accountId: account.id, amount: 120 }, originId, }); reservationIds.push(doc.id); const settled = await settleOperationReservation( payload, { reservationId: doc.id }, { outcome: "cancelled", actorId: commandUserId }, ); expect(settled.status).toBe("cancelled"); expect(settled.settledAt).toBeTruthy(); const record = settlementOf(settled); expect(record.outcome).toBe("cancelled"); expect(record.reason).toBe("cancelled"); expect(record.consumed).toEqual({ personnel: [], vehicles: [], cargo: [], budget: 0 }); expect(record.returned.personnel).toEqual([soldierOneId]); expect(record.returned.vehicles).toEqual([vehicleId]); expect(record.returned.cargo).toEqual([{ resource: resourceAId, amount: 25 }]); expect(record.returned.budget).toBe(120); // Second settle is a no-op: same status, same settlement timestamp. const again = await settleOperationReservation( payload, { reservationId: doc.id }, { outcome: "cancelled", actorId: commandUserId }, ); expect(again.status).toBe("cancelled"); expect(again.settledAt).toBe(settled.settledAt); // A different outcome cannot re-settle either. const third = await settleOperationReservation( payload, { reservationId: doc.id }, { outcome: "settled", consumed: { budgetAmount: 120 }, actorId: commandUserId }, ); expect(third.status).toBe("cancelled"); expect(third.settledAt).toBe(settled.settledAt); }); it("settles a partial success with exact consumed/returned math, exactly once", async () => { const originId = await makeOrigin([ { resourceId: resourceAId, amount: 100 }, { resourceId: resourceBId, amount: 10 }, ]); const vehicleId = await makeVehicle(originId); const account = await makeFundedAccount(400); const doc = await reserveOperationAssets(payload, { reservationKey: `${RUN}-partial`, operationId: `${RUN}-op-partial`, actorId: commandUserId, personnel: [{ userId: soldierOneId }, { userId: soldierTwoId }], vehicleIds: [vehicleId], cargo: [ { resourceId: resourceAId, amount: 50 }, { resourceId: resourceBId, amount: 10 }, ], budget: { accountId: account.id, amount: 300 }, originId, }); reservationIds.push(doc.id); // Over-consumption is rejected before any write. await expect( settleOperationReservation( payload, { reservationId: doc.id }, { outcome: "settled", consumed: { cargo: [{ resourceId: resourceAId, amount: 51 }] } }, ), ).rejects.toThrow(/exceeds the reserved amount/i); expect((await findByKey(`${RUN}-partial`))?.status).toBe("reserved"); const settled = await settleOperationReservation( payload, { reservationId: doc.id }, { outcome: "settled", reason: "partial-success", consumed: { personnel: [soldierOneId], vehicleIds: [vehicleId], cargo: [{ resourceId: resourceAId, amount: 20 }], budgetAmount: 100, }, actorId: commandUserId, }, ); expect(settled.status).toBe("settled"); const record = settlementOf(settled); expect(record.outcome).toBe("settled"); expect(record.reason).toBe("partial-success"); expect(record.consumed.personnel).toEqual([soldierOneId]); expect(record.consumed.vehicles).toEqual([vehicleId]); expect(record.consumed.cargo).toEqual([{ resource: resourceAId, amount: 20 }]); expect(record.consumed.budget).toBe(100); expect(record.returned.personnel).toEqual([soldierTwoId]); expect(record.returned.vehicles).toEqual([]); expect(record.returned.cargo).toEqual([ { resource: resourceAId, amount: 30 }, { resource: resourceBId, amount: 10 }, ]); expect(record.returned.budget).toBe(200); const again = await settleOperationReservation( payload, { reservationId: doc.id }, { outcome: "settled", reason: "partial-success", consumed: { budgetAmount: 100 } }, ); expect(again.settledAt).toBe(settled.settledAt); expect(settlementOf(again).consumed.budget).toBe(100); }); it("records a no-show as a distinct, auditable outcome", async () => { const originId = await makeOrigin([{ resourceId: resourceAId, amount: 50 }]); const doc = await reserveOperationAssets(payload, { reservationKey: `${RUN}-noshow`, operationId: `${RUN}-op-noshow`, actorId: commandUserId, personnel: [{ userId: soldierOneId }], cargo: [{ resourceId: resourceAId, amount: 10 }], originId, }); reservationIds.push(doc.id); const settled = await settleOperationReservation( payload, { reservationKey: `${RUN}-noshow` }, { outcome: "no-show", actorId: commandUserId }, ); expect(settled.status).toBe("no-show"); const record = settlementOf(settled); expect(record.outcome).toBe("no-show"); expect(record.reason).toBe("no-show"); expect(record.consumed).toEqual({ personnel: [], vehicles: [], cargo: [], budget: 0 }); expect(record.returned.personnel).toEqual([soldierOneId]); expect(record.returned.cargo).toEqual([{ resource: resourceAId, amount: 10 }]); }); }); describe("expireOperationReservations", () => { it("settles overdue reservations as expired and leaves future ones reserved", async () => { const originId = await makeOrigin([{ resourceId: resourceAId, amount: 100 }]); const overdue = await reserveOperationAssets(payload, { reservationKey: `${RUN}-exp-overdue`, operationId: `${RUN}-op-exp`, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 5 }], originId, expiresAt: new Date(Date.now() - 60_000).toISOString(), }); reservationIds.push(overdue.id); const future = await reserveOperationAssets(payload, { reservationKey: `${RUN}-exp-future`, operationId: `${RUN}-op-exp`, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 5 }], originId, expiresAt: new Date(Date.now() + 3_600_000).toISOString(), }); reservationIds.push(future.id); const settledCount = await expireOperationReservations(payload); expect(settledCount).toBeGreaterThanOrEqual(1); const overdueDoc = await findByKey(`${RUN}-exp-overdue`); expect(overdueDoc?.status).toBe("expired"); expect(settlementOf(overdueDoc as OperationReservation).returned.cargo).toEqual([ { resource: resourceAId, amount: 5 }, ]); const futureDoc = await findByKey(`${RUN}-exp-future`); expect(futureDoc?.status).toBe("reserved"); // Sweeping again is a no-op for the already-expired row. const secondCount = await expireOperationReservations(payload); const overdueAgain = await findByKey(`${RUN}-exp-overdue`); expect(overdueAgain?.settledAt).toBe(overdueDoc?.settledAt); expect(secondCount).toBe(0); }); }); describe("failOperationReservations", () => { it("settles only unextracted reservations and never reverses a recorded extraction", async () => { const operationId = `${RUN}-op-fail`; // Validated extraction evidence through the real hook path. const syncEvent = await payload.create({ collection: "arma-sync-events", data: { eventId: `${RUN}-extract-1`, server: serverId, type: "operation.extraction", occurredAt: new Date().toISOString(), payload: { operationId, version: 1, player: `${RUN} soldier1` }, }, overrideAccess: true, depth: 0, }); syncEventIds.push(syncEvent.id); const opEvents = await payload.find({ collection: "operation-events", where: { sourceEvent: { equals: syncEvent.id } }, limit: 1, depth: 0, overrideAccess: true, }); expect(opEvents.docs).toHaveLength(1); opEventIds.push(opEvents.docs[0].id); const effects = await payload.find({ collection: "operation-effects", where: { operationEvent: { equals: opEvents.docs[0].id } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effects.docs).toHaveLength(1); const extractionEffect = effects.docs[0]; expect(extractionEffect.effectType).toBe("operation.extraction-recorded"); expect(extractionEffect.status).toBe("applied"); opEffectIds.push(extractionEffect.id); // One reservation already settled by the extraction (partial success); // one still reserved (unextracted). const originId = await makeOrigin([{ resourceId: resourceAId, amount: 100 }]); const extracted = await reserveOperationAssets(payload, { reservationKey: `${RUN}-fail-extracted`, operationId, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 10 }], originId, }); reservationIds.push(extracted.id); const preSettled = await settleOperationReservation( payload, { reservationId: extracted.id }, { outcome: "settled", reason: "partial-success", consumed: { cargo: [{ resourceId: resourceAId, amount: 4 }] }, effectId: extractionEffect.id, actorId: commandUserId, }, ); expect(preSettled.status).toBe("settled"); const pending = await reserveOperationAssets(payload, { reservationKey: `${RUN}-fail-pending`, operationId, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 10 }], originId, }); reservationIds.push(pending.id); const settledCount = await failOperationReservations(payload, operationId, { effectId: extractionEffect.id, actorId: commandUserId, }); expect(settledCount).toBe(1); // The still-reserved row settles as mission-failed with everything returned. const failedDoc = await findByKey(`${RUN}-fail-pending`); expect(failedDoc?.status).toBe("settled"); const failedRecord = settlementOf(failedDoc as OperationReservation); expect(failedRecord.reason).toBe("mission-failed"); expect(failedRecord.consumed).toEqual({ personnel: [], vehicles: [], cargo: [], budget: 0 }); expect(failedRecord.returned.cargo).toEqual([{ resource: resourceAId, amount: 10 }]); expect(failedRecord.effectId).toBe(extractionEffect.id); expect(relId(failedDoc?.settledByEffect)).toBe(extractionEffect.id); // The previously settled (extracted) row is untouched. const extractedDoc = await findByKey(`${RUN}-fail-extracted`); expect(extractedDoc?.status).toBe("settled"); expect(extractedDoc?.settledAt).toBe(preSettled.settledAt); const extractedRecord = settlementOf(extractedDoc as OperationReservation); expect(extractedRecord.reason).toBe("partial-success"); expect(extractedRecord.consumed.cargo).toEqual([{ resource: resourceAId, amount: 4 }]); // The extraction effect is final: still applied, never reversed, and // failure settlement wrote no new effects for the operation. const freshEffect = await payload.findByID({ collection: "operation-effects", id: extractionEffect.id, depth: 0, overrideAccess: true, }); expect(freshEffect.status).toBe("applied"); expect(freshEffect.reversedBy).toBeNull(); const effectsAfter = await payload.find({ collection: "operation-effects", where: { operationId: { equals: operationId } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effectsAfter.docs).toHaveLength(1); }); }); });