# Invoice Approval Workflow UI

**Date:** 2026-06-01
**Status:** Draft
**Spec:** 155
**Tier:** All tiers
**Depends on:** `invoices-core`, `foundation-auth-rbac`, `foundation-design-system`
**Referenced by:** `invoices-core`

---

## Overview

Spec 15 (`invoices-core`) defines the invoice lifecycle: `DRAFT → SENT → APPROVED → TAX_ISSUED → PAID`. When an invoice is `SENT`, the customer must approve the חשבונית עסקה before a tax invoice (חשבונית מס) can be issued. Staff can also approve on behalf of a customer via `POST /api/invoices/:id/approve`. However, no spec defines:

1. A queue UI where staff can see all `SENT` invoices awaiting approval
2. Approval / rejection actions with reason
3. Bulk approval for multiple invoices
4. Approval history per invoice

This spec defines the `/invoices/approvals` UI and the approval actions in invoice detail.

---

## Route

`/invoices/approvals` — requires `invoices:write`.

Also: approval actions embedded in invoice detail (spec 15).

---

## Approvals Queue

```
┌──────────────────────────────────────────────────────────────┐
│  Invoices > Pending Approval  (12)                           │
│                                                              │
│  [Search customer...]    [Sort: Oldest first ▾]              │
│                                                              │
│  ☐  INV-PRO-001  Acme Corp       ₪4,200   Sent 3d ago       │
│  ☐  INV-PRO-002  Beta Ltd        ₪12,500  Sent 1d ago       │
│  ☐  INV-PRO-007  Gamma Inc       ₪890     Sent 5h ago       │
│  ☐  INV-PRO-009  Acme Corp       ₪7,100   Sent 12h ago      │
│  ...                                                         │
│                                                              │
│  ─── Selected: 2 ───────────────────────────────────────── │
│  [Approve selected]    [Reject selected]                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘
```

Each row shows: proforma number, customer name, total amount, date sent.  
Click row → opens invoice detail with approval actions visible.

**Sort options:** Oldest first · Newest first · Amount (high) · Customer A–Z

---

## Approve Action (single invoice)

In invoice detail (`SENT` state), the action bar:

```
┌──────────────────────────────────────────────────────────────┐
│  INV-PRO-001 · Acme Corp · ₪4,200                           │
│  Status: SENT   Sent 3 days ago                              │
│                                                              │
│  [Resend]   [Approve on behalf of customer]   [Reject]       │
└──────────────────────────────────────────────────────────────┘
```

**[Approve on behalf of customer]** → confirmation dialog:

```
Approve INV-PRO-001?
Approving on behalf of Acme Corp. This will transition
the invoice to APPROVED status.

  Approved by: [You (Dan Cohen) ▾]  or  [Customer — Acme Corp contact ▾]
  Approval note (optional): [________________________]

[Cancel]    [Approve]
```

On confirm: `POST /api/invoices/:id/approve` → `status → 'APPROVED'`, `approved_at = now()`.

---

## Reject Action (single invoice)

**[Reject]** → sheet:

```
Reject INV-PRO-001?
Invoice returns to DRAFT for editing. Customer will not
be notified automatically.

  Reason for rejection (shown in activity feed):
  [___________________________________]
  (required — min 5 characters)

  ☑ Notify customer by email

[Cancel]    [Reject invoice]
```

On confirm: `POST /api/invoices/:id/reject` body `{ reason, notifyCustomer }` → `status → 'REJECTED'`.

---

## Bulk Approve

Selecting multiple rows → **[Approve selected]** → confirmation:

```
Approve 4 invoices?
Total value: ₪24,690 · Customers: Acme, Beta, Gamma, Delta

[Cancel]    [Approve all]
```

Executes `POST /api/invoices/bulk-approve` with `{ ids: [...] }`. Each invoice processed in its own DB transaction. Partial failure (some already non-SENT): returns `{ approved: N, skipped: M, errors: [...] }` — toast shows summary.

---

## Approval History (Invoice Detail)

In invoice detail, the **Activity** tab (spec 62) records approval events:

```
✓  Approved on behalf of customer by Dan Cohen
   "Customer confirmed via phone call"
   Today at 14:32

→  Invoice sent to contact@acme.com
   3 days ago
```

An `invoices.approved_by UUID REFERENCES users(id)` records which staff member approved.

---

## Schema Delta

```sql
ALTER TABLE invoices
  ADD COLUMN IF NOT EXISTS approved_by UUID REFERENCES users(id) ON DELETE SET NULL,
  ADD COLUMN IF NOT EXISTS approval_note TEXT,
  ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
  ADD COLUMN IF NOT EXISTS rejection_notify_customer BOOLEAN NOT NULL DEFAULT false;
```

---

## API

> Request/response keys use camelCase to match the established invoices API resource contract (`mapInvoice`, `InvoiceObject`).

```
GET  /api/invoices/approvals
     → list SENT invoices awaiting approval (paginated)
       Query: search, sort (oldest|newest|amount|customer), page, perPage
       Returns: [{ id, proformaNumber, customerName, total, currency, sentAt }]
       Requires: invoices:write

POST /api/invoices/:id/approve
     → approve invoice (SENT → APPROVED)
       body: { approvedOnBehalfOf?: 'staff' | 'customer', note?: string }
       Requires: invoices:write

POST /api/invoices/:id/reject
     → reject invoice (SENT → REJECTED)
       body: { reason: string, notifyCustomer?: boolean }
       Requires: invoices:write

POST /api/invoices/bulk-approve
     → approve multiple invoices
       body: { ids: string[] }
       Returns: { approved: number, skipped: number, errors: Array<{ id, reason }> }
       Requires: invoices:write
```

---

## Sidebar Navigation

Add "Approvals" sub-item under Invoices in sidebar, with a badge showing count of `SENT` invoices:

```
Invoices
  All invoices
  Approvals  (12)   ← badge
  Drafts & Templates
  Recurring
```

Badge uses `--warning` color token (attention needed, not error).

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Separate `/invoices/approvals` route | Not a filter on `/invoices` | Approval queue is a distinct workflow for staff; a filter is discardable; a named route is bookmarkable and surfaces in nav with a count badge |
| Bulk approve as separate endpoint | Not loop of single-approve calls | Server-side loop avoids network round-trips; partial failure handling is cleaner server-side |
| `approved_by` references `users` | Not `tenant_memberships` | Consistent with all other `_by` audit columns in the codebase (`voided_by`, `created_by`, `resolved_by`); `users(id)` is the canonical FK target |
| Rejection requires reason | Not optional | IL tax law compliance — rejected proformas must have a documented reason for audit trail |
