# Israeli Tax Compliance Reports — Implementation Plan

**Spec:** docs/specs/2026-06-01-israeli-tax-reports.md  ·  **Slug:** israeli-tax-reports  ·  **Wave:** 14
**Depends on:** expenses-module, financial-statements, foundation-auth-rbac, invoices-core, multi-currency

## Goal
Deliver the Israeli statutory compliance reports generated from existing Zync invoice/expense data: the **PCN874** monthly/bi-monthly VAT return (with an ITA-portal-ready CP1255 CSV), the **Annual Income Summary** (Excel handoff for accountants), and the **Advance Tax Estimate** (מקדמות מס הכנסה) computed from `tax_rates` brackets. No data pushed to the ITA — Zync produces the figures and machine-readable files; staff file manually via shaam.gov.il. This is the aggregation base that `bituach-leumi` (spec 175) builds upon.

## Architecture
All three reports are read-only aggregations over upstream tables — this spec adds **no new tables**, only two columns on the existing module-config `tenant_settings` table.

Data flow:
- **Output VAT** sums `invoices.vat_amount` and turnover sums `invoices.total_ils` (multi-currency snapshot; falls back to `invoices.total` when `total_ils` is NULL), filtered by `invoices.tax_issue_date`, excluding `DRAFT|SENT|VOID|BAD_DEBT` statuses and `source = 'credit_note'`. Credit notes are summed separately (ABS of negative `vat_amount`) and reduce net output VAT.
- **Input VAT** sums `expenses.vat_amount` filtered by `expenses.expense_date`, `status = 'COMPLETED'`, `approval_status IN ('approved','not_required')` (the spec-65 approval ledger), and `vat_deductible = true`.
- **Annual summary** aggregates invoiced income (`total_ils`/`total`, excl. VAT = `subtotal`), bad-debt write-offs (status `BAD_DEBT`/`WRITTEN_OFF`), and deductible expenses by `expense_category`, plus mileage and contractor payouts.
- **Advance tax** reads IL personal-income brackets from `tax_rates` (`tax_type LIKE 'personal_bracket_%'`, newest `effective_from` set, ascending `threshold_ils`) via marginal banding, and `tenant_settings.advance_tax_rate_pct`.

Upstream tables consumed (no redefinition): `invoices`, `invoice_lines`, `expenses`, `tax_rates`, `vat_rates`, `tenant_settings`, `users`. Upstream exports consumed: `getVatRate`, `getTaxRate`, `tenantQuery`, `authMiddleware`, `requirePermission`, `buildPaginated`, `DB`, `Env`. Shared export writers from `financial-statements` (spec 170): the RTL/Hebrew + formula-injection-safe xlsx/CSV writer at `apps/zync-api/src/lib/financial-export.ts`, and the CP1255 mapping at `apps/zync-api/src/lib/cp1255.ts` (shared with `uniform-format-export` spec 180). This plan creates `cp1255.ts` and `financial-export.ts` defensively only if absent (idempotent — both are owned upstream; reuse if present).

Permissions: `reports:read` (view endpoints), `reports:export` (CSV/xlsx), `settings:write` (PATCH tax settings) — all seeded by `foundation-auth-rbac`.

## Tech Stack
- **App:** `apps/zync-api` (Hono on Cloudflare Workers) for routes + report aggregation queries; `apps/zync-app` (Vite + React) for the three report pages and the tax-settings form.
- **Packages:** `@zync/db` (Drizzle schema + query helpers), `@zync/types` (report DTOs), `@zync/ui` (Card, Button, Select, DataTable, StatCard), `@zync/auth` (`authMiddleware`, `requirePermission`).
- **Libraries:** `exceljs` (xlsx generation, via shared writer), Zod (route validation), Drizzle ORM (Neon Postgres via Hyperdrive).
- **Bindings:** `DB` (Hyperdrive→Neon), `STORAGE` (R2 — not required; exports stream inline). No new bindings.

