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

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

**Confidence scale:** 🟢 CONFIRMED (read directly from code) · 🟡 INFERRED (pattern-based, may be wrong) · 🔴 GAP (needs human validation)

This document synthesises the Scout's reconnaissance (`inventory.md`, `dependencies.md`), the Archaeologist's excavation (`code-analysis.md`, `data-dictionary.md`, `flowcharts/`) and the Detective's interpretation (`domain.md`, `state-machines.md`, `permissions.md`, `adrs/`) into a single architectural view. It is a transversal artifact: the C4 diagrams live in `c4-context.md`, `c4-containers.md`, `c4-components.md`; the data model in `erd-complete.md`; the change blast-radius in `traceability/spec-impact-matrix.md`.

---

## 1. Architectural summary

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

The architecture has three notable shapes:

1. **A thick monolith with a thin client.** All business logic runs server-side in Laravel controllers and Eloquent models. The one exception is the cashier terminal: `PosController::index` injects a ~630-line JavaScript cart engine into the page (`Admin::script($sc)`), so the POS cart is assembled entirely client-side and only *submitted* to `OrderController`. The POS "SPA" is therefore a browser-resident component with no separate build artefact. 🟢
2. **Domain-by-convention, not by folders.** There are no domain modules on disk — standard Laravel technical layering (`Controllers/`, `Models/`, `Http/Middleware/`). Domains (products, orders, customers, debts, loyalty, reference data) are expressed through resource controllers + Eloquent models grouped by naming. This is why the specs organisation was decided as `endpoint` granularity (`[specs]` in `config.toml`). 🟢
3. **Precomputed read models.** Per-customer statistics are not computed live; a nightly scheduler (`php artisan schedule:run` → `Order::summaryLogging()`) rebuilds the `customer_order_summary` denormalised table. Reads (`CustomerController::statis`) hit the snapshot. 🟢 (ADR-0005)

**Style:** classic layered MVC monolith (routing → controllers → Eloquent models → MariaDB), with the loyalty/debt/statistics concerns woven into fat controllers and rich models rather than dedicated service classes.

---

## 2. Technology stack

| Layer | Technology | Version | Confidence |
|-------|-----------|---------|------------|
| Runtime | PHP | 7.4.33 (EOL) | 🟢 |
| Framework | Laravel | 5.6.39 (`5.6.*`) | 🟢 |
| Admin/auth shell | `salipropham/laravel55-admin` (Encore\Admin fork) | `0.1.*` | 🟢 |
| View builders | `laravelcollective/html`, Blade (31 templates) | `5.6.*` | 🟢 |
| Image processing | `intervention/image` | `^2.7` | 🟢 |
| Proxy handling | `fideloper/proxy` | `^4.0` | 🟢 |
| 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) | — | 🟢 |

---

## 3. Containers (see `c4-containers.md`)

- **Web Application** — the Laravel monolith: HTTP request handling, Blade rendering, controllers, Eloquent models, business logic. Runs under Apache/mod_php as `www-data`.
- **POS Cart Client** — the injected in-browser JavaScript cart engine (Vue/jQuery/Bootstrap). Builds the cart, re-prices lines on customer change, enforces client-side debt rules, submits to `/orders`.
- **Scheduler / Cron** — `artisan schedule:run` fired every minute by system cron; runs the daily `Order::summaryLogging()` rollup (also exposed as the `CustomerOrderSummaryLogging` artisan command).
- **Database** — MariaDB, accessed via Eloquent.
- **Local File Storage** — the `public` disk; product and gift images written/resized by `intervention/image`.

---

## 4. Components (see `c4-components.md`)

The Web Application decomposes into resource controllers and rich models. The highest-complexity components are:

- **`PosController`** — renders the terminal, serves `pos/scan` product lookups, builds the per-product unit payload, injects the cart JS. (Complexity: high)
- **`OrderController`** — the totalling engine (store/update), point-award and POS-debt posting, print, and the destructive `destroy` that reverses side effects. Holds the pricing/finalisation orchestration. (Complexity: high)
- **`CustomerController`** — fat CRM controller: CRUD, POS quick-add, purchase history, statistics read model, points/gift redemption, and the debt/repayment write endpoints. (Complexity: high)
- **Rich models as domain services:** `Product::getPriceByCustomerType` (pricing rule), `CustomerDebt::record` / `voidForOrder` (ledger service), `Customer::checkGiftAvailable` / `redeem` (loyalty), `Order::summaryLogging` (statistics batch), `Customer::reversePointsForOrder` (claw-back).
- **Reference-data controllers** (`Brand/Category/Unit/GiftController`) — thin Encore\Admin `ModelForm` editors.

---

## 5. Data architecture (see `erd-complete.md`)

Eloquent over MariaDB, 12 models / ~20 application-relevant tables plus the Encore\Admin RBAC tables. Notable data-model characteristics:

- **Captured-at-sale-time pricing.** `order_product.price` and `conversion_qty` are snapshotted per line; catalogue changes never rewrite history. 🟢
- **Append-only debt ledger.** `customer_debts` is the source of truth; `customers.debt_total` is a locked running balance mutated only through `CustomerDebt::record`. Ledger rows keep `balance_after` for a full audit trail even after the originating order is hard-deleted (`order_id` set null; reversal linked via `related_debt_id`). 🟢
- **Denormalised read model.** `customer_order_summary` (PK = `customer_id`, one row per customer) is the nightly rollup. 🟢
- **Mixed unit modelling (smell).** The base unit is a free **string** (`products.unit`), while conversion units are **FKs** (`product_units.unit_id`, `order_product.unit_id`). Renaming a unit desyncs base-unit strings. 🟡 (GAP-U2)
- **Legacy/unused relations.** `discounts` + `orders.discount_id` exist but no code applies a discount by id; `units.category_id` is declared but never read/written. 🟡 (GAP-O2, GAP-U1)

