# Gifts (CRUD) — Contracts

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

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

External HTTP contract exposed by the `gifts-crud` unit — the Laravel `resource settings/gifts` (`GiftController`) under the admin group (`['web','admin']`) with the `settings` prefix. It is a server-rendered **HTML/form** CRUD (redirect-back, not JSON), requires an authenticated admin session, and is a thin Encore\Admin `ModelForm` scaffold with real business fields and lifecycle hooks. `store`/`update`/`destroy`/`show`/`create` are framework defaults driven by `form()`; only `index`/`edit`/`grid`/`form` are overridden. The `scan` JSON action on the same controller is the separate `gifts-scan` unit. 🟢 (`routes/web.php:86-91`, `GiftController.php`)

---

## Resource surface `settings/gifts` 🟢

| Method | Path | Purpose | Notes |
|--------|------|---------|-------|
| GET | `settings/gifts` | List + inline create form | Two-column: grid + `Widgets\Form`. 🟢 (`:19-41`) |
| POST | `settings/gifts` | Create a gift | `ModelForm::store` via `form()`; hooks run. 🟢 (`:76-113`) |
| GET | `settings/gifts/{id}/edit` | Edit form | `form()->edit($id)`; unknown id → `404`. 🟢 (`:43-49`) |
| PUT/PATCH | `settings/gifts/{id}` | Update a gift | `ModelForm::update`; `quantity ≥ used`. 🟢 (`:76-113`) |
| DELETE | `settings/gifts/{id}` | Delete a gift | `ModelForm::destroy` → `SoftDeletes` (`deleted_at`). **Enabled on all rows.** 🟢 (`Gift.php:10-14`; `:72`) |
| GET | `settings/gifts/{id}` (show) · GET `settings/gifts/create` | Framework defaults | Not surfaced in the UI (no create button; creation via inline widget). 🟡 (`:67`) |

- **Auth:** required; anonymous → `302 auth/login`. 🟢 (`routes/web.php:24-28`)
- **CSRF:** required on POST/PUT/DELETE (Laravel `web` middleware; Encore\Admin forms emit the token). 🟢
- **Encoding:** create/update accept `multipart/form-data` (file upload for `image`). 🟢 (`:32,80`)
- **Create/Update request fields (validated via `form()`):**

  | Field | Type | Required | Rules / Notes |
  |-------|------|----------|---------------|
  | `name` | string | ✅ | `required`. 🟢 (`:79`) |
  | `image` | file | ❌ | `max:1024` (KB) `|mimes:jpeg,png,jpg,gif,svg,webp`, stored to `dir('/')`, resized to 300×300 on save. 🟢 (`:80-81,106-111`) |
  | `points` | number | ❌ | `nullable|numeric`, `min(0)`; blank → `0`. `decimal(8,1)` storage. 🟢 (`:82,100`) |
  | `limit` | number | ❌ | `nullable|numeric`, `min(0)`; blank → `0`. Per-customer cap (0 = unlimited). 🟢 (`:83,101`) |
  | `quantity` | number | ❌ | `nullable|numeric`; on **edit** also `min:{used}` (msg "Số lượng không được bé hơn tổng đã dùng"); blank → `0`. 🟢 (`:84-92,102`) |
  | `active` | switch (0/1) | ❌ | Redeemable flag; inline create defaults to `1`. 🟢 (`:36,97`) |
  | `used` | — | ✖ (not user-set) | Forced to `0` for a new gift by the `saving` hook; preserved on edit; shown read-only. 🟢 (`:93-95,103`) |

- **Responses:** `200` HTML for list/edit; `302` redirect back on create/update/delete; `422`/redirect-back on validation failure; `404` for an unknown id. No JSON variant (that is `gifts-scan`). 🟢
- **No row-action locks:** every grid row offers edit and delete (empty row-action closure) — unlike `brands`/`units`/`categories`. 🟢 (`:72`)

---

## Response model (serialized `Gift`) 🟢

Consumers that read a gift (grid, `gifts-scan`, redemption) see:

| Field | Source | Notes |
|-------|--------|-------|
| `id`, `name`, `points`, `limit`, `quantity`, `used`, `active`, timestamps | columns | `points` decimal(8,1) |
| `image` | `getImageAttribute` | `asset(storage_url(value))` or `asset('images/noimage.png')` placeholder. 🟢 (`Gift.php:21-23`) |
| `quantity_available` | appended accessor | `quantity − used` (computed, never stored). 🟢 (`Gift.php:18,25-27`) |

---

## Consumed contracts (owned by other units)

`gifts-crud` reads/writes only the `gifts` table through Eloquent and the local image storage; it calls no other unit's HTTP endpoint and no external service. 🟢

| Reads / writes | Owner unit | Purpose |
|----------------|------------|---------|
| `gifts` rows (CRUD, soft delete) | this unit | loyalty-reward master data |
| `storage/app/public` image files | this unit (via `Intervention\Image`) | gift thumbnails resized to 300×300 |

---

## Producer/consumer relationships

| This unit is… | Counterparty | Contract |
|---------------|--------------|----------|
| **Producer** | `customers-loyalty` | the redemption gate `Customer::checkGiftAvailable` reads `points`/`limit`/`quantity_available`/`active`; `redeemRewardPoints` increments `used` and snapshots `points` into the `customer_gift` pivot. 🟢 (`gifts` flowchart:39-40) |
| **Producer** | `gifts-scan` | `GET settings/gifts/scan` lists gifts by `name` (+ optional `active`) for the redemption picker. 🟢 (`GiftController.php:115-126`) |
| **Producer** | `customers-statistics` | the customer stats screen offers a gift picker fed by `gifts-scan`. 🟢 |

---

## Cross-cutting contract notes

- **Scaffolded CRUD with hooks:** `store`/`update`/`destroy` are `ModelForm` defaults, but `form()` adds a dynamic `quantity ≥ used` rule and `saving`/`saved` hooks (zero-defaults, `used=0` for new, 300×300 image resize). 🟢 (`:84-113`)
- **HTML/form, not JSON:** all responses are HTML pages or redirects (the JSON surface is `gifts-scan`). 🟢
- **Soft delete:** deleting a gift sets `deleted_at` and hides it, preserving redemption history. 🟢 (`Gift.php:10-14`)
- **Stock semantics:** `quantity` = stock, `used` = redeemed, `quantity_available` = derived; the out-of-stock decision (strict `=== 0`) is made by the redemption gate, not here. 🟡 (`Gift.php:25-27`; `gifts` flowchart:41)
- **Authorization:** authentication only; any admin may manage gifts. 🟡 (ADR-0009)
- **No observability:** gift mutations and image resizing emit no telemetry. 🔴 (`GiftController.php`, absence)
