# Accountant Export — Movement File + Form 6111 — Implementation Plan

**Spec:** docs/specs/2026-06-01-accountant-export.md  ·  **Slug:** accountant-export  ·  **Wave:** 15
**Depends on:** contractor-payouts, expenses-module, financial-statements, foundation-auth-rbac, invoice-credit-notes, invoice-receipt-document, invoices-core, israeli-tax-reports, uniform-format-export, vendors-suppliers

## Goal
Back the `/settings/integrations/accounting` route with three deliverables for Israeli SMB bookkeeping: (1) a Hashavshevet-compatible **movement file** (קובץ תנועות) of journal movements derived at export time from source documents; (2) an ITA **Form 6111** (טופס 6111) annual P&L line mapping as structured Excel; (3) a scoped **Accountant** role so an external CPA can pull exports without full tenant access. The movement-derivation function `deriveMovements` is a published interface that `uniform-format-export` (spec 180) consumes to emit its B100/B110 journal records.

## Architecture
Zync is a documents-and-cash system, not a posted double-entry GL. Movements are **derived** on demand from source events (tax invoices, receipts, credit notes, expenses, contractor payouts) into a posting view, using a tenant **chart of accounts** mapping. New tables: `coa_accounts` (tenant-editable, seeded), `coa_mappings` (document category → account code), `accountant_export_jobs` (generated file jobs). The derivation reads upstream tables already defined: `invoices`, `invoice_lines`, `receipts`, `receipt_payment_lines`, `expenses`, `vendors`, `payout_bills`. The exported `deriveMovements(tenantId, periodFrom, periodTo) → Movement[]` and the "enabled signal" (coa_mappings rows exist) are wired into spec 180's `apps/zync-api/src/reports/uniform-format.ts` step 5 (B100/B110). Form 6111 sources ex-VAT P&L numbers mirroring `deriveMovements` semantics (`invoices`, `expenses`, `payout_bills`) mapped via `coa_accounts.form6111_code`, reconciling to the movement file (the financial-statements dashboard's VAT-inclusive gross revenue is a separate report). Movement file and Form 6111 Excel reuse shared infra: `apps/zync-api/src/lib/cp1255.ts` (CP1255 encoding, owned by spec 180) and the shared xlsx/CSV export utility with formula-injection neutralization (financial-statements). The Accountant role is built on `foundation-auth-rbac` via `seedPermissions`/`seedSystemRoles` and the `tenant_memberships.is_accountant` flag. Jobs write to R2 and serve via signed URLs gated by `download_expires_at`.

## Tech Stack
- **App:** `apps/zync-api` (Hono on Cloudflare Workers) — routes, derivation, generators, RBAC seed.
- **App:** `apps/zync-app` (Vite + React) — `/settings/integrations/accounting` page, chart-of-accounts editor, invite flow.
- **Packages:** `@zync/db` (Drizzle schema + migration), `@zync/types` (Movement type, job DTOs), `@zync/auth` (permission/role seed), `@zync/ui` (existing primitives).
- **Bindings:** Postgres (Neon via Hyperdrive) `DB`; R2 `STORAGE` for export files; `QUEUE` (`export.generate`) for large ranges; ANALYTICS/audit via existing `require-audit-in-transaction`.
- **Libs:** bundled `cp1255.ts` table; shared xlsx writer (`exceljs`-equivalent already in repo per spec 170).

## Wave Plan
| Sub-wave | Tasks | Files touched | Parallelizable? |
|----------|-------|---------------|-----------------|
| A — Schema & RBAC | 1, 2 | `@zync/db` schema + migration, `@zync/auth` seed | Task 1 ∥ Task 2 |
| B — Derivation core | 3, 4 | `apps/zync-api/src/reports/movements.ts`, `@zync/types` | After A |
| C — Generators | 5, 6 | movement-file generator, form6111 generator | After B (∥ each other) |
| D — API routes | 7, 8, 9 | coa routes, accountant export routes, accountant invite | After C |
| E — Spec 180 wiring | 10 | modify `apps/zync-api/src/reports/uniform-format.ts` | After B |
| F — UI | 11, 12, 13 | settings page, CoA editor, recent-exports + invite | After D |

## Tasks

