Skip to content

MOD-161 — Transfer pricing

System: SD01 Core Banking · Repo: bank-core Status: Deployed (dev) Canonical slug: transfer-pricing (per bank-wiki entity) Module directory: MOD-161-transfer-pricing/ Origin handoff: docs/handoffs/processed/2026-05-07/MOD-086-bank-core-treasury-tp-rates-migration.handoff.md


1. Purpose

Provision the SD01 treasury schema and treasury.tp_rates table so the MOD-086 (Funds Transfer Pricing) write-back Lambda in bank-risk-platform can land its daily 18-row TP rate grid (9 tenor buckets × 2 jurisdictions). Per ADR-046 repo-domain rules, bank-risk-platform must not provision SD01 tables — that responsibility sits with bank-core.

This module ships the bank-core slice of Transfer Pricing: the schema provisioning + a thin read endpoint over treasury.tp_rates. The upstream rate-curve computation lives in bank-risk-platform / MOD-086; this module is the SD01-side persistence + query surface.

2. Architecture

HTTP GET /internal/v1/treasury/tp-rates/latest?jurisdiction=NZ|AU
  → TreasuryGetLatestRatesHandler

1 Lambda, read-only. Backed by treasury.tp_rates. Returns the 9 tenor buckets for the latest rate_date for the requested jurisdiction. Used by smoke tests + ad-hoc internal callers; SD05 credit decisioning + SD01 product rate config can use this instead of going to Snowflake or doing direct schema reads.

3. Data model

treasury.tp_rates (V001)

Implemented exactly per the SD01 wiki spec (source/pages/design/system/data-models/SD01-core-banking.md, § Schema: treasury):

Column Type Constraints
id uuid PK DEFAULT gen_random_uuid()
rate_date date NOT NULL
tenor_bucket text NOT NULL CHECK (∈ {ON, 1M, 3M, 6M, 1Y, 2Y, 3Y, 5Y, 10Y})
tp_rate_bps int NOT NULL
base_curve_bps int NOT NULL
liquidity_premium_bps int NOT NULL
curve_source_version text NOT NULL
jurisdiction char(2) NOT NULL CHECK (∈ {NZ, AU})
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_tp_rates_date_tenor UNIQUE on (rate_date DESC, tenor_bucket, jurisdiction) — supports MOD-086's ON CONFLICT (rate_date, tenor_bucket, jurisdiction) upsert path - idx_tp_rates_current on (rate_date DESC) — supports latest-day reads

Privileges (V002)

GRANT USAGE ON SCHEMA treasury TO bank_core_app_user, bank_core_readonly;
GRANT SELECT ON treasury.tp_rates TO bank_core_app_user, bank_core_readonly;
GRANT SELECT, INSERT, UPDATE ON treasury.tp_rates TO tp_writeback_user;

The tp_writeback_user role is the identity behind the bank-risk-platform/{env}/sd01-tp-writeback secret (provisioned by MOD-104 / bank-platform secret-bootstrap, per the MOD-086 design doc).

4. State-vs-naming gap (compliance debt)

The module was originally built under the directory name treasury-bootstrap/ (a placeholder slug from the MOD-086 handoff) before the canonical MOD-161 / Transfer Pricing identity was adopted. To avoid breaking the deployed dev state, the following internal identifiers were deliberately NOT renamed:

Surface Value (preserved)
SST app name bank-core-treasury-bootstrap
AWS resource tag module_id treasury-bootstrap
Flyway history table flyway_schema_history_treasury_bootstrap
Flyway schema treasury (correct — schema name, not slug)
Postgres schema name treasury (correct — schema name, not slug)
SSM output paths /bank/{stage}/treasury-bootstrap/*
Logger module_id field treasury-bootstrap (observability)

Renaming any of these requires a coordinated cutover: existing SST stack destroy + re-create under the new app name, flyway schema-history table rename + migration replay verification, SSM path migration with consumer notification (MOD-086 in bank-risk-platform reads from these paths). This is compliance debt to be retired in a follow-up release window — not a hot-path rename.

What WAS renamed (cosmetic, no deployed state): - Directory: treasury-bootstrap/MOD-161-transfer-pricing/ - Package: @bank-core/treasury-bootstrap@bank-core/mod-161-transfer-pricing - pnpm workspace entry (now picked up by the MOD-* glob) - Design doc filename: docs/design/treasury-bootstrap.mddocs/design/MOD-161.md - GitLab CI file: new .gitlab/ci/mod-161.gitlab-ci.yml (MODULE_DIR points to MOD-161-transfer-pricing)

5. SSM outputs

Path Value
/bank/{stage}/treasury-bootstrap/api/base-url API GW base URL
/bank/{stage}/treasury-bootstrap/lambdas/get-latest-rates/arn Lambda ARN
/bank/{stage}/treasury-bootstrap/tables/tp-rates/name treasury.tp_rates

(Path prefix preserved deliberately — see §4. Downstream consumers including MOD-086 already wire to this prefix.)

SSM inputs

Path Source
/bank/{stage}/iam/lambda/bank-core/arn MOD-104
/bank/{stage}/observability/adot-nodejs-layer-arn MOD-104
/bank/{stage}/neon/direct-host MOD-103 (flyway)

6. Test approach + results

Tier Files Local result
Unit tests/unit/{errors,tp-rate-validator-pure}.test.ts 9 / 9
Contract tests/contract/treasury-types.test.ts 3 / 3
Integration tests/integration/tp-rates-table-shape.test.ts (table shape + CHECK constraints) runs in CI

Local total: 12 / 12 unit + contract.

7. Known follow-ups

  • Retire the state-vs-naming gap (§4) — coordinate a maintenance window to rename SST stack, flyway history table, SSM paths, and observability module_id field. Notify MOD-086 owner (bank-risk-platform) before SSM path cutover.
  • tp_writeback_user role provisioning — assumed to exist; bank-platform / MOD-104 owns the role definition. If the GRANT fails on first deploy, that's the signal to ship the role separately.
  • accounts.interest_rates.tp_rate_bps writeback — the MOD-005 / MOD-112 / MOD-006 product-rate config layer reads from this table to set the lending-product tp_rate_bps snapshot. v1 doesn't trigger that path; it's MOD-086's downstream consumer.