# Daily Simple Interest Accrual — exact data contract

D30-F02-A01. Frozen teaching profile, 25 October 2026.

## Learning and ownership

Calculate exact daily simple deposit interest from a complete UTC EOD calendar and causally available versioned nominal rates. Preserve fractional minor units, round only the period settlement half-even, retain signed carry and generate an uncommitted expense/payable accrual proposal.

Cedar's **separate product-calendar branch** uses ledger CB-GENERAL/entity CB-A/account deposits_product_usd. The 35 supplied daily values are independently authored synthetic evidence, not derived from Ada's original correction/SQL chronology. No interest is included in a day's base unless externally posted and explicitly supplied; this simple-interest engine never capitalizes its own accrual.

Primary comparisons: CFPB Regulation DD section 1030.7 and Appendix A/official interpretation, current pages accessed 25 October 2026. US examples only. ACT/365 Fixed, UTC EOD, strict rate chronology, exact rational representation and posting rounding are selected Cedar conventions, not universal legal requirements or an APY/EIR engine. A negative eligible principal routes unsupported policy rather than claiming compliance with the US zero-floor treatment.

## Frozen API and exact schemas

Python `accrue_daily_simple(request)` / TypeScript `accrueDailySimple(request)`. Self-contained pure functions, no sibling runtime imports, no source authentication/database activity. Trusted host supplies complete balance receipts and version facts; strings do not authenticate themselves.

Request keys exactly:
- schema_version: d30-daily-simple-interest-v1.
- calculation_id: strict Stage 1 ASCII identifier.
- ledger_id, entity_id, account_id: strict identifiers; canonical product account above.
- currency: USD in this posting-proposal profile; other currencies route UNSUPPORTED_CURRENCY.
- period_start, period_end: genuine Gregorian YYYY-MM-DD dates, half-open [start,end),1–366 days.
- knowledge_cutoff: strict genuine UTC millisecond timestamp, at least period_endT00:00:00.000Z.
- day_count: ACT_365_FIXED only; 366-denominator comparison is separate.
- opening_carry: exact rational object below, bounded to |carry|≤1/2 minor and canonical reduced form.
- rate_versions: array of exact rate-version objects; at most 512.
- days: array of exact daily rows, complete ordered one row per period day.

Daily row keys exactly:
- date: YYYY-MM-DD.
- balance_minor: canonical signed integer string, at most 30 magnitude digits, including 0; no−0/plus/leading zero/decimal/exponent/numeric/bool/null.
- balance_available_at: strict UTC milliseconds, day close≤receipt≤knowledge_cutoff.
- balance_receipt_ref: strict identifier; source provenance rather than proof.
- rate_version_id: strict identifier referring to a supplied version.

Rate-version keys exactly:
- rate_version_id: strict identifier.
- rate_ppm: canonical nonnegative digit string,0..1000000 (selected Cedar 0–100% annual nominal profile, not a universal bound).
- effective_from: genuine YYYY-MM-DD.
- available_at: strict UTC millisecond timestamp.

Frozen causal policy: available_at≤effective_fromT00. A version announced after its own effective date routes RETROACTIVE_RATE_POLICY_UNSUPPORTED rather than being silently applied later. At each day choose the latest effective_from≤day among versions available by day start; the row must reference that latest applicable version. Equal effective dates under different IDs reject AMBIGUOUS_RATE_SCHEDULE. This prevents a supplied stale row reference from bypassing a known effective rate change. Rate list order is not semantic; daily order is semantic and never silently sorted.

Duplicate version ID/exact payload rejects DUPLICATE_RATE_VERSION; changed immutable facts under one ID reject RATE_VERSION_CONFLICT. A later unneeded future-effective known version does not change earlier days. Source data must contain the schedule relevant to the entire period; full calendar receipts/schedule completeness are trusted adapter obligations.

Rational keys exactly numerator,denominator. Both integer strings: numerator signed canonical; denominator positive canonical,≤30 digits. gcd(abs(numerator),denominator)=1, zero represented 0/1. opening_carry numerator≤30 magnitude digits and abs(2*numerator)≤denominator. Output fractions are always reduced and string-valued. Input bounds and 366 days keep exact arithmetic bounded; no floating-point totals.

## Frozen output contract

