Files
orion/docs/proposals/transaction-categories.md
Samir Boulahtit cd4f83f2cb
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
docs: add proposal for transaction categories (what was sold)
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>
2026-04-19 00:29:41 +02:00

6.7 KiB

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

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

CategoryUpdate

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

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)