# Backend pivot: Iron Rod & Binding Wire store — frontend integration worksheet

## 1. What changed and why

This API used to serve a cosmetics POS (DB was literally `grobin_cosmetics`; products
were cartons of units with expiry dates). It has been pivoted to **Goju Brothers
Enterprises**, an iron rod and binding wire merchant. The product, stock, and order
models changed shape. This doc tells you exactly what the frontend needs to send/render
differently. Nothing about auth, clients, stores, categories CRUD, carts, reports, or
PDF receipts changed structurally — only **products, brands, stock, and the sale flow**.

Full implementation plan/history: see git log on this repo (migrations dated
`2026_10_09_*`) if you need more background than this doc covers.

## 2. New catalog taxonomy

```
Category (Iron Rods | Binding Wires)
   └── Brand (Monarch, Royal, RS, Kam, Top Steel, BL, ...)
         └── For ROD products: Rod Type (TMT | ORDINARY) × Size in mm (8, 10, 12, 16, 20, 25...)
         └── For WIRE products: Bundle Weight in kg (5, 10, 17, 20...)
```

A product is uniquely identified by `brand + rod_type + size_mm` (rods) or
`brand + bundle_weight_kg` (wire). There is no more "carton of units" packaging concept.

**Brand is a new top-level entity**, CRUD'd just like Category:

```
GET    /api/brands            (paginated, ?search=, ?limit=)
GET    /api/brands/load       (id+name only, for dropdowns)
POST   /api/brands            { name, slug?, store_id? }
GET    /api/brands/{id}
PUT    /api/brands/{id}
DELETE /api/brands/{id}
```

## 3. Product schema — what to send

`POST /api/products` and `PUT /api/products/{id}` now validate on `product_type`. Two
distinct shapes depending on whether you're creating a rod or wire product:

| Field | Type | Required | Notes |
|---|---|---|---|
| `name` | string | always | |
| `category_id` | int | always (create) | `exists:categories,id` |
| `brand_id` | int\|null | optional | `exists:brands,id` |
| `product_type` | `ROD` \| `WIRE` | always | drives every field below |
| `rod_type` | `TMT` \| `ORDINARY` | **required if ROD** | null for WIRE |
| `size_mm` | number | **required if ROD** | null for WIRE |
| `bundle_weight_kg` | number | **required if WIRE** | null for ROD |
| `unit_price` | number | always | price per **piece** (ROD) or per **bundle** (WIRE) |
| `price_per_ton` | number | **required if ROD** | null for WIRE |
| `pieces_per_ton` | number | **required if ROD** | conversion factor, varies by brand/size — ask the owner, don't guess |
| `cost_price` | number | always | cost basis matching `unit_price`'s unit (per piece/bundle) |
| `low_stock_threshold` | number | optional | nullable; defaults to 2 (tons) for ROD / 5 (bundles) for WIRE if omitted |
| `barcode`, `sku`, `description` | | optional | unchanged |

**Removed fields — do not send these anymore, they no longer exist on the model:**
`units_per_carton`, `has_expiry`, `bulk_price`.

### Computed/read-only fields on every product response

| Field | Meaning |
|---|---|
| `store_stock_status` | `"In Stock"` \| `"Low in Stock"` \| `"Out of Stock"`, computed against `low_stock_threshold` (or the type default) |
| `unit_quantity` / `bulk_quantity` | storefront stock quantity, in **tons** for ROD or **bundles** for WIRE |
| `warehouse_quantity` | same, warehouse-side |
| `total_quantity` | storefront + warehouse |
| `current_stock_value` | `unit_quantity × price_per_ton` (ROD) or `unit_quantity × unit_price` (WIRE) |
| `pieces_available` | ROD only: `round(unit_quantity × pieces_per_ton)` — convenience for showing "≈N lengths in stock"; `null` for WIRE |

### Example payloads

```jsonc
// Create a rod product
POST /api/products
{
  "name": "Royal TMT 12mm",
  "category_id": 1,
  "brand_id": 2,
  "product_type": "ROD",
  "rod_type": "TMT",
  "size_mm": 12,
  "unit_price": 87000,       // per length/piece
  "price_per_ton": 438000,
  "pieces_per_ton": 5,
  "cost_price": 70000,       // per piece
  "store_id": 1
}

// Create a wire product
POST /api/products
{
  "name": "BL Wire 17kg",
  "category_id": 2,
  "brand_id": 6,
  "product_type": "WIRE",
  "bundle_weight_kg": 17,
  "unit_price": 68000,       // per bundle
  "cost_price": 55000,
  "store_id": 1
}
```

## 4. Stock / inventory — unit semantics

