# Expense Approval Workflow

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 65  
**Tier:** Business+  
**Depends on:** `expenses-module`, `expense-settings-ui`, `system-communications-notifications`, `foundation-auth-rbac`  
**Referenced by:** `expenses-module`

---

## Overview

Multi-step approval gate for expenses above a configurable threshold. When enabled (spec 61 `expense_approval_threshold_ils`), expenses above the threshold enter a `pending_approval` state before being included in reports. Approver (OWNER or designated ADMIN) approves or rejects from a dedicated queue.

Tier: Business+. Freelancer tenants with single-user setup gain nothing from approval; the UI gate and threshold config (spec 61) are shown only for Business+.

---

## Data Model Changes

Two new columns on `expenses`:

```sql
ALTER TABLE expenses ADD COLUMN approval_status TEXT DEFAULT 'not_required';
  -- 'not_required' | 'pending' | 'approved' | 'rejected'
ALTER TABLE expenses ADD COLUMN approved_by UUID REFERENCES users(id);
ALTER TABLE expenses ADD COLUMN approved_at TIMESTAMPTZ;
ALTER TABLE expenses ADD COLUMN approval_note TEXT;    -- rejection reason
```

Expense lifecycle when threshold configured:
- Amount ≤ threshold: `approval_status = 'not_required'`
- Amount > threshold: `approval_status = 'pending'` on creation; OCR evaluation proceeds normally; included in reports only when `approved`

---

## Approval Queue: `/expenses/approvals`

Sub-route within the time module (or expenses module). Accessible to OWNER + configured approver.

```
┌────────────────────────────────────────────────────────────┐
│  Expenses · Pending Approval           [4 pending]         │
│                                                            │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  Date       Vendor         Amount   Submitted by      │  │
│  │  ──────────────────────────────────────────────────  │  │
│  │  2026-05-28  Office Depot  ₪2,400   Alex Katz         │  │
│  │  2026-05-27  AWS           ₪3,100   Dana Levi  ▲new   │  │
│  │  2026-05-25  Consultant X  ₪5,000   Alex Katz         │  │
│  │  2026-05-24  Travel        ₪1,800   Oren (contractor) │  │
│  └──────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────┘
```

Row click → opens approval drawer.

---

## Approval Drawer

```
┌──────────────────────────────────────────────────────────────┐
│  Expense: Office Depot                        ₪2,400         │
│                                                              │
│  Submitted by:  Alex Katz  ·  2026-05-28                    │
│  Category:      Office supplies (100% deductible)           │
│  Project:       Website Redesign                            │
│  AI confidence: 92% (auto-categorized)                      │
│                                                              │
│  [View receipt image]                                        │
│                                                              │
│  Notes (optional):                                          │
│  [_____________________________________________]             │
│                                                              │
│  [Reject]                          [Approve]                 │
└──────────────────────────────────────────────────────────────┘
```

"Approve" → `POST /api/expenses/:id/approve` → `approval_status = 'approved'`, toast + next item auto-loads.  
"Reject" → requires note (non-empty) → `POST /api/expenses/:id/reject` with `{ reason }` → `approval_status = 'rejected'`.

---

## Notifications

| Trigger | Recipient | Channel |
|---------|-----------|---------|
| Expense submitted (above threshold) | Approver | In-app + email: "Expense pending approval: ₪{amount} from {name}" |
| Expense approved | Submitter | In-app: "Your expense of ₪{amount} was approved" |
| Expense rejected | Submitter | In-app + email: "Your expense of ₪{amount} was rejected: {reason}" |

---

## Report Exclusion

Expense reports (spec 57) exclude `approval_status = 'rejected'` rows. `pending` rows are included but marked with a ⏳ indicator.  
PCN874 VAT report (spec 57 Tab 2) includes only `approved` + `not_required` expenses — pending/rejected excluded from tax reports.

---

## API Endpoints

```
GET  /api/expenses/approvals
     → list pending expenses for current approver
       query: status=pending|approved|rejected (default: pending)
       returns: paginated expense list with approval fields

POST /api/expenses/:id/approve
     → approve expense
       body: { note? }
       Sets: approval_status='approved', approved_by, approved_at
       Requires: expense approval role (OWNER or configured approver)

POST /api/expenses/:id/reject
     → reject expense
       body: { reason: string (required) }
       Sets: approval_status='rejected', approval_note
       Notifies: submitter
```

Both require OWNER or `expense_approver_role` from tenant settings (spec 61).

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Inline columns on `expenses` | Not a separate `expense_approvals` table | One-to-one relationship (each expense has one approval decision); extra table adds join with no benefit |
| `not_required` default | Not `NULL` | NULL is ambiguous — could mean "never configured" or "not yet decided". Explicit `not_required` makes queries predictable without a COALESCE |
| Business+ gate | Not all tiers | Single-user freelancer accounts have no approval hierarchy; adding the UI complexity adds no value and would confuse the UX |
| Rejection requires reason | Optional for approval | Rejected expenses need context for the submitter; approved ones rarely need notes; asymmetric requirement |
