D30-F02-A05 / Released engineering topic

Explain Minimum Balance Fees with Complete Daily Evidence, Grace and Waivers

A production-minded guide to Minimum-Balance Fee.

Explain Minimum Balance Fees with Complete Daily Evidence, Grace and WaiversD30 / D30-F02

A customer asks why a fee appeared when their average balance met the advertised number. Before answering, identify the actual balance method, period, threshold and exceptions. An average and a minimum answer different questions.

This tutorial implements fictional Cedar Teaching Bank's synthetic minimum end-of-day policy. An operator can show the exact low dates and waiver or grace decision. An engineer can verify the evidence and model why a new policy version must not create another accepted fee decision for the same period. All accounts, terms, amounts and receipts are synthetic.

Declare the product rule before calculating

Average-Daily-Balance Interest establishes the complete daily calendar. This fee uses its minimum and count of low days. The average remains a diagnostic.

Cedar's rule is precise: a day is low only when its posted end-of-day balance is strictly below the threshold. After complete validation, a trusted applicable waiver takes precedence. Otherwise, no low days means no fee; low days within the allowed grace count also mean no fee. A fee is due only when the count exceeds grace.

Plain text
low_days = count(balance[d] < threshold)
fee_due = no_waiver and low_days > allowed_low_days

In this selected policy, grace counts dates, not the depth of a shortfall or only consecutive low dates. Equality meets the threshold. Negative balances are valid signed observations; they are not routed through the interest calculator's unsupported-negative-principal policy.

US-scoped context explains why explicit terms matter. CFPB Regulation DD §1030.4(b)(3)–(4) addresses balance methods/periods and fee conditions, and its interpretation distinguishes fee and interest calculations. §1030.7 separately discusses minimum balances not affecting interest. These sources do not prescribe Cedar's synthetic fee or establish a real customer's entitlement.

Compare minimum, average and grace

Take two complete two-day paths at threshold 50000, fee 250 and zero allowed low days:

PathAverageMinimumLow daysOutcome
100000, 05000001Fee 250
50000, 5000050000500000No fee

The averages match. The minimum and strict low-day evidence differ. Substituting average balance would silently implement a different product.

Same-average paths and exact grace boundaries

Open the threshold/grace comparison at full size.

The main example retains all 35 dates from 1 October through 4 November 2026, with exclusive end 5 November. Its EOD runs are 21 days at 100000, two at 97600, two at 85600 and ten at 75600. Sum is 3222400 and exact average is 644480/7, but the chosen statistic is minimum 75600. Threshold 75000 yields no low day or fee.

Change one early date to 40000 and use threshold 50000. With grace zero, one low day produces fee 250. With grace one, that same complete calendar produces zero. Change two dates to 40000 while keeping grace one and the fee is 250 again. A date exactly 50000 is sufficient; 49999 is one low day.

These values were independently specified before reference execution. The arithmetic oracle retains the full primary calendar and accepted expectations. They belong to the separate synthetic product account deposits_product_usd, not Ada's earlier correction chronology or actual database-test timestamps.

Require every date before accepting an exception

The request declares one ledger/entity/account, USD credit-normal deposit liability perspective, a half-open UTC period, knowledge cutoff, account lifecycle, immutable policy, optional trusted waiver and daily evidence. It contains no irrelevant interest-rate fields.

Each date appears exactly once in ascending order. Its exact signed balance is a canonical decimal string, and its receipt covers the day's close and is available by the knowledge cutoff. Missing days are gaps, never zeros. Duplicate, reordered or malformed dates reject rather than being silently sorted or filled.

The policy must cover the whole period and be available by period-start UTC midnight. A later knowledge cutoff does not make newly announced terms applicable to an earlier period. Opening after the start or closing before the exclusive end routes partial-account-period policy for review; there is no invented proration. Closing exactly at the exclusive end is complete.

A waiver carries identity, trimmed nonempty reason and availability time no later than K. The trusted host establishes its authority and applicability. A local reason string is not permission. The implementation checks the complete input before returning a waiver, so a missing day remains incomplete even when a waiver was supplied.

