# 09 — Dynamic Product Creation Redesign — Report

**Date:** 2026-11-04 · **Branch:** `techtitans/main` (backend + frontend) · **Scope:** product create/edit for an
electronics retailer, built on the existing NH product, variant, pricing, inventory and storefront engines.

**Status:** done and verified, both in tests and end-to-end in a real browser against `techtitans_pos_dev`.

| Part | Status |
|---|---|
| Product types (templates) | ✅ |
| Dynamic fields: brand, model, specifications | ✅ |
| Sectioned form | ✅ |
| Create & receive | ✅ |
| Variant SKUs, tax class at create | ✅ |
| Quick-add entry points | ✅ |
| **Serialized device units (IMEI, condition, cost…)** | **Not built.** Depends on Inventory Increment 1.1 (§7) |

---

## 1. Existing architecture (Phase A findings)

| Area | What exists (reused, not duplicated) |
|---|---|
| Form | `ProductFormDrawer.tsx` (1,127 lines). A single long drawer, but with a solid create flow: one product call (with the default variant), then resumable steps (variant structure, online price, image) with stable request IDs; live duplicate-name warning; auto SKU; inline category create |
| Variants | `VariantOptionsEditor` + structure endpoints (`variant-structure` preview/apply, max 3 options / 20 values / 100 combinations). Presets come from config and were **all restaurant ones** (portion, temperature…) |
| `product_type` | Means standard vs **combo (bundle)**. Enforced by a DB CHECK, the validation rules, combo logic, import, the catalogue and the storefront. **Must not be reused** for retail kinds |
| Pricing | Variant base price, location prices, online price/sale (`variant_online_prices`). Tax: `products.tax_class_id`, assigned only through `PATCH products/{id}/tax-class` (`tax_classes.manage`). No UI assigned it to a product |
| Inventory | No "track" flag; a variant is tracked when it has an active consumption mapping. Barcode exists **only** on `inventory_items`. `lib/menu/inventorySetup.ts::runVariantSetup` = create item → map → settings → opening count (resumable) |
| Storefront (WordPress) | `buildProduct` / `buildVariant`. A variant's `attributes` key already holds option name/value pairs |
| Brand / model / specs / IMEI / condition | None anywhere |

## 2. New product creation design

**Three separate concepts:**
- **Product**: the catalogue item. It now carries `product_kind` + brand + model + specifications.
- **Variant**: unchanged. A sellable configuration with its own SKU and price, built by the existing structure engine (for example Storage × Colour).
- **Serialized unit**: an individual device. This is **not part of this work** (§7).

**Product types** (`config/product_templates.php`, served by `GET /menu/product-templates`):

| Key | Label | Brand / model | Specification fields | Suggested variant attributes | Stock |
|---|---|---|---|---|---|
| `general` | Accessory / general retail | optional ("Model / part number") | Compatible with | Color, Size, Style, Length, Connector | quantity |
| `phone` | Mobile phone | **required** | Release year, Screen size | Storage, Color, Carrier | serialized* |
| `computer` | Laptop / computer | **required** | Processor, Screen size, Graphics, OS | RAM, Storage, Color | serialized* |
| `tablet` | Tablet | **required** | Screen size, Release year | Storage, Connectivity, Color | serialized* |
| `repair_part` | Repair part | optional ("Part number") | **Compatible with** (required), Part quality | Color, Part quality | quantity |
| `service` | Service (no inventory) | hidden | Estimated duration | Service level | none |

\* Until the serialized backend exists, these are counted by quantity, and the form says so (§7).

**Template rules:**
- Templates describe **fields only**. There are no brand, model, capacity or colour value lists, and the new variant presets (Storage, RAM, Carrier, Connectivity…) carry **no values**.
- Product type is separate from both the **category** (unchanged) and **Sold as: Single product / Bundle** (the old `product_type`, now under "More options").
- `NULL` kind means a legacy product. It is validated exactly as before, so all existing products keep working.

**Form layout:** one scrolling drawer with four sections and a sticky section navigator:
- **Details:** product type cards, photo, name, brand, model, category, specifications, description. "More options" (collapsed) holds status, Sold as, and Available by default.
- **Variants & pricing:**
  - SKU (auto) and the one-version / multiple-versions choice;
  - the existing option editor, filtered to the type's suggested attributes (a custom attribute is always allowed);
  - in-store price, currency, generated variant preview and tax class.
- **Inventory:**
  - **Stock tracking:** Count quantity / Track each unit (IMEI/serial, shown disabled and marked "coming soon") / Don't track (only where the type allows it);
  - opening stock, low-stock alert, barcode (stored on the stock item), and prevent sale (on by default).
  - For phones, computers and tablets, a notice that per-unit tracking isn't available yet.
