# Uniform Format Export (מבנה אחיד / BKMVDATA)

**Date:** 2026-06-01
**Status:** Draft
**Spec:** 180
**Tier:** All tiers (legally required for any IL business with computerized bookkeeping)
**Depends on:** `invoices-core`, `invoice-receipt-document`, `invoice-credit-notes`, `expenses-module`, `vendors-suppliers`, `system-i18n`, `foundation-auth-rbac`
**Referenced by:** `israeli-tax-reports`, `accountant-export`, `reports-navigation-hub`, `data-export-gdpr`

---

## Overview

Israeli bookkeeping regulations (הוראות ניהול ספרים — תוספת ב' "מבנה אחיד") require every computerized accounting system to be able to produce, on demand, a **uniform-structure export** that an ITA auditor can ingest. This is a hard legal requirement for the bookkeeping system itself and is **distinct from `ita-einvoice` (spec 165)**, which registers individual invoices live via the Shaba API. מבנה אחיד is a bulk audit-file export over a date range.

The export is two fixed-format files:

| File | Purpose |
|------|---------|
| `INI.txt` | Index/summary — record-type counts + business header |
| `BKMVDATA.txt` | The data: documents, document lines, journal entries, accounts, inventory |

Both are **CP1255 (Windows-Hebrew) encoded**, newline-terminated, fixed-field records keyed by a 4-char record-type code.

---

## Record Types (BKMVDATA)

| Code | Record | Source in Zync |
|------|--------|----------------|
| `A000` | Opening / business header (software registration no., primary ID, period) | tenant business profile + Zync software ID |
| `A100` | Accounting opening record | generated |
| `C100` | Document header (invoice, receipt, credit note) | `invoices`, `receipts`, credit notes |
| `D110` | Document line item | `invoice_lines`, credit-note lines |
| `D120` | Receipt / payment detail (method, cheque, card, bank) | `receipt_payment_lines`, `invoice_payments` |
| `B100` | Journal-entry transaction (movement) | derived ledger movements (see `accountant-export`) |
| `B110` | Account record (chart-of-accounts entry) | tenant chart of accounts |
| `M100` | Inventory item | omitted in v1 (no inventory module) — count = 0 |
| `Z900` | Closing record (totals + control sum) | generated |

`INI.txt` carries one summary line per record-type code with its count, plus the same `A000` header fields, used by the auditor's tool to validate completeness against `BKMVDATA.txt`.

> **v1 scope:** Zync is a documents-and-cash system, not a double-entry GL. Document records (`C100`/`D110`/`D120`) are always produced. Journal/account records (`B100`/`B110`) are produced from the derived ledger when `accountant-export` (spec 181) is enabled; otherwise their counts are 0 and `INI.txt` declares the export as documents-only (`ערכים בלבד` mode), which is permitted.

---

## Generation

```ts
// apps/zync-api/src/reports/uniform-format.ts
// Streaming generation to avoid Worker memory limits on large ranges.
export async function generateUniformExport(env: Env, tenantId: string, from: string, to: string) {
  const enc = new TextEncoder()                 // → re-encode to CP1255 (see note)
  const counts: Record<string, number> = {}
  // 1. A000 header (business primary ID = ע.מ/ח.פ, software reg no.)
  // 2. Stream C100/D110 from invoices + invoice_lines in range
  // 3. Stream C100 (doc_type receipt) + D120 from receipts + receipt_payment_lines
  // 4. Stream C100/D110 for credit notes
  // 5. B110/B100 only if accountant-export ledger enabled
  // 6. Z900 control totals
  // counts accumulate per record code → INI.txt
}
```

**Encoding note:** Worker runtime is UTF-16/UTF-8 native; CP1255 is not built-in. Use a bundled CP1255 encoding table (`apps/zync-api/src/lib/cp1255.ts`) to map each output line to bytes. Lines with characters outside CP1255 (rare; e.g. emoji in a free-text field) are transliterated/stripped per ITA fallback rules and logged.

**Field formatting:** fixed-width numeric fields are zero-padded right-aligned; amounts in agorot (×100, no decimal point) per spec; dates `YYYYMMDD`; the record-type code occupies the first 4 chars of every line.

---

## UI

### `/reports/uniform-format` (linked from `reports-navigation-hub` + `/settings/data`)

```
┌──────────────────────────────────────────────────────────────┐
│  Uniform Format Export (מבנה אחיד)                            │
│                                                              │
│  Israeli Tax Authority audit file. Generates INI.txt +       │
│  BKMVDATA.txt for the selected period.                       │
│                                                              │
│  Period   ○ Tax year [2025 ▾]   ○ Custom [from] – [to]      │
│  Mode     ◉ Documents only   ○ Documents + journal           │
│                                                              │
│  Primary ID (ע.מ/ח.פ)  514888888   ← from business profile  │
│  Software reg. no.      [auto / Zync ITA registration]       │
│                                                              │
│  [Generate export]                                           │
│                                                              │
│  ── Recent exports ──────────────────────────────────────── │
│  2025 tax year   ·  generated 2026-04-12  ·  [⬇ ZIP]        │
└──────────────────────────────────────────────────────────────┘
```

Output is a ZIP (`INI.txt` + `BKMVDATA.txt`) written to R2, delivered via signed URL (24h TTL), and logged in `uniform_export_jobs`. Large ranges run through the `export.generate` queue (reuses spec 28 infra).

```sql
CREATE TABLE uniform_export_jobs (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id     UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  period_from   DATE NOT NULL,
  period_to     DATE NOT NULL,
  mode          TEXT NOT NULL CHECK (mode IN ('documents','documents_journal')),
  status        TEXT NOT NULL DEFAULT 'pending',  -- pending|running|done|error
  record_counts JSONB,                             -- {C100: 412, D110: 1033, ...}
  r2_key        TEXT,
  download_expires_at TIMESTAMPTZ,
  generated_by  UUID REFERENCES users(id) ON DELETE SET NULL,
  created_at    TIMESTAMPTZ DEFAULT NOW()
);
```

---

## API

```
POST /api/reports/uniform-format     → start generation (body: from, to, mode) → job id
GET  /api/reports/uniform-format     → list past export jobs
GET  /api/reports/uniform-format/:id → job status + signed ZIP url when done
```

Requires `reports:export` **and** OWNER/ADMIN (contains full financial history). Generation is audited.

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Distinct from spec 165 (ITA e-invoice) | Separate spec | Shaba registers single invoices live; מבנה אחיד is a bulk audit file — different format, trigger, and legal basis |
| Documents-only mode default | Not full GL | Zync is a documents+cash system; ITA permits a documents-only (`ערכים בלבד`) uniform export; full journal only when `accountant-export` ledger is on |
| CP1255 via bundled table | Not runtime `TextEncoder` | Worker has no CP1255; ITA tooling rejects UTF-8; explicit mapping is mandatory |
| Stream + queue for large ranges | Not in-memory build | Multi-year exports exceed Worker memory; reuse spec 28 streaming export pattern |
| Amounts in agorot | Integer ×100 | Uniform-format spec forbids decimal separators in monetary fields |