Implement the boundary directly

The reference preserves the full low-date list, running minimum and cumulative count. Python integers and TypeScript BigInt compare exact minor units, including values beyond Number-safe precision. The core test is deliberately simple:

Python
low = balance < threshold
if low:
    low_dates.append(row["date"])

Only after full validation does the decision apply waiver, zero-low-day, grace and fee precedence. The output retains minimum dates, every low date, threshold, grace, exact average diagnostic, waiver provenance, amount and trace. Invalid or unsupported evidence returns no decision, amount-bearing proposal or trace.

Reasons use an explicit shared Unicode-scalar trim policy. Blank tabs, NEL and control separators are rejected after trimming; BOM remains nonblank; supplementary characters count as single scalars, and lone surrogates reject. These are reproducible wire rules rather than a guessed language-default equivalence.

A positive result proposes debit of the requested deposit liability and credit of fee_income_usd. The bank owes that deposit, so the debit reduces the liability. Zero has no zero-value posting legs. The proposal still needs chart, scope, period, durable identity and applicable product authority at the actual posting boundary. It is not proof of legal entitlement or enough available funds, and it does not enforce a no-overdraft product rule.

Keep one accepted decision for the period

Idempotent Journal Posting explains actual durable identity. Here a separate sequential snapshot model teaches the period decision before integration with durable storage.

Its key is ledger, entity, account, period start/end and minimum-balance fee kind. Policy version and calculation ID are excluded. Authorize the full requested account scope before looking up an accepted result.

Exact period retry and changed-policy conflict

Open the period-identity explanation at full size.

Suppose the first accepted request records fee 250 under policy version one. An exact request, ignoring object property order, returns that original complete result without another record. Changing the policy version, calculation ID, evidence, cutoff or waiver under that same period conflicts. It requires review/correction; it cannot create a second charge identity.

Waived and other zero decisions are accepted records too. A later changed request cannot bypass an earlier waiver by selecting a new version. A failed incomplete request reserves no identity. Returned results and state are detached, so mutating an output cannot rewrite the caller's accepted state.

The reducer labels modeled acceptance committed. That label means a modeled accepted decision, never a durable database write. Exact replay works at the 1000-record educational state limit; new identities are refused at capacity. Strict state/context checks reject duplicate periods, malformed records and duplicate or extra authority fields.

The host still owns approved billing periods: exact key equality does not detect overlapping period definitions. Real duplicate protection must persist decision and corresponding journal atomically, or use the stable Stage 1 business-operation identity. Copying JSON is not live concurrency control.

Verify the explanation and the recorder

The package has 84 calculation cases: 24 accepted, 60 rejected, plus 30 complete recorder actions. Strict Python/TypeScript parity compares full calculation and recorder result/state JSON. Independent cases include threshold equality, grace exhaustion, negative balances, same-average/different-minimum paths, waiver precedence, incomplete coverage, policy/lifecycle chronology, large amounts, Unicode reasons, original replay/conflict, scope denial, malformed state, capacity and detached outputs. Parity evidence, validation report.

Open the guided fee calendar lab. It bundles the actual TypeScript reference and keeps all 35 primary dates. Step the full result's diagnostic trace, accept a candidate in the snapshot model, then change grace and try recording it again. The candidate can now show zero while the original accepted fee remains 250; the changed request conflicts, and an original retry still returns 250. Both outputs are labeled so a current hypothetical calculation is not confused with accepted history.

Scenario change and Reset start an independent branch. Missing coverage shows a gap, no complete amount and no reserved identity despite waiver. Reduced-motion Play advances one date without a timer. Static figures and tables preserve the explanation without scripts.

Historical example: not useful. Reproducible synthetic terms teach these boundaries without fabricated real-bank tariffs or customer observations. Continue to Exact Reference Match in the Reconciliation family: a plausible match is evidence to review, not authority to move or clear money.

