# Multi-Currency Support

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 67  
**Tier:** Business+ (multi-currency enabled); Freelancer locked to ILS  
**Depends on:** `invoices-core`, `expenses-module`, `settings-module`, `contractor-payouts`, `foundation-auth-rbac`  
**Referenced by:** `invoices-core`, `settings-module`

---

## Overview

Extends Zync beyond ILS-only invoicing. Business+ tenants can issue invoices in ILS, USD, or EUR. Exchange rates are manually entered by the tenant (no automatic FX feed). All internal accounting remains in ILS; foreign-currency invoices store both the face amount (in invoice currency) and the ILS equivalent (at the rate entered at time of issue).

IL law requirement: Tax invoices (חשבונית מס) must state the ILS amount. Foreign-currency invoices must include the exchange rate and ILS equivalent on the document.

---

## Supported Currencies

`ILS` (default), `USD`, `EUR`. Additional currencies are a future extension — not in this spec (YAGNI).

---

## Settings: `/settings/business` (existing page, extended)

Spec 25 (`settings-module`) already lists "Invoice currency: ILS / USD / EUR" in the Business Profile section. This spec defines the data and behavior behind that field.

**Default invoice currency**: stored in `tenants.settings->>'default_invoice_currency'` (default `ILS`). Applies to new invoices only — changing it does not retroactively affect existing invoices.

**Exchange rates**: tenants on Business+ can store manual reference rates:

```
┌────────────────────────────────────────────────────────────┐
│  Exchange Rates (manual)                                   │
│                                                            │
│  1 USD = [3.72___] ILS    Last updated: 2026-05-28 by Alex │
│  1 EUR = [4.01___] ILS    Last updated: 2026-05-25 by Alex │
│                                                            │
│  [Update rates]                                            │
│                                                            │
│  ⓘ Rates apply to new invoices. Issued invoices retain     │
│    the rate that was in effect at issue time.              │
└────────────────────────────────────────────────────────────┘
```