## Wave Plan
| Sub-wave | Tasks | Files touched | Parallelizable? |
|----------|-------|---------------|-----------------|
| 14a | 1 (schema delta), 2 (shared export libs) | `packages/db/src/schema/tenant-settings.ts`, drizzle migration, `apps/zync-api/src/lib/cp1255.ts`, `apps/zync-api/src/lib/financial-export.ts` | Yes (independent) |
| 14b | 3 (types), 4 (VAT aggregation queries), 5 (annual + advance-tax queries) | `packages/types`, `packages/db/src/queries/tax-reports.ts` | 3 parallel; 4 & 5 after 1 |
| 14c | 6 (PCN874 CSV writer), 7 (annual xlsx writer) | `apps/zync-api/src/lib/pcn874-csv.ts`, `apps/zync-api/src/lib/annual-summary-xlsx.ts` | Yes (after 2,4,5) |
| 14d | 8 (API routes), 9 (settings PATCH) | `apps/zync-api/src/routes/reports-tax.ts`, `apps/zync-api/src/routes/settings.ts` | Sequential after 3–7 |
| 14e | 10 (VAT page), 11 (annual page), 12 (advance-tax page), 13 (tax settings form) | `apps/zync-app/src/pages/reports/`, `apps/zync-app/src/pages/settings/` | Yes (after 8,9) |

## Tasks

### Task 1: Schema delta — tax settings columns on `tenant_settings`
**Blocks:** 4, 5, 9  ·  **Blocked by:** —
**Files:**
- Modify: `packages/db/src/schema/tenant-settings.ts`
- Create: `packages/db/drizzle/<timestamp>_tax_report_settings.sql`
**Steps:**
- [ ] Add `vatPeriod` and `advanceTaxRatePct` columns to the existing `tenantSettings` Drizzle table (do NOT create a new table — `tenant_settings` is an upstream module-config table).
- [ ] Generate the migration SQL exactly as the DDL below; the `vat_period` CHECK and DEFAULT must match the spec verbatim.
- [ ] Confirm no duplicate column if another spec already added them (idempotent `ADD COLUMN IF NOT EXISTS`).
**Schema / Interfaces:**
```sql
-- Tax settings on the existing tenant_settings table (module-config; NOT a new table)
ALTER TABLE tenant_settings ADD COLUMN IF NOT EXISTS vat_period TEXT NOT NULL DEFAULT 'bimonthly'
  CHECK (vat_period IN ('monthly', 'bimonthly'));
  -- monthly = file every month; bimonthly = every 2 months (turnover < ~₪1.5M)

ALTER TABLE tenant_settings ADD COLUMN IF NOT EXISTS advance_tax_rate_pct NUMERIC(5,2);
  -- ITA-assigned advance payment rate (% of annual tax); NULL = not configured
```
```ts
// Drizzle additions in tenantSettings table definition:
vatPeriod: text('vat_period').notNull().default('bimonthly'),       // CHECK ('monthly','bimonthly') via raw migration
advanceTaxRatePct: numeric('advance_tax_rate_pct', { precision: 5, scale: 2 }), // nullable
```
**Acceptance:**
- [ ] `pnpm drizzle-kit generate` produces only the two ALTER statements; `vat_period` defaults to `'bimonthly'`.
- [ ] Inserting `vat_period = 'quarterly'` fails the CHECK constraint.

