# Test Suite Documentation

This document explains what the feature tests cover and why they exist. It is intended for developers new to the project.

---

## Overview

All tests are written with **Pest v4** (functional syntax: `it()`, `describe()`, `beforeEach()`). Each test file ends with `uses(Tests\TestCase::class, RefreshDatabase::class)` which resets the database between runs to keep tests fully isolated.

Tests live in `Modules/{ModuleName}/Tests/Feature/`. Run the full suite with:

```bash
art test
```

Run a single file:

```bash
art test --filter=CreateDocumentTest
```

---

## Test Infrastructure — `beforeEach`

Because this is a multi-tenant, relational system with many mandatory foreign keys, most test files build a complete "minimum viable world" in their `beforeEach` block before any test runs. The standard stack looks like:

```
Province → District → Municipality → Town
BankAccount → Franchise
Contact → User (franchise employee)
user.franchises (pivot)
actingAs($user) + TenantContext::bind($user)   ← MUST come before any OwnedByTenantScope query
Business → ClientContact → ClientUser → business.clients (pivot)
Customer (polymorphic link to Business)
```

> **Why this order matters:** `OwnedByTenantScope` is applied as a global query scope on `Document` (and a few other models). It reads from `TenantContext` on every query — including inserts. If `TenantContext` is not bound before you touch those models, you will get a null-pointer error. Bind it first, always.

---

## BookKeeping Module

### `CreateDocumentTest.php`

**Purpose:** Service-level tests for `DocumentService::create()`. These are the most important tests in the project because `DocumentService` is the central piece of business logic — it creates documents, calculates all financial totals, builds document lines, and handles billing/issuer info.

| Describe block | What it checks |
|---|---|
| `quote creation` | A quote is persisted, gets `DRAFT` status, a `Q-` number prefix, a URL key, and starts with `balance = 0` |
| `invoice creation` | An invoice gets `INVOICE` type and a non-`Q-` number prefix |
| `financial calculations` | All six financial totals (`subtotal`, `discount_total`, `tax_total`, `royalty_total`, `profit_total`, `grand_total`) are calculated correctly from `pro_fee`, `misc_fee`, and `discount`. Tests with one item, with excluded child lines, and with multiple items |
| `document lines` | Parent + child lines are created; child is linked to parent via `parent_line_id`; parent group is set to `'Parent'` |
| `document line profits` | Profit records are only created for parent lines (not child lines); correct `pro_fee`, `misc_fee`, and `discount` values are stored |
| `billing info` | `bill_to_*` fields are populated from the customer; `issuer_*` fields come from the franchise; `user_*` fields come from the logged-in user |
| `document numbering` | Each creation increments `franchise.next_id`; sequential documents get unique numbers and unique URL keys |

**Key formulas tested:**
```
subtotal        = pro_fee + misc_fee
base            = pro_fee - discount
tax (15%)       = base × 0.15
royalty (30%)   = base × 0.30
profit (70%)    = base × 0.70
application_fee = sum of INCLUDED child line prices
grand_total     = subtotal + tax + application_fee - discount
balance         = 0 (quote) or grand_total (invoice)
```

---

### `DocumentPageTest.php`

**Purpose:** Frontend (Filament/Livewire) tests for every page in the Documents resource. These tests verify that the UI renders correctly, displays the right data, and exposes the correct fields and actions. They do not test deep form submission logic (that lives in `CreateDocumentTest`).

**Setup note:** Before any Livewire component can render, the Filament panel must be booted:
```php
Filament::setCurrentPanel(Filament::getPanel('myFranchise'));
```
This is called in `beforeEach` after `actingAs()`.

| Describe block | What it checks |
|---|---|
| `list quotes page` | Page renders; shows "Quotes" title; shows quote records in the table; does NOT show invoice records; shows the "Create Quote" header button |
| `list invoices page` | Page renders; shows "Invoices" title; shows invoice records; does NOT show quote records |
| `create quote page` | Page renders; key form fields exist (`customer_id_select`, `password`, `number`, `status`, `date`); defaults `document_type` to `QUOTE`, `status` to `DRAFT`, and `franchise_id` from the tenant context |
| `view document page` | Renders for both quotes and invoices; displays the document number; shows `edit` and `send_document` page-level actions |
| `edit document page` | Renders for both types; pre-fills `customer_id`, `franchise_id`, and `document_type` from the existing record; shows a `view` page-level action |
| `table columns` | All expected columns exist on the table: `status`, `number`, `date`, `grand_total`, `royalty_total`, `profit_total`, `bill_to_trading_as`, `url_key`, `document_type` |
| `table columns (hidden)` | The "Has Been Invoiced" (`document_type`) column is hidden on the invoices list (it only makes sense on the quotes list) |
| `table row actions` | Each row has: `send_document`, `edit`, `preview`, and `download` actions |
| `empty state` | Quotes and invoices lists both render successfully with zero records; record counts are filtered correctly per type (a quote does not count toward the invoices list and vice versa) |
| `view document infolist fields` | The infolist schema contains the `number`, `bill_to_trading_as`, `bill_to_reg_number`, and `is_read_only` fields; the document number value is visible in the rendered HTML |
| `create quote form submission` | Submitting the create form without a customer produces a validation error on `customer_id_select` |

