{
  "spec": "invoice-receipt-document",
  "spec_path": "docs/specs/2026-06-01-invoice-receipt-document.md",
  "audited_at": "2026-06-11",
  "summary": { "p0": 5, "p1": 15, "p2": 6 },
  "findings": [
    {
      "id": "ird-001",
      "severity": "P0",
      "dimension": "statutory",
      "title": "PDF route passes tenant UUID as business name",
      "location": "apps/zync-api/src/routes/receipts/index.ts:89-91",
      "evidence": "renderReceiptHtml(receipt, { tenantName: session.tid, r2PublicUrl }) — session.tid is the tenant UUID, not legal business name.",
      "expected": "Statutory קבלה header must show real עוסק מורשה business name + ח.פ./ע.מ. (spec PDF L192: business details). invoices-core fixed the identical bug via loadInvoiceRenderIdentity.",
      "actual": "Receipt PDF/HTML uses tenant UUID as the displayed business name. loadInvoiceRenderIdentity is never called; tenantTaxId and tenantAddress are also omitted.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-002",
      "severity": "P0",
      "dimension": "statutory",
      "title": "PDF never loads customer legal identity",
      "location": "apps/zync-api/src/routes/receipts/index.ts:89-91",
      "evidence": "ReceiptRenderContext accepts customerName/customerAddress but PDF route only passes tenantName and r2PublicUrl.",
      "expected": "PDF shows customer name (and tax id where applicable) in the לכבוד block (spec PDF L192).",
      "actual": "ctx.customerName is undefined; render.ts outputs an empty customer block. No DB lookup of customers row by receipt.customerId.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-003",
      "severity": "P0",
      "dimension": "statutory",
      "title": "PDF shows invoice UUID instead of invoice number",
      "location": "apps/zync-api/src/receipts/render.ts:233",
      "evidence": "<span>${esc(receipt.invoiceId)}</span> under מס׳ חשבונית",
      "expected": "Originating invoice number on the statutory document (spec PDF L192: originating invoice number).",
      "actual": "Raw invoice_id UUID is rendered. ReceiptObject carries only invoiceId, not invoiceNumber; no join to invoices at render time.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-004",
      "severity": "P0",
      "dimension": "void",
      "title": "Void does not reverse invoice payments or reopen invoice balance",
      "location": "packages/db/src/queries/receipts.ts:647-662",
      "evidence": "voidReceipt only SET invoice_payments.receipt_id = null; does not call reverseInvoicePayment or delete payment rows or recalc invoices.amount_paid/status.",
      "expected": "Void in one transaction: status VOIDED + reason, and reverse linked invoice_payments row re-opening invoice balance (spec Flow C L130-132).",
      "actual": "Payment rows remain; invoice amount_paid and status (PAID/PARTIALLY_PAID) unchanged. Invoice balance is not reopened after void.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-005",
      "severity": "P0",
      "dimension": "rbac",
      "title": "Invoice-scoped issue routes missing authMiddleware",
      "location": "apps/zync-api/src/routes/invoices/receipt-issue.ts:21-27",
      "evidence": "invoiceReceiptIssueRoutes has requirePermission('invoices:write') but no authMiddleware or requireModuleEnabled('invoices'). Mounted separately at routes/index.ts:527, unlike invoicePaymentsRoutes which calls invoicePaymentsRoutes.use('*', authMiddleware).",
      "expected": "POST /api/invoices/:id/receipts and POST /api/invoices/:id/invoice-receipt authenticated, module-gated, RBAC-protected (spec API L207).",
      "actual": "requirePermission reads c.get('session') which authMiddleware never sets on this router; handlers return 401 Unauthorized for all callers. Issue endpoints are non-functional.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-006",
      "severity": "P1",
      "dimension": "atomicity",
      "title": "Standalone receipt issue is not atomic with payment recording",
      "location": "packages/db/src/queries/receipts.ts:341-477",
      "evidence": "issueStandaloneReceipt inserts receipt + lines + links existing invoicePaymentId; never inserts invoice_payments or updates invoice status.",
      "expected": "Flow A single DB transaction: (1) insert invoice_payments + advance status, (2) insert receipt ISSUED + number, (3) lines, (4) set receipt_id, (5) PDF, (6) audit (spec L118-124).",
      "actual": "Payment recording (recordInvoicePayment) and receipt issue are separate API calls/transactions. Receipt can be issued against outstanding balance without a payment row in the same tx.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-007",
      "severity": "P1",
      "dimension": "ui",
      "title": "Record-payment modal lacks Issue receipt toggle (default ON)",
      "location": "apps/zync-app/src/features/invoices/RecordPaymentModal.tsx:40-175",
      "evidence": "Modal posts only to /api/invoices/:id/payments; no receipt toggle, no call to /receipts issue endpoint.",
      "expected": "Payment modal with ☑ Issue receipt (קבלה) now default ON → Record + issue in one confirm (spec Flow A L105-115).",
      "actual": "Recording a payment never issues a receipt. User must separately navigate to issue flow (which itself is broken — see ird-014).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-008",
      "severity": "P1",
      "dimension": "combined",
      "title": "Combined invoice-receipt does not issue TAX_ISSUED invoice atomically",
      "location": "packages/db/src/queries/receipts.ts:482-607",
      "evidence": "issueInvoiceReceipt requires inv.status TAX_ISSUED|PAID|PARTIALLY_PAID; only inserts receipts row, never calls issueTaxInvoice or recordInvoicePayment.",
      "expected": "Mark paid + issue invoice-receipt on DRAFT/SENT issues TAX_ISSUED invoice AND receipts(doc_type=invoice_receipt) atomically (spec Flow B L126-128).",
      "actual": "Endpoint only attaches a receipt document to an already tax-issued invoice. No atomic tax-issue + payment + combined receipt.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-009",
      "severity": "P1",
      "dimension": "pdf",
      "title": "pdf_r2_key never stored on issue",
      "location": "packages/db/src/queries/receipts.ts:400-418,532-548",
      "evidence": "insert into receipts sets issuedAt/issuedBy but never pdfR2Key. No generateAndStore* helper for receipts (invoices use generateAndStoreInvoiceSnapshot).",
      "expected": "Render + store PDF (pdf_r2_key) as part of issue flow (spec Flow A step 5; data model L49).",
      "actual": "pdf_r2_key column always null after issue. GET /:id/pdf re-renders live HTML on every request.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-010",
      "severity": "P1",
      "dimension": "pdf",
      "title": "PDF endpoint returns inline HTML not signed PDF",
      "location": "apps/zync-api/src/routes/receipts/index.ts:94-100",
      "evidence": "Returns text/html with Content-Disposition inline .html; comment in render.ts: No puppeteer — caller converts via browser print.",
      "expected": "GET /api/receipts/:id/pdf → signed PDF (spec API L203; PDF section). Reuse invoice-pdf-customization Worker pattern.",
      "actual": "Ephemeral HTML response. pdfUrl on ReceiptObject is always null because pdfR2Key is never set.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-011",
      "severity": "P1",
      "dimension": "ui",
      "title": "List API missing customer and date-range filters",
      "location": "packages/db/src/queries/receipts.ts:143-148,256-301",
      "evidence": "listReceiptsSchema only: cursor, limit, status, docType. No customer or issued_at date params.",
      "expected": "GET /api/receipts filters: doc_type, status, customer, date range; default sort issued_at DESC (spec L136-159, L201).",
      "actual": "Customer and date-range filters absent. Sort is orderBy(desc(receipts.createdAt)) not issued_at.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-012",
      "severity": "P1",
      "dimension": "ui",
      "title": "List API missing invoiceId filter used by invoice Receipts tab",
      "location": "apps/zync-app/src/features/receipts/api.ts:61-63; packages/db/src/queries/receipts.ts:143-148",
      "evidence": "useInvoiceReceipts calls GET /api/receipts?invoiceId=… but listReceiptsSchema has no invoiceId; query param ignored.",
      "expected": "Invoice detail Receipts tab backed by tenant-scoped receipt list for invoice (listReceiptsForInvoice exists in queries but is not exposed).",
      "actual": "InvoiceReceiptsSection fetches up to 100 all-tenant receipts and filters client-side. listReceiptsForInvoice is exported but unused by any route.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-013",
      "severity": "P1",
      "dimension": "ui",
      "title": "Receipts list page missing spec columns and export",
      "location": "apps/zync-app/src/features/receipts/ReceiptsListPage.tsx:113-157",
      "evidence": "Grid columns: מסמך, סוג, סטטוס, סכום, תאריך — no Customer column. No date-range filter, no Export menu, no pagination controls.",
      "expected": "Columns include customer name; filters for customer + date range; Export ▾; pagination Showing 1–25 of N (spec L146-159).",
      "actual": "Customer name never shown (API returns only customerId). Cursor pagination exists server-side but UI always loads limit 50 with no next/prev.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-014",
      "severity": "P1",
      "dimension": "ui",
      "title": "No functional UI to issue receipts",
      "location": "apps/zync-app/src/features/receipts/InvoiceReceiptsSection.tsx:76-91; apps/zync-app/src/features/receipts/api.ts:93-130",
      "evidence": "Issue buttons navigate to /receipts?issue=…&invoiceId=…; ReceiptsListPage does not read searchParams. useIssueReceipt/useIssueInvoiceReceipt hooks are defined but never imported by any component.",
      "expected": "Issue standalone receipt and combined invoice-receipt flows reachable from invoice editor / payment modal (spec Flow A/B UI).",
      "actual": "Issue navigation lands on list page with no issue modal. Combined with ird-005, receipt issuance is unreachable end-to-end.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-015",
      "severity": "P1",
      "dimension": "ui",
      "title": "Receipt detail page missing spec fields",
      "location": "apps/zync-app/src/features/receipts/ReceiptDetailPage.tsx:178-207",
      "evidence": "Metadata shows currency only; linked invoice shows UUID; no customer name, ils_exchange_rate, issued-by display, or voided banner with actor/date.",
      "expected": "Customer name, invoice number link, currency + Rate@issue, void banner Voided {date} by {user} — {reason}; void in ⋯ menu ISSUED only (spec L165-184).",
      "actual": "customerId not resolved to name. ilsExchangeRate omitted from UI. Void is inline button not ⋯ menu. VOIDED banner incomplete.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-016",
      "severity": "P1",
      "dimension": "currency",
      "title": "ILS rate/amount not auto-snapshotted at issue for foreign currency",
      "location": "packages/db/src/queries/receipts.ts:411-414,543-544",
      "evidence": "ilsExchangeRate optional in input; amountIls computed only when ilsExchangeRate provided. issueInvoiceReceipt hardcodes ilsExchangeRate: null for non-ILS.",
      "expected": "ils_exchange_rate + amount_ils snapshotted at ISSUE for multi-currency receipts (spec L42-45, multi-currency dependency).",
      "actual": "Foreign-currency receipts can be issued with null amount_ils unless caller manually supplies rate. No lookup from exchange_rates table at issue time.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-017",
      "severity": "P1",
      "dimension": "sequence",
      "title": "Receipt sequence prefix never applied",
      "location": "packages/db/src/queries/receipts.ts:236-251",
      "evidence": "INSERT INTO receipt_sequences … VALUES (…, '', 1); RETURNING prefix — always empty prefix. No sync from tenant settings.",
      "expected": "Gap-free per (tenant_id, doc_type) numbering with prefix applied (spec examples REC-00042, TIR-00018; architecture L218).",
      "actual": "Numbers are zero-padded digits only (e.g. 00001) with no REC-/TIR- prefix. Same class of bug as invoices-core ic-fresh-006 for invoice_sequences.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-018",
      "severity": "P1",
      "dimension": "atomicity",
      "title": "No customer email enqueued on receipt issue",
      "location": "packages/db/src/queries/receipts.ts:460-469,590-599",
      "evidence": "Issue transactions end after audit log insert; no email queue/job enqueue.",
      "expected": "Flow A step 5: enqueue customer email after PDF store (spec L123).",
      "actual": "No receipt email notification on issue. (PDF store itself also missing — ird-009.)",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-019",
      "severity": "P1",
      "dimension": "combined",
      "title": "Invoice-receipt does not validate payment lines sum against invoice total",
      "location": "packages/db/src/queries/receipts.ts:554-570",
      "evidence": "issueInvoiceReceipt inserts lines from input without summing amounts or comparing to inv.total.",
      "expected": "Combined document amount equals full invoice total received; payment-method breakdown should match (spec combined flow + payment lines L62-77).",
      "actual": "Lines can sum to any amount while receipt.amount is set to inv.total unconditionally.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-020",
      "severity": "P1",
      "dimension": "payment-lines",
      "title": "Record-payment source enum does not map to receipt payment-method lines",
      "location": "apps/zync-app/src/features/invoices/RecordPaymentModal.tsx:33-38,119-128",
      "evidence": "Payment modal uses PaymentSource (manual, gateway, bank_transfer, auto_billing); receipt lines require method IN (cash, bank_transfer, cheque, credit_card, other).",
      "expected": "Payment-method detail persisted in receipt_payment_lines with structured cheque/card/bank fields at issue-on-payment (spec L62-77, D120).",
      "actual": "No bridge from payment recording to receipt line methods. Even if atomic flow were added, source values do not align with receipt method CHECK constraint without explicit mapping UI.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-021",
      "severity": "P2",
      "dimension": "schema",
      "title": "idx_receipts_tenant sort direction drift migration vs Drizzle",
      "location": "packages/db/migrations/0026_invoice_receipt_document.sql:29; packages/db/src/schema/receipts.ts:59",
      "evidence": "Migration: CREATE INDEX idx_receipts_tenant ON receipts(tenant_id, status, issued_at DESC). Drizzle: index('idx_receipts_tenant').on(tenantId, status, issuedAt) — no DESC.",
      "expected": "Schema and migration agree (spec L58 index on tenant_id, status, issued_at).",
      "actual": "Minor drift; list sort uses createdAt anyway (ird-011), so index may not match query pattern.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-022",
      "severity": "P2",
      "dimension": "schema",
      "title": "invoice_payments.receipt_id FK comment references wrong migration",
      "location": "packages/db/src/schema/invoice-payments.ts:38-40",
      "evidence": "Comment: FK invoice_payments_receipt_id_fkey added in migration 0017 (spec 179).",
      "expected": "FK wired in 0026_invoice_receipt_document.sql per spec L93-95.",
      "actual": "Constraint is correctly added in 0026; Drizzle schema comment is stale (column existed earlier nullable from partial-payment-recording).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-023",
      "severity": "P2",
      "dimension": "ui",
      "title": "DRAFT receipt status unreachable",
      "location": "packages/db/src/queries/receipts.ts:407,539",
      "evidence": "All issue paths insert status: 'ISSUED' directly; receipt_number assigned at same insert.",
      "expected": "receipt_number assigned at ISSUE never in draft (spec L35-36). DRAFT rows may exist for in-progress documents (spec list mock shows DRAFT row).",
      "actual": "UI exposes DRAFT filter but no code path creates DRAFT receipts. Filter always empty unless manually seeded.",
      "fix_direction": "spec_review"
    },
    {
      "id": "ird-024",
      "severity": "P2",
      "dimension": "pdf",
      "title": "Receipt render does not reuse tenant_invoice_pdf_config",
      "location": "apps/zync-api/src/receipts/render.ts:99-262",
      "evidence": "Standalone inline CSS template; no load of tenant PDF branding config.",
      "expected": "HTML-to-PDF reuses tenant_invoice_pdf_config like invoice-pdf-customization (spec PDF L192).",
      "actual": "Hard-coded minimal Hebrew template. No logo/colors/footer from tenant PDF settings.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ird-025",
      "severity": "P2",
      "dimension": "sequence",
      "title": "nextReceiptNumber uses atomic UPDATE not explicit SELECT FOR UPDATE",
      "location": "packages/db/src/queries/receipts.ts:241-247; docs/plans/tasks/invoice-receipt-document.md:174",
      "evidence": "UPDATE receipt_sequences SET next_number = next_number + 1 … RETURNING (no FOR UPDATE in task plan).",
      "expected": "Task plan: SELECT FOR UPDATE on receipt_sequences inside transaction.",
      "actual": "Single-statement UPDATE is concurrency-safe in PostgreSQL and avoids TOCTOU; implementation diverges from task doc wording but is likely correct.",
      "fix_direction": "spec_review"
    },
    {
      "id": "ird-026",
      "severity": "P2",
      "dimension": "isolation",
      "title": "Void unlinks payments only via receipt_payment_lines.invoice_payment_id",
      "location": "packages/db/src/queries/receipts.ts:653-661",
      "evidence": "for (const paymentId of lines.filter(l => l.invoicePaymentId)) — no fallback UPDATE invoice_payments WHERE receipt_id = :id.",
      "expected": "All payments linked to voided receipt are reversed/unlinked tenant-scoped.",
      "actual": "If receipt_id were set on invoice_payments without a matching line.invoice_payment_id, void would miss it. Current issue path sets both together, so low risk but incomplete.",
      "fix_direction": "code_to_spec"
    }
  ]
}