### Task 2: Shared export libraries (CP1255 + RTL/formula-safe writer)
**Blocks:** 6, 7  ·  **Blocked by:** —
**Files:**
- Create (if absent): `apps/zync-api/src/lib/cp1255.ts`
- Create (if absent): `apps/zync-api/src/lib/financial-export.ts`
**Steps:**
- [ ] If `cp1255.ts` already exists (owned by `financial-statements`/`uniform-format-export`), reuse it — do not duplicate. Otherwise create it: a UTF-16-codepoint→CP1255 byte map covering Hebrew (U+05D0–U+05EA), ASCII, and the CP1255 punctuation range; export `encodeCp1255(s: string): Uint8Array` that throws/replaces on unmappable codepoints (no UTF-8 fallback, no BOM).
- [ ] If `financial-export.ts` already exists, reuse its `writeFinancialWorkbook`/`sanitizeCell` exports. Otherwise create it: a wrapper over `exceljs` that, for every worksheet, sets `worksheet.views[0].rightToLeft = true` and applies an explicit Hebrew-capable font (`Arial` or `David`) to header + data cells.
- [ ] Implement `sanitizeCell(value)` — formula-injection guard: any string value whose first char is one of `= + - @`, tab (`\t`), or carriage-return (`\r`) is prefixed with a single quote `'` before writing. Apply to every string cell including CSV cells.
- [ ] Export `toCsvRow(fields: string[]): string` that quotes/escapes fields and runs each through `sanitizeCell`.
**Schema / Interfaces:**
```ts
// apps/zync-api/src/lib/cp1255.ts
export function encodeCp1255(input: string): Uint8Array; // Windows-Hebrew bytes, no BOM, no UTF-8 fallback

// apps/zync-api/src/lib/financial-export.ts
import type { Workbook, Worksheet } from 'exceljs';
export function sanitizeCell(value: unknown): unknown;            // formula-injection guard ('= + - @ \t \r' -> prefix ')
export function newRtlWorksheet(wb: Workbook, name: string): Worksheet; // sets views[0].rightToLeft=true + Hebrew font
export function writeFinancialWorkbook(
  build: (wb: Workbook) => void
): Promise<Uint8Array>;                                            // returns xlsx bytes
export function toCsvRow(fields: string[]): string;               // CSV-escaped + sanitized
```
**Acceptance:**
- [ ] A worksheet built via `newRtlWorksheet` has `views[0].rightToLeft === true` and a Hebrew-capable font on cells.
- [ ] `sanitizeCell('=SUM(A1)')` returns `"'=SUM(A1)"`; `sanitizeCell('+1')`, `'-1'`, `'@x'`, `'\tx'`, `'\rx'` are all prefixed.
- [ ] `encodeCp1255('שלום')` yields CP1255 bytes (0xF9 0xEC 0xE5 0xED), no BOM, length 4.

### Task 3: Report DTO types
**Blocks:** 8, 10, 11, 12  ·  **Blocked by:** —
**Files:**
- Create: `packages/types/src/tax-reports.ts`
- Modify: `packages/types/src/index.ts` (re-export)
**Steps:**
- [ ] Define and export the report DTOs and the tax-settings DTO below.
- [ ] Re-export from the package barrel so routes and React pages share one source of truth.
**Schema / Interfaces:**
```ts
export interface Pcn874Report {
  periodStart: string;            // YYYY-MM-DD
  periodEnd: string;              // YYYY-MM-DD
  vatPeriod: 'monthly' | 'bimonthly';
  outputVat: {
    taxInvoiceCount: number;
    turnoverIls: number;          // SUM(total_ils|total), excl credit notes
    outputVatIls: number;         // SUM(vat_amount), excl credit notes
    creditNoteCount: number;
    creditNoteTotalIls: number;   // negative (reduces turnover)
    creditNoteVatIls: number;     // positive ABS reversal
    netOutputVatIls: number;      // outputVatIls - creditNoteVatIls
  };
  inputVat: {
    deductibleExpenseCount: number;
    deductibleTotalIls: number;
    inputVatIls: number;          // SUM(vat_amount) for deductible/approved/COMPLETED
    netInputVatIls: number;
  };
  vatPayableIls: number;          // netOutputVat - netInputVat
}

export interface AnnualIncomeSummary {
  year: number;
  income: {
    totalInvoicedExclVatIls: number;   // SUM(subtotal) on issued invoices
    badDebtsWrittenOffIls: number;     // negative
    adjustedIncomeIls: number;
  };
  deductions: {
    totalExpensesExclVatIls: number;
    mileageDeductionIls: number;
    contractorPayoutsIls: number;
    totalDeductionsIls: number;
  };
  netTaxableIncomeIls: number;         // estimate
  expenseByCategory: { category: string; amountIls: number }[];
}

export interface AdvanceTaxEstimate {
  year: number;
  ytdNetIncomeIls: number;             // = AnnualIncomeSummary.netTaxableIncomeIls (YTD)
  advanceTaxRatePct: number | null;    // from tenant_settings.advance_tax_rate_pct
  estimatedAnnualIncomeTaxIls: number; // marginal banding from tax_rates
  advancePaymentsYtdIls: number;       // user-entered (0 until tracked)
  recommendedAdvancePerRemainingMonthIls: number;
}

export interface TaxSettingsPatch {
  advance_tax_rate_pct?: number;       // NUMERIC(5,2)
  vat_period?: 'monthly' | 'bimonthly';
}
```
**Acceptance:**
- [ ] Types compile and are importable from `@zync/types`.

