Files
Dune/game/auction-payment-routing.ts
T
Quintine 599af2475d
Verify and publish checkpoint container / container (push) Canceled after 0s
Route Advanced Emperor allied auction funding
2026-10-29 02:48:11 +11:00

84 lines
3.9 KiB
TypeScript

/** Actual positive payer legs of a single paid physical Treachery auction.
* The engine supplies already-authorized, funded contributors and owns the
* debit, escrow, income responses and any later diversion or cancellation. */
export type AuctionPaymentInput = Readonly<{
advanced: boolean;
free: boolean;
price: number;
buyer: string;
emperor: string | null;
/** Null is an ordinary auction; a Richese seller may retain its own card. */
seller: string | null;
/** Existing seller-income prevention may route the entire invoice to Bank. */
bankOnly?: boolean;
contributions: readonly Readonly<{ payer: string; amount: number }>[];
}>;
export type AuctionPaymentLeg = {
payer: string;
amount: number;
recipient: 'bank' | 'emperor' | 'seller';
};
export class AuctionPaymentRoutingError extends Error {
constructor(message: string) { super(message); this.name = 'AuctionPaymentRoutingError'; }
}
function check(condition: unknown, message: string): asserts condition {
if (!condition) throw new AuctionPaymentRoutingError(message);
}
const id = (value: unknown): value is string =>
typeof value === 'string' && value.length > 0 && value.trim() === value;
const amount = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) >= 0;
/**
* Route the exact already-funded invoice without touching any wallet.
* `seller` identifies Richese, not a generic normal-auction bank seller.
* In Advanced, Emperor's direct contribution to another buyer goes to Bank;
* Richese's contribution to another buyer of its own card goes to Emperor or
* Bank. A direct Emperor gift is part of the buyer's own leg, not aid.
*
* Payer identities/alliances and escrow authority are checked by the caller;
* this helper ensures the invoice has unique, positive, exactly funded legs.
* Free acquisitions carry no paid legs, even when their nominal price is > 0.
*/
export function routeAuctionPayment(input: AuctionPaymentInput): AuctionPaymentLeg[] {
check(!!input && typeof input === 'object' && !Array.isArray(input) &&
typeof input.advanced === 'boolean' && typeof input.free === 'boolean' &&
amount(input.price) && id(input.buyer) &&
(input.emperor === null || id(input.emperor)) &&
(input.seller === null || id(input.seller)) &&
(input.bankOnly === undefined || typeof input.bankOnly === 'boolean') &&
Array.isArray(input.contributions), 'Supply one valid auction invoice.');
check(input.seller === null || input.seller !== input.emperor,
'The Richese seller and Emperor must be distinct factions.');
const seen = new Set<string>();
let total = 0;
for (const leg of input.contributions) {
check(!!leg && typeof leg === 'object' && !Array.isArray(leg) && id(leg.payer) &&
amount(leg.amount) && leg.amount > 0 && !seen.has(leg.payer),
'Every actual payer must have one distinct positive contribution.');
seen.add(leg.payer);
total += leg.amount;
check(Number.isSafeInteger(total), 'The total auction invoice exceeds safe integer bounds.');
}
if (input.free) {
check(input.contributions.length === 0, 'A free acquisition cannot carry paid contributions.');
return [];
}
check(total === input.price, 'Actual auction contributions must exactly fund the price.');
const emperorOrBank = (): AuctionPaymentLeg['recipient'] =>
input.emperor ? 'emperor' : 'bank';
return input.contributions.map(leg => {
let recipient: AuctionPaymentLeg['recipient'];
if (input.bankOnly) recipient = 'bank';
else if (input.advanced && input.emperor && leg.payer === input.emperor && leg.payer !== input.buyer)
recipient = 'bank';
else if (input.seller === null || input.seller === input.buyer)
recipient = input.emperor && input.emperor !== input.buyer ? 'emperor' : 'bank';
else if (input.advanced && leg.payer === input.seller)
recipient = emperorOrBank();
else recipient = 'seller';
return {payer:leg.payer, amount:leg.amount, recipient};
});
}