{
  "slug": "expenses-module",
  "spec_file": "docs/specs/2026-05-30-expenses-module.md",
  "audited_against": "654a8ff",
  "findings": [
    {
      "id": "ex-001",
      "severity": "P0",
      "code_ref": "packages/expenses/src/per-diem.ts:46",
      "spec_ref": "2026-05-30-expenses-module.md — Per-Diem Rules (tier rates are flat allowances; domestic_half_day default ₪50)",
      "summary": "Per-diem half-day logs ₪25 instead of configured half-day allowance ₪50",
      "current_behavior": "Half-day selects domestic_half_day rate (50) then computes amount = rate × days with days=0.5 → 25.00. UI mirrors at PerDiemForm.tsx:59.",
      "expected_per_spec": "Half-day allowance equals the configured tier rate (₪50 domestic half-day), not tier-rate × 0.5.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "For discrete half/full tiers set amount = rate (store perDiemDays separately); reserve rate×days only for custom multi-day entries using full-day rate."
    },
    {
      "id": "ex-002",
      "severity": "P0",
      "code_ref": "packages/expenses/src/ai/ocr.ts:104",
      "spec_ref": "2026-05-30-expenses-module.md — Processing Pipeline step 2 (amount := invoice_total normalized to ILS)",
      "summary": "Foreign-currency OCR receipts store raw foreign total in canonical amount column",
      "current_behavior": "When currency !== 'ILS', amount is set to String(invoice_total) with a pass-through comment; no exchange-rate conversion in pipeline or queue consumer.",
      "expected_per_spec": "Canonical amount must be ILS-normalized gross total; invoice_total+currency retain raw OCR capture.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Call tenant country adapter / FX service during OCR persist; store ILS in amount and keep original currency+invoice_total."
    },
    {
      "id": "ex-003",
      "severity": "P0",
      "code_ref": "packages/db/src/queries/expense-approvals.ts:37",
      "spec_ref": "2026-05-31-expense-approval-workflow.md — Expense lifecycle when threshold configured",
      "summary": "Approval threshold gate never applied — resolveApprovalStatus is defined but never called",
      "current_behavior": "resolveApprovalStatus exists but grep shows zero production callers. Upload, per-diem, OCR completion, and recurring generation all leave approval_status at default not_required.",
      "expected_per_spec": "Amount > expense_approval_threshold_ils on Business+ tenants should set approval_status=pending at creation/finalization; queue should populate pending for approvers.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Invoke resolveApprovalStatus after amount is known (upload finalize, per-diem create, expense-process COMPLETED, recurring generate) and persist approval_status."
    },
    {
      "id": "ex-004",
      "severity": "P0",
      "code_ref": "packages/db/src/queries/recurring-expenses.ts:229",
      "spec_ref": "2026-05-30-expenses-module.md — Data Model file_type check; 2026-06-01-recurring-expenses.md — auto-generation",
      "summary": "Recurring expense cron inserts file_type='auto' violating DB CHECK constraint",
      "current_behavior": "generateDueExpenses inserts fileType: 'auto' but expenses_file_type_check allows only pdf|jpg|png|heic (schema/expenses.ts:146, migration 0003).",
      "expected_per_spec": "Cron-generated expenses must insert valid expense rows; generation should not fail per template.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Use an allowed sentinel (e.g. pdf) plus sourceMetadata.recurring=true, or extend the CHECK to include a synthetic type."
    },
    {
      "id": "ex-005",
      "severity": "P1",
      "code_ref": "packages/db/src/queries/expenses.ts:618",
      "spec_ref": "2026-05-30-expenses-module.md — VAT Summary (PCN874) Output VAT from invoices",
      "summary": "PCN874 output VAT hardcoded to zero — issued invoice VAT never queried",
      "current_behavior": "vatSummaryPcn874 sets const outputVat = '0' with no invoices join; tax-reports.ts already aggregates output VAT elsewhere.",
      "expected_per_spec": "Output VAT (מע\"מ עסקאות) summed from invoices for the period; net VAT due includes output minus input.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Reuse tax-reports output VAT aggregation or query invoices.vat_amount for the report period."
    },
    {
      "id": "ex-006",
      "severity": "P1",
      "code_ref": "apps/zync-api/src/intake/email-expense.ts:42",
      "spec_ref": "2026-05-30-expenses-module.md — Inbound Channels Email forwarding",
      "summary": "Email expense intake handler exists but is not wired to any Worker/route",
      "current_behavior": "handleExpenseEmail is only defined in email-expense.ts; no imports from routes, queues, or email routing workers.",
      "expected_per_spec": "Forwarding to expenses@{tenantSlug}.zync.is creates expense records with source=email.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Register CF Email Routing worker that calls handleExpenseEmail and enqueues expense.process."
    },
    {
      "id": "ex-007",
      "severity": "P1",
      "code_ref": "apps/zync-api/src/routes/webhooks/whatsapp.ts:75",
      "spec_ref": "2026-05-30-expenses-module.md — Inbound Channels WhatsApp / Telegram",
      "summary": "WhatsApp/Telegram webhooks route to comms/AI queues, not expense intake",
      "current_behavior": "WhatsApp/Telegram handlers enqueue comms.inbound or ai-telegram jobs; no path creates expenses with source whatsapp|telegram from receipt photos.",
      "expected_per_spec": "Receipt photos via tenant bot create expense records with appropriate source metadata.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Add expense-intake branch on image attachments in comms inbound consumer (mirror email-expense flow)."
    },
    {
      "id": "ex-008",
      "severity": "P1",
      "code_ref": "apps/zync-app/src/features/expenses/ExpensesPage.tsx:138",
      "spec_ref": "2026-05-30-expenses-module.md — Expense List bulk actions",
      "summary": "Bulk toolbar only supports delete — evaluate-all and export-selected missing",
      "current_behavior": "Selected-rows toolbar renders deleteSelected only; no evaluateAll or exportSelected handlers.",
      "expected_per_spec": "Bulk actions: Evaluate all pending, Export selected, Delete selected.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Add bulk evaluate endpoint call and Excel/CSV export for selected IDs in toolbar."
    },
    {
      "id": "ex-009",
      "severity": "P1",
      "code_ref": "apps/zync-app/src/features/expenses/ExpensesPage.tsx:32",
      "spec_ref": "2026-05-30-expenses-module.md — Expense List filters (URL-synced)",
      "summary": "Expense list reads URL filter params but renders no filter controls",
      "current_behavior": "dateFrom/dateTo/category/status/source/projectId read from searchParams; JSX has tabs+table only. deductionPct never read from URL.",
      "expected_per_spec": "Filter UI for date range, category, deduction %, status, source, project synced to URL.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Add filter bar component wired to searchParams including deductionPct."
    },
    {
      "id": "ex-010",
      "severity": "P1",
      "code_ref": "apps/zync-app/src/features/expenses/MileageTab.tsx:32",
      "spec_ref": "2026-05-30-expenses-module.md — Mileage Tab list filters and trip list",
      "summary": "Mileage tab list uses wrong query params and expects wrong response shape",
      "current_behavior": "UI sends dateFrom/dateTo and expects {items,nextCursor,total}. API expects startDate/endDate and returns {entries} (mileage.ts:62-79).",
      "expected_per_spec": "Date-range filters drive mileage trip list; client parses API list response.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Align client query param names and map API entries[] into items/total."
    },
    {
      "id": "ex-011",
      "severity": "P1",
      "code_ref": "apps/zync-app/src/features/expenses/MileageTab.tsx:73",
      "spec_ref": "2026-05-30-expenses-module.md — Mileage annual report export from logbook header",
      "summary": "Annual mileage report link targets non-existent /api/mileage/annual-report/xlsx",
      "current_behavior": "Header anchor href=/api/mileage/annual-report/xlsx; API only exposes GET /api/mileage/summary.",
      "expected_per_spec": "Annual report export reachable from logbook header (spec 166).",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Add xlsx export route or link to existing summary export endpoint."
    },
    {
      "id": "ex-012",
      "severity": "P1",
      "code_ref": "packages/db/src/queries/expenses.ts:585",
      "spec_ref": "2026-05-31-expense-approval-workflow.md — Report Exclusion (PCN874 approved+not_required only)",
      "summary": "PCN874 VAT summary ignores approval_status — pending/rejected expenses included in input VAT",
      "current_behavior": "vatSummaryPcn874 filters vatDeductible and deductionPct but not approval_status; tax-reports.ts filters approved|not_required.",
      "expected_per_spec": "PCN874 includes only approved and not_required expenses; pending/rejected excluded from tax reports.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Add inArray(expenses.approvalStatus, ['approved','not_required']) to vatSummaryPcn874 WHERE clauses."
    },
    {
      "id": "ex-013",
      "severity": "P1",
      "code_ref": "packages/db/src/queries/expenses.ts:147",
      "spec_ref": "2026-06-01-recurring-expenses.md — Recurring Expense List (/expenses/recurring)",
      "summary": "Recurring tab filters wrong metadata; templates page built but not routed",
      "current_behavior": "tab=recurring filters sourceMetadata->>'recurring' but cron never sets it. routes/expenses/recurring.tsx exists but modules/expenses/index.tsx has no /recurring route.",
      "expected_per_spec": "Recurring tab shows template management UI at /expenses/recurring linked from expenses header.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Mount RecurringExpensesPage route; replace tab filter with link or template list; tag generated expenses in sourceMetadata."
    },
    {
      "id": "ex-014",
      "severity": "P1",
      "code_ref": "apps/zync-api/src/routes/expenses/index.ts:76",
      "spec_ref": "2026-05-31-expense-personal-business-split.md — API PATCH business_percent; Expense Form split field",
      "summary": "Personal/business split column exists but API and UI are unwired",
      "current_behavior": "expenses.business_percent in schema; SplitField.tsx built but unused. patchExpenseSchema omits business_percent; serializeExpenseRow omits it; reports SUM raw amount.",
      "expected_per_spec": "PATCH/GET expose business_percent; detail form has split slider; profitability uses business amount.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Add business_percent to patch schema, serializer, ExpenseDetailSheet via SplitField; scale report/invoice amounts by percent."
    },
    {
      "id": "ex-015",
      "severity": "P1",
      "code_ref": "packages/db/src/queries/expenses.ts:541",
      "spec_ref": "2026-05-31-expense-approval-workflow.md — Report Exclusion (reject rows from expense reports)",
      "summary": "Expense report query does not exclude approval_status=rejected rows",
      "current_behavior": "expenseReport WHERE only tenant_id, deleted_at, and optional filters; no approval_status predicate.",
      "expected_per_spec": "Expense reports exclude rejected rows; pending rows included with indicator.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Add sql approval_status != 'rejected' (or explicit inclusion rules) to expenseReport."
    },
    {
      "id": "ex-016",
      "severity": "P2",
      "code_ref": "apps/zync-api/wrangler.toml:107",
      "spec_ref": "2026-05-30-expenses-module.md — RATE_LIMITER_EXPENSE_UPLOAD 10 uploads/minute",
      "summary": "Upload rate limiter configured 20/min in wrangler vs spec 10/min",
      "current_behavior": "RATE_LIMITER_EXPENSE_UPLOAD limit=20 period=60; route error message still says 10/min.",
      "expected_per_spec": "10 uploads/minute per user via CF RateLimiter binding.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Set wrangler limit=10 or update spec if 20 is intentional."
    },
    {
      "id": "ex-017",
      "severity": "P2",
      "code_ref": "packages/db/src/queries/expenses.ts:642",
      "spec_ref": "2026-05-30-expenses-module.md — Vendor Analysis group by vendor_id",
      "summary": "Vendor analysis never groups by vendor_id — vendorId always null in SELECT",
      "current_behavior": "vendorId: sql`null::text` and GROUP BY normalized vendor_name only despite expenses.vendor_id column.",
      "expected_per_spec": "Group by linked vendors entity when vendor_id present; fallback to vendor_name.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "COALESCE vendor_id grouping with vendors.name join for linked rows."
    },
    {
      "id": "ex-018",
      "severity": "P2",
      "code_ref": "apps/zync-app/src/shell/nav-model.ts:185",
      "spec_ref": "2026-05-30-expenses-module.md — Mileage logbook link in expenses secondary nav",
      "summary": "Sidebar has Expenses link only — no Mileage logbook sibling nav item",
      "current_behavior": "nav.expenses → /expenses; mileage reachable only via tab Link inside ExpensesPage.",
      "expected_per_spec": "Mileage logbook link appears in expenses secondary nav / sidebar.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Add nav item to /expenses/mileage under expenses module group."
    },
    {
      "id": "ex-019",
      "severity": "P2",
      "code_ref": "apps/zync-api/src/routes/expenses/settings.ts:59",
      "spec_ref": "2026-05-31-expense-settings-ui.md — settings.expenses.update audit row",
      "summary": "PATCH /api/settings/expenses persists without audit_log entry",
      "current_behavior": "updateExpenseSettings called directly; no audit_log insert unlike expense field PATCH.",
      "expected_per_spec": "Settings changes write audit row action settings.expenses.update in same transaction.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Wrap updateExpenseSettings in transaction with audit_log insert."
    },
    {
      "id": "ex-020",
      "severity": "P2",
      "code_ref": "apps/zync-api/src/queues/expense-process.ts:183",
      "spec_ref": "2026-05-30-expenses-module.md — Processing Pipeline transient errors retry",
      "summary": "Transient queue failure marks FAILED and emits expense.failed before retry",
      "current_behavior": "Outer catch sets status FAILED + webhook then msg.retry(); spurious failed state/webhook on first transient error.",
      "expected_per_spec": "Terminal AI failures mark FAILED; transient infra errors retry without terminal status.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Only mark FAILED on terminal errors or final retry exhaustion; leave PROCESSING during retries."
    },
    {
      "id": "ex-021",
      "severity": "P2",
      "code_ref": "apps/zync-app/src/features/expenses/ExpenseTable.tsx:182",
      "spec_ref": "2026-05-30-expenses-module.md — Expense List receipt thumbnail column",
      "summary": "List receipt column shows PDF/IMG placeholder text, not signed thumbnail",
      "current_behavior": "ExpenseRow renders static PDF/IMG span; no file URL fetch or img element.",
      "expected_per_spec": "Receipt thumbnail image in list column.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Lazy-load signed /api/expenses/:id/file URLs for completed rows."
    },
    {
      "id": "ex-022",
      "severity": "P2",
      "code_ref": "apps/zync-app/src/features/expenses/MileageTab.tsx:184",
      "spec_ref": "2026-05-30-expenses-module.md — Mileage Log Trip POST /api/mileage",
      "summary": "Log Trip slide-over sends snake_case fields and date-only string rejected by API schema",
      "current_behavior": "POST body uses from_location, date YYYY-MM-DD; createMileageEntrySchema expects camelCase fromLocation and datetime offset.",
      "expected_per_spec": "Log Trip form POST creates mileage entry via validated API contract.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Map to camelCase and ISO datetime; include required purpose/isBusinessTrip fields."
    },
    {
      "id": "ex-023",
      "severity": "P2",
      "code_ref": "packages/db/src/queries/expenses.ts:593",
      "spec_ref": "2026-05-30-expenses-module.md — VAT Summary input VAT for deduction_pct>0",
      "summary": "PCN874 splits 100% vs partial VAT buckets; base spec prose is looser",
      "current_behavior": "inputVat requires deductionPct=100; partialInputVat scales 0<pct<100. Aligns with plan tasks but diverges from spec line 317 literal.",
      "expected_per_spec": "Clarify whether all deduction_pct>0 VAT sums at full amount or partial scaling applies.",
      "fix_direction": "spec_review",
      "suggested_fix": "Orchestrator to reconcile base spec prose with implemented split-bucket logic."
    },
    {
      "id": "ex-024",
      "severity": "P2",
      "code_ref": "apps/zync-app/src/features/expenses/UploadDropzone.tsx:235",
      "spec_ref": "2026-05-30-expenses-module.md — Upload accessibility success role=status",
      "summary": "Upload announcements use role=alert for both success and failure",
      "current_behavior": "Single div role=alert aria-live=assertive receives all upload announcements.",
      "expected_per_spec": "Success uses role=status; failure uses role=alert.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Split live regions or toggle role based on outcome."
    },
    {
      "id": "ex-025",
      "severity": "P2",
      "code_ref": "apps/zync-api/src/routes/expenses/index.ts:321",
      "spec_ref": "2026-05-30-expenses-module.md — DELETE /api/expenses/:id soft delete",
      "summary": "DELETE returns ok:true even when expense missing or other-tenant",
      "current_behavior": "softDeleteExpense runs but route always returns {ok:true} without checking rows affected.",
      "expected_per_spec": "Delete should indicate not found when zero rows updated.",
      "fix_direction": "code_to_spec",
      "suggested_fix": "Return 404 when softDelete affects zero rows."
    }
  ],
  "old_findings_status": [
    { "id": "expenses-module-001", "status": "STILL_OPEN" },
    { "id": "expenses-module-002", "status": "STILL_OPEN" },
    { "id": "expenses-module-003", "status": "ALREADY_FIXED" },
    { "id": "expenses-module-004", "status": "ALREADY_FIXED" },
    { "id": "expenses-module-005", "status": "STILL_OPEN" },
    { "id": "expenses-module-006", "status": "STILL_OPEN" },
    { "id": "expenses-module-007", "status": "STILL_OPEN" },
    { "id": "expenses-module-008", "status": "STILL_OPEN" },
    { "id": "expenses-module-009", "status": "STILL_OPEN" },
    { "id": "expenses-module-010", "status": "STILL_OPEN" },
    { "id": "expenses-module-011", "status": "STILL_OPEN" },
    { "id": "expenses-module-012", "status": "STALE_WRONG" },
    { "id": "expenses-module-013", "status": "STILL_OPEN" },
    { "id": "expenses-module-014", "status": "STILL_OPEN" },
    { "id": "expenses-module-015", "status": "STILL_OPEN" },
    { "id": "expenses-module-016", "status": "STILL_OPEN" },
    { "id": "expenses-module-017", "status": "STILL_OPEN" },
    { "id": "expenses-module-018", "status": "STILL_OPEN" },
    { "id": "expenses-module-019", "status": "STILL_OPEN" },
    { "id": "expenses-module-020", "status": "STILL_OPEN" },
    { "id": "expenses-module-021", "status": "STILL_OPEN" },
    { "id": "expenses-module-022", "status": "STILL_OPEN" },
    { "id": "expenses-module-023", "status": "STILL_OPEN" }
  ],
  "summary": {
    "p0": 4,
    "p1": 11,
    "p2": 10,
    "new": 7,
    "already_fixed": 2,
    "stale_wrong": 1
  }
}
