# Billing Flow: Clients, Invoices, Operations & Reports

## Client Creation

**Controller:** `app/Http/Controllers/ClientController.php` — `store()`

A client represents a tenant in the system. All invoices, expenses, operations, and users are scoped to a `client_id`.

**Creation steps:**
1. Basic fields are validated: name, contact name, email, password, phone, package
2. If `showInvoiceInformation === '1'`, fiscal/SAT fields are also validated: `tipo_persona`, `razon_social`, `cp_fiscal` (5 digits), `rfc` (≤15 chars), `regimen_id`, `uso_id`
3. A `Client` record is created. Status is auto-set:
   - **Prueba** — if the assigned package has a `trial_period > 0`
   - **Activo** — otherwise
4. A default `User` is created and linked to the client (hashed password, two-factor setup, role from `package.default_role_id`)
5. A default `quote_terms` template is set
6. Email verification is sent to the new user

**Key client fields:**
| Field | Purpose |
|-------|---------|
| `package_id` | Subscription package, controls features and roles |
| `rfc`, `razon_social`, `cp_fiscal` | Mexican tax identity |
| `regimen_id`, `uso_id` | SAT catalog references |
| `file_cer`, `file_key`, `passcode` | Mexican digital signature (e.firma) for CFDI stamping |
| `factura_uid`, `empresa_uid` | Factura.com entity IDs |
| `empresa_api_key`, `empresa_secret_key` | Per-client Factura.com credentials (falls back to `.env` globals) |
| `status` | Prueba / Activo / Inactivo / Cancelado |
| `trial_ends_at`, `subscription_ends_at` | Subscription lifecycle dates |

---

## Invoice Generation

**Model:** `app/Models/Invoice.php`
**Controller:** `app/Http/Controllers/InvoiceController.php`

### Step 1 — Create draft (`store()`)

Saves the invoice header and its line items (`Concept` records):

- **Invoice header**: client, subclient, contact, project, optional linked quote; SAT fields (`uso_cfdi`, `forma_pago`, `metodo_pago`, `moneda`, `tipo_cambio`); financial totals (`subtotal`, `iva_amount`, retention amounts, `total`); `invoice_date`, `payment_date`
- **Concepts** (`app/Models/Concept.php`): one row per line item — `clave_prod_serv` (SAT product/service code), `clave_unidad`, `cantidad`, `valor_unitario`, `importe`, `descuento`, plus related tax records

Initial status: `Borrador`

### Step 2 — Stamp CFDI (`generate($id, $draft)`)

Calls `FacturaService::facturaCreateInvoice(Invoice $invoice, $draft)`:
- Uses `client.empresa_api_key` / `empresa_secret_key` if set, otherwise falls back to global `.env` credentials
- `isTestPackage()` determines whether to hit the Factura.com sandbox or production API
- On success: stores `uuid`, `serie_folio` on the invoice; sets status → `Facturada`
- On failure: sets status → `Error`

**Service:** `app/Services/FacturaService.php`

### Invoice statuses

```
Borrador → Facturada → Enviada ─┬→ Parciales → Pagada
                                 └→ Pagada
                    (any state) → Cancelada | Error
```

### Status recalculation (`Invoice::updateStatus()`)

Called automatically whenever an operation or credit note is created/updated:

```
effective_total = invoice.total × invoice.tipo_cambio
               − SUM(nc.total × nc.tipo_cambio)         ← credit notes applied

total_paid      = SUM(operations.amount × operations.exchange_rate)
                  WHERE operations.invoice_id = invoice.id

balance = effective_total − total_paid

if balance == effective_total  → Facturada
if balance == 0                → Pagada
if 0 < balance < effective_total → Parciales
```

Credit notes are invoices where `tipo_documento = 'nota_credito'` and `ref_invoice_id = invoice.id`.

---

## Operations (Payments Toward Invoices and Expenses)

**Model:** `app/Models/Operation.php`
**Controller:** `app/Http/Controllers/OperationController.php`

> The `Payment` model (`app/Models/Payment.php`) is for **Stripe subscription billing only** and is not used for invoice payments.

Invoice and expense payments are recorded as `Operation` records.

**Key operation fields:**
| Field | Purpose |
|-------|---------|
| `account_id` | Bank account the money moved through |
| `invoice_id` | Set when paying an invoice |
| `expense_id` | Set when paying an expense |
| `transfer_id` | Set for inter-account transfers |
| `type` | `Ingreso` (income) or `Egreso` (outgoing) |
| `amount` | Amount in the operation's currency |
| `exchange_rate` | Conversion rate to base currency |
| `operation_date` | Date of the transaction |

**Paying an invoice:**
`InvoiceController::storePayment()` creates an `Operation` with `invoice_id` set, then calls `Invoice::updateStatus()`.

