Functional Weave
Code in TypeScript

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),
  };
}