84 lines
3.9 KiB
TypeScript
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};
|
|
});
|
|
}
|