# Units Design

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

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

## Data Model

### `units` table 🟢 (`create_units_table` migration)

| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | bigint PK | No | Auto-increment |
| `name` | string | No | Required; the value that consumers display |
| `description` | string | Yes | Free text |
| `category_id` | integer | Yes | Dead column — no code reads or writes it 🟡 |
| `created_at` / `updated_at` | timestamps | Yes | Laravel defaults |

### `Unit` model 🟢 (`Unit.php`)

Bare Eloquent model. No `$fillable`, no casts, no defined relations. The inverse relation `ProductUnit::unit()` = `belongsTo(Unit)` lives on `ProductUnit`, not here.

### Consumer coupling — two keying schemes 🟡 (`units` flowchart:28-32)

```mermaid
erDiagram
    units {
        bigint id PK
        string name
        string description
        int category_id "unused/dead"
    }
    products {
        bigint id PK
        string unit "name string — NOT FK"
    }
    product_units {
        bigint id PK
        bigint product_id FK
        bigint unit_id FK
    }
    order_product {
        bigint id PK
        bigint unit_id FK
    }

    units ||--o{ product_units : "unit_id (integer FK)"
    units ||--o{ order_product : "unit_id (integer FK)"
    products }o--|| units : "unit name string (no FK)"
```

`products.unit` stores the unit **name as a string** (no FK); `product_units.unit_id` and `order_product.unit_id` store the unit **id as an integer FK**. A unit rename does not propagate to `products.unit`.

---

## Internal Flows

### GET `settings/units` — list + inline create 🟢 (`UnitController.php:20-41`)

```mermaid
sequenceDiagram
    actor Admin
    participant Middleware as ['web','admin']
    participant index as UnitController::index()
    participant grid as ::grid()
    participant WidgetForm as Widgets\Form

    Admin->>Middleware: GET settings/units
    Middleware-->>Admin: 302 auth/login (if unauthenticated)
    Middleware->>index: authenticated
    index->>grid: grid()->render()
    grid-->>index: HTML grid (id/name/description, all controls disabled)
    index->>WidgetForm: Widgets\Form(action=settings/units)<br/>name(required), description
    WidgetForm-->>index: form HTML inside green Box
    index-->>Admin: 200 Admin::content two-column layout
```

### POST `settings/units` — create 🟢 (`UnitController.php:76-82`)

```mermaid
flowchart TD
    A[POST settings/units] --> B{name present?}
    B -- no --> C[422 / redirect back with errors]
    B -- yes --> D[ModelForm::store via form()]
    D --> E[INSERT INTO units]
    E --> F[302 redirect back to list]
```

### GET `settings/units/{id}/edit` + PUT `settings/units/{id}` — edit 🟢 (`:43-49,76-82`)

```mermaid
flowchart TD
    A[GET settings/units/{id}/edit] --> B[UnitController::edit]
    B --> C{id exists?}
    C -- no --> D[404]
    C -- yes --> E[form()->edit(id) → 200 HTML]
    E --> F[Admin submits PUT]
    F --> G{name present?}
    G -- no --> H[422 / redirect back with errors]
    G -- yes --> I[ModelForm::update → 302]
```

### Row-action lock logic 🟢 (`UnitController.php:66-71`)

```mermaid
flowchart TD
    R[Each grid row] --> A{row key == 1?}
    A -- yes --> B[disableEdit]
    A -- no --> C[edit action shown]
    R --> D[disableDelete always]
```

---

## Technical Decisions

| Decision | Rationale | Confidence |
|----------|-----------|------------|
| `Encore\Admin` `ModelForm` trait — `store`/`update`/`destroy` are scaffolded defaults driven by `form()` | Reduces boilerplate; only `index`/`edit`/`grid`/`form` are overridden | 🟢 (`:18`) |
| Inline create widget in the `index` right column instead of a separate create page | `disableCreateButton()` on the grid; creation is via `Widgets\Form` in the same view | 🟢 (`:29-38,60`) |
| Delete disabled unconditionally on all rows | `product_units.unit_id` and `order_product.unit_id` reference units; receipts and order views label lines via `Unit::find(pivot.unit_id)->name` — deleting a unit breaks those consumers | 🟡 (`:70`; flowchart:30-32) |
| Default unit (id 1) edit-locked by literal id comparison | `in_array($actions->getKey(), [1])` → `disableEdit()`; unlike `categories`, this block is active, not commented out | 🟢 (`:67-69`) |
| Unpaginated / unfiltered grid | `disableFilter()` + `disablePagination()` — unit catalogue assumed to be small | 🟡 (`:63-64`) |
| Base unit stored as name string (`products.unit`) while conversions use integer FK (`product_units.unit_id`) | Two keying schemes for the same table; a rename does not propagate to `products.unit` | 🟡 (flowchart:28-32) |

---

## Notes

- **Dead `units.category_id` column** 🟡 — present in the migration (column 20) but never read or written by any code. Confirm removal or intended use before relying on it.
- **Default-unit lock fragility** 🟡 — the edit lock keys off the literal integer `1`; a re-seed that assigns a different id silently protects the wrong row.
- **N+1 in consuming blades** 🟡 — consumers (`orders-print`, `orders-crud`, `customers-purchase-history`) call `Unit::find(pivot.unit_id)` per order line inside print/detail loops; the N+1 is in those units, not here.
- **No observability** 🔴 — unit mutations emit no domain-level log, metric, or trace; only framework-default behaviour.
