property.completion-statement@1.0.1
impl/typescript.ts
3,188 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { daysBetween } from "./dates_days_between.ts"; ← from dates.days-between ^1.0.0 · built alongside by fune
import { prorate } from "./finance_proration.ts"; ← from finance.proration ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type ApportionmentLine, type CompletionStatement, type FeeLine, type PeriodCharge } from "./property_completion_statement_types.ts";
/**
* A buyer's completion statement. Each outgoing is split at the end of the
* completion day (the seller owns the property until then) by
* finance.proration, so the two shares always add back to the charge.
*/
export function completionStatement(
price: Money,
depositPaid: Money,
completionDate: string,
apportionments: readonly PeriodCharge[],
fees: readonly FeeLine[],
): CompletionStatement {
const currency = price.currency;
const same = (m: Money, what: string) => {
if (m.currency !== currency) {
throw new RangeError(`currency mismatch: ${what} is ${m.currency}, the price is ${currency}`);
}
};
same(depositPaid, "depositPaid");
if (!Number.isInteger(price.minor) || price.minor < 0) {
throw new RangeError(`price must not be negative, received ${price.minor}`);
}
if (!Number.isInteger(depositPaid.minor) || depositPaid.minor < 0 || depositPaid.minor > price.minor) {
throw new RangeError(`depositPaid must be between 0 and the price, received ${depositPaid.minor}`);
}
let balance = price.minor - depositPaid.minor;
const lines: ApportionmentLine[] = [];
for (const charge of apportionments) {
same(charge.amount, `"${charge.label}"`);
if (!Number.isInteger(charge.amount.minor) || charge.amount.minor < 0) {
throw new RangeError(`"${charge.label}" must not be negative, received ${charge.amount.minor}`);
}
const totalDays = daysBetween(charge.periodStart, charge.periodEnd) + 1;
if (totalDays < 1) {
throw new RangeError(`"${charge.label}": periodEnd ${charge.periodEnd} is before periodStart ${charge.periodStart}`);
}
// The seller owns the property to the end of the completion day.
const upToCompletion = daysBetween(charge.periodStart, completionDate) + 1;
const sellerDays = Math.min(totalDays, Math.max(0, upToCompletion));
const split = prorate(charge.amount, totalDays, sellerDays);
const adjustment = charge.paidBySeller ? split.unused.minor : -split.used.minor;
balance += adjustment;
lines.push({ label: charge.label, sellerDays, buyerDays: totalDays - sellerDays, adjustment: money(adjustment, currency) });
}
let feesTotal = 0;
const feeLines: FeeLine[] = [];
for (const fee of fees) {
same(fee.amount, `"${fee.label}"`);
if (!Number.isInteger(fee.amount.minor) || fee.amount.minor < 0) {
throw new RangeError(`"${fee.label}" must not be negative, received ${fee.amount.minor}`);
}
feesTotal += fee.amount.minor;
feeLines.push({ label: fee.label, amount: money(fee.amount.minor, currency) });
}
return {
price: money(price.minor, currency),
depositPaid: money(depositPaid.minor, currency),
apportionments: lines,
balanceToSeller: money(balance, currency),
fees: feeLines,
feesTotal: money(feesTotal, currency),
totalDue: money(balance + feesTotal, currency),
};
}