Skip to content

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:

  1. Requires a second approver distinct from the proposer (FR-573 / GOV-007 GATE — DB-level CHECK constraint).
  2. 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).
  3. Takes effect only on or after effective_date via a scheduled apply-due-proposals job (FR-575 / REP-002 AUTO mechanism).
  4. Is logged immutably to MOD-047 via published events (FR-576 / GOV-006 LOG).
  5. 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

Cross-bus to bank-platform bus

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"

Subscribed from bank-platform bus (AD-5 v2 path)

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)

  1. BankAppRole → events:PutEvents on bank-platform bus (MOD-063 trigger)
  2. 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