# Operational Data Audit Trail

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 83  
**Tier:** Business+ (capture), All tiers (view own changes)  
**Depends on:** `audit-compliance`, `tenant-audit-log`, `foundation-auth-rbac`  
**Referenced by:** `audit-compliance`, `tenant-audit-log`, `invoices-core`, `customers-module`

---

## Overview

Field-level change tracking on operational data: invoices, customers, projects, contracts, expenses, time entries. Spec 28 (audit-compliance) covers immutable admin-level audit log; spec 50 covers the tenant-facing UI for that log. Neither specifies who changed what field on an invoice at 3pm. This spec adds a `before`/`after` diff trail for all writes to core entities.

---

## Scope

Entities tracked:

| Entity table | Tracked events |
|---|---|
| `invoices` | status change, line item edit, field update |
| `invoice_lines` | create, update, delete |
| `customers` | field update, status change (archive) |
| `contacts` | create, update, delete |
| `projects` | field update, status change |
| `contracts` | status change |
| `expenses` | amount/category/date update, status change |
| `time_entries` | duration change, project/task reassign, billable toggle |

---

## Storage

Change events are appended to `tenant_audit_log` (spec 50) with the addition of `before_state` and `after_state` JSONB:

```sql
-- Schema delta on tenant_audit_log:
ALTER TABLE tenant_audit_log ADD COLUMN before_state JSONB;
ALTER TABLE tenant_audit_log ADD COLUMN after_state  JSONB;
-- both NULL for create events; after_state NULL for delete events
```

`event_type` values follow the pattern `{entity}.{action}`:
- `invoice.status_changed`, `invoice.field_updated`, `invoice_line.created`, etc.

`before_state` / `after_state` contain only the changed fields (diff, not full row snapshot):
```json
{ "status": "APPROVED", "updated_at": "2026-05-28T10:22:00Z" }
```

---

## Write Middleware

All affected route handlers include the diff capture in the same audit transaction already required by spec 28. Pattern:

```ts
// Before update:
const before = await tx.select().from(invoices).where(eq(invoices.id, id)).limit(1)

// Perform update:
const after = await tx.update(invoices).set(changes).where(eq(invoices.id, id)).returning()

// Compute diff:
const changedFields = Object.keys(changes).reduce((acc, k) => ({
  ...acc,
  ...(before[0][k] !== after[0][k] ? { [k]: before[0][k] } : {})
}), {})

// Audit entry (same tx — spec 28 requirement):
await tx.insert(tenant_audit_log).values({
  tenant_id: tenantId,
  actor_id:  actorId,
  event_type: 'invoice.field_updated',
  entity_type: 'invoice',
  entity_id: id,
  before_state: changedFields,
  after_state: changes,
})
```

---

## UI: Change History in Entity Detail Views

Each tracked entity detail view (invoice, customer, project) gains a "History" tab alongside the activity timeline:

```
┌──────────────────────────────────────────────────────────────┐
│  Invoice #INV-0042                                           │
│  [Details]  [Activity]  [Payments]  [History]               │
│                                                              │
│  ── Field Changes ─────────────────────────────────────── │
│                                                              │
│  2026-05-28 10:22  Alex Katz                                 │
│  status: APPROVED → TAX_ISSUED                               │
│                                                              │
│  2026-05-27 15:44  Dana Levi                                 │
│  due_date: 2026-06-01 → 2026-06-15                          │
│                                                              │
│  2026-05-25 09:11  Alex Katz                                 │
│  [created]                                                   │
└──────────────────────────────────────────────────────────────┘
```

History tab is available to OWNER, ADMIN. MEMBER sees only their own changes.

---

## Tier Gate

- **All tiers**: capture writes to `tenant_audit_log` with before/after (always on — compliance requires it)
- **All tiers**: entity History tab visible (own changes only for MEMBER)
- **Business+**: full History visible to OWNER/ADMIN; 1-year retention
- **Freelancer**: History tab shows last 90 days

---

## API

```
GET /api/{entity}/{id}/history
    → field change log for this entity
      entity: invoices | customers | projects | expenses | contracts
      query: from?, to?, cursor, limit=50
      Requires: OWNER, ADMIN (or MEMBER for own changes)
      returns: { changes: [{ eventType, actorId, actorName, changedAt, before, after }] }
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Extend `tenant_audit_log` | Not new `entity_change_log` table | Audit events already flow through the audit queue (spec 50); before/after is additive, not a new pipeline |
| Diff only (not full snapshot) | Not full row snapshot per change | Full snapshots bloat storage for wide tables (invoices have 30+ columns); diff is what users need for "who changed what" |
| Same-transaction write | Not post-commit event | Spec 28 cross-cutting rule; audit must never be out of sync with the operation |
| All tiers capture | Business+ display | IL law may require change history for invoices; capture is cheap; display gated is acceptable |
