# 10 — Cross-Cutting Rules

Rules that apply everywhere. Getting these wrong is expensive to fix later, so they are
decided once, here, before any code is written.

---

## 1. Identifiers

| Rule | Reason |
|---|---|
| **Primary keys are UUIDs generated on the device that creates the record.** | Five branches creating records offline cannot share an auto-increment sequence without collisions. |
| **Human-facing numbers are separate from primary keys.** | `order_number`, `invoice_number`, `receipt_number`, `po_number` are for humans; UUIDs are for the system. |
| **Order numbers:** `<BRANCH_CODE>-<YYYYMMDD>-<seq>`, resetting daily per branch. | Generated offline without coordination; instantly readable ("that's a Karbala order from the 4th"). |
| **Invoice numbers are gapless and issued centrally.** | Billing runs centrally (doc 05 §3), so a single sequence is safe and gaplessness is often an accounting requirement. |
| **No identifier is ever reused**, including after cancellation. | |

---

## 2. Money

### Storage

```
money { amount: integer, currency: 'IQD' | 'USD' }
```

- **Never floating point.** Ever.
- **`IQD` is stored as whole dinars** (scale 0). There is no circulating subunit; a "fils"
  column would be permanently zero and invite rounding bugs.
- **`USD` is stored in cents** (scale 2).
- Currency scale is a property of the currency, held in one place.

### Cash rounding — a real requirement in Iraq

The smallest practically circulating note is **250 IQD**. Prices and totals must round to a
denomination that can actually be handed over.

| Rule |
|---|
| `cash_rounding_increment` is configured per branch (default **250 IQD**) |
| Rounding applies **only to the cash tender**, never to the invoice or the line items |
| The difference is recorded as an explicit `cash_rounding` amount on the order |
| Rounding differences are reported and are an expected component of shift variance tolerance (doc 06 §3) |

Applying rounding to the invoice total instead of the tender causes account balances that
never reconcile with the sum of their charges.

### Multi-currency

| Rule | Reason |
|---|---|
| **Every branch has a base currency (IQD).** All reporting normalises to it. | Comparing branches requires one unit. |
| **`fx_rate` is snapshotted on every transaction that crosses currencies**, together with the IQD equivalent. | The rate moves; historical records must not move with it. |
| **The active rate is set centrally with an optional branch override**, and is versioned with `effective_from`. | Rates differ by city and change frequently. |
| **Change is given in IQD by default**, regardless of tender currency. | |
| **Rent contracts and imported supplies quoted in USD keep their contract currency** and are converted for reporting only. | Doc 08 §2. |

---

## 3. Dates and time

| Rule |
|---|
| All timestamps stored in **UTC**; displayed in the branch's local time |
| All branches are **`Asia/Baghdad` (UTC+3, no DST)** — but timezone is stored per branch anyway |
| **`business_day` is a distinct stored field**, derived from the branch's `business_day_cutoff` (e.g. 04:00). A sale at 01:30 belongs to the previous business day. |
| **All reporting groups by `business_day`, never by calendar timestamp** |
| **There is no weekend.** Every day is a trading day unless the owner declares otherwise — see §3.1. No report, cycle or schedule may assume a weekly off-day pattern. |
| The **Hijri date** may be shown alongside the Gregorian date on documents; Gregorian is authoritative for all calculations |

### 3.1 Business calendar — owner-declared closures

The business trades **every day**. Non-working days exist only when the owner declares one,
usually at short notice ("tomorrow there is no work"). This is a **calendar of exceptions**,
not a recurring pattern.

```
calendar_exception {
  scope: ORG | BRANCH,  branch_id?,
  date,                              // or date_from / date_to for a range
  status: CLOSED | REDUCED_HOURS,
  reason: owner_declared | public_holiday | religious_holiday
        | maintenance | security | force_majeure,
  is_paid_for_staff: bool,           // default true — doc 08 §4
  declared_by_user_id, declared_at, note
}
```

