# Morning Payments — Design Doc

**Date:** 2026-06-09
**Status:** Draft → drives spec deltas + build plan
**Author:** orchestration layer (product intent: user/owner authorized Morning as the zync.is payment provider)

> Companion to `docs/specs/2026-05-31-payment-gateway-adapters.md` (Epic A spec home) and
> `docs/specs/2026-05-31-zync-subscription.md` / `2026-05-30-billing-module.md` (Epic B home).
> This doc captures the *decisions*; the canonical contract lives in those specs after the deltas land.

---

## 1. Why

Owner directive: **payment is built now; the adapter is Morning** (morning.co.il, ex-Green
Invoice — Israeli clearing + invoicing provider); **configurable in the admin dashboard.**

Two independent epics, both transacting through Morning:

- **Epic A — per-tenant collection (BYO).** Each tenant brings their own Morning API
  key+secret. Their customers pay the tenant's invoices via Morning's hosted clearing page.
  Money flows tenant-customer → tenant directly. zync.is never touches the funds. Config
  lives in zync-app `/settings/integrations/payments`.
- **Epic B — platform billing.** zync.is charges *its* customers (the tenants) the
  subscription fee via a **platform** Morning account configured by a superadmin in
  **zync-admin**. Modify-not-create: `zync-subscription.ts` / `admin-subscription.ts` likely
  already assume a provider seam — fold Morning into it, don't greenfield.

**Build order:** Epic A core vertical slice end-to-end and verified against sandbox FIRST.
Everything else (Epic A siblings + Epic B) is follow-on, gated on the core contract proven.

---

## 2. Verified Morning model (load-bearing)

Facts established by research (WooCommerce "Morning Clearing" plugin SVN source, make.com
integration docs). Durable copy in memory `reference-morning-payment-model`.

- **API base:** `https://api.greeninvoice.co.il/api/v1`.
- **Auth:** `apiKey` + `secret` → `POST /account/token` → short-lived JWT bearer. Same
  credential pair the `invoices-adapters` spec already stores for Morning *issuance*.
- **Collection = hosted clearing page.** Morning *clearing* (סליקה) returns a redirect URL
  to a Morning-hosted payment form (PCI-safe; card data never touches zync). Proven by the
  open-source `wc-gateway-greeninvoice` plugin (author: Morning) which redirects to the
  hosted page and auto-produces the tax invoice/receipt on payment.
- **Completion = authenticated re-fetch confirm.** The generic
  `PaymentGatewayAdapter.verifyWebhook(payload, signature, config)` contract assumes an
  HMAC-signed callback (Payplus/Cardcom/Stripe). Morning's adapter instead **re-fetches the
  payment/document status via the authenticated API and confirms `paid`** — never trusts the
  raw callback body. This is correct *regardless* of Morning's signature scheme. IF Morning
  also supplies a signature, the adapter verifies it too; we do **not** assert "Morning is
  unsigned" as fact (unverified). Push (webhook) + poll (`getPaymentStatus`) both available;
  both routes funnel into the same re-fetch confirmation.

### 2.1 The #1 sandbox check — correlation handle

The session model (`invoice_payment_sessions`, `UNIQUE(gateway, session_id)`) depends on a
property not yet directly observed in source: **Morning's payment-form creation endpoint must
return a queryable payment/document id AND accept a return/callback URL that carries our
session id back** (path param or attachable metadata). The capability must exist — the
WooCommerce plugin correlates payments to orders — but the **exact handle is the first thing
to confirm against sandbox creds.** If correlation can't be established, the session model
needs revision before anything downstream is built. The build plan makes this the gating
sandbox step, not a live-e2e afterthought.

### 2.2 Exact endpoint strings

Endpoint paths and payload shapes are extracted **at build time** by the implementer from
live docs (`app.greeninvoice.co.il/api`) + the WooCommerce plugin `includes/` source — not
hardcoded from guesses into spec or code.

---

## 3. Epic A — per-tenant collection

### 3.1 Core vertical slice (Plan #1 — build + verify FIRST)

The minimum end-to-end path that proves the Morning contract:

```
Pay Now (portal)
  → POST /api/invoices/:id/payment/session
  → MorningAdapter.createPaymentSession(invoice, config, returnUrl, webhookUrl)
       → POST /account/token (JWT) → create hosted clearing page
       → { sessionId, redirectUrl }
  → persist invoice_payment_sessions row (status=pending)
  → redirect customer → Morning hosted page → pays
  → Morning → webhook POST .../webhooks/payment/morning/{tenantWebhookToken}
              AND/OR customer returns to returnUrl
  → MorningAdapter.verifyWebhook → authenticated RE-FETCH payment status → confirm paid
  → idempotent settle (session_id + status): session→paid, invoice→PAID, paid_at
  → emit invoice.paid event → receipt email
  → returnUrl shows ?payment=success
```

