# Customers Loyalty — Implementation Tasks

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

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

## Prerequisites

- [ ] `Customer` model with `points` (numeric) and a `gifts()` belongsToMany relation over the `customer_gift` pivot (`points`, `note`) is available (`customers-crud`).
- [ ] `Gift` model with `active` scope, `quantity`, `used`, `limit`, `points`, and a computed `quantity_available = quantity − used` is available (`gifts-crud`).
- [ ] `customer_gift` pivot table exists (`id`, `customer_id`, `gift_id`, `points` decimal(8,1) default 0, `note` text nullable, timestamps).
- [ ] Admin auth guard (`['web','admin']`) and `admin_toastr` / redirect-with-errors helpers available (`auth`).
- [ ] A transactional DB that supports `SELECT … FOR UPDATE` (row locks).

## Tasks

- [ ] **T-01 — Register the three routes before `resource('/customers')`.**
  - Legacy origin: `routes/web.php:55,56,59,62`
  - `GET /customers/{customer}/check-gift` (`customers.check-gift`), `GET /customers/{customer}/gift-received` (`customers.gift-received`), `POST /customers/{id}/redeem-points` (`customers.redeem-points`), all declared **above** the resource route so they are not shadowed.
  - Done when: each route resolves to its action and is not captured by the `customers` resource show route.
  - Confidence: 🟢

- [ ] **T-02 — Implement `checkGiftAvailable(Gift $gift)` gate on the customer.**
  - Legacy origin: `app/Models/Customer.php:64-89`
  - Returns `false` + message when: gift missing or `quantity_available === 0` ("Quà tặng không khả dụng"); prior redemption count `>= limit` with `limit !== 0` ("Đã vượt quá số lần đổi quà tối đa"); `gift.points > customer.points` ("Không đủ điều kiện để nhận quà"). Otherwise `['status'=>true]`.
  - Done when: all three gate branches return the exact messages; `limit=0` bypasses the count check.
  - Confidence: 🟢

- [ ] **T-03 — Implement `redeem-points` validation and pre-lock checks.**
  - Legacy origin: `CustomerController.php:296-311`
  - Validate `gift_id` required, `note` nullable string; `Customer::findOrFail`; `Gift::active()->find`; early-return `back()->withErrors('Quà tặng không khả dụng')` when no active gift; early-return with the gate message when `checkGiftAvailable` fails.
  - Done when: each early-return path redirects back with the correct message and opens no transaction.
  - Confidence: 🟢

- [ ] **T-04 — Implement the atomic redemption transaction.**
  - Legacy origin: `CustomerController.php:313-334`
  - Inside `DB::transaction`: `lockForUpdate` the customer and gift; throw if the gift vanished; re-check `checkGiftAvailable` under the lock (throw its message on failure); `gifts()->attach([gift.id => ['note'=>note,'points'=>gift.points]])`; `gift.used += 1`; `customer.points -= gift.points`. Wrap in try/catch → `back()->withInput()->withErrors($e->getMessage())` on any throw.
  - Done when: a lost race rolls back cleanly (no pivot row, no `used`/`points` change) and surfaces the availability error; a success applies all three mutations together.
  - Confidence: 🟢

- [ ] **T-05 — Emit redemption success feedback.**
  - Legacy origin: `CustomerController.php:336-337`
  - `admin_toastr('Đổi quà thành công')` then `redirect()->back()`.
  - Done when: a successful redemption lands back on the referring page with the success toast.
  - Confidence: 🟢

- [x] **T-06 — Implement `check-gift` advisory JSON endpoint.**
  - Legacy origin: `CustomerController.php:361-377`
  - `Customer::findOrFail`; `Gift::active()->find(gift_id)`; null-guard the gift (returns `{status:false,message:'Quà tặng không khả dụng'}`); run `checkGiftAvailable`; return `json($result)` when blocked, `json(['status'=>true])` when available.
  - Done when: an available pair returns `{"status":true}`; a blocked pair returns `{status:false,message}`; a missing/inactive `gift_id` returns a clean result instead of a 500.
  - Confidence: 🟢 — ✅ Fixed 2026-09-21 (null-guard added after `Gift::active()->find()`; was a 500 crash).

- [x] **T-07 — Implement `gift-received` list page.**
  - Legacy origin: `CustomerController.php:340-359`, `resources/views/pages/customer-gift-received.blade.php`
  - `Customer::findOrFail`; `customer.gifts()->orderBy('id','desc')`; optional `?q=` filter (grouped closure, `name` only); `paginate(30)`; render image, name, pivot `points`, pivot `note`, gift `created_at`; `@empty` → "Không có dữ liệu".
  - Done when: the page renders the customer's redeemed gifts newest-first; search filters without leaking other customers' gifts and without SQL error.
  - Confidence: 🟢 — ✅ Fixed 2026-09-21 (was referencing a non-existent `code` column via an ungrouped `orWhere`). The unqualified `orderBy('id','desc')` was flagged 2026-09-22 as a possible ambiguous-column error, then empirically retested 2026-09-23 against the real MariaDB 10.11.13 database via `php artisan tinker` — runs and paginates correctly, no error. Verified not a bug; no qualification needed (`questions.md#question-5`).

## Test Tasks

- [ ] **TT-01 — Happy-path redemption** (RF-02): points debited, `used` incremented, pivot row created with snapshotted cost, success toast.
- [ ] **TT-02 — Redemption blocked** for each gate branch: out of stock (`=== 0`), over `limit`, insufficient points — assert exact messages, no mutation.
- [ ] **TT-03 — Concurrency**: two simultaneous redemptions of a `quantity_available === 1` (or last-`limit`) gift — exactly one succeeds, the other rolls back with the availability error.
- [ ] **TT-04 — `check-gift`**: available → `{status:true}`; blocked → `{status:false,message}`; missing/invalid `gift_id` → clean 400/`{status:false}` (regression for the missing-return/null-gift fix).
- [ ] **TT-05 — `gift-received`**: lists redeemed gifts newest-first; `?q=` search filters correctly (regression for the `code`-column + ungrouped-orWhere fix); empty state renders.
- [ ] **TT-06 — Auth**: anonymous request to each route → `302 auth/login`.

## Data Migration Tasks

- [ ] **TM-01 — Preserve `customer_gift` pivot rows** including historical `points` snapshots and `note`; they are the redemption history the `gift-received` page reads. Legacy schema: `customer_gift` migration.
- [ ] **TM-02 — Preserve `gifts.used`** counters so `quantity_available` stays consistent post-migration.

## Suggested Order

1. T-01 (routes) → T-02 (gate) — everything else depends on the gate.
2. T-03 → T-04 → T-05 (redeem write path, the critical path).
3. T-06 (`check-gift`) and T-07 (`gift-received`) can proceed in parallel once T-02 exists.
4. Apply the T-06 and T-07 fixes only after confirming the 🔴 gaps with the team.

## Pending Gaps (🔴)

- None. Both prior gaps (T-06 crash, T-07 SQL error) were fixed 2026-09-21 — see Tasks above.
