# Code / Spec Matrix — TinyPOS (`tnx-pos`)

> Produced by the Reversa **Writer** (phase: generation) · doc_level: `complete`
> Generated on 2026-09-22

**Confidence scale:** 🟢 CONFIRMED (behaviour of this file is captured by the listed unit) · 🟡 INFERRED / PARTIAL (touched or documented indirectly, e.g. a shared table, a consumed accessor, or a file documented as intentionally dead) · 🔴 GAP (behaviour matters but no unit covers it) · **n/a** (framework/scaffold/theme file with no application behaviour of its own — no unit needed)

This is the **code→spec** view: for each legacy source file, which generated SDD unit(s) cover its behaviour and how completely. It complements the Architect's [`spec-impact-matrix.md`](./spec-impact-matrix.md) (component-level change blast-radius) and the [`openapi/tnx-pos.yaml`](../openapi/tnx-pos.yaml) + [`user-stories/`](../user-stories/) global views. Files marked `n/a` are Laravel/Encore\Admin scaffolding with no bespoke logic; files marked 🔴 or `n/a` under "no unit" are the candidates for additional analysis (see §11).

Granularity is `endpoint`, so a single controller is frequently split across several units (one per route). The matrix records that split explicitly.

---

## 1. Coverage summary

| Category | Files | 🟢 covered | 🟡 partial/dead | 🔴 gap | n/a scaffold |
|----------|:-----:|:----------:|:---------------:|:------:|:------------:|
| Controllers (`app/Http/Controllers/`) | 17 | 10 | 5 | 0 | 2 |
| Models (`app/Models/`) | 12 | 10 | 2 | 0 | 0 |
| Routes (`routes/`) | 3 | 1 | 1 | 0 | 1 |
| Page views (`resources/views/pages/`) | 16 | 16 | 0 | 0 | 0 |
| Console / scheduler (`app/Console/`) | 2 | 2 | 0 | 0 | 0 |
| Migrations (`database/migrations/`) | 28 | 23 | 4 | 0 | 1 |
| Seeds / factories (`database/`) | 3 | 0 | 2 | 0 | 1 |
| Middleware / providers / kernel / helpers | 14 | 0 | 4 | 0 | 10 |
| Partials / vendor / welcome views | 16 | 1 | 1 | 0 | 14 |
| Tests (`tests/`) | 3 | 0 | 1 | 0 | 2 |

**Application-behaviour coverage:** every route in `routes/web.php` maps to exactly one of the 23 units (🟢). No live HTTP surface is left un-specified. The residual 🟡/🔴/`n/a` rows are framework scaffolding, seeded admin/RBAC plumbing, or code the units explicitly document as **dead / do-not-reimplement**.

---

## 2. Controllers

