# System Architecture — TinyPOS (`tnx-pos`)

> Extracted from verified Reversa Architect artifacts (generated 2026-09-18).
> Confidence: 🟢 CONFIRMED (read from code) · 🟡 INFERRED (pattern-based) · 🔴 GAP (needs validation)

---

## Architectural summary

TinyPOS is a **single-store, single-operator Point-of-Sale and back-office** for a neighbourhood shop. It is a **server-rendered Laravel monolith** (Laravel 5.6.39, PHP 7.4.33) built on the `salipropham/laravel55-admin` fork of Encore\Admin, which supplies authentication, an RBAC schema (seeded but unenforced) and the reference-data CRUD screens. 🟢

Three structural characteristics define the architecture:

1. **Thick monolith, thin client.** All business logic runs server-side. The exception is the cashier terminal: `PosController` injects a ~630-line JavaScript cart engine into the page via `Admin::script()`, making the POS cart a browser-resident component with no separate build artefact. 🟢
2. **Domain-by-convention, not by folders.** Standard Laravel technical layering (`Controllers/`, `Models/`). Domains (products, orders, customers, debts, loyalty, reference data) are expressed through resource controllers and Eloquent models grouped by naming. 🟢
3. **Precomputed read models.** Per-customer statistics are rebuilt nightly by `Order::summaryLogging()` into the `customer_order_summary` table. Same-day sales are invisible until the rollup runs. 🟢

---

## C4 Level 1 — System Context

```mermaid
flowchart TB
    admin["👤 Store Administrator\n(cashier / owner)\nthe only system user"]
    shopper["👤 Shopper / Customer\nserved in-store; CRM record only,\nno login"]

    subgraph tinypos_boundary [" "]
        tinypos["🟦 TinyPOS\nPoint-of-Sale & back-office\nLaravel 5.6 monolith (PHP 7.4, MariaDB)\nsales, catalogue, customers,\nloyalty, debt, statistics"]
    end

    scanner["🔌 Barcode scanner\nin-store peripheral"]
    printer["🖨️ Receipt printer\nin-store peripheral"]
    cron["⏱️ System cron\nartisan schedule:run"]
    ci["🚀 GitLab CI\npos-dev deploy job"]

    admin -->|"operates terminal & back-office\n(HTTPS, session auth)"| tinypos
    admin -->|"serves"| shopper
    scanner -->|"scans product / customer codes\n→ /pos/scan, /customers/scan, /orders/scan"| tinypos
    tinypos -->|"renders receipt\nGET /orders/{id}/print"| printer
    cron -->|"triggers nightly rollup\nOrder::summaryLogging()"| tinypos
    ci -->|"deploys & installs cron"| tinypos
    shopper -.->|"data captured as CRM record\n(pricing tier, points, debt)"| tinypos

    classDef system fill:#1168bd,stroke:#0b4884,color:#fff
    classDef person fill:#08427b,stroke:#052e56,color:#fff
    classDef ext fill:#999,stroke:#6b6b6b,color:#fff
    class tinypos system
    class admin,shopper person
    class scanner,printer,cron,ci ext
```

### Context actors

| Element | Kind | Notes | Confidence |
|---------|------|-------|------------|
| **Store Administrator** | Person | Only system user. Single Encore\Admin `Administrator` role — authorization is authenticate-only. | 🟢 |
| **Shopper / Customer** | Person (indirect) | No login. Represented as a CRM record for pricing tier, points, and debt. | 🟢 |
| **Barcode scanner** | Peripheral | Feeds codes into `scan` endpoints (exact-code lookup + `LIKE` fallback). | 🟡 |
| **Receipt printer** | Peripheral | Renders `GET /orders/{order}/print`. | 🟢 |
| **System cron** | Operational | Fires `artisan schedule:run` every minute; drives the daily statistics rollup. | 🟢 |
| **GitLab CI** | Operational | `pos-dev` deploy job redeploys the app and reinstalls the cron file. | 🟢 |

There are **no external software integrations** — no payment gateway, third-party API, or messaging provider is present in configuration or dependencies. 🟢

---

## C4 Level 2 — Containers