Always keys status,code,accrual,trace,posting_proposal. Rejected/unsupported input returns status rejected, typed code, accrual:null, trace:[], posting_proposal:null; no apparently complete amount from missing/invalid evidence.

Success status ok, code INTEREST_ACCRUED:
- accrual: ledger_id/entity_id/account_id/currency/period_start/period_end/day_count/day_count_value/balance_sum_minor/opening_carry/earned_exact/settlement_exact/rounded_amount_minor/closing_carry/knowledge_cutoff.
- day_count_value is the actual number of calendar days as a canonical string; the fixed 365 denominator is identified by day_count.
- earned_exact: Σ(B[d]*rate_ppm[d]/365000000), excluding incoming carry.
- settlement_exact: earned_exact+opening_carry.
- rounded_amount_minor: half-even(settlement_exact), canonical nonnegative string.
- closing_carry: settlement_exact−rounded_amount_minor; signed, potentially negative after rounding up.
- trace: per date, balance_minor/balance_receipt_ref/balance_available_at/rate_version_id/rate_ppm/exact_daily_interest/cumulative_earned_exact.
- posting_proposal: a calculation record, not a Stage 1 accepted journal. Exact keys schema_version(d30-interest-accrual-proposal-v1),calculation_id,ledger_id,entity_id,account_id,currency,period_start,period_end,knowledge_cutoff,status,amount_minor,legs.
- status ready: positive amount≤10^30−1; legs exactly Dr interest_expense_usd/Cr interest_payable_usd, same amount.
- status zero: amount 0, legs[]; never create zero-value posting legs.
- status amount-limit: exact calculation remains inspectable, amount exceeds Stage 1 positive leg bound, legs[] and no posting-ready claim. The exact calculation remains status ok; the blocked proposal explicitly carries no posting-ready claim.

Host must assign stable journal/business identity, effective/booking/recording context and validate the proposed legs through Stage 1. calculation_id alone does not establish exactly-once durable accrual. Different evidence/K can change a recomputation; actual posted prior accrual requires a correction, not a second expense.

Later customer credit is Dr interest_payable_usd/Cr deposits_product_usd in this product branch; it does not debit expense again. Customer crediting and posting eligibility are outside this calculation. 

## Independent expectations

The independent oracle in examples/independent-oracle.json is computed from explicit weighted arithmetic/Fraction, not reference algorithm output.

- 35-day full branch: balance sum 3222400; 50000 ppm earned 32224/73; rounded 441; carry 31/73.
- October 31 days: sum 2920000; exact 400; rounded 400; carry 0.
- 20–28 October focus 9 days: sum 793200; earned 7932/73; rounded 109; carry −25/73.
- Focus rate change last 5 days 60000 ppm: earned 8728/73; rounded 120; carry −32/73.
- Same ending balance 75600 every day of focus: earned 6804/73; rounded 93; carry 15/73.
- Two days 3650 at 50000 ppm: each 1/2; total 1; rounded 1; carry 0. Per-day whole-minor half-even would incorrectly pay 0.
- One 10950 day: earned 3/2; rounded 2; carry −1/2.
- Negative incoming carry −1/2 plus new earned 1/2: settlement 0; rounded 0; carry 0. Negative carry is not negative principal.
- Leap 2028 Feb 28–Mar 1: three 10000 days at 36500 ppm earn 3 under ACT/365F; denominator 366 variant earns 365/122 before rounding.

Acceptance must include missing/duplicate/reordered/no-transaction/leap days, end exclusion, future/unavailable/stale/conflicting versions, same-close different paths, signed carry boundaries, exact half-even ties, wrong currency/day count/scope, huge integers, proposal amount bound, empty/zero legs and unchanged inputs; full-output Python/TypeScript parity and responsive same-kernel lab.

## Implementation limits and trust

The profile uses 0..1000000 ppm cap; strict rate announcement≤effective-day start; latest applicable version selection; 512-version cap; K≥period-end midnight; incoming canonical carry≤half minor; exact-calculation-success with amount-limit/legs[] proposal above the Stage 1 bound. Source provenance/scope ownership is a trusted host obligation: validate identifier syntax and preserve declared scope without claiming JSON authenticates ownership. USD proposal account names are pinned; branch customer credit uses deposits_product_usd.