### Task 4: VAT aggregation queries (output + input + credit notes)
**Blocks:** 6, 8  ·  **Blocked by:** 1
**Files:**
- Create: `packages/db/src/queries/tax-reports.ts`
**Steps:**
- [ ] Implement `getPcn874Report(db, tenantId, from, to)` returning `Pcn874Report`. All queries are tenant-scoped via `tenantQuery`/explicit `tenant_id = :tenantId`.
- [ ] Output VAT + turnover query: filter on `tax_issue_date BETWEEN from AND to`, exclude `status IN ('DRAFT','SENT','VOID','BAD_DEBT')`, exclude `source = 'credit_note'`. Turnover uses `COALESCE(total_ils, total)` (foreign-currency snapshot, ILS fallback per multi-currency spec).
- [ ] Credit-note query: `source = 'credit_note'` within the same date window → `SUM(ABS(vat_amount))` as reversal and `SUM(total_ils|total)` as negative turnover.
- [ ] Input VAT query: `expenses` filter `expense_date BETWEEN from AND to`, `status = 'COMPLETED'`, `approval_status IN ('approved','not_required')`, `vat_deductible = true` → `SUM(vat_amount)` and `SUM(amount)` and `COUNT(*)`.
- [ ] Compute `netOutputVatIls`, `netInputVatIls`, `vatPayableIls`. Read `vat_period` from `tenant_settings` for the report header.
**Schema / Interfaces:**
```sql
-- Output VAT (excludes credit notes + non-final statuses):
SELECT COUNT(*) AS tax_invoice_count,
       COALESCE(SUM(vat_amount),0) AS output_vat,
       COALESCE(SUM(COALESCE(total_ils, total)),0) AS turnover_ils
FROM invoices
WHERE tenant_id = :tenantId
  AND tax_issue_date BETWEEN :from AND :to
  AND status NOT IN ('DRAFT','SENT','VOID','BAD_DEBT')
  AND source != 'credit_note';

-- Credit-note reversal:
SELECT COUNT(*) AS credit_note_count,
       COALESCE(SUM(ABS(vat_amount)),0) AS credit_note_vat,
       COALESCE(SUM(COALESCE(total_ils, total)),0) AS credit_note_total_ils
FROM invoices
WHERE tenant_id = :tenantId
  AND source = 'credit_note'
  AND tax_issue_date BETWEEN :from AND :to;

-- Input VAT (approved expense ledger only):
SELECT COUNT(*) AS deductible_count,
       COALESCE(SUM(vat_amount),0) AS input_vat,
       COALESCE(SUM(amount),0) AS deductible_total_ils
FROM expenses
WHERE tenant_id = :tenantId
  AND expense_date BETWEEN :from AND :to
  AND status = 'COMPLETED'
  AND approval_status IN ('approved','not_required')
  AND vat_deductible = true;
```
```ts
export async function getPcn874Report(
  db: DB, tenantId: string, from: string, to: string
): Promise<Pcn874Report>;
```
**Acceptance:**
- [ ] Credit notes never inflate output VAT; their VAT reduces `netOutputVatIls`.
- [ ] A `pending` or `rejected` expense contributes zero input VAT; an ILS invoice with NULL `total_ils` falls back to `total`.

