# Proposal: Transaction Categories (What Was Sold)

**Created:** 2026-04-19
**Status:** Approved — ready to implement
**Priority:** Urgent (client requirement for production launch)

## Context

The client requires sellers to select what was sold (e.g., Men, Women, Accessories, Kids) when entering a loyalty transaction. This enables per-category sales analytics for merchants. Each store can have 4-5 categories, configured by the admin or merchant owner.

## Data Model

### New table: `store_transaction_categories`

| Column | Type | Description |
|---|---|---|
| `id` | Integer PK | Auto-increment |
| `store_id` | FK → stores | Store this category belongs to |
| `name` | String(100) | Display name (e.g., "Men", "Women", "Accessories") |
| `display_order` | Integer | Sort order in the selector (1, 2, 3...) |
| `is_active` | Boolean | Soft-disable without deleting |
| `created_at` | DateTime | Timestamp |
| `updated_at` | DateTime | Timestamp |

**Unique constraint:** `(store_id, name)` — no duplicate names per store.
**Max categories per store:** 10 (enforced at API level, soft limit).

### Modify: `loyalty_transactions`

| Column | Type | Description |
|---|---|---|
| `category_id` | FK → store_transaction_categories (nullable) | What was sold |

Nullable because: existing transactions don't have categories, and enrollment/expiration/void transactions don't involve a sale.

## Migration

`loyalty_007_add_transaction_categories.py`:
1. Create `store_transaction_categories` table
2. Add `category_id` FK column to `loyalty_transactions`

## API Endpoints

### Admin CRUD (on behalf of store)

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/loyalty/stores/{store_id}/categories` | List categories for a store |
| `POST` | `/admin/loyalty/stores/{store_id}/categories` | Create category |
| `PATCH` | `/admin/loyalty/stores/{store_id}/categories/{id}` | Update category (name, order, active) |
| `DELETE` | `/admin/loyalty/stores/{store_id}/categories/{id}` | Delete category |

### Store/Merchant CRUD (own store)

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/store/loyalty/categories` | List categories for current store |
| `POST` | `/store/loyalty/categories` | Create category |
| `PATCH` | `/store/loyalty/categories/{id}` | Update category |
| `DELETE` | `/store/loyalty/categories/{id}` | Delete category |

### Modified transaction endpoints

`POST /store/loyalty/stamp`, `POST /store/loyalty/points/earn`, `POST /store/loyalty/points/redeem` — add optional `category_id` field to request body.

`GET /store/loyalty/transactions` and card detail transactions — include `category_name` in response.

## Schemas

### CategoryCreate
```python
class CategoryCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    display_order: int = Field(default=0, ge=0)
```

### CategoryUpdate
```python
class CategoryUpdate(BaseModel):
    name: str | None = Field(None, min_length=1, max_length=100)
    display_order: int | None = Field(None, ge=0)
    is_active: bool | None = None
```

### CategoryResponse
```python
class CategoryResponse(BaseModel):
    id: int
    store_id: int
    name: str
    display_order: int
    is_active: bool
```

## Service Layer

New `category_service.py` with:
- `list_categories(db, store_id)` — ordered by display_order
- `create_category(db, store_id, data)` — enforce max 10 per store
- `update_category(db, category_id, store_id, data)` — ownership check
- `delete_category(db, category_id, store_id)` — ownership check
- `validate_category_for_store(db, category_id, store_id)` — used by stamp/points services

## UI Changes

### Admin panel
- New "Transaction Categories" section on the merchant store detail page
- Table with name, order, active toggle, edit/delete buttons
- "Add Category" button with modal form

### Store/Merchant settings
- New "Transaction Categories" tab or section on the store settings page
- Same CRUD UI as admin

### Web terminal
- Category selector (radio buttons or button group) shown before the PIN modal
- Only shown when the store has categories configured
- Stored on the transaction

### Android terminal
- Same category selector in the transaction flow
- Categories cached locally (refreshed from API periodically)
- Works offline (category list is static, just an ID reference)

## Card Lookup Response

Add `categories: list[CategoryResponse]` to `CardLookupResponse` so the terminal has the list without a separate API call.

Or: fetch categories once on terminal load and cache them.

**Recommendation:** Fetch once on terminal load — categories are store-level, not card-level. Add `GET /store/loyalty/categories` call to terminal init.

## Transaction History

`TransactionResponse` gets a new `category_name: str | None` field so card detail and terminal transaction tables can show what was sold.

## Analytics

The `analytics_service` can later be extended to group revenue by category — but that's a future enhancement, not in this scope.

## Files to create/modify

| File | Action |
|---|---|
| `app/modules/loyalty/models/transaction_category.py` | NEW — model |
| `app/modules/loyalty/models/loyalty_transaction.py` | Add `category_id` FK |
| `app/modules/loyalty/migrations/versions/loyalty_007_*.py` | NEW — migration |
| `app/modules/loyalty/services/category_service.py` | NEW — CRUD service |
| `app/modules/loyalty/schemas/category.py` | NEW — Pydantic schemas |
| `app/modules/loyalty/schemas/card.py` | Add `category_id` to stamp/points requests |
| `app/modules/loyalty/routes/api/admin.py` | Add admin CRUD endpoints |
| `app/modules/loyalty/routes/api/store.py` | Add store CRUD + modify transaction endpoints |
| `app/modules/loyalty/services/stamp_service.py` | Accept + store `category_id` |
| `app/modules/loyalty/services/points_service.py` | Accept + store `category_id` |
| `app/modules/loyalty/templates/loyalty/store/terminal.html` | Add category selector |
| `app/modules/loyalty/static/store/js/loyalty-terminal.js` | Load categories, pass to transactions |
| `clients/terminal-android/.../data/model/ApiModels.kt` | Add `category_id` to request models |

## Effort

~0.5 day backend (model + migration + service + routes + schemas)
~0.5 day frontend (web terminal category selector + admin CRUD UI)
Android terminal gets it for free via the API models already scaffolded.

## Test plan

- Create 5 categories for a store via admin API
- Verify max 10 limit
- Add stamp with category_id → transaction has category
- Add points with category_id → transaction has category
- Transaction history shows category name
- Terminal loads categories on init
- Category selector appears only when categories exist
- Delete category → existing transactions keep the name (FK nullable, name denormalized or joined)
