Functional Weave
Code in Rust

subscriptions.invoice@1.0.0

impl/typescript.ts

4,242 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 { type Invoice, type InvoiceLine, calculateInvoice } from "./finance_invoice_calculate.ts";  ← from finance.invoice.calculate ^1.0.0 · built alongside by fune
import { lineTotal } from "./finance_invoice_line_total.ts";  ← from finance.invoice.line-total ^1.0.0 · built alongside by fune
import { allocate } from "./money_allocate.ts";  ← from money.allocate ^1.0.0 · built alongside by fune
import { assertSameCurrency, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { applyRate } from "./money_apply_rate.ts";  ← from money.apply-rate ^1.0.0 · built alongside by fune
import { type Discount } from "./subscriptions_invoice_types.ts";

function checkDiscount(d: Discount, currency: string): void {
  if ((d.basisPoints === null) === (d.amountOff === null)) {
    throw new RangeError(`a discount needs exactly one of basisPoints and amountOff: "${d.description}"`);
  }
  if (d.basisPoints !== null && (!Number.isInteger(d.basisPoints) || d.basisPoints < 0 || d.basisPoints > 10000)) {
    throw new RangeError(`discount basis points must be between 0 and 10000, received ${d.basisPoints}`);
  }
  if (d.amountOff !== null) {
    assertSameCurrency(money(0, currency), d.amountOff);
    if (d.amountOff.minor < 0) throw new RangeError(`discount amount must not be negative, received ${d.amountOff.minor}`);
  }
  if (!Number.isInteger(d.startPeriod) || d.startPeriod < 1) {
    throw new RangeError(`discount start period must be 1 or more, received ${d.startPeriod}`);
  }
  if (d.duration === "repeating") {
    if (d.durationPeriods === null || !Number.isInteger(d.durationPeriods) || d.durationPeriods < 1) {
      throw new RangeError(`a repeating discount needs durationPeriods of 1 or more, received ${d.durationPeriods}`);
    }
  } else if (d.duration === "once" || d.duration === "forever") {
    if (d.durationPeriods !== null) throw new RangeError(`durationPeriods applies only to a repeating discount: "${d.description}"`);
  } else {
    throw new RangeError(`unknown discount duration "${d.duration}": expected once, repeating or forever`);
  }
}

function applies(d: Discount, period: number): boolean {
  if (d.duration === "once") return period === d.startPeriod;
  if (d.duration === "repeating") return period >= d.startPeriod && period < d.startPeriod + (d.durationPeriods as number);
  return period >= d.startPeriod;
}

/**
 * The renewal invoice for one billing period: plan, add-ons and usage, less
 * the coupons in force in that period, taxed per line by
 * finance.invoice.calculate. Each coupon becomes negative lines, one per VAT
 * category, so the reduction is taxed at the rate of what it reduces.
 */
export function renewalInvoice(
  plan: InvoiceLine,
  addOns: readonly InvoiceLine[],
  usage: readonly InvoiceLine[],
  discounts: readonly Discount[],
  periodNumber: number,
  jurisdiction: string,
  invoiceDate: string,
): Invoice {
  if (!Number.isInteger(periodNumber) || periodNumber < 1) {
    throw new RangeError(`period number must be 1 or more, received ${periodNumber}`);
  }
  const currency = plan.unitPrice.currency;
  const items = [plan, ...addOns, ...usage];

  const categories: string[] = [];
  const categoryNet: number[] = [];
  let running = 0;
  for (const item of items) {
    const net = lineTotal(item.unitPrice, item.quantity, item.discountBasisPoints);
    assertSameCurrency(money(0, currency), net);
    let k = categories.indexOf(item.taxCategory);
    if (k < 0) {
      k = categories.length;
      categories.push(item.taxCategory);
      categoryNet.push(0);
    }
    categoryNet[k] += net.minor;
    running += net.minor;
  }
  // Credit-only categories take no share of a discount.
  const weights = categoryNet.map((n) => Math.max(n, 0));

  const discountLines: InvoiceLine[] = [];
  for (const d of discounts) {
    checkDiscount(d, currency);
    if (!applies(d, periodNumber) || running <= 0) continue;
    const amount = d.basisPoints !== null ? applyRate(money(running, currency), d.basisPoints, "half-up").minor : Math.min((d.amountOff as { minor: number }).minor, running);
    if (amount === 0) continue;
    running -= amount;
    allocate(money(amount, currency), weights).forEach((share, k) => {
      if (share.minor !== 0) {
        discountLines.push({ description: d.description, unitPrice: money(-share.minor, currency), quantity: 1, discountBasisPoints: 0, taxCategory: categories[k] });
      }
    });
  }
  return calculateInvoice([...items, ...discountLines], jurisdiction, invoiceDate);
}