Validate first, then explain the fee

Waiver is a valid decision only after complete evidence and product terms pass validation.

The lowest balance and all low dates remain explainable. An uncommitted proposal and a sequential accepted-decision record remain separate from a real posted charge.

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

  • Accessed: 25 October 2026.
  • Supports: method/period/fee-condition context and exact technical arithmetic, assigned per source below.
  • Limitations: no source establishes Cedar's synthetic fee tariff, waiver authority, entitlement or regulatory compliance.
IDOrganization, source type, version, jurisdictionSourceSupportsLimitations
R01CFPB, official current Regulation DD and interpretation, US§1030.4(b)(3)–(4) account disclosuresExplicit balance methods/periods and fee conditions; fee and interest calculations may use different methods/periodsDoes not prescribe this synthetic minimum/grace/waiver contract
R02CFPB, official current interpretation, US§1030.7 payment of interestFee-minimum computation is distinct from minimum balance to earn interestNo claim this implements all US requirements; negative fee balances follow declared Cedar policy
R03Python Software Foundation, official Python 3.11 documentation, technicalfractionsExact reduced average diagnostic from integer inputsAverage is not the chosen fee statistic; local runtime 3.11.3
R04Ecma TC39, official living language specification, technicalBigInt typeExact integer arithmetic for signed balances/threshold comparisonNeither language arithmetic nor a JSON reducer provides durable acceptance

Historical case: not useful. All policy/account/date/rate-free calendar inputs are synthetic and independently specified before reference code. Threshold/grace/waiver, scope, Unicode reason policy and recorder identity are implementation choices. Calendar arithmetic is author-derived; parity/browser outcomes are locally observed. Host source ownership, scope authority, complete accepted evidence, applicable product terms and durable posting remain outside the reference. No real customer data, credentials or provider license is involved.

algorithm.ts
/** Synthetic minimum-EOD fee and sequential accepted-decision model.
 * Committed means modeled acceptance; source authority and durable posting
 * remain host responsibilities. All JSON money/fractions are exact strings.
 */
type Obj = Record<string, any>;
const REQUEST = [
  "schema_version",
  "calculation_id",
  "ledger_id",
  "entity_id",
  "account_id",
  "currency",
  "account_type",
  "normal_side",
  "period_start",
  "period_end",
  "knowledge_cutoff",
  "opened_at",
  "closed_at",
  "policy",
  "waiver",
  "days",
];
const DAY = [
  "date",
  "balance_minor",
  "balance_available_at",
  "balance_receipt_ref",
];
const POLICY = [
  "policy_version_id",
  "fee_kind",
  "threshold_minor",
  "fee_minor",
  "allowed_low_days",
  "effective_from",
  "effective_until",
  "available_at",
];
const WAIVER = ["waiver_id", "reason", "available_at"];
const SCOPE = ["ledger_id", "entity_id", "account_id"];
const IDENTITY = [...SCOPE, "period_start", "period_end", "fee_kind"];
const DECISION = [
  "calculation_id",
  "ledger_id",
  "entity_id",
  "account_id",
  "currency",
  "period_start",
  "period_end",
  "knowledge_cutoff",
  "fee_kind",
  "policy_version_id",
  "day_count_value",
  "balance_sum_minor",
  "minimum_balance_minor",
  "minimum_balance_dates",
  "average_balance_exact",
  "threshold_minor",
  "fee_minor",
  "low_balance_dates",
  "low_day_count",
  "allowed_low_days",
  "amount_minor",
  "waiver",
];
const TRACE = [
  ...DAY,
  "is_low",
  "cumulative_low_days",
  "running_minimum_minor",
];
const PROPOSAL = [
  "schema_version",
  "calculation_id",
  "ledger_id",
  "entity_id",
  "account_id",
  "currency",
  "period_start",
  "period_end",
  "knowledge_cutoff",
  "status",
  "amount_minor",
  "legs",
];
const CODES = ["WAIVER_APPLIED", "THRESHOLD_MET", "GRACE_APPLIED", "FEE_DUE"];
const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
const DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
const STAMP =
  /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/;
