{
  "slug": "contract-to-invoice",
  "spec_file": "docs/specs/2026-05-31-contract-to-invoice.md",
  "findings": [
    {
      "id": "contract-to-invoice-001",
      "severity": "P0",
      "type": "broken",
      "summary": "generateInvoiceFromContract creates zero-amount invoice; contract pricing never carried",
      "spec_ref": "Invoice New Page — line items pre-filled from contract pricing; Pre-fill logic",
      "code_ref": "packages/db/src/queries/contract-invoice.ts:116",
      "evidence": "Insert sets subtotal:'0', vatAmount:'0', total:'0' (:116-118). Line item unitPrice:'0', lineTotal:'0' (:132-135). No call to extractInvoicePrefillFromContract; grep extractInvoicePrefillFromContract in repo (excl .claude) → 0 implementation files.",
      "repro": "POST /api/contracts/{signedId}/generate-invoice → invoice.lines[0].unitPrice='0', invoice.total='0' despite signed contract amounts.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-002",
      "severity": "P0",
      "type": "broken",
      "summary": "Duplicate invoices possible — link stored in contract.metadata only; no invoices.contract_id dedup",
      "spec_ref": "Trigger Points — no invoice linked when invoices.contract_id empty; API POST /api/invoices sets invoices.contract_id",
      "code_ref": "packages/db/src/queries/contract-invoice.ts:98",
      "evidence": "Idempotency checks metadata.invoiceId only (:98-100), writes metadata.invoiceId (:147). grep contract_id in packages/db/src/schema/invoices.ts → 0 matches (proposalId exists at :140). createInvoiceSchema has no contractId (packages/db/src/queries/invoices.ts:41-56). UI link check reads metadata only (apps/zync-app/src/modules/contracts/ContractDetailPage.tsx:565-567). Contrast proposal dedup queries invoices.proposalId (packages/db/src/queries/proposal-invoice.ts:113-126).",
      "repro": "POST /api/invoices with manual draft for same customer while metadata.invoiceId unset → second billable invoice; or clear metadata.invoiceId after first generate → POST generate-invoice succeeds again.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-003",
      "severity": "P1",
      "type": "missing",
      "summary": "GET /api/contracts/:id/invoice linked-invoice lookup absent",
      "spec_ref": "API — GET /api/contracts/:id/invoice",
      "code_ref": "apps/zync-api/src/routes/contracts/index.ts:1",
      "evidence": "File header lists routes through /:id/lineage; grep '/:id/invoice' in apps/zync-api/src/routes/contracts → 0 matches. getContractInvoiceId exported (packages/db/src/queries/contract-invoice.ts:180) but grep getContractInvoiceId outside packages/db → 0 API/UI callers.",
      "repro": "GET /api/contracts/{id}/invoice → 404 (route not registered).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-004",
      "severity": "P1",
      "type": "missing",
      "summary": "POST /api/invoices does not accept or validate contractId",
      "spec_ref": "API — POST /api/invoices extended: accepts contract_id; contract must be SIGNED, same tenant",
      "code_ref": "packages/db/src/queries/invoices.ts:41",
      "evidence": "createInvoiceSchema fields: customerId, projectId, currency, dueDate, notes, source, lines, billedEntryIds, parentInvoiceId — no contractId (:41-56). POST handler passes parsed.data straight to createInvoice (apps/zync-api/src/routes/invoices/index.ts:136-151).",
      "repro": "POST /api/invoices with body.contractId → zod strips/validation-fails; invoices row never gets contract link.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-005",
      "severity": "P1",
      "type": "missing",
      "summary": "extractInvoicePrefillFromContract helper and ContractInvoiceLink types absent",
      "spec_ref": "Pre-fill logic; Plan Task 1–2",
      "code_ref": "NONE",
      "evidence": "glob packages/db/**/extract-invoice-prefill.ts → 0 files. grep ContractInvoiceLink|InvoicePrefillFromContract in packages/types → 0 matches. Only references are docs/plans/tasks/contract-to-invoice.md.",
      "repro": "",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-006",
      "severity": "P1",
      "type": "missing",
      "summary": "/invoices/new?contract_id= route and pre-fill UI not implemented",
      "spec_ref": "Invoice New Page (/invoices/new?contract_id={id}); Trigger Points — navigates to /invoices/new?contract_id={id}",
      "code_ref": "apps/zync-app/src/modules/invoices.tsx:22",
      "evidence": "Invoices module routes: index, reconcile, :id only (:22-46); no 'new' route. /invoices/new links exist (e.g. drafts.tsx:568) but resolve to InvoiceDetailPage with id='new'. InvoiceFormSheet has no contract_id/proposal_id query handling (apps/zync-app/src/pages/invoices/InvoiceFormSheet.tsx:1-55). grep useContractPrefill in apps/zync-app → 0 matches.",
      "repro": "Navigate /invoices/new?contract_id={uuid} → invoice detail fetch for id 'new' fails or 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-007",
      "severity": "P1",
      "type": "missing",
      "summary": "Contract detail Invoice tab absent",
      "spec_ref": "Contract Detail: Invoice Tab — [Details] [Signatories] [Invoice]",
      "code_ref": "apps/zync-app/src/modules/contracts/ContractDetailPage.tsx:758",
      "evidence": "Page sections: SignatoriesPanel (:761-764), Lineage, EntityHistoryTab — no Tabs, no Invoice tab, no ContractInvoiceTab.tsx (glob apps/zync-app/**/ContractInvoiceTab.tsx → 0).",
      "repro": "Open /contracts/{id} → no Invoice tab; no linked-invoice summary (number, total, status badge).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-008",
      "severity": "P1",
      "type": "missing",
      "summary": "Signing completion-flow Generate Invoice button absent",
      "spec_ref": "Trigger Points §1 — completion panel [Download PDF] [Generate Invoice →] [Done]",
      "code_ref": "NONE",
      "evidence": "grep ContractCompletionFlow|signed by all parties|Generate Invoice in apps/zync-app/src/features/contracts → 0 matches. glob **/ContractCompletionFlow.tsx → 0 files. No public signing completion UI in apps/zync-app (glob *sign* → SignatoriesPanel, useSignatories only).",
      "repro": "Complete all signatures → no completion panel with Generate Invoice CTA in app.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-009",
      "severity": "P1",
      "type": "missing",
      "summary": "Planned tests for contract-invoice flow absent",
      "spec_ref": "Plan Task 8 — contracts-invoice-link, invoices-create-contract-id, extract-invoice-prefill tests",
      "code_ref": "NONE",
      "evidence": "glob apps/zync-api/test/*contract*invoice* → 0. glob packages/db/test/extract-invoice-prefill.test.ts → 0. grep generate-invoice|contract-invoice in apps/zync-api/test → 0 matches.",
      "repro": "",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-010",
      "severity": "P2",
      "type": "divergent",
      "summary": "Undocumented POST /api/contracts/:id/generate-invoice auto-creates draft instead of spec pre-fill flow",
      "spec_ref": "Trigger Points; Architecture — pre-fill from contract, user saves via POST /api/invoices",
      "code_ref": "apps/zync-api/src/routes/contracts/index.ts:325",
      "evidence": "Route POST /:id/generate-invoice calls generateInvoiceFromContract (:325-345). Not listed in contract-to-invoice spec API section (spec lists GET .../invoice and POST /api/invoices). UI uses dialog + this endpoint (ContractDetailPage.tsx:92-103, 325).",
      "repro": "Signed contract → Generate Invoice dialog → immediate DRAFT invoice created server-side without user editing pre-filled lines.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-011",
      "severity": "P2",
      "type": "divergent",
      "summary": "Invoice link uses contracts.metadata.invoiceId instead of invoices.contract_id FK",
      "spec_ref": "Pre-fill logic — invoices.contract_id set on save; Contract detail checks invoices.contract_id",
      "code_ref": "packages/db/src/queries/contract-invoice.ts:147",
      "evidence": "UPDATE contracts SET metadata.invoiceId (:143-150). Invoice insert has no contract_id column (schema invoices.ts has proposalId :140, no contractId). contracts-esignature schema delta ALTER invoices ADD contract_id not applied (grep ADD COLUMN contract_id on invoices in packages/db/migrations → 0).",
      "repro": "After generate-invoice, SELECT contract_id FROM invoices WHERE id={invoiceId} → column does not exist / always null.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-012",
      "severity": "P2",
      "type": "divergent",
      "summary": "generate-invoice permission model diverges from spec (contracts:write+invoices:write vs contracts:read / invoices:write)",
      "spec_ref": "Plan — contracts:read for linked invoice lookup; invoices:write for create",
      "code_ref": "apps/zync-api/src/routes/contracts/index.ts:326",
      "evidence": "POST /:id/generate-invoice requires contracts:write (:326) and invoices:write (:333-335). Spec GET /api/contracts/:id/invoice requires contracts:read; no contracts:write generate endpoint defined.",
      "repro": "User with contracts:read + invoices:write but not contracts:write cannot use spec GET lookup; current generate path returns 403.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-013",
      "severity": "P2",
      "type": "divergent",
      "summary": "Cross-tenant missing contract on generate-invoice returns 500 not 404",
      "spec_ref": "Plan Task 3/4 — 404 when contract not found in tenant",
      "code_ref": "packages/db/src/queries/contract-invoice.ts:93",
      "evidence": "generateInvoiceFromContract throws new Error('Contract not found') (:93), not ContractNotFoundError. Route only maps ContractNotFoundError to 404 (:347); generic Error re-thrown (:358) → 500.",
      "repro": "POST /api/contracts/{other-tenant-uuid}/generate-invoice with valid session → 500 Internal Server Error (tenant filter returns no row).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-014",
      "severity": "P2",
      "type": "divergent",
      "summary": "generate-invoice rejects contracts without customer; spec allows nullable customer pre-fill",
      "spec_ref": "Pre-fill logic — customer_id from contracts.customer_id; Invoice New Page customer pre-filled (editable)",
      "code_ref": "packages/db/src/queries/contract-invoice.ts:95",
      "evidence": "Throws 'Contract has no customer — add a customer before generating an invoice' when !contract.customerId (:95). Plan Task 5: leave customer unselected if customerId null.",
      "repro": "Signed contract with customer_id NULL → POST generate-invoice → 409 with customer error.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-015",
      "severity": "P2",
      "type": "divergent",
      "summary": "Contract content is plain text; pricing JSONB extraction path impossible",
      "spec_ref": "Pre-fill logic — line items from content JSONB pricing section",
      "code_ref": "packages/db/src/schema/contracts.ts:45",
      "evidence": "contracts.content defined as text('content') (:45), not jsonb. ContractDetailPage renders contract.content in <pre> (:751). Plan Task 2 expects Tiptap JSONB with {{amount}} / pricing rows.",
      "repro": "Cannot parse structured pricing section from contracts.content as spec describes.",
      "fix_direction": "spec_review"
    },
    {
      "id": "contract-to-invoice-016",
      "severity": "P2",
      "type": "divergent",
      "summary": "Generate Invoice CTA uses modal POST instead of link to /invoices/new?contract_id=",
      "spec_ref": "Trigger Points §2 — [Generate Invoice] navigates to /invoices/new?contract_id={id}",
      "code_ref": "apps/zync-app/src/modules/contracts/ContractDetailPage.tsx:576",
      "evidence": "Button opens GenerateInvoiceDialog (:576-578) calling POST /api/contracts/:id/generate-invoice (:92-103), not navigate to pre-fill route.",
      "repro": "Click Generate Invoice on signed contract → dialog → server auto-create; never opens new-invoice pre-fill page.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "contract-to-invoice-017",
      "severity": "P2",
      "type": "divergent",
      "summary": "Status literal casing — spec SIGNED vs schema signed",
      "spec_ref": "Trigger Points — contracts.status = 'SIGNED'",
      "code_ref": "packages/db/src/schema/contracts.ts:75",
      "evidence": "CHECK constraint allows 'draft','sent','signed','voided' lowercase (:74-76). generateInvoiceFromContract checks contract.status !== 'signed' (:94). UI checks contract.status === 'signed' (ContractDetailPage.tsx:546).",
      "repro": "",
      "fix_direction": "spec_review"
    },
    {
      "id": "contract-to-invoice-018",
      "severity": "P3",
      "type": "divergent",
      "summary": "Plan registry marks contract-to-invoice COMPLETED despite spec flow largely unimplemented",
      "spec_ref": "Plan Task acceptance criteria (Tasks 1–8)",
      "code_ref": "docs/plans/status/contract-to-invoice.json:4",
      "evidence": "status:'COMPLETED', completed_at:'2026-06-03' while Tasks 1–8 unchecked in docs/plans/tasks/contract-to-invoice.md and core deliverables missing.",
      "repro": "",
      "fix_direction": "spec_review"
    }
  ],
  "summary": { "P0": 2, "P1": 7, "P2": 8, "P3": 1, "total": 18 }
}