---

## 6. Cross-cutting concerns

### Security & access control 🟢 (see `permissions.md`)
- Exactly **one user class**: the Encore\Admin **Administrator** (`admin_users`). No customer login — customers are CRM records.
- Authorization collapses to **authentication**: the route group runs 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/action. Confirmed intentional for a single-operator shop (PERM-1). 🟢
- The RBAC tables are seeded (a single `administrator` role with `*`) but decorative. Default `admin`/`admin` seed credential is rotated in production; risk is theoretical on fresh seed (PERM-2). 🟢

### Business-rule guards (within-role) 🟢
The only within-role limits are action guards, not user roles: order `is_editable` (24h window), `debt_locked` (post-once), debt-requires-customer + `≤ total`, repayment ceiling, gift-redemption gate, protected reference rows. See `permissions.md` §Non-route restrictions and `state-machines.md`.

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

### Statistics / batch 🟢
Nightly `Order::summaryLogging` rollup; category ids 1 (milk) and 8 (medicine) are special-cased, everything else aggregated as "other" (ADR-0006).

---

## 7. Integrations (see `c4-context.md`)

- **External software integrations: none.** No payment gateways, third-party HTTP clients or messaging providers were found in config or dependencies. Image handling is local. 🟢 (confirmed at excavation)
- **Device/peripheral touchpoints (in-store):**
  - **Barcode scanner** → the `scan` endpoints (`/pos/scan`, `/orders/scan`, `/customers/scan`, `settings/gifts/scan`): exact-code lookup vs `LIKE` search. 🟡 (hardware inferred from the barcode-oriented flow; the endpoints themselves are confirmed)
  - **Receipt printer** ← `GET /orders/{order}/print` renders the `pos-print` view. 🟢
- **Operational triggers:** system cron drives `artisan schedule:run`; GitLab CI redeploys and reinstalls the cron file. 🟢

---

## 8. Technical debt (Architect roll-up)

Consolidated from the Archaeologist's per-module smells and the Detective's gaps. Ordered roughly by blast radius.

| # | Debt | Impact | Status | Source |
|---|------|--------|--------|--------|
| 1 | **Duplicated totalling logic** — `OrderController::store` and `::update` repeat ~60 lines of near-identical pricing/totalling. | Any pricing rule change must be made twice; drift risk. | Open (Refactor target) | `code-analysis.md` orders |
| 2 | **Picture-cleanup path bug** — `update()` unlinks from `app/public2` (wrong root); old product images never deleted, orphans accumulate. | Disk growth; silent (caught + logged only). | 🟡 CONFIRMED bug, fix pending team decision on `public2` intent (GAP-P1). | products |
| 3 | **Mixed unit modelling** — base unit by name string vs conversion by FK. | Unit rename desyncs base-unit strings. | Open (GAP-U2) | units |
| 4 | **`generateCode` race** — `max(id)+1` not concurrency-safe. | Duplicate/colliding codes under concurrent create. | Open | products |
| 5 | **N+1 in receipts** — `Unit::find($pivot->unit_id)->name` per line in print/detail/history loops. | Query amplification on long receipts. | Open | units |
| 6 | **Category parent linkage** — UI never sets `parent_id`, yet only child categories are product-assignable; UI-created categories can never be attached to a product. | Broken authoring flow; needs DB edits. | 🔴 GAP-C1 pending | categories |
| 7 | **Category default lock disabled** — id-1 edit-lock commented out (unlike brand/unit). | Inconsistent protected-default behaviour. | 🔴 GAP-C2 pending | categories |
| 8 | **Legacy/unused** — `discounts` + `orders.discount_id` (GAP-O2), dead `units.category_id` (GAP-U1). | Dead schema; confusion. | Open | orders/units |
| 9 | **Dead auth scaffolding** — Laravel `Auth/*` controllers reference a non-existent `App\User`; unreachable by design. | Confusing dead code; no active risk. | 🟢 Confirmed intentional | auth |
| 10 | **Effectively no automated coverage** — 1 PHPUnit feature test for a system with rich money/points/debt rules. | Regressions land silently. | Open | inventory |
| 11 | **EOL platform** — Laravel 5.6 (2018) + PHP 7.4 (EOL). | Security/support risk; relevant to `/reversa-migrate`. | Open | dependencies |

---

## 9. Confidence & open questions

Architecture-level confidence is **high** for structure, data model and control flow (read directly from code). Open business-intent questions are carried in `domain.md` (GAP-C1, GAP-C2, GAP-U1, GAP-U2, GAP-O2, GAP-P1) and `permissions.md` (PERM-3: no app-level policies/gates exist, so any future authorization starts from zero). The barcode-scanner hardware assumption is the main 🟡 inference at the context boundary.

See `c4-context.md`, `c4-containers.md`, `c4-components.md`, `erd-complete.md` and `traceability/spec-impact-matrix.md` for the diagrammatic detail.
</content>
</invoke>
