# Feature: Sundry products

## Purpose

Team-scoped catalog of reusable extras (supplies, travel, add-on charges) that staff can pick onto quotes and invoices. Catalog prices and names are snapshotted onto document lines at add time.

## Boundaries

- **Owns:** `sundry_products`; Products domain Actions, Policies, HTTP (web + API), Inertia pages under `Pages/SundryProducts/`
- **Does not own:** Quotes, invoices, line items, discount codes, jobs
- **Depends on (platform only):** User / Team / auth (Jetstream abilities `create` / `read` / `update` / `delete`)
- **Depends on (other domains):** none required
- **Consumed by (soft, optional):** Invoices domain soft-reads `sundry_products` via `Schema::hasTable` / `DB::table` when building quote/invoice line pickers and resolving `sundry_product_id`. Invoices must not import this domain’s models.
- **Platform UI touchpoint:** side nav link **Sundry products** in `resources/js/Layouts/AppLayout.vue` (hidden for `staff` / `customer` / `provider`; revert on remove)

## Models

| Model | Table | Notes |
|-------|-------|-------|
| `SundryProduct` | `sundry_products` | `team_id` FK → `teams`; `name`; optional `sku` unique per team; `unit_amount`; optional `unit_label`; `is_active` |

Schema is normalised (3NF). Inactive rows stay in the catalog but are omitted from quote/invoice pickers. Deleting a catalog row does not rewrite existing document lines (Invoices stores a snapshot). Migration: `database/migrations/2026_08_28_100000_create_sundry_products_table.php`. Factory: `database/factories/Domains/Products/`.

## Routes

### Web

| Method | URI | Name | Controller |
|--------|-----|------|------------|
| GET | `/sundry-products` | `sundry-products.index` | Web\SundryProductController@index |
| GET | `/sundry-products/create` | `sundry-products.create` | Web\SundryProductController@create |
| POST | `/sundry-products` | `sundry-products.store` | Web\SundryProductController@store |
| GET | `/sundry-products/{sundry_product}/edit` | `sundry-products.edit` | Web\SundryProductController@edit |
| PUT/PATCH | `/sundry-products/{sundry_product}` | `sundry-products.update` | Web\SundryProductController@update |
| DELETE | `/sundry-products/{sundry_product}` | `sundry-products.destroy` | Web\SundryProductController@destroy |

### API

| Method | URI | Name | Controller |
|--------|-----|------|------------|
| GET | `/api/sundry-products` | `api.sundry-products.index` | Api\SundryProductController@index |
| POST | `/api/sundry-products` | `api.sundry-products.store` | Api\SundryProductController@store |
| GET | `/api/sundry-products/{sundry_product}` | `api.sundry-products.show` | Api\SundryProductController@show |
| PUT/PATCH | `/api/sundry-products/{sundry_product}` | `api.sundry-products.update` | Api\SundryProductController@update |
| DELETE | `/api/sundry-products/{sundry_product}` | `api.sundry-products.destroy` | Api\SundryProductController@destroy |

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

## Permissions

Editor+ create/read/update; owner/admin delete; staff/customer/provider forbidden (`ProductAccess`). Super Admin browse key: `sundry-products`.

## Tests

- Path: `laravel/tests/Feature/Domains/Products/`
- Cover: index team isolation, create/update/deactivate/delete, unique SKU per team, guest redirect, staff forbidden, API store/index isolation

## Add / remove checklist

### Add

- [x] Domain folder + `DomainServiceProvider`
- [x] Provider registered
- [x] This wiki page linked from `docs/README.md`
- [x] Feature tests passing

### Remove

- [ ] Provider unregistered
- [ ] Domain folder deleted
- [ ] This page and index link removed
- [ ] Feature tests deleted
- [ ] Remaining suite still green
- [ ] Remove `AppLayout` **Sundry products** nav links
- [ ] Invoices picker continues to work with empty options (`SoftSundryProducts` no-ops when the table is gone)