### Task 1: Database schema — chart of accounts, mappings, export jobs, accountant flag
**Blocks:** 2, 3, 5, 6, 7, 8, 9, 10  ·  **Blocked by:** —
**Files:**
- Modify: `packages/db/src/schema.ts` (add three tables + alter `tenant_memberships`)
- Create: `packages/db/migrations/<timestamp>_accountant_export.sql`
**Steps:**
- [ ] Add `coa_accounts`, `coa_mappings`, `accountant_export_jobs` Drizzle table definitions matching the DDL below.
- [ ] Add `is_accountant BOOLEAN NOT NULL DEFAULT false` column to existing `tenant_memberships`.
- [ ] Harmonize spec DDL to canonical dialect: `accountant_export_jobs.created_at` is `TIMESTAMPTZ NOT NULL DEFAULT now()`; add a `CHECK` on `status`.
- [ ] Generate the SQL migration and confirm it applies against a Neon branch.
- [ ] Add indexes: `coa_accounts(tenant_id)`, `accountant_export_jobs(tenant_id, created_at DESC)`.
**Schema / Interfaces:**
```sql
CREATE TABLE coa_accounts (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id     UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  code          TEXT NOT NULL,                 -- e.g. '4000' revenue, '1100' A/R
  name          TEXT NOT NULL,
  type          TEXT NOT NULL CHECK (type IN ('asset','liability','equity','revenue','expense')),
  form6111_code TEXT,                           -- mapping to Form 6111 field
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (tenant_id, code)
);
CREATE INDEX coa_accounts_tenant_idx ON coa_accounts (tenant_id);

-- composite-PK mapping table (no surrogate UUID, like role_permissions)
CREATE TABLE coa_mappings (
  tenant_id    UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  source_kind  TEXT NOT NULL,                   -- 'revenue'|'vat_payable'|'ar'|'bank'|'vat_input'|'ap'|'withholding'|<expense_category>
  account_code TEXT NOT NULL,
  PRIMARY KEY (tenant_id, source_kind)
);

CREATE TABLE accountant_export_jobs (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  kind                TEXT NOT NULL CHECK (kind IN ('movement_file','form6111')),
  period_from         DATE NOT NULL,
  period_to           DATE NOT NULL,
  status              TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','done','error')),
  r2_key              TEXT,
  download_expires_at TIMESTAMPTZ,
  generated_by        UUID REFERENCES users(id) ON DELETE SET NULL,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX accountant_export_jobs_tenant_idx ON accountant_export_jobs (tenant_id, created_at DESC);

ALTER TABLE tenant_memberships ADD COLUMN is_accountant BOOLEAN NOT NULL DEFAULT false;
```

### Task 2: RBAC — Accountant role + permissions seed
**Blocks:** 8, 9  ·  **Blocked by:** 1
**Files:**
- Modify: `packages/auth/src/seed.ts` (or wherever `seedPermissions`/`seedSystemRoles` live)
- Modify: `packages/auth/src/permissions.ts` (permission constant list)
**Steps:**
- [ ] Verify whether `reports:read` already exists in the foundation-auth-rbac seed; if present, reference it, do not redeclare.
- [ ] Add new permissions `reports:export` and `accountant:export` to the permission catalog seeded by `seedPermissions`.
- [ ] Add a system role `Accountant` via `seedSystemRoles` with exactly: `reports:read`, `reports:export`, `accountant:export`. No edit, user-management, or non-financial-module permissions.
- [ ] Grant `accountant:export` to OWNER and ADMIN roles in addition to Accountant.
- [ ] Ensure `requirePermission('accountant:export')` resolves for OWNER/ADMIN/Accountant only.
**Schema / Interfaces:**
```ts
// permission ids (TEXT, seeded into permissions table)
const ACCOUNTANT_PERMISSIONS = ['reports:read', 'reports:export', 'accountant:export'] as const;
// role seed: { id: 'accountant', name: 'Accountant', permissions: ACCOUNTANT_PERMISSIONS }
```
**Acceptance:**
- [ ] `seedSystemRoles` produces an `Accountant` role holding only the three financial permissions.
- [ ] OWNER/ADMIN/Accountant pass `requirePermission('accountant:export')`; a plain MEMBER does not.

