## Context

`tnx-pos` is a Laravel 5.6 + Blade app (not an SPA) using an in-house admin package (`salipropham/laravel55-admin`). Column additions follow a one-migration-per-change pattern (see `add_reward_point_to_products_table.php`, `add_wholesale_prices_to_products_table.php`). "Settings" pages today (`brand`, `categories`, `units`, `gifts`) are each a full CRUD resource with its own table — there is no existing generic config mechanism, so `near_expiry_days` needs a new, minimal settings concept rather than reusing an existing one.

## Goals / Non-Goals

**Goals:**
- Let a product optionally carry an expiry date and a free-text promotion note.
- Surface near-expiry products in the existing product list without a separate page.
- Make the near-expiry threshold admin-configurable instead of hardcoded.
- Warn the cashier on the POS "Bán hàng" screen when a scanned/selected product is already expired, and show any promotion note.

**Non-Goals:**
- No notifications/alerts (email, push) for near-expiry products — display-only for now.
- No general-purpose settings UI framework; only the one `near_expiry_days` value is exposed on the settings page in this change.
- No historical tracking of expiry date changes.

## Decisions

**Settings table shape: `key` / `key_group` / `content` generic row store.**
Chosen by the user over a single-row typed table. Rationale: future settings can be added by inserting a row instead of writing a new migration + column. `content` is stored as a string; callers cast as needed (e.g. `(int) $setting->content`). `key` is unique; `key_group` exists purely for organizing/display grouping on future settings pages, not enforced by any behavior in this change.

**Expiry columns live directly on `products`, not a separate table.**
One expiry date per product matches current requirements (no batch/lot tracking). A `nullable date` column is the simplest fit and matches how `attr_weight` etc. already live directly on `products`.

**Near-expiry filter implemented as a query-string toggle on the existing `/products` index**, not a new route.
Matches the existing search pattern (`?q=`) already on that page and was the user's explicit choice over a dedicated page — keeps one list, one controller action.

**Threshold default of 30, read with a fallback.**
`Setting::get('near_expiry_days', 30)` (or equivalent helper) is used everywhere the threshold is needed, so a missing row (e.g. before the seeder runs in an existing environment) never breaks the filter/highlight.

**Already-expired products are excluded from "near expiry" — superseded.**
Originally the filter/highlight was scoped to "expiring soon, still sellable" only, with expired stock explicitly out of scope for a future change. That was revised during the same change: expired products now get their own red highlight and their own filter option ("Đã hết hạn") on the product list, and the POS screen warns the cashier with an icon when a scanned/added product is already expired.

## Risks / Trade-offs

- [Existing rows have no `expiry_date`] → Column is nullable with no default; existing products simply show no expiry date and are never highlighted/filtered until an admin sets one. No backfill needed.
- [`content` as a plain string loses type safety for numeric settings] → Acceptable for a single integer-like setting today; validate on write (`SettingController@update`) rather than at the schema level.
- [Timezone/"today" boundary edge cases for date comparisons] → Use `Carbon::today()` (start of day) consistently for both the highlight and filter comparisons so the two stay in sync.
- [Adding a settings page introduces a new controller/route with no precedent for a single-purpose settings form] → Kept intentionally small (one form, one field) rather than building a generic settings CRUD, to match the actual scope requested.

## Migration Plan

1. Run new migrations: add `expiry_date`/`promotion_note` to `products`; create `settings` table.
2. Seed the `near_expiry_days=30` row (via migration data insert or a seeder run once during deploy).
3. Deploy code (model/controller/view changes) alongside the migrations — no separate rollout phase needed since new columns are nullable and additive.
4. Rollback: standard `migrate:rollback` drops the new columns/table; no data migration to reverse.