| Rule | Reason |
|---|---|
| **Declaring a closure takes one action by the Owner** and is pushed to the affected branches' terminals immediately. | It is announced the evening before. It cannot require a back-office session. |
| A closure may be **org-wide or per branch**. | A Karbala-only closure is as likely as a national one. |
| **A closed day still exists as a `business_day` with zero sales.** It is never skipped or deleted. | Deleting it silently inflates every average that divides by day count. |
| **Reporting comparisons skip closed days.** "Yesterday vs the same day last week" compares the nearest *trading* day and states which day it used. | Comparing a trading day to a closed day produces a meaningless −100%. |
| **Averages divide by trading days, not calendar days.** | Same reason. |
| **Standing orders do not fire on a closed day** (doc 04 §8). | Nobody should be charged for coffee delivered to an empty office. |
| **Billing cycle grids run on calendar days regardless of closures.** A `MONTHLY` cycle still closes on its anchor; a closed day simply contributes no charges. | Tenants expect invoices on predictable dates. Shifting the grid for closures makes them unpredictable. |
| **A `DAILY`-cycle account produces no invoice for a closed day** — it falls under the existing zero-activity rule (doc 05 §3). | |
| **Facts beat the calendar.** If a branch trades on a day marked closed, the sales are valid and the day is reclassified as trading, with the discrepancy reported. | The calendar is a plan; sales are evidence. |
| Closures are **audited** — who declared it, when, and how far in advance. | |
| Declaring a closure **retroactively** requires Owner approval and is flagged, because it moves reported figures. | |

Public and religious holidays are entered as ordinary calendar exceptions. They are not a
separate mechanism — the owner still decides whether the cafe opens.

---

## 4. Offline-first and synchronization

The defining technical constraint: **a branch must keep selling with no internet.**

### What must work fully offline

Selling, printing, modifiers, discounts within limits, voids, shift open/close, cash
movements, the prep queue, waste recording, stock deduction, **and charging to an account
using the locally cached balance**.

### What requires connectivity

Billing runs, cross-branch reporting, master data changes, payroll, user administration,
credit-limit changes, and any cross-branch account view.

### Sync model

```
Device (local store)
   │  append-only outbox of domain events
   ▼
Sync queue ──▶ Centre ──▶ authoritative store ──▶ rollups & reports
   ▲                          │
   └──── master data pull ◀───┘   (catalog, accounts, users, prices)
```

| Rule | Reason |
|---|---|
| **Sales flow up; master data flows down.** No two-way merge on the same entity. | Removes almost all conflict cases by construction. |
| **Every event carries an idempotency key.** Re-delivery never double-posts. | Retries after a dropped connection are normal, not exceptional. |
| **Events are append-only and ordered per device.** | Gives a deterministic replay. |
| **The centre never rejects a synced sale.** It accepts and flags. | Refusing a sale that already happened, and for which the customer already has coffee, is not a real option. |
| **Late arrivals post to the next open period** with a `late_arrival` flag (docs 05 §3, 06 §5). | Reopening a closed day or invoice makes every prior report unstable. |
| **Master data changes carry a version**; devices apply them at the next quiet moment, never mid-order. | A price changing between "add item" and "pay" is a real bug source. |
| **Sync status is visible on the POS**, with the count of unsynced records and time since last sync. | Staff need to know; managers need to notice a device that has been dark for two days. |
| **A device offline beyond N hours raises a central alert.** | An offline terminal is unmonitored revenue. |

### Known conflict cases and their resolutions

| Case | Resolution |
|---|---|
| Two branches charge the same cross-branch account beyond its limit while offline | Both accepted; both flagged `provisional`; surfaced on **Credit Exceptions** |
| Loyalty reward redeemed twice offline | Both honoured; flagged for review. One free coffee costs less than refusing a customer. |
| Stock driven negative by offline sales | Accepted; flagged; corrected at the next stock count |
| A price changed centrally after an offline sale used the old price | The **snapshotted** price on the order line wins, always |
| The same order syncs twice | Rejected by idempotency key; no duplicate |

---

## 5. Data integrity

1. **Financial documents are immutable.** Orders, charges, invoices, payments, payroll runs
   and stock movements are never edited after commit. Corrections are new opposing records.
2. **Balances and quantities are derived, not stored as mutable totals.** Cache them for
   speed, but they must always be recomputable from the underlying ledger. A stored total
   that can disagree with its own history is the defining bug of this class of system.
3. **Soft delete only.** Nothing operational is hard-deleted; records are deactivated.
4. **Every mutation is audited** (doc 01 §4).
5. **Referential integrity is enforced**, including across the sync boundary — an order
   referencing an unknown account is quarantined, not silently accepted.

---

## 6. Localization