### Task 3: Movement type + `deriveMovements` published interface
**Blocks:** 5, 10  ·  **Blocked by:** 1
**Files:**
- Create: `apps/zync-api/src/reports/movements.ts`
- Modify: `packages/types/src/index.ts` (export `Movement`)
**Steps:**
- [ ] Define the `Movement` type with the exact fields the spec lists: date, reference (doc number), account_code, debit/credit indicator, amount, counter_account, vat_code, description.
- [ ] Implement `deriveMovements(env, tenantId, periodFrom, periodTo): Promise<Movement[]>` by computing posting pairs from the source-event table below, resolving account codes through `coa_mappings` (falling back to `coa_accounts.code` by `source_kind`).
- [ ] All amounts in ILS using `invoices.total_ils` for FX invoices, `expenses.amount` (already ILS) for expenses, `payout_bills.net_amount` for payouts; agorot conversion (×100) is applied by the file generator, not here (return decimal ILS).
- [ ] Implement and export the enabled-signal helper `hasAccountantLedger(env, tenantId): Promise<boolean>` returning true iff ≥1 `coa_mappings` row exists for the tenant.
- [ ] Each derivation reads only source rows whose document date falls within `[periodFrom, periodTo]`.
**Schema / Interfaces:**
```ts
export interface Movement {
  date: string;            // YYYY-MM-DD of the source document
  reference: string;       // source document number (invoice/receipt/credit-note/expense/payout)
  account_code: string;    // resolved debit-or-credit account
  side: 'debit' | 'credit';
  amount: number;          // ILS, decimal
  counter_account: string; // the opposing account code
  vat_code: string | null; // ITA VAT code or null
  description: string;
}

// Source-event → posting rules (each row emits a balanced debit+credit pair, expanded per VAT split):
//  Tax invoice issued   : Dr Customer(A/R)             Cr Revenue + VAT-payable
//  Receipt / payment in : Dr Bank/Cash                 Cr Customer(A/R)
//  Credit note          : Dr Revenue + VAT-payable     Cr Customer(A/R)
//  Expense (deductible) : Dr Expense category + VAT-input  Cr Vendor(A/P) / Bank
//  Contractor payout    : Dr Subcontractor expense     Cr Bank + Withholding-payable(856)

export async function deriveMovements(
  env: Env, tenantId: string, periodFrom: string, periodTo: string
): Promise<Movement[]>;

export async function hasAccountantLedger(env: Env, tenantId: string): Promise<boolean>;
```
**Acceptance:**
- [ ] For a tenant with mapped accounts, `deriveMovements` returns balanced (Σdebit = Σcredit) movements across all five source-event types in range.
- [ ] `hasAccountantLedger` returns false when `coa_mappings` is empty, true otherwise.
- [ ] `Movement` and `deriveMovements` are exported from the API package surface so spec 180 can import them.

### Task 4: Chart-of-accounts defaults + seed-on-first-use
**Blocks:** 7  ·  **Blocked by:** 1
**Files:**
- Create: `apps/zync-api/src/reports/coa-defaults.ts`
- Modify: `apps/zync-api/src/reports/movements.ts` (consume defaults)
**Steps:**
- [ ] Define a default Israeli chart of accounts (revenue `4000`, A/R `1100`, bank `1000`, VAT-payable `2200`, VAT-input `1200`, A/P `2100`, withholding-payable `2300`, and one expense account per expenses-module category) with `form6111_code` pre-mapped for the P&L lines.
- [ ] Define default `coa_mappings` rows mapping each `source_kind` (`revenue`, `vat_payable`, `ar`, `bank`, `vat_input`, `ap`, `withholding`, plus each of the 8 expense categories from expenses-module) to a default account code.
- [ ] Provide `seedDefaultChartOfAccounts(env, tenantId)` that inserts defaults idempotently when a tenant first opens the accounting integration (called by the CoA GET route when no rows exist).
**Schema / Interfaces:**
```ts
export async function seedDefaultChartOfAccounts(env: Env, tenantId: string): Promise<void>;
// expense categories per expenses-module spec (8): map each to a 5xxx expense account + form6111 code.
```
**Acceptance:**
- [ ] First call to the CoA endpoint for a fresh tenant materializes the default accounts + mappings.
- [ ] Default `coa_accounts` cover all five posting `type` values and every expense category.

