# Gifts Scan — Requirements

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

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

## Overview

`gifts-scan` is the JSON gift-autocomplete endpoint (`GET settings/gifts/scan`, `GiftController::scan`) that a redemption picker calls to list gifts matching a typed name, optionally filtered by `active` status. It is the read-only lookup counterpart to the `gifts-crud` catalogue editor on the same controller. 🟢 (`routes/web.php:90`, `GiftController.php:115-126`)

The route is declared **before** `resource settings/gifts` (`routes/web.php:90` vs `:91`), so `settings/gifts/scan` is matched as its own action and never shadowed by the resource `show` route (`settings/gifts/{id}`). 🟢 (`routes/web.php:90-91`)

It returns a `{ "data": [ …gifts ] }` envelope of up to 10 full `Gift` records. It does **not** compute availability or enforce redeemability — that authority lives in `customers-loyalty` (`Customer::checkGiftAvailable` + `redeemRewardPoints`). This endpoint only lists candidate gifts. 🟢 (`gifts` flowchart:39-40)

## Responsibilities

- Read `q` from the request (`request('q')`), with no validation or default. 🟢 (`:117`)
- Query `Gift::where('name','like',"%$q%")` — a case-insensitive substring match on **name only** (gifts have no code column). 🟢 (`:118`)
- Apply an **optional** `active` filter **only when the `active` query param is present** (`request()->has('active')` → `where('active', request('active'))`). 🟢 (`:119-121`)
- Cap the result at 10 rows (`take(10)->get()`) with **no explicit `orderBy`**, so row ordering is DB-engine-defined (conventionally primary-key ascending = oldest-first, **not** newest-first). ⚠ Reviewer 2026-09-22: downgraded from 🟢 — the earlier "newest-first" characterization is unsupported by the code. 🟡 (`:122`)
- Return `response()->json(['data' => $result])` — always a 200 JSON object with a `data` array (empty `[]` when nothing matches). 🟢 (`:123-125`)

## Business Rules

- **Name-only substring search.** Only `name LIKE %q%` is queried; there is no code/points/id search path (contrast `pos-scan`'s barcode-first lookup). 🟢 (`:118`)
- **`active` filter is opt-in.** Without the `active` param the query returns **both** active and inactive gifts; with it, only rows matching the passed value. The out-of-stock and redeemability decisions are **not** made here. 🟢 (`:119-121`; `gifts` flowchart:39-41)
- **Hard cap of 10, no pagination or total.** The caller gets at most 10 rows and no count metadata. 🟢 (`:122`)
- **Soft-deleted gifts are excluded** automatically by the `Gift` `SoftDeletes` default scope. 🟢 (`Gift.php:10-14`)
- **Full record serialization.** Each element is the complete `Gift` (all columns + appended `quantity_available` + the `image` accessor URL), so the picker can show stock and thumbnail without a second call. 🟢 (`Gift.php:16-27`)
- **Envelope shape `{data:[...]}`.** Distinct from `customers-scan` (bare array) and `pos-scan` (an `is_barcode` discriminated union) — callers must read `.data`. 🟡 (`:123-125`)

## Functional Requirements

| ID | Requirement | Priority | Acceptance criterion |
|----|-------------|----------|----------------------|
| RF-01 | Return gifts whose `name` contains `q` as JSON `{data:[…]}` | Must | `GET settings/gifts/scan?q=abc` returns ≤10 gifts whose name contains "abc". 🟢 |
| RF-02 | Filter by `active` when the param is present | Should | `?active=1` returns only active gifts; omitting `active` returns both. 🟢 |
| RF-03 | Cap results at 10 | Should | At most 10 gifts are returned regardless of matches. 🟢 |
| RF-04 | Serialize full gift records (incl. `quantity_available`, image URL) | Must | Each element carries name, points, limit, quantity, used, `quantity_available`, active, image URL. 🟢 |
| RF-05 | Exclude soft-deleted gifts | Must | A deleted gift never appears in results. 🟢 |
| RF-06 | Require an authenticated admin session | Must | Anonymous request → `302` to `auth/login`. 🟢 (`routes/web.php:24-28`) |

## Non-Functional Requirements

| Type | Inferred requirement | Evidence in code | Confidence |
|------|----------------------|------------------|------------|
| Security | Admin authentication required (admin route group middleware `['web','admin']`) | `routes/web.php:24-28,86-92` | 🟢 |
| Performance | Leading-wildcard `LIKE '%q%'` on `name` cannot use an index → full scan; degrades per-keystroke as the catalogue grows | `GiftController.php:118` | 🟡 |
| Security | User-typed `%`/`_` are treated as LIKE wildcards; parameter-bound so no SQL injection | `GiftController.php:118` | 🟡 |
| Observability | None — no logging/metric on this lookup path | `GiftController.php:115-126` (absence) | 🔴 |

> Inferred from code. Validate with the operations team.

## Acceptance Criteria

```gherkin
Given an authenticated administrator
When he calls GET settings/gifts/scan?q=voucher
Then he receives HTTP 200 with {data:[...]} containing up to 10 gifts whose name contains "voucher"

Given the call GET settings/gifts/scan?q=voucher&active=1
When active and inactive gifts exist with that name
Then only gifts with active=1 are returned

Given the call GET settings/gifts/scan with no active parameter
When active and inactive gifts exist
Then both may appear in the result (no status filter)

Given a soft-deleted gift
When settings/gifts/scan is called
Then that gift never appears in data

Given a request with no authenticated admin session
When settings/gifts/scan is accessed
Then it receives HTTP 302 redirecting to auth/login
```

## Priority (MoSCoW)

| Requirement | MoSCoW | Justification |
|-------------|--------|---------------|
| Name search + JSON envelope (RF-01, RF-04) | Must | The picker's only data source |
| Exclude soft-deleted (RF-05) | Must | Deleted rewards must not be offered |
| Admin authentication (RF-06) | Must | Enforced by the route group |
| `active` filter (RF-02) | Should | Lets the picker request only redeemable rows |
| Cap at 10 (RF-03) | Should | Bounds the autocomplete payload |

> Priority inferred from this being the lookup feeding the loyalty redemption picker.

## Code Traceability

| File | Function / Class | Coverage |
|------|------------------|----------|
| `app/Http/Controllers/GiftController.php:115-126` | `GiftController::scan` (name LIKE search + optional active filter + `{data:[…]}` envelope) | 🟢 |
| `app/Models/Gift.php:16-27` | `Gift` fillable + appended `quantity_available` + image accessor (serialized payload) | 🟢 |
| `app/Models/Gift.php:10-14` | `Gift` `SoftDeletes` (excludes deleted from results) | 🟢 |
| `routes/web.php:90` | `GET settings/gifts/scan` (declared before the resource) | 🟢 |
