# Feature: Providers

## Purpose

Team-scoped third-party provider CRM with fee models (flat / hourly / invoice %), optional Jetstream portal login scoped to assigned appointments, soft `provider_id` on appointments (owned by Jobs), and provider payments that record payout + tip until Invoices lands.

## Boundaries

- **Owns:** `providers`, `provider_portal_users`, `provider_payments`; Providers domain Actions, Policies, HTTP (web + API), Inertia pages under `Pages/Providers/`
- **Does not own:** appointments / jobs (Jobs stores soft `provider_id` with no FK and no Providers Eloquent imports); invoices / booking totals
- **Depends on (platform only):** User / Team / auth (Jetstream abilities); portal accounts use Jetstream `provider` role via `CreateTeamMemberWithPassword`
- **Depends on (other domains):** none required — soft-reads `service_jobs` / `job_appointments` via `SoftJobLabels` (DB) for payment labels; validation may use `exists:service_jobs,id` / `exists:job_appointments,id` while Jobs is present
- **Platform UI touchpoint:** Providers + Provider payments links in `resources/js/Layouts/AppLayout.vue` (hidden for `customer`, `staff`, `provider` roles)

## Models

| Model | Table | Notes |
|-------|-------|-------|
| `Provider` | `providers` | `team_id` FK → `teams`; `fee_type` `flat` \| `hourly` \| `percentage`; `fee_value`; `is_active` |
| `ProviderPortalUser` | `provider_portal_users` | Soft link `provider_id` ↔ Jetstream `user_id` (unique each); cascade |
| `ProviderPayment` | `provider_payments` | Soft `job_id` / `job_appointment_id` / `invoice_id`; `payout_amount`, `tip_amount` (default 0), `paid_on`, `status` `pending` \| `paid` \| `held`; total = payout + tip (UI) |

Schema is normalised (3NF). Migration: `database/migrations/2026_08_12_170000_create_providers_tables.php` (also adds nullable `job_appointments.provider_id` without FK — documented under Jobs). Factories: `database/factories/Domains/Providers/`.

### Fee model

Stored on the provider for later suggested payouts from invoices. v1 payments are **manual entry** (percentage needs invoice totals, which are out of scope).

### Provider portal account

Owners create a Jetstream user with role `provider` from the provider show page (`POST /providers/{provider}/portal-account`). One-time password is flashed like customer portal. Portal providers cannot access the providers CRM (policy). Jobs scopes visibility via `ProviderPortalAccess` (DB read of `provider_portal_users`, no Providers model imports).

### Payments (v1)

Store provider **payout + tip** only. Do not store booking / invoice total yet.

## Routes

### Web

| Method | URI | Name | Controller |
|--------|-----|------|------------|
| GET | `/providers` | `providers.index` | Web\ProviderController@index |
| GET | `/providers/create` | `providers.create` | Web\ProviderController@create |
| POST | `/providers` | `providers.store` | Web\ProviderController@store |
| GET | `/providers/{provider}` | `providers.show` | Web\ProviderController@show |
| GET | `/providers/{provider}/edit` | `providers.edit` | Web\ProviderController@edit |
| PUT/PATCH | `/providers/{provider}` | `providers.update` | Web\ProviderController@update |
| DELETE | `/providers/{provider}` | `providers.destroy` | Web\ProviderController@destroy |
| POST | `/providers/{provider}/portal-account` | `providers.portal-account.store` | Web\ProviderPortalAccountController@store |
| GET | `/provider-payments` | `provider-payments.index` | Web\ProviderPaymentController@index |
| GET | `/provider-payments/create` | `provider-payments.create` | Web\ProviderPaymentController@create |
| POST | `/provider-payments` | `provider-payments.store` | Web\ProviderPaymentController@store |
| PUT | `/provider-payments/{providerPayment}` | `provider-payments.update` | Web\ProviderPaymentController@update |

### API

| Method | URI | Name | Controller |
|--------|-----|------|------------|
| GET | `/api/providers` | `api.providers.index` | Api\ProviderController@index |
| POST | `/api/providers` | `api.providers.store` | Api\ProviderController@store |
| GET | `/api/providers/{provider}` | `api.providers.show` | Api\ProviderController@show |
| PUT/PATCH | `/api/providers/{provider}` | `api.providers.update` | Api\ProviderController@update |
| DELETE | `/api/providers/{provider}` | `api.providers.destroy` | Api\ProviderController@destroy |
| GET | `/api/provider-payments` | `api.provider-payments.index` | Api\ProviderPaymentController@index |
| POST | `/api/provider-payments` | `api.provider-payments.store` | Api\ProviderPaymentController@store |
| PUT | `/api/provider-payments/{providerPayment}` | `api.provider-payments.update` | Api\ProviderPaymentController@update |

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

## Permissions

Same spirit as Customers CRM:

- `create` / `read` / `update` for editor+ (owners/admin)
- `delete` for owner/admin
- `staff`, `customer`, and `provider` roles: **no** providers CRM or payments access

Portal `provider` role: Jetstream `read` only; Jobs further scopes to assigned appointments with a limited payload.

## Tests

- Path: `laravel/tests/Feature/Domains/Providers/`
- Cover: provider CRUD; portal create; appointment assign `provider_id`; provider user only sees assigned appointments; payments create/list with tip; provider role forbidden on providers CRM

## 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/Providers/`
- [x] Nav links in `AppLayout.vue`
- [x] Jetstream `provider` role

### Remove

- [ ] Provider unregistered
- [ ] Domain folder deleted
- [ ] This page and index link removed
- [ ] Feature tests deleted
- [ ] Drop `job_appointments.provider_id` usage / migration column if unused
- [ ] Remove Jetstream `provider` role + nav links
- [ ] Remaining suite still green
