1
0
Fork 0

feat(operations): bounded economy effects and market repair

Link operation loot to the supply/demand ledger with faucet/drain caps and provenance-marked notes; repair buyListing to preflight locker credit before payment with a compensating refund on credit failure.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-09-20 02:43:53 -04:00
parent 67c993ca10
commit 67adaa301c
4 changed files with 1082 additions and 1 deletions

View file

@ -16,6 +16,7 @@ import {
getMainCurrencyId, getMainCurrencyId,
getTreasuryAccountId, getTreasuryAccountId,
} from "@/lib/banking"; } from "@/lib/banking";
import { preflightLockerCredit } from "@/lib/operations/settlementFit";
export { getTreasuryAccountId } from "@/lib/banking"; export { getTreasuryAccountId } from "@/lib/banking";
@ -299,6 +300,21 @@ export async function buyListing(
if (treasuryId) sellerAccountId = treasuryId; if (treasuryId) sellerAccountId = treasuryId;
} }
// Preflight: verify the buyer's locker has room BEFORE moving money so a
// full locker can never leave the buyer paid with no items. This mirrors
// the real credit path in `creditLockerQuantity` (merge-then-place); the
// writer re-runs its own check, so a passing preflight cannot partially
// apply. Reuses the settlement-layer pure preflight so the two stay in sync.
const buyerLocker = await ensureLockerStorage(payload, buyer.id as number);
const buyerLockerFull = (await payload.findByID({
collection: "locker-storages",
id: buyerLocker.id,
depth: 1,
overrideAccess: true,
})) as unknown as LockerStorage;
const grid = await getLockerGridDimensions(payload);
preflightLockerCredit(buyerLockerFull, grid, [{ assetId: asset.id, quantity, asset }]);
const transaction = await applyTransaction(payload, { const transaction = await applyTransaction(payload, {
type: "payment", type: "payment",
fromAccountId: buyerAccount.id, fromAccountId: buyerAccount.id,
@ -308,7 +324,31 @@ export async function buyListing(
actorId: buyer.id as number, actorId: buyer.id as number,
}); });
await creditLockerQuantity(payload, buyer.id as number, asset.id, quantity); try {
await creditLockerQuantity(payload, buyer.id as number, asset.id, quantity);
} catch (error) {
// Compensating refund: the payment moved but the locker credit failed
// (e.g. a concurrent deposit filled the locker between the preflight and
// the credit). Reverse the payment so the buyer is never left paid with
// no items; a refund failure is logged and the original error rethrown.
try {
await applyTransaction(payload, {
type: "adjustment",
fromAccountId: sellerAccountId,
toAccountId: buyerAccount.id,
amount: total,
memo: `Refund: purchase TXN ${transaction.id} failed to credit locker`,
actorId: buyer.id as number,
});
} catch (refundError) {
payload.logger.error(
`[Market] CRITICAL: purchase ${transaction.id} credit failed AND refund failed; manual reconciliation required: ${
refundError instanceof Error ? refundError.message : "unknown"
}`,
);
}
throw error instanceof Error ? error : new Error("Locker credit failed after payment.");
}
const remaining = stock - quantity; const remaining = stock - quantity;
const updated = (await payload.update({ const updated = (await payload.update({

View file

@ -0,0 +1,203 @@
/**
* Operation economy-effect adapter - Payload interaction.
*
* Consumes APPLIED operation effects and adjusts the abstract supply/demand
* ledger (`market-state`) exactly once per effect. Mirrors the settlement
* dispatcher's fresh-re-read + terminal-guard shape so a replay applies
* nothing twice, and records the applied/rejected deltas on the effect row as
* reconciliation evidence.
*
* Only market-state docs that already exist are touched; items without a ledger
* are recorded as provenance-eligible only (never auto-marketable).
*/
import type { Payload } from "payload";
import type { OperationEffect } from "@/payload-types";
import { findByIdOrNull } from "./settlementResolve";
import {
clampEconomyCaps,
computeEconomyDelta,
DEFAULT_ECONOMY_CAPS,
EconomyEffectCaps,
EconomyPlan,
EconomyRecord,
hasEconomyProcessed,
appendEconomyNote,
MarketDeltaSummary,
ProvenanceItem,
ECONOMY_NOTE_PREFIX,
} from "./economyConfig";
type PayloadType = Awaited<ReturnType<typeof import("payload").getPayload>>;
interface MarketStateRow {
id: number;
currentSupply: number;
}
async function findMarketStateRow(
payload: PayloadType,
itemType: "resource" | "asset",
itemId: number,
): Promise<MarketStateRow | null> {
const docs = await payload.find({
collection: "market-state",
where: {
and: [{ itemType: { equals: itemType } }, { itemId: { equals: itemId } }],
},
limit: 1,
depth: 0,
overrideAccess: true,
});
const doc = docs.docs[0] as { id: number; currentSupply: number } | undefined;
if (!doc) return null;
return { id: doc.id, currentSupply: doc.currentSupply ?? 0 };
}
async function applyEconomyDelta(
payload: PayloadType,
plan: EconomyPlan,
caps: EconomyEffectCaps,
): Promise<{ applied: MarketDeltaSummary[]; rejected: MarketDeltaSummary[]; provenance: ProvenanceItem[] }> {
const applied: MarketDeltaSummary[] = [];
const rejected: MarketDeltaSummary[] = [];
const provenance: ProvenanceItem[] = [...plan.provenance];
for (const delta of plan.deltas) {
const row = await findMarketStateRow(payload, delta.itemType, delta.itemId);
if (!row) {
// No market-state ledger for this item: provenance-eligible only.
// Never create a doc or listing (extraction loot is not auto-marketable).
provenance.push({ itemType: delta.itemType, itemId: delta.itemId });
continue;
}
const magnitude = Math.abs(delta.supply);
const cap = delta.supply > 0 ? caps.faucetCapPerItem : caps.drainCapPerItem;
let appliedSupply = delta.supply;
let rejectedAmount = 0;
if (magnitude > cap) {
appliedSupply = delta.supply > 0 ? cap : -cap;
rejectedAmount = magnitude - cap;
}
await payload.update({
collection: "market-state",
id: row.id,
data: { currentSupply: Math.max(0, row.currentSupply + appliedSupply) },
depth: 0,
overrideAccess: true,
});
if (appliedSupply !== 0) applied.push({ ...delta, supply: appliedSupply });
if (rejectedAmount > 0) {
rejected.push({ itemType: delta.itemType, itemId: delta.itemId, supply: rejectedAmount });
}
}
return { applied, rejected, provenance };
}
/**
* Apply one effect's market impact. Never throws: a recoverable failure is
* recorded on the effect row as reconciliation evidence (the effect status is
* left for settlement) so the ledger write path stays unbreakable.
*/
export async function applyEconomyEffect(
payload: PayloadType,
effectId: number,
caps?: Partial<EconomyEffectCaps>,
): Promise<OperationEffect | null> {
const effect = (await findByIdOrNull(payload, "operation-effects", effectId)) as
| OperationEffect
| null;
if (!effect) return null;
// Only effects settlement has applied are eligible; pending/failed/reversed
// rows are never reprocessed.
if (effect.status !== "applied") return effect;
// Idempotency: the economy changes market state exactly once per effect.
if (hasEconomyProcessed(effect.reconciliationNotes)) return effect;
try {
const plan = computeEconomyDelta(effect);
// Nothing for the economy to touch: leave the effect for other consumers.
if (plan.deltas.length === 0 && plan.provenance.length === 0) return effect;
const resolvedCaps = clampEconomyCaps(caps);
const { applied, rejected, provenance } = await applyEconomyDelta(payload, plan, resolvedCaps);
return (await payload.update({
collection: "operation-effects",
id: effect.id,
data: {
reconciliationNotes: appendEconomyNote(effect.reconciliationNotes, {
operationId: effect.operationId,
effectKey: effect.effectKey,
effectType: effect.effectType,
applied,
rejected,
provenance,
}),
},
depth: 0,
overrideAccess: true,
})) as unknown as OperationEffect;
} catch (error) {
const message = error instanceof Error ? error.message : "unknown error";
payload.logger.error(`[Operations] Economy effect ${effect.effectKey} failed: ${message}`);
return (await payload.update({
collection: "operation-effects",
id: effect.id,
data: {
reconciliationNotes: appendEconomyNote(effect.reconciliationNotes, {
operationId: effect.operationId,
effectKey: effect.effectKey,
effectType: effect.effectType,
applied: [],
rejected: [],
provenance: [],
error: message,
}),
},
depth: 0,
overrideAccess: true,
})) as unknown as OperationEffect;
}
}
/**
* Apply the economy impact of every not-yet-processed applied effect for an
* operation. Returns the number of effects that had economy impact. Skips
* pending/failed/reversed rows and effects already marked by the economy.
*/
export async function applyEconomyEffectsForOperation(
payload: PayloadType,
operationId: string,
caps?: Partial<EconomyEffectCaps>,
): Promise<number> {
const resolvedCaps = clampEconomyCaps(caps);
const found = await payload.find({
collection: "operation-effects",
where: {
and: [{ operationId: { equals: operationId } }, { status: { equals: "applied" } }],
},
limit: 200,
depth: 0,
overrideAccess: true,
});
let processed = 0;
for (const doc of found.docs as OperationEffect[]) {
if (doc.reconciliationNotes?.includes(ECONOMY_NOTE_PREFIX)) continue;
const plan = computeEconomyDelta({ effectType: doc.effectType, payload: doc.payload });
if (plan.deltas.length === 0 && plan.provenance.length === 0) continue;
await applyEconomyEffect(payload, doc.id, resolvedCaps);
processed += 1;
}
return processed;
}
export { DEFAULT_ECONOMY_CAPS };

View file

@ -0,0 +1,171 @@
/**
* Operation economy-effect adapter - configuration and pure delta logic.
*
* Consumes APPLIED operation effects and adjusts the abstract supply/demand
* ledger (`market-state`) exactly once per effect. This is a PURE module
* (no Payload imports) so the delta rules and caps are unit-testable without
* a database and importable anywhere.
*
* Supply model: a logistics extraction deposits goods that flow into the
* market ledger as available supply (a faucet); death/loss removes goods from
* circulation (a drain). Player extraction credits the extractor's locker, not
* the market, so those goods are recorded as provenance-eligible only.
*
* Provenance guard: the adapter records provenance eligibility, it never flips
* an asset's `tradeable`/`isLive` flags and never creates listings, so
* extraction loot is never auto-marketable. Vendor `minPrice` floors are never
* surfaced in any output.
*/
import {
parseExtractionPayload,
parseInventoryLossPayload,
parseLogisticsLine,
parsePlayerLine,
} from "./settlementParse";
export type EconomyItemType = "resource" | "asset";
/** A net supply change to one item's market-state ledger. */
export interface MarketDelta {
itemType: EconomyItemType;
itemId: number;
/** Positive = faucet (supply in), negative = drain (supply out). */
supply: number;
}
/** An item that is provenance-eligible for the market but has no ledger write. */
export interface ProvenanceItem {
itemType: EconomyItemType;
itemId: number;
}
/** The market impact of one operation effect. */
export interface EconomyPlan {
deltas: MarketDelta[];
provenance: ProvenanceItem[];
}
/** A single applied or rejected supply movement, recorded as reconciliation evidence. */
export interface MarketDeltaSummary {
itemType: EconomyItemType;
itemId: number;
supply: number;
}
/** The economy record stored on the effect row after processing. */
export interface EconomyRecord {
operationId: string;
effectKey: string;
effectType: string;
applied: MarketDeltaSummary[];
rejected: MarketDeltaSummary[];
provenance: ProvenanceItem[];
/** Populated only when applying the delta failed; the effect status is left for settlement. */
error?: string;
}
/**
* Faucet/drain caps: the largest single effect may move one item's supply by.
* Excess is rejected (not applied) and recorded as reconciliation evidence so
* a runaway extraction or loss cannot flood or hollow out the market ledger.
*/
export interface EconomyEffectCaps {
faucetCapPerItem: number;
drainCapPerItem: number;
}
export const DEFAULT_ECONOMY_CAPS: EconomyEffectCaps = {
faucetCapPerItem: 1000,
drainCapPerItem: 1000,
};
/** Coerce partial/invalid/absent cap overrides into valid non-negative caps. */
export function clampEconomyCaps(caps?: Partial<EconomyEffectCaps>): EconomyEffectCaps {
const toCap = (value: number | null | undefined, fallback: number): number =>
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
return {
faucetCapPerItem: toCap(caps?.faucetCapPerItem, DEFAULT_ECONOMY_CAPS.faucetCapPerItem),
drainCapPerItem: toCap(caps?.drainCapPerItem, DEFAULT_ECONOMY_CAPS.drainCapPerItem),
};
}
/**
* Marker written onto an effect's `reconciliationNotes` once the economy has
* processed it. Preserves the settlement-layer note (written first) by
* appending after it; idempotency is detected by the marker's presence.
*/
export const ECONOMY_NOTE_PREFIX = "economy:";
export function hasEconomyProcessed(notes: string | null | undefined): boolean {
return typeof notes === "string" && notes.includes(ECONOMY_NOTE_PREFIX);
}
/** Append the economy record to any existing reconciliation note. */
export function appendEconomyNote(existing: string | null | undefined, record: EconomyRecord): string {
const prefix = typeof existing === "string" && existing ? `${existing}\n` : "";
return `${prefix}${ECONOMY_NOTE_PREFIX}${JSON.stringify(record)}`;
}
/**
* Compute one effect's market impact. Untrusted effect payloads are parsed
* exactly once here; anything the parsers reject yields an empty plan (the
* adapter records nothing and the effect carries no market delta).
*/
export function computeEconomyDelta(effect: {
effectType: string;
payload?: unknown;
}): EconomyPlan {
switch (effect.effectType) {
case "operation.extraction-recorded":
return extractionDelta(effect.payload);
case "operation.inventory-recorded":
return lossDelta(effect.payload);
default:
return { deltas: [], provenance: [] };
}
}
function extractionDelta(raw: unknown): EconomyPlan {
const parsed = parseExtractionPayload(raw);
if (!parsed.ok) return { deltas: [], provenance: [] };
const ext = parsed.value;
if (!ext.kind) return { deltas: [], provenance: [] };
if (ext.kind === "logistics") {
// Extracted goods flow into the market ledger as available supply.
const deltas: MarketDelta[] = [];
for (const rawLine of ext.items ?? []) {
const line = parseLogisticsLine(rawLine);
if (!line.ok) continue;
deltas.push({ itemType: "resource", itemId: line.value.resourceId, supply: line.value.amount });
}
return { deltas, provenance: [] };
}
// Player extraction credits the extractor's locker, not the market. The
// goods are provenance-eligible (the player may list them later) but are
// never auto-marketable here.
const provenance: ProvenanceItem[] = [];
for (const rawLine of ext.items ?? []) {
const line = parsePlayerLine(rawLine);
if (!line.ok) continue;
provenance.push({ itemType: "asset", itemId: line.value.assetId });
}
return { deltas: [], provenance };
}
function lossDelta(raw: unknown): EconomyPlan {
const parsed = parseInventoryLossPayload(raw);
if (!parsed.ok) return { deltas: [], provenance: [] };
const loss = parsed.value;
if (!loss.reason) return { deltas: [], provenance: [] };
// Lost goods leave circulation: a drain on the market ledger.
const deltas: MarketDelta[] = [];
for (const rawLine of loss.items ?? []) {
const line = parsePlayerLine(rawLine);
if (!line.ok) continue;
deltas.push({ itemType: "asset", itemId: line.value.assetId, supply: -line.value.quantity });
}
return { deltas, provenance: [] };
}

View file

@ -0,0 +1,667 @@
import { getPayload, Payload } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { applyEconomyEffect, DEFAULT_ECONOMY_CAPS } from "@/lib/operations/economy";
import { ECONOMY_NOTE_PREFIX, type EconomyRecord } from "@/lib/operations/economyConfig";
import { resetMarketStateDocs } from "@/lib/economy/consumption";
import { applyTransaction, ensurePersonalAccount } from "@/lib/banking";
import { ensureLockerStorage } from "@/lib/locker";
import type { Asset, GameStructure, LockerStorage, MarketListing, Structure } from "@/payload-types";
let payload: Payload;
const RUN = `econ-${Date.now().toString(36)}`;
const LEXICAL_EMPTY = {
root: {
children: [{ text: "" }],
direction: null,
format: "" as const,
indent: 0,
type: "text",
version: 1,
},
};
interface EconomyNote {
applied: { itemType: string; itemId: number; supply: number }[];
rejected: { itemType: string; itemId: number; supply: number }[];
provenance: { itemType: string; itemId: number }[];
error?: string;
}
function economyRecordFrom(effect: { reconciliationNotes: string | null }): EconomyRecord | null {
const notes = effect.reconciliationNotes ?? "";
const line = notes.split("\n").find((l) => l.startsWith(ECONOMY_NOTE_PREFIX));
if (!line) return null;
return JSON.parse(line.slice(ECONOMY_NOTE_PREFIX.length)) as EconomyRecord;
}
// ---------------------------------------------------------------------------
// Fixture tracking for cleanup
// ---------------------------------------------------------------------------
const syncEventIds: number[] = [];
const opEventIds: number[] = [];
const opEffectIds: number[] = [];
const userIds: number[] = [];
const accountIds: number[] = [];
const gameStructureIds: number[] = [];
const blueprintIds: number[] = [];
const resourceIds: number[] = [];
const assetIds: number[] = [];
const marketStateIds: number[] = [];
const listingIds: number[] = [];
const lockerIds: number[] = [];
let serverId: number;
let mapId: number;
let actorId: number;
let sellerId: number;
let buyerId: number;
let resourceAlphaId: number;
let resourceBetaId: number;
let tradeableAssetId: number;
let fillerAssetId: number;
let hqId: number;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async function supplyOf(itemType: "resource" | "asset", itemId: number): Promise<number | null> {
const docs = await payload.find({
collection: "market-state",
where: { and: [{ itemType: { equals: itemType } }, { itemId: { equals: itemId } }] },
limit: 1,
depth: 0,
overrideAccess: true,
});
if (docs.docs.length === 0) return null;
return (docs.docs[0] as { currentSupply: number }).currentSupply;
}
async function makeMarketState(itemType: "resource" | "asset", itemId: number): Promise<number> {
const existing = await payload.find({
collection: "market-state",
where: { and: [{ itemType: { equals: itemType } }, { itemId: { equals: itemId } }] },
limit: 1,
depth: 0,
overrideAccess: true,
});
if (existing.docs.length > 0) return existing.docs[0].id;
const doc = await payload.create({
collection: "market-state",
data: {
itemType,
itemId,
gmBaselineSupply: 100,
gmBaselineDemand: 50,
currentSupply: 100,
artificialDemand: 50,
realDemand: 0,
priceModifier: 1,
},
overrideAccess: true,
depth: 0,
});
marketStateIds.push(doc.id);
return doc.id;
}
/**
* Full hook chain: creating the raw sync event runs the afterChange hook,
* which validates it into an operation-event, derives the pending effect, and
* applies settlement. Returns the applied effect.
*/
async function hookExtraction(
label: string,
payloadManifest: Record<string, unknown>,
): Promise<{ effectId: number; eventId: string }> {
const sync = await payload.create({
collection: "arma-sync-events",
data: {
eventId: `${RUN}-${label}`,
server: serverId,
type: "operation.extraction",
occurredAt: new Date().toISOString(),
payload: { operationId: RUN, version: 1, ...payloadManifest },
},
overrideAccess: true,
depth: 0,
});
syncEventIds.push(sync.id);
const events = await payload.find({
collection: "operation-events",
where: { sourceEvent: { equals: sync.id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(events.docs).toHaveLength(1);
opEventIds.push(events.docs[0].id);
const effects = await payload.find({
collection: "operation-effects",
where: { operationEvent: { equals: events.docs[0].id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(effects.docs).toHaveLength(1);
const effect = effects.docs[0] as { id: number; status: string };
opEffectIds.push(effect.id);
expect(effect.status).toBe("applied");
return { effectId: effect.id, eventId: sync.eventId as string };
}
/** Create an applied extraction effect for adapter-level tests. The afterChange
* hook already consumes every raw sync event (unique sourceEvent), so instead
* of creating a second operation-event, the hook-derived effect row is
* repurposed with an extraction type and manifest. */
async function manualExtractionEffect(label: string, resourceId: number, amount: number): Promise<number> {
const sync = await payload.create({
collection: "arma-sync-events",
data: {
eventId: `${RUN}-${label}`,
server: serverId,
type: "operation.start",
occurredAt: new Date().toISOString(),
payload: { operationId: RUN, version: 1 },
},
overrideAccess: true,
depth: 0,
});
syncEventIds.push(sync.id);
const events = await payload.find({
collection: "operation-events",
where: { sourceEvent: { equals: sync.id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(events.docs).toHaveLength(1);
opEventIds.push(events.docs[0].id);
const effects = await payload.find({
collection: "operation-effects",
where: { operationEvent: { equals: events.docs[0].id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(effects.docs).toHaveLength(1);
const effect = effects.docs[0] as { id: number };
opEffectIds.push(effect.id);
const updated = await payload.update({
collection: "operation-effects",
id: effect.id,
data: {
effectType: "operation.extraction-recorded",
payload: {
operationId: RUN,
kind: "logistics",
hqStructureId: hqId,
actorId,
items: [{ resourceId, amount }],
},
status: "applied",
appliedAt: new Date().toISOString(),
},
overrideAccess: true,
depth: 0,
});
return updated.id;
}
// ---------------------------------------------------------------------------
// Setup / teardown
// ---------------------------------------------------------------------------
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
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 ["actor", "seller", "buyer"] as const) {
const user = await payload.create({
collection: "users",
data: {
username: `${RUN}-${label}`,
discordUsername: `${RUN}-${label}-discord`,
displayName: `${RUN} ${label}`,
steamId: `${RUN}-steam-${label}`,
password: "Test1234",
},
overrideAccess: true,
depth: 0,
});
userIds.push(user.id);
if (label === "actor") actorId = user.id;
if (label === "seller") sellerId = user.id;
if (label === "buyer") buyerId = user.id;
}
const map = await payload.create({
collection: "maps",
data: { name: `${RUN} Map`, worldSizeWidth: 8192, worldSizeHeight: 8192, basemapMode: "image" },
overrideAccess: true,
depth: 0,
});
mapId = map.id;
for (const [label, setter] of [
["alpha", (id: number) => (resourceAlphaId = id)],
["beta", (id: number) => (resourceBetaId = id)],
] 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);
setter(resource.id);
}
const tradeableAsset = await payload.create({
collection: "assets",
data: {
name: `${RUN} Tradeable Rifle`,
className: `${RUN}_tradeable_rifle`,
assetType: "weapon",
approvalStatus: "in_progress",
tradeable: true,
isLive: true,
crafting: { craftable: false, craftingData: { craftingTimePerUnit: 1, batchSize: 1 } },
storageDimensions: { gridWidth: 1, gridHeight: 1, stackable: false },
},
overrideAccess: true,
depth: 0,
});
assetIds.push(tradeableAsset.id);
tradeableAssetId = tradeableAsset.id;
const fillerAsset = await payload.create({
collection: "assets",
data: {
name: `${RUN} Filler Rifle`,
className: `${RUN}_filler_rifle`,
assetType: "weapon",
approvalStatus: "in_progress",
crafting: { craftable: false, craftingData: { craftingTimePerUnit: 1, batchSize: 1 } },
storageDimensions: { gridWidth: 1, gridHeight: 1, stackable: false },
},
overrideAccess: true,
depth: 0,
});
assetIds.push(fillerAsset.id);
fillerAssetId = fillerAsset.id;
const blueprint = await payload.create({
collection: "structures",
data: {
name: `${RUN} HQ Depot`,
codeName: `${RUN}-hq-depot`,
approvalStatus: "in_progress",
description: LEXICAL_EMPTY as unknown as Structure["description"],
category: "logistics",
materials: [{ resource: resourceAlphaId, amount: 1 }],
constructionDurationMinutes: 1,
terrainType: "land",
maxHealth: 100,
},
overrideAccess: true,
depth: 0,
});
blueprintIds.push(blueprint.id);
const hq = await payload.create({
collection: "game-structures",
data: {
name: `${RUN} HQ`,
type: blueprint.id,
map: mapId,
coordinates: [100, 100],
constructionStatus: "complete",
},
overrideAccess: true,
depth: 0,
});
gameStructureIds.push(hq.id);
hqId = hq.id;
});
afterAll(async () => {
if (!payload) return;
for (const id of listingIds) {
await payload.delete({ collection: "market-listings", id, overrideAccess: true }).catch(() => {});
}
for (const id of marketStateIds) {
await payload.delete({ collection: "market-state", 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 lockerIds) {
await payload.delete({ collection: "locker-storages", 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) {
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 id of gameStructureIds) {
await payload.delete({ collection: "game-structures", id, overrideAccess: true }).catch(() => {});
}
for (const id of blueprintIds) {
await payload.delete({ collection: "structures", id, overrideAccess: true }).catch(() => {});
}
for (const id of assetIds) {
await payload.delete({ collection: "assets", id, overrideAccess: true }).catch(() => {});
}
for (const id of resourceIds) {
await payload.delete({ collection: "resources", id, overrideAccess: true }).catch(() => {});
}
for (const id of userIds) {
await payload.delete({ collection: "users", id, overrideAccess: true }).catch(() => {});
}
await payload.delete({ collection: "maps", id: mapId, overrideAccess: true }).catch(() => {});
});
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("Operation economy adapter", () => {
it("feeds a logistics extraction into the resource market supply exactly once", async () => {
await makeMarketState("resource", resourceAlphaId);
const before = (await supplyOf("resource", resourceAlphaId)) as number;
const { effectId } = await hookExtraction("logi", {
kind: "logistics",
hqStructureId: hqId,
actorId,
items: [{ resourceId: resourceAlphaId, amount: 5 }],
});
const result = await applyEconomyEffect(payload, effectId);
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record).not.toBeNull();
expect(record?.applied).toEqual([
{ itemType: "resource", itemId: resourceAlphaId, supply: 5 },
]);
expect(record?.rejected).toEqual([]);
expect(await supplyOf("resource", resourceAlphaId)).toBe(before + 5);
});
it("is a no-op when the same effect is applied twice", async () => {
const effectId = await manualExtractionEffect("dup", resourceAlphaId, 5);
const before = (await supplyOf("resource", resourceAlphaId)) as number;
await applyEconomyEffect(payload, effectId);
const afterFirst = (await supplyOf("resource", resourceAlphaId)) as number;
const second = await applyEconomyEffect(payload, effectId);
expect(await supplyOf("resource", resourceAlphaId)).toBe(afterFirst);
expect(await supplyOf("resource", resourceAlphaId)).toBe(before + 5);
// The marker is present exactly once: the retry did not append a second record.
expect((second as { reconciliationNotes: string }).reconciliationNotes.split(ECONOMY_NOTE_PREFIX).length - 1).toBe(1);
});
it("records player extractions as provenance without touching the market", async () => {
await makeMarketState("asset", tradeableAssetId);
const before = (await supplyOf("asset", tradeableAssetId)) as number;
const { effectId } = await hookExtraction("player", {
kind: "player",
actorId: buyerId,
items: [{ assetId: tradeableAssetId, quantity: 2 }],
});
const result = await applyEconomyEffect(payload, effectId);
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record?.applied).toEqual([]);
expect(record?.provenance).toEqual([{ itemType: "asset", itemId: tradeableAssetId }]);
expect(await supplyOf("asset", tradeableAssetId)).toBe(before);
});
it("drains the market supply for inventory losses", async () => {
await makeMarketState("asset", tradeableAssetId);
const before = (await supplyOf("asset", tradeableAssetId)) as number;
const sync = await payload.create({
collection: "arma-sync-events",
data: {
eventId: `${RUN}-loss`,
server: serverId,
type: "operation.inventory",
occurredAt: new Date().toISOString(),
payload: { operationId: RUN, version: 1, reason: "loss", items: [{ assetId: tradeableAssetId, quantity: 3 }] },
},
overrideAccess: true,
depth: 0,
});
syncEventIds.push(sync.id);
const events = await payload.find({
collection: "operation-events",
where: { sourceEvent: { equals: sync.id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
opEventIds.push(events.docs[0].id);
const effects = await payload.find({
collection: "operation-effects",
where: { operationEvent: { equals: events.docs[0].id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
opEffectIds.push(effects.docs[0].id);
const effectId = effects.docs[0].id;
const result = await applyEconomyEffect(payload, effectId);
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record?.applied).toEqual([
{ itemType: "asset", itemId: tradeableAssetId, supply: -3 },
]);
expect(await supplyOf("asset", tradeableAssetId)).toBe(before - 3);
});
it("caps excess supply and records the rejected remainder", async () => {
const effectId = await manualExtractionEffect("cap", resourceAlphaId, 5000);
const before = (await supplyOf("resource", resourceAlphaId)) as number;
const result = await applyEconomyEffect(payload, effectId, { faucetCapPerItem: 100 });
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record?.applied).toEqual([
{ itemType: "resource", itemId: resourceAlphaId, supply: 100 },
]);
expect(record?.rejected).toEqual([
{ itemType: "resource", itemId: resourceAlphaId, supply: 4900 },
]);
expect(await supplyOf("resource", resourceAlphaId)).toBe(before + 100);
});
it("keeps items without a market-state ledger provenance-eligible only", async () => {
const effectId = await manualExtractionEffect("nol", resourceBetaId, 7);
const result = await applyEconomyEffect(payload, effectId);
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record?.applied).toEqual([]);
expect(record?.provenance).toEqual([{ itemType: "resource", itemId: resourceBetaId }]);
const docs = await payload.find({
collection: "market-state",
where: { itemId: { equals: resourceBetaId } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(docs.docs).toHaveLength(0);
});
it("rejects a purchase into a full locker before any payment moves", async () => {
const listing = await payload.create({
collection: "market-listings",
data: {
asset: tradeableAssetId,
seller: sellerId,
quantity: 2,
price: 100,
status: "active",
listedAt: new Date().toISOString(),
},
overrideAccess: true,
depth: 0,
});
listingIds.push(listing.id);
const buyerAccount = await ensurePersonalAccount(payload, buyerId);
accountIds.push(buyerAccount.id);
await applyTransaction(payload, {
type: "deposit",
toAccountId: buyerAccount.id,
amount: 10000,
memo: `${RUN} buyer funding`,
});
const buyerAccountAfterFunding = (await payload.findByID({
collection: "bank-accounts",
id: buyerAccount.id,
depth: 0,
overrideAccess: true,
})) as { balance: number };
expect(buyerAccountAfterFunding.balance).toBe(10000);
const buyerLocker = await ensureLockerStorage(payload, buyerId);
lockerIds.push(buyerLocker.id);
const grid = await payload.findByID({
collection: "locker-storages",
id: buyerLocker.id,
depth: 0,
overrideAccess: true,
});
const entries = Array.from({ length: 60 }, (_, i) => ({
asset: fillerAssetId,
quantity: 1,
gridX: i % 10,
gridY: Math.floor(i / 10),
rotated: false,
}));
await payload.update({
collection: "locker-storages",
id: buyerLocker.id,
data: { items: entries },
overrideAccess: true,
depth: 0,
});
expect((grid as unknown as LockerStorage).items).toBeDefined();
const buyerUser = (await payload.findByID({
collection: "users",
id: buyerId,
depth: 0,
overrideAccess: true,
})) as { username: string };
await expect(
(async () => {
const { buyListing } = await import("@/lib/market");
const listingDoc = (await payload.findByID({
collection: "market-listings",
id: listing.id,
depth: 1,
overrideAccess: true,
})) as unknown as MarketListing;
const buyerDoc = (await payload.findByID({
collection: "users",
id: buyerId,
depth: 0,
overrideAccess: true,
})) as never;
await buyListing(payload, listingDoc, buyerDoc, { quantity: 1 });
})(),
).rejects.toThrow();
const after = (await payload.findByID({
collection: "bank-accounts",
id: buyerAccount.id,
depth: 0,
overrideAccess: true,
})) as { balance: number };
expect(after.balance).toBe(10000);
const stillActive = (await payload.findByID({
collection: "market-listings",
id: listing.id,
depth: 0,
overrideAccess: true,
})) as { status: string };
expect(stillActive.status).toBe("active");
expect(buyerUser.username).toContain(RUN);
expect((await payload.findByID({ collection: "game-structures", id: hqId, depth: 0, overrideAccess: true })) as GameStructure).toBeDefined();
});
it("rebuilds market baselines on reset while operation provenance survives", async () => {
const before = (await supplyOf("resource", resourceAlphaId)) as number;
const result = await resetMarketStateDocs(payload, {
items: [{ itemType: "resource", itemId: resourceAlphaId }],
});
expect(result.reset).toBe(1);
const after = (await supplyOf("resource", resourceAlphaId)) as number;
expect(after).toBe(100);
expect(before).toBeGreaterThanOrEqual(100);
const effects = await payload.find({
collection: "operation-effects",
where: { operationId: { equals: RUN } },
limit: 10,
depth: 0,
overrideAccess: true,
});
expect(effects.docs.length).toBeGreaterThan(0);
const withNote = effects.docs.filter((d) =>
((d as { reconciliationNotes: string | null }).reconciliationNotes ?? "").includes(ECONOMY_NOTE_PREFIX),
);
expect(withNote.length).toBeGreaterThan(0);
});
it("exposes the default caps constant for surfaces", () => {
expect(DEFAULT_ECONOMY_CAPS.faucetCapPerItem).toBeGreaterThan(0);
expect(DEFAULT_ECONOMY_CAPS.drainCapPerItem).toBeGreaterThan(0);
});
});