# Units — Requirements

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

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

## Overview

`units` is the back-office reference-data editor for measurement units (`resource settings/units`, `UnitController`). It is a thin **Encore\Admin** scaffolded CRUD structurally identical to `brands`: a two-column screen with a read-only grid on the left and an inline "new unit" widget form on the right, plus an edit form. A unit is little more than a `name` + optional `description`. 🟢 (`routes/web.php:89`, `UnitController.php`)

Only `index`, `edit`, `grid`, and `form` are overridden in the controller; `store`, `update`, and `destroy` come from the `Encore\Admin\Controllers\ModelForm` trait and are driven by `form()`. 🟢 (`UnitController.php:18,76-82`)

Units are consumed three ways across the app: the product base-unit dropdown (`products.unit`, a **string** keyed by name), the conversion-units table (`product_units.unit_id`, an integer FK), and unit labels on receipts/order detail/customer-order history (`Unit::find(pivot.unit_id)->name`). 🟢 (`units` flowchart:24-40)

## Responsibilities

- Render a two-column management screen: left = unit grid, right = an inline create form posting to `settings/units`. 🟢 (`:20-41`)
- List units in a grid with columns `id` (sortable), `name`, `description`, and no create button / export / row selector / filter / pagination. 🟢 (`:54-64`)
- Gate row actions: **disable delete on every row**, and **disable edit on the row with key `1`** (the mandatory default unit). 🟢 (`:66-71`)
- Provide an edit form (`name` required, `description`) for non-locked units. 🟢 (`:43-49,76-82`)
- Persist create/update via the `ModelForm` trait using `form()`'s field set (`name` required, `description`). 🟢 (`:18,76-82`)

## Business Rules

- **A unit is `name` (required) + `description` (optional).** Both the inline widget form and `form()` expose exactly these two fields. 🟢 (`:33-34,79-80`)
- **Unit id 1 is the mandatory default unit and is edit-locked.** `in_array($actions->getKey(), [1])` → `disableEdit()`; combined with the app-wide delete lock, id 1 is permanent and immutable through the UI. This id-1 lock is **active** here, unlike in `categories` where the equivalent block is commented out. 🟢 (`:67-69`; `units` flowchart:37)
- **No unit is UI-deletable.** `disableDelete()` runs for every row, and there is no create button (`disableCreateButton()`); creation happens only through the inline widget form. 🟢 (`:60,70`)
- **The `Unit` model is empty.** No relations, casts, or `$fillable`; it is a bare Eloquent model. The inverse relation `ProductUnit::unit()` = `belongsTo(Unit)` lives on `ProductUnit`, not here. 🟢 (`Unit.php`)
- **No custom algorithm or lifecycle hook.** The controller adds no `saving`/`saved` hooks and no validation beyond `name` required; all persistence is the framework default. 🟢 (`UnitController.php`)
- **Grid is unpaginated / unfiltered.** Export, row selector, filter, and pagination are all disabled — the unit list is expected to be short. 🟡 (`:61-64`)

## Functional Requirements

| ID | Requirement | Priority | Acceptance criterion |
|----|-------------|----------|----------------------|
| RF-01 | List units at `GET settings/units` in a two-column screen (grid + inline create form) | Must | The page shows the id/name/description grid and a "new unit" box with `name`/`description` fields. 🟢 |
| RF-02 | Create a unit via the inline form (`POST settings/units`) | Must | Submitting `name` (required) + `description` persists a new unit and returns to the list. 🟢 |
| RF-03 | Edit a unit's `name`/`description` (`GET settings/units/{id}/edit`, `PUT settings/units/{id}`) | Must | Editing a non-locked unit updates its fields; `name` is required. 🟢 |
| RF-04 | Lock the default unit (id 1) from editing | Should | The edit action is absent on the id-1 row. 🟢 |
| RF-05 | Disable deletion for all units | Should | No delete action is offered on any row. 🟢 |
| RF-06 | Require an authenticated admin session | Must | Anonymous request → `302` to `auth/login`. 🟢 (`routes/web.php:24-28`) |

## Non-Functional Requirements

| Type | Inferred requirement | Evidence in code | Confidence |
|------|----------------------|------------------|------------|
| Security | Admin authentication required (admin route group middleware `['web','admin']`) | `routes/web.php:24-28,86-92` | 🟢 |
| Integrity | Units are undeletable by design — `product_units.unit_id` references them (and receipts label lines via `Unit::find(pivot.unit_id)`); deleting a unit would break conversion rows and labels | `UnitController.php:70`; `units` flowchart:30-32 | 🟡 |
| Usability | Unpaginated single-screen grid — assumes a small unit catalogue | `UnitController.php:61-64` | 🟡 |
| Performance | Unit labels on receipts use `Unit::find(pivot.unit_id)` per line → N+1 in print loops (in the consuming blades, not this unit) | `units` flowchart:40 | 🟡 |
| Observability | None — framework CRUD emits no domain log/metric | `UnitController.php` (absence) | 🔴 |

> Inferred from code. Validate with the operations team.

## Acceptance Criteria

```gherkin
Given an authenticated administrator
When he accesses GET settings/units
Then he receives HTTP 200 with the units grid (id/name/description) and the inline "new unit" form

Given the inline new-unit form
When he submits a filled name and description
Then a new unit is persisted and the list is re-rendered

Given the inline new-unit form
When he submits without name
Then required validation fails and the unit is not created

Given the default unit of id 1
When the grid is rendered
Then the edit action is absent from that row and no row offers delete

Given a request with no authenticated admin session
When settings/units is accessed
Then it receives HTTP 302 redirecting to auth/login
```

## Priority (MoSCoW)

| Requirement | MoSCoW | Justification |
|-------------|--------|---------------|
| List + create units (RF-01, RF-02) | Must | Units are required master data for product base units and conversions |
| Edit unit (RF-03) | Must | Correcting unit names/descriptions |
| Admin authentication (RF-06) | Must | Enforced by the route group |
| Lock default unit id 1 (RF-04) | Should | Protects the mandatory fallback unit |
| Disable deletion (RF-05) | Should | Prevents breaking `product_units` conversion rows and line labels |

> Priority inferred from units being mandatory reference data referenced by products (base unit) and by `product_units`.

## Code Traceability

| File | Function / Class | Coverage |
|------|------------------|----------|
| `app/Http/Controllers/UnitController.php:20-41` | `UnitController::index` (two-column grid + inline form) | 🟢 |
| `app/Http/Controllers/UnitController.php:43-49` | `UnitController::edit` | 🟢 |
| `app/Http/Controllers/UnitController.php:54-74` | `UnitController::grid` (columns, disabled features, row-action locks) | 🟢 |
| `app/Http/Controllers/UnitController.php:76-82` | `UnitController::form` (drives ModelForm store/update) | 🟢 |
| `app/Models/Unit.php` | `Unit` model (empty — no relations/casts/fillable) | 🟢 |
| `database/migrations/2019_05_15_121417_create_units_table.php` | `units` schema (`id`, `name`, `description`, dead `category_id`, timestamps) | 🟢 |
| `routes/web.php:89` | `resource settings/units` | 🟢 |
