Skip to content

MOD-158 — Test seed data loader

Purpose

MOD-158 loads deterministic seed data into the dev and UAT Neon branches on first deploy to a fresh stage. Customers, accounts, and the GL chart are cross-domain entities that no single per-domain module owns; centralising their seeding here gives every integration test a known baseline without each team owning fixture-data plumbing.

FR scope: requirements: [] per the wiki. Tooling module. Tests map to the spec's documented behaviour, not FR-NNN.

Architectural decisions: ADR-045.

Architecture

sst deploy
  ├── creates seed-loader Lambda + IAM role + log group
  └── creates aws.lambda.Invocation (Custom Resource)
        │   triggers map carries SEED_VERSION + stage
      synchronous Lambda invocation
        ├── reads /bank-platform/{stage}/seed-version from SSM
        ├── decideReload(recorded, SEED_VERSION):
        │     null     → truncate-and-reload
        │     same     → skip (no-op)
        │     +patch   → truncate-and-reload
        │     +minor   → additive (ON CONFLICT DO NOTHING)
        │     +major   → truncate-and-reload
        ├── if not skip:
        │     fetch bank-neon/{branch}/bank_core/migrate_user
        │     apply schema DDL (CREATE IF NOT EXISTS)
        │     INSERT customers / accounts / gl_accounts
        │     write SEED_VERSION to SSM
        └── return { action, counts, duration_ms }

Deployment scope

dev and uat ONLY. sst.config.ts short-circuits when stage === 'prod'. Single source of truth: src/config/stages.ts (SEED_DEPLOY_STAGES = ['dev', 'uat']). Unit-tested in __tests__/unit/stages-prod-skip.test.ts.

Stage What happens
dev Lambda + Custom Resource provisioned; loads dev profile (10 customers, 13 accounts)
uat Same; loads uat profile (103 customers, 200+ accounts)
prod No resources. No Lambda, no Custom Resource, no SSM seed-version key

Versioning (SEED_VERSION constant)

src/config/seed-version.ts exports SEED_VERSION = "1.0.0".

Bump Effect
Patch (1.0.0 → 1.0.1) truncate seed tables, reload from scratch
Minor (1.0.0 → 1.1.0) additive — new rows appended via ON CONFLICT DO NOTHING
Major (1.0.0 → 2.0.0) truncate + reload

Pulumi's aws.lambda.Invocation triggers on SEED_VERSION change, so a constant bump auto-triggers reload on the next sst deploy. The Lambda is also internally idempotent so manual invocations or re-deploys without bumps are safe no-ops.

Seed profile contract

Both profiles are pure data definitions in src/profiles/:

  • dev (dev.ts): 10 customers (CUST-D001..D010), 13 accounts, 11 GL codes. Mirrors bank-wiki/source/pages/operations/seed-data.md exactly. CUST-D009 has kyc_status=Rejected; CUST-D010 has sanctions_status=Hit.
  • uat (uat.ts): superset of dev — 10 dev customers passthrough
  • 90 deterministically-generated customers (CUST-U001..U090)
  • 3 edge-case rows (Pending / Dormant / Restricted). 90 generated customers come from SHA256(seed20260427:ix:salt) → uint32 for every random choice (jurisdiction, name, DOB, account types, balances). Same SEED_VERSION → identical rows on every load.

eIDV doc refs use the prefixes MOD-157 stubs match on: PASS-*, FAIL-*, REFER-*. Seeded customers are designed to trigger known stub outcomes — bridging MOD-158 (data) with MOD-157 (provider behaviour) gives integration tests deterministic input on both sides.

Schema MOD-158 owns

Two schemas in the bank_core Neon database:

  • customers.customers — customer_id PK, full_name, jurisdiction, date_of_birth, eidv_doc_ref, kyc_status, sanctions_status, seeded_by_mod_158 boolean flag, created_at.
  • accounts.accounts — account_id PK, customer_id, account_type, currency, balance (numeric(20,2)), state, seeded_by_mod_158, created_at.
  • accounts.gl_accounts — gl_code PK, account_name, account_class, currency, seeded_by_mod_158.

Per-domain modules (MOD-007 account state machine, MOD-009 KYC, etc.) ALTER these tables to add columns they own. The seeded_by_mod_158 flag scopes truncate-on-reset to seed-owned rows only.

Reset

pnpm run seed:reset --stage dev
pnpm run seed:reset --stage uat --confirm

UAT requires --confirm because the reset truncates seed tables AND deletes accumulated synthetic transaction history (MOD-159's output into the same tables). The CLI:

  1. Resolves the Lambda function name from /bank/{stage}/mod158/seed-loader/fn-name.
  2. Invokes synchronously with {trigger: "reset", reset: true}.
  3. Lambda deletes the SSM version key, runs full truncate-and-reload, re-writes the version key, returns {action: "reset", counts}.

SSM outputs

Module-internal

Path Value Consumed by
/bank/{stage}/mod158/seed-loader/fn-name Lambda name seed-reset CLI
/bank/{stage}/mod158/seed-loader/fn-arn Lambda ARN Ops
/bank/{stage}/mod158/seed-loader/log-group-arn Log group MOD-076 dashboards

Cross-module contract (the FR-734 SSM-verifiable key)

Path Value Owner
/bank-platform/{stage}/seed-version Current loaded SEED_VERSION (e.g. 1.0.0) Lambda writes; orchestrator reads to verify

The version key is the authoritative record of which seed profile is loaded. Per ADR-045, never manually insert seed-table rows without bumping this key.

Dependencies

  • MOD-103 — Neon branches (dev, uat) and per-database role secrets (bank-neon/{branch}/bank_core/migrate_user).
  • MOD-043 — EventBridge schema registry. (MOD-158 doesn't use it directly today; reserved for future schema-validated event emission of seed-customer changes.)
  • MOD-045 — Secrets Manager infrastructure (the secret values come from MOD-103 but MOD-045 owns the encryption keys).
  • MOD-157 — provider stubs. Seed eidv_doc_ref values (PASS-NZ-D001 etc.) trigger known stub outcomes; MOD-157 must be deployed for end-to-end test flows to behave deterministically.

Constraints

  • No prod deploy. Three layers: stages allow-list, sst.config short-circuit, workflow inputs.stage.options excludes prod.
  • bank_core database only. Seed customers and accounts are cross-domain banking entities that live in bank_core. If a future profile needs to seed bank_kyc-specific data (e.g. KYC check records), extend the loader to multi-database — for now, per-domain seed data is each domain module's responsibility.
  • No FK constraints across schemas. MOD-158 doesn't enforce customer→account integrity at the SQL level so per-domain modules can ALTER the tables independently without ordering constraints. The loader inserts customers before accounts in transaction order; the data layer's referential integrity is application-level.
  • seeded_by_mod_158 flag preserved. Per-domain modules that ALTER these tables must NOT drop the flag column — it's how reset identifies which rows to truncate.