# 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)