Slice scope (everything below is **in** the first plan):
- `packages/payments/` foundation: `adapter.ts` (interface, generalized verify contract),
  `registry.ts`, `config-crypto.ts` (AES-256-GCM), types.
- `MorningAdapter` (the only built adapter; payplus/cardcom/stripe stay registry stubs).
- DB: `invoice_payment_sessions` table; `payment_gateway_configs` (+ `morning` in CHECK);
  `tenants.payment_webhook_token`.
- Services: create-session, settle-webhook (idempotent), load-active-config.
- Webhook route `/webhooks/payment/:gateway/:token` (re-fetch confirm + idempotency, return
  200 on invalid).
- Settings API GET/PUT/POST(test)/DELETE `/api/settings/integrations/payments`.
- Settings UI page — **exposes Morning ONLY** (stubs not selectable; selecting a stub must be
  impossible, not just discouraged — audit finding 002's "COMPLETED ≠ working" trap).
- Portal: Pay Now button + `GET /api/invoices/:id/payment/status`.
- `invoice.paid` domain event + receipt email. **These are IN the slice** — the contract
  isn't proven until the invoice actually flips PAID and a receipt is produced.

### 3.2 Credential storage decision — decoupled

Payment config is a **separate `payment_gateway_configs` row** (`gateway='morning'`, fields
`apiKey`+`secret`, AES-256-GCM encrypted), stored independently of the invoicing-adapter
Morning credentials. The settings UI MAY offer a convenience "use my Morning invoicing
credentials" prefill, but the stored copy is independent. Rationale: keeps the collection and
issuance subsystems decoupled — neither breaks if the other's creds rotate; avoids a hidden
cross-subsystem dependency.

### 3.3 Follow-on (Plan #2+, gated on core verified against sandbox)

Sibling specs, built only after the core slice transacts a real sandbox payment:
`invoice-payment-link-generation`, `invoice-payment-ux`, `partial-payment-recording`,
`invoice-payment-reminders`, `payment-retry-dunning`, `payment-reconciliation`.

---

## 4. Epic B — platform billing (deferred, separate plan)

zync.is charges tenants the subscription via a **platform** Morning account, configured by a
superadmin in **zync-admin**.

- **Modify-not-create.** Read `apps/zync-api/src/routes/zync-subscription.ts`,
  `routes/admin-subscription.ts`, `routes/admin/provision-tenant.ts`,
  `routes/cron/subscription-trial-check.ts` FIRST — they may already assume a billing
  provider seam. Fold Morning into that seam.
- Platform Morning credentials configured in zync-admin (superadmin), stored encrypted,
  **separate** from any tenant's BYO config — one platform account, site-wide.
- Spec home: extend `2026-05-31-zync-subscription.md` and/or `2026-05-30-billing-module.md`.
  Create a new spec only if neither covers platform-provider configuration.
- Out of scope for this doc's first build; specced + planned after Epic A core proven.

---

## 5. Spec deltas (this doc authorizes; applied to canonical specs)

### `2026-05-31-payment-gateway-adapters.md`
1. `GatewaySlug` union → add `'morning'`.
2. Registry → add `MorningAdapter`.
3. DB CHECK constraints (`payment_gateway_configs.gateway`,
   `invoice_payment_sessions.gateway`) → add `'morning'`.
4. New "Supported Gateways → Morning (Israeli)" section: API base, JWT auth, hosted clearing
   page, `verifyWebhook` = authenticated re-fetch confirm (+ verify signature if present),
   `getPaymentStatus` poll, config fields `apiKey`/`secret`, optional invoicing-cred prefill.
5. **Generalize the `verifyWebhook` contract** from "HMAC verify" to "verify authenticity —
   by signature where the gateway signs, by authenticated re-fetch where it doesn't." Add a
   one-line in-spec rationale (per project spec rule: this is a contract change, not just a
   new gateway).
6. Architecture Decisions → row: "Morning webhook authenticity | authenticated re-fetch
   confirm (signature optional) — Morning's callback signing unconfirmed; re-fetch is correct
   regardless."
7. Note correlation-handle as the gating sandbox verification.

### Epic B specs (when reached)
Platform-provider configuration in zync-admin folded into `zync-subscription.md` /
`billing-module.md` (or new spec if absent).

---

## 6. Open / deferred

- **Morning sandbox key+secret** — not yet supplied. Build + unit/integration verifiable now;
  live e2e (incl. the §2.1 correlation check) deferred until creds land in `Docs/` (gitignored).
- **Architecture Decisions** (this doc): no new module collapses — `packages/payments`
  adapter seam is deep (multi-gateway, swappable), config-crypto is deep (hides AES-GCM),
  services are medium. No decorative seams introduced.