**Paying an expense:**
`ExpenseController::storePayment()` creates an `Operation` with `expense_id` set, then calls `Expense::updateStatus()`.

In both cases, `updateStatus()` recalculates the balance using the operations linked to that document.

---

## Receivables Report (Accounts Receivable)

**Controller:** `app/Http/Controllers/ReceivableReportController.php`
**Permission gate:** `reporte de cuentas por cobrar`

Reports on unpaid invoices — money owed **to** the client by their customers (subclients).

### Source data

- Table: `invoices` joined with `subclients`, `operations` (left), and `invoices as nc` (credit notes, left)
- Filter: `tipo_documento = 'factura'` AND `status IN ('Facturada', 'Enviada', 'Parciales')`
- Scoped to `currentClientId()`

### Balance formula per invoice

```sql
balance = (invoices.total * IFNULL(invoices.tipo_cambio, 1))
        - IFNULL(SUM(operations.amount * IFNULL(operations.exchange_rate, 1)), 0)
        - IFNULL(SUM(nc.total * IFNULL(nc.tipo_cambio, 1)), 0)
```

### Payment status

```
invoices.payment_date < NOW() AND balance > 0  →  'Vencido' (past due)
otherwise                                       →  'Al corriente' (current)
```

`days_past_due` = `DATEDIFF(NOW(), invoices.payment_date)` when positive.

### Aging buckets

The report breaks past-due balances into three forward-looking buckets relative to `today`:

| Bucket | Condition on `payment_date` |
|--------|----------------------------|
| 0–30 days | `≤ today + 30` |
| 31–60 days | `> today + 30` AND `≤ today + 60` |
| 60+ days | `> today + 60` |

Each bucket returns a total amount and a percentage of total outstanding.

### Filters

| Filter | Field |
|--------|-------|
| Date range | `invoice_date`, `payment_date`, or `updated_at` (via `date_range_field`) |
| Customer | `subclient_id` |
| Payment status | `'Vencido'` or `'Al corriente'` (applied via `HAVING`) |

### Totals returned

- `due` — total outstanding across all rows
- `past_due` — total for `'Vencido'` rows
- `total_pct_past_due` — percentage past due
- `total_past_due_1/2/3` and `pct_past_due_1/2/3` — aging bucket breakdowns

### Search

Applied via `HAVING` clauses on computed columns (required because totals are aggregates): searches `serie_folio`, `subclient_number`, `subclient_name`, `invoice_date`, `payment_date`, `payment_status`, `total_formatted`, `balance_formatted`, and numeric matches on `total`, `balance`, `days_past_due`.

---

## Payables Report (Accounts Payable)

**Controller:** `app/Http/Controllers/PayableReportController.php`

Reports on unpaid expenses — money owed **by** the client to their providers.

### Source data

- Table: `expenses` joined with `providers` and `operations` (left, on `expense_id`)
- Filter: `expenses.status != 'Pagado'`
- Scoped to `currentClientId()`

### Balance formula per expense

```sql
amount_in_base = expenses.amount * IFNULL(expenses.exchange_rate, 1)
total_paid     = SUM(operations.amount * IFNULL(operations.exchange_rate, 1))
balance        = amount_in_base - total_paid
```

### Payment status

```
expenses.payment_date < NOW() AND balance > 0  →  'Vencido'
otherwise                                       →  'Al corriente'
```

### Filters

Date range (on `updated_at`, `created_at`, or `payment_date`), `provider_id`, `payment_status`.

### Totals returned

- `total_due` — total outstanding
- `total_past_due` — total for `'Vencido'`
- `pct_past_due` — percentage past due

No aging buckets (unlike receivables).

---

## Receivables vs. Payables — Comparison

| | Receivables | Payables |
|---|---|---|
| Source table | `invoices` | `expenses` |
| Counterparty | `subclients` | `providers` |
| Payment link | `operations.invoice_id` | `operations.expense_id` |
| Credit offsets | Credit notes (`ref_invoice_id`) | None |
| Currency field | `tipo_cambio` | `exchange_rate` |
| Aging buckets | 3 buckets (0–30, 31–60, 60+) | None |
| Excluded records | Not in `['Facturada','Enviada','Parciales']` | `status = 'Pagado'` |
| Status update trigger | `Invoice::updateStatus()` | `Expense::updateStatus()` |

---

## Key Relationships

```
Client
 ├── has many Invoices (client_id)
 │    ├── has many Concepts          (invoice_id)  ← line items
 │    ├── has many Operations        (invoice_id)  ← payments received
 │    └── has many credit-note Invoices (ref_invoice_id)
 ├── has many Expenses (client_id)
 │    └── has many Operations        (expense_id)  ← payments made
 ├── has many Operations (client_id)
 │    ├── belongs to Account
 │    ├── belongs to Invoice
 │    ├── belongs to Expense
 │    └── belongs to Transfer
 └── has many Users (client_id)
```
