{
  "slug": "invoices-core",
  "spec_file": "docs/specs/2026-05-30-invoices-core.md",
  "findings": [
    {
      "id": "invoices-core-001",
      "severity": "P0",
      "type": "broken",
      "summary": "Tax HTML snapshot rendered before TAX_ISSUED; lacks invoice number",
      "spec_ref": "Invoice HTML generation / On TAX_ISSUED snapshot",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:467",
      "evidence": "issue-tax calls renderInvoiceHtml + R2 put while invoice is still APPROVED (invoiceNumber=null); issueTaxInvoice runs afterward. Immutable snapshot shows proforma title/number, not חשבונית מס number.",
      "repro": "POST /api/invoices/:id/issue-tax on APPROVED invoice; inspect R2 key or htmlSnapshotUrl HTML — title uses proforma, invoice_number absent."
    },
    {
      "id": "invoices-core-002",
      "severity": "P0",
      "type": "broken",
      "summary": "Void endpoint requires invoices:void permission that is never seeded",
      "spec_ref": "API Endpoints POST /void / Permissions (OWNER or ADMIN)",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:575",
      "evidence": "Route stacks requirePermission('invoices:void'); packages/db/src/seed/permission-keys.ts lists invoices:read|write|delete|send only — no invoices:void. OWNER role gets ALL from that list, so every void returns 403.",
      "repro": "As OWNER, POST /api/invoices/:id/void with {reason} on DRAFT invoice → 403 Forbidden requiredPermission invoices:void."
    },
    {
      "id": "invoices-core-003",
      "severity": "P1",
      "type": "missing",
      "summary": "GET /api/invoices/unbilled-time is a stub returning empty items",
      "spec_ref": "Time Entry Billing / API Endpoints",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:187",
      "evidence": "Route returns {items:[], note:'Requires spec 77...'}; listUnbilledTimeEntries exists in packages/db/src/queries/time-invoice.ts:97 but is not called from this path (wired only at /api/time/unbilled).",
      "repro": "GET /api/invoices/unbilled-time?projectId=<uuid> → items:[]."
    },
    {
      "id": "invoices-core-004",
      "severity": "P1",
      "type": "missing",
      "summary": "invoice.generate queue consumer for automated generation absent",
      "spec_ref": "Automated Invoice Generation",
      "code_ref": "NONE",
      "evidence": "Grep invoice.generate and apps/zync-api/src/jobs/invoice-generate.ts in repo root returns no matches; retainer depletion, task-status trigger, and fixed-price deposit auto-create not implemented.",
      "repro": ""
    },
    {
      "id": "invoices-core-005",
      "severity": "P1",
      "type": "missing",
      "summary": "/invoices/new route and inbound query-param pre-fill not implemented",
      "spec_ref": "Create / edit invoice / Inbound pre-fill",
      "code_ref": "apps/zync-app/src/modules/invoices.tsx:20",
      "evidence": "Module routes only index, reconcile, :id — no /new. /invoices/new links resolve to :id='new' detail 404. InvoiceFormSheet has no contract_id/proposal_id/from_time handling; grep in pages/invoices finds no matches.",
      "repro": "Navigate to /invoices/new or /invoices/new?project_id=x&from_time=true → invoice not found."
    },
    {
      "id": "invoices-core-006",
      "severity": "P2",
      "type": "divergent",
      "summary": "DELETE draft uses invoices:write not invoices:delete",
      "spec_ref": "Permissions",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:241",
      "evidence": "DELETE /:id gated with requirePermission('invoices:write'); spec permission table requires invoices:delete for draft deletion.",
      "repro": ""
    },
    {
      "id": "invoices-core-007",
      "severity": "P2",
      "type": "divergent",
      "summary": "R2 snapshot key/path omits invoices/, tax-invoice number, and locale tag",
      "spec_ref": "R2 snapshot locale flag / R2 key format",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:483",
      "evidence": "Key is `${tenantId}/${id}/invoice.html`; spec requires `{tenantId}/invoices/{invoiceId}/tax-invoice-{number}-{lang}.html`. Uses INVOICE_SNAPSHOTS_BUCKET not STORAGE binding named in plan.",
      "repro": ""
    },
    {
      "id": "invoices-core-008",
      "severity": "P2",
      "type": "divergent",
      "summary": "HTML template uses Google Fonts import, not R2 Heebo @font-face embed",
      "spec_ref": "PDF Locale — Hebrew font embed",
      "code_ref": "packages/db/src/queries/invoices.ts:1655",
      "evidence": "renderInvoiceHtml @import url('https://fonts.googleapis.com/...Heebo'); spec requires @font-face from R2 _static/fonts/heebo-variable.woff2 with font-display:block.",
      "repro": ""
    },
    {
      "id": "invoices-core-009",
      "severity": "P2",
      "type": "divergent",
      "summary": "HTML render hardcodes he-IL; tenant/customer/business ID omitted",
      "spec_ref": "PDF Locale / Israeli tax fields on tax invoice",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:472",
      "evidence": "issue-tax and GET /html pass locale:'he-IL', tenantName:session.tid, customerName:invoice.customerId with TODO comments; no tenants.locale lookup; template lacks business ID (ח.פ./ע.מ.).",
      "repro": ""
    },
    {
      "id": "invoices-core-010",
      "severity": "P2",
      "type": "divergent",
      "summary": "GET /html missing Content-Disposition; TAX_ISSUED redirects instead of inline serve",
      "spec_ref": "Invoice HTML generation — Content-Disposition inline",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:633",
      "evidence": "TAX_ISSUED with htmlSnapshotUrl returns c.redirect(url,302); live render Response has no Content-Disposition header. Spec: inline filename invoice-{number}.html.",
      "repro": ""
    },
    {
      "id": "invoices-core-011",
      "severity": "P2",
      "type": "divergent",
      "summary": "voidInvoice allows APPROVED/REJECTED; spec limits void to DRAFT or SENT",
      "spec_ref": "Invoice detail VOID / API POST /void",
      "code_ref": "packages/db/src/queries/invoices.ts:1455",
      "evidence": "voidInvoice only blocks taxIssuedOrLater statuses; APPROVED and REJECTED pass through to VOID. Spec: VOID terminal from DRAFT or SENT only.",
      "repro": ""
    },
    {
      "id": "invoices-core-012",
      "severity": "P2",
      "type": "divergent",
      "summary": "No REJECTED→DRAFT reopen; send allows REJECTED→SENT directly",
      "spec_ref": "Invoice States / Invoice detail REJECTED actions",
      "code_ref": "packages/db/src/queries/invoices.ts:715",
      "evidence": "Grep reopen in apps/zync-api/src/routes/invoices: no route. sendInvoice accepts status REJECTED; updateInvoice allows REJECTED edit without status reset to DRAFT.",
      "repro": ""
    },
    {
      "id": "invoices-core-013",
      "severity": "P2",
      "type": "divergent",
      "summary": "POST credit-note creates DRAFT only; spec one-shot paired issued credit note",
      "spec_ref": "API POST /credit-note / Architecture credit notes",
      "code_ref": "apps/zync-api/src/routes/invoices/credit-notes.ts:27",
      "evidence": "Route calls createCreditNoteDraft (status DRAFT); separate POST /:creditNoteId/issue required. Deprecated createCreditNote in queries issues immediately but is not used by route.",
      "repro": ""
    },
    {
      "id": "invoices-core-014",
      "severity": "P2",
      "type": "missing",
      "summary": "auto-issue does not write R2 HTML snapshot",
      "spec_ref": "API POST /auto-issue / On TAX_ISSUED snapshot",
      "code_ref": "packages/db/src/queries/invoices.ts:1508",
      "evidence": "autoIssueInvoice transaction inserts TAX_ISSUED invoice but never renders HTML or writes R2; issue-tax route does snapshot but auto-issue path does not.",
      "repro": ""
    },
    {
      "id": "invoices-core-015",
      "severity": "P2",
      "type": "divergent",
      "summary": "recordPayment accepts method/date but does not persist them",
      "spec_ref": "API POST /record-payment",
      "code_ref": "packages/db/src/queries/invoices.ts:984",
      "evidence": "recordPaymentSchema includes paymentMethod and paymentDate; recordPayment only updates amountPaid/status/paidAt — input fields discarded.",
      "repro": ""
    },
    {
      "id": "invoices-core-016",
      "severity": "P2",
      "type": "divergent",
      "summary": "POST /api/invoices does not verify customer/project belong to tenant",
      "spec_ref": "API POST /api/invoices (plan acceptance: foreign customer → 404)",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:150",
      "evidence": "Route calls createInvoice directly with no customer_id or project_id existence check against tenant.",
      "repro": ""
    },
    {
      "id": "invoices-core-017",
      "severity": "P2",
      "type": "divergent",
      "summary": "billedEntryIds accepted in schema but never applied on create",
      "spec_ref": "Time Entry Billing — sets time_entries.invoice_id on creation",
      "code_ref": "packages/db/src/queries/invoices.ts:53",
      "evidence": "createInvoiceSchema includes billedEntryIds; createInvoice ignores field (comment says owned by spec 77). No markTimeEntriesBilled call from POST /api/invoices.",
      "repro": ""
    },
    {
      "id": "invoices-core-018",
      "severity": "P2",
      "type": "divergent",
      "summary": "No assertTransition/InvalidTransitionError; transitions use ad-hoc Error strings",
      "spec_ref": "State machine / plan Task 4 acceptance",
      "code_ref": "NONE",
      "evidence": "Grep InvalidTransitionError in packages/db returns no class; routes map generic Error substrings to 409, not {error:'invalid_transition'}.",
      "repro": ""
    },
    {
      "id": "invoices-core-019",
      "severity": "P2",
      "type": "divergent",
      "summary": "List date filters apply to issueDate not created_at per plan cursor spec",
      "spec_ref": "List Performance / plan Task 3 dateFrom on created_at",
      "code_ref": "packages/db/src/queries/invoices.ts:361",
      "evidence": "listInvoices filters dateFrom/dateTo on invoices.issueDate; plan specifies created_at. Drafts without issue_date excluded from date-range filter.",
      "repro": ""
    },
    {
      "id": "invoices-core-020",
      "severity": "P2",
      "type": "divergent",
      "summary": "Invoice list UI missing customer/project/source filters and sort controls",
      "spec_ref": "Invoice list — Filter and Sort",
      "code_ref": "apps/zync-app/src/pages/invoices/InvoicePage.tsx:155",
      "evidence": "Toolbar only status + date range; no customer, project, or source filters; no sort by issue date, amount, or number.",
      "repro": ""
    },
    {
      "id": "invoices-core-021",
      "severity": "P2",
      "type": "missing",
      "summary": "InvoiceStatusTimeline component absent; detail uses ActivityTimeline instead",
      "spec_ref": "Invoice detail — Status timeline DRAFT→SENT→APPROVED→TAX_ISSUED→PAID",
      "code_ref": "apps/zync-app/src/pages/invoices/InvoiceDetailPage.tsx:698",
      "evidence": "Grep InvoiceStatusTimeline in apps/zync-app: no file. Detail imports ActivityTimeline for generic activity, not state-step timeline with timestamps.",
      "repro": ""
    },
    {
      "id": "invoices-core-022",
      "severity": "P2",
      "type": "divergent",
      "summary": "Detail actions missing Resend, Delete, and explicit Reopen for REJECTED",
      "spec_ref": "Invoice detail — Actions vary by state",
      "code_ref": "apps/zync-app/src/pages/invoices/InvoiceDetailPage.tsx:212",
      "evidence": "Action bar has Send/Approve/Reject/Issue tax/Record payment/Credit note/Void/Print; no Resend for SENT, no Delete for DRAFT, no Reopen-to-draft for REJECTED.",
      "repro": ""
    },
    {
      "id": "invoices-core-023",
      "severity": "P2",
      "type": "divergent",
      "summary": "Create/edit sheet missing issue date, add-time-entries, drag-reorder lines",
      "spec_ref": "Create / edit invoice",
      "code_ref": "apps/zync-app/src/pages/invoices/InvoiceFormSheet.tsx:225",
      "evidence": "Form has due date and LineItemEditor without drag-to-reorder; no issue date field; no Add time entries control; customer is raw UUID input.",
      "repro": ""
    },
    {
      "id": "invoices-core-024",
      "severity": "P2",
      "type": "divergent",
      "summary": "Outbound send email locale hardcoded he-IL, not tenant.settings.locale",
      "spec_ref": "Outbound Email Locale",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:354",
      "evidence": "sendInvoice route passes locale:'he-IL' to sendEmail unconditionally; spec uses tenant.settings.locale with en/he templates.",
      "repro": ""
    },
    {
      "id": "invoices-core-025",
      "severity": "P2",
      "type": "missing",
      "summary": "Verification tests from plan Task 15 not present",
      "spec_ref": "plan Task 15 — invoices.test.ts",
      "code_ref": "NONE",
      "evidence": "Glob **/invoices*.test.ts and packages/db/src/queries/invoices.test.ts: no files. Plan requires numbering, state machine, VAT, HTML locale tests.",
      "repro": ""
    },
    {
      "id": "invoices-core-026",
      "severity": "P2",
      "type": "divergent",
      "summary": "invoice_sequences.type CHECK includes credit_note; spec allows invoice|proforma only",
      "spec_ref": "Data Model invoice_sequences",
      "code_ref": "packages/db/src/schema/invoices.ts:267",
      "evidence": "typeCheck allows 'invoice','proforma','credit_note'; spec DDL and plan Task 1 list only invoice|proforma.",
      "repro": ""
    },
    {
      "id": "invoices-core-027",
      "severity": "P3",
      "type": "divergent",
      "summary": "unbilled-time gated on time:read not invoices:read",
      "spec_ref": "Permissions / GET unbilled-time",
      "code_ref": "apps/zync-api/src/routes/invoices/index.ts:180",
      "evidence": "requirePermission('time:read'); invoices-core permission table maps invoice endpoints to invoices:read.",
      "repro": ""
    },
    {
      "id": "invoices-core-028",
      "severity": "P3",
      "type": "divergent",
      "summary": "Cursor JSON uses created_at key; spec documents createdAt",
      "spec_ref": "List Performance cursor shape",
      "code_ref": "packages/db/src/queries/invoices.ts:192",
      "evidence": "encodeCursor serializes {id, created_at}; spec interface shows {id, createdAt}. Internally consistent but divergent from spec field name.",
      "repro": ""
    },
    {
      "id": "invoices-core-029",
      "severity": "P3",
      "type": "divergent",
      "summary": "List/detail display customer and project UUIDs not resolved names",
      "spec_ref": "Invoice list columns Customer, Project",
      "code_ref": "apps/zync-app/src/pages/invoices/InvoicePage.tsx:343",
      "evidence": "Table cells render invoice.customerId and invoice.projectId raw strings.",
      "repro": ""
    },
    {
      "id": "invoices-core-030",
      "severity": "P3",
      "type": "divergent",
      "summary": "RTL table dir attribute on <table> not set for Hebrew",
      "spec_ref": "PDF Locale — RTL table layout",
      "code_ref": "packages/db/src/queries/invoices.ts:1696",
      "evidence": "renderInvoiceHtml sets <html dir> but line items <table> has no dir='rtl' attribute; spec requires dir on table for Hebrew column reversal.",
      "repro": ""
    }
  ],
  "summary": {
    "P0": 2,
    "P1": 3,
    "P2": 21,
    "P3": 4,
    "total": 30
  }
}