### Task 5: Annual summary + advance-tax queries (with `tax_rates` marginal banding)
**Blocks:** 7, 8  ·  **Blocked by:** 1
**Files:**
- Modify: `packages/db/src/queries/tax-reports.ts`
**Steps:**
- [ ] Implement `getAnnualIncomeSummary(db, tenantId, year)` returning `AnnualIncomeSummary`. Income = `SUM(subtotal)` on invoices issued in `[year-01-01, year-12-31]` with non-final statuses excluded as in Task 4; bad debts = sum of `COALESCE(total_ils,total)` for `status IN ('BAD_DEBT','WRITTEN_OFF')` (negative). Deductible expenses grouped by `expense_category` over `amount` (gross-excl-VAT base) for `status='COMPLETED'` and `approval_status IN ('approved','not_required')`. Mileage and contractor payouts derived from their canonical totals (mileage: sum of mileage deduction; contractor: payout net) — when those modules' tables are present, sum them; otherwise emit 0 (the spec lists them as supporting rows).
- [ ] Implement `computeMarginalIncomeTax(brackets, taxableIncome)`: load the most-recent `effective_from` set of `tax_type LIKE 'personal_bracket_%'` rows for `country_code = 'IL'` ordered by `threshold_ils ASC`, iterate bands ascending applying each `rate` to the income slice between the previous and current `threshold_ils` ceiling (NULL ceiling = top, open-ended band).
- [ ] Implement `getAdvanceTaxEstimate(db, tenantId, year)` returning `AdvanceTaxEstimate`: `ytdNetIncomeIls` from the annual summary (YTD), `advanceTaxRatePct` from `tenant_settings`, `estimatedAnnualIncomeTaxIls` from `computeMarginalIncomeTax`, `recommendedAdvancePerRemainingMonthIls = max(0, (estimatedAnnualTax*ratePct/100) - advancePaymentsYtd) / remainingMonths`.
**Schema / Interfaces:**
```sql
-- Marginal banding source (no new table; admin-managed via /admin/tax-rates):
SELECT tax_type, threshold_ils, rate
FROM tax_rates
WHERE country_code = 'IL'
  AND tax_type LIKE 'personal_bracket_%'
  AND effective_from <= :as_of_date
ORDER BY effective_from DESC, threshold_ils ASC;
-- Use the newest effective_from set; iterate bands ascending to compute marginal tax.

-- Income (excl VAT) + bad-debt adjustment:
SELECT COALESCE(SUM(subtotal),0) AS total_invoiced_excl_vat
FROM invoices
WHERE tenant_id = :tenantId AND status NOT IN ('DRAFT','SENT','VOID','BAD_DEBT')
  AND source != 'credit_note'
  AND tax_issue_date BETWEEN :year_start AND :year_end;

SELECT COALESCE(SUM(COALESCE(total_ils, total)),0) AS bad_debts
FROM invoices
WHERE tenant_id = :tenantId AND status IN ('BAD_DEBT','WRITTEN_OFF')
  AND tax_issue_date BETWEEN :year_start AND :year_end;

-- Deductible expenses by category:
SELECT expense_category, COALESCE(SUM(amount),0) AS amount_ils
FROM expenses
WHERE tenant_id = :tenantId
  AND expense_date BETWEEN :year_start AND :year_end
  AND status = 'COMPLETED'
  AND approval_status IN ('approved','not_required')
GROUP BY expense_category;
```
```ts
export function computeMarginalIncomeTax(
  brackets: { thresholdIls: number | null; rate: number }[], taxableIncomeIls: number
): number;
export async function getAnnualIncomeSummary(db: DB, tenantId: string, year: number): Promise<AnnualIncomeSummary>;
export async function getAdvanceTaxEstimate(db: DB, tenantId: string, year: number): Promise<AdvanceTaxEstimate>;
```
**Acceptance:**
- [ ] `computeMarginalIncomeTax` applies each band's rate only to the income slice within that band; the top (NULL-ceiling) band is open-ended.
- [ ] Changing `tax_rates` rows with a newer `effective_from` changes the computed tax with no code change.
- [ ] Bad debts reduce adjusted income.

