feat(loyalty): implement complete loyalty module MVP

Add stamp-based and points-based loyalty programs for vendors with:

Database Models (5 tables):
- loyalty_programs: Vendor program configuration
- loyalty_cards: Customer cards with stamp/point balances
- loyalty_transactions: Immutable audit log
- staff_pins: Fraud prevention PINs (bcrypt hashed)
- apple_device_registrations: Apple Wallet push tokens

Services:
- program_service: Program CRUD and statistics
- card_service: Customer enrollment and card lookup
- stamp_service: Stamp operations with anti-fraud checks
- points_service: Points earning and redemption
- pin_service: Staff PIN management with lockout
- wallet_service: Unified wallet abstraction
- google_wallet_service: Google Wallet API integration
- apple_wallet_service: Apple Wallet .pkpass generation

API Routes:
- Admin: /api/v1/admin/loyalty/* (programs list, stats)
- Vendor: /api/v1/vendor/loyalty/* (stamp, points, cards, PINs)
- Public: /api/v1/loyalty/* (enrollment, Apple Web Service)

Anti-Fraud Features:
- Staff PIN verification (configurable per program)
- Cooldown period between stamps (default 15 min)
- Daily stamp limits (default 5/day)
- PIN lockout after failed attempts

Wallet Integration:
- Google Wallet: LoyaltyClass and LoyaltyObject management
- Apple Wallet: .pkpass generation with PKCS#7 signing
- Apple Web Service endpoints for device registration/updates

Also includes:
- Alembic migration for all tables with indexes
- Localization files (en, fr, de, lu)
- Module documentation
- Phase 2 interface and user journey plan

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 23:04:00 +01:00
parent fbcf07914e
commit b5a803cde8
44 changed files with 8073 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
# app/modules/loyalty/tasks/wallet_sync.py
"""
Wallet synchronization task.
Handles syncing loyalty card data to Google Wallet and Apple Wallet
for cards that may have missed real-time updates.
"""
import logging
from celery import shared_task
logger = logging.getLogger(__name__)
@shared_task(name="loyalty.sync_wallet_passes")
def sync_wallet_passes() -> dict:
"""
Sync wallet passes for cards that may be out of sync.
This catches any cards that missed real-time updates due to
errors or network issues.
Returns:
Summary of synced passes
"""
from datetime import UTC, datetime, timedelta
from app.core.database import SessionLocal
from app.modules.loyalty.models import LoyaltyCard, LoyaltyTransaction
from app.modules.loyalty.services import wallet_service
db = SessionLocal()
try:
# Find cards with transactions in the last hour that have wallet IDs
one_hour_ago = datetime.now(UTC) - timedelta(hours=1)
# Get card IDs with recent transactions
recent_tx_card_ids = (
db.query(LoyaltyTransaction.card_id)
.filter(LoyaltyTransaction.transaction_at >= one_hour_ago)
.distinct()
.all()
)
card_ids = [row[0] for row in recent_tx_card_ids]
if not card_ids:
logger.info("No cards with recent transactions to sync")
return {
"status": "success",
"cards_checked": 0,
"google_synced": 0,
"apple_synced": 0,
}
# Get cards with wallet integrations
cards = (
db.query(LoyaltyCard)
.filter(
LoyaltyCard.id.in_(card_ids),
(LoyaltyCard.google_object_id.isnot(None))
| (LoyaltyCard.apple_serial_number.isnot(None)),
)
.all()
)
google_synced = 0
apple_synced = 0
for card in cards:
try:
results = wallet_service.sync_card_to_wallets(db, card)
if results.get("google_wallet"):
google_synced += 1
if results.get("apple_wallet"):
apple_synced += 1
except Exception as e:
logger.warning(f"Failed to sync card {card.id} to wallets: {e}")
logger.info(
f"Wallet sync complete: {len(cards)} cards checked, "
f"{google_synced} Google, {apple_synced} Apple"
)
return {
"status": "success",
"cards_checked": len(cards),
"google_synced": google_synced,
"apple_synced": apple_synced,
}
except Exception as e:
logger.error(f"Wallet sync task failed: {e}")
return {
"status": "error",
"error": str(e),
}
finally:
db.close()