{
  "spec_file": "docs/specs/2026-06-01-invoice-approval-workflow.md",
  "audited_at_commit": "e307875",
  "summary": "Marked COMPLETED but approvals queue route unwired, sidebar gated on missing permission, badge/activity/detail UX missing, and reject notify-email plus API contract gaps.",
  "findings": [
    {
      "id": "iaw-001",
      "severity": "P0",
      "title": "Approvals queue UI route not mounted",
      "detail": "Spec §Route `/invoices/approvals`; apps/zync-app/src/routes/invoices/approvals.tsx exists but apps/zync-app/src/modules/invoices.tsx:25-57 registers only index/reconcile/new/:id — no `approvals` route. Nav links to /invoices/approvals (nav-model.ts:151) are captured by `:id` and load InvoiceDetailPage for id=\"approvals\" instead of the queue.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-002",
      "severity": "P0",
      "title": "Sidebar Approvals item uses non-existent invoices:approve permission",
      "detail": "Spec §Route + §API require `invoices:write`; nav-model.ts:153 sets `permission: 'invoices:approve'` on the Approvals child. packages/db/src/seed/permission-keys.ts:14 lists only invoices:read/write/delete/send/void — no invoices:approve — so Sidebar.tsx:77 hides the nav item for every seeded role.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-003",
      "severity": "P1",
      "title": "Sidebar pending-count badge never rendered",
      "detail": "Spec §Sidebar Navigation requires Approvals badge with SENT count using --warning token; nav-model.ts:27,155 declares badge:'invoices_pending' but Sidebar.tsx SidebarNavItem/InvoicesSubTree never reads item.badge or fetches GET /api/invoices/approvals/count (implemented at apps/zync-api/src/routes/invoices/approvals.ts:71-79).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-004",
      "severity": "P1",
      "title": "Approvals nav gated to Business tier despite All-tiers spec",
      "detail": "Spec header Tier: All tiers; nav-model.ts:154 sets minTier:'business' on Approvals child, so useTierGate in Sidebar.tsx:72-127 shows upgrade prompt instead of the queue for Freelancer-tier tenants.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-005",
      "severity": "P1",
      "title": "Invoice detail approve action missing spec dialog and body fields",
      "detail": "Spec §Approve Action requires confirmation dialog with approved_on_behalf_of selector and optional approval note, posting to POST /api/invoices/:id/approve; InvoiceDetailPage.tsx:231-245 calls useApproveInvoice with invoice id only (api/invoices.ts:276 POST with empty body) — no ApproveDialog, no note, no on-behalf-of, button label \"Approve\" not \"Approve on behalf of customer\".",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-006",
      "severity": "P1",
      "title": "Invoice detail reject missing required reason UX and notify checkbox",
      "detail": "Spec §Reject Action requires sheet with reason min 5 chars and notify_customer checkbox; InvoiceDetailPage.tsx:372-410 inline form only has a textarea, enables submit on any non-empty trim (!rejectReason.trim()), no notify_customer, no min-length client guard; api/invoices.ts:291 sends only {reason}.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-007",
      "severity": "P1",
      "title": "Reject notify_customer never triggers customer email",
      "detail": "Task plan Task 4 + spec §Reject Action: when notify_customer is true, best-effort sendEmail to customer primary contact; rejectInvoiceEnhanced in packages/db/src/queries/invoice-approvals.ts:240-288 only persists rejection_notify_customer column; apps/zync-api/src/routes/invoices/index.ts:552-578 has no sendEmail branch (unlike send route at :470).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-008",
      "severity": "P1",
      "title": "Approval history not surfaced in Activity tab",
      "detail": "Spec §Approval History: Activity tab records \"Approved on behalf of customer by …\" with approval note; approveInvoiceEnhanced/rejectInvoiceEnhanced (invoice-approvals.ts:177-288) insert audit_log only — never appendInvoiceActivity to invoice_activities. ActivityTimeline on InvoiceDetailPage.tsx:641 reads GET /api/invoices/:id/activity (invoice_activities table).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-009",
      "severity": "P1",
      "title": "approved_by and approval columns not exposed on invoice API responses",
      "detail": "Spec §Approval History + Schema Delta require durable approved_by/approval_note/rejection_reason; schema columns exist (invoices.ts:128-131) and enhanced writers set them via SQL, but mapInvoice in packages/db/src/queries/invoices.ts:223-257 omits approvedBy/approvalNote/rejectionReason/rejectionNotifyCustomer from Invoice interface and GET responses.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-010",
      "severity": "P1",
      "title": "Approve/reject endpoints return stub not full InvoiceObject",
      "detail": "Spec §API POST approve/reject return serialized invoice; approveInvoiceEnhanced returns {id,status,approvedAt} (invoice-approvals.ts:229) and reject returns {id,status} (:287); routes/invoices/index.ts:537-538 and :568 return that stub directly instead of getInvoice/serializeInvoice full object.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-011",
      "severity": "P1",
      "title": "Bulk-approve API path diverges from spec",
      "detail": "Spec §API POST /api/invoices/bulk-approve; implementation mounts POST /api/invoices/approvals/bulk-approve at apps/zync-api/src/routes/invoices/approvals.ts:85 and apps/zync-app/src/routes/invoices/approvals.tsx:97 — spec path returns 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-012",
      "severity": "P1",
      "title": "No route or query tests for approval workflow",
      "detail": "Task plan Task 9 requires apps/zync-api/test/invoices-approvals.test.ts and packages/db/test/invoices-approval-queries.test.ts; repo grep finds zero *approval*.test* files covering listPendingApprovals, bulkApprove, permission 403, reject min-5, or cross-tenant 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-013",
      "severity": "P1",
      "title": "SENT invoice detail missing Resend action in action bar",
      "detail": "Spec §Approve Action wireframe shows [Resend] alongside Approve/Reject for SENT state; InvoiceDetailPage.tsx:215-229 only renders Send for DRAFT|REJECTED. SendAgainDialog exists under features/invoices/ but is only wired in EmailHistoryTab, not the SENT action bar.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-014",
      "severity": "P2",
      "title": "Queue row click does not open invoice detail",
      "detail": "Spec §Approvals Queue: \"Click row → opens invoice detail\"; approvals.tsx:533-584 rows are Stack listitems with checkbox and action buttons only — no navigate to /invoices/:id on row click.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-015",
      "severity": "P2",
      "title": "Bulk reject selected not implemented",
      "detail": "Spec §Approvals Queue wireframe shows [Reject selected] in selection bar; approvals.tsx:470-489 bulk bar only renders Approve selected — no bulk reject flow.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-016",
      "severity": "P2",
      "title": "GET approvals list response shape uses camelCase not spec snake_case",
      "detail": "Spec §API GET /api/invoices/approvals returns proforma_number, customer_name, sent_at; listPendingApprovals returns PendingApprovalItem with proformaNumber/customerName/sentAt (invoice-approvals.ts:66-74,151-158).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-017",
      "severity": "P2",
      "title": "Approve/reject request bodies expect camelCase not spec snake_case",
      "detail": "Spec §API body keys approved_on_behalf_of, notify_customer; approveInvoiceEnhancedSchema uses approvedOnBehalfOf (invoice-approvals.ts:37-44) and reject uses notifyCustomer (:46-50) with no snake_case alias — spec-shaped clients get 400 validation failures.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-018",
      "severity": "P2",
      "title": "bulkApprove ids cap 100 not task-plan 200",
      "detail": "Task plan Task 4 bulkApproveBodySchema max 200; bulkApproveSchema in invoice-approvals.ts:53-55 caps ids at max(100).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-019",
      "severity": "P2",
      "title": "Queue client schema rejects null customerId",
      "detail": "invoices.customerId is nullable (schema/invoices.ts:65-66); listPendingApprovals can return customerId:null (invoice-approvals.ts:154) but pendingApprovalItemSchema in apps/zync-app/src/lib/invoice-validation.ts:68 requires customerId: z.string().uuid() — Zod parse on fetch can throw for SENT invoices without customer.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "iaw-020",
      "severity": "P2",
      "title": "Approvals count endpoint permission looser than spec",
      "detail": "Spec §API all approval routes require invoices:write; GET /api/invoices/approvals/count uses requirePermission('invoices:read') at apps/zync-api/src/routes/invoices/approvals.ts:71 while list/bulk use invoices:write.",
      "fix_direction": "code_to_spec"
    }
  ]
}