### Task 6: PCN874 machine-readable CSV writer (CP1255)
**Blocks:** 8  ·  **Blocked by:** 2, 4
**Files:**
- Create: `apps/zync-api/src/lib/pcn874-csv.ts`
**Steps:**
- [ ] Implement `buildPcn874Csv(report, tenant)` (signature below) producing the fixed ITA PCN874 record layout: a header line plus per-transaction lines (output and input totals), not free-form columns.
- [ ] Run every string field through `sanitizeCell` (Task 2) before assembly, then encode the entire output with `encodeCp1255` (Windows-Hebrew, no UTF-8 BOM). The function returns `Uint8Array` bytes ready to stream.
- [ ] Header carries the tenant business tax id, reporting period (`YYYYMM`), and `vat_period`; total lines carry net output VAT, net input VAT, and VAT payable as integer agorot or shekels per the ITA fixed layout.
**Schema / Interfaces:**
```ts
import { encodeCp1255 } from './cp1255';
import { sanitizeCell } from './financial-export';
export function buildPcn874Csv(
  report: Pcn874Report,
  tenant: { businessTaxId: string; name: string }
): Uint8Array; // CP1255 bytes, no BOM, fixed ITA PCN874 record layout
```
**Acceptance:**
- [ ] Output bytes contain no UTF-8 BOM and decode as CP1255 (Hebrew names intact when opened in the ITA portal).
- [ ] A vendor/customer name beginning with `=` is neutralized by `sanitizeCell` before encoding.

### Task 7: Annual summary Excel writer (RTL Hebrew, formula-safe)
**Blocks:** 8  ·  **Blocked by:** 2, 5
**Files:**
- Create: `apps/zync-api/src/lib/annual-summary-xlsx.ts`
**Steps:**
- [ ] Implement `buildAnnualSummaryXlsx(summary, supporting)` using `writeFinancialWorkbook`/`newRtlWorksheet` from Task 2. Each worksheet sets `views[0].rightToLeft = true` and a Hebrew-capable font on header + data cells.
- [ ] Worksheets: **Summary** (income, deductions, net taxable estimate), **Invoices** (issued invoice list), **Expenses by Category**, **Mileage Log**, **Contractor Payouts** — matching "Excel export includes all supporting data".
- [ ] Every string cell passes through `sanitizeCell` (formula-injection guard) before writing.
**Schema / Interfaces:**
```ts
import { writeFinancialWorkbook, newRtlWorksheet } from './financial-export';
export function buildAnnualSummaryXlsx(
  summary: AnnualIncomeSummary,
  supporting: {
    invoices: { number: string; date: string; customer: string; subtotalIls: number }[];
    expensesByCategory: { category: string; amountIls: number }[];
    mileage: { date: string; description: string; km: number; deductionIls: number }[];
    payouts: { contractor: string; netIls: number }[];
  }
): Promise<Uint8Array>;
```
**Acceptance:**
- [ ] Every worksheet has `rightToLeft === true` and a Hebrew font; Hebrew headers render correctly in Excel.
- [ ] A cell value starting with `+`/`-`/`@`/`=`/tab/CR is prefixed with `'`.