| Rule |
|---|
| **Arabic is the primary UI language; English is secondary.** Full **RTL** layout, not a mirrored afterthought. |
| Every user-facing entity carries `name_ar` and `name_en`. Arabic is required. |
| Receipts, statements and invoices print **Arabic-first**, optionally bilingual |
| **Arabic-Indic numerals (٠١٢٣) are a display preference**; storage and calculation always use Western digits |
| Phone numbers stored in **E.164** (`+964...`), displayed locally |
| Search must be **Arabic-normalization-aware** — أ/إ/ا, ة/ه, ى/ي, and diacritics must not prevent a match when a cashier types a tenant's name |
| Fonts must render Arabic correctly on **receipt printers**, which is a real constraint on thermal hardware and must be verified before hardware is bought |

---

## 7. Security & privacy

| Rule |
|---|
| PINs and passwords are **hashed**, never recoverable |
| Local device databases are **encrypted at rest** — a stolen terminal contains the tenant list and their balances |
| All sync traffic over **TLS**; devices authenticate with a per-device credential that can be revoked centrally |
| A lost or stolen device can be **remotely deauthorized**, blocking sync and wiping local data on next contact |
| Role-based access is enforced **server-side**, never only in the UI |
| Customer data (tenant contacts, phone numbers, consumption history) is only visible to roles that need it; cashiers see balances, not full histories |
| Backups are automatic, encrypted, off-site, and **restore-tested on a schedule** — an untested backup is not a backup |

---

## 8. Non-functional targets

| Aspect | Target |
|---|---|
| POS action response (add item, settle) | < 100 ms, local |
| Account search on device | < 200 ms |
| Order completion end-to-end | < 3 seconds including print |
| Offline operation | Indefinite; must survive a multi-day outage |
| Sync latency when online | < 60 seconds |
| Concurrent devices per branch | **1** — one Windows PC per branch, confirmed (§9) |
| Branch recovery after PC failure | Trading again within 1 hour, using a rehearsed procedure (§9.1) |
| Central reporting query | < 3 seconds for a 12-month, 5-branch comparison (via rollups) |
| Data retention | Financial records retained indefinitely; audit log indefinitely |
| Recovery point objective | ≤ 5 minutes of central data |

---

## 9. Hardware assumptions (to confirm with the stack decision)

Listed here because they constrain functionality and must be settled before Phase 1 ships,
not after.

**Confirmed:** each branch has **one Windows PC, and it is the only terminal.** That PC is
the whole branch system — till, local database and sync client in one machine.

| Item | Note |
|---|---|
| Branch PC | Windows. Runs the application and the local database; the cashier works directly on it. One per branch. |
| Receipt printer | Thermal 80mm, **verified Arabic rendering** before committing to five units |
| Cash drawer | Opened by the printer; every open is logged |
| Label printer | Optional, for multi-drink office orders |
| Prep-queue screen | **Not applicable at one terminal** — see doc 04 §5. The prep queue is a printed ticket or a panel on the same screen. |
| Barcode scanner | Only if retail items justify it |
| Power | **UPS is not optional in Iraq**, and with a single terminal it is the branch's only protection. Grid interruption during a sale must not corrupt the local database. |
| Connectivity | Assume unreliable. This is the premise of §4, not an edge case. |

### 9.1 Single-terminal consequences

| Consequence | Handling |
|---|---|
| **No device concurrency.** One terminal means one writer. | Removes intra-branch conflict entirely. Local writes are strictly serial. |
| **The branch PC is a single point of failure.** There is no second till to fall back on. | Recovery must be a rehearsed procedure, not an improvisation — see below. |
| **Multiple cashiers share one machine.** | PIN-per-action and shift handover (doc 01 §2, doc 06 §1) become more important, not less. |
| **`device_id` is effectively `branch_id`.** | Keep the field distinct anyway — a replacement PC is a new device, and that boundary matters for audit. |

**Branch recovery procedure** — because every confirmed sale syncs to the centre, a dead PC
loses only what had not yet synced:

```
1. Install the application on the replacement PC
2. Authenticate it as a new device for that branch (old device deauthorized centrally)
3. Pull master data + the branch's synced history from the centre
4. Reconcile the last open shift by hand against the drawer and printed receipts
5. Resume trading
```

This must be **tested before go-live**, not discovered during an outage. Target: back
trading within one hour. A daily local database backup to a USB or synced folder shortens
step 3 and recovers anything that had not yet reached the centre.
