94 lines
4.7 KiB
TypeScript
94 lines
4.7 KiB
TypeScript
import { validatePublicDeals, voidPublicDealPayment, type PublicDeal, type PublicDealSeat } from './public-deals';
|
|
|
|
/** A deal-linked obligation is independent of every other deal and of
|
|
* Truthtrance. It is due once for its exact gross payment, not a threshold
|
|
* shared with a different agreement. */
|
|
export type PendingPublicDealPayment = {
|
|
deal: string;
|
|
payer: string;
|
|
recipient: string;
|
|
dueTurn: number;
|
|
amount: number;
|
|
};
|
|
|
|
export class PublicDealObligationError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'PublicDealObligationError';
|
|
}
|
|
}
|
|
|
|
const identifier = (value: unknown): value is string =>
|
|
typeof value === 'string' && value.length > 0 && value === value.trim();
|
|
const nonnegative = (value: unknown): value is number =>
|
|
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
|
|
/** Return a payer's unsettled active terms that can still be paid now,
|
|
* including future deadlines. Past-due rows stay in the authoritative ledger
|
|
* for the engine's expiry reconciliation; this helper never voids them. */
|
|
export function outstandingPublicDealPayments(
|
|
deals: readonly PublicDeal[], payer: string, turn: number,
|
|
): PendingPublicDealPayment[] {
|
|
if (!Array.isArray(deals) || !identifier(payer) || !Number.isSafeInteger(turn) || turn < 1)
|
|
throw new PublicDealObligationError('Retain the payer and original turn of the deferred payments.');
|
|
return deals.flatMap(deal => {
|
|
const payment = deal.payment;
|
|
if (deal.status !== 'active' || deal.paymentStatus !== 'pending' ||
|
|
payment?.kind !== 'deferredBribe' || payment.payer !== payer || payment.dueTurn < turn) return [];
|
|
return [{ deal: deal.id, payer, recipient: payment.recipient, dueTurn: payment.dueTurn, amount: payment.amount }];
|
|
});
|
|
}
|
|
|
|
/** Existing turn-specific planners and funding totals consider only payments
|
|
* due in that turn. They must not reserve every future promise immediately. */
|
|
export function pendingPublicDealPayments(
|
|
deals: readonly PublicDeal[], payer: string, turn: number,
|
|
): PendingPublicDealPayment[] {
|
|
return outstandingPublicDealPayments(deals,payer,turn).filter(payment => payment.dueTurn === turn);
|
|
}
|
|
|
|
/** Separate deals add, including two deals for the same recipient. */
|
|
export function publicDealPaymentTotal(deals: readonly PublicDeal[], payer: string, turn: number): number {
|
|
let total = 0;
|
|
for (const payment of pendingPublicDealPayments(deals, payer, turn)) {
|
|
if (!Number.isSafeInteger(payment.amount) || payment.amount <= 0 ||
|
|
!Number.isSafeInteger(total + payment.amount))
|
|
throw new PublicDealObligationError('The outstanding deal payments exceed the safe spice range.');
|
|
total += payment.amount;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
export type PublicDealPaymentWitness = {
|
|
/** The engine's currently available uncommitted funding, or null if a
|
|
* pending continuation means no reliable current quote is available. */
|
|
available: number | null;
|
|
change: 'voluntary' | 'external';
|
|
/** Each ID must be proven individually impossible by the engine. A mere
|
|
* cash shortage does not prove loss of future income or the other debts. */
|
|
impossibleDealIds?: readonly string[];
|
|
};
|
|
|
|
/** An actor may not voluntarily make their accepted, funded payments
|
|
* impossible. External uncertainty leaves every agreement intact; only
|
|
* independently proven impossible terms are voided. No funds move here. */
|
|
export function reconcilePublicDealPayments(
|
|
deals: readonly PublicDeal[], players: readonly PublicDealSeat[], payer: string, turn: number,
|
|
witness: PublicDealPaymentWitness,
|
|
): PublicDeal[] {
|
|
validatePublicDeals(deals, players);
|
|
if (!witness || (witness.change !== 'voluntary' && witness.change !== 'external') ||
|
|
(witness.available !== null && !nonnegative(witness.available)) ||
|
|
(witness.impossibleDealIds !== undefined && !Array.isArray(witness.impossibleDealIds)))
|
|
throw new PublicDealObligationError('Retain the actual funded witness and cause of the change.');
|
|
const pending = pendingPublicDealPayments(deals, payer, turn);
|
|
const ids = witness.impossibleDealIds ?? [];
|
|
if (new Set(ids).size !== ids.length || ids.some(id => !pending.some(row => row.deal === id)))
|
|
throw new PublicDealObligationError('Void only individually proven pending terms of this payer and turn.');
|
|
if (witness.change === 'voluntary' && (ids.length > 0 ||
|
|
witness.available !== null && witness.available < publicDealPaymentTotal(deals, payer, turn)))
|
|
throw new PublicDealObligationError('Keep the funding needed for your accepted public deal payments.');
|
|
if (witness.change !== 'external' || !ids.length) return [...deals];
|
|
return deals.map(deal => ids.includes(deal.id) ? voidPublicDealPayment(deal, players) : deal);
|
|
}
|