Fee engine¶
| ID | MOD-110 |
| System | SD01 |
| Repo | bank-core |
| Build status | Deployed |
| Deployed | Yes |
| Last commit | 35402a8a7d9c6f1e2b5c8d0e4f7a3b6c9d2e5f8a |
What it does¶
MOD-110 evaluates, assesses, waives, and posts fees for all products on the platform. It is the single point of truth for fee logic — no module posts fees directly to MOD-001; all fee postings flow through MOD-110. This ensures consistent fee audit trails, correct waiver evaluation, and compliant advance notification before any new fee type or rate change takes effect.
In a SaaS context, the fee schedule is tenant-configurable: each institution on the platform defines its own fee types, amounts, waiver conditions, and notice periods via the fee schedule configuration table. MOD-110 evaluates those rules; it does not hardcode any amounts.
Fee types supported¶
Monthly account fee, transaction fee (per debit/credit above threshold), dishonour/return fee (for failed direct debits or returned payments), overlimit fee, late payment fee (for credit products), break cost fee (for term deposits and fixed-rate loans — computed by MOD-111 and MOD-112 respectively, posted by MOD-110), early repayment fee, paper statement fee.
Waiver conditions¶
Each fee type supports configurable waiver conditions evaluated at assessment time: zero balance (do not charge a fee against an account that has no funds), negative balance (account already in debit), recently opened (waive for first N months post-account opening), waiver flag on the account record (one-time or standing waiver applied by an agent), promotional period (product-level promotional window). The waiver evaluation result is recorded in posting metadata whether or not a waiver is applied, enabling audit reconstruction without replaying business logic.
Staff/employee rate waiver and promotional waiver codes via agent deal (MOD-109) are deferred to v2.
Advance notice gate¶
When a fee schedule change is published (new fee type or rate increase), the engine computes the operative effective date as MAX(proposed_effective_from, published_at + notice_days). notice_days defaults to 14 and is subject to a minimum of 14 for all retail products (CON-005 floor — not a configurable option). The GATE prevents posting of any fee under the new schedule until the computed effective date is reached. Fee reductions take effect at proposed_effective_from immediately, without a notice gate. Same-rate re-publications still write a new schedule row with updated metadata for the audit trail; no gate is applied. The response for any assessed fee that has been gated to a future date must surface the effective_from date in the response body.
Reversal¶
Any fee may be reversed by an authorised agent via MOD-083. In v1, reversals are record-only — no approval tier is validated. The reversal is posted as a compensating credit entry via MOD-001 and logged to the fee audit trail with the authorising staff_id and reason. Approval tier gating (configurable thresholds per fee type and amount) is deferred to the MOD-109 integration in v2.
Schedule administration¶
v1: fee schedules are seeded via the V005 migration (psql) only. No admin API ships in v1. When MOD-006 (product catalogue) delivers its admin API, MOD-110 will subscribe to the bank.core.fee_schedule_published event to apply changes at runtime.
Cron-driven fees¶
Monthly recurring fees (account maintenance, card fees) are out of scope for v1. MOD-110 is an HTTP-triggered service only in v1. Recurring fee scheduling is deferred until the use case is confirmed by product and a reliable cron + idempotency mechanism is in place.
Idempotency¶
All fee assessment requests must supply a caller-generated idempotency key. The key is stored under a UNIQUE constraint on core.fee_events. On duplicate key, the engine returns 200 with the original response body — not 409. Callers (event handlers, API gateway integrations) are responsible for generating and durably storing the key before calling the engine.
Currency validation¶
If the account's currency does not match the fee schedule's currency, the engine returns CURRENCY_MISMATCH 422. This is a non-retryable client error validated in the pre-flight block, before any posting attempt.
Data model¶
-- core.fee_schedule (Postgres — tenant-configurable)
CREATE TABLE core.fee_schedule (
schedule_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id text NOT NULL,
product_id text NOT NULL,
fee_type text NOT NULL,
amount numeric(18,2) NOT NULL,
currency text NOT NULL,
waiver_conditions jsonb, -- array of condition objects
notice_days int NOT NULL DEFAULT 14,
effective_from date NOT NULL,
effective_to date,
version int NOT NULL,
published_at timestamptz NOT NULL DEFAULT now()
);
-- core.fee_events (append-only)
CREATE TABLE core.fee_events (
event_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
account_id uuid NOT NULL,
party_id uuid NOT NULL,
fee_type text NOT NULL,
assessed_amount numeric(18,2) NOT NULL,
posted_amount numeric(18,2), -- null if waived
waived boolean NOT NULL DEFAULT false,
waiver_reason text,
waiver_check jsonb, -- {"evaluated": [...], "applied": "condition_name" | null}
schedule_version int NOT NULL,
posting_id uuid, -- references core.postings(id) if posted
reversal_of uuid REFERENCES core.fee_events(event_id),
jurisdiction text NOT NULL,
idempotency_key text NOT NULL,
assessed_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (idempotency_key)
);
Module dependencies¶
Depends on¶
| Module | Title | Required? | Contract | Reason |
|---|---|---|---|---|
| MOD-001 | Double-entry posting engine | Required | — | Fee posting is executed as a double-entry ledger entry through the posting engine. |
| MOD-003 | Real-time balance engine | Required | — | Balance checks for fee waiver conditions (minimum balance threshold) require the real-time balance engine. |
| MOD-006 | Rate change propagation | Optional | — | v1 loads fee schedules via direct SQL seed (V005 migration) only. When MOD-006 ships, MOD-110 will subscribe to the fee_schedule_published event to apply admin-initiated schedule changes without a migration. |
| MOD-104 | AWS shared infrastructure bootstrap | Required | — | MOD-104 provisions the S3 Iceberg bucket (Snowflake external tables), KMS key, and bank-core EventBridge bus ARN. Required before this module can be deployed. |
| MOD-103 | Neon database platform bootstrap | Required | — | Neon database provisioned by MOD-103 must exist before this module can read or write Postgres. |
Required by¶
| Module | Title | As | Contract |
|---|---|---|---|
| MOD-111 | Term deposit maturity engine | Hard dependency | — |
| MOD-112 | Amortisation schedule engine | Hard dependency | — |
| MOD-113 | Statement generation | Hard dependency | — |
| MOD-114 | Direct debit mandate management | Hard dependency | — |
| MOD-117 | Overdraft management engine | Optional enhancement | — |
| MOD-127 | Product configuration panel | Optional enhancement | — |
Policies satisfied¶
| Policy | Title | Mode | How |
|---|---|---|---|
| CON-005 | Fee & Pricing Transparency Policy | GATE |
Fee posting is blocked if the required advance notice period has not elapsed since the fee schedule was last changed — enforcing the notification-before-deduction obligation. |
| CON-004 | Product Disclosure & Sales Practice Policy | LOG |
Every fee event (assessment, waiver, posting, reversal) is logged immutably with the fee type, amount, waiver reason if applicable, and applicable fee schedule version. |
| PAY-001 | Payment Operations Policy | AUTO |
Fees are posted as double-entry ledger entries via MOD-001 — fee debit from the customer account, credit to the bank's fee income GL account — in a single atomic transaction. |
Capabilities satisfied¶
| Capability | Title | Mode | How |
|---|---|---|---|
| CAP-041 | No monthly account fee | AUTO |
Fee schedule configuration sets the monthly account maintenance fee to zero — the engine assesses no fee for standard account maintenance events, satisfying the no-monthly-fee product promise. |
Part of SD01 — Core Banking Platform
Compiled 2026-05-22 from source/entities/modules/MOD-110.yaml