# Feature: Customers

## Purpose

Team-scoped customer CRM: residential and commercial customers with primary contact details, secondary contacts, billing addresses, and multiple service sites (geo coordinates, bedrooms/bathrooms, site access notes).

## Boundaries

- **Owns:** `customers`, `customer_contacts`, `customer_billing_addresses`, `customer_service_sites`, `customer_portal_users`; Customers domain Actions, Policies, HTTP (web + API), Inertia pages under `Pages/Customers/`
- **Does not own:** scheduling, jobs, invoices, map UI (Jobs soft-links `customer_id` / `customer_service_site_id` without importing this domain)
- **Depends on (platform only):** User / Team / auth (Jetstream abilities `create` / `read` / `update` / `delete`); portal accounts use Jetstream `customer` role via `CreateTeamMemberWithPassword`
- **Depends on (external):** [Order Market Areas API](https://ordermarket.com/wiki/areas) for service-site postcode → latitude/longitude
- **Depends on (other domains):** none
- **Platform UI touchpoint:** side nav link in `resources/js/Layouts/AppLayout.vue` (hidden for portal `customer` role; revert on remove)

## Models

| Model | Table | Notes |
|-------|-------|-------|
| `Customer` | `customers` | `team_id` FK → `teams`; `type` `residential` \| `commercial`; primary `name`, `email`, `phone`, `notes` |
| `CustomerContact` | `customer_contacts` | Secondary contacts; cascade on customer delete |
| `CustomerBillingAddress` | `customer_billing_addresses` | Address lines only; cascade on customer delete |
| `CustomerServiceSite` | `customer_service_sites` | Address lines + `latitude`/`longitude` (from Order Market postcode lookup), `bedrooms`, `bathrooms`, `site_access_notes`; cascade on customer delete |
| `CustomerPortalUser` | `customer_portal_users` | Soft link `customer_id` ↔ Jetstream `user_id` (unique each); cascade on customer/user delete |

Schema is normalised (3NF). Service-only attributes live on `customer_service_sites`, not billing addresses. Migration: `database/migrations/2026_08_08_100000_create_customers_tables.php` (+ portal table in `2026_08_12_140000_create_company_settings_and_portal_tables.php`). Factories: `database/factories/Domains/Customers/`.

### Customer portal account

Owners create a Jetstream user with role `customer` from the customer show page (`POST /customers/{customer}/portal-account`). One-time password is flashed like staff create. Portal users cannot access the CRM customers list (policy).

### Postcode geocoding

Service site coordinates are resolved from `ORDERMARKET_AREAS_URL` (`GET /postcodes/{slug}`) when a `postal_code` is saved. Manual latitude/longitude input is not accepted. Preview lookup: `POST /customers/lookup-postcode`.

## Routes

### Web

| Method | URI | Name | Controller |
|--------|-----|------|------------|
| GET | `/customers` | `customers.index` | Web\CustomerController@index |
| GET | `/customers/create` | `customers.create` | Web\CustomerController@create |
| POST | `/customers` | `customers.store` | Web\CustomerController@store |
| GET | `/customers/{customer}` | `customers.show` | Web\CustomerController@show |
| GET | `/customers/{customer}/edit` | `customers.edit` | Web\CustomerController@edit |
| PUT/PATCH | `/customers/{customer}` | `customers.update` | Web\CustomerController@update |
| DELETE | `/customers/{customer}` | `customers.destroy` | Web\CustomerController@destroy |
| POST | `/customers/lookup-postcode` | `customers.lookup-postcode` | Web\PostcodeLookupController |
| POST | `/customers/{customer}/portal-account` | `customers.portal-account.store` | Web\CustomerPortalAccountController@store |
| POST | `/customers/{customer}/contacts` | `customers.contacts.store` | Web\CustomerContactController@store |
| PUT | `/customers/{customer}/contacts/{contact}` | `customers.contacts.update` | Web\CustomerContactController@update |
| DELETE | `/customers/{customer}/contacts/{contact}` | `customers.contacts.destroy` | Web\CustomerContactController@destroy |
| POST | `/customers/{customer}/billing-addresses` | `customers.billing-addresses.store` | Web\CustomerBillingAddressController@store |
| PUT | `/customers/{customer}/billing-addresses/{billingAddress}` | `customers.billing-addresses.update` | Web\CustomerBillingAddressController@update |
| DELETE | `/customers/{customer}/billing-addresses/{billingAddress}` | `customers.billing-addresses.destroy` | Web\CustomerBillingAddressController@destroy |
| POST | `/customers/{customer}/service-sites` | `customers.service-sites.store` | Web\CustomerServiceSiteController@store |
| PUT | `/customers/{customer}/service-sites/{serviceSite}` | `customers.service-sites.update` | Web\CustomerServiceSiteController@update |
| DELETE | `/customers/{customer}/service-sites/{serviceSite}` | `customers.service-sites.destroy` | Web\CustomerServiceSiteController@destroy |

### API

| Method | URI | Name | Controller |
|--------|-----|------|------------|
| GET | `/api/customers` | `api.customers.index` | Api\CustomerController@index |
| POST | `/api/customers` | `api.customers.store` | Api\CustomerController@store |
| GET | `/api/customers/{customer}` | `api.customers.show` | Api\CustomerController@show |
| PUT/PATCH | `/api/customers/{customer}` | `api.customers.update` | Api\CustomerController@update |
| DELETE | `/api/customers/{customer}` | `api.customers.destroy` | Api\CustomerController@destroy |
| POST | `/api/customers/{customer}/contacts` | `api.customers.contacts.store` | Api\CustomerContactController@store |
| PUT | `/api/customers/{customer}/contacts/{contact}` | `api.customers.contacts.update` | Api\CustomerContactController@update |
| DELETE | `/api/customers/{customer}/contacts/{contact}` | `api.customers.contacts.destroy` | Api\CustomerContactController@destroy |
| POST | `/api/customers/{customer}/billing-addresses` | `api.customers.billing-addresses.store` | Api\CustomerBillingAddressController@store |
| PUT | `/api/customers/{customer}/billing-addresses/{billingAddress}` | `api.customers.billing-addresses.update` | Api\CustomerBillingAddressController@update |
| DELETE | `/api/customers/{customer}/billing-addresses/{billingAddress}` | `api.customers.billing-addresses.destroy` | Api\CustomerBillingAddressController@destroy |
| POST | `/api/customers/{customer}/service-sites` | `api.customers.service-sites.store` | Api\CustomerServiceSiteController@store |
| PUT | `/api/customers/{customer}/service-sites/{serviceSite}` | `api.customers.service-sites.update` | Api\CustomerServiceSiteController@update |
| DELETE | `/api/customers/{customer}/service-sites/{serviceSite}` | `api.customers.service-sites.destroy` | Api\CustomerServiceSiteController@destroy |

All records are scoped to the authenticated user’s **current team**.

## Permissions

Uses Jetstream team abilities:

- `read` — list / view
- `create` — create customers
- `update` — update customers; manage contacts, billing addresses, service sites
- `delete` — delete customers

Team **admin** has all four; **editor** has create/read/update (no delete). Team owners always pass ability checks.

## Tests

- Path: `laravel/tests/Feature/Domains/Customers/`
- Cover: web CRUD + validation; nested contact / billing / service-site CRUD; API CRUD; guest/unauthorized; editor cannot delete; team isolation; portal account create (see Jobs portal tests)

## Add / remove checklist

### Add

- [x] Domain folder + `DomainServiceProvider`
- [x] Provider registered (`bootstrap/providers.php`)
- [x] This wiki page linked from `docs/README.md`
- [x] Feature tests under `tests/Feature/Domains/Customers/`
- [x] Nav links in `AppLayout.vue`

### Remove

- [ ] Provider unregistered
- [ ] Domain folder deleted
- [ ] This page and index link removed
- [ ] Feature tests deleted
- [ ] Migration / factories removed (or left as historical migrations with care)
- [ ] Nav links removed from `AppLayout.vue`
- [ ] Remaining suite still green
