{
  "slug": "accountant-export",
  "spec_file": "docs/specs/2026-06-01-accountant-export.md",
  "findings": [
    {
      "id": "accountant-export-001",
      "severity": "P0",
      "type": "bug",
      "summary": "Expense movements debit gross amount to expense AND separately debit VAT — ledger unbalanced / overstates expense",
      "spec_ref": "Derived ledger — Expense (deductible): Dr Expense category + VAT-input, Cr Vendor/Bank; expenses-module: net expense = amount − vat_amount",
      "code_ref": "apps/zync-api/src/reports/movements.ts:232",
      "evidence": "First leg debits expenseAccount with amountIls=toIls(exp.amount) (gross per expenses-module spec line 91). Second leg debits vatInputAccount with toIls(exp.vatAmount) while also crediting apAccount. Gross amount already includes VAT, so expense+VAT debits exceed credits.",
      "repro": "COMPLETED expense amount=118, vat_amount=18 → movements Dr expense 118 Cr AP 118 + Dr VAT 18 Cr AP 18 (ΣDr 136, ΣCr 236).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-002",
      "severity": "P0",
      "type": "bug",
      "summary": "Invoice movements use subtotal/total without ILS normalization for foreign-currency invoices",
      "spec_ref": "Plan Task 3 — amounts in ILS using invoices.total_ils for FX invoices",
      "code_ref": "apps/zync-api/src/reports/movements.ts:138",
      "evidence": "subtotal=toIls(inv.subtotal), total=toIls(inv.total) with no currency check and no total_ils column on invoices schema (packages/db/src/schema/invoices.ts:86-88). Foreign-currency invoice totals are summed as if ILS.",
      "repro": "TAX_ISSUED invoice currency='USD' subtotal=100 → movement amountIls=100 treated as ₪100.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-003",
      "severity": "P0",
      "type": "bug",
      "summary": "Contractor payout movements debit net_amount to expense instead of gross amount; credits A/P not Bank",
      "spec_ref": "Derived ledger — Contractor payout: Dr Subcontractor expense, Cr Bank, Withholding-payable (856)",
      "code_ref": "apps/zync-api/src/reports/movements.ts:263",
      "evidence": "Uses netAmount=toIls(bill.netAmount ?? bill.amount) for expense debit; credits apAccount not bank. payout_bills.amount is gross (packages/db/src/schema/contractors.ts:153-161). Withholding reclassified via Dr AP Cr withholding instead of Cr bank + Cr withholding from expense.",
      "repro": "PAID payout amount=1000, withholding=100, net=900 → Dr expense 900 (should 1000), Cr AP 900 (should Cr bank 900).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-004",
      "severity": "P0",
      "type": "bug",
      "summary": "Input-VAT movements ignore vat_deductible and deduction_pct partial deductibility",
      "spec_ref": "expenses-module — PCN874 input VAT uses vat_amount × deduction_pct/100 where vat_deductible; per-diem vat_deductible=false",
      "code_ref": "apps/zync-api/src/reports/movements.ts:244",
      "evidence": "VAT-input leg posted whenever vatAmount>0 with no eq(exp.vatDeductible,true) filter and no deduction_pct scaling. expenses schema has vatDeductible and deductionPct (packages/db/src/schema/expenses.ts:75,86).",
      "repro": "Expense vat_deductible=false vat_amount=18 still emits Dr VAT-input 18. Expense deduction_pct=50 still posts full vat_amount.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-005",
      "severity": "P0",
      "type": "bug",
      "summary": "Form 6111 Excel emits non-statutory field codes (600/710-series) instead of ITA 1000–3999 P&L codes",
      "spec_ref": "Form 6111 — fields 1000–3999 (e.g. 1010 revenue, 2010 cost of sales); grouped via coa_accounts.form6111_code",
      "code_ref": "apps/zync-api/src/reports/form6111.ts:102",
      "evidence": "Hardcoded codes '600','601','710','799','800','900' in categoryForm6111/revenue rows. Spec and coa-defaults comment reference 1010/2010-style numbering; output is not ITA 6111 field-coded structure.",
      "repro": "Generate Form 6111 → Excel row code column shows 600/710 not 1010/2010.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-006",
      "severity": "P0",
      "type": "bug",
      "summary": "Form 6111 generator calls getProfitLoss SQL referencing non-existent invoices.total_ils column — runtime failure",
      "spec_ref": "Plan Task 6 — source P&L from financial-statements data sources",
      "code_ref": "packages/db/src/reports/profit-loss.ts:56",
      "evidence": "Raw SQL COALESCE(total_ils, total) but grep packages/db/migrations and packages/db/src/schema/invoices.ts shows no total_ils column. generateForm6111 calls getProfitLoss at form6111.ts:74.",
      "repro": "POST /api/reports/accountant/form6111 → generateForm6111 → PostgreSQL error column total_ils does not exist.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-007",
      "severity": "P0",
      "type": "security",
      "summary": "generateMovementFile updates accountant_export_jobs by job id only — no tenant_id predicate",
      "spec_ref": "Plan Task 8 — tenant-scope every query; cross-tenant writes are P0",
      "code_ref": "apps/zync-api/src/reports/movement-file.ts:110",
      "evidence": "UPDATE accountant_export_jobs SET status='running' WHERE id=jobId (lines 107-110, 140-147, 150-153) — no eq(accountantExportJobs.tenantId, tenantId). Same pattern in setAccountantExportJobDone/Error at packages/db/src/queries/accountant-export.ts:162.",
      "repro": "Caller holding foreign-tenant job UUID can mutate that job row status/r2_key.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-008",
      "severity": "P0",
      "type": "security",
      "summary": "generateForm6111 updates accountant_export_jobs by job id only — no tenant_id predicate",
      "spec_ref": "Plan Task 8 — tenant-scope every query",
      "code_ref": "apps/zync-api/src/reports/form6111.ts:71",
      "evidence": "UPDATE accountant_export_jobs WHERE eq(accountantExportJobs.id, jobId) at lines 68-71, 208-215 with no tenantId guard.",
      "repro": "Same cross-tenant job mutation vector as movement-file generator.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-009",
      "severity": "P1",
      "type": "missing",
      "summary": "is_accountant column added in migration but absent from Drizzle schema and never set on invite accept",
      "spec_ref": "Accountant access — ALTER TABLE tenant_memberships ADD COLUMN is_accountant; set on Accountant invite acceptance",
      "code_ref": "packages/db/migrations/0041_accountant_export.sql:43",
      "evidence": "Migration adds is_accountant. packages/db/src/schema/rbac.ts tenantMemberships has no isAccountant field. acceptInvitationExistingUser insert at packages/db/src/queries/auth-writes.ts:350-362 sets userId/tenantId/roleId only.",
      "repro": "grep is_accountant|isAccountant under packages/ apps/ (excl. worktrees) — only migration SQL, zero application references.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-010",
      "severity": "P1",
      "type": "missing",
      "summary": "Accountant invite Task 9 not implemented — no is_accountant grant, no PII scoping, generic invite only",
      "spec_ref": "Plan Task 9 — invite Accountant role, is_accountant=true, financial-only data scoping",
      "code_ref": "apps/zync-api/src/routes/auth/invite.ts:46",
      "evidence": "Invite flow accepts any roleId with permission subset check; no Accountant-specific handling, no is_accountant flag, no middleware stripping non-billing customer PII for accountant sessions.",
      "repro": "Invite user with Accountant role → membership created without is_accountant; accountant session can reach full customer routes if role misconfigured.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-011",
      "severity": "P1",
      "type": "missing",
      "summary": "B110 chart-of-accounts records never emitted when ledger enabled in uniform-format export",
      "spec_ref": "uniform-format-export B110 — Account record from tenant chart of accounts; accountant-export enables B100/B110",
      "code_ref": "apps/zync-api/src/reports/uniform-format/generate.ts:253",
      "evidence": "Step 6 emits B100 from deriveMovements when hasCoa; counts['B110'] stays 0 (line 115). grep buildB110 across repo — no implementation.",
      "repro": "Tenant with coa_mappings → uniform-format ZIP INI.txt shows B110 count 0 despite mapped coa_accounts rows.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-012",
      "severity": "P1",
      "type": "missing",
      "summary": "No automated tests for deriveMovements, movement-file, form6111, or accountant routes",
      "spec_ref": "Plan acceptance — balanced movements, CP1255 output, Form 6111 field rows, permission gates",
      "code_ref": "apps/zync-api/src/reports/movements.ts:68",
      "evidence": "grep accountant-export|deriveMovements|movement-file|form6111 in **/*test* — zero matches.",
      "repro": "No test file covers movement derivation or export generators.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-013",
      "severity": "P1",
      "type": "missing",
      "summary": "Chart-of-accounts UI edits mappings only — cannot edit accounts or form6111_code per spec",
      "spec_ref": "UI — Chart of accounts Edit mapping; Plan Task 12 — editable code/name/type/form6111_code",
      "code_ref": "apps/zync-app/src/pages/settings/integrations/ChartOfAccountsEditor.tsx:43",
      "evidence": "Editor loads/saves coa_mappings via useCoaMappings/useSaveCoaMappings only. No PUT /api/coa/accounts UI; form6111_code not editable.",
      "repro": "Open Edit mapping → source_kind grid only; no 6111 code column.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-014",
      "severity": "P1",
      "type": "missing",
      "summary": "Recent-exports list never fetches per-job downloadUrl — download link never renders",
      "spec_ref": "UI Recent exports — row with download affordance; GET exports/:id returns signed url",
      "code_ref": "apps/zync-app/src/hooks/useAccountantExports.ts:24",
      "evidence": "listExports calls GET /exports returning raw job rows without downloadUrl. ExportJobRow gates link on job.downloadUrl (AccountingPage.tsx:289) which list response omits.",
      "repro": "Job status=done in list → no Download link unless client calls GET /exports/:id separately (not implemented in list UI).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-015",
      "severity": "P1",
      "type": "missing",
      "summary": "POST movement omits required format parameter (hashavshevet)",
      "spec_ref": "API POST /api/reports/accountant/movement — body { periodFrom, periodTo, format }; UI Format [Hashavshevet ▾]",
      "code_ref": "apps/zync-api/src/routes/reports/accountant-exports.ts:63",
      "evidence": "movementJobSchema has periodFrom/periodTo only. UI has no format selector (AccountingPage.tsx:139-151).",
      "repro": "POST body has no format field; server cannot validate or support alternate export formats.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-016",
      "severity": "P2",
      "type": "divergence",
      "summary": "Expense movements include all COMPLETED rows — no approval_status filter",
      "spec_ref": "Plan Task 6 / financial-statements — expenses where status=COMPLETED with approval workflow",
      "code_ref": "apps/zync-api/src/reports/movements.ts:109",
      "evidence": "WHERE eq(expenses.status,'COMPLETED') only. expenses.approvalStatus exists (packages/db/src/schema/expenses.ts:115) but not filtered.",
      "repro": "Expense approval_status='pending' but status COMPLETED → still derived into movement file.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-017",
      "severity": "P2",
      "type": "divergence",
      "summary": "Form 6111 ignores coa_accounts.form6111_code — uses hardcoded category→code map",
      "spec_ref": "Form 6111 — P&L via coa_accounts.form6111_code + category breakdown for reconciliation",
      "code_ref": "apps/zync-api/src/reports/form6111.ts:149",
      "evidence": "categoryForm6111 hardcoded at lines 149-160; listCoaAccounts never called. Spec requires mapping via tenant chart.",
      "repro": "Edit form6111_code on account via API → Form 6111 Excel unchanged.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-018",
      "severity": "P2",
      "type": "divergence",
      "summary": "Form 6111 missing tax-adjustment section stub",
      "spec_ref": "Form 6111 v1 — tax-adjustment section stub for accountant to complete",
      "code_ref": "apps/zync-api/src/reports/form6111.ts:186",
      "evidence": "Workbook ends at net_profit row (code 900); no empty adjustment rows appended after P&L section.",
      "repro": "Open generated xlsx — no tax-adjustment stub section.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-019",
      "severity": "P2",
      "type": "divergence",
      "summary": "Expired export download refreshes presigned URL instead of returning 410",
      "spec_ref": "Plan Task 8 — GET exports/:id returns 410 if download_expires_at past",
      "code_ref": "apps/zync-api/src/routes/reports/accountant-exports.ts:202",
      "evidence": "When expired, code issues new presign and updateAccountantExportJobDownloadExpiry instead of 410 response.",
      "repro": "Job with downloadExpiresAt in past → GET /exports/:id returns 200 with fresh downloadUrl.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-020",
      "severity": "P2",
      "type": "divergence",
      "summary": "Export generation audited fire-and-forget, not in same transaction as job creation",
      "spec_ref": "Plan Task 8 — wrap job creation + audit in require-audit-in-transaction",
      "code_ref": "apps/zync-api/src/routes/reports/accountant-exports.ts:108",
      "evidence": "void logAuditEvent(c, ...) after createAccountantExportJob; no db.transaction wrapping job insert + audit.",
      "repro": "Audit row can be missing if logAuditEvent fails after job insert.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-021",
      "severity": "P2",
      "type": "divergence",
      "summary": "Large-period movement export runs inline — export.generate queue not wired",
      "spec_ref": "Plan Task 5 — route large ranges through export.generate queue",
      "code_ref": "apps/zync-api/src/routes/reports/accountant-exports.ts:121",
      "evidence": "void generateMovementFile(...) inline with comment only. grep export.generate under apps/zync-api/src/reports — no matches.",
      "repro": "Multi-year movement request executes synchronously in Worker; no queue consumer.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-022",
      "severity": "P2",
      "type": "divergence",
      "summary": "UI missing 6111 mapped-count warning, custom period picker, and invited-accountant display",
      "spec_ref": "UI wireframe — Period custom, 6111 codes mapped 21/23 ⚠, accountant@cpa.co.il invited 3/2026",
      "code_ref": "apps/zync-app/src/pages/settings/integrations/AccountingPage.tsx:215",
      "evidence": "CoA summary shows account count only (line 216). QUARTER_OPTIONS presets only — no custom dates. Accountant block is generic invite link, no member list.",
      "repro": "Page never shows mapped/total 6111 ratio or invited accountant email/date.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-023",
      "severity": "P2",
      "type": "divergence",
      "summary": "Accounting page does not enforce reports:read guard documented in spec/plan",
      "spec_ref": "UI Guards — page visible to sessions with reports:read; generate behind accountant:export",
      "code_ref": "apps/zync-app/src/pages/settings/integrations/AccountingPage.tsx:79",
      "evidence": "Comment claims reports:read guard but component only checks sessionLoading and accountant:export for buttons. No redirect/deny without reports:read.",
      "repro": "Session without reports:read but with settings access may render full page.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-024",
      "severity": "P2",
      "type": "divergence",
      "summary": "ACCOUNTANT system role grants reports:export_external — not in spec permission triple",
      "spec_ref": "Accountant role — permissions reports:read, reports:export, accountant:export only",
      "code_ref": "packages/db/src/seed/permission-keys.ts:74",
      "evidence": "ACCOUNTANT array includes reports:export_external alongside the three spec permissions.",
      "repro": "Accountant role seed includes four permissions not three.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-025",
      "severity": "P2",
      "type": "divergence",
      "summary": "Movement type omits vat_code and side fields from published plan interface",
      "spec_ref": "Plan Task 3 Movement — vat_code, side debit|credit, counter_account",
      "code_ref": "packages/types/src/movement.ts:8",
      "evidence": "Movement has debitAccount/creditAccount/amountIls but no vatCode, side, or counterAccount fields defined in plan Task 3 schema.",
      "repro": "Compare packages/types/src/movement.ts to plan Task 3 interface block.",
      "fix_direction": "spec_review"
    },
    {
      "id": "accountant-export-026",
      "severity": "P2",
      "type": "divergence",
      "summary": "UI does not poll job status until done — only delayed list refetch",
      "spec_ref": "Plan Task 13 — poll GET exports/:id until done then surface download",
      "code_ref": "apps/zync-app/src/pages/settings/integrations/AccountingPage.tsx:96",
      "evidence": "onSuccess uses setTimeout(refetchExports, 2000). useAccountantExportJob hook exists with refetchInterval but is unused on generate flow.",
      "repro": "Generate movement file → user must manually refresh; no auto-poll to done + download link.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-027",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "seedSystemRoles comment still says 5 system roles while ACCOUNTANT is the 6th",
      "spec_ref": "Plan Task 2 — seed Accountant system role",
      "code_ref": "packages/db/src/queries/seed-rbac.ts:4",
      "evidence": "File header says 'insert the 5 system roles' but SYSTEM_ROLE_NAMES has 6 entries including ACCOUNTANT.",
      "repro": "Read seed-rbac.ts header vs packages/db/src/seed/permission-keys.ts:53.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "accountant-export-028",
      "severity": "P3",
      "type": "cosmetic",
      "summary": "Movement B100 comment says accounts left-padded but textField uses padEnd (space-fill)",
      "spec_ref": "uniform-format-export — fixed-width numeric/text field conventions",
      "code_ref": "apps/zync-api/src/reports/movement-file.ts:72",
      "evidence": "Comment 'Debit account (8 chars, left-padded)' but textField does padEnd(width,' '). Matches uniform-format generate.ts:266 padEnd — comment wording only.",
      "repro": "Inspect buildB100 textField vs comment.",
      "fix_direction": "code_to_spec"
    }
  ],
  "summary": {
    "P0": 8,
    "P1": 7,
    "P2": 11,
    "P3": 2,
    "total": 28
  }
}
