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

275
docs/modules/loyalty.md Normal file
View File

@@ -0,0 +1,275 @@
# Loyalty Module
The Loyalty Module provides stamp-based and points-based loyalty programs for Wizamart vendors with Google Wallet and Apple Wallet integration.
## Overview
| Aspect | Description |
|--------|-------------|
| Module Code | `loyalty` |
| Dependencies | `customers` |
| Status | Phase 1 MVP Complete |
### Key Features
- **Stamp-based loyalty**: Collect N stamps, get a reward (e.g., "Buy 10 coffees, get 1 free")
- **Points-based loyalty**: Earn points per euro spent, redeem for rewards
- **Hybrid programs**: Support both stamps and points simultaneously
- **Anti-fraud system**: Staff PINs, cooldown periods, daily limits, lockout protection
- **Wallet integration**: Google Wallet and Apple Wallet pass generation
- **Full audit trail**: Transaction logging with IP, user agent, and staff attribution
## Entity Model
```
┌─────────────────┐ ┌─────────────────┐
│ Vendor │───────│ LoyaltyProgram │
└─────────────────┘ 1:1 └─────────────────┘
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ StaffPin │ │LoyaltyCard│ │ (config)│
└──────────┘ └──────────┘ └──────────┘
│ │
│ ▼
│ ┌──────────────┐
└───▶│ Transaction │
└──────────────┘
┌──────────────────────┐
│AppleDeviceRegistration│
└──────────────────────┘
```
### Database Tables
| Table | Purpose |
|-------|---------|
| `loyalty_programs` | Vendor's program configuration (type, targets, branding) |
| `loyalty_cards` | Customer cards with stamp/point balances |
| `loyalty_transactions` | Immutable audit log of all operations |
| `staff_pins` | Hashed PINs for fraud prevention |
| `apple_device_registrations` | Apple Wallet push notification tokens |
## Configuration
Environment variables (prefix: `LOYALTY_`):
```bash
# Anti-fraud defaults
LOYALTY_DEFAULT_COOLDOWN_MINUTES=15
LOYALTY_MAX_DAILY_STAMPS=5
LOYALTY_PIN_MAX_FAILED_ATTEMPTS=5
LOYALTY_PIN_LOCKOUT_MINUTES=30
# Points
LOYALTY_DEFAULT_POINTS_PER_EURO=10
# Google Wallet
LOYALTY_GOOGLE_ISSUER_ID=3388000000012345678
LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON=/path/to/service-account.json
# Apple Wallet
LOYALTY_APPLE_PASS_TYPE_ID=pass.com.example.loyalty
LOYALTY_APPLE_TEAM_ID=ABCD1234
LOYALTY_APPLE_WWDR_CERT_PATH=/path/to/wwdr.pem
LOYALTY_APPLE_SIGNER_CERT_PATH=/path/to/signer.pem
LOYALTY_APPLE_SIGNER_KEY_PATH=/path/to/signer.key
```
## API Endpoints
### Vendor Endpoints (`/api/v1/vendor/loyalty/`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/program` | Get vendor's loyalty program |
| `POST` | `/program` | Create loyalty program |
| `PATCH` | `/program` | Update loyalty program |
| `GET` | `/stats` | Get program statistics |
| `GET` | `/cards` | List customer cards |
| `POST` | `/cards/enroll` | Enroll customer in program |
| `POST` | `/cards/lookup` | Look up card by QR/number |
| `POST` | `/stamp` | Add stamp to card |
| `POST` | `/stamp/redeem` | Redeem stamps for reward |
| `POST` | `/points` | Earn points from purchase |
| `POST` | `/points/redeem` | Redeem points for reward |
| `GET` | `/pins` | List staff PINs |
| `POST` | `/pins` | Create staff PIN |
| `PATCH` | `/pins/{id}` | Update staff PIN |
| `DELETE` | `/pins/{id}` | Delete staff PIN |
| `POST` | `/pins/{id}/unlock` | Unlock locked PIN |
### Admin Endpoints (`/api/v1/admin/loyalty/`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/programs` | List all loyalty programs |
| `GET` | `/programs/{id}` | Get specific program |
| `GET` | `/programs/{id}/stats` | Get program statistics |
| `GET` | `/stats` | Platform-wide statistics |
### Public Endpoints (`/api/v1/loyalty/`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/programs/{vendor_code}` | Get program info for enrollment |
| `GET` | `/passes/apple/{serial}.pkpass` | Download Apple Wallet pass |
| `POST` | `/apple/v1/devices/...` | Apple Web Service: register device |
| `DELETE` | `/apple/v1/devices/...` | Apple Web Service: unregister |
| `GET` | `/apple/v1/devices/...` | Apple Web Service: get updates |
| `GET` | `/apple/v1/passes/...` | Apple Web Service: get pass |
## Anti-Fraud System
### Staff PIN Verification
All stamp and points operations require staff PIN verification (configurable per program).
```python
# PIN is hashed with bcrypt
pin.set_pin("1234") # Stores bcrypt hash
pin.verify_pin("1234") # Returns True/False
```
### Lockout Protection
- **Max failed attempts**: 5 (configurable)
- **Lockout duration**: 30 minutes (configurable)
- After lockout expires, PIN can be used again
- Admin can manually unlock via API
### Cooldown Period
Prevents rapid stamp collection (fraud prevention):
```
Customer scans card → Gets stamp → Must wait 15 minutes → Can get next stamp
```
### Daily Limits
Maximum stamps per card per day (default: 5).
## Wallet Integration
### Google Wallet
Architecture: **Server-side storage with API updates**
1. Program created → Create `LoyaltyClass` via Google API
2. Customer enrolls → Create `LoyaltyObject` via Google API
3. Stamp/points change → `PATCH` the object
4. Generate JWT for "Add to Wallet" button
No device registration needed - Google syncs automatically.
### Apple Wallet
Architecture: **Push notification model**
1. Customer adds pass → Device registers with our server
2. Stamp/points change → Send push notification to APNs
3. Device receives push → Fetches updated pass from our server
Requires `apple_device_registrations` table for push tokens.
## Usage Examples
### Create a Loyalty Program
```python
from app.modules.loyalty.services import program_service
from app.modules.loyalty.schemas import ProgramCreate
data = ProgramCreate(
loyalty_type="stamps",
stamps_target=10,
stamps_reward_description="Free coffee",
cooldown_minutes=15,
max_daily_stamps=5,
require_staff_pin=True,
card_color="#4F46E5",
)
program = program_service.create_program(db, vendor_id=1, data=data)
```
### Enroll a Customer
```python
from app.modules.loyalty.services import card_service
card = card_service.enroll_customer(db, customer_id=123, vendor_id=1)
# Returns LoyaltyCard with unique card_number and qr_code_data
```
### Add a Stamp
```python
from app.modules.loyalty.services import stamp_service
result = stamp_service.add_stamp(
db,
qr_code="abc123xyz",
staff_pin="1234",
ip_address="192.168.1.1",
)
# Returns dict with stamp_count, reward_earned, next_stamp_available_at, etc.
```
### Earn Points from Purchase
```python
from app.modules.loyalty.services import points_service
result = points_service.earn_points(
db,
card_number="123456789012",
purchase_amount_cents=2500, # €25.00
order_reference="ORD-12345",
staff_pin="1234",
)
# Returns dict with points_earned (250 at 10pts/€), points_balance, etc.
```
## Services
| Service | Purpose |
|---------|---------|
| `program_service` | Program CRUD and statistics |
| `card_service` | Card enrollment, lookup, management |
| `stamp_service` | Stamp operations with anti-fraud |
| `points_service` | Points operations and redemption |
| `pin_service` | Staff PIN CRUD and verification |
| `wallet_service` | Unified wallet abstraction |
| `google_wallet_service` | Google Wallet API integration |
| `apple_wallet_service` | Apple Wallet pass generation |
## Scheduled Tasks
| Task | Schedule | Description |
|------|----------|-------------|
| `loyalty.sync_wallet_passes` | Hourly | Sync cards that missed real-time updates |
| `loyalty.expire_points` | Daily 02:00 | Expire old points (future enhancement) |
## Localization
Available in 4 languages:
- English (`en.json`)
- French (`fr.json`)
- German (`de.json`)
- Luxembourgish (`lu.json`)
## Future Enhancements (Phase 2)
- Rewards catalog with configurable tiers
- Customer tiers (Bronze/Silver/Gold)
- Referral program
- Gamification (spin wheel, scratch cards)
- POS integration
- Points expiration rules
- Batch import of existing loyalty cards