{
  "slug": "payment-gateway-adapters",
  "spec_file": "docs/specs/2026-05-31-payment-gateway-adapters.md",
  "findings": [
    {
      "id": "payment-gateway-adapters-001",
      "severity": "P0",
      "type": "security",
      "summary": "Inbound payment webhook route absent — no provider signature verification exists",
      "spec_ref": "API Endpoints — POST https://api.zync.is/webhooks/payment/:gateway; Webhook endpoint security §1 adapter.verifyWebhook",
      "code_ref": "apps/zync-api/src/routes/webhooks/index.ts:24",
      "evidence": "webhookRoutes mounts /telegram, /slack, /whatsapp, /resend only. Grep webhooks/payment across apps/zync-api returns zero route files. No verifyWebhook call path for Payplus/Cardcom/Stripe invoice payments.",
      "repro": "POST /api/webhooks/payment/payplus/<token> with any body → 404; attacker could forge payment completion if a route existed without verification.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-002",
      "severity": "P0",
      "type": "security",
      "summary": "Gateway adapters implement generatePaymentLink stubs only — no verifyWebhook, HMAC, or Stripe constructEvent",
      "spec_ref": "Adapter Interface — verifyWebhook throws if signature invalid; Supported Gateways webhook verification",
      "code_ref": "apps/zync-api/src/integrations/payment-gateways/payplus.ts:29",
      "evidence": "payplus.ts, cardcom.ts, stripe.ts each export generatePaymentLink returning mock URLs (lines 78-83). Grep verifyWebhook in apps/zync-api/src/integrations/payment-gateways → no matches. packages/payments has ZyncPaymentAdapter for zync-subscription, not PaymentGatewayAdapter.",
      "repro": "Cannot verify x-ppplus-signature, Cardcom terminal match, or Stripe webhook signature anywhere in codebase.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-003",
      "severity": "P0",
      "type": "security",
      "summary": "Webhook idempotency and settlement (settleWebhook) not implemented — double-charge / forged settlement risk once webhooks exist",
      "spec_ref": "Webhook endpoint security §3 idempotency; Customer Payment Flow — update session paid, invoice PAID",
      "code_ref": "absent",
      "evidence": "Grep settleWebhook, invoice_payment_sessions across repo (excluding docs/worktrees) → zero implementation. No session lookup by (gateway, session_id). No check session status before update.",
      "repro": "N/A until webhook route exists; spec-mandated idempotency path wholly absent.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-004",
      "severity": "P0",
      "type": "broken",
      "summary": "Payment completion flow cannot mark invoices PAID — no webhook handler, no session persistence",
      "spec_ref": "Customer Payment Flow — Update invoice_payment_sessions.status = paid; Update invoices.status = PAID",
      "code_ref": "apps/zync-api/src/routes/invoices/payment-link.ts:142",
      "evidence": "POST /:id/payment-link returns mock gateway URL and does not insert invoice_payment_sessions. packages/db has no invoice_payment_sessions table/migration. Mock URLs (payplus.ts:78) cannot complete real payment.",
      "repro": "Generate payment link → customer visits mock URL → no webhook → invoice stays unpaid.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-005",
      "severity": "P0",
      "type": "broken",
      "summary": "POST /api/invoices/:id/payment-link routes lack authMiddleware — session never populated, always 401",
      "spec_ref": "Portal payment routes — portal auth; payment-link comment claims authMiddleware",
      "code_ref": "apps/zync-api/src/routes/invoices/payment-link.ts:15",
      "evidence": "File imports requirePermission only (line 15), no authMiddleware import/use. requirePermission (packages/auth/src/middleware.ts:35) returns 401 when c.get('session') undefined. invoiceRoutes at invoices/index.ts:84 uses authMiddleware; paymentLinkInvoiceRoutes mounted separately at routes/index.ts:396.",
      "repro": "Authenticated POST /api/invoices/:id/payment-link → 401 Unauthorized (no session on context).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-006",
      "severity": "P1",
      "type": "missing",
      "summary": "PaymentGatewayAdapter interface and packages/payments adapter registry not implemented",
      "spec_ref": "Adapter Interface; Adapter registry — getAdapter(gateway)",
      "code_ref": "packages/payments/src/adapter.ts:8",
      "evidence": "adapter.ts re-exports ZyncPaymentAdapter from types.ts (zync-subscription). registry.ts getPaymentAdapter(env, db) resolves ZYNC_PAYMENT_ADAPTER null adapter only. No PaymentGatewayAdapter, createPaymentSession, getPaymentStatus, testConnection.",
      "repro": "import { getAdapter } from '@zync/payments' for payplus → symbol does not exist.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-007",
      "severity": "P1",
      "type": "missing",
      "summary": "packages/payments config-crypto.ts (encryptConfig/decryptConfig) absent",
      "spec_ref": "Config encryption — packages/payments/src/config-crypto.ts AES-256-GCM PAYMENT_CONFIG_ENCRYPTION_KEY",
      "code_ref": "absent",
      "evidence": "Glob packages/payments/src/** → no config-crypto.ts, no adapters/ directory. Encryption lives in packages/db/src/queries/payment-gateways.ts via @zync/utils encryptCredential.",
      "repro": "pnpm --filter @zync/payments — no encryptConfig export.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-008",
      "severity": "P1",
      "type": "missing",
      "summary": "invoice_payment_sessions table and Drizzle model absent",
      "spec_ref": "Data Model — CREATE TABLE invoice_payment_sessions",
      "code_ref": "absent",
      "evidence": "Grep invoice_payment_sessions in packages/db → zero schema/migration matches (only docs). packages/db/src/schema/payment-gateways.ts defines payment_gateway_configs only.",
      "repro": "No migration CREATE TABLE invoice_payment_sessions in packages/db/migrations/.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-009",
      "severity": "P1",
      "type": "missing",
      "summary": "tenants.payment_webhook_token column absent — per-tenant webhook routing impossible",
      "spec_ref": "Foundation Delta — ALTER TABLE tenants ADD COLUMN payment_webhook_token; Per-tenant webhook token",
      "code_ref": "packages/db/src/schema/tenants.ts",
      "evidence": "Grep payment_webhook in packages/db/src/schema/tenants.ts → no matches. No migration adding payment_webhook_token.",
      "repro": "Cannot resolve tenant from webhook URL token per spec.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-010",
      "severity": "P1",
      "type": "missing",
      "summary": "Session and config services (createPaymentSession, settleWebhook, loadActiveGatewayConfig) absent",
      "spec_ref": "Plan Task 6–7 — apps/zync-api/src/services/payments/*",
      "code_ref": "absent",
      "evidence": "Grep services/payments in apps/zync-api → zero files. Plan paths config-service.ts, session-service.ts, webhook-token.ts not present.",
      "repro": "No createPaymentSession(db, env, tenantId, invoiceId) export.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-011",
      "severity": "P1",
      "type": "missing",
      "summary": "POST /api/invoices/:id/payment/session portal endpoint absent",
      "spec_ref": "API Endpoints — POST /api/invoices/:id/payment/session Portal auth",
      "code_ref": "absent",
      "evidence": "Grep payment/session in apps/zync-api → no route. Existing POST /api/invoices/:id/payment-link (payment-link.ts:58) is staff invoices:write, not portal customer flow.",
      "repro": "Portal customer Pay Now cannot create hosted payment session per spec.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-012",
      "severity": "P1",
      "type": "missing",
      "summary": "GET /api/invoices/:id/payment/status poll endpoint absent",
      "spec_ref": "API Endpoints — GET /api/invoices/:id/payment/status Portal auth",
      "code_ref": "absent",
      "evidence": "Grep payment/status in apps/zync-api → no matches.",
      "repro": "Return URL cannot verify DB-authoritative payment status.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-013",
      "severity": "P1",
      "type": "missing",
      "summary": "Settings API at spec paths /api/settings/integrations/payments* not implemented",
      "spec_ref": "API Endpoints — GET/PUT/POST test/DELETE /api/settings/integrations/payments",
      "code_ref": "apps/zync-api/src/routes/settings/payment-gateway.ts:3",
      "evidence": "Implemented route mounted at /api/settings/payment-gateway (routes/index.ts:393). Only GET / and PATCH / exist. No PUT, POST .../test, DELETE. Grep integrations/payments in apps/zync-api → zero routes.",
      "repro": "PUT /api/settings/integrations/payments → 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-014",
      "severity": "P1",
      "type": "missing",
      "summary": "Test connection and remove gateway endpoints absent",
      "spec_ref": "API Endpoints — POST .../test; DELETE .../payments",
      "code_ref": "apps/zync-api/src/routes/settings/payment-gateway.ts:49",
      "evidence": "payment-gateway.ts defines GET / and PATCH / only. No testConnection handler. packages/db/queries/payment-gateways.ts has upsert/get but no removeGatewayConfig.",
      "repro": "POST /api/settings/payment-gateway/test → 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-015",
      "severity": "P1",
      "type": "missing",
      "summary": "Portal Pay Now UI and customer payment session flow absent",
      "spec_ref": "Invoice Integration — Pay Now button portal; Customer Payment Flow",
      "code_ref": "apps/zync-app/src/routes/portal/invoices.tsx:55",
      "evidence": "Portal invoices page fetches GET /api/portal/invoices only. Grep Pay Now|payment/session in apps/zync-app → no portal payment UI.",
      "repro": "Customer portal invoice list has no Pay Now action.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-016",
      "severity": "P1",
      "type": "missing",
      "summary": "Payment sessions collapsible section and gateway status badge on tenant invoice detail absent",
      "spec_ref": "Invoice Integration — Payment gateway status badge; Payment sessions section",
      "code_ref": "absent",
      "evidence": "Grep PaymentSessionsSection across apps/zync-app → no matches. Invoice detail uses PaymentLinkPanel (payment-link-generation) not session list.",
      "repro": "Open invoice detail — no payment session attempts list.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-017",
      "severity": "P1",
      "type": "missing",
      "summary": "invoice.paid outbound event not fired on gateway payment confirmation",
      "spec_ref": "invoice.paid event — enqueue webhook.deliver on webhook confirmation",
      "code_ref": "apps/zync-api/src/features/webhooks/catalog.ts:41",
      "evidence": "catalog.ts lists invoice.paid event type for outbound webhooks but no gateway settlement code enqueues it. zync-subscription.ts:219 handles invoice.paid for SaaS billing only.",
      "repro": "Complete gateway payment → no invoice.paid outbound webhook.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-018",
      "severity": "P1",
      "type": "missing",
      "summary": "Receipt email on payment success not wired to gateway webhooks",
      "spec_ref": "invoice.paid — enqueue receipt email via communications adapter",
      "code_ref": "absent",
      "evidence": "settleWebhook absent. payment-link send emails HMAC token link (payment-link.ts:270), not post-payment receipt with PDF.",
      "repro": "Gateway payment succeeds (hypothetically) → no receipt email path.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-019",
      "severity": "P1",
      "type": "missing",
      "summary": "Spec-mandated tests absent (config-crypto, adapters verifyWebhook, webhook route)",
      "spec_ref": "Plan Task 14 — packages/payments/__tests__, webhooks/__tests__/payment.test.ts",
      "code_ref": "absent",
      "evidence": "Glob **/payment*.test.ts in repo → 0 files. packages/payments has no __tests__ directory.",
      "repro": "pnpm test — no payment gateway adapter or webhook route tests.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-020",
      "severity": "P1",
      "type": "missing",
      "summary": "Settings UI page not registered — /settings/payment-gateway and /settings/integrations/payments unroutable",
      "spec_ref": "Settings UI — /settings/integrations/payments",
      "code_ref": "apps/zync-app/src/modules/settings.tsx:294",
      "evidence": "PaymentGatewaySettingsPage exists at routes/settings/payment-gateway.tsx but grep payment in modules/settings.tsx → no Route. Integrations hub has integrations, integrations/webhooks only. PaymentLinkPanel links href=/settings/integrations/payments (PaymentLinkPanel.tsx:111) — no matching Route.",
      "repro": "Navigate /settings/integrations/payments or /settings/payment-gateway → 404 in app shell.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-021",
      "severity": "P2",
      "type": "diverges",
      "summary": "payment_gateway_configs schema diverges from spec (column names, uniqueness, active flag)",
      "spec_ref": "Data Model — config_encrypted, active, UNIQUE(tenant_id, gateway), partial index WHERE active=true",
      "code_ref": "packages/db/src/schema/payment-gateways.ts:45",
      "evidence": "Drizzle uses encrypted_config, is_active (not config_encrypted/active). unique on tenant_id only (line 52), not (tenant_id, gateway). Migration 0010:154 CONSTRAINT payment_gateway_configs_tenant_unique UNIQUE(tenant_id). Index idx_payment_gateway_configs_tenant has no WHERE active=true.",
      "repro": "Cannot store multiple gateway rows per tenant per spec; one gateway per tenant only.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-022",
      "severity": "P2",
      "type": "diverges",
      "summary": "Credential encryption uses INTEGRATION_ENCRYPTION_KEY and {ciphertext,iv,authTag} not spec PAYMENT_CONFIG_ENCRYPTION_KEY / EncryptedConfig",
      "spec_ref": "Config encryption — PAYMENT_CONFIG_ENCRYPTION_KEY; EncryptedConfig {iv, tag, data}",
      "code_ref": "packages/db/src/queries/payment-gateways.ts:53",
      "evidence": "getEncryptionKey reads process.env INTEGRATION_ENCRYPTION_KEY (line 54). Blob shape JSON.parse encryptedConfig as {ciphertext, iv, authTag} (line 79). Spec requires packages/payments config-crypto with {iv, tag, data}.",
      "repro": "Env only has PAYMENT_CONFIG_ENCRYPTION_KEY → decrypt throws INTEGRATION_ENCRYPTION_KEY env not set.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-023",
      "severity": "P2",
      "type": "diverges",
      "summary": "GET settings lacks settings:read; uses PATCH not PUT; redaction shape wrong",
      "spec_ref": "Permissions settings:read; API GET redacted fields {configured:true} per field",
      "code_ref": "apps/zync-api/src/routes/settings/payment-gateway.ts:30",
      "evidence": "GET / has authMiddleware but no requirePermission('settings:read'). PATCH not PUT. getPaymentGatewayConfigMasked returns {gateway, isActive, testMode, configured: boolean} not per-field {apiKey: {configured:true}}.",
      "repro": "GET returns single configured flag; cannot show which credential fields saved.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-024",
      "severity": "P2",
      "type": "diverges",
      "summary": "Payable invoice statuses include SENT proforma — spec allows only TAX_ISSUED and PARTIALLY_PAID",
      "spec_ref": "Invoice Integration — Pay Now only TAX_ISSUED|PARTIALLY_PAID; IL law SENT not payable",
      "code_ref": "apps/zync-api/src/services/payment-link.ts:31",
      "evidence": "paymentLinkStatusForInvoice case 'SENT' returns {state:'active'} (line 31-34). Spec createPaymentSession rejects SENT/DRAFT.",
      "repro": "SENT proforma invoice gets active payment link per payment-link service.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-025",
      "severity": "P2",
      "type": "diverges",
      "summary": "Amount passed to gateway stubs as decimal string, not minor units (agorot/cents INTEGER)",
      "spec_ref": "invoice_payment_sessions.amount INTEGER agorot; createPaymentSession minor units",
      "code_ref": "apps/zync-api/src/routes/invoices/payment-link.ts:92",
      "evidence": "invoicePayload.amount = invoice.total (numeric string from schema invoices.ts:88). Stubs use parseFloat(invoice.amount) in comments (payplus.ts:63). No conversion to integer minor units.",
      "repro": "Invoice total 1500.00 ILS passed as string; spec expects 150000 agorot integer in session row.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-026",
      "severity": "P2",
      "type": "diverges",
      "summary": "Gateway adapter stubs return mock URLs — no real API integration",
      "spec_ref": "Supported Gateways — Payplus generateLink, Cardcom BillGold, Stripe Checkout Sessions",
      "code_ref": "apps/zync-api/src/integrations/payment-gateways/stripe.ts:82",
      "evidence": "All three files labeled STUB; return https://.../mock/ URLs without fetch to provider APIs. No createPaymentSession, sessionId, webhookUrl parameters.",
      "repro": "POST payment-link → redirect URL is non-functional mock.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-027",
      "severity": "P2",
      "type": "diverges",
      "summary": "Stripe settings UI missing publishableKey and webhookSecret; Payplus missing terminalNumber",
      "spec_ref": "Gateway credential forms — pk_/sk_/whsec_ validation; Payplus terminalNumber",
      "code_ref": "apps/zync-app/src/routes/settings/payment-gateway.tsx:157",
      "evidence": "StripeFields only secretKey input (line 167). PayPlusFields apiKey+secretKey only (line 133-152), no terminalNumber. No Zod pk_/whsec_ prefix validation in upsertPaymentGatewaySchema.",
      "repro": "Save Stripe config without webhook secret → webhook verification impossible per spec.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-028",
      "severity": "P2",
      "type": "diverges",
      "summary": "Architecture uses staff payment-link flow instead of customer portal hosted checkout session",
      "spec_ref": "Customer Payment Flow — portal Pay Now → createPaymentSession → redirectUrl",
      "code_ref": "apps/zync-api/src/routes/invoices/payment-link.ts:6",
      "evidence": "Implemented POST/GET /payment-link for tenant staff (invoices:write/read). Parallel invoice-payment-link-generation HMAC token at app.zync.is. Spec portal returnUrl portal_slug.zync.is/portal/invoices/{id}.",
      "repro": "Spec customer self-serve Pay Now path not implemented; alternate payment-link spec partially overlaps.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-029",
      "severity": "P2",
      "type": "diverges",
      "summary": "getEncryptionKey uses process.env in query helper — Workers use c.env bindings",
      "spec_ref": "Config service — decrypt at Worker request time",
      "code_ref": "packages/db/src/queries/payment-gateways.ts:53",
      "evidence": "getEncryptionKey() reads process.env['INTEGRATION_ENCRYPTION_KEY']. Cloudflare Workers typically pass secrets via env binding on c.env, not process.env.",
      "repro": "May fail to decrypt credentials in Worker runtime if only c.env populated.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-030",
      "severity": "P2",
      "type": "diverges",
      "summary": "Integration hub manage_url /settings/payment-gateway vs spec /settings/integrations/payments",
      "spec_ref": "Settings UI path; integration-hub catalog manage_url",
      "code_ref": "apps/zync-app/src/features/settings/integrations/catalog.ts:157",
      "evidence": "catalog.ts manage_url '/settings/payment-gateway' for cardcom/payplus/stripe. Spec and integration-hub plan use /settings/integrations/payments.",
      "repro": "Inconsistent settings URLs across UI components.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-031",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "Settings UI missing webhook URL copy, Test Connection, Remove, Switch Gateway per spec layout",
      "spec_ref": "Settings UI Connected state — webhook URL Copy, Test Connection, Remove",
      "code_ref": "apps/zync-app/src/routes/settings/payment-gateway.tsx:331",
      "evidence": "Page ends with Save button only (line 331-340). No webhook URL display, no test POST, no DELETE, no per-field configured placeholders after reload.",
      "repro": "Configure gateway — no webhook URL to paste into Payplus dashboard.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-032",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "Broken deep links to /settings/integrations/payments from invoice UI and onboarding",
      "spec_ref": "Settings UI — /settings/integrations/payments",
      "code_ref": "apps/zync-app/src/features/invoices/components/PaymentLinkPanel.tsx:111",
      "evidence": "PaymentLinkPanel href /settings/integrations/payments. first-run-onboarding.tsx:51 same path. No app route registered for that path.",
      "repro": "Click Go to Payment Settings from invoice → 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "payment-gateway-adapters-033",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "test_mode on config row only — invoice_payment_sessions.test_mode column absent",
      "spec_ref": "Test mode — sessions tagged test_mode=true in invoice_payment_sessions",
      "code_ref": "packages/db/src/schema/payment-gateways.ts:47",
      "evidence": "payment_gateway_configs has testMode boolean. No invoice_payment_sessions table for per-session test_mode tag or portal test badge.",
      "repro": "Cannot show test mode badge on customer Pay Now per session.",
      "fix_direction": "code_to_spec"
    }
  ],
  "summary": {
    "P0": 5,
    "P1": 15,
    "P2": 10,
    "P3": 3,
    "total": 33
  }
}
