# Gifts Scan Specification

## Purpose

`gifts-scan` exposes a read-only JSON autocomplete endpoint (`GET settings/gifts/scan`) that returns up to ten gifts matching a name substring, optionally filtered by active status. It serves the loyalty-redemption picker so operators can select a gift by typing part of its name. Availability enforcement and redeemability checks are not performed here.

## Requirements

### Requirement: Authentication Required

The system SHALL require a valid authenticated admin session for every request to `GET settings/gifts/scan`. Unauthenticated requests SHALL be redirected with HTTP 302 to `auth/login`. (Implemented in `routes/web.php:24-28,86-92`)

#### Scenario: Unauthenticated access is redirected

- **GIVEN** a client that has no authenticated admin session
- **WHEN** the client sends `GET settings/gifts/scan`
- **THEN** the system responds with HTTP 302 and a `Location` header pointing to `auth/login`

#### Scenario: Authenticated access proceeds

- **GIVEN** a client with a valid authenticated admin session
- **WHEN** the client sends `GET settings/gifts/scan`
- **THEN** the system responds with HTTP 200 and a JSON body

### Requirement: Name Substring Search

The system SHALL filter gifts by performing a case-insensitive substring match of the `q` query parameter against the gift `name` field (`name LIKE %q%`). When `q` is absent or empty the system SHALL return the first ten gifts without a name filter. (Implemented in `app/Http/Controllers/GiftController.php:117-118`)

#### Scenario: Matching gifts are returned

- **GIVEN** an authenticated admin and gifts named "Voucher 50k" and "Weekend Trip" exist
- **WHEN** the client sends `GET settings/gifts/scan?q=voucher`
- **THEN** the response body contains only gifts whose name includes "voucher" (case-insensitive) and does not include "Weekend Trip"

#### Scenario: No matching gifts returns empty data array

- **GIVEN** an authenticated admin and no gift name contains "xyz"
- **WHEN** the client sends `GET settings/gifts/scan?q=xyz`
- **THEN** the response body is `{"data":[]}` with HTTP 200

### Requirement: Optional Active Status Filter

The system SHALL apply a filter on the `active` column only when the `active` query parameter is present in the request. When `active` is absent the system SHALL return gifts of any active status. (Implemented in `app/Http/Controllers/GiftController.php:119-121`)

#### Scenario: Active filter restricts to active gifts

- **GIVEN** an authenticated admin and both active and inactive gifts exist with the matching name
- **WHEN** the client sends `GET settings/gifts/scan?q=voucher&active=1`
- **THEN** the response includes only gifts with `active` equal to `1`

#### Scenario: Inactive filter restricts to inactive gifts

- **GIVEN** an authenticated admin and both active and inactive gifts exist
- **WHEN** the client sends `GET settings/gifts/scan?active=0`
- **THEN** the response includes only gifts with `active` equal to `0`

#### Scenario: Omitting active param returns both statuses

- **GIVEN** an authenticated admin and both active and inactive gifts exist with the matching name
- **WHEN** the client sends `GET settings/gifts/scan?q=voucher` without an `active` parameter
- **THEN** the response may include gifts regardless of their `active` value

### Requirement: Result Cap of Ten

The system SHALL return at most ten gift records per request. No pagination cursor or total count is included in the response. (Implemented in `app/Http/Controllers/GiftController.php:122`)

#### Scenario: Large result set is capped

- **GIVEN** an authenticated admin and more than ten gifts whose names contain "a"
- **WHEN** the client sends `GET settings/gifts/scan?q=a`
- **THEN** the `data` array in the response contains exactly ten elements

### Requirement: Soft-Deleted Gifts Excluded

The system SHALL never include soft-deleted gifts in the response. (Implemented in `app/Models/Gift.php:10-14`)

#### Scenario: Deleted gift does not appear

- **GIVEN** an authenticated admin and a gift that has been soft-deleted
- **WHEN** the client sends `GET settings/gifts/scan` with a query that would otherwise match the deleted gift's name
- **THEN** the deleted gift does not appear in the `data` array

### Requirement: Full Gift Record Serialization

The system SHALL serialize each matched gift as a full record including all catalogue columns plus the computed field `quantity_available` (equal to `quantity` minus `used`) and the resolved image URL (or the `noimage.png` placeholder when no image is set). (Implemented in `app/Models/Gift.php:16-27`)

#### Scenario: Response payload carries stock and image

- **GIVEN** an authenticated admin and a gift with `quantity` 50, `used` 7, and a stored image
- **WHEN** the client sends `GET settings/gifts/scan?q=<name substring>`
- **THEN** the matching element in `data` includes `"quantity_available": 43` and a non-empty `"image"` URL

### Requirement: JSON Envelope Shape

The system SHALL return HTTP 200 with `Content-Type: application/json` and a body of the form `{"data":[…]}` for every successful request, including when no gifts match (empty array). (Implemented in `app/Http/Controllers/GiftController.php:123-125`)

#### Scenario: Successful response uses data envelope

- **GIVEN** an authenticated admin
- **WHEN** the client sends `GET settings/gifts/scan?q=voucher` and matching gifts exist
- **THEN** the response status is 200, the content type is `application/json`, and the body is `{"data":[…]}` where each element is a gift object

#### Scenario: Zero matches still use data envelope

- **GIVEN** an authenticated admin and no gift matches the query
- **WHEN** the client sends `GET settings/gifts/scan?q=nomatch`
- **THEN** the response status is 200 and the body is `{"data":[]}`
