# Wave 1 — Morning Payments — Verification Record

**Date:** 2026-06-09
**Wave:** Epic A core, Wave 1 (foundation)
**Commits under test:** 3821352, 1048352, b464454, 9307462 (fix)

## Result: GREEN — Wave 1 CLOSED (advisor-clean 2026-06-09)

### Compile + tests (cursor-run, IRON-LAW: implementer verifies)
- `@zync/payments` typecheck: **pass**
- `@zync/payments` tests: **pass** — adapter.test.ts (2) + config-crypto.test.ts (3) = 5/5
- Wave-1 regression found + fixed: `getPaymentGatewayConfigMasked` returned `row.active` after the
  select alias was renamed to `isActive` (TS2551) → fixed `row.isActive`; doc comment
  `encrypted_config`→`config_encrypted`. Commit **9307462**.
- Repo-wide `pnpm -w typecheck`: **red, but pre-existing & unrelated** — `@zync/db` query files
  (`expense-invoice.ts`, `expenses.ts`, `expense-ocr-corrections.ts`, `invoice-adapters.ts`)
  reference `Buffer`/`node:crypto` while their tsconfig carries only `@cloudflare/workers-types`,
  and `zync-api/.../cron/dispatch-group.ts` (`Property 'group'`). None are in Wave 1's diff
  (verified `git diff --name-only b464454~3 HEAD`). Not a Wave-1 regression.

### Migration 0046 — applied + verified on isolated Neon branch
Project `spring-smoke-45518495` (zync-ddl-verify), throwaway branch `br-odd-bonus-a6soryy3`,
isolated schema `z` seeded with pre-0046 shape (old `encrypted_config`/`is_active` cols, old
`tenant_unique`, old gateway CHECK without `morning`) + a pre-existing `payplus` row:

- All 25 statements apply clean over pre-0046 data.
- Rename preserved data: seeded `payplus` row intact, `config_encrypted='OLDCIPHERTEXT'`, `active=true`.
- gateway CHECK **accepts** `morning`, **rejects** `bogus`.
- `UNIQUE(gateway, session_id)` **enforced** (duplicate `morning/SESS-1` rejected) — the idempotency cornerstone.
- Token backfill: 2 tenants → 2 distinct, non-null, 64-char hex tokens.
- Partial index `idx_payment_gateway_configs_tenant ... WHERE active=true` + unique partial
  `idx_tenants_payment_webhook_token ... WHERE payment_webhook_token IS NOT NULL` both present.
- pgcrypto dependency of the backfill (`gen_random_bytes`) is satisfied in prod — migration
  0003 line 195 runs `CREATE EXTENSION IF NOT EXISTS pgcrypto;` before 0046.
- Concern #4 (existing rows violate new CHECK) is logically impossible: new allowed set ⊇ old set.

### FROM-state representativeness — proven against real prior migration (advisor blind-spot closed)
The synthetic schema-`z` test built its pre-0046 table using the same column/constraint names
0046 assumes — so it could not, by itself, prove 0046's two UNGUARDED ops are safe against the
REAL prior schema. Verified directly against `packages/db/migrations/0010_wave10_consolidated.sql`
(the migration that CREATEs `payment_gateway_configs`; no intervening migration renames these):
- `encrypted_config` text + `is_active` boolean → exactly the RENAME sources in 0046:4,6 (the
  renames are NOT `IF EXISTS`; a name mismatch would HARD-FAIL in prod — confirmed match).
- `payment_gateway_configs_tenant_unique` UNIQUE(tenant_id) → exactly the `DROP CONSTRAINT IF EXISTS`
  target in 0046:8 (this one IS `IF EXISTS`; a name mismatch would SILENTLY no-op and leave a stale
  `UNIQUE(tenant_id)`, blocking a 2nd gateway per tenant — confirmed match, no stale constraint).
- `payment_gateway_configs_gateway_check` → matches 0046:12 `DROP IF EXISTS` target.
Synthetic FROM-state was representative → Wave 1 migration safe on real data.

### Schema-drift fixer queued (not blocking the DDL apply, but must fix before next drizzle generate)
`packages/db/src/schema/payment-gateways.ts:50` declares `idx_payment_gateway_configs_tenant`
as a **full** index (`index(...).on(t.tenantId)`), but migration 0046 creates it **partial**
(`WHERE active = true`). Next `drizzle-kit generate` will see drift and try to recreate it full,
dropping the partial. Fix: add `.where(sql\`${t.active} = true\`)` to the Drizzle index def so
schema matches the migration (spec intent = index only active configs).

### Schema-drift fixer — LANDED (commit 80b634b)
`packages/db/src/schema/payment-gateways.ts` index now partial:
`index('idx_payment_gateway_configs_tenant').on(t.tenantId).where(eq(t.active, true))` (matches
repo pattern in `user-2fa-backup-codes.ts`). Commit is schema-only (2 lines), no scope creep.
`9307462` diff eyeballed: exactly `row.active`→`row.isActive` + doc comment; TS2551 absent on
`payment-gateways.ts` in `@zync/db` typecheck.

**Drift proof note:** the advisor-requested "empty `drizzle-kit generate`" gate is unreliable in
THIS repo — see pre-existing gap B below. Drift was instead proven (a) by direct DB apply of 0046
verbatim against real Postgres (all constraints/indexes/CHECK/UNIQUE correct), and (b) by textual
match of schema.ts's index predicate to migration 0046. Both stronger than drizzle snapshot diff.

## Two PRE-EXISTING infra gaps surfaced (not Wave-1 regressions; fix before Wave 4)
Wave 4/6 wire payments into `zync-api`, which depends on `@zync/db`. Verifying changes inside a
package that's already red/ungated forces "absence-of-specific-error" grepping (fragile). Land both
before Wave 4; neither blocks Wave 2 (`packages/payments` is clean).

- **Gap A — `@zync/db` typecheck RED.** `expense-invoice.ts`, `expenses.ts`,
  `expense-ocr-corrections.ts`, `invoice-adapters.ts` reference `Buffer`/`node:crypto` but the
  tsconfig carries only `@cloudflare/workers-types`. On the Workers runtime these may be *real*
  runtime bugs (no Node `Buffer`/`crypto`), not just missing types — warrants a read-only audit,
  not a blind `@types/node` patch. `zync-api/.../cron/dispatch-group.ts` also red (`Property 'group'`).
- **Gap B — drizzle migration snapshots missing since 0033.** 47 journal entries, only 27 committed
  snapshots (highest `0032_snapshot.json`); 0033–0046 absent on master. `drizzle-kit generate` can't
  rebuild a baseline → hangs on rename prompts / emits spurious migrations. Rebuilding the
  snapshot chain (0033→0046) is a separate infra task; until then, migration correctness is verified
  by direct DB apply, not generate-diff.

### Open
- Throwaway branch `br-odd-bonus-a6soryy3` (schema `z`) left on scratch project `spring-smoke-45518495`
  (0.25 CU autosuspend) — delete on request.
- Live Morning sandbox e2e + correlation-handle check still deferred (no creds).