`GET /api/inventories/storefront` and `/api/inventories/warehouse` return the same
shape as before, **minus `expiry_date`** (dropped — iron doesn't expire) and
**`batch_no` is now optional free text** — repurpose it as a delivery/truck reference if
useful (e.g. the vehicle plate number off a delivery note), not a pharmacy batch code.

`quantity_current` / `quantity_stocked` / `quantity_transferred` are unchanged in name
but their **unit is implicit from the product**: tons for `product_type: ROD`, whole
bundles for `product_type: WIRE`. These columns support 3 decimal places now (e.g.
`7.400` tons), so don't round them client-side when displaying — a fraction of a ton is
real stock, not a rendering artifact.

`StockTransfer` and the wholesale `StockRequest` fulfillment payload both renamed
**`quantity_cartons` → `quantity`**:

```jsonc
POST /api/inventories/warehouse/transfer
{
  "product_id": 5,
  "quantity": 10,          // was quantity_cartons
  "transferred_by": "...",
  "batch_no": "TRUCK-KJA416XR"   // optional now
}

POST /api/inventories/requests   // fulfilling a wholesale stock request
{
  "request_id": 3,
  "status": "FULFILLED",
  "fulfilled_by": "...",
  "products": [
    { "product_id": 5, "quantity": 10, "batch_no": "..." }  // was quantity_cartons
  ]
}
```

The `expired` / `expiring_soon` / `valid` / `no_expiry` status filter options on
`/api/inventories/storefront` and `/api/inventories/warehouse` are **gone** — don't
render that filter UI anymore.

## 5. Sale flow — this is the part that needs real UI changes

The biggest behavioral change: **a cart line now needs to say how it's being sold.**

```ts
type SaleUnit = "TONS" | "PIECES" | "BUNDLES";
```

- ROD products can be sold **either** by the ton (wholesale/truckload, like a customer
  buying 40 tons) **or** by the piece/length (retail, like 5 lengths of 12mm). The
  cashier needs to pick which one per cart line — this is a new UI decision point that
  didn't exist before (previously every product had exactly one `unit_price`).
- WIRE products are always sold in whole bundles — default `sale_unit` to `"BUNDLES"`
  and there's no toggle needed.

When adding a cart line, send `sale_unit` in the cart item and price it accordingly:
- `sale_unit: "TONS"` → use `product.price_per_ton`
- `sale_unit: "PIECES"` → use `product.unit_price`
- `sale_unit: "BUNDLES"` → use `product.unit_price`

If you omit `sale_unit`, the backend defaults it (`BUNDLES` for WIRE, `PIECES` for ROD)
— but you should show the picker explicitly for ROD products so the cashier isn't
surprised by which price got applied.

```jsonc
// POST /api/transactions/create
{
  "cart": [
    { "id": 5, "quantity": 2, "price": 438000, "sale_unit": "TONS" },
    { "id": 5, "quantity": 3, "price": 87000,  "sale_unit": "PIECES" },
    { "id": 9, "quantity": 1, "price": 68000,  "sale_unit": "BUNDLES" }
  ],
  "total": 1205000,
  "user_id": 1,
  "cashier": "...",
  "sale_type": "RETAIL"
}
```

```jsonc
// Quick single-tap add: POST /api/orders
{
  "product_id": 5,
  "transaction_id": 12,
  "sale_type": "RETAIL",
  "sale_unit": "PIECES"   // optional, same default rule as above
}
```

Every `Order` line returned by the API now includes:
- `sale_unit`: what was picked
- `tons_equivalent`: for ROD lines, the actual tons that will be/were deducted from
  stock (`quantity` directly if `TONS`, `quantity / pieces_per_ton` if `PIECES`, `null`
  for WIRE). Useful if you want to show "≈0.6 tons" next to a piece-sale line item.

**Stock deduction on payment** (`POST /api/transactions/{id}/pay`) now deducts
`tons_equivalent` for ROD orders and raw `quantity` (bundles) for WIRE orders — no
frontend change needed here, just know that a 3-piece sale on a 5-pieces-per-ton product
correctly takes 0.6 tons off the shelf, not 3 (tons).

## 6. Quick checklist for the frontend agent

- [ ] Product create/edit forms: branch the form on `product_type` (ROD vs WIRE), per
      the field table in §3. Drop any carton/units-per-carton/expiry/bulk-price fields.
- [ ] Add a Brand picker (new `/api/brands` endpoints) to product forms and product
      list filters.
- [ ] POS cart line UI: for ROD products, add a TONS/PIECES toggle per line that swaps
      which price (`price_per_ton` vs `unit_price`) is applied; WIRE stays
      single-price.
- [ ] Inventory screens: relabel quantity columns as "Tons" or "Bundles" depending on
      `product.product_type`; remove expiry-related columns/filters/badges.
- [ ] Stock transfer / wholesale fulfillment forms: rename any `quantity_cartons` field
      to `quantity`.
- [ ] Low-stock badges: use `product.store_stock_status` as returned by the API rather
      than re-deriving a threshold client-side (the default threshold differs by
      product type: 2 tons vs 5 bundles, unless `low_stock_threshold` is set).
- [ ] Consider surfacing `pieces_available` on rod product cards ("≈37 lengths in
      stock") since tons alone isn't an intuitive retail-facing number.
