Skip to content

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