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¶
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.md → docs/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_idfield. Notify MOD-086 owner (bank-risk-platform) before SSM path cutover. tp_writeback_userrole 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_bpswriteback — the MOD-005 / MOD-112 / MOD-006 product-rate config layer reads from this table to set the lending-producttp_rate_bpssnapshot. v1 doesn't trigger that path; it's MOD-086's downstream consumer.