### Task 8: Tax report API routes
**Blocks:** 10, 11, 12  ·  **Blocked by:** 3, 4, 5, 6, 7
**Files:**
- Create: `apps/zync-api/src/routes/reports-tax.ts`
- Modify: `apps/zync-api/src/index.ts` (mount router)
**Steps:**
- [ ] Mount under `/api/reports`. All routes pass through `authMiddleware`; read routes require `requirePermission('reports:read')`, export routes `requirePermission('reports:export')`.
- [ ] Validate query params with Zod: `from`/`to` as `YYYY-MM-DD`, `year` as a 4-digit integer. Reject malformed input with 400.
- [ ] `GET /api/reports/vat` → `getPcn874Report` → JSON `Pcn874Report`.
- [ ] `GET /api/reports/vat/csv` → `buildPcn874Csv` → respond with `Content-Type: text/csv; charset=windows-1255`, `Content-Disposition: attachment; filename="pcn874-<period>.csv"`, body = raw CP1255 `Uint8Array` (do not re-encode to UTF-8).
- [ ] `GET /api/reports/annual-summary` → `getAnnualIncomeSummary` → JSON.
- [ ] `GET /api/reports/annual-summary/xlsx` → `buildAnnualSummaryXlsx` → `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, attachment filename `annual-summary-<year>.xlsx`.
- [ ] `GET /api/reports/advance-tax` → `getAdvanceTaxEstimate` → JSON.
- [ ] Set security response headers consistent with the app CSP (no inline content; attachments only).
**Schema / Interfaces:**
```
GET   /api/reports/vat                  ?from&to        reports:read   -> Pcn874Report
GET   /api/reports/vat/csv              ?from&to        reports:export -> CP1255 CSV (windows-1255)
GET   /api/reports/annual-summary       ?year           reports:read   -> AnnualIncomeSummary
GET   /api/reports/annual-summary/xlsx  ?year           reports:export -> xlsx
GET   /api/reports/advance-tax          ?year           reports:read   -> AdvanceTaxEstimate
```
**Acceptance:**
- [ ] CSV response carries `charset=windows-1255` and streams the raw CP1255 bytes unmodified.
- [ ] A caller lacking `reports:export` gets 403 on `/vat/csv` but 200 on `/vat` if they hold `reports:read`.
- [ ] Invalid `from` (not `YYYY-MM-DD`) returns 400.

### Task 9: Tax settings PATCH route
**Blocks:** 13  ·  **Blocked by:** 1, 3
**Files:**
- Modify: `apps/zync-api/src/routes/settings.ts` (or create `apps/zync-api/src/routes/settings-tax.ts` and mount it)
**Steps:**
- [ ] Implement `PATCH /api/settings/tax` behind `authMiddleware` + `requirePermission('settings:write')`.
- [ ] Zod body `TaxSettingsPatch`: `advance_tax_rate_pct` (number, 0–100, NUMERIC(5,2) precision) optional; `vat_period` enum `'monthly'|'bimonthly'` optional. Reject unknown keys.
- [ ] Update the tenant's `tenant_settings` row (tenant-scoped); return the updated effective tax settings.
**Schema / Interfaces:**
```
PATCH /api/settings/tax   settings:write
  body: { advance_tax_rate_pct?: number, vat_period?: 'monthly'|'bimonthly' }
  -> { vat_period, advance_tax_rate_pct }