### Task 5: Movement-file generator (Hashavshevet, CP1255)
**Blocks:** 8  ·  **Blocked by:** 3
**Files:**
- Create: `apps/zync-api/src/reports/movement-file.ts`
**Steps:**
- [ ] Implement `generateMovementFile(env, tenantId, periodFrom, periodTo): Promise<{ r2Key: string }>` that calls `deriveMovements`, formats each movement as a fixed-field Hashavshevet uniform movement record (one line per movement leg), and writes the result to R2.
- [ ] Encode output bytes via the shared `apps/zync-api/src/lib/cp1255.ts` table (owned by spec 180) — CP1255 (Windows-Hebrew), no UTF-8 BOM. Characters outside CP1255 are transliterated/stripped per ITA fallback and logged.
- [ ] Field formatting matches spec 180 conventions: amounts in agorot (×100, no decimal point), dates `YYYYMMDD`, numeric fields zero-padded right-aligned; reuse spec 180's B100/B110 record layout — do not invent a new fixed-width layout.
- [ ] Stream generation to stay within Worker memory limits; route large ranges through the `export.generate` queue.
- [ ] Update the `accountant_export_jobs` row status `pending → running → done` (or `error`), set `r2_key` and `download_expires_at` (24h TTL).
**Schema / Interfaces:**
```ts
export async function generateMovementFile(
  env: Env, tenantId: string, periodFrom: string, periodTo: string, jobId: string
): Promise<void>; // updates accountant_export_jobs(jobId) in place
```
**Acceptance:**
- [ ] Generated file is CP1255-encoded with no UTF-8 BOM; Hebrew descriptions round-trip through the bundled table.
- [ ] Movement legs from `deriveMovements` map 1:1 to output records; amounts are in agorot.
- [ ] Job row reaches `done` with a valid `r2_key` and a future `download_expires_at`.

### Task 6: Form 6111 generator (structured Excel)
**Blocks:** 8  ·  **Blocked by:** 1, 4
**Files:**
- Create: `apps/zync-api/src/reports/form6111.ts`
**Steps:**
- [ ] Implement `generateForm6111(env, tenantId, taxYear, jobId): Promise<void>` producing structured Excel: one row per 6111 P&L field code (ITA codes 1300–6666, e.g. 1300 revenue, 2000 cost of sales, 3500 marketing, 5000 G&A, 6666 net profit — per gov.il itc6111) with amount + the supporting Zync category breakdown.
- [ ] Source ex-VAT P&L numbers mirroring `deriveMovements` semantics (invoice subtotals for revenue/credit-notes/bad-debts; expenses net of deductible VAT; contractor payouts gross) so 6111 reconciles to the movement file. The financial-statements dashboard's VAT-inclusive gross revenue is a separate report per its own spec.
- [ ] Treat expense `sourceMetadata.stockLines` / inventory receipt lines as a dedicated `inventory_stock` source and map it to the tenant CoA / default field 2000 instead of leaving those amounts inside the ordinary expense category bucket.
- [ ] Group amounts into 6111 fields via `coa_accounts.form6111_code`; report unmapped accounts so the UI can show the "21/23 mapped ⚠" warning count.
- [ ] Append a **tax-adjustment section stub** (empty field rows) for the accountant to complete; v1 emits the P&L section only.
- [ ] Write the workbook through the shared xlsx export utility with **formula-injection neutralization** (cells beginning `= + - @ \t \r` are prefixed/escaped) — security cross-cutting requirement, mandatory.
- [ ] Upload to R2, update the job row (`r2_key`, `download_expires_at` 24h, status).
**Schema / Interfaces:**
```ts
export async function generateForm6111(
  env: Env, tenantId: string, taxYear: number, jobId: string
): Promise<void>;
// row shape: { field_code: string, amount: number, breakdown: { category: string, amount: number }[] }
```
**Acceptance:**
- [ ] Excel contains one row per mapped 6111 P&L field with amount + category breakdown.
- [ ] Every string cell passes formula-injection neutralization.
- [ ] Counts of mapped vs total accounts are derivable for the UI warning.

### Task 7: CoA API routes
**Blocks:** 11, 12  ·  **Blocked by:** 1, 4
**Files:**
- Create: `apps/zync-api/src/routes/coa.ts`
- Modify: `apps/zync-api/src/index.ts` (mount router)
**Steps:**
- [ ] `GET /api/coa/accounts` → returns chart of accounts; if none exist, call `seedDefaultChartOfAccounts` first. Requires `reports:read`.
- [ ] `PUT /api/coa/accounts` → upsert accounts + `form6111_code` mapping (zod-validated body). Requires `reports:export` (edit is an export-config action) or ADMIN.
- [ ] `GET /api/coa/mappings` → returns category→account mapping. Requires `reports:read`.
- [ ] `PUT /api/coa/mappings` → upsert `coa_mappings` rows (zod-validated). Requires `reports:export` or ADMIN.
- [ ] All routes scoped through `tenantQuery`; enforce zod validation (`require-zod-validation-in-routes`) and `no-raw-drizzle-from-routes`.
**Schema / Interfaces:**
```
GET  /api/coa/accounts   → { accounts: CoaAccount[] }
PUT  /api/coa/accounts   → body { accounts: { code, name, type, form6111_code? }[] } → { updated: number }
GET  /api/coa/mappings   → { mappings: { source_kind, account_code }[] }
PUT  /api/coa/mappings   → body { mappings: { source_kind, account_code }[] } → { updated: number }
```
**Acceptance:**
- [ ] CoA edits persist and are tenant-isolated; invalid bodies 400 via zod.
- [ ] Fresh tenant GET auto-seeds defaults.

