{
  "slug": "bituach-leumi",
  "spec_file": "docs/specs/2026-06-01-bituach-leumi.md",
  "findings": [
    {
      "id": "bituach-leumi-001",
      "severity": "P0",
      "type": "money",
      "summary": "Gross revenue excludes credit_note invoices without subtracting their amounts — overstates net income and NII estimate when credit notes exist",
      "spec_ref": "Data Sources — Gross revenue: SUM(invoices.total - invoices.vat_amount) WHERE status NOT IN ('DRAFT','SENT','VOID') AND tax_issue_date IN year (no credit_note exclusion)",
      "code_ref": "packages/db/src/queries/nii-advances.ts:44",
      "evidence": "getNiiReportAggregates filters sql`${invoices.source} != 'credit_note'` (:44) and sums only non-credit invoices (:36-37). Credit notes are stored with negative totals (packages/db/src/schema/invoices.ts:116; invoices.ts:1327-1330 uses negative remaining). Excluding them omits the credit entirely instead of reducing revenue. Same filter in apps/zync-api/src/lib/bituach-leumi-xlsx.ts:76. tax-reports.ts:53-67 handles credit notes via a separate ABS(SUM) deduction; bituach-leumi has no equivalent.",
      "repro": "Issue a TAX_ISSUED credit_note for ₪1,000 net in the report year → gross_revenue_net_vat unchanged vs pre-credit → net_income and nii_contributions.total overstated by ₪1,000 (before marginal banding).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-002",
      "severity": "P1",
      "type": "missing",
      "summary": "NII advance write routes require reports:write but that permission is not seeded — POST/PATCH/DELETE always 403",
      "spec_ref": "API — POST/PATCH/DELETE /api/nii-advances; Plan Task 7 — Guarded by requirePermission('reports:write')",
      "code_ref": "apps/zync-api/src/routes/nii-advances.ts:76",
      "evidence": "POST (:76), PATCH (:112), DELETE (:141) call requirePermission('reports:write'). packages/db/src/seed/permission-keys.ts:32 lists reports:read, reports:export, reports:export_external only — no reports:write. grep reports:write packages/db packages/auth — zero seed entries. Plan fallback to settings:write not implemented in routes.",
      "repro": "Authenticated OWNER session → POST /api/nii-advances {year,month,amount,paid_at} → 403 {requiredPermission:'reports:write'}. NiiAdvancesTab Add advance always fails.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-003",
      "severity": "P2",
      "type": "divergence",
      "summary": "Report tier-gated to Business+ in nav and hub despite spec declaring All tiers",
      "spec_ref": "Spec header — Tier: All tiers; Architecture — All tiers | Not Business+ | Freelancers are primary audience",
      "code_ref": "apps/zync-app/src/shell/nav-model.ts:284",
      "evidence": "nav-model.ts:280-284 sets minTier:'business' on bituach-leumi nav item. apps/zync-app/src/features/reports/nav-map.ts:239 requiresBusiness:true. API routes (apps/zync-api/src/routes/reports/bituach-leumi.ts) have no requireTier — only nav/hub block Freelancer tier users.",
      "repro": "Freelancer-tier tenant → Tax & Compliance nav tile hidden; direct /reports/bituach-leumi may still load if user has reports:read.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-004",
      "severity": "P2",
      "type": "divergence",
      "summary": "Excel Invoices tab omits customer column required by spec",
      "spec_ref": "Excel Export — Invoices tab: number, date, customer, amount (net of VAT)",
      "code_ref": "apps/zync-api/src/lib/bituach-leumi-xlsx.ts:154",
      "evidence": "Invoices sheet columns are Invoice Number, Date, Total, VAT, Net of VAT (:154-161). Select at :64-69 fetches invoiceNumber, taxIssueDate, total, vatAmount only — no customer join or name field.",
      "repro": "GET /api/reports/bituach-leumi/xlsx?year=2026 → Invoices sheet has no Customer column.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-005",
      "severity": "P2",
      "type": "divergence",
      "summary": "GET /api/nii-advances returns bare array; plan specifies { advances, ytd_total }",
      "spec_ref": "Plan Task 7 — GET /api/nii-advances?year= → { advances: NiiAdvancePayment[], ytd_total: number }",
      "code_ref": "apps/zync-api/src/routes/nii-advances.ts:70",
      "evidence": "Route returns c.json(advances, 200) (:70-71) where advances is NiiAdvancePayment[]. YTD total only available via GET /api/reports/bituach-leumi advances_paid block.",
      "repro": "GET /api/nii-advances?year=2026 → JSON array, not wrapped object with ytd_total.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-006",
      "severity": "P2",
      "type": "divergence",
      "summary": "Duplicate-month POST 409 does not switch UI to edit mode as spec/plan require",
      "spec_ref": "Advances tab UI — re-submitting existing month rejected 409; form switches to edit mode (PATCH); Plan Task 9 acceptance",
      "code_ref": "apps/zync-app/src/pages/reports/NiiAdvancesTab.tsx:111",
      "evidence": "NiiAdvancesTab handleSubmit catch sets formError string (:111-113) — no branch on 409 to load existing row into editingId. useCreateNiiAdvance throws generic Error from err.error (:33-36).",
      "repro": "Record March advance → POST same month again → error message shown; Edit pre-fill/PATCH path not auto-triggered.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-007",
      "severity": "P2",
      "type": "divergence",
      "summary": "loadNIIRates JSON-parses KV without validating NIIRates fields — malformed config yields NaN contributions",
      "spec_ref": "NII Contribution Calculation — rates/thresholds from nii_rates:{year}; Plan Task 4 — throw clear error if not configured",
      "code_ref": "apps/zync-api/src/lib/nii-rates.ts:21",
      "evidence": "loadNIIRates returns JSON.parse(raw) as NIIRates (:21) with no zod/schema check on reduced_rate, full_rate, band1_monthly_ils, income_cap_monthly_ils. estimateNIIContributions (apps/zync-api/src/lib/nii-estimate.ts:30-35) arithmetic on undefined → NaN; Math.round(NaN)*12 → NaN serialized as null in JSON.",
      "repro": "KV nii_rates:2026 = '{}' → GET /api/reports/bituach-leumi?year=2026 → nii_contributions fields null/invalid despite 200.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-008",
      "severity": "P2",
      "type": "divergence",
      "summary": "Export Excel button not disabled without reports:export permission",
      "spec_ref": "Report UI — Export Excel; Plan Task 8 — disabled unless user has reports:export",
      "code_ref": "apps/zync-app/src/pages/reports/BituachLeumiPage.tsx:134",
      "evidence": "Button disabled={!data} only (:134). grep reports:export BituachLeumiPage.tsx — no permission check. xlsx route correctly requires reports:export (bituach-leumi.ts:42).",
      "repro": "User with reports:read but not reports:export → Export Excel enabled → GET /api/reports/bituach-leumi/xlsx → 403.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-009",
      "severity": "P2",
      "type": "missing",
      "summary": "No unit tests for estimateNIIContributions marginal banding, cap, or zero-income edge cases",
      "spec_ref": "Plan Task 3 acceptance — band1-only reduced rate; band1→cap full rate; above cap zero; zero income returns zeros",
      "code_ref": "apps/zync-api/src/lib/nii-estimate.ts:21",
      "evidence": "grep estimateNII|nii-estimate across *.test.* — zero matches. Formula implemented at nii-estimate.ts:21-44 but untested.",
      "repro": "pnpm test --filter nii-estimate → no test file exists.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-010",
      "severity": "P2",
      "type": "divergence",
      "summary": "NII estimator lives in apps/zync-api not packages/reports per plan",
      "spec_ref": "Plan Architecture — packages/reports/src/nii-estimate.ts (estimateNIIContributions)",
      "code_ref": "apps/zync-api/src/lib/nii-estimate.ts:21",
      "evidence": "Implementation at apps/zync-api/src/lib/nii-estimate.ts. glob packages/reports — directory absent. packages/reports/src/nii-estimate.ts — not found.",
      "repro": "ls packages/reports → path does not exist.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-011",
      "severity": "P3",
      "type": "divergence",
      "summary": "DELETE /api/nii-advances/:id returns 200 JSON instead of 204",
      "spec_ref": "Plan Task 7 — DELETE /api/nii-advances/:id → 204",
      "code_ref": "apps/zync-api/src/routes/nii-advances.ts:155",
      "evidence": "Route returns c.json({ success: true }, 200) (:155) when deleted.",
      "repro": "DELETE /api/nii-advances/:id → HTTP 200 body {success:true}.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-012",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "Estimate disclaimer missing role=\"note\" accessibility semantics required by plan",
      "spec_ref": "Plan Task 8 — estimate notice with role=\"note\" / appropriate aria semantics",
      "code_ref": "apps/zync-app/src/pages/reports/BituachLeumiPage.tsx:279",
      "evidence": "Disclaimer Stack at :279-291 has no role=\"note\" attribute.",
      "repro": "Inspect disclaimer element in BituachLeumiPage — no role=note.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-013",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "Depreciation adjustment requires Apply button; plan specifies debounced live re-query on change",
      "spec_ref": "Plan Task 8 — re-queries with depreciation_deduction on change (debounced)",
      "code_ref": "apps/zync-app/src/pages/reports/BituachLeumiPage.tsx:320",
      "evidence": "depreciation state updated only in applyDepreciation() on Apply click (:87-90, :320-322). useBituachLeumiReport keyed on depreciation state, not live input.",
      "repro": "Type depreciation value → net income/NII unchanged until Apply clicked.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "bituach-leumi-014",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "Drizzle schema omits UNIQUE(tenant_id,user_id,year,month) though migration defines it",
      "spec_ref": "nii_advance_payments DDL — UNIQUE (tenant_id, user_id, year, month)",
      "code_ref": "packages/db/src/schema/nii-advance-payments.ts:27",
      "evidence": "Schema exports check + index (:27-30) but no unique() constraint. Migration 0042_nii_advance_payments.sql:12 defines CONSTRAINT nii_advance_payments_tenant_user_year_month_unique.",
      "repro": "Compare schema file vs packages/db/migrations/0042_nii_advance_payments.sql — unique only in SQL migration.",
      "fix_direction": "code_to_spec"
    }
  ],
  "summary": {
    "P0": 1,
    "P1": 1,
    "P2": 8,
    "P3": 4,
    "total": 14
  }
}
