docs: add proposal for transaction categories (what was sold)
Some checks failed
Some checks failed
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:
174
docs/proposals/transaction-categories.md
Normal file
174
docs/proposals/transaction-categories.md
Normal 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)
|
||||
Reference in New Issue
Block a user