# Vendors / Suppliers (ספקים)

**Date:** 2026-06-01
**Status:** Draft
**Spec:** 182
**Tier:** All tiers (supplier withholding report Business+)
**Depends on:** `expenses-module`, `expense-reports-ui`, `contractor-payouts`, `reports-analytics`, `foundation-auth-rbac`, `unified-attachments`, `settings-module`
**Referenced by:** `expenses-module`, `expense-reports-ui`, `reports-analytics`, `recurring-expenses`, `accountant-export`, `israeli-tax-reports`

---

## Overview

Expenses currently store the supplier as **free text** (`expenses.vendor_name`, `expenses.vendor_tax_id`) with no entity behind it. This breaks two things:

1. **Vendor analysis** (`/api/expenses/reports/vendors`) groups by a typo-prone string — "AWS", "Amazon Web Services", and "amazon" become three vendors.
2. **Supplier withholding (ניכוי במקור)** — `reports-analytics` promises an *"annual supplier withholding report"* (consolidated **Form 856**, filed to the ITA), but there is no supplier entity to hold a withholding rate or exemption certificate (only **contractors** have these, per `contractor-payouts`). That flow is currently un-buildable.

This spec adds a first-class `vendors` entity, CRUD UI, expense linkage, and completes the supplier Form 856 flow.

> **Form 856 vs 857:** Form **856** is the consolidated annual report the payer files *to the ITA* (route `withholding` → `/reports/withholding`, nav label "Mas 856"). Form **857** is the per-supplier annual certificate the payer issues *to each* supplier/contractor. This spec produces the consolidated 856 report; per-vendor 857 certificates are a downloadable per-row artifact.

---

## Data Model

```sql
CREATE TABLE vendors (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id     UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  name          TEXT NOT NULL,
  tax_id        TEXT,                       -- ח.פ. / ע.מ.
  -- Withholding (ניכוי במקור)
  withholding_rate            NUMERIC(5,4), -- NULL = apply statutory default from tax_rates.withholding_default (currently 0.30); 0 = exempt with valid cert
  withholding_cert_number     TEXT,
  withholding_cert_expiry     DATE,
  withholding_cert_r2_key     TEXT,         -- via unified-attachments
  -- Defaults
  default_category   TEXT,                  -- prefills expense category
  default_vat_deductible BOOLEAN DEFAULT true,
  payment_terms_days INTEGER DEFAULT 30,
  -- Contact
  email         TEXT,
  phone         TEXT,
  address       TEXT,
  notes         TEXT,
  is_archived   BOOLEAN NOT NULL DEFAULT false,
  created_at    TIMESTAMPTZ DEFAULT NOW(),
  updated_at    TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE (tenant_id, tax_id)               -- partial: WHERE tax_id IS NOT NULL
);

CREATE INDEX idx_vendors_tenant ON vendors(tenant_id, is_archived);
```

**Schema delta on `expenses`:**
```sql
ALTER TABLE expenses ADD COLUMN vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL;
-- vendor_name/vendor_tax_id retained for OCR capture + legacy; vendor_id is the linked entity when matched
```

**Schema delta on `recurring_expenses`** (spec 172): add `vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL`.

---

## Features

### Vendor list — `/vendors` (also reachable from the `/expenses` header)

```
┌──────────────────────────────────────────────────────────────┐
│  Vendors                              [⌕]      [+ New vendor] │
│                                                              │
│  Name                Tax ID       Withholding   YTD spend     │
│  ─────────────────────────────────────────────────────────── │
│  Amazon Web Services 514xxxxxx    —             ₪10,680      │
│  Office rent — Migdal 058xxxxxx   0% (cert✓)    ₪42,000      │
│  Freelance design Co 999xxxxxx    30% ⚠ exp.   ₪18,500      │
│                                                              │
│  3 vendors · 1 certificate expiring soon                     │
└──────────────────────────────────────────────────────────────┘
```

⚠ badge when `withholding_cert_expiry < now()+30d`. "Withholding" column: `—` (none), `X% (cert✓)`, or `statutory default (30%)` when rate NULL.

### Vendor detail — `/vendors/:id`

Tabs: **Overview** (contact, defaults), **Withholding** (rate + certificate upload via `unified-attachments`, expiry), **Expenses** (all linked expenses, totals), **Activity** (`activity-timeline`).

On phone widths, list and detail routes use 16px inline padding. Header controls and detail tabs wrap or scroll horizontally without clipping controls or content.

Rationale: preserve vendor management workflows inside the mobile app frame.

### Expense → vendor linkage

Expense create/edit + OCR review (`expense-ocr-correction-ux`) gains a vendor **type-ahead** (matches `vendors.name`). On no match: "Create vendor 'X'" inline, prefilling `tax_id` from OCR. The existing free-text fields become the OCR capture; selecting a vendor sets `vendor_id` and inherits defaults.

### Supplier withholding report (Form 856) — completes `reports-analytics` flow

`/reports/withholding` (extends the contractor report) now unions **contractors + vendors**: any vendor with `withholding_rate IS NOT NULL OR withheld amounts > 0` over the year, grouped by vendor, with withheld totals for annual Form 856 filing. Per-vendor row offers a downloadable **857** certificate. Withheld amount per expense computed at payment using `vendor.withholding_rate` (when NULL + no valid cert, fall back to the statutory default resolved from `tax_rates.withholding_default` for `country_code = 'IL'`, latest `effective_from <= expense_date` — currently 30%, mirroring `contractor-payouts` logic).

---

## API

```
GET    /api/vendors                  → list (search, archived filter); includes YTD spend
POST   /api/vendors                  → create
GET    /api/vendors/:id              → detail + linked expenses summary
PATCH  /api/vendors/:id              → edit
POST   /api/vendors/:id/archive      → archive (kept for history; unlinkable)
GET    /api/vendors/suggest?q=       → type-ahead for expense form
GET    /api/reports/withholding      → contractors + vendors 856 report (Business+)
```

CRUD requires `expenses:write`; withholding report requires `reports:read`.

---

## Migration

One-off backfill: group existing `expenses` by normalized `vendor_name`, create `vendors` rows, set `vendor_id`. Ambiguous/blank names left unlinked (free text preserved). Runs as a data migration, not destructive.

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| First-class `vendors` entity | Not free text | Enables dedup, withholding tracking, defaults, and the promised supplier 856 report |
| Keep `vendor_name`/`vendor_tax_id` on expenses | Alongside `vendor_id` | OCR captures raw text before a vendor is matched; legacy rows stay valid |
| Reuse contractor withholding logic | Shared statutory-default rule (`tax_rates.withholding_default`) | Supplier and contractor withholding follow the same ITA mechanics; one code path, one seeded default (30%) |
| Vendor type-ahead with inline create | Not a separate step | Keeps expense entry fast; vendor is created in the flow, not as a prerequisite |
| Archive, not delete | Soft state | Vendors are referenced by historical expenses; deletion would orphan financial history |