```mermaid
flowchart TB
    admin["👤 Store Administrator"]
    scanner["🔌 Barcode scanner"]
    printer["🖨️ Receipt printer"]
    cronsys["⏱️ System cron"]

    subgraph tinypos [TinyPOS]
        direction TB
        web["🟦 Web Application\nLaravel 5.6 · PHP 7.4 · Apache/mod_php\nEncore\\Admin shell\ncontrollers + Eloquent models,\nall business logic"]
        client["🟩 POS Cart Client\ninjected JS (Vue2/jQuery/Bootstrap)\nclient-side cart, live re-pricing,\ndebt rules, submit"]
        sched["🟧 Scheduler / Cron\nartisan schedule:run → Kernel\ndaily Order::summaryLogging()"]
        db[("🗄️ Database\nMariaDB 10.11\nall entities + debt ledger + RBAC")]
        files["📁 Local File Storage\npublic disk · intervention/image\nproduct & gift images"]
    end

    admin -->|"HTTPS, session (admin guard)"| web
    web -->|"serves page + injects cart JS"| client
    client -->|"AJAX get-price / scan;\nPOST /orders (submit)"| web
    scanner -->|"codes → scan endpoints"| client
    web -->|"render print view"| printer
    cronsys -->|"every minute"| sched
    sched -->|"batch rollup writes"| db
    web -->|"Eloquent read/write"| db
    web -->|"read/write/resize images"| files

    classDef person fill:#08427b,stroke:#052e56,color:#fff
    classDef cont fill:#1168bd,stroke:#0b4884,color:#fff
    classDef store fill:#2e7d32,stroke:#1b4d20,color:#fff
    classDef ext fill:#999,stroke:#6b6b6b,color:#fff
    class admin person
    class web,client,sched cont
    class db,files store
    class scanner,printer,cronsys ext
```

### Containers

| Container | Technology | Responsibility | Confidence |
|-----------|-----------|----------------|------------|
| **Web Application** | Laravel 5.6.39 · PHP 7.4.33 · Apache + mod_php; Encore\Admin shell | HTTP routing, session auth, Blade rendering, all business logic (controllers + Eloquent models). Single deployable. | 🟢 |
| **POS Cart Client** | Injected JS via `Admin::script()` — Vue 2 / jQuery / Bootstrap 4 built by Laravel Mix | Client-side cart assembly, live re-pricing on customer change, client-side debt rules, order submission to `/orders`. No standalone build artefact. | 🟢 |
| **Scheduler / Cron** | `php artisan schedule:run` (system cron, every minute) → `App\Console\Kernel` | Runs the daily `Order::summaryLogging()` rollup into `customer_order_summary`; also exposed as the `CustomerOrderSummaryLogging` artisan command. | 🟢 |
| **Database** | MariaDB 10.11.13 (Eloquent, `mysql` driver) | Persistent store for all entities, the append-only debt ledger, and the RBAC tables. | 🟢 |
| **Local File Storage** | Laravel `public` disk; `intervention/image` | Stores and resizes product/gift images (`fit(300,300)`); served as public assets. | 🟢 |

### Container interaction notes

- **Web ↔ POS Cart Client is a chatty, synchronous coupling.** The client fetches prices synchronously per line via `GET /products/get-price` and looks up products via `GET /pos/scan`. The server is the pricing source of truth; the client mirrors debt rules for UX but the server re-enforces them. 🟢
- **Scheduler is decoupled and eventually-consistent.** Statistics reads (`CustomerController::statis`) are only as fresh as the last nightly run; same-day sales are invisible until the rollup. 🟢
- **Single database, no cache or queue container.** `config/cache.php` and `config/queue.php` exist but no Redis or queue worker is deployed; all work is synchronous request-time or the nightly cron. 🟡
- **File storage is local, not object storage.** Image cleanup on product update targets the wrong root (`app/public2`) and silently fails — GAP-P1. 🔴

---

