# M06 — Time & Billing

Phase 3. Time capture, fee arrangements, and invoice generation — VAT/WHT aware
for Nigerian legal billing. Feeds M07's ledger for actual money movement.

## Dependencies
M03 (matters), M02 (clients), M07's `LedgerService` (invoices post through it).

## Database

### `fee_arrangements`
```php
$table->id();
$table->foreignId('matter_id')->constrained()->cascadeOnDelete();
$table->string('type');   // fixed / hourly / retainer / contingency / appearance_based / pro_bono
$table->decimal('fixed_amount', 18, 2)->nullable();
$table->decimal('hourly_rate', 18, 2)->nullable();
$table->decimal('retainer_amount', 18, 2)->nullable();
$table->string('retainer_period')->nullable();   // monthly/quarterly/annual
$table->decimal('contingency_percent', 8, 2)->nullable();
$table->decimal('cap_amount', 18, 2)->nullable();
$table->text('notes')->nullable();
$table->string('status')->default('active');
$table->timestamps();
```
One active arrangement per matter (enforced in the Action).

### `time_entries`
```php
$table->id(); $table->ulid('ulid')->unique();
$table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->foreignId('matter_id')->constrained()->restrictOnDelete();
$table->foreignId('user_id')->constrained()->restrictOnDelete();
$table->date('worked_on');
$table->unsignedInteger('minutes');
$table->text('description');
$table->decimal('rate', 18, 2)->nullable();   // snapshot at entry time
$table->boolean('billable')->default(true);
$table->foreignId('invoice_item_id')->nullable()->constrained()->nullOnDelete();
$table->timestamps();
$table->index(['matter_id', 'billable']);
$table->index(['user_id', 'worked_on']);
```

### `invoices` / `invoice_items`
```php
// invoices
$table->id(); $table->ulid('ulid')->unique();
$table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->string('invoice_number')->unique();    // INV/2026/0001
$table->foreignId('client_id')->constrained()->restrictOnDelete();
$table->foreignId('matter_id')->nullable()->constrained()->nullOnDelete();
$table->date('issue_date'); $table->date('due_date');
$table->decimal('subtotal', 18, 2);
$table->decimal('vat_amount', 18, 2)->default(0);
$table->decimal('wht_amount', 18, 2)->default(0);
$table->decimal('total', 18, 2);
$table->decimal('amount_paid', 18, 2)->default(0);
$table->string('status')->default('draft');  // draft/sent/partially_paid/paid/overdue/void
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
$table->index(['firm_id', 'status', 'due_date']);

// invoice_items
$table->id();
$table->foreignId('invoice_id')->constrained()->cascadeOnDelete();
$table->string('description');
$table->decimal('quantity', 10, 2)->default(1);
$table->decimal('unit_price', 18, 2);
$table->decimal('amount', 18, 2);
$table->string('source_type')->nullable();  // time_entry / expense / fixed_fee / retainer_drawdown
$table->unsignedBigInteger('source_id')->nullable();
$table->timestamps();
```

### `receipts`
```php
$table->id(); $table->ulid('ulid')->unique();
$table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->string('receipt_number')->unique();
$table->foreignId('invoice_id')->constrained()->restrictOnDelete();
$table->foreignId('bank_account_id')->nullable()->constrained()->nullOnDelete();
$table->decimal('amount', 18, 2);
$table->date('received_on'); $table->string('method')->nullable(); // transfer/cash/cheque/pos
$table->text('reference')->nullable();
$table->foreignId('recorded_by')->constrained('users');
$table->timestamps();
```

## Nigerian tax handling

Settings-configurable rates (`SettingsService`): `vat_rate` (default 7.5%),
`wht_rate` (default 5% or 10% per client type — companies vs individuals per
FIRS rules), toggled per-invoice if a client is VAT-exempt. Invoice total
formula: `subtotal + vat_amount - wht_amount = total` (WHT is typically
deducted by the paying client, so it reduces the collectible amount — modeled
as a deduction line, tracked separately for tax credit reconciliation, not
silently dropped).

## Invoice generation flow

`App\Actions\Billing\GenerateInvoiceFromUnbilled::handle(Matter $matter, array $timeEntryIds, array $expenseIds, User $actor)`:
1. Validate all entries belong to the matter and are unbilled.
2. Lock and mark them (prevents double-billing under concurrent invoice runs).
3. Build `invoice_items` (one per entry, or grouped per lawyer — user choice via
   a flag).
4. Compute subtotal/VAT/WHT/total via `SettingsService` rates (overridable per
   invoice).
5. `NumberingService::next('invoice')`.
6. Create invoice `status = draft`; on `send` transition, mark `status = sent`
   and fire `InvoiceSent` (M09 notification + client portal visibility).
7. Fixed-fee/retainer-drawdown items created directly (no time entries) via the
   same Action with a `source_type` override.

Receipt allocation: `AllocateReceipt` Action applies payment to invoice(s)
(oldest-due-first default, override allowed), updates `amount_paid` and
recomputes `status`; posts through `LedgerService` (M07) — never adjusts
`invoices.amount_paid` without a corresponding journal entry.

## Routes

```
RESTful /matters/{matter}/time-entries      (time_entries)
RESTful /invoices                            (index/create/show; store via Action, not raw CRUD)
GET  /matters/{matter}/billing/unbilled      billing.unbilled
POST /matters/{matter}/billing/generate      billing.generate
PATCH /invoices/{invoice}/send               invoices.send
POST /invoices/{invoice}/void                invoices.void  (reason required; only if unpaid)
RESTful /receipts
POST /receipts/{receipt}/allocate            receipts.allocate
RESTful /fee-arrangements                    (nested under matters)
```

## Views

`time-entries` quick-add widget (matter, date, duration as `h:mm`, description)
on the matter Billing tab + a personal "My Time" weekly timesheet page.
`invoices/index`: KPI (draft, sent, overdue, collected this month), filters
(client, matter, status, date range), table with aging indicator. Matter
Billing tab aggregates: fee arrangement, unbilled time total, invoices,
receipts, (Phase 3/M07) retainer balance, outstanding balance — one consolidated
financial view per matter.

## Acceptance criteria

- [ ] Fee arrangement CRUD; only one active per matter
- [ ] Time entries CRUD; unbilled total correct on Billing tab
- [ ] Invoice generation from selected time entries/expenses computes VAT/WHT/
      total correctly (unit tests with known figures)
- [ ] Double-billing of the same time entry is impossible even under concurrent
      requests (locking test)
- [ ] Receipt allocation updates invoice status correctly (partial → paid) and
      posts a matching ledger entry (integration with M07)
- [ ] Void only allowed on unpaid invoices, requires a reason, is logged
- [ ] Aging indicator on invoice list matches the M07 aging report logic exactly

## Claude Code kickoff prompt

> Read CLAUDE.md, docs/03-DATA-MODEL.md, docs/modules/M07-finance.md (for the
> LedgerService interface you'll call), and this file. Implement in order:
> fee_arrangements → time_entries (with the unbilled-locking test first) →
> invoice generation Action (unit test VAT/WHT math with fixed numbers before
> wiring the UI) → receipts + allocation (integration-test against
> LedgerService) → views. Never write to `invoices.amount_paid` except via the
> allocation Action.