```
**Acceptance:**
- [ ] `vat_period: 'quarterly'` is rejected with 400 (Zod) before hitting the DB CHECK.
- [ ] A user without `settings:write` gets 403.

### Task 10: PCN874 VAT report page (`/reports/vat`)
**Blocks:** —  ·  **Blocked by:** 8
**Files:**
- Create: `apps/zync-app/src/pages/reports/VatReportPage.tsx`
- Modify: `apps/zync-app/src/router.tsx` (route registration)
**Steps:**
- [ ] Build the PCN874 layout per the spec mockup: Output VAT block (tax invoices count + turnover, output VAT, credit notes + reversal, net output VAT), Input VAT block (deductible expenses + input VAT), and the bold **VAT payable to ITA** line.
- [ ] Period selector (month picker; respects `vat_period` for the default window). Fetch `GET /api/reports/vat?from&to` via react-query.
- [ ] **Export CSV** button → downloads `/api/reports/vat/csv` (browser handles the attachment). **Export PDF** button → print-to-PDF of the report view.
- [ ] Render the informational note "Submit PCN874 via the ITA online portal (shaam.gov.il). Use the exported CSV to pre-fill the form."
- [ ] All currency uses `Intl.NumberFormat` with `currency: 'ILS'`. Layout RTL-aware (logical properties, no hardcoded left/right). Honor `prefers-reduced-motion` on any transitions.
**Acceptance:**
- [ ] Net output VAT = output VAT − credit-note reversal; VAT payable = net output − net input, matching API.
- [ ] CSV download triggers the CP1255 file; UI is RTL-correct in Hebrew locale.

### Task 11: Annual Income Summary page (`/reports/annual-summary`)
**Blocks:** —  ·  **Blocked by:** 8
**Files:**
- Create: `apps/zync-app/src/pages/reports/AnnualSummaryPage.tsx`
- Modify: `apps/zync-app/src/router.tsx`
**Steps:**
- [ ] Build the Income / Deductible Expenses / Net taxable income layout per the spec mockup, with `?year=` selector.
- [ ] Fetch `GET /api/reports/annual-summary?year=`; **Export Excel** button downloads `/api/reports/annual-summary/xlsx?year=`.
- [ ] Render the estimate disclaimer ("This is an estimate. Your accountant will apply additional deductions, credits, and adjustments.").
- [ ] ILS formatting; RTL-aware layout; reduced-motion respected.
**Acceptance:**
- [ ] Adjusted income = total invoiced − bad debts; net taxable = adjusted income − total deductions, matching API.
- [ ] Excel export downloads an RTL workbook with all supporting sheets.

### Task 12: Advance Tax Estimate page (`/reports/advance-tax`)
**Blocks:** —  ·  **Blocked by:** 8
**Files:**
- Create: `apps/zync-app/src/pages/reports/AdvanceTaxPage.tsx`
- Modify: `apps/zync-app/src/router.tsx`
**Steps:**
- [ ] Build the layout per the spec mockup: YTD net income, monthly advance rate (editable — links to settings), estimated annual income tax, advance payments YTD, recommended advance per remaining month.
- [ ] Fetch `GET /api/reports/advance-tax?year=`. If `advance_tax_rate_pct` is NULL, show the "enter your rate" prompt linking to the tax settings form (Task 13).
- [ ] Render the informational note about the ITA setting the advance rate.
- [ ] ILS formatting; RTL-aware; reduced-motion respected; label the figures as estimates.
**Acceptance:**
- [ ] When the advance rate is unset, the page shows the prompt and recommended-per-month is hidden or clearly marked as unavailable.
- [ ] Recommended advance per remaining month reflects rate, estimated annual tax, and payments-to-date from the API.

### Task 13: Tax settings form (`/settings/tax`)
**Blocks:** —  ·  **Blocked by:** 9
**Files:**
- Create: `apps/zync-app/src/pages/settings/TaxSettingsPage.tsx`
- Modify: `apps/zync-app/src/router.tsx`
**Steps:**
- [ ] Build a form with a `vat_period` Select (`monthly` / `bimonthly`) and an `advance_tax_rate_pct` numeric input (0–100, two decimals).
- [ ] Submit via `PATCH /api/settings/tax`; show success toast; invalidate the advance-tax query so `/reports/advance-tax` refreshes.
- [ ] Form fields use `@zync/ui` Form/FormField/FormLabel with proper `aria-` wiring; validation errors announced to assistive tech. RTL-aware.
**Acceptance:**
- [ ] Saving `vat_period` updates the default VAT report period window on `/reports/vat`.
- [ ] Saving the advance rate clears the "enter your rate" prompt on `/reports/advance-tax`.