## C4 Level 3 — Components (Web Application)

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

    subgraph web [Web Application]
        direction TB

        subgraph ctrls [Controllers]
            pos["PosController\nterminal, scan, unit payload"]
            ord["OrderController\ntotalling, award, debt post, destroy"]
            cust["CustomerController\nCRM, stats, loyalty, debt writes"]
            prod["ProductController\ncatalogue, get-price"]
            dash["DashboardController\nKPIs, chart, top-10"]
            debt["DebtController\n/debts projection"]
            refc["Brand/Category/Unit/GiftController\nEncore\\Admin editors"]
        end

        subgraph models [Models / domain services]
            mProd["Product\ngetPriceByCustomerType"]
            mOrd["Order\nsummaryLogging"]
            mDebt["CustomerDebt\nrecord / voidForOrder"]
            mCust["Customer\ncheckGiftAvailable / reversePoints"]
            mUnit["ProductUnit / Unit"]
            mMisc["Gift · CustomerOrderSummary ·\nBrand · 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
```

### 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 and 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 — references non-existent `App\User`. | low | 🟢 |

### Models as domain services

| Component | Role | Confidence |
|-----------|------|------------|
| **Product** | Catalogue entity + pricing rule `getPriceByCustomerType($type,$unitId)`; soft-delete rename hook; `generateCode`. | 🟢 |
| **Order** | Sale aggregate; appends `code`/`is_editable`/`debt_locked`; `summaryLogging()` batch rollup. | 🟢 |
| **CustomerDebt** | Ledger service `record()` / `voidForOrder()` — locked, transactional, append-only accounts receivable. | 🟢 |
| **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. | 🟢 |

### 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 in a transaction. 🟢
- **Pricing has a single source of truth** — `Product::getPriceByCustomerType` — consumed by both the POS `get-price` endpoint and `OrderController` totalling. The totalling logic that calls it is duplicated across `store`/`update` (technical debt item #1). 🟢
- **All debt writes are funnelled through one method** — every `pos_debt` / `manual_debt` / `repayment` / `debt_void` goes through `CustomerDebt::record` under a row lock. 🟢
- **The scheduler reaches only `Order::summaryLogging`** — the one asynchronous path in the system. 🟢

---

## Technology stack

| Layer | Technology | Version | Confidence |
|-------|-----------|---------|------------|
| Runtime | PHP | 7.4.33 (EOL) | 🟢 |
| Framework | Laravel | 5.6.39 | 🟢 |
| Admin / auth shell | `salipropham/laravel55-admin` (Encore\Admin fork) | `0.1.*` | 🟢 |
| View | Blade (31 templates); `laravelcollective/html` | `5.6.*` | 🟢 |
| Image processing | `intervention/image` | `^2.7` | 🟢 |
| Front-end (build) | Vue 2.5, Bootstrap 4, jQuery 3.2, axios, lodash, Laravel Mix | — | 🟢 |
| ORM | Eloquent (12 models) | — | 🟢 |
| Database | MariaDB | 10.11.13 | 🟢 |
| Web server | Apache + mod_php (`www-data`); Nginx config also present | — | 🟢 |
| Scheduling | System cron → `artisan schedule:run` (every minute; job runs daily) | — | 🟢 |
| Packaging | Docker / docker-compose | — | 🟢 |
| CI/CD | GitLab CI (`pos-dev` deploy job) | — | 🟢 |
| Tests | PHPUnit 7 (1 feature test) | — | 🟢 |

---

## Cross-cutting concerns

### Security and access control 🟢
- Exactly **one user class**: the Encore\Admin `Administrator`. No customer login — customers are CRM records.
- Authorization collapses to authentication: all routes run under `['web','admin']` (auth guard only). The package's `admin.permission` middleware, Laravel policies, gates, and `can:` middleware are all absent from `app/`. Any authenticated admin can reach every screen and action. Confirmed intentional for a single-operator shop.
- The RBAC tables are seeded (a single `administrator` role with `*`) but decorative.

### Business-rule guards (within-role) 🟢
Action guards, not user roles: order `is_editable` (24-hour window), `debt_locked` (post-once), debt requires a customer and must not exceed the order total, repayment ceiling, gift-redemption gate, protected reference rows.

### Idempotency and concurrency 🟢
- Points award is idempotent via `points_awarded_at`; POS debt posts exactly once via `debt_locked`.
- `CustomerDebt::record` and gift redemption use `DB::transaction()` + `lockForUpdate()`.
- `Product::generateCode` (`max(id)+1`) is **not** concurrency-safe. 🟡

---

## Notable technical debt

| # | Item | Impact |
|---|------|--------|
| 1 | **Duplicated totalling logic** — `OrderController::store` and `::update` repeat ~60 lines of near-identical pricing/totalling. | Pricing rule changes must be made twice; drift risk. |
| 2 | **Picture-cleanup path bug** — `update()` unlinks from `app/public2` (wrong root); old images accumulate. | Silent disk growth (GAP-P1). |
| 3 | **Mixed unit modelling** — base unit by name string vs. conversion unit by FK. | Unit rename desyncs base-unit strings (GAP-U2). |
| 4 | **`generateCode` race** — `max(id)+1` not concurrency-safe. | Duplicate codes under concurrent create. |
| 5 | **N+1 in receipts** — `Unit::find($pivot->unit_id)->name` per line in print/detail/history loops. | Query amplification on long receipts. |
| 6 | **Category parent linkage** — UI never sets `parent_id`; UI-created categories can never be attached to a product (GAP-C1). | Broken authoring flow. |
| 7 | **Legacy/unused schema** — `discounts` + `orders.discount_id`; dead `units.category_id`. | Dead schema; confusion. |
| 8 | **EOL platform** — Laravel 5.6 (2018) + PHP 7.4 (EOL). | Security and support risk. |
| 9 | **Effectively no automated coverage** — 1 PHPUnit feature test for a system with rich money/points/debt rules. | Regressions land silently. |
