# Customers Statistics — Implementation Tasks

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

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

## Prerequisites

- [ ] The `customer_order_summary` read model exists and is populated (see the batch/`summaryLogging` unit) — this screen only reads it.
- [ ] `Customer`, `CustomerOrderSummary`, and `Order` models (with the `$category_milk_id` / `$category_medicine_id` constants) are available.
- [ ] The admin auth middleware (`['web','admin']`) and layout are in place.
- [ ] The `pages.customer-statis` view (or its reimplementation) and its sibling routes exist: `customers.edit`, `customers.orders`, `customers.debt`, `customers.gift-received`, `customers.redeem-points`, `customers.check-gift`, `orders.update_note`.

## Tasks

> Each task references the legacy file the behavior was extracted from.

- [ ] T-01, Register `GET /customers/{customer}/statistic` as route name `customers.statis` **before** the `/customers` resource so it isn't shadowed by the resource `show` route.
  - Legacy origin: `routes/web.php:57` (declared before `routes/web.php:62`)
  - Done when: hitting the route reaches `CustomerController::statis`, not the resource `show`
  - Confidence: 🟢

- [ ] T-02, Resolve the customer with `findOrFail($id)`; return 404 for unknown/soft-deleted ids.
  - Legacy origin: `CustomerController.php:223`
  - Done when: a non-existent id yields HTTP 404
  - Confidence: 🟢

- [ ] T-03, Load the precomputed summary row `CustomerOrderSummary::where('customer_id', id)->first()`, tolerating `null`.
  - Legacy origin: `CustomerController.php:224`
  - Done when: a customer without a summary row does not error
  - Confidence: 🟢

- [ ] T-04, Compute headline scalars with null-coalescing fallbacks: `orders_count`, `amountTotal`, `pointsTotal` from the summary (`?? 0`); `debtTotal` from the LIVE `customer.debt_total`; expose live `customer.points` to the view.
  - Legacy origin: `CustomerController.php:226-229`, `customer-statis.blade.php:47-51`
  - Done when: totals match the summary; debt & current points reflect the live customer even when the summary is stale
  - Confidence: 🟢

- [ ] T-05, Capture `statisticsUpdatedAt = summary.updated_at ?? null` and render it (formatted `d/m/Y H:i`) or a "not yet updated" placeholder.
  - Legacy origin: `CustomerController.php:231`, `customer-statis.blade.php:107-111`
  - Done when: freshness timestamp shows for a computed customer, placeholder for an uncomputed one
  - Confidence: 🟢

- [ ] T-06, Decode `categories_statistic` (already an object array via the `object` cast) and re-key by `category_id`.
  - Legacy origin: `CustomerController.php:233-238`, `CustomerOrderSummary.php:13-15`
  - Done when: each category object is addressable by its id
  - Confidence: 🟢

- [ ] T-07, Split out milk (`Order::$category_milk_id` = 1) into `attrStatistic` and medicine (`Order::$category_medicine_id` = 8) into `medicineStatis`, removing each from the map after extraction.
  - Legacy origin: `CustomerController.php:239-243`, `Order.php:23-25`
  - Done when: milk items appear only in the milk table, medicine items only in the medicine table
  - Confidence: 🟢 (medicine id `8` is a known placeholder per `openspec/changes/archive/2026-08-27-customer-statis-and-order-filter/design.md`, deferred to the business — implement as-is)

- [ ] T-08, Aggregate every remaining category into `goodsStatis`: per category `{name, qty_total = Σ items.qty_total, amount_total = Σ items.amount_total}`, sorted by `name`.
  - Legacy origin: `CustomerController.php:245-251`
  - Done when: the goods table shows one row per non-milk/non-medicine tracked category, name-sorted, with summed qty & amount
  - Confidence: 🟢

- [ ] T-09, Wrap the entire category decode/split/aggregate in a `try/catch` that, on any exception, sets all three category collections to empty — leaving headline totals intact.
  - Legacy origin: `CustomerController.php:233-256`
  - Done when: a corrupt `categories_statistic` still returns HTTP 200 with empty tables and correct headline totals
  - Confidence: 🟢

- [ ] T-10, Build the annotated-orders panel: `Order::where('customer_id', id)->whereNotNull('notes')->orderBy('id','desc')->paginate(10)`.
  - Legacy origin: `CustomerController.php:258`
  - Done when: only orders with a non-null note appear, newest first, 10/page, with working pagination links
  - Confidence: 🟢

- [ ] T-11, Render `pages.customer-statis` with `item, amountTotal, debtTotal, orders, attrStatistic, goodsStatis, pointsTotal, medicineStatis, statisticsUpdatedAt`, including the tab links and the inline note-edit / redeem-points modals (delegating to the `orders-note` and `customers-loyalty` endpoints).
  - Legacy origin: `CustomerController.php:260`, `customer-statis.blade.php`
  - Done when: the page renders all five headline cells, three category tables, the annotated-orders list, and functional modal actions
  - Confidence: 🟢

- [ ] T-12, Enforce authenticated admin access via the `['web','admin']` group.
  - Legacy origin: `routes/web.php:57`, `config/admin.php`
  - Done when: an unauthenticated request is redirected `302 → auth/login`
  - Confidence: 🟡

## Test Tasks

- [ ] TT-01, Happy path: a customer with a full summary (milk + medicine + other categories) renders all five totals, all three tables correctly split, and the freshness timestamp (see `requirements.md` Acceptance Criteria).
- [ ] TT-02, No-summary path: a customer without a `customer_order_summary` row returns 200 with all-zero totals and empty tables.
- [ ] TT-03, Malformed-JSON path: a corrupt `categories_statistic` returns 200, headline totals intact, category tables empty (no 500).
- [ ] TT-04, 404 path: an unknown/soft-deleted customer id returns 404.
- [ ] TT-05, Annotated-orders filter: only orders with a non-null note appear; orders without notes are excluded; pagination caps at 10/page.
- [ ] TT-06, Live vs precomputed: after a redemption, `points` (current) drops but `points_total` (earned) is unchanged; after a debt change, `debtTotal` reflects it immediately while the summary totals do not.
- [ ] TT-07, Auth path: unauthenticated request redirects to login.

## Data Migration Tasks

- [ ] TM-01, None owned by this unit. The `customer_order_summary` table and its population are provided by the `summaryLogging` batch unit; this screen consumes it read-only. Ensure that unit is migrated/scheduled first so this screen has data to show.

## Suggested Order

1. T-01 → T-03 (routing + customer + summary load) form the skeleton.
2. T-04, T-05 (headline totals + freshness) — independently testable once the skeleton exists.
3. T-06 → T-09 (category decode/split/aggregate + resilience) — the analytical core; do T-09's try/catch alongside T-06–T-08.
4. T-10, T-11 (orders panel + render) — depend on all data being assembled.
5. T-12 (auth) is orthogonal, verifiable at any point.
6. Blocker: everything meaningful depends on the `customer_order_summary` read model existing and being populated (TM-01 / prerequisites).

## Pending Gaps (🔴)

- 🔴 Decide whether "summary not yet computed" (`summary === null`) should be visually distinct from a genuine all-zero customer, so a broken nightly rebuild is not silently indistinguishable from a new customer.
- ✅ **Medicine category id already resolved as a known, deferred decision.** `Order.php:25` marks id `8` as a `placeholder`; `openspec/changes/archive/2026-08-27-customer-statis-and-order-filter/design.md` documents this as an explicit Non-Goal, deferred to the business — implement against `8` as-is, no new confirmation needed.
