---
description: Double-entry accounting, immutable transactions, invoice lifecycle.
globs: ["app/Domain/Financial/**", "database/migrations/**financial**", "database/migrations/**invoice**", "database/migrations/**payment**", "database/migrations/**wallet**", "database/migrations/**transaction**"]
---

# Financial Integrity

## Double-Entry — ALWAYS in Pairs

Every financial movement creates TWO transaction records (debit + credit).
Transaction amounts are ALWAYS positive. The type field (debit/credit) determines direction.

```php
// The ONLY way to write transactions
Transaction::create(['account_id' => $debit, 'type' => 'debit', 'amount' => $amount]);
Transaction::create(['account_id' => $credit, 'type' => 'credit', 'amount' => $amount]);
```

## Transactions Are IMMUTABLE

- No `updated_at` on transactions table
- No `deleted_at` — never soft-delete
- Corrections create NEW reversing entries — never modify existing

## Invoice State Machine (ONLY valid transitions)

```
draft → sent → partially_paid → paid
draft → cancelled
sent → cancelled (ONLY if paid_amount == 0)
sent → overdue (auto, when due_date passes)
overdue → partially_paid → paid
```

INVALID: `partially_paid → cancelled`, `paid → cancelled`

## Invoice Calculation Order

```
subtotal = sum(item.line_total)
discount_amount = applied discount
total_amount = subtotal - discount_amount + tax_amount
balance_due = total_amount - paid_amount
```

Total is FROZEN at creation. Never recalculate from items after invoice is sent.

## Payment Rules

- Amount MUST be > 0 AND <= invoice.balance_due
- Cannot pay cancelled or draft invoice
- After payment: update invoice.paid_amount, recalculate balance_due
- If balance_due == 0: invoice status = 'paid'
- Double-entry: Debit Cash/Card/Wallet, Credit Accounts Receivable

## Wallet Rules

- Balance NEVER goes negative (check BEFORE deduct)
- Always `lockForUpdate()` before modifying balance
- Every change = wallet_transactions record
- available_balance = balance - frozen_amount

## Refund Rules

- Refund amount <= payment.amount - payment.refunded_amount
- Creates reversing double-entry transactions
- Updates invoice.paid_amount and balance_due
- Refunds above threshold require approval