- **Website:** online name and description (still inherit until edited), and online regular/sale price (one version).

Every field stays mounted, so validation errors and the resumable flow work across sections.

**Create flow** (each step is resumable, uses a stable request ID, and can be retried or skipped):
1. product + default variant;
2. variant structure;
3. **variant SKUs** (existing bulk `sku.generate`);
4. **tax class**;
5. **stock setup per variant** (existing `runVariantSetup`);
6. online price;
7. image.

**Day-to-day entry points** (Products page, next to **Add Product**, under **More ways to add**):

| Entry | What it opens |
|---|---|
| Quick add accessory (create & receive) | general type, one version, stock on, opening stock field |
| Product with variants | multiple versions |
| Phone / device model | phone type, multiple versions, stock on |
| Service (no stock) | service type, no stock |
| Receive stock for an existing product | opens Inventory, which uses the **existing Receive Stock drawer**. Receiving more units never requires re-creating the product |

## 3. Database changes

There is one **additive** migration: `2026_11_21_100000_add_retail_template_fields_to_products`.

| Column | Type | Purpose |
|---|---|---|
| `products.product_kind` | varchar(40), nullable | Template key |
| `products.brand` | varchar(120), nullable | |
| `products.model_name` | varchar(160), nullable | Not called `model`, to avoid Eloquent ambiguity |
| `products.specs` | JSON, nullable | Product-level specifications, keyed by the template's spec keys |
| index `(tenant_id, location_id, product_kind)` | | Filtering by type |

- No existing data is modified, and the migration is reversible.
- Allowed kinds are enforced by the application, since templates are configurable; there is no DB CHECK.
- It was applied to `techtitans_pos_dev` and runs automatically in tests.

**No unit data on products.** IMEI, serial, condition, battery health, purchase cost, testing status and warranty are **rejected** if sent as specs (a 422; this is tested).

## 4. Files modified

### Backend (`techpos/backend`)

| File | Change |
|---|---|
| `config/product_templates.php` | **New.** The 6 product kinds, declaring fields only |
| `config/menu_variant_presets.php` | Adds electronics attribute names (no values). Restaurant presets are kept for compatibility |
| `app/Menu/Templates/ProductTemplateRegistry.php` | **New.** Template list plus the one validator: allowed kind, required brand/model/spec per kind, spec keys limited to the kind, whitespace normalisation, length limits |
| `app/Http/Controllers/Api/V1/Menu/ProductTemplateController.php`, `routes/api.php` | **New** `GET menu/product-templates`, next to the variant presets route |
| `database/migrations/2026_11_21_100000_…` | **New** (§3) |
| `app/Models/Product.php` | Fillable fields plus a `specs` array cast |
| `Store/UpdateProductRequest.php` | Shape rules for the new fields. Template rules apply to the **final merged state** in the service |
| `app/Menu/ProductService.php` | Create/update template validation (update validates only when a descriptor is sent, so legacy updates are untouched). The new fields are added to the **idempotency fingerprint** (a replay with different specs returns 409) and to the audit snapshot |
| `app/Http/Controllers/Api/V1/Menu/ProductController.php` | Product JSON adds `product_kind`, `brand`, `model_name`, `specs` and `tax_class_id` (new keys only) |
| `app/Menu/ProductDuplicationService.php` | Duplicates keep kind, brand, model and specs |
| `app/Storefront/StorefrontCatalogueService.php` | WordPress payload adds `product_kind`, `brand`, `model_name`, `specifications`. Deliberately **not** named `attributes`, which is the variant option key; existing keys are unchanged |
| `tests/Feature/Menu/ProductRetailTemplateTest.php` | **New**, 14 tests |
| `tests/Feature/Storefront/StorefrontCatalogueApiTest.php` | +1 test |
| `docs/techtitans/09-…`, `docs/techtitans/screenshots/product-form/*.png` | This report plus 6 screenshots |

### Frontend (`techpos/frontend`)

| File | Change |
|---|---|
| `src/components/menu/ProductFormDrawer.tsx` | Restructured as described in §2. All previous behaviour and labels are preserved |
| `src/components/menu/product-form.css` | **New**: navigator, type cards, collapsible blocks, grids, responsive layout |
| `src/lib/api/menu/productTemplates.ts` | **New** templates client and types |
| `src/lib/api/menu/products.ts` | New optional fields on product types and inputs |
| `src/lib/api/taxClasses.ts` | `assignProduct()` (an existing endpoint that had no client) |
| `src/lib/menu/inventorySetup.ts` | Optional `itemBarcode` for the new stock item |
| `src/pages/menu/ProductsListPage.tsx` | "More ways to add" menu and create presets |
| `ProductFormDrawer.retail.test.tsx` | **New**, 18 tests |
| `ProductFormDrawer.structure.test.tsx`, `ProductsListPage.test.tsx` | Mocks for the new SKU step; +1 list test |