| Legacy file | Unit(s) | Coverage | Notes |
|-------------|---------|:--------:|-------|
| `app/Http/Controllers/PosController.php` | `pos-terminal/`, `pos-scan/` | 🟢 | `index` → pos-terminal; `scan` + `buildUnitsPayload` → pos-scan. |
| `app/Http/Controllers/ProductController.php` | `products-catalog/`, `products-pricing/` | 🟢 | Resource CRUD + `syncProductUnits` → products-catalog; `getPriceByCustomerType` (`get-price`) → products-pricing. |
| `app/Http/Controllers/CustomerController.php` | `customers-crud/`, `customers-scan/`, `customers-purchase-history/`, `customers-statistics/`, `customers-debt-actions/`, `customers-loyalty/` | 🟢 | Six units, one per route family (resource CRUD, `scan`, `orders`, `statis`, `debt`/`storeDebt`/`storeRepayment`, `checkGift`/`redeemRewardPoints`/`getListGiftReceived`). |
| `app/Http/Controllers/OrderController.php` | `orders-crud/`, `orders-scan/`, `orders-print/`, `orders-note/` | 🟢 | Resource spine → orders-crud; `scan`/`printOrder`/`updateNote` → the three sibling units. |
| `app/Http/Controllers/DashboardController.php` | `dashboard/` | 🟢 | `index` only (other resource verbs unused). |
| `app/Http/Controllers/DebtController.php` | `debts/` | 🟢 | Read-only A/R overview `index`. |
| `app/Http/Controllers/BrandController.php` | `brands/` | 🟢 | Encore\Admin `ModelForm` scaffold. |
| `app/Http/Controllers/CategoryController.php` | `categories/` | 🟢 | Encore\Admin scaffold + parent_id hierarchy / ids 1,8 special-casing. |
| `app/Http/Controllers/UnitController.php` | `units/` | 🟢 | Encore\Admin scaffold. |
| `app/Http/Controllers/GiftController.php` | `gifts-crud/`, `gifts-scan/` | 🟢 | Resource + hooks → gifts-crud; `scan` → gifts-scan. |
| `app/Http/Controllers/Auth/LoginController.php` | `auth/` | 🟡 | Documented as the **intentionally-dead** Laravel framework auth stack (`App\User` missing); real auth is Encore\Admin `AuthController`. Marked do-not-reimplement. |
| `app/Http/Controllers/Auth/RegisterController.php` | `auth/` | 🟡 | Dead framework auth stack (see auth unit). |
| `app/Http/Controllers/Auth/ForgotPasswordController.php` | `auth/` | 🟡 | Dead framework auth stack. |
| `app/Http/Controllers/Auth/ResetPasswordController.php` | `auth/` | 🟡 | Dead framework auth stack. |
| `app/Http/Controllers/HomeController.php` | `pos-terminal/`, `dashboard/` | 🟡 | Documented as **dead**: root `/` 301-redirects to `/pos`, so the HomeController builder is never reached. Recorded in pos-terminal/dashboard, not reimplemented. |
| `app/Http/Controllers/Controller.php` | — | n/a | Laravel base controller. |
| `app/Http/Controllers/bootstrap.php` | — | n/a | Encore\Admin controller-bootstrap scaffold. |

---

## 3. Models

| Legacy file | Unit(s) | Coverage | Notes |
|-------------|---------|:--------:|-------|
| `app/Models/Product.php` | `products-catalog/`, `products-pricing/`, `pos-scan/` | 🟢 | SoftDeletes + deleted-rename, wholesale_prices accessor/mutator, is_expired, generateCode, units relation, getPriceByCustomerType. |
| `app/Models/ProductUnit.php` | `products-catalog/`, `products-pricing/` | 🟢 | conversion_qty float cast, full-replace sync. |
| `app/Models/Order.php` | `orders-crud/`, `orders-scan/`, `orders-print/`, `orders-note/`, `customers-purchase-history/`, `customers-statistics/` | 🟢 | Appends code/is_editable/debt_locked, status machine, products pivot, summaryLogging, milk/medicine ids 1/8. |
| `app/Models/Customer.php` | `customers-*` (all six), `pos-terminal/`, `debts/` | 🟢 | SoftDeletes, type accessor, points/debt_total, gifts/debts relations, checkGiftAvailable, reversePointsForOrder. |
| `app/Models/CustomerDebt.php` | `customers-debt-actions/`, `orders-crud/` | 🟢 | `record` (locked ledger primitive) → debt-actions; `voidForOrder` → orders-crud. |
| `app/Models/CustomerOrderSummary.php` | `customers-statistics/` | 🟢 | Nightly read model; categories_statistic object cast. |
| `app/Models/Gift.php` | `gifts-crud/`, `gifts-scan/`, `customers-loyalty/` | 🟢 | SoftDeletes, quantity_available, active scope, image accessor. |
| `app/Models/Brand.php` | `brands/` | 🟢 | products hasMany via required FK. |
| `app/Models/Category.php` | `categories/` | 🟢 | products hasMany; parent_id hierarchy. |
| `app/Models/Unit.php` | `units/` | 🟢 | Empty Eloquent model; consumed by products/orders line labels. |
| `app/Models/Setting.php` | `products-catalog/` | 🟡 | Only `Setting::get('near_expiry_days')` usage is documented (products index expiry filter). The table is an extensible config seam; no dedicated Settings unit exists. |
| `app/Models/Discount.php` | `orders-crud/` (schema only) | 🟡 | Model + `discounts` table + a live `orders.discount_id` FK (`onDelete('cascade')`) + `Order::discount()` `belongsTo` all exist and are internally consistent, but no controller ever sets `discount_id` — checkout discounts flow through the free-form `orders.discount_amount` column instead. Confirmed 2026-09-23 (`questions.md#question-10`): **kept intentionally for future use**, not dead — do not drop on migration/rewrite. |

