# Expense to Invoice Line

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 98  
**Tier:** All tiers  
**Depends on:** `expenses-module`, `invoices-core`, `projects-module`, `foundation-auth-rbac`  
**Referenced by:** `expenses-module`, `invoices-core`

---

## Overview

Approved project expenses can be billed to a customer by adding them as invoice line items. This spec adds: the "Add to invoice" action on approved expenses, the link from invoice lines back to source expenses, and the expense billing status indicator.

---

## Data Model

```sql
-- Schema delta on invoice_lines:
ALTER TABLE invoice_lines ADD COLUMN expense_id UUID REFERENCES expenses(id);
-- NULL for non-expense lines. Set when line was generated from an expense.

-- Schema delta on expenses:
ALTER TABLE expenses ADD COLUMN billed_at TIMESTAMPTZ;
ALTER TABLE expenses ADD COLUMN invoice_id UUID REFERENCES invoices(id);
-- Set when expense is added to an invoice line. NULL = not yet billed.
```

---

## Expense Detail: "Add to Invoice" Action

On expense detail (`/expenses/:id`) when `status = 'COMPLETED'` and `billed_at IS NULL` and `project_id IS NOT NULL` and the target invoice is in `DRAFT` status:

```
┌──────────────────────────────────────────────────────────────┐
│  Expense #EXP-0091              [COMPLETED]                  │
│                                                              │
│  Supersol Deal — Office supplies                             │
│  ₪347.50   2026-05-28   Website redesign                    │
│                                                              │
│  Billable:  ✓  [Add to invoice ▾]                            │
│             ├ Add to existing invoice...                     │
│             └ Create new invoice with this expense           │
└──────────────────────────────────────────────────────────────┘
```

If `billed_at IS NOT NULL`:
```
│  Billable:  ✓  Billed — INV-0042  (2026-05-30)              │
│             [View invoice]                                   │
```

---

## Add to Existing Invoice Flow

Modal: lists DRAFT invoices for the same customer (`status = 'DRAFT'` with same `customer_id`). SENT/TAX_ISSUED invoices are excluded — mutating a sent invoice total is disallowed:

```
┌──────────────────────────────────────────────────────────────┐
│  Add to invoice                                              │
│                                                              │
│  Expense: Office supplies — ₪347.50                          │
│  Project: Website redesign → Customer: Acme Corp            │
│                                                              │
│  Select invoice:                                             │
│  ○ INV-0042  May 2026   ₪18,000   DRAFT                     │
│  ○ INV-0051  Jun 2026   ₪27,000   DRAFT                     │
│                                                              │
│  Line description (editable):                                │
│  [Office supplies — Supersol Deal 2026-05-28___]             │
│                                                              │
│  Amount:  ₪ [347.50]  (pre-filled from expense invoice_total)│
│                                                              │
│  [Cancel]                          [Add to invoice]          │
└──────────────────────────────────────────────────────────────┘
```

On confirm:
1. Insert `invoice_lines` row: `{ invoice_id, description, quantity: 1, unit_price: amount, expense_id }`
2. Update `expenses`: `{ billed_at: now(), invoice_id }`
3. Recalculate invoice total (same logic as manual line add)
4. Audit entry (spec 28 cross-cutting rule in same transaction)

---

## Create New Invoice Flow

Redirects to `/invoices/new` with prefilled state:
- Customer = expense's customer (via project → customer)
- One line item pre-populated from expense
- Expense marked as billed on invoice save

---

## Expenses List: Billing Status Column

`/expenses` list gains "Billing" column:

```
│ Expense          │ Amount   │ Project     │ Status    │ Billing    │
│ Office supplies  │ ₪347     │ Website     │ COMPLETED │ ✓ Billed   │
│ BP Gas Station   │ ₪280     │ API project │ COMPLETED │ Unbilled   │
│ Software license │ ₪500     │ —           │ COMPLETED │ No project │
```

Filter chip: "Unbilled expenses" — shows COMPLETED expenses with `billed_at IS NULL AND project_id IS NOT NULL`.

---

## Invoice Line: Expense Traceability

Invoice detail shows expense source for expense-backed lines:

```
│ Description           │ Qty │ Unit price │ Total   │         │
│ Office supplies       │ 1   │ ₪347.50    │ ₪347.50 │ 📎 EXP  │
│ Web development       │ 40h │ ₪250/h     │ ₪10,000 │         │
```

"📎 EXP" badge links to expense detail. Tooltip: "Added from EXP-0091".

---

## API

```
POST /api/invoices/:id/lines/from-expense
     → add expense as invoice line
       body: { expense_id, description?, amount? }
       Requires: invoices:write + expenses:read

GET /api/expenses?billable=true
    → list billable expenses (COMPLETED, project linked, unbilled)
      Requires: expenses:read
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| `expense_id` on `invoice_lines` | Not a separate mapping table | Single expense → single line; nullable FK is simpler than a join table for this 1:1 relationship |
| `billed_at` + `invoice_id` on expenses | Not inferred from line lookup | Direct lookup "is this expense billed?" without joining invoice_lines; also survives line deletion without losing billing history |
| Amount editable in modal | Not locked to expense total | Some expenses are partially billable or have markup; staff should be able to adjust the billed amount |
| Only project-linked expenses billable | Not all expenses | Non-project expenses (e.g., office rent) aren't customer-billable; requiring project_id is the gate |
| DRAFT-only invoice target | Not SENT/TAX_ISSUED | Mutating a sent invoice total after the customer received it creates discrepancy; TAX_ISSUED invoices are especially frozen (tax authority record) |
