# Customers Purchase History — 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` model exists with SoftDeletes and an `orders()` `hasMany(Order)` relation (see `customers-crud` / `design.md`). 🟢 (`Customer.php:9-12,34-37`)
- [ ] The `Order` model exists with `customer_id`, a numeric `total` column, and a `products()` `belongsToMany` through `order_product` (see `orders-crud`). 🟢 (`Order.php:29-42`; `data-dictionary.md:200-223`)
- [ ] The `Category` table is populated (dropdown source). 🟢 (`:202`)
- [ ] The admin auth route group and shared admin layout (`$this->view`) are available. 🟢 (`routes/web.php:23-28`)
- [ ] A `pages.customer-orders` view exists to render `item`, `amountTotal`, `orders`, `categories`. 🟡

## Tasks

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

- [ ] T-01, Register the route `GET /customers/{customer}/orders` → `CustomerController@orders` named `customers.orders`, **before** `resource('/customers')` so it is not shadowed.
  - Origin no legado: `routes/web.php:54,62`
  - Critério de pronto: `GET /customers/{id}/orders` reaches `orders()`, not the resource `show`.
  - Confiança: 🟢

- [ ] T-02, Set the page header `Đơn hàng đã mua` and the breadcrumb `Khách hàng` → header.
  - Origin no legado: `CustomerController.php:194-198`
  - Critério de pronto: the rendered page shows the header and the two-crumb trail.
  - Confiança: 🟢

- [ ] T-03, Resolve the customer with `Customer::withCount('orders')->findOrFail($id)`; return `404` for unknown or soft-deleted ids; expose `orders_count`.
  - Origin no legado: `CustomerController.php:200`
  - Critério de pronto: a valid id loads the customer with an `orders_count`; a missing/soft-deleted id → HTTP `404`.
  - Confiança: 🟢

- [x] T-04, Build the category dropdown: `['' => 'Tất cả danh mục'] + Category::whereNotNull('parent_id')->pluck('name','id')->toArray()` (child categories only).
  - Origin no legado: `CustomerController.php:202`
  - Critério de pronto: the dropdown's first option is the empty "all categories" sentinel, followed by every child category (no parents).
  - Confiança: 🟢 — ✅ Fixed 2026-09-21 (was `Category::pluck(...)` with no `parent_id` filter).

- [ ] T-05, Build the base order query `Order::where('customer_id', $item->id)->orderBy('id','desc')` — all statuses, newest first.
  - Origin no legado: `CustomerController.php:204`
  - Critério de pronto: both `draft` and `done` orders of the customer are returned, ordered by `id` desc.
  - Confiança: 🟢

- [ ] T-06, Apply the optional category filter only when `request('category')` is truthy: `whereHas('products', fn ($q) => $q->where('category_id', request('category')))`.
  - Origin no legado: `CustomerController.php:205-207`
  - Critério de pronto: with `?category=<child-id>`, only orders containing ≥1 product in that category are listed, each once; without it, all orders are listed.
  - Confiança: 🟢

- [ ] T-07, Paginate the list at 20 per page: `$orders = $ordersQuery->paginate(20)`.
  - Origin no legado: `CustomerController.php:208`
  - Critério de pronto: a customer with 25 matching orders shows 20 on page 1, 5 on page 2.
  - Confiança: 🟢

- [ ] T-08, Compute the lifetime total with a separate unfiltered query: `$amountTotal = Order::where('customer_id', $id)->sum('total')`. Note: verified the legacy view never renders this value — decide whether to wire it into the reimplemented UI or drop it.
  - Origin no legado: `CustomerController.php:210`
  - Critério de pronto: `amountTotal` equals `Σ total` over all the customer's orders regardless of the category filter (if the reimplementation chooses to display it).
  - Confiança: 🟢

- [ ] T-09, Render `pages.customer-orders` with `compact('item','amountTotal','orders','categories')`.
  - Origin no legado: `CustomerController.php:212`
  - Critério de pronto: the view receives all four variables and renders the list, total, and dropdown.
  - Confiança: 🟢

- [ ] T-10, Enforce the admin auth precondition via the `['web','admin']` route group (inherited, not re-declared here).
  - Origin no legado: `routes/web.php:23-28`
  - Critério de pronto: an unauthenticated request → `302` to `auth/login`.
  - Confiança: 🟢

## Tarefas de Teste

- [ ] TT-01, Happy path: a customer with mixed `draft`/`done` orders across pages returns them `id` desc, 20/page, with the correct lifetime total (see `requirements.md`, Acceptance Criteria).
- [ ] TT-02, Error path: an unknown or soft-deleted customer id → HTTP `404`.
- [ ] TT-03, Category filter: `?category=<child-id>` narrows the list to orders with a line in that category, each order once (verify `EXISTS`, no duplicate rows).
- [ ] TT-04, Filter/total asymmetry: with a category filter applied, assert `amountTotal` remains the customer's unfiltered lifetime total (documents current behavior; not user-visible in the legacy UI).
- [ ] TT-05, Dropdown scope: the category `<select>` only lists child categories (no parent/group entries) — verifies the 2026-09-21 fix.
- [ ] TT-06, Auth: an unauthenticated request → `302` to `auth/login`.

## Tarefas de Migração de Dados (se aplicável)

- [ ] n/a — read-only reporting screen; no data migration.

## Ordem Sugerida

1. T-01 (route) → T-02/T-03 (header + customer resolution) — the page cannot render without a resolved customer.
2. T-04..T-08 (dropdown, query, filter, pagination, total) — the data assembly, independent of each other except the filter builds on the base query (T-06 after T-05).
3. T-09 (render) once its four inputs exist; T-10 is inherited from the route group and needs no per-action code.

## Lacunas Pendentes (🔴)

- 🔴 **Observability absent** — no logging/metrics on a screen that runs a `whereHas` `EXISTS` plus an aggregate per request; decide whether to add instrumentation on reimplementation.
- ✅ **Total-vs-filter asymmetry (T-08) — verified not user-visible.** `amountTotal` is computed but never rendered by the legacy view; no live confusion exists. Product call (not urgent): wire it into the UI or drop it on reimplementation.
- ✅ **Parent categories in the dropdown (T-04) — fixed 2026-09-21.** Now limited to child categories, matching the product forms.
- 🟡 **Per-row `debt_locked` N+1** — if the view renders `debt_locked` per order, decide whether to eager-load/precompute to avoid ~20 queries/page.