### Task 8: Accountant export job routes
**Blocks:** 13  ·  **Blocked by:** 2, 5, 6
**Files:**
- Create: `apps/zync-api/src/routes/accountant-exports.ts`
- Modify: `apps/zync-api/src/index.ts` (mount router)
**Steps:**
- [ ] `POST /api/reports/accountant/movement` → validate `{ periodFrom, periodTo, format }`, insert an `accountant_export_jobs` row (`kind='movement_file'`), enqueue/run `generateMovementFile`, return the job. Requires `accountant:export`.
- [ ] `POST /api/reports/accountant/form6111` → validate `{ year }`, insert job (`kind='form6111'`), run `generateForm6111` (period = full tax year), return job. Requires `accountant:export`.
- [ ] `GET /api/reports/accountant/exports` → list jobs for tenant (newest first). Requires `accountant:export`.
- [ ] `GET /api/reports/accountant/exports/:id` → job status + a fresh signed R2 URL when `status='done'` and `download_expires_at` is in the future; 410 if expired. Requires `accountant:export`.
- [ ] Wrap job creation + generation kickoff in a transaction that records an audit entry (`require-audit-in-transaction`) — generation is audited.
- [ ] Enforce zod validation; tenant-scope every query.
**Schema / Interfaces:**
```
POST /api/reports/accountant/movement  → body { periodFrom: YYYY-MM-DD, periodTo: YYYY-MM-DD, format: 'hashavshevet' } → { job }
POST /api/reports/accountant/form6111  → body { year: number } → { job }
GET  /api/reports/accountant/exports   → { jobs: AccountantExportJob[] }
GET  /api/reports/accountant/exports/:id → { job, downloadUrl?: string }
GET  /api/reports/accountant/inventory-count/xlsx?asOf=YYYY-MM-DD
                                        → direct XLSX stock-count sheet from current stock positions
```
**Acceptance:**
- [ ] Only OWNER/ADMIN/Accountant can hit these routes; others 403.
- [ ] Each generation writes an audit row in the same transaction.
- [ ] `:id` returns a signed URL only while unexpired; expired returns 410.

### Task 9: Accountant invite (scoped role grant)
**Blocks:** 13  ·  **Blocked by:** 2
**Files:**
- Modify: `apps/zync-api/src/routes/users.ts` (existing `/settings/users` invite handler)
**Steps:**
- [ ] Allow inviting a member with role `Accountant` via the existing invitations flow (`invitations` table), setting `tenant_memberships.is_accountant = true` on acceptance.
- [ ] Enforce that an Accountant membership is granted only the Accountant role's permissions (financial read+export); reject attempts to attach other roles to an accountant invite.
- [ ] Apply **data scoping** for accountant sessions: financial report/export queries only — no customer PII beyond billing identity (name + tax/VAT id); strip phone/email/address from any payload an accountant-role session can reach.
**Acceptance:**
- [ ] Inviting `accountant@cpa.co.il` as Accountant creates a membership with `is_accountant=true` and only the three financial permissions.
- [ ] An accountant-role session cannot read non-billing customer PII or non-financial modules.

### Task 10: Wire `deriveMovements` into uniform-format-export (spec 180)
**Blocks:** —  ·  **Blocked by:** 3
**Files:**
- Modify: `apps/zync-api/src/reports/uniform-format.ts`
**Steps:**
- [ ] At step 5 ("B110/B100 only if accountant-export ledger enabled"), call `hasAccountantLedger(env, tenantId)`; when true, call `deriveMovements(env, tenantId, from, to)` and emit B100/B110 journal records from the returned movements.
- [ ] When false (no `coa_mappings`), keep B100/B110 counts at 0 and let `INI.txt` declare documents-only (`ערכים בלבד`) mode — legally permitted; do not re-derive movements locally.
- [ ] Reuse the same CP1255 byte mapping and agorot/date formatting already in that file; the only change is sourcing B100/B110 rows from `deriveMovements`.
**Acceptance:**
- [ ] With a mapped chart of accounts, the uniform-format ZIP's `BKMVDATA.txt` includes B100/B110 records and `INI.txt` counts them.
- [ ] With no mapping, B100/B110 counts are 0 and the export is documents-only — unchanged from spec 180 baseline.

