Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { parseIsoDate } from "./dates_add_days.ts"; ← from dates.add-days ^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 CappedCharge, type FeeCapResult, type FeeCharge } from "./banking_fees_cap_types.ts";
/**
* The charging period a date falls in, named by the year and month it starts
* in. With cycleDay 15, 2026-03-14 belongs to the period that began on
* 2026-02-15 and 2026-03-15 to the one that begins that day.
*/
function periodKey(iso: string, cycleDay: number): string {
const d = parseIsoDate(iso);
let year = d.year;
let month = d.month;
if (d.day < cycleDay) {
month -= 1;
if (month === 0) {
month = 12;
year -= 1;
}
}
return `${year}-${month}`;
}
/**
* Apply a monthly cap to a list of charges. Within each charging period the
* charges are taken in date order (ties in input order) until the cap is
* used up: the charge that crosses the cap is reduced to what is left, and
* every later charge in that period is waived in full. The result keeps the
* input order, so it lines up with the caller's own list.
*/
export function applyFeeCap(charges: readonly FeeCharge[], monthlyCap: Money, cycleDay: number): FeeCapResult {
if (!Number.isInteger(cycleDay) || cycleDay < 1 || cycleDay > 28) {
throw new RangeError(`cycleDay must be 1 to 28, received ${cycleDay}`);
}
if (monthlyCap.minor < 0) {
throw new RangeError(`monthlyCap must not be negative, received ${monthlyCap.minor}`);
}
const currency = monthlyCap.currency;
const keys: string[] = [];
charges.forEach((charge) => {
if (charge.amount.currency !== currency) {
throw new RangeError(`currency mismatch: ${charge.amount.currency} and ${currency}`);
}
if (charge.amount.minor < 0) {
throw new RangeError(`charge "${charge.label}" must not be negative, received ${charge.amount.minor}`);
}
keys.push(periodKey(charge.date, cycleDay));
});
const order = charges.map((_, index) => index);
// Array.prototype.sort is stable, so equal dates keep their input order.
order.sort((a, b) => (charges[a].date < charges[b].date ? -1 : charges[a].date > charges[b].date ? 1 : 0));
const used = new Map<string, number>();
const chargedMinor: number[] = charges.map(() => 0);
for (const index of order) {
const spent = used.get(keys[index]) ?? 0;
const room = monthlyCap.minor - spent;
const take = Math.min(charges[index].amount.minor, room);
chargedMinor[index] = take;
used.set(keys[index], spent + take);
}
let totalCharged = 0;
let totalWaived = 0;
const out: CappedCharge[] = charges.map((charge, index) => {
const charged = chargedMinor[index];
const waived = charge.amount.minor - charged;
totalCharged += charged;
totalWaived += waived;
return {
date: charge.date,
label: charge.label,
amount: money(charge.amount.minor, currency),
charged: money(charged, currency),
waived: money(waived, currency),
};
});
return { charges: out, totalCharged: money(totalCharged, currency), totalWaived: money(totalWaived, currency) };
}