---

## Business Module

### `CreateBusinessTest.php`

**Purpose:** Service-level tests for `BusinessService::create()` and `BusinessService::update()`. Covers the two different creation paths — creating with a **new client** vs attaching an **existing client** — and the ownership/consent logic that differs between them.

| Describe block | What it checks |
|---|---|
| `creating a business with a new client` | Business, contact, and `CLIENT` user are all persisted; a `ClientProfile` is created with `FLAG_PROSPECTIVE`; a polymorphic `Customer` record is created; the client is attached to the business via the `business_clients` pivot; a `BusinessOwnership` record with `ACTIVE` status and `CREATED_BY_USER` consent method is created; the consenting admin is recorded; `jurisdiction_acknowledged` is stored; trading name falls back to contact name when not supplied |
| `creating a business with an existing client` | An existing user is reused without creating a new `Contact`; business and customer are still persisted; ownership gets `PENDING` status and `null` consent method (client consent is still required) |
| `updating a business` | Fields are updated and persisted; other businesses are not affected |

**Key concept — ownership status:**
- **New client** → `ACTIVE` (the franchisee created the business on behalf of the client, so consent is implied)
- **Existing client** → `PENDING` (the client must consent to the franchisee's ownership of their business)

---

<!-- ### `CreateMandateTest.php` (will be reworked)

**Purpose:** Service-level tests for `MandateService::create(Document)`. A mandate is a legal document that accompanies a document and formalises the relationship between a franchisee and a client. It is created from an existing document.

| Describe block | What it checks |
|---|---|
| `mandate creation` | Mandate is persisted and linked to the source document and customer; inherits the document's `url_key`; starts with `DRAFT` status and `DEFAULT` type; `province_id` and `town_id` are copied from the business; financial fields (`remuneration_excl_vat`, `vat_amount`) start at zero; `tax_rate` is read from global settings; `signed_at_place` is null; `document` and `customer` relationships resolve correctly |

---

## Item Module (Will be reworked)

### `CreateItemTest.php`

**Purpose:** Service-level tests for `ItemService::create()` and `ItemService::update()`. Items are the product/service catalog entries that are used when building document line items.

| Describe block | What it checks |
|---|---|
| `item creation` | Item is persisted; associated item lines are created; correct count of lines when multiple are supplied; an item with zero lines works; `type` is cast to `LicenseApplicationType` enum; `meta` array is stored and retrieved correctly |
| `item update` | Fields are updated in place; new lines can be added during update; lines absent from the updated payload are deleted; existing lines are updated in place by primary key (no duplicate created); when only a subset of lines is passed back, the rest are deleted |

--- -->

## User Module

### `CreateUserTest.php`

**Purpose:** Service-level tests for `UserService`. Covers creating both franchise employees and client users, verifying that the correct user type, profile, and franchise/business relationships are established.

> See the file at [Modules/User/Tests/Feature/CreateUserTest.php](Modules/User/Tests/Feature/CreateUserTest.php) for the full list of describe blocks and individual tests.

---

## Common Gotchas

These are things that tripped up the tests at some point and are worth remembering:

| Issue | Explanation |
|---|---|
| `TenantContext` must be bound before `Business::create()` | `Business` previously had an `OwnedByTenantScope` and still triggers related scope logic on some queries. Bind `TenantContext` immediately after `actingAs()`. |
| `Business` has no `user_id` or `franchise_id` | Those columns live on `business_ownerships`, not `businesses`. Never pass them to `Business::create()`. |
| `Document` uses `url_key` as its route key | `Document::getRouteKeyName()` returns `url_key`, so pass `$document->getRouteKey()` (not the primary key) when mounting the `ViewDocument` or `EditDocument` Livewire component. |
| `APP_KEY` must be set in `phpunit.xml` | Filament uses Laravel's encryption for password fields. Without `APP_KEY`, Livewire components that include a password field throw a `ViewException`. |
| `Filament::setCurrentPanel()` must be called before any Livewire test | Filament components rely on the panel being booted. Call this in `beforeEach`, after `actingAs()`. |
| Table row action names | `PreviewDocumentAction` registers as `'preview'`, `DownloadDocumentAction` as `'download'`. Use `assertTableActionExists('preview', null, $record)` — not `'preview_document'`. |
| Infolist null-safety on parent reference | `DocumentInfolist` shows a "Parent Reference" section that is marked `visible(false)` for new documents. The Filament test helper `assertSchemaComponentExists(withHidden: true)` still evaluates closures inside hidden components, so the section's heading closure must use null-safe operators: `$record->parent?->document_type?->value ?? 'Parent'`. |
