# Bulk Invoice Generation

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

---

## Overview

Spec 55 (`time-to-invoice`) defines converting a single customer's unbilled time to an invoice. For agencies or businesses with many active clients, end-of-month invoicing requires generating invoices for every client at once. This spec adds a billing run wizard at `/invoices/generate` that creates invoices in bulk for a selected period.

---

## Entry Points

- **Invoices list** header: **[Generate invoices]** button (admin only)
- **Route:** `/invoices/generate`

---

## Wizard — 3 Steps

### Step 1: Select Period & Clients

```
┌──────────────────────────────────────────────────────────────┐
│  Generate invoices — Step 1: Select period                   │
│                                                              │
│  Billing period                                              │
│  ● Last month (May 2026)                                     │
│  ○ This month (June 2026)                                    │
│  ○ Custom range: [__________] to [__________]                │
│                                                              │
│  Include                                                     │
│  ☑ Unbilled time entries                                     │
│  ☑ Approved expenses (billable)                              │
│  ☐ Open milestones (fixed-fee)                               │
│                                                              │
│  [Cancel]                         [Next: Preview →]          │
└──────────────────────────────────────────────────────────────┘
```

---

### Step 2: Preview

System queries all billable items in period grouped by customer. Shows preview table:

```
┌──────────────────────────────────────────────────────────────┐
│  Generate invoices — Step 2: Preview                         │
│                                                              │
│  9 invoices will be created for May 2026.                    │
│                                                              │
│  ☑  Customer          Hours    Expenses   Milestones  Total  │
│  ─────────────────────────────────────────────────────────   │
│  ☑  Acme Corp         12.5h    ₪450       —           ₪2,325 │
│  ☑  Beta Ltd          8.0h     —          —           ₪1,200 │
│  ☑  Gama Inc          —        ₪1,200     ₪5,000      ₪6,200 │
│  ☐  Delta Co          3.0h     —          —           ₪450   │
│     (already has unpaid invoice for May — skip recommended)  │
│  ☑  Epsilon Ltd       20.0h    ₪800       —           ₪3,800 │
│  ...                                                         │
│                                                              │
│  Selected: 8 invoices · Total: ₪14,275                        │
│                                                              │
│  [← Back]                      [Generate 8 invoices →]      │
└──────────────────────────────────────────────────────────────┘
```

**Smart pre-deselection:** customers with an existing open/unpaid invoice for the same period are pre-deselected (still selectable by staff with a warning).

**[▾ Expand]** on each row shows the line-item breakdown that will be on the invoice.

---

### Step 3: Generating

```
┌──────────────────────────────────────────────────────────────┐
│  Generating invoices...                                      │
│                                                              │
│  ✓ Acme Corp — INV-0048 created                              │
│  ✓ Beta Ltd — INV-0049 created                               │
│  ✓ Gama Inc — INV-0050 created                               │
│  ⏳ Epsilon Ltd — generating...                               │
│                                                              │
│  5 of 8 done                                                 │
│  [████████████░░░░░░░] 62%                                   │
└──────────────────────────────────────────────────────────────┘
```

Invoices created as `DRAFT` — not yet sent. After generation, redirect to invoices list filtered to `status=DRAFT&created_at>=today` showing the batch.

---

## Large Batches

Batches > 20 customers dispatch to a Cloudflare Queue job (spec `bulk-operations`, spec 42). The job is tracked in the shared `import_jobs` table with `type = 'bulk_action'` (this generation registers `meta.action = 'invoice_generate'`) — no new job table. UI shows "Generating in background" state:

```
│  Generating 24 invoices in the background...                 │
│  You'll be notified when complete.                           │
│  [View progress →]   [Go to invoices →]                      │
```

Notification (spec 97) on completion: "24 invoices generated for May 2026."

### Job-Status Screen (`/invoices/generate/jobs/:jobId`)

**[View progress →]** routes to `/invoices/generate/jobs/:jobId`, a job-status screen that reuses the bulk-action progress pattern (spec 42 `import_jobs` lifecycle: `pending → processing → completed/failed`). It polls `GET /api/invoices/bulk-generate/jobs/:jobId` (and live-updates via the same WebSocket invalidation as spec 42 when the completion notification arrives):

```
┌──────────────────────────────────────────────────────────────┐
│  Billing run — May 2026                          processing  │
│                                                              │
│  [██████████████░░░░░░░░] 18 of 24                           │
│                                                              │
│  ✓ Acme Corp — INV-0048                                      │
│  ✓ Beta Ltd — INV-0049                                       │
│  ⚠ Delta Co — skipped (no billable items)                   │
│  ⏳ Epsilon Ltd — generating…                                │
│  …                                                           │
│                                                              │
│  Created: 17 · Skipped: 1 · Failed: 0                        │
│  [Go to invoices →]  (enabled on completion, filters to batch)│
└──────────────────────────────────────────────────────────────┘
```

On `completed`, the per-customer results (`meta.results`) list each created invoice number, skip, or error; **[Go to invoices →]** filters the invoices list to the generated batch (`status=DRAFT&job_id=:jobId`). The screen is reachable both from the in-progress card and from the completion notification's deep link.

---

## Invoice Content Rules

Each generated invoice follows the same line-item rules as `time-to-invoice` (spec 55):

**Time entries:** grouped by project → task → one line item per group (or per-entry if tenant setting requires).

**Expenses:** one line item per approved expense (description, amount).

**Milestones:** one line item per completed milestone not yet invoiced.

Invoice defaults: `payment_terms_days`, `tax_rate`, `invoice_number_prefix` from `tenant_settings` (spec 125).

---

## API

```
POST /api/invoices/bulk-generate/preview
     → compute preview: what invoices would be generated
       body: { period_start: date, period_end: date,
               include_time: boolean, include_expenses: boolean,
               include_milestones: boolean }
       Returns: { customers: [{ customer_id, customer_name, time_hours, expense_total,
                                milestone_total, invoice_total, has_open_invoice }] }
       Requires: invoices:write, admin

POST /api/invoices/bulk-generate
     → generate invoices for selected customers
       body: { period_start, period_end,
               customer_ids: string[],
               include_time, include_expenses, include_milestones }
       Returns: { job_id?: string, invoices?: [{ id, customer_name, number }] }
       (job_id returned for batches >20; inline results for ≤20)
       Requires: invoices:write, admin

GET /api/invoices/bulk-generate/jobs/:jobId
     → poll status of a background billing run (reads import_jobs, spec 42)
       Returns: { status: 'pending'|'processing'|'completed'|'failed',
                  total, processed, created, skipped, failed,
                  results: [{ customer_name, status: 'created'|'skipped'|'error',
                              invoice_number?, reason? }] }
       Requires: invoices:write, admin (job must belong to caller's tenant)
```

The `/invoices/generate/jobs/:jobId` page route is owned by this spec (133) and must be registered in the route registry.

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Wizard with preview step | Not one-click bulk | Staff need to verify amounts before sending; preview catches data entry errors and skips customers with duplicate invoices |
| Generate as DRAFT | Not send immediately | Bulk-generated invoices need human review before sending; DRAFT allows inspection and deletion |
| Pre-deselect duplicate-period customers | Not block | Staff may legitimately want to generate a second invoice (e.g., project expansion); pre-deselect warns but doesn't prevent |
| Queue for >20 customers | Not synchronous | Large batches may time out; queue ensures every invoice is created even if request times out |
| Line items same as time-to-invoice | Not simplified | Consistent line-item logic prevents confusion; reuses the same service layer |
