Products — Developer Guide
Products is the Phase 5 catalog module. It follows the Leads-style module layout: tenant model, form requests, resources, policy, service, events/subscriber, activity logging, frontend list/form/detail UI, and Playwright coverage.
Backend
| Piece | Path |
|---|---|
| Models | Product, ProductCategory, ProductNote, ProductActivity |
| Service | app/Services/Tenant/ProductService.php |
| HTTP | ProductController, ProductCategoryController, app/Http/Requests/Tenant/Api/V1/Product* |
| Resources | app/Http/Resources/Tenant/Api/V1/Product* |
| Authorization | ProductPolicy, ProductCategoryPolicy |
| Tests | tests/Feature/Tenant/Product/ |
Products is gated by module:products and products.view|create|update|delete|restore|force.delete. Category CRUD uses the same permissions.
Domain rules
skuis unique per tenant; category is nullable and is nullified if removed.- On create,
skumay be omitted or blank —ProductServiceassigns the next SKU (products_sku_prefixtenant setting, defaultSKU-, plus a 5-digit sequence including soft-deleted rows) and retries on unique collisions. Clients may override with an explicit SKU.GET /products/next-skupreviews the next value for the SPA. - Product
descriptionaccepts sanitized HTML (same allowlist as billing document notes;stylestripped,javascript:/data:hrefs neutralized); empty HTML is stored asnull. - There is no separate product/service type. Services are catalog rows with
track_stock = false. track_stockcontrols whetherStockServicecan mutate a product's stock.- Product notes and
product_activitiesprovide the domain timeline; the model also uses SpatieLogsActivity. product_idonpurchase_order_linesand billing document lines (quotation_lines,estimate_lines,customer_invoice_lines) is nullable and validated byLinkableProductwhen supplied: Products module entitled, actor hasproducts.view(or superadmin), product is active and not soft-deleted, same tenant.- Billing SPA line pickers (and purchase-order picker) gate on Products +
products.view, list active products with server search, auto-fill line text on select, and leave edited fields intact when clearing the link. Convert estimate → invoice and recurring invoice generation copyproduct_id.
Catalog: slug products, version 1.2.0 (auto SKU + HTML description + link hardening).
Frontend and verification
Pages are in src/pages/products/ (products-page.tsx, product-form.tsx + product-form-page.tsx, product-view-page.tsx). Category CRUD stays in product-categories-dialog.tsx. Service/types/query keys/permissions live in the shared API, type, constants, and navigation layers. The route and nav item use module:products plus products.view. Currency uses shared currencyOptions() / SearchableSelect and defaults to tenant settings.currency on create.
Run:
php artisan test --compact tests/Feature/Tenant/Product
npm run test:e2e:products