# C4 — Components (Level 3) — TinyPOS (`tnx-pos`)

> Produced by the Reversa **Architect** (phase: interpretation) · doc_level: `complete`
> Generated on 2026-09-18

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

This view opens the **Web Application** container. TinyPOS has no service layer — business logic lives in **fat resource controllers** and **rich Eloquent models** that act as domain services. The diagram focuses on the high-complexity core (POS → Orders → Customers → Debt/Loyalty/Statistics); the thin reference-data editors are grouped.

---

## Components

### Controllers (`app/Http/Controllers/`)

| Component | Responsibility | Complexity | Confidence |
|-----------|----------------|:----------:|------------|
| **PosController** | Renders the terminal; `pos/scan` lookup; builds per-product unit payload; injects the ~630-line cart JS. | high | 🟢 |
| **OrderController** | Totalling engine (`store`/`update`), point award, POS-debt posting, `print`, `destroy` (reverses side effects). | high | 🟢 |
| **CustomerController** | CRM CRUD + POS quick-add, purchase history, statistics read, points/gift redemption, debt & repayment writes. | high | 🟢 |
| **ProductController** | Catalogue CRUD, unit-conversion setup, `get-price` (POS pricing endpoint). | medium | 🟢 |
| **DashboardController** | KPI counts + sales chart (dynamic SQL bucketing) + top-10 products. | medium | 🟢 |
| **DebtController** | Read-only `/debts` projection over `customers.debt_total`. | low | 🟢 |
| **Brand/Category/Unit/GiftController** | Encore\Admin `ModelForm` reference-data editors. | low | 🟢 |
| **Auth (Encore\Admin)** | Login/logout/session via `salipropham/laravel55-admin`. (Laravel `Auth/*` scaffolding is dead — non-existent `App\User`.) | low | 🟢 |

### Models as domain services (`app/Models/`)

| Component | Role | Confidence |
|-----------|------|------------|
| **Product** | Catalogue entity + **pricing rule** `getPriceByCustomerType($type,$unitId)` (source of truth); soft-delete rename hook; `generateCode`. | 🟢 |
| **Order** | Sale aggregate; appended `code`/`is_editable`/`debt_locked`; **`summaryLogging()`** batch rollup. | 🟢 |
| **CustomerDebt** | **Ledger service** `record()` / `voidForOrder()` — locked, transactional, append-only A/R. | 🟢 |
| **Customer** | CRM entity; **loyalty** `checkGiftAvailable` / redeem; `reversePointsForOrder` claw-back; `type` pricing tier. | 🟢 |
| **ProductUnit / Unit** | Unit conversion (`conversion_qty`); base unit is a plain string on `Product`. | 🟢 |
| **Gift / CustomerOrderSummary / Brand / Category / Setting / Discount** | Loyalty catalogue; nightly read model; reference data; legacy/unused discount. | 🟢 |

---

## Diagram

```mermaid
flowchart TB
    client["🟩 POS Cart Client<br/>(browser JS)"]
    scheduler["🟧 Scheduler / Cron"]
    db[("🗄️ MariaDB")]
    files["📁 File Storage"]

    subgraph web [Web Application]
        direction TB

        subgraph ctrls [Controllers]
            pos["PosController<br/><i>terminal, scan, unit payload</i>"]
            ord["OrderController<br/><i>totalling, award, debt post, destroy</i>"]
            cust["CustomerController<br/><i>CRM, stats, loyalty, debt writes</i>"]
            prod["ProductController<br/><i>catalogue, get-price</i>"]
            dash["DashboardController<br/><i>KPIs, chart, top-10</i>"]
            debt["DebtController<br/><i>/debts projection</i>"]
            refc["Brand/Category/Unit/GiftController<br/><i>Encore\\Admin editors</i>"]
        end

        subgraph models [Models / domain services]
            mProd["Product<br/><b>getPriceByCustomerType</b>"]
            mOrd["Order<br/><b>summaryLogging</b>"]
            mDebt["CustomerDebt<br/><b>record / voidForOrder</b>"]
            mCust["Customer<br/><b>checkGiftAvailable / reversePoints</b>"]
            mUnit["ProductUnit / Unit"]
            mMisc["Gift · CustomerOrderSummary ·<br/>Brand · Category · Setting · Discount"]
        end
    end

    client -->|"get-price / scan / submit"| pos
    client --> prod
    pos --> ord
    ord -->|"resolve price"| mProd
    ord -->|"post pos_debt"| mDebt
    ord -->|"award points"| mCust
    ord --> mOrd
    ord --> mUnit
    prod --> mProd
    prod --> mUnit
    prod -->|"images"| files
    cust -->|"redeem / reverse"| mCust
    cust -->|"manual debt / repayment"| mDebt
    cust -->|"read snapshot"| mMisc
    dash --> mOrd
    debt --> mCust
    refc --> mMisc
    scheduler -->|"nightly"| mOrd
    mProd --> db
    mOrd --> db
    mDebt --> db
    mCust --> db
    mUnit --> db
    mMisc --> db

    classDef ctrl fill:#1168bd,stroke:#0b4884,color:#fff
    classDef model fill:#4b8bbe,stroke:#2c5a80,color:#fff
    classDef store fill:#2e7d32,stroke:#1b4d20,color:#fff
    classDef ext fill:#999,stroke:#6b6b6b,color:#fff
    class pos,ord,cust,prod,dash,debt,refc ctrl
    class mProd,mOrd,mDebt,mCust,mUnit,mMisc model
    class db,files store
    class client,scheduler ext
```

---

## Component-interaction notes

- **The Order finalisation flow is the architectural spine.** `OrderController::store/update` orchestrates three domain services in one request: `Product::getPriceByCustomerType` (pricing) → `Customer` points award (guarded by `points_awarded_at`) → `CustomerDebt::record('pos_debt')` (guarded by `debt_locked`). `destroy` runs the inverse (`reversePointsForOrder` + `CustomerDebt::voidForOrder`) in a transaction. 🟢
- **Pricing has a single source of truth** — `Product::getPriceByCustomerType` — consumed by `ProductController::getPriceByCustomerType` (the POS `get-price` endpoint) **and** `OrderController` totalling. A pricing change is therefore contained to one model method (good), but the totalling that *calls* it is duplicated across `store`/`update` (debt #1). 🟢
- **Debt is funnelled through one method** — every `pos_debt` / `manual_debt` / `repayment` / `debt_void` goes through `CustomerDebt::record` under a row lock. `DebtController` and the `/debts` screen never write. 🟢
- **Reference-data controllers bypass `app/` for persistence** — `store`/`update`/`destroy` come from the vendored Encore\Admin `ModelForm` trait; only `form()`/`grid()` overrides in `app/` govern validation. 🟡
- **The scheduler reaches only `Order::summaryLogging`** — a clean batch seam, the one asynchronous path in the system. 🟢

See `erd-complete.md` for the data these components read/write and `traceability/spec-impact-matrix.md` for change blast-radius.
</content>