## 5. Visual verification

These were captured in Microsoft Edge (headless, via the project's Playwright). The script logs in through the real login form as the dev owner, against the local dev API and DB. They are in `docs/techtitans/screenshots/product-form/`:
1. `01`: Add product, default (Accessory selected).
2. `02`: Mobile phone selected, with Brand and Model required.
3. `03`: the Inventory section with the unit-tracking notice.
4. `04`: the Website section.
5. `05`: the "More ways to add" menu.
6. `06`: a phone with Storage variants, a generated-variant preview, tax class and stock.

**End-to-end UI create (real backend):** three phones with Storage 128GB/256GB were created through the browser. The result in `techtitans_pos_dev`:
- products with `product_kind=phone`, brand, model and specs;
- variant SKUs generated (`SAMSUNG-GALAXY-S24-128GB` / `-256GB`);
- one inventory item per variant carrying the variant SKU, with low-stock 2 and prevent-sale on.

These are synthetic dev records and can be deleted.

During verification, three layout issues were found and fixed: the navigator collapsing in the flex column, Model squeezed next to Brand, and content showing above the sticky navigator.

## 6. Tests

All tests ran against `techtitans_pos_test` with Xdebug off.

| Suite | Result |
|---|---|
| New `ProductRetailTemplateTest` (templates endpoint, no value lists, presets, accessory, phone required fields, laptop specs, unit data rejected, repair part compatibility, unknown kind, legacy untouched, merged-state edit, duplicate SKU, idempotent replay conflict, structured variants on a phone, duplication) | **14/14** |
| Backend Menu (all, including the above) | **787/787** |
| Storefront, Checkout, Orders, Channels, Payments, Tax, Inventory, Audit | **606/606** |
| Storefront catalogue, including the new additive-payload test (re-run after Pint) | **15/15** |
| New `ProductFormDrawer.retail.test.tsx`: sections and nav, dynamic visibility (phone/service), client-required fields, honest serialized notice (option disabled, no unit inputs), template-filtered presets, accessory create & receive (item, map, settings, opening count, barcode), laptop specs, service with no stock, phone with variants (SKUs generated **before** stock items, one item per variant), tax class on create, resumed stock setup without duplicates, duplicate-SKU error mapping, edit save, legacy product, legacy adopts a type, templates offline (legacy form), tax class on edit | **18/18** |
| Frontend menu and product suites | **493/493** |
| **Frontend full run** | **1,707/1,707** (166 files) |

Typecheck and lint also pass: `tsc -b` 0; eslint 0 errors (3 pre-existing warnings); Pint passes on all changed backend files.

Playwright e2e specs were **not** run, because they still use NH fixtures. Only the screenshot/e2e script above was used, against TechTitans dev.

## 7. Remaining dependencies for full device inventory (not claimed as done)

1. **Serialized units (Inventory Increment 1.1/1.2).** This needs:
   - a `inventory_units` table (IMEI/serial unique per tenant, condition grade, battery health, purchase cost, testing status, warranty, notes, status lifecycle);
   - unit-level receive, sell, return and transfer, writing to the existing ledger;
   - a sale path that picks a specific unit.

   Until then:
   - "Track each unit" is disabled;
   - phones, computers and tablets are counted by quantity;
   - **"Add Device" (intake of an individual new/used unit) is not built.** The menu offers "Phone / device model" (the catalogue model) instead;
   - the templates' `unit_fields` list what the unit form will need.
2. **POS stock deduction from the UI** (from report 08, §7.1). The in-store UI still never calls commit, so serialized sales need that decided first.
3. Per-variant **opening stock** at create (multiple versions). Variants are created with tracked, empty stock and are received afterwards through Receive Stock. This is intentional: there is no per-variant quantity grid yet.
4. **Barcode / UPC per variant.** Barcodes live on inventory items, so they are set at create for a single-version product only. A variant-level barcode and scan lookup is planned for Increment 1.1.
5. **Brand list / brand filter.** Brand is free text per product (no brands table). A product-type filter on the product list and catalogue search by brand/model are not added yet.
6. The **WordPress connector** (v0.4.0 source missing) must be updated to read the new storefront keys. Existing keys are unchanged, so current consumers are unaffected.
7. **CSV import** does not accept the new columns. Import still creates legacy (un-typed) products.
8. Templates are **backend config**, editable by a developer. A tenant-level template editor UI is not built.
9. Remaining restaurant wording outside the product form (sidebar "Menu", "Kitchen", "Tables", the Cacart logo and so on) belongs to the separate rebranding task.
