# User Stories — Reference Data (Brands / Categories / Units)

> Produced by the Reversa **Writer** (phase: generation) · doc_level: `complete`
> Generated on 2026-09-21

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

**Actor:** Store Administrator.
**Owning units:** `brands` (`resource settings/brand`), `categories` (`resource settings/categories`), `units` (`resource settings/units`).

These three are thin Encore\Admin `ModelForm` scaffolds for product reference data. Each renders a two-column screen (a read grid on the left, an inline create/edit widget on the right) and exposes just `name` + optional `description`. Grids disable create-button/export/selector/filter/pagination, and **delete is disabled on all rows** because each is a required FK target. 🟢

---

### US-REF-1 — Manage product brands

**As a** Store Administrator, **I want** to maintain the list of brands, **so that** products can be labelled by manufacturer.

- **Given** the brands admin ("Nhãn hiệu")
- **When** I add or edit a brand (`name` required, `description`)
- **Then** it saves via the ModelForm; the default brand (**id 1**) is edit-locked and no brand is deletable (products' `brand_id` is a required FK) 🟢

Traces to: `brands/` (`BrandController`)

---

### US-REF-2 — Manage product categories

**As a** Store Administrator, **I want** to maintain categories, **so that** products can be classified and reported.

- **Given** the categories admin ("Danh mục")
- **When** I add or edit a category (`name` required, `description`)
- **Then** it saves; delete is disabled on all rows, and — unlike brands/units — **every row is editable** (the id-1 edit-lock is commented out) 🟢

Notes / gaps:
- `parent_id` is **never settable through the UI** (absent from both the widget form and `form()`), so a UI-created category keeps `parent_id` null and can **never** appear in the product dropdown (which lists only `whereNotNull('parent_id')` children). Hierarchy must be seeded directly in the DB. 🔴 (GAP-C1; intentional pre-provisioning vs unfinished feature unconfirmed)
- Category ids **1** (milk) and **8** (medicine) are hard-coded in the statistics rollup — a re-seed with different ids silently breaks the milk/medicine breakdowns. 🟡
- The commented-out id-1 lock (GAP-C2): any category, including ids 1/8, is freely renamable. 🔴

Traces to: `categories/` (`CategoryController`); consumers `products-catalog/`, `customers-statistics/`

---

### US-REF-3 — Manage measurement units

**As a** Store Administrator, **I want** to maintain units of measure, **so that** products can be sold in base and conversion units.

- **Given** the units admin ("Đơn vị")
- **When** I add or edit a unit (`name` required, `description`)
- **Then** it saves; the default unit (**id 1**) is edit-locked (the lock is active here, unlike categories) and no unit is deletable 🟢
- **And** units are consumed three ways: the product base-unit **string** dropdown (`products.unit`), the conversion table `product_units.unit_id` (FK), and order/receipt line labels via `Unit::find(pivot.unit_id)->name` 🟢

Notes / gaps:
- `units.category_id` is a **dead** column no code reads or writes. 🟡 (GAP-U1)
- Base-unit-by-name vs conversion-by-id inconsistency: renaming a unit doesn't propagate to products that captured the old name string. 🟡 (GAP-U2)
- Per-line `Unit::find` on receipts is an N+1 in print loops (in the consuming blades). 🟡

Traces to: `units/` (`UnitController`); consumers `products-catalog/`, `orders-crud/`, `orders-print/`

---

> **Cross-cutting note (all three):** the UI locks (no create button, delete disabled, id-1 edit-lock) are **grid-display gates, not server-side authorization** — the underlying resource routes still exist and are only authenticate-guarded (ADR-0009). Grids are unpaginated (`disablePagination`) — fine for short lists, unbounded as they grow. No observability on any reference-data mutation. 🟡/🔴