Stored in `tenant_settings.exchange_rates JSONB` (spec 17's `tenant_settings` table — module configuration separate from `tenants.settings` JSONB which holds business-profile prefs per spec 25): `{ "USD": 3.72, "EUR": 4.01, updatedAt: "...", updatedBy: "..." }`.

---

## Invoice Creation: Currency Selection

New invoice form (spec 15): currency dropdown defaults to `default_invoice_currency`. Can be changed per invoice. Currency is locked once the invoice reaches `SENT`.

Line items: all `unit_price` and `subtotal` stored in the invoice's currency. `invoices.currency` column (already present in spec 15) holds the face currency.

At `TAX_ISSUED` transition:
- System records the current exchange rate from `tenant_settings.exchange_rates`
- Stores: `invoices.ils_exchange_rate NUMERIC(10,4)` and `invoices.total_ils NUMERIC(12,2)`
- These fields are immutable after `TAX_ISSUED` (snapshot at issue time)

New columns added to `invoices`:

```sql
ALTER TABLE invoices ADD COLUMN ils_exchange_rate NUMERIC(10,4);
  -- NULL for ILS invoices; set at TAX_ISSUED for foreign currency
ALTER TABLE invoices ADD COLUMN total_ils        NUMERIC(12,2);
  -- total in ILS at exchange rate; NULL for ILS invoices
```

---

## Invoice Document (PDF/HTML)

Foreign-currency invoices must show both the face amount and the ILS equivalent:

```
Item 1 — Product A            USD 500.00
Item 2 — Consulting           USD 300.00
─────────────────────────────────────────
Subtotal                      USD 800.00
VAT (17%)                     USD 136.00
Total                         USD 936.00

Exchange rate at issue: 1 USD = 3.72 ILS
Total in ILS: ₪3,482.00  (for tax purposes)
```

ILS total on document = `invoices.total_ils` (immutable snapshot). The "exchange rate at issue" line is mandatory under Israeli revenue law.

---

## Report Handling

Spec 24 (`reports-analytics`) aggregates invoice revenues. Multi-currency impact:
- All `revenue` aggregations sum `invoices.total_ils` (not `invoices.total`)
- Foreign-currency invoices without `total_ils` (pre-feature, or ILS invoices) use `total` directly (both are ILS in that case)
- Report UI shows "All amounts converted to ILS at rate of issue"

---

## Expense Currency

Expenses (spec 17) may also be in foreign currencies (e.g. SaaS subscription billed in USD). Two new optional columns on `expenses`:

```sql
ALTER TABLE expenses ADD COLUMN original_currency TEXT;          -- 'USD', 'EUR', etc.
ALTER TABLE expenses ADD COLUMN original_amount   NUMERIC(12,2); -- face amount in foreign currency
-- existing `amount` column = ILS equivalent (already how OCR works for foreign receipts)
```

OCR extraction: Claude Vision already extracts amounts in whatever currency appears on the receipt. The ILS amount in `expenses.amount` is the deductibility base (IL law). `original_currency` + `original_amount` are informational.

---

## API Changes

```
PATCH /api/settings/business
  body extended: { defaultInvoiceCurrency?, exchangeRates?: { USD?, EUR? } }

GET  /api/invoices (list)
  response extended: each invoice includes { currency, totalIls?, ilsExchangeRate? }

POST /api/invoices/:id/issue-tax
  → at TAX_ISSUED: snapshots exchange rate from tenant_settings + computes total_ils
```

---

## Bank of Israel Exchange Rate Sync (Optional)

Tenants on Business+ can opt in to automatic exchange rate fetching from the Bank of Israel's official XML feed. This replaces manual entry for the reference rate, but staff still confirm before rates are applied (see "Staff confirmation" below).

### BoI XML feed

Bank of Israel publishes official daily exchange rates at:
```
https://www.boi.org.il/currency.xml
```

Published each business day at approximately 16:00 IL time. The feed includes USD and EUR rates (among others). Rates are the official representative rates used for tax purposes under Israeli revenue law (Income Tax Ordinance, Section 2(e)).

### Cron job

```toml
# wrangler.toml
[[triggers.crons]]
name = "boi-exchange-rates"
cron = "0 14 * * 1-5"  # 14:00 UTC = 17:00 IL (post-BoI publication); Mon–Fri only
```

```ts
// apps/zync-api/src/cron/boi-exchange-rates.ts
export async function fetchBoiRates(env: Env): Promise<void> {
  const res = await fetch('https://www.boi.org.il/currency.xml', {
    headers: { 'Accept': 'application/xml' },
    cf: { cacheTtl: 3600 }  // cache at edge for 1 hour; BoI publishes once/day
  })
  if (!res.ok) {
    console.error('BoI feed fetch failed:', res.status)
    return  // silent fail; manual rates remain unchanged
  }

  const xml = await res.text()
  const rates = parseBoiXml(xml)  // extracts CURRENCY[@id='USD'] and ['EUR'] RATE values

  // Store pending rates in KV (not yet applied to tenant_settings)
  // Key: boi_rates:YYYY-MM-DD → { USD: 3.72, EUR: 4.01, publishedAt: "..." }
  const dateKey = new Date().toISOString().slice(0, 10)
  await env.KV.put(`boi_rates:${dateKey}`, JSON.stringify({
    USD: rates.USD,
    EUR: rates.EUR,
    publishedAt: new Date().toISOString(),
    source: 'bank_of_israel'
  }), { expirationTtl: 86400 * 30 })  // keep 30 days of history

  // Notify tenants with auto-sync enabled (opt-in flag below)
  // Notification type: 'exchange_rates_updated' (informational; staff must still confirm)
  await notifyTenantsWithAutoSync(env, rates)
}
```

### Tenant opt-in

```
┌────────────────────────────────────────────────────────────┐
│  Exchange Rates                                            │
│                                                            │
│  ☐ Auto-fetch from Bank of Israel (daily, 17:00 IL time)  │
│                                                            │
│  When enabled: BoI rates fetched daily and shown here as   │
│  "Suggested rate". You confirm before rates are applied.   │
└────────────────────────────────────────────────────────────┘
```

Schema delta:
```sql
ALTER TABLE tenant_settings ADD COLUMN boi_rate_sync_enabled BOOLEAN NOT NULL DEFAULT false;
```

### Staff confirmation flow

When `boi_rate_sync_enabled = true` and new BoI rates are available:
1. Settings page shows pending rates with diff vs current stored rate:
   ```
   USD: 3.72 → 3.78 (BoI, 2026-06-01)   [Apply]  [Dismiss]
   EUR: 4.01 → 4.05 (BoI, 2026-06-01)   [Apply]  [Dismiss]
   ```
2. Staff clicks [Apply] → `PATCH /api/settings/business` with `{ exchangeRates: { USD: 3.78 } }`
3. Rate stored in `tenant_settings.exchange_rates` as before; new invoices use updated rate

**Staff confirmation is mandatory** — rates are never applied automatically without a human action. This preserves the legal responsibility model: the tenant declares the rate, not the system.

### API addition

```
GET /api/settings/exchange-rates/suggested
    → { USD?: number, EUR?: number, publishedAt?: string, source?: 'bank_of_israel' }
      Returns today's BoI rates from KV if available; null if not yet fetched or tenant has sync disabled
      Requires: settings:read
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Manual exchange rates (default) | No automatic application | IL revenue law requires the declared exchange rate to match official Bank of Israel rates — automatic application without staff sign-off creates legal ambiguity. Manual entry (or BoI-suggested + confirmed) makes the tenant responsible |
| BoI sync as opt-in | Not enabled by default | Not all tenants need foreign currency; adding a daily cron fetch for all tenants wastes resources. Only Business+ tenants who deal in USD/EUR benefit |
| Staff confirmation required | Not auto-apply | Even with BoI data, the human click creates the audit trail that the declared rate was intentional and tenant-approved |
| ILS snapshot at issue | Not at send or payment | TAX_ISSUED is the legally binding moment. Exchange rate on that date determines the ILS tax amount. Rate changes after issue do not affect a finalized invoice |
| ILS amounts in reports | Not foreign currency | All compliance reports (PCN874, income tax estimate) are ILS-denominated. Mixing currencies in aggregates would produce incorrect totals |
| Only ILS, USD, EUR | Not all ISO 4217 | Israeli businesses predominantly deal in these three. Full currency list adds UI complexity (260+ options) for negligible benefit |
