docs: add proposal for transaction categories (what was sold)
Some checks failed
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 20s
CI / pytest (push) Failing after 2h34m11s
CI / validate (push) Successful in 35s
CI / dependency-scanning (push) Successful in 44s
CI / docs (push) Has been skipped

Client requirement: sellers must select a product category (e.g.,
Men, Women, Accessories, Kids) when entering loyalty transactions.
Categories are per-store, configured via admin/merchant CRUD.

Proposal covers: data model (StoreTransactionCategory + FK on
transactions), CRUD API for admin + store, web terminal UI, Android
terminal integration, and analytics extension path.

Priority: urgent for production launch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 00:29:41 +02:00
parent 457350908a
commit cd4f83f2cb

View File

@@ -0,0 +1,174 @@
# 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)