# Wave 2 — Morning Adapter — Verification Record

**Date:** 2026-06-09
**Wave:** Epic A core, Wave 2 (MorningAdapter + gateway registry)
**Commits under test:** 94da5b7 (adapter + registry + stubs + tests), ada0702 (fixer — remove fabricated client.name)

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

### Build (cursor-run, IRON-LAW: implementer verifies)
- `@zync/payments` typecheck: **pass**
- `@zync/payments` tests: **pass** — 9/9, incl. the acceptance test
  `verifyWebhook re-fetch proof: lying raw body "paid" but fetched status "pending" → payment.pending`
  (proves the raw webhook body's claimed status is NEVER trusted — authenticated re-fetch governs).
- `src/registry.ts` (PLATFORM-billing / Epic B) confirmed **untouched** — the two registries stay
  in separate modules. Epic-A gateway registry is the new `src/adapters/registry.ts`
  (`getAdapter(gateway)`, `SELECTABLE_GATEWAYS=['morning']`).

### What was built (to documented behavior, with real source citations)
- JWT auth: `POST /account/token` body `{id: apiKey, secret}` → bearer cached to expiry
  (module-level `tokenCache` keyed `${sandbox|prod}:${apiKey}:${secret}` — no cross-tenant leak).
- `createPaymentSession` → `POST /payments/form` → `{sessionId: id, redirectUrl: url}`;
  correlation carried back via `successUrl?zyncInvoiceId=<invoice.id>`.
- `verifyWebhook` → parse id from payload → authenticated GET `/documents/{id}` re-fetch →
  WebhookEvent reflects FETCHED status. `verifyOptionalSignature` best-effort HMAC (UNVERIFIED).
- `getPaymentStatus`, `testConnection` (token + GET `/account/me`, never throws).
- Workers-safe: Web Crypto + fetch only, no node:crypto/Buffer.

### Fixer (ada0702) — fabricated placeholder removed
Wave-2 advisor finding: `createPaymentSession` sent a hardcoded `client: { name: 'Customer' }` on
the `/payments/form` body — a fabricated value (no-stubs violation) that would silently stamp the
literal "Customer" on a real customer's Morning record. Primary-source check: the `Invoice` type
(`packages/db/src/queries/invoices.ts`) has `customerId: string | null` and NO denormalized
name/email — a real name is genuinely not reachable from the adapter's inputs. Further, the only
`client` citation (`class-document-client-mapper.php`) maps the DOCUMENT-ISSUANCE payload, not the
clearing endpoint — so the `client` block's applicability to `/payments/form` is itself unproven.
Fix: removed the block, replaced with an UNVERIFIED annotation. **Fail-loud > fail-silent** — a
required-but-omitted field 400s in the creds pass (where the requirement gets discovered), whereas a
fabricated value succeeds and corrupts data. Customer-identity wiring deferred to the service layer
(create-session loads by `invoice.customerId` → `@zync/db` Customer{name, email}) — confirmed that
query path exists. Captured below as creds-gated DoD #2.

## Creds-gated DoD — deferred checks the live Morning sandbox pass MUST close
No sandbox creds yet (drop in `Docs/`, gitignored). Mocked tests pass regardless of whether the
endpoint paths/payloads are real, so these CANNOT be verified until creds arrive. Each is the
acceptance bar for the item it guards. Ordered by load-bearing risk:

1. **session_id correlation (#1, highest risk).** Confirm `POST /payments/form` returns a stable
   `id` that is the SAME id later queryable at `GET /documents/{id}` AND echoed in the webhook/IPN
   body — i.e. the create→settle handle actually correlates end-to-end. If Morning's form id ≠ the
   document id ≠ the IPN id, the whole settle path is mis-wired. This is the cornerstone; verify
   first. (Shapes Wave 3 settle-webhook + the `UNIQUE(gateway, session_id)` idempotency key.)
2. **customer identity on `/payments/form` (#2, from fixer ada0702).** Does the clearing form
   accept/require a `client` object? If REQUIRED → wire it at the Wave-3 service layer
   (create-session loads the customer by `invoice.customerId`, passes name/email). If OPTIONAL →
   leave omitted. The omitted-but-required case fails loudly here — that's the design intent.
3. **numeric status-code mapping (`mapMorningStatus`).** UNVERIFIED codes (2/4=paid, 3/5=failed,
   1=pending). Confirm the real document/payment numeric status enum against sandbox payments.
4. **callback signature scheme (`verifyOptionalSignature`).** UNVERIFIED best-effort HMAC-SHA256.
   Confirm whether Morning signs the IPN, with what algorithm/header, over what payload — or whether
   authenticity rests solely on the re-fetch (in which case signature check is belt-and-suspenders).

Until creds arrive, authenticity rests on the authenticated re-fetch (item #1's GET /documents/{id}),
which is already proven by the re-fetch test to override any lying body. Items 2–4 degrade safely:
2 omits rather than fabricates, 3 falls back to amountDue/amountPaid then `pending`, 4 only adds a
check on top of the re-fetch.

### Open
- Live Morning sandbox e2e deferred until creds supplied (Docs/, gitignored).
- Throwaway Neon branch `br-odd-bonus-a6soryy3` (schema `z`, project `spring-smoke-45518495`) from
  Wave 1 — delete on request.
