feat(loyalty): implement Phase 2 - company-wide points system
Complete implementation of loyalty module Phase 2 features: Database & Models: - Add company_id to LoyaltyProgram for chain-wide loyalty - Add company_id to LoyaltyCard for multi-location support - Add CompanyLoyaltySettings model for admin-controlled settings - Add points expiration, welcome bonus, and minimum redemption fields - Add POINTS_EXPIRED, WELCOME_BONUS transaction types Services: - Update program_service for company-based queries - Update card_service with enrollment and welcome bonus - Update points_service with void_points for returns - Update stamp_service for company context - Update pin_service for company-wide operations API Endpoints: - Admin: Program listing with stats, company detail views - Vendor: Terminal operations, card management, settings - Storefront: Customer card/transactions, self-enrollment UI Templates: - Admin: Programs dashboard, company detail, settings - Vendor: Terminal, cards list, card detail, settings, stats, enrollment - Storefront: Dashboard, history, enrollment, success pages Background Tasks: - Point expiration task (daily, based on inactivity) - Wallet sync task (hourly) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
"""
|
||||
Staff PIN database model.
|
||||
|
||||
Company-based staff PINs:
|
||||
- PINs belong to a company's loyalty program
|
||||
- Each vendor (location) has its own set of staff PINs
|
||||
- Staff can only use PINs at their assigned location
|
||||
|
||||
Provides fraud prevention by requiring staff to authenticate
|
||||
before performing stamp/points operations. Includes:
|
||||
- Secure PIN hashing with bcrypt
|
||||
@@ -34,13 +39,25 @@ class StaffPin(Base, TimestampMixin):
|
||||
Each staff member can have their own PIN to authenticate
|
||||
stamp/points operations. PINs are hashed with bcrypt and
|
||||
include lockout protection.
|
||||
|
||||
PINs are scoped to a specific vendor (location) within the
|
||||
company's loyalty program.
|
||||
"""
|
||||
|
||||
__tablename__ = "staff_pins"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Relationships
|
||||
# Company association
|
||||
company_id = Column(
|
||||
Integer,
|
||||
ForeignKey("companies.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="Company that owns the loyalty program",
|
||||
)
|
||||
|
||||
# Program and vendor relationships
|
||||
program_id = Column(
|
||||
Integer,
|
||||
ForeignKey("loyalty_programs.id", ondelete="CASCADE"),
|
||||
@@ -52,7 +69,7 @@ class StaffPin(Base, TimestampMixin):
|
||||
ForeignKey("vendors.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="Denormalized for query performance",
|
||||
comment="Vendor (location) where this staff member works",
|
||||
)
|
||||
|
||||
# Staff identity
|
||||
@@ -104,17 +121,19 @@ class StaffPin(Base, TimestampMixin):
|
||||
# =========================================================================
|
||||
# Relationships
|
||||
# =========================================================================
|
||||
company = relationship("Company", backref="staff_pins")
|
||||
program = relationship("LoyaltyProgram", back_populates="staff_pins")
|
||||
vendor = relationship("Vendor", backref="staff_pins")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index("idx_staff_pin_company_active", "company_id", "is_active"),
|
||||
Index("idx_staff_pin_vendor_active", "vendor_id", "is_active"),
|
||||
Index("idx_staff_pin_program_active", "program_id", "is_active"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<StaffPin(id={self.id}, name='{self.name}', active={self.is_active})>"
|
||||
return f"<StaffPin(id={self.id}, name='{self.name}', vendor_id={self.vendor_id}, active={self.is_active})>"
|
||||
|
||||
# =========================================================================
|
||||
# PIN Operations
|
||||
|
||||
Reference in New Issue
Block a user