# Flowchart — `customers`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-17

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

## `CustomerController::store` — create (web + POS quick-add) 🟢

```mermaid
flowchart TD
    A["POST /customers"] --> B["validate<br/>(fullname, phone numeric unique-live,<br/>gender in male/female/other, birthday d/m/yyyy regex)"]
    B --> C{"parse birthday d/m/Y → birthday2 (Y-m-d)"}
    C -->|ok| D["birthday2 set"]
    C -->|fail| E["catch: birthday = null"]
    D --> F["Customer::create"]
    E --> F
    F --> G{"request expectsJson?"}
    G -->|yes POS| H["JSON customer (or 400)"]
    G -->|no| I["toastr + redirect customers.index"]
```

## `CustomerController::redeemRewardPoints` — gift redemption ✅ (fixed: now atomic)

```mermaid
flowchart TD
    A["POST /customers/id/redeem-points<br/>(gift_id, note)"] --> B["Gift::active()->find(gift_id)"]
    B --> C["customer.checkGiftAvailable(gift)"]
    C --> D{"available?}"}
    D -->|no| E["back with error<br/>(unavailable / over limit / not enough points)"]
    D -->|yes| T["DB::transaction: lockForUpdate(customer, gift)<br/>+ re-check checkGiftAvailable under lock"]
    T --> F["gifts()->attach(gift, {points, note})"]
    F --> G["gift.used += 1"]
    G --> H["customer.points -= gift.points"]
    H --> I["toastr success, redirect back"]
```

✅ Fixed (2026-09-17): steps F–H now run inside `DB::transaction()` with `lockForUpdate()` on both rows, plus a re-check under the lock — a failure or concurrent redemption can no longer desynchronise pivot / `used` / `points`.

## Debt entry points → `CustomerDebt::record` (locked) 🟢

```mermaid
flowchart TD
    A1["POST /customers/id/debts<br/>(manual_debt)"] --> R
    A2["POST /customers/id/repayments<br/>(repayment)"] --> R
    A3["orders store/update<br/>(pos_debt)"] --> R
    R["CustomerDebt::record(customer, type, amount, ...)"]
    R --> T["DB::transaction + lockForUpdate(customer)"]
    T --> U{"type == repayment?"}
    U -->|yes| V{"amount > debt_total?"}
    V -->|yes| W["throw (repayment exceeds balance)"]
    V -->|no| X["debt_total -= amount"]
    U -->|no| Y["debt_total += amount"]
    X --> Z["insert ledger row (balance_after) + save customer"]
    Y --> Z
```

## `CustomerController::statis` — precomputed statistics 🟡

```mermaid
flowchart TD
    A["GET /customers/id/statistic"] --> B["read CustomerOrderSummary row<br/>(built nightly by Order::summaryLogging)"]
    B --> C["try: unpack categories_statistic JSON"]
    C --> D["split milk (cat 1) / medicine (cat 8) / other (aggregated)"]
    C -->|exception| E["degrade to empty collections"]
    D --> F["render customer-statis (amount/owe/debt/points totals)"]
    E --> F
```

**Notes:**
- `scan` (`phone`/`fullname LIKE`, take 10) always returns a 200 collection; its `204` branch is dead (a Collection is always truthy). 🟢 Confirmed harmless by the team — left as-is, not fixed.
- `orders` lists a customer's orders with an optional `category` filter (`whereHas('products')`) and `amountTotal = Σ total`. 🟢
- `debt` paginates the `CustomerDebt` ledger and shows the live `debt_total`. 🟢
