# 02 — Catalog

The catalog is **global master data with per-branch overrides**. The menu is defined once
by head office; each branch controls only its own price, availability and today's sold-out
list. This keeps five branches consistent without forcing them to be identical.

```
Category ──▶ Item ──▶ ModifierGroup ──▶ ModifierOption
                │
                └──▶ Recipe ──▶ Ingredient (inventory item)

Item ──▶ BranchItemOverride (price, availability, 86'd)  [per branch]
```

---

## 1. Category

| Field | Notes |
|---|---|
| `name_ar` / `name_en` | Both required |
| `sort_order` | Controls POS button layout |
| `color` / `icon` | POS visual grouping — real speed gain on a touch screen |
| `is_active` | |

Typical: Hot Coffee, Cold Coffee, Tea, Fresh Juice, Soft Drinks, Water, Pastry,
Sandwiches, Desserts, Snacks, Cigarettes (common in Iraqi workspace cafes), Other.

Categories are **flat, one level**. Sub-categories are not needed at this scale and slow
down the POS.

---

## 2. Item

| Field | Notes |
|---|---|
| `sku` | Unique, stable, human-readable |
| `name_ar` / `name_en` | |
| `category_id` | |
| `base_price` | Default selling price, org-wide (see Pricing below) |
| `tax_class` | Reserved; see open question on VAT in doc 00 |
| `item_type` | `prepared` (made to order) or `retail` (sold as-is, e.g. bottled water) |
| `prep_station` | `bar` / `kitchen` / `none` — routes to the preparation queue |
| `is_stock_tracked` | `retail` items deduct themselves; `prepared` items deduct via recipe |
| `image` | Optional; helps new cashiers |
| `sort_order`, `is_active` | |

### Item type matters

- **`retail`** — one inventory unit consumed per unit sold. Simple.
- **`prepared`** — consumes ingredients through a **recipe**. This is what makes COGS and
  stock deduction automatic rather than a monthly guess.
- **`none` prep station** — goes straight to the customer, never appears on the bar screen.

---

## 3. Modifiers

Modifiers are where a cafe's complexity actually lives. Model them properly at the start;
retrofitting them is expensive.

### ModifierGroup

| Field | Notes |
|---|---|
| `name_ar` / `name_en` | e.g. "الحجم / Size", "الحليب / Milk", "السكر / Sugar" |
| `selection_type` | `single` (radio) or `multiple` (checkbox) |
| `min_select` / `max_select` | `Size` = min 1, max 1. `Extras` = min 0, max 4. |
| `is_required` | If true the POS blocks adding the item until chosen |
| `sort_order` | |

### ModifierOption

| Field | Notes |
|---|---|
| `name_ar` / `name_en` | e.g. "وسط / Medium", "حليب لوز / Almond milk" |
| `price_delta` | May be zero, positive, or negative. Signed. |
| `recipe_delta` | Ingredient additions/substitutions this option causes |
| `is_default` | Preselected when the item is added — the single biggest POS speed win |
| `is_active` | |

**Rules**
- Groups are **reusable across items** (`Size` attaches to every hot drink).
- Attachment is per item, with a per-item override of `is_required` and defaults.
- `price_delta` and `name` are **snapshotted onto the order line** at sale time.
- A modifier can be `86'd` per branch independently of the item (e.g. almond milk is out,
  but latte is still available).

### Worked example

```
Item: Latte              base 3,000 IQD
├─ Group "Size"          single, required
│   ├─ Small    +0        (default)
│   ├─ Medium   +1,000
│   └─ Large    +2,000
├─ Group "Milk"          single, required
│   ├─ Regular  +0        (default)
│   └─ Almond   +1,000    (recipe_delta: -150ml regular milk, +150ml almond)
└─ Group "Extras"        multiple, 0–3
    ├─ Extra shot +750    (recipe_delta: +7g coffee beans)
    ├─ Vanilla    +500
    └─ Caramel    +500

Large + Almond + Extra shot = 3,000 + 2,000 + 1,000 + 750 = 6,750 IQD
```

---

## 4. Pricing

Price resolution order, first match wins:

```
1. BranchItemOverride.price      (branch-specific price)
2. Item.base_price               (org default)
```

Then add the sum of selected `ModifierOption.price_delta`
(also branch-overridable, same order).

| Rule | Reason |
|---|---|
| **The resolved unit price is copied onto the order line.** | Reports of past sales must never re-join to today's price list. |
| **Price changes are versioned with `effective_from`.** | Lets you answer "what did a latte cost in March?" and prevents a price edit from rewriting history. |
| **A branch price override requires `catalog.edit_branch_overrides`.** | Stops cashiers inventing prices. |
| **Every price change is audited.** | See doc 01 §4. |

### Price lists (optional, Phase 2+)

A named price list lets the same item carry a different price for a class of buyer —
for example a negotiated **corporate rate** for a large tenant, or a **staff price**.

```
PriceList { name, applies_to: account | account_group | staff, valid_from, valid_to }
PriceListEntry { price_list_id, item_id, price }
```

Resolution becomes: `PriceListEntry` → `BranchItemOverride` → `Item.base_price`.

This is the correct place to express "company X gets 10% off" — as a price list or an
account-level discount, never as a manual discount typed by the cashier.

---

## 5. Availability

Three independent switches, all needed:

| Switch | Scope | Who sets it | Meaning |
|---|---|---|---|
| `Item.is_active` | Global | Owner | Delisted everywhere; historical sales keep working |
| `BranchItemOverride.is_available` | Branch | Manager | This branch does not sell it at all |
| `BranchItemOverride.is_86d` + `until` | Branch, temporary | Cashier / Barista | Sold out today; auto-clears at the next business day |

`86`ing must be doable **in one tap from the POS and from the bar screen**, because it is
done mid-rush by whoever notices the milk ran out.

Optional automation: auto-86 when the recipe's ingredient stock reaches zero.
Recommended as a warning, not a hard block — stock counts drift, and blocking a sale on a
bad stock number costs real money.

---

## 6. Recipes

| Field | Notes |
|---|---|
| `item_id` | |
| `ingredient_id` | An inventory item (doc 07) |
| `quantity` | In the ingredient's base unit (g, ml, piece) |
| `is_optional` | Excluded from mandatory stock deduction |
| `yield` | Portions produced, for batch recipes (e.g. one 1L jug of syrup) |

**Uses**
1. **Automatic stock deduction** at sale time — the only sustainable way to track inventory
   in a cafe.
2. **Theoretical COGS** per item → gross margin per item → the "what should I promote"
   report.
3. **Theoretical vs actual variance** at stock count → the shrinkage/waste number.

**Rules**
- Recipes are global; ingredient **cost** is per branch (each branch buys at its own price).
- A recipe change is versioned with `effective_from`, so historical COGS stays correct.
- Sub-recipes are supported one level deep (a syrup batch used by drinks). Deeper nesting
  is out of scope.
- Recipes are **optional per item**. An item with no recipe simply does not deduct stock —
  useful when starting up before recipes are entered.

---

## 7. Combos / offers (Phase 3)

Kept deliberately simple:

- **Fixed combo** — "Coffee + croissant = 5,000 IQD". A `combo` item whose components are
  listed for stock deduction and reported as its components for COGS.
- **Time-window promotion** — a price list valid `08:00–10:00` for a morning offer.
- **Loyalty free item** — see doc 03 §7.

Nothing more complex (BOGO trees, cart-level conditional rules) is justified at this scale.
