# Customer Statement (כרטסת לקוח)

**Date:** 2026-06-01
**Status:** Draft
**Spec:** 183
**Tier:** All tiers
**Depends on:** `customers-module`, `invoices-core`, `invoice-receipt-document`, `invoice-credit-notes`, `partial-payment-recording`, `ar-aging-report`, `multi-currency`, `print-layouts`, `hebrew-locale-dates`
**Referenced by:** `customers-module`, `ar-aging-report`, `print-layouts`, `tenant-portals`

---

## Overview

A **customer statement (כרטסת לקוח / statement of account)** lists every financial document for a customer over a date range with a running balance — the standard collections artifact a business sends when chasing payment. It is referenced by `print-layouts` (spec 161) and `ar-aging-report` (spec 140) but **no screen generates it**. This spec adds the screen, PDF, and email/portal delivery. No new tables — it is a read-only projection over existing invoices, receipts, credit notes, and payments.

---

## Statement contents

For a customer + date range `[from, to]`:

- **Opening balance** — net of all documents dated before `from`.
- **Rows**, chronologically: tax invoices (debit), credit notes (credit), receipts/payments (credit), each with date, document type + number, reference, debit, credit, running balance.
- **Closing balance** — equals the customer's current A/R when `to = today`.
- Aging summary footer (0-30/31-60/61-90/90+) reused from `ar-aging-report`.
- Multi-currency: grouped per currency (a customer billed in ILS + USD gets two sub-statements); ILS snapshot values used for the aging footer.

---

## UI — `/customers/:id/statement`

Reached from customer detail header ("Statement") and from `ar-aging-report` row drill-down.

```
┌──────────────────────────────────────────────────────────────┐
│  Statement of Account — Acme Ltd            [⬇ PDF] [✉ Send] │
│  Period [2026-01-01] – [2026-06-01]   Currency [ILS ▾]       │
│                                                              │
│  Opening balance (before 01/01)                  ₪ 0.00     │
│  ─────────────────────────────────────────────────────────── │
│  Date        Document            Debit    Credit   Balance   │
│  01/02/26    Invoice #2026-0042  ₪1,170             ₪1,170   │
│  15/02/26    Receipt #R-0011               ₪1,170    ₪0.00   │
│  03/03/26    Invoice #2026-0061  ₪2,340             ₪2,340   │
│  12/03/26    Credit note #C-0003           ₪234     ₪2,106   │
│  ─────────────────────────────────────────────────────────── │
│  Closing balance                                 ₪2,106     │
│                                                              │
│  Aging:  0-30 ₪2,106 · 31-60 ₪0 · 61-90 ₪0 · 90+ ₪0        │
└──────────────────────────────────────────────────────────────┘
```

- **⬇ PDF** — HTML-to-PDF Worker (reuses `invoice-pdf-customization` config); Hebrew RTL; `window.print()` fallback per `print-layouts`. The API accepts provider output only when it is `application/pdf` with structurally plausible PDF bytes (header, cross-reference pointer, terminal `%%EOF`, and non-empty length); invalid output returns typed 502 without provider-body disclosure and is logged with status diagnostics. Rationale: provider `2xx` responses can still contain HTML/JSON errors or truncated bytes, and must never be downloaded as a PDF.
- **✉ Send** — emails the PDF to the customer (uses tenant email adapter; Hebrew subject/body per locale).
- **Customer portal:** statement is available read-only in the customer portal (`tenant-portals`) under "Account", honoring `customer-portal-access-control` visibility.

---

## API

```
GET /api/customers/:id/statement?from=&to=&currency=   → JSON rows + opening/closing/aging
GET /api/customers/:id/statement/pdf?from=&to=&currency= → signed PDF
POST /api/customers/:id/statement/send                  → email PDF to customer (body: from, to, currency, message?)
```

Read requires `customers:read` (+ `invoices:read`); send requires `customers:write`. Portal access via portal JWT (`customer-portal-access-control`).

### Query shape

```sql
-- Opening balance: sum of debits − credits dated < :from
-- Rows: UNION of invoices (TAX_ISSUED+), credit notes, receipts/payments in range, ordered by date
-- Running balance computed in the API layer over the ordered rows (window function in SQL also acceptable)
```

Indexes already present on `invoices(customer_id, status)`, `invoice_payments(...)`; statement adds no new index requirement beyond `receipts(invoice_id)` (spec 179) and the report-path indexes in `financial-statements`.

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| No new table | Read-only projection | All data exists in invoices/receipts/credit-notes/payments; a stored statement would duplicate and drift |
| Per-currency sub-statements | Not single mixed column | Running balance is meaningless across currencies; IL law tracks ILS, but customers see their billed currency |
| Reuse aging from spec 140 | Not re-derived | Single source for bucket logic; statement footer and AR-aging stay consistent |
| Running balance in API layer | Not denormalized | Cheap over a bounded date range; avoids a maintained ledger balance column |
| Portal-visible | Read-only via portal JWT | Customers self-serve their account history, reducing collections email load |