---

## 4. Routes

| Legacy file | Unit(s) | Coverage | Notes |
|-------------|---------|:--------:|-------|
| `routes/web.php` | all 23 units | 🟢 | Every route (auth, `/`, `/artisan`, dashboard, pos, products, customers, orders, debts, settings/*) maps 1:1 to a unit. Route-ordering (named routes before resources) is documented per unit. |
| `routes/api.php` | — | 🟡 | Only the default Laravel `GET /user` behind `auth:api`. No token/Passport guard is wired; effectively dead. No unit; noted here as a known unused surface. |
| `routes/console.php` | — | n/a | Default `inspire` Artisan closure; scheduling lives in `app/Console/Kernel.php` (covered). |

---

## 5. Page views (`resources/views/pages/`)

| Legacy file | Unit(s) | Coverage | Notes |
|-------------|---------|:--------:|-------|
| `pos.blade.php` | `pos-terminal/` | 🟢 | Cashier terminal + injected cart engine. |
| `pos-print.blade.php` | `orders-print/`, `orders-crud/` | 🟢 | 80mm receipt; rendered by print route and by store/update done path. |
| `products.blade.php` | `products-catalog/` | 🟢 | Product index/list. |
| `products-add.blade.php` | `products-catalog/` | 🟢 | Create form. |
| `products-edit.blade.php` | `products-catalog/` | 🟢 | Edit form. |
| `customers.blade.php` | `customers-crud/` | 🟢 | Customer index. |
| `customer-add.blade.php` | `customers-crud/` | 🟢 | Create form (also POS quick-add modal target). |
| `customer-edit.blade.php` | `customers-crud/` | 🟢 | Edit form. |
| `customer-orders.blade.php` | `customers-purchase-history/` | 🟢 | Purchase history + inline note editor. |
| `customer-statis.blade.php` | `customers-statistics/` | 🟢 | Quick-statistics page + annotated-orders panel. |
| `customer-debt.blade.php` | `customers-debt-actions/` | 🟢 | Ledger + repayment/manual-debt modals. |
| `customer-gift-received.blade.php` | `customers-loyalty/` | 🟢 | Redeemed-gifts history. |
| `dashboard.blade.php` | `dashboard/` | 🟢 | KPIs + sales chart + top products. |
| `debts.blade.php` | `debts/` | 🟢 | Debtor table + action modals. |
| `orders.blade.php` | `orders-crud/`, `orders-print/` | 🟢 | Order list + re-print (`?ref=orders`) link. |
| `orders-detail.blade.php` | `orders-crud/` | 🟢 | `show` detail page. |

---

## 6. Console / scheduler (`app/Console/`)

| Legacy file | Unit(s) | Coverage | Notes |
|-------------|---------|:--------:|-------|
| `app/Console/Kernel.php` | `customers-statistics/` | 🟢 | `->daily()` schedule calling `Order::summaryLogging()`. |
| `app/Console/Commands/CustomerOrderSummaryLogging.php` | `customers-statistics/` | 🟢 | On-demand `customer-summary:logging` command. |

---

## 7. Migrations (`database/migrations/`)

Every migration is also collectively executed by the **`artisan-migrate/`** unit (`GET /artisan` runs `migrate --force`); the "Unit(s)" column below lists the unit that owns the *schema semantics* of each table/column.

| Legacy file | Unit(s) | Coverage | Notes |
|-------------|---------|:--------:|-------|
| `2016_01_04_173148_create_admin_tables.php` | `auth/` | 🟡 | Encore\Admin RBAC tables — seeded but unenforced (permissions.md, ADR-0009); documented, not reimplemented as behaviour. |
| `2019_04_09_082515_create_categories_table.php` | `categories/` | 🟢 | |
| `2019_04_09_082524_create_brands_table.php` | `brands/` | 🟢 | |
| `2019_04_09_082930_create_discounts_table.php` | `orders-crud/` (schema only) | 🟡 | Backs the `Discount` model and the live `orders.discount_id` FK (§3); never populated, kept intentionally for future use — confirmed 2026-09-23 (`questions.md#question-10`). |
| `2019_04_09_094008_create_customers_table.php` | `customers-crud/` | 🟢 | |
| `2019_04_09_104651_create_products_table.php` | `products-catalog/` | 🟢 | |
| `2019_04_09_104708_create_orders_table.php` | `orders-crud/` | 🟢 | |
| `2019_04_10_044846_create_order_product_table.php` | `orders-crud/`, `products-catalog/` | 🟢 | Line-item pivot. |
| `2019_05_15_121417_create_units_table.php` | `units/` | 🟢 | Includes the dead `category_id` column (GAP-U1). |
| `2022_03_20_001839_add_reward_point_to_products_table.php` | `products-catalog/`, `customers-loyalty/` | 🟢 | Points-per-product source. |
| `2022_03_21_114856_add_earned_point_to_orders_table.php` | `orders-crud/` | 🟢 | earned_point on finalise. |
| `2022_05_09_092607_add_col_birthday2_to_customers_table.php` | `customers-crud/` | 🟢 | Parsed birthday2. |
| `2022_05_10_192107_change_status_col_in_orders_table.php` | `orders-crud/` | 🟢 | draft/done status. |
| `2022_05_18_122119_create_gifts_table.php` | `gifts-crud/` | 🟢 | |
| `2022_05_20_151916_create_customer_gift_table.php` | `customers-loyalty/` | 🟢 | Redemption pivot. |
| `2022_05_25_121331_add_total_used_to_gifts_table.php` | `gifts-crud/` | 🟢 | used counter. |
| `2022_05_25_171040_add_type_to_customers_table.php` | `customers-crud/` | 🟢 | Pricing tier. |
| `2022_05_25_181832_add_wholesale_prices_to_products_table.php` | `products-pricing/`, `products-catalog/` | 🟢 | JSON tier prices. |
| `2026_01_02_102722_create_customer_order_summary_table.php` | `customers-statistics/` | 🟢 | Nightly read model. |
| `2026_08_26_120000_create_settings_table.php` | `products-catalog/` | 🟡 | Only near_expiry_days consumed; no Settings unit. |
| `2026_08_26_120200_add_expiry_date_and_promotion_note_to_products_table.php` | `products-catalog/` | 🟢 | Expiry filter fields. |
| `2026_08_27_000001_create_product_units_table.php` | `products-catalog/`, `products-pricing/` | 🟢 | Conversion units. |
| `2026_08_27_000002_add_unit_columns_to_order_product_table.php` | `orders-crud/`, `products-catalog/` | 🟢 | Pivot unit_id/conversion_qty. |
| `2026_08_28_000001_create_customer_debts_table.php` | `customers-debt-actions/` | 🟢 | A/R ledger. |
| `2026_08_28_000002_add_debt_total_to_customers_table.php` | `customers-debt-actions/` | 🟢 | Denormalised balance. |
| `2026_08_28_000003_add_debt_menu_item.php` | — | n/a | Encore\Admin menu-row seed migration (no app schema). |
| `2026_08_28_000004_add_debt_amount_to_orders_table.php` | `orders-crud/` | 🟢 | Order debt_amount. |
| `2026_09_17_000001_drop_owe_total_from_customer_order_summary_table.php` | `customers-statistics/` | 🟢 | Read-model column drop. |

---

## 8. Seeds, factories, middleware, providers, framework scaffolding

| Legacy file | Unit(s) | Coverage | Notes |
|-------------|---------|:--------:|-------|
| `database/seeds/DatabaseSeeder.php` | `auth/` | 🟡 | Runs the admin/RBAC seeders (permissions.md, ADR-0009); documented, not reimplemented. |
| `database/seeds/MenuTableSeeder.php` | — | 🟡 | Encore\Admin menu seed; UI-only, referenced in permissions.md. |
| `database/factories/UserFactory.php` | — | n/a | Default factory for the missing `App\User`; dead. |
| `app/Http/Middleware/RedirectIfAuthenticated.php` | `auth/` | 🟡 | Guest-redirect middleware; auth-flow adjacent. |
| `app/Http/Kernel.php` | `auth/` | 🟡 | `web`/`admin` middleware stack wiring (authenticate-only, ADR-0009). |
| `app/Providers/AuthServiceProvider.php` | `auth/` | 🟡 | No policies/gates registered — confirms authenticate-only access (permissions.md). |
| `app/Providers/RouteServiceProvider.php` | — | n/a | Route-group bootstrapping (behaviour documented in routes/web.php coverage). |
| `app/Http/Middleware/CheckForMaintenanceMode.php` | — | n/a | Laravel default. |
| `app/Http/Middleware/EncryptCookies.php` | — | n/a | Laravel default. |
| `app/Http/Middleware/TrimStrings.php` | — | n/a | Laravel default. |
| `app/Http/Middleware/TrustProxies.php` | — | n/a | Laravel default. |
| `app/Http/Middleware/VerifyCsrfToken.php` | — | n/a | Laravel default (CSRF via `web` group, noted per write unit). |
| `app/Providers/AppServiceProvider.php` | — | n/a | Laravel default. |
| `app/Providers/BroadcastServiceProvider.php` | — | n/a | Laravel default (broadcasting unused). |
| `app/Providers/EventServiceProvider.php` | — | n/a | Laravel default (no domain events registered). |
| `app/Exceptions/Handler.php` | — | n/a | Laravel default exception handler. |
| `app/helpers.php` | — | n/a | Global helper shims. |

---

## 9. Partials, vendor and welcome views

| Legacy file | Unit(s) | Coverage | Notes |
|-------------|---------|:--------:|-------|
| `resources/views/vendor/admin/login.blade.php` | `auth/` | 🟢 | The Encore\Admin login screen — the real auth entry view. |
| `resources/views/welcome.blade.php` | `pos-terminal/` | 🟡 | Default Laravel welcome page; unreachable (root 301→/pos). Documented dead. |
| `resources/views/vendor/admin/index.blade.php` | — | n/a | Encore\Admin dashboard-shell theme view. |
| `resources/views/partials/*.blade.php` (12 files: `ajaxmodal`, `breadcrumb`, `error`, `footer`, `header`, `menu`, `nav`, `pager`, `sidebar`, `sidebar-right`, `topnav-left`, `topnav-right`) | — | n/a | Shared Encore\Admin layout/theme chrome, included by all pages; no per-unit behaviour. |

---

## 10. Tests (`tests/`)

| Legacy file | Unit(s) | Coverage | Notes |
|-------------|---------|:--------:|-------|
| `tests/Feature/OrderControllerTest.php` | `orders-crud/` | 🟡 | The single legacy feature test; exercises order creation. Its assertions are reflected in orders-crud tasks but the test itself is not reproduced. |
| `tests/TestCase.php` | — | n/a | PHPUnit scaffold. |
| `tests/CreatesApplication.php` | — | n/a | PHPUnit scaffold. |

---

## 11. Files without a covering unit (candidates for further analysis)

These carry no live application behaviour that a unit needed to specify. Listed so a future pass can decide whether to prune or formalise them:

- ✅ **`app/Models/Discount.php`** + **`database/migrations/2019_04_09_082930_create_discounts_table.php`** — confirmed 2026-09-23 (`questions.md#question-10`): kept intentionally for future use, not dead legacy. The table, model, and the live `orders.discount_id` FK must be **carried forward** on any migration/rewrite, not dropped, even though nothing populates them today.
- 🟡 **`routes/api.php`** (`GET /user`, `auth:api`) — default Laravel API route with no token guard wired; effectively unreachable. Confirm it can be removed.
- 🟡 **Dead framework auth stack** — `app/Http/Controllers/Auth/*`, `database/factories/UserFactory.php`: the standard Laravel auth built around the absent `App\User`. Real authentication is Encore\Admin's `AuthController` (see `auth/`). Marked do-not-reimplement.
- 🟡 **`resources/views/welcome.blade.php`** and **`app/Http/Controllers/HomeController.php`** — unreachable because `/` 301-redirects to `/pos`.
- n/a **Laravel/Encore\Admin scaffolding** — base controllers, default middleware, service providers, layout partials, exception handler, helpers. No bespoke logic; excluded by design.

No **live HTTP route** and no **domain model in active use** is left uncovered.

---

## 12. Reverse index — unit → primary legacy files

| Unit | Primary legacy source |
|------|-----------------------|
| `auth/` | Encore\Admin `AuthController` (vendored), `routes/web.php` (`registerAuthRoutes`), `config/admin.php`, `vendor/admin/login.blade.php`, `app/Http/Kernel.php`, `AuthServiceProvider.php` |
| `dashboard/` | `DashboardController::index`, `pages/dashboard.blade.php` |
| `pos-terminal/` | `PosController::index`, `pages/pos.blade.php` |
| `pos-scan/` | `PosController::scan` + `buildUnitsPayload`, `Product.php` |
| `products-catalog/` | `ProductController` (CRUD + `syncProductUnits`), `Product.php`, `ProductUnit.php`, `Setting.php`, `pages/products*.blade.php` |
| `products-pricing/` | `ProductController::getPriceByCustomerType`, `Product::getPriceByCustomerType` |
| `customers-crud/` | `CustomerController` (resource), `Customer.php`, `pages/customers*.blade.php` |
| `customers-scan/` | `CustomerController::scan` |
| `customers-purchase-history/` | `CustomerController::orders`, `pages/customer-orders.blade.php` |
| `customers-statistics/` | `CustomerController::statis`, `CustomerOrderSummary.php`, `Order::summaryLogging`, `Console/Kernel.php`, `CustomerOrderSummaryLogging.php`, `pages/customer-statis.blade.php` |
| `customers-debt-actions/` | `CustomerController::debt`/`storeDebt`/`storeRepayment`, `CustomerDebt::record`, `pages/customer-debt.blade.php` |
| `customers-loyalty/` | `CustomerController::checkGift`/`redeemRewardPoints`/`getListGiftReceived`, `Customer::checkGiftAvailable`/`reversePointsForOrder`, `Gift.php`, `pages/customer-gift-received.blade.php` |
| `debts/` | `DebtController::index`, `pages/debts.blade.php` |
| `orders-crud/` | `OrderController` (resource), `Order.php`, `CustomerDebt::voidForOrder`, `pages/orders*.blade.php`, `pages/pos-print.blade.php` |
| `orders-scan/` | `OrderController::scan` |
| `orders-print/` | `OrderController::printOrder`, `pages/pos-print.blade.php`, `Unit.php` |
| `orders-note/` | `OrderController::updateNote` |
| `brands/` | `BrandController`, `Brand.php` |
| `categories/` | `CategoryController`, `Category.php` |
| `units/` | `UnitController`, `Unit.php` |
| `gifts-crud/` | `GiftController` (resource + hooks), `Gift.php` |
| `gifts-scan/` | `GiftController::scan` |
| `artisan-migrate/` | `routes/web.php` (`GET /artisan` closure), `database/migrations/*` (collectively) |

---

*This is the third and final global artifact of the generation phase (3/3), following `openapi/tnx-pos.yaml` and `user-stories/`. It completes the Writer's output; the review phase is next.*
