Files
orion/app/modules/loyalty/docs/business-logic.md
Samir Boulahtit f141cc4e6a docs: migrate module documentation to single source of truth
Move 39 documentation files from top-level docs/ into each module's
docs/ folder, accessible via symlinks from docs/modules/. Create
data-model.md files for 10 modules with full schema documentation.
Replace originals with redirect stubs. Remove empty guide stubs.

Modules migrated: tenancy, billing, loyalty, marketplace, orders,
messaging, cms, catalog, inventory, hosting, prospecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:38:37 +01:00

265 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Loyalty Business Logic
Core algorithms, anti-fraud systems, and wallet integration logic for the loyalty module.
## Anti-Fraud System
The loyalty module implements a multi-layer fraud prevention system to prevent abuse of stamp and points operations.
### Layer 1: Staff PIN Verification
Every stamp/points operation can require a staff PIN. PINs are bcrypt-hashed and scoped to a specific store within a merchant.
**Flow:**
1. Staff enters 4-digit PIN on terminal
2. System checks all active PINs for the program
3. On match: records success, updates `last_used_at`
4. On mismatch: increments `failed_attempts`
5. After N failures (configurable, default 5): PIN is locked for M minutes (default 30)
**PIN Policy** (set via `MerchantLoyaltySettings.staff_pin_policy`):
| Policy | Behavior |
|--------|----------|
| `REQUIRED` | All stamp/point operations require PIN |
| `OPTIONAL` | PIN can be provided but not required |
| `DISABLED` | PIN entry is hidden from UI |
### Layer 2: Stamp Cooldown
Prevents rapid-fire stamping (e.g., customer stamps 10 times in one visit).
- Configurable via `LoyaltyProgram.cooldown_minutes` (default: 15)
- Checks `LoyaltyCard.last_stamp_at` against current time
- Returns `next_stamp_available` timestamp in response
### Layer 3: Daily Stamp Limits
Prevents excessive stamps per day per card.
- Configurable via `LoyaltyProgram.max_daily_stamps` (default: 5)
- Counts today's `STAMP_EARNED` transactions for the card
- Returns `remaining_stamps_today` in response
### Layer 4: Audit Trail
Every transaction records:
- `staff_pin_id` — Which staff member verified
- `store_id` — Which location
- `ip_address` — Client IP (if `log_ip_addresses` enabled)
- `user_agent` — Client device
- `transaction_at` — Exact timestamp
## Stamp Operations
### Adding a Stamp
```
Input: card_id, staff_pin (optional), store_id
Checks:
1. Card is active
2. Program is active and stamps-enabled
3. Staff PIN valid (if required by policy)
4. Cooldown elapsed since last_stamp_at
5. Daily limit not reached
Action:
- card.stamp_count += 1
- card.total_stamps_earned += 1
- card.last_stamp_at = now
- Create STAMP_EARNED transaction
- Sync wallet passes
Output:
- stamp_count, stamps_target, stamps_until_reward
- reward_earned (true if stamp_count >= target)
- next_stamp_available, remaining_stamps_today
```
### Redeeming Stamps
```
Input: card_id, staff_pin (optional), store_id
Checks:
1. stamp_count >= stamps_target
2. Staff PIN valid (if required)
Action:
- card.stamp_count -= stamps_target (keeps overflow stamps)
- card.stamps_redeemed += 1
- Create STAMP_REDEEMED transaction (with reward_description)
- Sync wallet passes
Output:
- success, reward_description, redemption_count
- remaining stamp_count after reset
```
### Voiding Stamps
```
Input: card_id, stamps_count OR transaction_id, staff_pin, store_id
Checks:
1. allow_void_transactions enabled in merchant settings
2. Card has enough stamps to void
3. Staff PIN valid (if required)
Action:
- card.stamp_count -= stamps_count
- Create STAMP_VOIDED transaction (linked to original via related_transaction_id)
- Sync wallet passes
```
## Points Operations
### Earning Points
```
Input: card_id, purchase_amount_cents, staff_pin, store_id, order_reference
Calculation:
euros = purchase_amount_cents / 100
points = floor(euros × program.points_per_euro)
Checks:
1. Card is active, program is active and points-enabled
2. Purchase amount >= minimum_purchase_cents (if configured)
3. Order reference provided (if require_order_reference enabled)
4. Staff PIN valid (if required)
Action:
- card.points_balance += points
- card.total_points_earned += points
- Create POINTS_EARNED transaction (with purchase_amount_cents)
- Sync wallet passes
Output:
- points_earned, points_balance, purchase_amount, points_per_euro
```
### Redeeming Points
```
Input: card_id, reward_id, staff_pin, store_id
Checks:
1. Reward exists in program.points_rewards
2. card.points_balance >= reward.points_cost
3. points_balance >= minimum_redemption_points (if configured)
4. Staff PIN valid (if required)
Action:
- card.points_balance -= reward.points_cost
- card.points_redeemed += reward.points_cost
- Create POINTS_REDEEMED transaction (with reward_id, reward_description)
- Sync wallet passes
Output:
- reward name/description, points_spent, new balance
```
### Voiding Points
```
Input: card_id, transaction_id OR order_reference, staff_pin, store_id
Checks:
1. allow_void_transactions enabled
2. Original transaction found and is an earn transaction
3. Staff PIN valid (if required)
Action:
- card.points_balance -= original points
- card.total_points_voided += original points
- Create POINTS_VOIDED transaction (linked via related_transaction_id)
- Sync wallet passes
```
### Adjusting Points
Admin/store operation for manual corrections.
```
Input: card_id, points_delta (positive or negative), notes, store_id
Action:
- card.points_balance += points_delta
- Create POINTS_ADJUSTMENT transaction with notes
- Sync wallet passes
```
## Wallet Integration
### Google Wallet
Uses the Google Wallet API with a service account for server-to-server communication.
**Class (Program-level):**
- One `LoyaltyClass` per program
- Contains program name, branding (logo, hero), rewards info
- Created when program is activated; updated when settings change
**Object (Card-level):**
- One `LoyaltyObject` per card
- Contains balance (stamps or points), card number, member name
- Created on enrollment; updated on every balance change
- "Add to Wallet" URL is a JWT-signed save link
### Apple Wallet
Uses PKCS#7 signed `.pkpass` files and APNs push notifications.
**Pass Generation:**
1. Build `pass.json` with card data (stamps grid or points balance)
2. Add icon/logo/strip images
3. Create `manifest.json` (SHA256 of all files)
4. Sign manifest with PKCS#7 using certificates and private key
5. Package as `.pkpass` ZIP file
**Push Updates:**
1. When card balance changes, send APNs push to all registered devices
2. Device receives push → requests updated pass from server
3. Server generates fresh `.pkpass` with current balance
**Device Registration (Apple Web Service protocol):**
- `POST /v1/devices/{device}/registrations/{passType}/{serial}` — Register device
- `DELETE /v1/devices/{device}/registrations/{passType}/{serial}` — Unregister device
- `GET /v1/devices/{device}/registrations/{passType}` — List passes for device
- `GET /v1/passes/{passType}/{serial}` — Get latest pass
## Cross-Store Redemption
When `allow_cross_location_redemption` is enabled in merchant settings:
- Cards are scoped to the **merchant** (not individual stores)
- Customer can earn stamps at Store A and redeem at Store B
- Each transaction records which `store_id` it occurred at
- The `enrolled_at_store_id` field tracks where the customer first enrolled
When disabled, stamp/point operations are restricted to the enrollment store.
## Enrollment Flow
### Store-Initiated Enrollment
Staff enrolls customer via terminal:
1. Enter customer email (and optional name)
2. System resolves or creates customer record
3. Creates loyalty card with unique card number and QR code
4. Creates `CARD_CREATED` transaction
5. Awards welcome bonus points (if configured) via `WELCOME_BONUS` transaction
6. Creates Google Wallet object and Apple Wallet serial
7. Returns card details with "Add to Wallet" URLs
### Self-Enrollment (Public)
Customer enrolls via public page (if `allow_self_enrollment` enabled):
1. Customer visits `/loyalty/join` page
2. Enters email and name
3. System creates customer + card
4. Redirected to success page with card number
5. Can add to Google/Apple Wallet from success page
## Scheduled Tasks
| Task | Schedule | Logic |
|------|----------|-------|
| `loyalty.sync_wallet_passes` | Hourly | Re-sync cards that missed real-time wallet updates |
| `loyalty.expire_points` | Daily 02:00 | Find cards with `points_expiration_days` set and no activity within that window; create `POINTS_EXPIRED` transaction |
## Feature Gating
The loyalty module declares these billable features via `LoyaltyFeatureProvider`:
- `loyalty_stamps`, `loyalty_points`, `loyalty_hybrid`
- `loyalty_cards`, `loyalty_enrollment`, `loyalty_staff_pins`
- `loyalty_anti_fraud`, `loyalty_google_wallet`, `loyalty_apple_wallet`
- `loyalty_stats`, `loyalty_reports`
These integrate with the [billing module's feature gating system](../billing/feature-gating.md) to control access based on subscription tier.