### Task 11: UI — `/settings/integrations/accounting` page shell
**Blocks:** 12, 13  ·  **Blocked by:** 7
**Files:**
- Create: `apps/zync-app/src/pages/settings/integrations/AccountingPage.tsx`
- Modify: `apps/zync-app/src/router.tsx` (route registration)
**Steps:**
- [ ] Build the page layout from the spec wireframe: Movement file section (period selector Q1/Q2/Q3/Q4/custom + format `Hashavshevet`), Form 6111 section (tax-year selector), Chart-of-accounts summary with "Edit mapping" link, Your-accountant block, Recent-exports list.
- [ ] Guard the route behind `reports:read`; the generate actions behind `accountant:export` (hide/disable for read-only sessions).
- [ ] Render Hebrew labels alongside English: קובץ תנועות (movement file), טופס 6111 (Form 6111); ensure RTL layout for Hebrew tenants via `useDirection`.
- [ ] Honor `prefers-reduced-motion` on any transitions; use `@zync/ui` primitives (`Card`, `Select`, `Button`, `Stack`).
**Acceptance:**
- [ ] Page renders all five sections; generate controls hidden for non-export sessions.
- [ ] Hebrew labels present and layout flips RTL for Hebrew locale.

### Task 12: UI — Chart-of-accounts editor
**Blocks:** —  ·  **Blocked by:** 7, 11
**Files:**
- Create: `apps/zync-app/src/pages/settings/integrations/ChartOfAccountsEditor.tsx`
- Create: `apps/zync-app/src/hooks/useChartOfAccounts.ts`
**Steps:**
- [ ] Fetch accounts + mappings (`GET /api/coa/accounts`, `GET /api/coa/mappings`); editable table of code / name / type / `form6111_code` and the category→account mapping grid.
- [ ] Save via `PUT /api/coa/accounts` and `PUT /api/coa/mappings`; optimistic + toast on success.
- [ ] Compute and display the "6111 codes mapped: 21/23 ⚠" summary; give the warning an accessible label (`aria-label` / `role="status"`) describing how many accounts still need a 6111 code.
**Schema / Interfaces:**
```ts
export function useChartOfAccounts(): {
  accounts: CoaAccount[]; mappings: CoaMapping[];
  saveAccounts(a: CoaAccountInput[]): Promise<void>;
  saveMappings(m: CoaMappingInput[]): Promise<void>;
  mappedCount: number; totalCount: number;
};
```
**Acceptance:**
- [ ] Editing an account's 6111 code persists and updates the mapped-count badge.
- [ ] The ⚠ warning has a screen-reader-accessible description.

### Task 13: UI — generate actions, recent exports, accountant invite
**Blocks:** —  ·  **Blocked by:** 8, 9, 11
**Files:**
- Modify: `apps/zync-app/src/pages/settings/integrations/AccountingPage.tsx`
- Create: `apps/zync-app/src/hooks/useAccountantExports.ts`
**Steps:**
- [ ] Wire "Generate movement file" → `POST /api/reports/accountant/movement`; "Generate 6111 Excel" → `POST /api/reports/accountant/form6111`; poll `GET /api/reports/accountant/exports/:id` until `done`, then surface a download link from the signed URL.
- [ ] Render the Recent-exports list from `GET /api/reports/accountant/exports` with a download (`⬇`) affordance per row, disabled/expired when past `download_expires_at`.
- [ ] "Invite accountant" opens an invite dialog posting to the existing `/settings/users` invite endpoint with role `Accountant`; show the invited accountant's email + role + invite date.
- [ ] Surface job `error` status with an accessible alert; respect reduced-motion on the polling spinner.
**Schema / Interfaces:**
```ts
export function useAccountantExports(): {
  jobs: AccountantExportJob[];
  generateMovement(p: { periodFrom: string; periodTo: string }): Promise<AccountantExportJob>;
  generateForm6111(p: { year: number }): Promise<AccountantExportJob>;
  downloadUrl(id: string): Promise<string>;
};
```
**Acceptance:**
- [ ] Generating either export shows progress then a working download link.
- [ ] Expired rows show no live download link.
- [ ] Inviting an accountant creates the scoped grant and shows in the "Your accountant" block.