const SIGNED = /^(0|-?[1-9][0-9]{0,29})$/;
const UNSIGNED = /^(0|[1-9][0-9]{0,29})$/;
const POSITIVE = /^[1-9][0-9]{0,29}$/;
const WIDE_SIGNED = /^(0|-?[1-9][0-9]{0,119})$/;
const WIDE_POSITIVE = /^[1-9][0-9]{0,119}$/;
const STRIP =
  /^[\u0009-\u000d\u001c-\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+|[\u0009-\u000d\u001c-\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+$/g;
export class FeeError extends Error {}
function need(condition: any, code: string): asserts condition {
  if (!condition) throw new FeeError(code);
}
const object = (value: any): value is Obj =>
  value !== null &&
  typeof value === "object" &&
  !Array.isArray(value) &&
  [Object.prototype, null].includes(Object.getPrototypeOf(value));
const exact = (value: any, keys: string[]): value is Obj =>
  object(value) &&
  Object.keys(value).length === keys.length &&
  keys.every((key) => Object.hasOwn(value, key));
const matches = (pattern: RegExp, value: any) =>
  typeof value === "string" && pattern.exec(value)?.[0] === value;
function civil(value: any): number | null {
  if (!matches(DATE, value) || value.slice(0, 4) === "0000") return null;
  const d = new Date(value + "T00:00:00.000Z");
  return Number.isFinite(d.getTime()) && d.toISOString().slice(0, 10) === value
    ? d.getTime()
    : null;
}
function timestamp(value: any): boolean {
  if (!matches(STAMP, value) || value.slice(0, 4) === "0000") return false;
  const d = new Date(value);
  return Number.isFinite(d.getTime()) && d.toISOString() === value;
}
function gcd(a: bigint, b: bigint): bigint {
  while (b !== 0n) [a, b] = [b, a % b];
  return a;
}
function fractionJson(n: bigint, d: bigint) {
  const g = gcd(n < 0n ? -n : n, d);
  return { numerator: (n / g).toString(), denominator: (d / g).toString() };
}
function equal(a: any, b: any): boolean {
  if (a === b) return true;
  if (Array.isArray(a) || Array.isArray(b))
    return (
      Array.isArray(a) &&
      Array.isArray(b) &&
      a.length === b.length &&
      a.every((value, index) => equal(value, b[index]))
    );
  if (!object(a) || !object(b)) return false;
  const keys = Object.keys(a);
  return (
    keys.length === Object.keys(b).length &&
    keys.every((key) => Object.hasOwn(b, key) && equal(a[key], b[key]))
  );
}
const identityKey = (value: Obj) =>
  [...SCOPE, "period_start", "period_end", "fee_kind"]
    .map((key) => value[key])
    .join("\u0000");

function parseWaiver(value: any, knowledge: string): Obj | null {
  if (value === null) return null;
  need(exact(value, WAIVER), "INVALID_WAIVER");
  need(
    matches(ID, value.waiver_id) && typeof value.reason === "string",
    "INVALID_WAIVER",
  );
  const characters = Array.from(value.reason as string);
  need(
    characters.every((c) => {
      const n = c.codePointAt(0)!;
      return n < 0xd800 || n > 0xdfff;
    }),
    "INVALID_WAIVER",
  );
  const reason = value.reason.replace(STRIP, "");
  need(
    Array.from(reason).length >= 1 &&
      Array.from(reason).length <= 240 &&
      timestamp(value.available_at),
    "INVALID_WAIVER",
  );
  need(value.available_at <= knowledge, "WAIVER_BEYOND_KNOWLEDGE");
  return { ...value, reason };
}

function validateRequest(request: any): { count: number; waiver: Obj | null } {
  need(exact(request, REQUEST), "INVALID_SCHEMA");
  need(
    request.schema_version === "d30-minimum-balance-fee-v1",
    "UNSUPPORTED_SCHEMA",
  );
  need(
    ["calculation_id", ...SCOPE].every((key) => matches(ID, request[key])),
    "INVALID_IDENTIFIER",
  );
  need(request.currency === "USD", "UNSUPPORTED_CURRENCY");
  need(
    request.account_type === "liability" && request.normal_side === "credit",
    "UNSUPPORTED_ACCOUNT_PERSPECTIVE",
  );
  const start = civil(request.period_start),
    end = civil(request.period_end);
  need(start !== null && end !== null, "INVALID_DATE");
  const count = (end - start) / 86400000;
  need(count >= 1 && count <= 366, "INVALID_PERIOD");
  const knowledge = request.knowledge_cutoff;
  need(timestamp(knowledge), "INVALID_TIMESTAMP");
  need(
    knowledge >= request.period_end + "T00:00:00.000Z",
    "KNOWLEDGE_BEFORE_PERIOD_END",
  );
  const policy = request.policy;
  need(exact(policy, POLICY), "INVALID_SCHEMA");
  need(matches(ID, policy.policy_version_id), "INVALID_IDENTIFIER");
  need(policy.fee_kind === "minimum-balance", "UNSUPPORTED_FEE_KIND");
  need(matches(UNSIGNED, policy.threshold_minor), "INVALID_THRESHOLD");
  need(matches(POSITIVE, policy.fee_minor), "INVALID_FEE");
  need(
    matches(UNSIGNED, policy.allowed_low_days) &&
      BigInt(policy.allowed_low_days) <= 366n,
    "INVALID_GRACE",
  );
  const pStart = civil(policy.effective_from),
    pEnd = civil(policy.effective_until);
  need(
    pStart !== null && pEnd !== null && pStart < pEnd,
    "INVALID_POLICY_PERIOD",
  );
  need(pStart <= start && pEnd >= end, "POLICY_PERIOD_UNSUPPORTED");
  need(timestamp(policy.available_at), "INVALID_TIMESTAMP");
  need(
    policy.available_at <= request.period_start + "T00:00:00.000Z",
    "POLICY_NOT_AVAILABLE",
  );
  const days = request.days;
  need(Array.isArray(days), "INVALID_CALENDAR");
  need(days.length === count, "INCOMPLETE_CALENDAR");
  const seen = new Set<string>();
  for (const [index, row] of days.entries()) {
    need(exact(row, DAY), "INVALID_SCHEMA");
    const current = civil(row.date);
    need(current !== null, "INVALID_DATE");
    need(current >= start && current < end, "DAY_OUTSIDE_PERIOD");
    need(!seen.has(row.date), "DUPLICATE_DAY");
    need(current === start + index * 86400000, "OUT_OF_ORDER_DAY");
    seen.add(row.date);
    need(matches(SIGNED, row.balance_minor), "INVALID_BALANCE");
    need(matches(ID, row.balance_receipt_ref), "INVALID_IDENTIFIER");
    need(timestamp(row.balance_available_at), "INVALID_TIMESTAMP");
    need(
      row.balance_available_at >= row.date + "T23:59:59.999Z",
      "BALANCE_BEFORE_DAY_CLOSE",
    );
    need(row.balance_available_at <= knowledge, "BALANCE_BEYOND_KNOWLEDGE");
  }
  need(
    timestamp(request.opened_at) &&
      (request.closed_at === null || timestamp(request.closed_at)),
    "INVALID_TIMESTAMP",
  );
  if (request.closed_at !== null) {
    need(request.closed_at >= request.opened_at, "INVALID_ACCOUNT_LIFECYCLE");
    need(request.closed_at <= knowledge, "CLOSE_BEYOND_KNOWLEDGE");
  }
  need(
    request.opened_at <= request.period_start + "T00:00:00.000Z" &&
      (request.closed_at === null ||
        request.closed_at >= request.period_end + "T00:00:00.000Z"),
    "PARTIAL_ACCOUNT_PERIOD_POLICY_UNSUPPORTED",
  );
  return { count, waiver: parseWaiver(request.waiver, knowledge) };
}

function makeProposal(request: Obj, amount: bigint): Obj {
  const output: Obj = {};
  for (const key of [
    "calculation_id",
    ...SCOPE,
    "currency",
    "period_start",
    "period_end",
    "knowledge_cutoff",
  ])
    output[key] = request[key];
  return {
    schema_version: "d30-minimum-balance-fee-proposal-v1",
    ...output,
    status: amount === 0n ? "zero" : "ready",
    amount_minor: amount.toString(),
    legs:
      amount === 0n
        ? []
        : [
            {
              account_id: request.account_id,
              side: "debit",
              amount_minor: amount.toString(),
            },
            {
              account_id: "fee_income_usd",
              side: "credit",
              amount_minor: amount.toString(),
            },
          ],
  };
}

export function calculateMinimumBalanceFee(request: any): Obj {
  try {
    const validated = validateRequest(request),
      policy = request.policy,
      threshold = BigInt(policy.threshold_minor);
    let total = 0n,
      minimum: bigint | null = null;
    const lowDates: string[] = [],
      trace: Obj[] = [];
    let minimumDates: string[] = [];
    for (const row of request.days) {
      const balance = BigInt(row.balance_minor);
      total += balance;
      if (minimum === null || balance < minimum) {
        minimum = balance;
        minimumDates = [row.date];
      } else if (balance === minimum) minimumDates.push(row.date);
      const low = balance < threshold;
      if (low) lowDates.push(row.date);
      trace.push({
        ...row,
        is_low: low,
        cumulative_low_days: String(lowDates.length),
        running_minimum_minor: minimum.toString(),
      });
    }
    const code =
        validated.waiver !== null
          ? "WAIVER_APPLIED"
          : lowDates.length === 0
            ? "THRESHOLD_MET"
            : BigInt(lowDates.length) <= BigInt(policy.allowed_low_days)
              ? "GRACE_APPLIED"
              : "FEE_DUE",
      amount = code === "FEE_DUE" ? BigInt(policy.fee_minor) : 0n,
      decision: Obj = {};
    for (const key of [
      "calculation_id",
      ...SCOPE,
      "currency",
      "period_start",
      "period_end",
      "knowledge_cutoff",
    ])
      decision[key] = request[key];
    Object.assign(decision, {
      fee_kind: "minimum-balance",
      policy_version_id: policy.policy_version_id,
      day_count_value: String(validated.count),
      balance_sum_minor: total.toString(),
      minimum_balance_minor: minimum!.toString(),
      minimum_balance_dates: minimumDates,
      average_balance_exact: fractionJson(total, BigInt(validated.count)),
      threshold_minor: policy.threshold_minor,
      fee_minor: policy.fee_minor,
      low_balance_dates: lowDates,
      low_day_count: String(lowDates.length),
      allowed_low_days: policy.allowed_low_days,
      amount_minor: amount.toString(),
      waiver: validated.waiver,
    });
    return {
      status: "ok",
      code,
      decision,
      trace,
      posting_proposal: makeProposal(request, amount),
    };
  } catch (error) {
    if (!(error instanceof FeeError)) throw error;
    return {
      status: "rejected",
      code: error.message,
      decision: null,
      trace: [],
      posting_proposal: null,
    };
  }
}

export function initialState(): Obj {
  return { schema_version: "d30-minimum-balance-decisions-v1", records: [] };
}
function identityFromRequest(request: any): Obj {
  need(
    object(request) &&
      [...SCOPE, "period_start", "period_end"].every((key) =>
        Object.hasOwn(request, key),
      ),
    "INVALID_SCHEMA",
  );
  need(
    SCOPE.every((key) => matches(ID, request[key])),
    "INVALID_IDENTIFIER",
  );
  const start = civil(request.period_start),
    end = civil(request.period_end);
  need(start !== null && end !== null, "INVALID_DATE");
  need(
    end - start >= 86400000 && end - start <= 366 * 86400000,
    "INVALID_PERIOD",
  );
  const identity: Obj = { fee_kind: "minimum-balance" };
  for (const key of [...SCOPE, "period_start", "period_end"])
    identity[key] = request[key];
  return identity;
}
function validateContext(context: any): Obj[] {
  need(exact(context, ["authorized_scopes"]), "INVALID_AUTHORIZATION_CONTEXT");
  const scopes = context.authorized_scopes;
  need(
    Array.isArray(scopes) && scopes.length <= 1000,
    "INVALID_AUTHORIZATION_CONTEXT",
  );
  const seen = new Set<string>();
  for (const scope of scopes) {
    need(
      exact(scope, SCOPE) && SCOPE.every((key) => matches(ID, scope[key])),
      "INVALID_AUTHORIZATION_CONTEXT",
    );
    const key = SCOPE.map((k) => scope[k]).join("\u0000");
    need(!seen.has(key), "INVALID_AUTHORIZATION_CONTEXT");
    seen.add(key);
  }
  return scopes;
}
function validFractionShape(value: any): boolean {
  if (
    !exact(value, ["numerator", "denominator"]) ||
    !matches(WIDE_SIGNED, value.numerator) ||
    !matches(WIDE_POSITIVE, value.denominator)
  )
    return false;
  const n = BigInt(value.numerator);
  return gcd(n < 0n ? -n : n, BigInt(value.denominator)) === 1n;
}

function validateState(state: any): void {
  // Structural checks use accepted stored facts, never a new request's policy.
  try {
    need(
      exact(state, ["schema_version", "records"]) &&
        state.schema_version === "d30-minimum-balance-decisions-v1",
      "INVALID_STATE",
    );
    need(
      Array.isArray(state.records) && state.records.length <= 1000,
      "INVALID_STATE",
    );
    const seen = new Set<string>();
    for (const entry of state.records) {
      need(
        exact(entry, ["identity", "request", "result"]) &&
          exact(entry.identity, IDENTITY),
        "INVALID_STATE",
      );
      const identity = identityFromRequest(entry.request),
        key = identityKey(identity);
      need(equal(entry.identity, identity) && !seen.has(key), "INVALID_STATE");
      seen.add(key);
      const validated = validateRequest(entry.request),
        result = entry.result,
        request = entry.request;
      need(
        exact(result, [
          "status",
          "code",
          "decision",
          "trace",
          "posting_proposal",
        ]) &&
          result.status === "ok" &&
          CODES.includes(result.code),
        "INVALID_STATE",
      );
      const decision = result.decision,
        proposal = result.posting_proposal;
      need(
        exact(decision, DECISION) && exact(proposal, PROPOSAL),
        "INVALID_STATE",
      );
      need(
        [
          "calculation_id",
          ...SCOPE,
          "currency",
          "period_start",
          "period_end",
          "knowledge_cutoff",
        ].every(
          (k) => decision[k] === request[k] && proposal[k] === request[k],
        ),
        "INVALID_STATE",
      );
      need(
        decision.fee_kind === "minimum-balance" &&
          decision.policy_version_id === request.policy.policy_version_id,
        "INVALID_STATE",
      );
      need(
        decision.day_count_value === String(validated.count),
        "INVALID_STATE",
      );
      need(
        [
          "threshold_minor",
          "fee_minor",
          "low_day_count",
          "allowed_low_days",
          "amount_minor",
        ].every((k) => matches(UNSIGNED, decision[k])),
        "INVALID_STATE",
      );
      need(
        matches(WIDE_SIGNED, decision.balance_sum_minor) &&
          matches(SIGNED, decision.minimum_balance_minor) &&
          validFractionShape(decision.average_balance_exact),
        "INVALID_STATE",
      );
      need(equal(decision.waiver, validated.waiver), "INVALID_STATE");
      need(
        ["threshold_minor", "fee_minor", "allowed_low_days"].every(
          (k) => decision[k] === request.policy[k],
        ),
        "INVALID_STATE",
      );
      const lowCount = BigInt(decision.low_day_count),
        allowed = BigInt(decision.allowed_low_days);
      need(lowCount <= BigInt(validated.count), "INVALID_STATE");
      need(
        (result.code === "WAIVER_APPLIED") === (validated.waiver !== null),
        "INVALID_STATE",
      );
      need(
        result.code === "WAIVER_APPLIED" ||
          (result.code === "THRESHOLD_MET" && lowCount === 0n) ||
          (result.code === "GRACE_APPLIED" &&
            lowCount > 0n &&
            lowCount <= allowed) ||
          (result.code === "FEE_DUE" && lowCount > allowed),
        "INVALID_STATE",
      );
      for (const k of ["minimum_balance_dates", "low_balance_dates"]) {
        const values = decision[k];
        need(
          Array.isArray(values) &&
            values.every(
              (v) =>
                civil(v) !== null &&
                v >= request.period_start &&
                v < request.period_end,
            ),
          "INVALID_STATE",
        );
        need(equal(values, [...new Set(values)].sort()), "INVALID_STATE");
      }
      need(
        decision.minimum_balance_dates.length > 0 &&
          BigInt(decision.low_balance_dates.length) ===
            BigInt(decision.low_day_count),
        "INVALID_STATE",
      );
      need(
        Array.isArray(result.trace) && result.trace.length === validated.count,
        "INVALID_STATE",
      );
      for (let i = 0; i < result.trace.length; i++) {
        const row = result.trace[i],
          original = request.days[i];
        need(
          exact(row, TRACE) && DAY.every((k) => row[k] === original[k]),
          "INVALID_STATE",
        );
        need(
          typeof row.is_low === "boolean" &&
            matches(UNSIGNED, row.cumulative_low_days) &&
            BigInt(row.cumulative_low_days) <= BigInt(validated.count) &&
            matches(SIGNED, row.running_minimum_minor),
          "INVALID_STATE",
        );
      }
      const amount =
        result.code === "FEE_DUE" ? BigInt(request.policy.fee_minor) : 0n;
      need(
        decision.amount_minor === amount.toString() &&
          equal(proposal, makeProposal(request, amount)),
        "INVALID_STATE",
      );
    }
  } catch (error) {
    if (
      error instanceof FeeError ||
      error instanceof TypeError ||
      error instanceof RangeError
    )
      throw new FeeError("INVALID_STATE");
    throw error;
  }
}

export function recordMinimumBalanceFee(
  state: any,
  request: any,
  context: any,
): Obj {
  const original = structuredClone(state);
  try {
    const identity = identityFromRequest(request),
      scopes = validateContext(context);
    need(
      scopes.some((scope) =>
        SCOPE.every((key) => scope[key] === identity[key]),
      ),
      "SCOPE_FORBIDDEN",
    );
    validateState(state);
    const prior = state.records.find((entry: Obj) =>
      equal(entry.identity, identity),
    );
    if (prior) {
      need(equal(prior.request, request), "DECISION_CONFLICT");
      return {
        status: "replayed",
        code: "PRIOR_DECISION_REPLAYED",
        result: structuredClone(prior.result),
        state: original,
      };
    }
    need(state.records.length < 1000, "STATE_CAPACITY");
    const result = calculateMinimumBalanceFee(request);
    need(result.status === "ok", result.code);
    original.records.push({
      identity: structuredClone(identity),
      request: structuredClone(request),
      result: structuredClone(result),
    });
    return {
      status: "committed",
      code: "FEE_DECISION_RECORDED",
      result: structuredClone(result),
      state: original,
    };
  } catch (error) {
    if (!(error instanceof FeeError)) throw error;
    return {
      status: "rejected",
      code: error.message,
      result: null,
      state: original,
    };
  }
}
Full-height labminimum fee labOpen full screen
Written by

Fintech engineer building market-data and financial systems, and the author of every article, glossary record, and reference implementation on The Fintech Builder.