Technical design — MOD-127 Product configuration panel
Module: MOD-127 — Product configuration panel
System: SD08 — Customer App & Back Office Platform
Repo: bank-app
Module type: Hybrid (back-office UI extension into MOD-069 + 6 BFF Lambdas + 1 scheduled Lambda + 1 cross-bus consumer + Postgres migrations + @bank/product-config workspace library)
FR scope: FR-573, FR-574, FR-575, FR-576
NFR scope: NFR-009 (component library divergence = 0 — UI extension reuses @bank/ui-shell)
Policies satisfied: GOV-006 (LOG), CON-005 (GATE), REP-002 (AUTO), GOV-007 (GATE)
Capabilities: none directly (composition of existing CAP- entries)
Author: AI agent (Claude Opus 4.7)
Date: 2026-05-08
Dependencies (all Deployed): MOD-063 (notification orchestration, bank-platform), MOD-047 (agent action audit, bank-platform), MOD-050 (disclosure enforcement, bank-app), MOD-110 (fee engine, bank-core, optional). Implicit: MOD-068, MOD-052, MOD-069, MOD-103, MOD-104.
Objective
Maker/checker-governed back-office panel for product managers and pricing analysts to change rates / fees / thresholds / feature flags. Every change:
- Requires a second approver distinct from the proposer (FR-573 / GOV-007 GATE — DB-level CHECK constraint).
- Triggers a notification dispatch via MOD-063 if it's an unfavourable change to customers (FR-574 / CON-005 GATE — application held until
notification_confirmed_at is set).
- Takes effect only on or after
effective_date via a scheduled apply-due-proposals job (FR-575 / REP-002 AUTO mechanism).
- Is logged immutably to MOD-047 via published events (FR-576 / GOV-006 LOG).
- Fans out to MOD-110 (fee engine), MOD-050 (disclosure), MOD-113 (statement gen, when built) via
bank.app.product_config_applied event on the bank-app bus.
The wiki design's "what configurable parameters" matrix lives in app.product_parameter_metadata (V004 seed) — 10 parameter keys at v1, jurisdiction-agnostic notice periods (v2 splits per-jurisdiction).
Architectural decisions (scope review, ratified 2026-05-08)
| AD |
Ruling |
| 1 |
Hybrid module type — back-office UI extension into MOD-069 + 6 BFF Lambdas + 1 scheduled Lambda + 1 cross-bus consumer + Postgres + @bank/product-config workspace library. NFR-009 stays 0 (UI reuses @bank/ui-shell) |
| 2 |
Schema strategy — keep MOD-050's app.product_configurations stub for gate-metadata; add three new tables (app.product_parameter_metadata startup seed; app.product_parameters EAV current-state; app.product_config_proposals workflow) |
| 3 |
Four-eyes DB CHECK — CONSTRAINT proposed_by_neq_reviewed_by CHECK (reviewed_by IS NULL OR reviewed_by <> proposed_by) |
| 4 |
Six BFF Lambdas + one scheduled. check-permission route uses authorizationType: 'AWS_IAM' |
| 5 |
Notification confirmation stop-gap — notification_confirmed_at = now() at proposal-approval (OPTIMISTIC_V1). Advance-notice window enforced via effective_date >= today + advance_notice_days. v2 path: MOD-063 ships bank.platform.notification_dispatched and the cross-bus consumer flips notification_method to MOD_063_CONFIRMED |
| 6 |
Daily scheduled apply at 14:00 UTC (~02:00 ap-southeast-2) |
| 7 |
Workspace library @bank/product-config with getCurrentParameter + listProductParameters |
| 8 |
No PRODUCT_ADMIN role — V005 seeds access.role_permissions for operations/senior (full), compliance (read-only), customer-facing (none). bank-app CLAUDE.md PRODUCT_ADMIN reference is a follow-up docs fix |
| 9 |
Idempotency via app.idempotency_keys (MOD-068 V006) namespaced mod127: |
| 10 |
Five outbound events on bank-app bus (proposed, reviewed, applied, superseded) + one cross-bus to bank-platform (change_proposed_unfavourable). MOD-002 listed as consumer of every bank.app.* event (platform standard) |
| 11 |
Two cross-bus IAM grants needed (both on bank-platform bus): PutEvents + PutRule. MOD-110 subscribes from its own side via the bank-core handoff |
| 12 |
"Unfavourable" classification metadata-driven; boolean DOWN_TO_FALSE handled in a separate code path from numeric UP/DOWN |
| 13 |
UI extension lives in MOD-069's _back-office/products slot — three new route files, no modifications to existing MOD-069 files |
| 14 |
Test surface — unit (≥ 80/80/75/80) + contract (5 events) + policy (4 — GOV-007, CON-005, REP-002, GOV-006) + integration (FR + infra) |
| 15 |
Four cross-team handoffs filed (MOD-063 dispatch event, MOD-104 cross-bus grants, MOD-050 library consumer, MOD-110 event subscription) + complete handoff to bank-wiki |
Stacks
MOD-127-product-configuration-panel/
├── package.json # @bank/product-config workspace package
├── tsconfig.json
├── vitest.config.ts
├── sst.config.ts # SST app: bank-app-mod-127
├── scripts/
│ ├── build-lambda.mjs # esbuild bundling per handler
│ └── flyway-runner.mjs # -table=flyway_schema_history_mod127
├── infra/
│ ├── audit-log.ts # /aws/bank-app/product-config-events-{env}
│ ├── functions.ts # 8 Lambdas
│ ├── api.ts # HTTP API v2 — 6 routes
│ ├── scheduled-rule.ts # daily 14:00 UTC apply-due-proposals
│ ├── cross-bus-subscription.ts # bank-platform bus rule for notification_dispatched
│ ├── ssm-outputs.ts # 12 SSM parameters
│ └── index.ts
├── src/
│ ├── lib/
│ │ ├── classify-unfavourable.ts ★ AD-12 binding
│ │ ├── get-current-parameter.ts ★ workspace library exports
│ │ ├── jwt-claims.ts
│ │ ├── idempotency.ts # mod127: namespace
│ │ ├── audit.ts # CW Logs best-effort writer
│ │ ├── errors.ts, logger.ts, trace.ts, db.ts
│ ├── services/
│ │ ├── proposal-service.ts # propose, review (FR-573, FR-574)
│ │ ├── apply-service.ts # daily effective-date gate (CON-005, REP-002)
│ │ ├── event-publisher.ts # 5 outbound events (AD-10)
│ │ └── notification-trigger.ts # cross-bus thin wrapper
│ ├── handlers/
│ │ ├── propose-change.ts POST /product-configs/proposals
│ │ ├── review-change.ts POST /product-configs/proposals/{id}/review
│ │ ├── list-proposals.ts GET /product-configs/proposals
│ │ ├── get-proposal.ts GET /product-configs/proposals/{id}
│ │ ├── get-current-config.ts GET /product-configs/current
│ │ ├── check-permission.ts POST /product-configs/check-permission (AWS_IAM)
│ │ ├── apply-due-proposals.ts cron daily 14:00 UTC
│ │ ├── consume-notification-dispatched.ts cross-bus from bank-platform
│ │ └── _shared.ts
│ └── index.ts # @bank/product-config exports
├── db/
│ ├── migrations/
│ │ ├── V001__app_product_parameter_metadata.sql
│ │ ├── V002__app_product_config_proposals.sql # incl. AD-3 CHECK + ADR-048 Cat 2 trigger
│ │ ├── V003__app_product_parameters.sql
│ │ ├── V004__seed_parameter_metadata.sql # 10 parameter keys
│ │ └── V005__role_permissions_seed.sql # MOD-052 grants
│ └── migrations-rollback/V001..V005.sql
└── tests/
├── unit/ # ≥ 80/75/80/80
├── contract/ # 5 events
├── policy/ # GOV-007, CON-005, REP-002, GOV-006
└── integration/
├── fr/
├── infra/ # SSM outputs
└── policy/ # live four-eyes + immutability
MOD-069-customer-app-shell/src/routes/_back-office/products/ # AD-13 extension
├── proposals.tsx
├── proposals.new.tsx
└── proposals.detail.tsx
Postgres schema (Flyway in db/migrations/)
Database: bank_app (Neon, ap-southeast-2). Per-module Flyway history table flyway_schema_history_mod127 keeps MOD-127 migrations independent of MOD-068/049/050/052/068.
| Migration |
Tables / objects |
V001__app_product_parameter_metadata.sql |
app.product_parameter_metadata (parameter_key PK; type / value_kind / unfavourable_direction / advance_notice_days / value_schema) + touch trigger |
V002__app_product_config_proposals.sql |
app.product_config_proposals with AD-3 four-eyes CHECK + ADR-048 Cat 2 trigger fn_product_config_proposals_immutable (rejects DELETE; enumerates allowed status transitions) |
V003__app_product_parameters.sql |
app.product_parameters (EAV current-state, PK (product_id, jurisdiction, parameter_key)) + touch trigger |
V004__seed_parameter_metadata.sql |
10 v1 parameter keys (rate., fee., threshold., feature.) — idempotent ON CONFLICT DO NOTHING |
V005__role_permissions_seed.sql |
MOD-052 role_permissions for app.product_config_proposals / app.product_parameters / app.product_parameter_metadata |
SSM outputs (consumer contract)
Path convention: /bank/{env}/mod127/{name}. 12 outputs:
| SSM path |
Value |
Consumed by |
/bank/{env}/mod127/product-config-api/url |
API base URL |
back-office UI (MOD-069 extension) |
/bank/{env}/mod127/product-config-api/id |
API ID |
observability |
/bank/{env}/mod127/{handler}/fn-arn (×8) |
Lambda ARNs |
observability + diagnostics |
/bank/{env}/mod127/product-config-events-log/group-{arn,name} |
CW Logs group |
MOD-076 dashboards |
Events
Published on bank-app bus
| Event |
DetailType |
Required fields |
bank-app.product_config_proposed |
bank-app.product_config_proposed |
trace_id, proposal_id, product_id, jurisdiction, parameter_key, proposed_by, change_kind, effective_date, notification_required |
bank-app.product_config_reviewed |
bank-app.product_config_reviewed |
trace_id, proposal_id, status, reviewed_by, review_comment, product_id, jurisdiction, parameter_key |
bank-app.product_config_applied |
bank-app.product_config_applied |
trace_id, proposal_id, product_id, jurisdiction, parameter_key, previous_value, current_value, applied_at |
bank-app.product_config_superseded |
bank-app.product_config_superseded |
trace_id, proposal_id, superseded_by, product_id, jurisdiction, parameter_key |
| Event |
DetailType |
Filter |
bank-app.product_config_change_proposed_unfavourable |
bank-app.product_config_change_proposed_unfavourable |
trace_id, proposal_id, advance_notice_days, effective_date, external_trigger.module = "MOD-127" |
| Event |
Source bus |
Filter |
Handler |
bank-platform.notification_dispatched |
bank-platform |
detail.external_trigger.module = "MOD-127" |
consume-notification-dispatched.ts |
Cross-bus IAM grants (filed via MOD-104-cross-bus-grants-mod127.handoff.md)
- BankAppRole → events:PutEvents on bank-platform bus (MOD-063 trigger)
- BankAppRole → events:PutRule + events:PutTargets on bank-platform bus (notification_dispatched inbound)
Policy satisfaction
| Policy |
Mode |
Mechanism |
Tests |
| GOV-007 |
GATE |
DB CHECK constraint proposed_by_neq_reviewed_by + handler short-circuit + ADR-048 Cat 2 trigger forbidding terminal-row edits |
tests/policy/gov-007-four-eyes.test.ts (static) + tests/integration/policy/four-eyes.test.ts (live UPDATE rejected by DB code 23514) |
| CON-005 |
GATE |
apply-due-proposals SELECT filters notification_required = false OR notification_confirmed_at IS NOT NULL; V002 live_requires_confirmation_for_unfavourable belt-and-braces CHECK |
tests/policy/con-005-disclosure-gate.test.ts (static SQL inspection) |
| REP-002 |
AUTO |
apply-service publishes bank.app.product_config_applied on every transition to live; downstream MOD-110/050/113 subscribe |
tests/policy/rep-002-event-published.test.ts |
| GOV-006 |
LOG |
app.product_config_proposals is append-only on terminal-state rows; ADR-048 Cat 2 trigger rejects DELETE + non-permitted status transitions |
tests/policy/gov-006-immutability.test.ts (static) + tests/integration/policy/immutability.test.ts (live DELETE rejected, rejected→live blocked) |
Operational notes
- Daily cron at 14:00 UTC. Effective-date gate is date-granular; sub-day precision not needed. Idempotent on same-day replay (status=live filter excludes already-applied rows).
- Audit hierarchy: the durable compliance record is the
app.product_config_proposals row (ADR-048 Cat 2; append-only on terminal states). CW Logs is secondary SIEM; MOD-047 is the cross-domain audit feed via published events. Page on Postgres insert failures, NOT on audit_log_cw_write_failed stderr.
- Cross-bus PutEvents to bank-platform (unfavourable-change MOD-063 trigger) fails AccessDenied to stderr until the MOD-104 grant lands. The Postgres proposal row is unaffected; the OPTIMISTIC_V1 method flag records the v1 stop-gap.
- MOD-063 dispatch-confirmed event is a v2 enhancement filed via handoff. Until shipped,
notification_method = OPTIMISTIC_V1 and notification_confirmed_at = now() (set at proposal approval). Advance notice is enforced via effective_date, not via a future timestamp.
- MOD-110 (fee engine) subscription is bank-core's responsibility per AD-11 ratification. v1 marks FR-575 partial-met until MOD-110 subscribes.
- UI extension lives in MOD-069's directory (additions only, no modifications). Routes are role-filtered via the existing MOD-069 sidebar machinery.
Cross-module handoffs filed alongside
MOD-127-complete.handoff.md — bank-wiki status transition
MOD-063-dispatch-confirmed-event.handoff.md — request bank.platform.notification_dispatched emission for the AD-5 v2 path
MOD-104-cross-bus-grants-mod127.handoff.md — 2 grants on bank-platform bus (PutEvents + PutRule)
MOD-050-product-config-library-consumer.handoff.md — switch disclosure-context lookup to @bank/product-config once MOD-127 ships
MOD-110-product-config-event-subscription.handoff.md — heads-up to subscribe to bank.app.product_config_applied
- Wiki spec:
bank-wiki/source/entities/modules/MOD-127.{yaml,md}
- Data model:
bank-wiki/source/pages/design/system/data-models/SD08-app.md
- Event catalogue:
bank-wiki/source/pages/design/system/event-catalogue.md (5 entries added)
- ADRs in effect: ADR-001, ADR-004, ADR-024, ADR-025, ADR-029, ADR-030, ADR-031, ADR-042, ADR-043, ADR-048, ADR-051, ADR-053