feat(loyalty): transaction categories (what was sold)
Some checks failed
CI / ruff (push) Successful in 20s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Merchants can configure per-store product categories (e.g., Men,
Women, Accessories, Kids) that sellers select when entering loyalty
transactions. Enables per-category sales analytics.

Backend:
- New model: StoreTransactionCategory (store-scoped, max 10 per store)
- Migration loyalty_007: creates table + adds category_id FK on
  loyalty_transactions
- New category_service.py with CRUD + validation
- New schemas/category.py (Create, Update, Response, ListResponse)
- Admin CRUD: GET/POST/PATCH/DELETE /admin/loyalty/stores/{id}/categories
- Store CRUD: GET/POST/PATCH/DELETE /store/loyalty/categories
- Stamp/Points request schemas accept optional category_id
- Stamp/Points services pass category_id to transaction creation
- TransactionResponse includes category_id + category_name

342 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 12:23:17 +02:00
parent cd4f83f2cb
commit 1cf9fea40a
14 changed files with 508 additions and 0 deletions

View File

@@ -49,6 +49,10 @@ from app.modules.loyalty.models.staff_pin import (
# Model
StaffPin,
)
from app.modules.loyalty.models.transaction_category import (
# Model
StoreTransactionCategory,
)
__all__ = [
# Enums
@@ -62,4 +66,5 @@ __all__ = [
"StaffPin",
"AppleDeviceRegistration",
"MerchantLoyaltySettings",
"StoreTransactionCategory",
]

View File

@@ -104,6 +104,12 @@ class LoyaltyTransaction(Base, TimestampMixin):
index=True,
comment="Staff PIN used for this operation",
)
category_id = Column(
Integer,
ForeignKey("store_transaction_categories.id", ondelete="SET NULL"),
nullable=True,
comment="Product category (e.g., Men, Women, Accessories)",
)
# Related transaction (for voids/returns)
related_transaction_id = Column(

View File

@@ -0,0 +1,44 @@
# app/modules/loyalty/models/transaction_category.py
"""
Store-scoped transaction categories.
Merchants configure 4-10 categories per store (e.g., Men, Women,
Accessories, Kids) that sellers select when entering transactions.
"""
from sqlalchemy import (
Boolean,
Column,
ForeignKey,
Index,
Integer,
String,
UniqueConstraint,
)
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class StoreTransactionCategory(Base, TimestampMixin):
"""Product category for loyalty transactions."""
__tablename__ = "store_transaction_categories"
id = Column(Integer, primary_key=True, index=True)
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
name = Column(String(100), nullable=False)
display_order = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, nullable=False, default=True)
# Relationships
store = relationship("Store")
__table_args__ = (
UniqueConstraint("store_id", "name", name="uq_store_category_name"),
Index("idx_store_category_store", "store_id", "is_active"),
)
def __repr__(self):
return f"<StoreTransactionCategory(id={self.id}, store={self.store_id}, name='{self.name}')>"