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>
This commit is contained in:
264
app/modules/loyalty/docs/business-logic.md
Normal file
264
app/modules/loyalty/docs/business-logic.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# 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.
|
||||
235
app/modules/loyalty/docs/data-model.md
Normal file
235
app/modules/loyalty/docs/data-model.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Loyalty Data Model
|
||||
|
||||
Entity relationships and database schema for the loyalty module.
|
||||
|
||||
## Entity Relationship Diagram
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Merchant │ (from tenancy module)
|
||||
│ (one program per │
|
||||
│ merchant) │
|
||||
└──────────┬───────────┘
|
||||
│ 1
|
||||
│
|
||||
┌─────┴─────┐
|
||||
│ │
|
||||
▼ 1 ▼ 1
|
||||
┌──────────┐ ┌──────────────────────┐
|
||||
│ Loyalty │ │ MerchantLoyalty │
|
||||
│ Program │ │ Settings │
|
||||
│ │ │ │
|
||||
│ type │ │ staff_pin_policy │
|
||||
│ stamps │ │ allow_self_enrollment│
|
||||
│ points │ │ allow_void │
|
||||
│ branding │ │ allow_cross_location │
|
||||
│ anti- │ │ require_order_ref │
|
||||
│ fraud │ │ log_ip_addresses │
|
||||
└──┬───┬───┘ └──────────────────────┘
|
||||
│ │
|
||||
│ │ 1..*
|
||||
│ ▼
|
||||
│ ┌──────────────┐
|
||||
│ │ StaffPin │
|
||||
│ │ │
|
||||
│ │ name │
|
||||
│ │ pin_hash │ (bcrypt)
|
||||
│ │ store_id │
|
||||
│ │ failed_ │
|
||||
│ │ attempts │
|
||||
│ │ locked_until │
|
||||
│ └──────────────┘
|
||||
│
|
||||
│ 1..*
|
||||
▼
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ LoyaltyCard │ │ Customer │ (from customers module)
|
||||
│ │ *───1 │ │
|
||||
│ card_number │ └──────────────────┘
|
||||
│ qr_code_data │
|
||||
│ stamp_count │ ┌──────────────────┐
|
||||
│ points_balance │ │ Store │ (from tenancy module)
|
||||
│ google_object_id│ *───1 │ (enrolled_at) │
|
||||
│ apple_serial │ └──────────────────┘
|
||||
│ is_active │
|
||||
└──────┬───────────┘
|
||||
│
|
||||
│ 1..*
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ LoyaltyTransaction │ (immutable audit log)
|
||||
│ │
|
||||
│ transaction_type │
|
||||
│ stamps_delta │ (signed: +1 earn, -N redeem)
|
||||
│ points_delta │ (signed: +N earn, -N redeem)
|
||||
│ stamps_balance_after│
|
||||
│ points_balance_after│
|
||||
│ purchase_amount │
|
||||
│ staff_pin_id │──── FK to StaffPin
|
||||
│ store_id │──── FK to Store (location)
|
||||
│ related_txn_id │──── FK to self (for voids)
|
||||
│ ip_address │
|
||||
│ user_agent │
|
||||
└──────────────────────┘
|
||||
|
||||
┌──────────────────────────┐
|
||||
│ AppleDeviceRegistration │
|
||||
│ │
|
||||
│ card_id │──── FK to LoyaltyCard
|
||||
│ device_library_id │
|
||||
│ push_token │
|
||||
│ │
|
||||
│ UNIQUE(device, card) │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
### LoyaltyProgram
|
||||
|
||||
Merchant-wide loyalty program configuration. One program per merchant, shared across all stores.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merchant_id` | FK (unique) | One program per merchant |
|
||||
| `loyalty_type` | Enum | STAMPS, POINTS, or HYBRID |
|
||||
| `stamps_target` | Integer | Stamps needed for reward |
|
||||
| `stamps_reward_description` | String | Reward description text |
|
||||
| `stamps_reward_value_cents` | Integer | Reward monetary value |
|
||||
| `points_per_euro` | Integer | Points earned per euro spent |
|
||||
| `points_rewards` | JSON | Reward catalog (id, name, points_cost) |
|
||||
| `points_expiration_days` | Integer | Days until points expire (nullable) |
|
||||
| `welcome_bonus_points` | Integer | Points given on enrollment |
|
||||
| `minimum_redemption_points` | Integer | Minimum points to redeem |
|
||||
| `minimum_purchase_cents` | Integer | Minimum purchase for earning |
|
||||
| `cooldown_minutes` | Integer | Minutes between stamps (anti-fraud) |
|
||||
| `max_daily_stamps` | Integer | Max stamps per card per day |
|
||||
| `require_staff_pin` | Boolean | Whether PIN is required |
|
||||
| `card_name` | String | Display name on card |
|
||||
| `card_color` | String | Primary brand color (hex) |
|
||||
| `card_secondary_color` | String | Secondary brand color (hex) |
|
||||
| `logo_url` | String | Logo image URL |
|
||||
| `hero_image_url` | String | Hero/banner image URL |
|
||||
| `google_issuer_id` | String | Google Wallet issuer ID |
|
||||
| `google_class_id` | String | Google Wallet class ID |
|
||||
| `apple_pass_type_id` | String | Apple Wallet pass type identifier |
|
||||
| `terms_text` | Text | Terms and conditions |
|
||||
| `privacy_url` | String | Privacy policy URL |
|
||||
| `is_active` | Boolean | Whether program is live |
|
||||
| `activated_at` | DateTime | When program was activated |
|
||||
|
||||
### LoyaltyCard
|
||||
|
||||
Customer loyalty card linking a customer to a merchant's program. One card per customer per merchant.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merchant_id` | FK | Links to program's merchant |
|
||||
| `customer_id` | FK | Card owner |
|
||||
| `program_id` | FK | Associated program |
|
||||
| `enrolled_at_store_id` | FK | Store where customer enrolled |
|
||||
| `card_number` | String (unique) | Formatted XXXX-XXXX-XXXX |
|
||||
| `qr_code_data` | String (unique) | URL-safe token for QR codes |
|
||||
| `stamp_count` | Integer | Current stamp count |
|
||||
| `total_stamps_earned` | Integer | Lifetime stamps earned |
|
||||
| `stamps_redeemed` | Integer | Total redemptions |
|
||||
| `points_balance` | Integer | Current points balance |
|
||||
| `total_points_earned` | Integer | Lifetime points earned |
|
||||
| `points_redeemed` | Integer | Total points redeemed |
|
||||
| `total_points_voided` | Integer | Total points voided |
|
||||
| `google_object_id` | String | Google Wallet object ID |
|
||||
| `google_object_jwt` | Text | Google Wallet JWT |
|
||||
| `apple_serial_number` | String | Apple Wallet serial number |
|
||||
| `apple_auth_token` | String | Apple Wallet auth token |
|
||||
| `last_stamp_at` | DateTime | Last stamp timestamp |
|
||||
| `last_points_at` | DateTime | Last points timestamp |
|
||||
| `last_redemption_at` | DateTime | Last redemption timestamp |
|
||||
| `last_activity_at` | DateTime | Last activity of any kind |
|
||||
| `is_active` | Boolean | Whether card is active |
|
||||
|
||||
### LoyaltyTransaction
|
||||
|
||||
Immutable audit log of all loyalty operations. Every stamp, point, redemption, and void is recorded.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merchant_id` | FK | Merchant program owner |
|
||||
| `card_id` | FK | Affected card |
|
||||
| `store_id` | FK | Store where transaction occurred |
|
||||
| `staff_pin_id` | FK (nullable) | Staff who verified |
|
||||
| `related_transaction_id` | FK (nullable) | For void/return linking |
|
||||
| `transaction_type` | Enum | See transaction types below |
|
||||
| `stamps_delta` | Integer | Signed stamp change |
|
||||
| `points_delta` | Integer | Signed points change |
|
||||
| `stamps_balance_after` | Integer | Stamp count after transaction |
|
||||
| `points_balance_after` | Integer | Points balance after transaction |
|
||||
| `purchase_amount_cents` | Integer | Purchase amount for points earning |
|
||||
| `order_reference` | String | External order reference |
|
||||
| `reward_id` | String | Redeemed reward identifier |
|
||||
| `reward_description` | String | Redeemed reward description |
|
||||
| `ip_address` | String | Client IP (audit) |
|
||||
| `user_agent` | String | Client user agent (audit) |
|
||||
| `notes` | Text | Staff/admin notes |
|
||||
| `transaction_at` | DateTime | When transaction occurred |
|
||||
|
||||
**Transaction Types:**
|
||||
|
||||
| Type | Category | Description |
|
||||
|------|----------|-------------|
|
||||
| `STAMP_EARNED` | Stamps | Customer earned a stamp |
|
||||
| `STAMP_REDEEMED` | Stamps | Stamps exchanged for reward |
|
||||
| `STAMP_VOIDED` | Stamps | Stamp reversed (return) |
|
||||
| `STAMP_ADJUSTMENT` | Stamps | Manual adjustment |
|
||||
| `POINTS_EARNED` | Points | Points from purchase |
|
||||
| `POINTS_REDEEMED` | Points | Points exchanged for reward |
|
||||
| `POINTS_VOIDED` | Points | Points reversed (return) |
|
||||
| `POINTS_ADJUSTMENT` | Points | Manual adjustment |
|
||||
| `POINTS_EXPIRED` | Points | Points expired due to inactivity |
|
||||
| `CARD_CREATED` | Lifecycle | Card enrollment |
|
||||
| `CARD_DEACTIVATED` | Lifecycle | Card deactivated |
|
||||
| `WELCOME_BONUS` | Bonus | Welcome bonus points on enrollment |
|
||||
|
||||
### StaffPin
|
||||
|
||||
Staff authentication PINs for fraud prevention. Scoped to a store within a merchant's program.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merchant_id` | FK | Merchant |
|
||||
| `program_id` | FK | Associated program |
|
||||
| `store_id` | FK | Store this PIN is for |
|
||||
| `name` | String | Staff member name |
|
||||
| `staff_id` | String | External staff identifier |
|
||||
| `pin_hash` | String | Bcrypt-hashed PIN |
|
||||
| `failed_attempts` | Integer | Consecutive failed attempts |
|
||||
| `locked_until` | DateTime | Lockout expiry (nullable) |
|
||||
| `last_used_at` | DateTime | Last successful use |
|
||||
| `is_active` | Boolean | Whether PIN is active |
|
||||
|
||||
### MerchantLoyaltySettings
|
||||
|
||||
Admin-controlled settings for a merchant's loyalty program. Separate from program config to allow admin overrides.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merchant_id` | FK (unique) | One settings record per merchant |
|
||||
| `staff_pin_policy` | Enum | REQUIRED, OPTIONAL, or DISABLED |
|
||||
| `staff_pin_lockout_attempts` | Integer | Failed attempts before lockout |
|
||||
| `staff_pin_lockout_minutes` | Integer | Lockout duration |
|
||||
| `allow_self_enrollment` | Boolean | Whether customers can self-enroll |
|
||||
| `allow_void_transactions` | Boolean | Whether voids are allowed |
|
||||
| `allow_cross_location_redemption` | Boolean | Cross-store redemption |
|
||||
| `require_order_reference` | Boolean | Require order ref for points |
|
||||
| `log_ip_addresses` | Boolean | Log IPs in transactions |
|
||||
|
||||
### AppleDeviceRegistration
|
||||
|
||||
Tracks Apple devices registered for wallet push notifications when card balances change.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `card_id` | FK | Associated loyalty card |
|
||||
| `device_library_identifier` | String | Apple device identifier |
|
||||
| `push_token` | String | APNs push token |
|
||||
|
||||
Unique constraint on `(device_library_identifier, card_id)`.
|
||||
110
app/modules/loyalty/docs/index.md
Normal file
110
app/modules/loyalty/docs/index.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Loyalty Programs
|
||||
|
||||
Stamp-based and points-based loyalty programs with Google Wallet and Apple Wallet integration. Includes anti-fraud features like staff PINs, cooldown periods, and daily limits.
|
||||
|
||||
## Overview
|
||||
|
||||
| Aspect | Detail |
|
||||
|--------|--------|
|
||||
| Code | `loyalty` |
|
||||
| Classification | Optional |
|
||||
| Dependencies | `customers` |
|
||||
| Status | Active |
|
||||
|
||||
## Features
|
||||
|
||||
- `loyalty_stamps` — Stamp-based loyalty (collect N, get reward)
|
||||
- `loyalty_points` — Points-based loyalty (earn per euro spent)
|
||||
- `loyalty_hybrid` — Combined stamps and points programs
|
||||
- `loyalty_cards` — Digital loyalty card management
|
||||
- `loyalty_enrollment` — Customer enrollment flow
|
||||
- `loyalty_staff_pins` — Staff PIN verification
|
||||
- `loyalty_anti_fraud` — Cooldown periods, daily limits, lockout protection
|
||||
- `loyalty_google_wallet` — Google Wallet pass integration
|
||||
- `loyalty_apple_wallet` — Apple Wallet pass integration
|
||||
- `loyalty_stats` — Program statistics
|
||||
- `loyalty_reports` — Loyalty reporting
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Description |
|
||||
|------------|-------------|
|
||||
| `loyalty.view_programs` | View loyalty programs |
|
||||
| `loyalty.manage_programs` | Create/edit loyalty programs |
|
||||
| `loyalty.view_rewards` | View rewards |
|
||||
| `loyalty.manage_rewards` | Manage rewards |
|
||||
|
||||
## Data Model
|
||||
|
||||
See [Data Model](data-model.md) for full entity relationships.
|
||||
|
||||
- **LoyaltyProgram** — Program configuration (type, targets, branding)
|
||||
- **LoyaltyCard** — Customer cards with stamp/point balances
|
||||
- **LoyaltyTransaction** — Immutable audit log of all operations
|
||||
- **StaffPin** — Hashed PINs for fraud prevention
|
||||
- **MerchantLoyaltySettings** — Admin-controlled merchant settings
|
||||
- **AppleDeviceRegistration** — Apple Wallet push notification tokens
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Store Endpoints (`/api/v1/store/loyalty/`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/program` | Get store'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` | `/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 |
|
||||
| `*` | `/pins/*` | Staff PIN management |
|
||||
|
||||
### Admin Endpoints (`/api/v1/admin/loyalty/`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/programs` | List all loyalty programs |
|
||||
| `GET` | `/programs/{id}` | Get specific program |
|
||||
| `GET` | `/stats` | Platform-wide statistics |
|
||||
|
||||
### Storefront Endpoints (`/api/v1/storefront/loyalty/`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/card` | Get customer's loyalty card |
|
||||
| `GET` | `/transactions` | Transaction history |
|
||||
| `POST` | `/enroll` | Self-enrollment |
|
||||
|
||||
## Scheduled Tasks
|
||||
|
||||
| Task | Schedule | Description |
|
||||
|------|----------|-------------|
|
||||
| `loyalty.sync_wallet_passes` | Hourly | Sync cards that missed real-time updates |
|
||||
| `loyalty.expire_points` | Daily 02:00 | Expire points for inactive cards |
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables (prefix: `LOYALTY_`):
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `LOYALTY_DEFAULT_COOLDOWN_MINUTES` | 15 | Cooldown between stamps |
|
||||
| `LOYALTY_MAX_DAILY_STAMPS` | 5 | Max stamps per card per day |
|
||||
| `LOYALTY_PIN_MAX_FAILED_ATTEMPTS` | 5 | PIN lockout threshold |
|
||||
| `LOYALTY_PIN_LOCKOUT_MINUTES` | 30 | PIN lockout duration |
|
||||
| `LOYALTY_DEFAULT_POINTS_PER_EURO` | 10 | Points earned per euro |
|
||||
| `LOYALTY_GOOGLE_ISSUER_ID` | — | Google Wallet issuer ID |
|
||||
| `LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON` | — | Google service account path |
|
||||
| `LOYALTY_APPLE_*` | — | Apple Wallet certificate paths |
|
||||
|
||||
## Additional Documentation
|
||||
|
||||
- [Data Model](data-model.md) — Entity relationships and database schema
|
||||
- [Business Logic](business-logic.md) — Anti-fraud system, wallet integration, enrollment flow
|
||||
- [User Journeys](user-journeys.md) — Detailed user journey flows with dev/prod URLs
|
||||
- [Program Analysis](program-analysis.md) — Business analysis and platform vision
|
||||
- [UI Design](ui-design.md) — Admin and store interface mockups and implementation roadmap
|
||||
387
app/modules/loyalty/docs/program-analysis.md
Normal file
387
app/modules/loyalty/docs/program-analysis.md
Normal file
@@ -0,0 +1,387 @@
|
||||
# Loyalty Program Platform - Business Analysis
|
||||
|
||||
**Session Date:** 2026-01-13
|
||||
**Status:** Initial Analysis - Pending Discussion
|
||||
**Next Steps:** Resume discussion to clarify requirements
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Multiple retailers have expressed interest in a loyalty program application. This document analyzes how the current OMS platform could be leveraged to provide a loyalty program offering as a new product line.
|
||||
|
||||
---
|
||||
|
||||
## Business Proposal Overview
|
||||
|
||||
### Concept
|
||||
- **Multi-platform offering**: Different platform tiers (A, B, C) with varying feature sets
|
||||
- **Target clients**: Merchants (retailers) with one or multiple shops
|
||||
- **Core functionality**:
|
||||
- Customer email collection
|
||||
- Promotions and campaigns
|
||||
- Discounts and rewards
|
||||
- Points accumulation
|
||||
|
||||
### Platform Architecture Vision
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PLATFORM LEVEL │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Platform A │ │ Platform B │ │ Platform C │ ... │
|
||||
│ │ (Loyalty+) │ │ (Basic) │ │ (Enterprise) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CLIENT LEVEL (Merchant) │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Retailer X (e.g., Bakery Chain) │ │
|
||||
│ │ ├── Shop 1 (Luxembourg City) │ │
|
||||
│ │ ├── Shop 2 (Esch) │ │
|
||||
│ │ └── Shop 3 (Differdange) │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CUSTOMER LEVEL │
|
||||
│ • Email collection • Points accumulation │
|
||||
│ • Promotions/Offers • Discounts/Rewards │
|
||||
│ • Purchase history • Tier status │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current OMS Architecture Leverage
|
||||
|
||||
The existing platform has several components that map directly to loyalty program needs:
|
||||
|
||||
| Current OMS Component | Loyalty Program Use |
|
||||
|-----------------------|---------------------|
|
||||
| `Merchant` model | Client (retailer chain) |
|
||||
| `Store` model | Individual shop/location |
|
||||
| `Customer` model | Loyalty member base |
|
||||
| `Order` model | Transaction for points calculation |
|
||||
| `User` (store role) | Shop staff for check-in/redemption |
|
||||
| Multi-tenant auth | Per-client data isolation |
|
||||
| Admin dashboard | Retailer management interface |
|
||||
| Store dashboard | Shop-level operations |
|
||||
| API infrastructure | Integration capabilities |
|
||||
|
||||
### Existing Infrastructure Benefits
|
||||
- Authentication & authorization system
|
||||
- Multi-tenant data isolation
|
||||
- Merchant → Store hierarchy
|
||||
- Customer management
|
||||
- Email/notification system (if exists)
|
||||
- Celery background tasks
|
||||
- API patterns established
|
||||
|
||||
---
|
||||
|
||||
## New Components Required
|
||||
|
||||
### 1. Core Loyalty Models
|
||||
|
||||
```python
|
||||
# New database models needed
|
||||
|
||||
LoyaltyProgram
|
||||
- id
|
||||
- merchant_id (FK)
|
||||
- name
|
||||
- points_per_euro (Decimal)
|
||||
- points_expiry_days (Integer, nullable)
|
||||
- is_active (Boolean)
|
||||
- settings (JSON) - flexible configuration
|
||||
|
||||
LoyaltyMember
|
||||
- id
|
||||
- customer_id (FK to existing Customer)
|
||||
- loyalty_program_id (FK)
|
||||
- points_balance (Integer)
|
||||
- lifetime_points (Integer)
|
||||
- tier_id (FK)
|
||||
- enrolled_at (DateTime)
|
||||
- last_activity_at (DateTime)
|
||||
|
||||
LoyaltyTier
|
||||
- id
|
||||
- loyalty_program_id (FK)
|
||||
- name (e.g., "Bronze", "Silver", "Gold")
|
||||
- min_points_required (Integer)
|
||||
- benefits (JSON)
|
||||
- sort_order (Integer)
|
||||
|
||||
LoyaltyTransaction
|
||||
- id
|
||||
- member_id (FK)
|
||||
- store_id (FK) - which shop
|
||||
- transaction_type (ENUM: earn, redeem, expire, adjust)
|
||||
- points (Integer, positive or negative)
|
||||
- reference_type (e.g., "order", "promotion", "manual")
|
||||
- reference_id (Integer, nullable)
|
||||
- description (String)
|
||||
- created_at (DateTime)
|
||||
- created_by_user_id (FK, nullable)
|
||||
|
||||
Promotion
|
||||
- id
|
||||
- loyalty_program_id (FK)
|
||||
- name
|
||||
- description
|
||||
- promotion_type (ENUM: bonus_points, discount_percent, discount_fixed, free_item)
|
||||
- value (Decimal)
|
||||
- conditions (JSON) - min spend, specific products, etc.
|
||||
- start_date (DateTime)
|
||||
- end_date (DateTime)
|
||||
- max_redemptions (Integer, nullable)
|
||||
- is_active (Boolean)
|
||||
|
||||
PromotionRedemption
|
||||
- id
|
||||
- promotion_id (FK)
|
||||
- member_id (FK)
|
||||
- store_id (FK)
|
||||
- redeemed_at (DateTime)
|
||||
- order_id (FK, nullable)
|
||||
|
||||
Reward
|
||||
- id
|
||||
- loyalty_program_id (FK)
|
||||
- name
|
||||
- description
|
||||
- points_cost (Integer)
|
||||
- reward_type (ENUM: discount, free_product, voucher)
|
||||
- value (Decimal or JSON)
|
||||
- is_active (Boolean)
|
||||
- stock (Integer, nullable) - for limited rewards
|
||||
```
|
||||
|
||||
### 2. Platform Offering Tiers
|
||||
|
||||
```python
|
||||
# Platform-level configuration
|
||||
|
||||
class PlatformOffering(Enum):
|
||||
BASIC = "basic"
|
||||
PLUS = "plus"
|
||||
ENTERPRISE = "enterprise"
|
||||
|
||||
# Feature matrix per offering
|
||||
OFFERING_FEATURES = {
|
||||
"basic": {
|
||||
"max_shops": 1,
|
||||
"points_earning": True,
|
||||
"basic_promotions": True,
|
||||
"tiers": False,
|
||||
"custom_rewards": False,
|
||||
"api_access": False,
|
||||
"white_label": False,
|
||||
"analytics": "basic",
|
||||
},
|
||||
"plus": {
|
||||
"max_shops": 10,
|
||||
"points_earning": True,
|
||||
"basic_promotions": True,
|
||||
"tiers": True,
|
||||
"custom_rewards": True,
|
||||
"api_access": False,
|
||||
"white_label": False,
|
||||
"analytics": "advanced",
|
||||
},
|
||||
"enterprise": {
|
||||
"max_shops": None, # Unlimited
|
||||
"points_earning": True,
|
||||
"basic_promotions": True,
|
||||
"tiers": True,
|
||||
"custom_rewards": True,
|
||||
"api_access": True,
|
||||
"white_label": True,
|
||||
"analytics": "full",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Feature Matrix
|
||||
|
||||
| Feature | Basic | Plus | Enterprise |
|
||||
|---------|:-----:|:----:|:----------:|
|
||||
| Customer email collection | ✓ | ✓ | ✓ |
|
||||
| Points earning | ✓ | ✓ | ✓ |
|
||||
| Basic promotions | ✓ | ✓ | ✓ |
|
||||
| Multi-shop support | 1 shop | Up to 10 | Unlimited |
|
||||
| Tier system (Bronze/Silver/Gold) | - | ✓ | ✓ |
|
||||
| Custom rewards catalog | - | ✓ | ✓ |
|
||||
| API access | - | - | ✓ |
|
||||
| White-label branding | - | - | ✓ |
|
||||
| Analytics dashboard | Basic | Advanced | Full |
|
||||
| Customer segmentation | - | ✓ | ✓ |
|
||||
| Email campaigns | - | ✓ | ✓ |
|
||||
| Dedicated support | - | - | ✓ |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Options
|
||||
|
||||
### Option A: Standalone Application
|
||||
- Separate codebase
|
||||
- Shares database patterns but independent deployment
|
||||
- **Pros**: Clean separation, can scale independently
|
||||
- **Cons**: Duplication of auth, admin patterns; more maintenance
|
||||
|
||||
### Option B: Module in Current OMS (Recommended)
|
||||
- Add loyalty as a feature module within existing platform
|
||||
- Leverages existing infrastructure
|
||||
|
||||
**Proposed directory structure:**
|
||||
```
|
||||
letzshop-product-import/
|
||||
├── app/
|
||||
│ ├── api/v1/
|
||||
│ │ ├── loyalty/ # NEW
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── programs.py # Program CRUD
|
||||
│ │ │ ├── members.py # Member management
|
||||
│ │ │ ├── transactions.py # Points transactions
|
||||
│ │ │ ├── promotions.py # Promotion management
|
||||
│ │ │ ├── rewards.py # Rewards catalog
|
||||
│ │ │ └── public.py # Customer-facing endpoints
|
||||
│ │ │
|
||||
│ ├── services/
|
||||
│ │ ├── loyalty/ # NEW
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── points_service.py # Points calculation logic
|
||||
│ │ │ ├── tier_service.py # Tier management
|
||||
│ │ │ ├── promotion_service.py # Promotion rules engine
|
||||
│ │ │ └── reward_service.py # Reward redemption
|
||||
│ │ │
|
||||
│ ├── templates/
|
||||
│ │ ├── loyalty/ # NEW - if web UI needed
|
||||
│ │ │ ├── admin/ # Platform admin views
|
||||
│ │ │ ├── retailer/ # Retailer dashboard
|
||||
│ │ │ └── member/ # Customer-facing portal
|
||||
│ │ │
|
||||
├── models/
|
||||
│ ├── database/
|
||||
│ │ ├── loyalty.py # NEW - All loyalty models
|
||||
│ ├── schema/
|
||||
│ │ ├── loyalty.py # NEW - Pydantic schemas
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Questions (To Discuss)
|
||||
|
||||
### 1. Points Model
|
||||
- **Q1.1**: Fixed points per euro spent? (e.g., 1 point = €0.10 spent)
|
||||
- **Q1.2**: Variable points by product category? (e.g., 2x points on bakery items)
|
||||
- **Q1.3**: Bonus points for specific actions? (e.g., sign-up bonus, birthday bonus)
|
||||
- **Q1.4**: Points expiration policy? (e.g., expire after 12 months of inactivity)
|
||||
|
||||
### 2. Redemption Methods
|
||||
- **Q2.1**: In-store redemption only? (requires POS integration or staff app)
|
||||
- **Q2.2**: Online shop redemption?
|
||||
- **Q2.3**: Both in-store and online?
|
||||
- **Q2.4**: What POS systems do target retailers use?
|
||||
|
||||
### 3. Customer Identification
|
||||
- **Q3.1**: Email only?
|
||||
- **Q3.2**: Phone number as alternative?
|
||||
- **Q3.3**: Physical loyalty card with barcode/QR?
|
||||
- **Q3.4**: Mobile app with digital card?
|
||||
- **Q3.5**: Integration with existing customer accounts?
|
||||
|
||||
### 4. Multi-Platform Architecture
|
||||
- **Q4.1**: Different domains per offering tier?
|
||||
- e.g., loyalty-basic.lu, loyalty-pro.lu, loyalty-enterprise.lu
|
||||
- **Q4.2**: Same domain with feature flags based on subscription?
|
||||
- **Q4.3**: White-label with custom domains for enterprise clients?
|
||||
|
||||
### 5. Data & Privacy
|
||||
- **Q5.1**: Can retailers see each other's customers? (Assumed: No)
|
||||
- **Q5.2**: Can a customer be enrolled in multiple loyalty programs? (Different retailers)
|
||||
- **Q5.3**: GDPR considerations for customer data?
|
||||
- **Q5.4**: Data export/portability requirements?
|
||||
|
||||
### 6. Business Model
|
||||
- **Q6.1**: Pricing model? (Monthly subscription, per-transaction fee, hybrid?)
|
||||
- **Q6.2**: Free trial period?
|
||||
- **Q6.3**: Upgrade/downgrade path between tiers?
|
||||
|
||||
### 7. Integration Requirements
|
||||
- **Q7.1**: POS system integrations needed?
|
||||
- **Q7.2**: Email marketing platform integration? (Mailchimp, SendGrid, etc.)
|
||||
- **Q7.3**: SMS notifications?
|
||||
- **Q7.4**: Accounting/invoicing integration?
|
||||
|
||||
### 8. MVP Scope
|
||||
- **Q8.1**: What is the minimum viable feature set for first launch?
|
||||
- **Q8.2**: Which offering tier to build first?
|
||||
- **Q8.3**: Target timeline?
|
||||
- **Q8.4**: Pilot retailers identified?
|
||||
|
||||
---
|
||||
|
||||
## Potential User Flows
|
||||
|
||||
### Retailer Onboarding Flow
|
||||
1. Retailer signs up on platform
|
||||
2. Selects offering tier (Basic/Plus/Enterprise)
|
||||
3. Configures loyalty program (name, points ratio, branding)
|
||||
4. Adds shop locations
|
||||
5. Invites staff members
|
||||
6. Sets up initial promotions
|
||||
7. Goes live
|
||||
|
||||
### Customer Enrollment Flow
|
||||
1. Customer visits shop or website
|
||||
2. Provides email (and optionally phone)
|
||||
3. Receives welcome email with member ID/card
|
||||
4. Starts earning points on purchases
|
||||
|
||||
### Points Earning Flow (In-Store)
|
||||
1. Customer makes purchase
|
||||
2. Staff asks for loyalty member ID (email, phone, or card scan)
|
||||
3. System calculates points based on purchase amount
|
||||
4. Points credited to member account
|
||||
5. Receipt shows points earned and balance
|
||||
|
||||
### Reward Redemption Flow
|
||||
1. Customer views available rewards (app/web/in-store)
|
||||
2. Selects reward to redeem
|
||||
3. System validates sufficient points
|
||||
4. Generates redemption code/voucher
|
||||
5. Customer uses at checkout
|
||||
6. Points deducted from balance
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Clarify requirements** - Answer open questions above
|
||||
2. **Define MVP scope** - What's the minimum for first launch?
|
||||
3. **Technical design** - Database schema, API design
|
||||
4. **UI/UX design** - Retailer dashboard, customer portal
|
||||
5. **Implementation plan** - Phased approach
|
||||
6. **Pilot program** - Identify first retailers for beta
|
||||
|
||||
---
|
||||
|
||||
## Session Notes
|
||||
|
||||
### 2026-01-13
|
||||
- Initial business proposal discussion
|
||||
- Analyzed current OMS architecture fit
|
||||
- Identified reusable components
|
||||
- Outlined new models needed
|
||||
- Documented open questions
|
||||
- **Action**: Resume discussion to clarify requirements
|
||||
|
||||
---
|
||||
|
||||
*Document created for session continuity. Update as discussions progress.*
|
||||
670
app/modules/loyalty/docs/ui-design.md
Normal file
670
app/modules/loyalty/docs/ui-design.md
Normal file
@@ -0,0 +1,670 @@
|
||||
# Loyalty Module Phase 2: Admin & Store Interfaces
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the plan for building admin and store interfaces for the Loyalty Module, along with detailed user journeys for stamp-based and points-based loyalty programs. The design follows market best practices from leading loyalty platforms (Square Loyalty, Toast, Fivestars, Belly, Punchh).
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Interface Design
|
||||
|
||||
### 1.1 Store Dashboard (Retail Store)
|
||||
|
||||
#### Main Loyalty Dashboard (`/store/loyalty`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🎯 Loyalty Program [Setup] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐│
|
||||
│ │ 1,247 │ │ 892 │ │ 156 │ │ €2.3k ││
|
||||
│ │ Members │ │ Active │ │ Redeemed │ │ Saved ││
|
||||
│ │ Total │ │ 30 days │ │ This Month │ │ Value ││
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────┘│
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ 📊 Activity Chart (Last 30 Days) ││
|
||||
│ │ [Stamps Issued] [Rewards Redeemed] [New Members] ││
|
||||
│ │ ═══════════════════════════════════════════════ ││
|
||||
│ └─────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ 🔥 Quick Actions │ │ 📋 Recent Activity │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ [➕ Add Stamp] │ │ • John D. earned stamp #8 │ │
|
||||
│ │ [🎁 Redeem Reward] │ │ • Marie L. redeemed reward │ │
|
||||
│ │ [👤 Enroll Customer] │ │ • Alex K. joined program │ │
|
||||
│ │ [🔍 Look Up Card] │ │ • Sarah M. earned 50 pts │ │
|
||||
│ │ │ │ │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Stamp/Points Terminal (`/store/loyalty/terminal`)
|
||||
|
||||
**Primary interface for daily operations - optimized for tablet/touchscreen:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🎯 Loyalty Terminal │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ 📷 SCAN QR CODE │ │
|
||||
│ │ │ │
|
||||
│ │ [Camera Viewfinder Area] │ │
|
||||
│ │ │ │
|
||||
│ │ or enter card number │ │
|
||||
│ │ ┌─────────────────────────┐ │ │
|
||||
│ │ │ Card Number... │ │ │
|
||||
│ │ └─────────────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Use Camera] [Enter Manually] [Recent Cards ▼] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**After scanning - Customer Card View:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ← Back Customer Card │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ 👤 Marie Laurent │ │
|
||||
│ │ marie.laurent@email.com │ │
|
||||
│ │ Member since: Jan 2024 │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ ☕ ☕ ☕ ☕ ☕ ☕ ☕ ☕ ○ ○ │ │
|
||||
│ │ │ │
|
||||
│ │ 8 / 10 stamps │ │
|
||||
│ │ 2 more until FREE COFFEE │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ [ ➕ ADD STAMP ] [ 🎁 REDEEM ] │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ⚠️ Next stamp available in 12 minutes │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**PIN Entry Modal (appears when adding stamp):**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Enter Staff PIN │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ ● ● ● ● │ │
|
||||
│ └─────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ 1 │ │ 2 │ │ 3 │ │
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ 4 │ │ 5 │ │ 6 │ │
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ 7 │ │ 8 │ │ 9 │ │
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ ⌫ │ │ 0 │ │ ✓ │ │
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
│ │
|
||||
│ [Cancel] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Program Setup (`/store/loyalty/settings`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ⚙️ Loyalty Program Settings │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Program Type │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ ☑️ Stamps │ │ ☐ Points │ │ ☐ Hybrid │ │
|
||||
│ │ Buy 10 Get 1 │ │ Earn per € │ │ Both systems │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ │
|
||||
│ ───────────────────────────────────────────────────────────── │
|
||||
│ │
|
||||
│ Stamp Configuration │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Stamps needed for reward: [ 10 ▼ ] │ │
|
||||
│ │ Reward description: [ Free coffee of choice ] │ │
|
||||
│ │ Reward value (optional): [ €4.50 ] │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ───────────────────────────────────────────────────────────── │
|
||||
│ │
|
||||
│ 🛡️ Fraud Prevention │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ ☑️ Require staff PIN for operations │ │
|
||||
│ │ Cooldown between stamps: [ 15 ] minutes │ │
|
||||
│ │ Max stamps per day: [ 5 ] │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ───────────────────────────────────────────────────────────── │
|
||||
│ │
|
||||
│ 🎨 Card Branding │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Card name: [ Café Loyalty Card ] │ │
|
||||
│ │ Primary color: [████] #4F46E5 │ │
|
||||
│ │ Logo: [Upload] cafe-logo.png ✓ │ │
|
||||
│ │ │ │
|
||||
│ │ Preview: ┌────────────────────┐ │ │
|
||||
│ │ │ ☕ Café Loyalty │ │ │
|
||||
│ │ │ ████████░░ │ │ │
|
||||
│ │ │ 8/10 stamps │ │ │
|
||||
│ │ └────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Cancel] [Save Changes] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Staff PIN Management (`/store/loyalty/pins`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🔐 Staff PINs [+ Add PIN] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 👤 Marie (Manager) [Edit] [🗑️] │ │
|
||||
│ │ Last used: Today, 14:32 │ │
|
||||
│ │ Status: ✅ Active │ │
|
||||
│ ├─────────────────────────────────────────────────────────┤ │
|
||||
│ │ 👤 Thomas (Staff) [Edit] [🗑️] │ │
|
||||
│ │ Last used: Today, 11:15 │ │
|
||||
│ │ Status: ✅ Active │ │
|
||||
│ ├─────────────────────────────────────────────────────────┤ │
|
||||
│ │ 👤 Julie (Staff) [Edit] [🗑️] │ │
|
||||
│ │ Last used: Yesterday │ │
|
||||
│ │ Status: 🔒 Locked (3 failed attempts) [Unlock] │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ℹ️ Staff PINs prevent unauthorized stamp/point operations. │
|
||||
│ PINs are locked after 5 failed attempts for 30 minutes. │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Customer Cards List (`/store/loyalty/cards`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 👥 Loyalty Members 🔍 [Search...] [Export]│
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Filter: [All ▼] [Active ▼] [Has Reward Ready ▼] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ Customer │ Card # │ Stamps │ Last Visit │ ⋮ ││
|
||||
│ ├───────────────────┼──────────────┼────────┼────────────┼────┤│
|
||||
│ │ Marie Laurent │ 4821-7493 │ 8/10 ⭐│ Today │ ⋮ ││
|
||||
│ │ Jean Dupont │ 4821-2847 │ 10/10 🎁│ Yesterday │ ⋮ ││
|
||||
│ │ Sophie Martin │ 4821-9382 │ 3/10 │ 3 days ago │ ⋮ ││
|
||||
│ │ Pierre Bernard │ 4821-1029 │ 6/10 │ 1 week ago │ ⋮ ││
|
||||
│ │ ... │ ... │ ... │ ... │ ⋮ ││
|
||||
│ └─────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ Showing 1-20 of 1,247 members [← Prev] [1] [2] [Next →]│
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 Admin Dashboard (Platform)
|
||||
|
||||
#### Platform Loyalty Overview (`/admin/loyalty`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🎯 Loyalty Programs Platform │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐│
|
||||
│ │ 47 │ │ 38 │ │ 12,847 │ │ €47k ││
|
||||
│ │ Programs │ │ Active │ │ Members │ │ Saved ││
|
||||
│ │ Total │ │ Programs │ │ Total │ │ Value ││
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────┘│
|
||||
│ │
|
||||
│ Programs by Type: │
|
||||
│ ═══════════════════════════════════════ │
|
||||
│ Stamps: ████████████████████ 32 (68%) │
|
||||
│ Points: ███████ 11 (23%) │
|
||||
│ Hybrid: ████ 4 (9%) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ Store │ Type │ Members │ Activity │ Status ││
|
||||
│ ├───────────────────┼─────────┼─────────┼──────────┼──────────┤│
|
||||
│ │ Café du Coin │ Stamps │ 1,247 │ High │ ✅ Active││
|
||||
│ │ Boulangerie Paul │ Points │ 892 │ Medium │ ✅ Active││
|
||||
│ │ Pizza Roma │ Stamps │ 456 │ Low │ ⚠️ Setup ││
|
||||
│ │ ... │ ... │ ... │ ... │ ... ││
|
||||
│ └─────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 2: User Journeys
|
||||
|
||||
### 2.1 Stamp-Based Loyalty Journey
|
||||
|
||||
#### Customer Journey: Enrollment
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ STAMP LOYALTY - ENROLLMENT │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ DISCOVER│────▶│ JOIN │────▶│ SAVE │────▶│ USE │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 1. Customer sees │ 2. Scans QR at │ 3. Card added │ 4. Ready to │
|
||||
│ sign at counter│ register or │ to Google/ │ collect │
|
||||
│ "Join our │ gives email │ Apple Wallet│ stamps! │
|
||||
│ loyalty!" │ to cashier │ │ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Detailed Steps:**
|
||||
|
||||
1. **Discovery** (In-Store)
|
||||
- Customer sees loyalty program signage/tent card
|
||||
- QR code displayed at counter
|
||||
- Staff mentions program during checkout
|
||||
|
||||
2. **Sign Up** (30 seconds)
|
||||
- Customer scans QR code with phone
|
||||
- Lands on mobile enrollment page
|
||||
- Enters: Email (required), Name (optional)
|
||||
- Accepts terms with checkbox
|
||||
- Submits
|
||||
|
||||
3. **Card Creation** (Instant)
|
||||
- System creates loyalty card
|
||||
- Generates unique card number & QR code
|
||||
- Shows "Add to Wallet" buttons
|
||||
- Sends welcome email with card link
|
||||
|
||||
4. **Wallet Save** (Optional but encouraged)
|
||||
- Customer taps "Add to Google Wallet" or "Add to Apple Wallet"
|
||||
- Pass appears in their wallet app
|
||||
- Always accessible, works offline
|
||||
|
||||
#### Customer Journey: Earning Stamps
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ STAMP LOYALTY - EARNING │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Customer Staff System Wallet
|
||||
│ │ │ │
|
||||
│ 1. Makes │ │ │
|
||||
│ purchase │ │ │
|
||||
│───────────────▶│ │ │
|
||||
│ │ │ │
|
||||
│ 2. Shows │ │ │
|
||||
│ loyalty card │ │ │
|
||||
│───────────────▶│ │ │
|
||||
│ │ 3. Scans QR │ │
|
||||
│ │─────────────────▶│ │
|
||||
│ │ │ │
|
||||
│ │ 4. Enters PIN │ │
|
||||
│ │─────────────────▶│ │
|
||||
│ │ │ │
|
||||
│ │ 5. Confirms │ │
|
||||
│ │◀─────────────────│ │
|
||||
│ │ "Stamp added!" │ │
|
||||
│ │ │ │
|
||||
│ 6. Verbal │ │ 7. Push │
|
||||
│ confirmation │ │ notification │
|
||||
│◀───────────────│ │────────────────▶│
|
||||
│ │ │ │
|
||||
│ │ 8. Pass updates│
|
||||
│◀───────────────────────────────────│────────────────▶│
|
||||
│ "8/10 stamps" │ │
|
||||
```
|
||||
|
||||
**Anti-Fraud Checks (Automatic):**
|
||||
|
||||
1. ✅ Card is active
|
||||
2. ✅ Program is active
|
||||
3. ✅ Staff PIN is valid
|
||||
4. ✅ Cooldown period elapsed (15 min since last stamp)
|
||||
5. ✅ Daily limit not reached (max 5/day)
|
||||
|
||||
**Success Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"stamp_count": 8,
|
||||
"stamps_target": 10,
|
||||
"stamps_until_reward": 2,
|
||||
"message": "2 more stamps until your free coffee!",
|
||||
"next_stamp_available": "2024-01-28T15:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Customer Journey: Redeeming Reward
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ STAMP LOYALTY - REDEMPTION │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Customer Staff System
|
||||
│ │ │
|
||||
│ 1. "I'd like │ │
|
||||
│ to redeem my │ │
|
||||
│ free coffee" │ │
|
||||
│───────────────▶│ │
|
||||
│ │ │
|
||||
│ 2. Shows card │ │
|
||||
│ (10/10 stamps)│ │
|
||||
│───────────────▶│ │
|
||||
│ │ 3. Scans + sees │
|
||||
│ │ "REWARD READY" │
|
||||
│ │─────────────────▶│
|
||||
│ │ │
|
||||
│ │ 4. Clicks │
|
||||
│ │ [REDEEM REWARD] │
|
||||
│ │─────────────────▶│
|
||||
│ │ │
|
||||
│ │ 5. Enters PIN │
|
||||
│ │─────────────────▶│
|
||||
│ │ │
|
||||
│ │ 6. Confirms │
|
||||
│ │◀─────────────────│
|
||||
│ │ "Reward redeemed"│
|
||||
│ │ Stamps reset: 0 │
|
||||
│ │ │
|
||||
│ 7. Gives free │ │
|
||||
│ coffee │ │
|
||||
│◀───────────────│ │
|
||||
│ │ │
|
||||
│ 🎉 HAPPY │ │
|
||||
│ CUSTOMER! │ │
|
||||
```
|
||||
|
||||
### 2.2 Points-Based Loyalty Journey
|
||||
|
||||
#### Customer Journey: Earning Points
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ POINTS LOYALTY - EARNING │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Customer Staff System
|
||||
│ │ │
|
||||
│ 1. Purchases │ │
|
||||
│ €25.00 order │ │
|
||||
│───────────────▶│ │
|
||||
│ │ │
|
||||
│ 2. Shows │ │
|
||||
│ loyalty card │ │
|
||||
│───────────────▶│ │
|
||||
│ │ 3. Scans card │
|
||||
│ │─────────────────▶│
|
||||
│ │ │
|
||||
│ │ 4. Enters amount │
|
||||
│ │ €25.00 │
|
||||
│ │─────────────────▶│
|
||||
│ │ │
|
||||
│ │ 5. Enters PIN │
|
||||
│ │─────────────────▶│
|
||||
│ │ │ ┌──────────┐
|
||||
│ │ │ │Calculate:│
|
||||
│ │ │ │€25 × 10 │
|
||||
│ │ │ │= 250 pts │
|
||||
│ │ │ └──────────┘
|
||||
│ │ 6. Confirms │
|
||||
│ │◀─────────────────│
|
||||
│ │ "+250 points!" │
|
||||
│ │ │
|
||||
│ 7. Receipt │ │
|
||||
│ shows points │ │
|
||||
│◀───────────────│ │
|
||||
```
|
||||
|
||||
**Points Calculation:**
|
||||
```
|
||||
Purchase: €25.00
|
||||
Rate: 10 points per euro
|
||||
Points Earned: 250 points
|
||||
New Balance: 750 points
|
||||
```
|
||||
|
||||
#### Customer Journey: Redeeming Points
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ POINTS LOYALTY - REDEMPTION │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Customer Staff System
|
||||
│ │ │
|
||||
│ 1. Views │ │
|
||||
│ rewards in │ │
|
||||
│ wallet app │ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ ┌──────────┐ │ │
|
||||
│ │ REWARDS │ │ │
|
||||
│ │──────────│ │ │
|
||||
│ │ 500 pts │ │ │
|
||||
│ │ Free │ │ │
|
||||
│ │ Drink │ │ │
|
||||
│ │──────────│ │ │
|
||||
│ │ 1000 pts │ │ │
|
||||
│ │ Free │ │ │
|
||||
│ │ Meal │ │ │
|
||||
│ └──────────┘ │ │
|
||||
│ │ │
|
||||
│ 2. "I want to │ │
|
||||
│ redeem for │ │
|
||||
│ free drink" │ │
|
||||
│───────────────▶│ │
|
||||
│ │ 3. Scans card │
|
||||
│ │ Selects reward │
|
||||
│ │─────────────────▶│
|
||||
│ │ │
|
||||
│ │ 4. Enters PIN │
|
||||
│ │─────────────────▶│
|
||||
│ │ │
|
||||
│ │ 5. Confirms │
|
||||
│ │◀─────────────────│
|
||||
│ │ "-500 points" │
|
||||
│ │ Balance: 250 pts │
|
||||
│ │ │
|
||||
│ 6. Gets free │ │
|
||||
│ drink │ │
|
||||
│◀───────────────│ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Market Best Practices
|
||||
|
||||
### 3.1 Competitive Analysis
|
||||
|
||||
| Feature | Square Loyalty | Toast | Fivestars | **Orion** |
|
||||
|---------|---------------|-------|-----------|--------------|
|
||||
| Stamp cards | ✅ | ✅ | ✅ | ✅ |
|
||||
| Points system | ✅ | ✅ | ✅ | ✅ |
|
||||
| Google Wallet | ✅ | ❌ | ✅ | ✅ |
|
||||
| Apple Wallet | ✅ | ✅ | ✅ | ✅ |
|
||||
| Staff PIN | ❌ | ✅ | ✅ | ✅ |
|
||||
| Cooldown fraud protection | ❌ | ❌ | ✅ | ✅ |
|
||||
| Daily limits | ❌ | ❌ | ✅ | ✅ |
|
||||
| Tablet terminal | ✅ | ✅ | ✅ | ✅ (planned) |
|
||||
| Customer app | ✅ | ✅ | ✅ | Via Wallet |
|
||||
| Analytics dashboard | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
### 3.2 Best Practices to Implement
|
||||
|
||||
#### UX Best Practices
|
||||
|
||||
1. **Instant gratification** - Show stamp/points immediately after transaction
|
||||
2. **Progress visualization** - Clear progress bars/stamp grids
|
||||
3. **Reward proximity** - "Only 2 more until your free coffee!"
|
||||
4. **Wallet-first** - Push customers to save to wallet
|
||||
5. **Offline support** - Card works even without internet (via wallet)
|
||||
|
||||
#### Fraud Prevention Best Practices
|
||||
|
||||
1. **Multi-layer security** - PIN + cooldown + daily limits
|
||||
2. **Staff accountability** - Every transaction tied to a staff PIN
|
||||
3. **Audit trail** - Complete history with IP/device info
|
||||
4. **Lockout protection** - Automatic PIN lockout after failures
|
||||
5. **Admin oversight** - Unlock and PIN management in dashboard
|
||||
|
||||
#### Engagement Best Practices
|
||||
|
||||
1. **Welcome bonus** - Give 1 stamp on enrollment (configurable)
|
||||
2. **Birthday rewards** - Extra stamps/points on customer birthday
|
||||
3. **Milestone notifications** - "Congrats! 50 stamps earned lifetime!"
|
||||
4. **Re-engagement** - Remind inactive customers via email
|
||||
5. **Double points days** - Promotional multipliers (future)
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Implementation Roadmap
|
||||
|
||||
### Phase 2A: Store Interface (Priority)
|
||||
|
||||
| Task | Effort | Priority |
|
||||
|------|--------|----------|
|
||||
| Loyalty terminal (scan/stamp/redeem) | 3 days | P0 |
|
||||
| Program setup wizard | 2 days | P0 |
|
||||
| Staff PIN management | 1 day | P0 |
|
||||
| Customer cards list | 1 day | P1 |
|
||||
| Dashboard with stats | 2 days | P1 |
|
||||
| Export functionality | 1 day | P2 |
|
||||
|
||||
### Phase 2B: Admin Interface
|
||||
|
||||
| Task | Effort | Priority |
|
||||
|------|--------|----------|
|
||||
| Programs list view | 1 day | P1 |
|
||||
| Platform-wide stats | 1 day | P1 |
|
||||
| Program detail view | 0.5 day | P2 |
|
||||
|
||||
### Phase 2C: Customer Experience
|
||||
|
||||
| Task | Effort | Priority |
|
||||
|------|--------|----------|
|
||||
| Enrollment page (mobile) | 1 day | P0 |
|
||||
| Card detail page | 0.5 day | P1 |
|
||||
| Wallet pass polish | 1 day | P1 |
|
||||
| Email templates | 1 day | P2 |
|
||||
|
||||
### Phase 2D: Polish & Advanced
|
||||
|
||||
| Task | Effort | Priority |
|
||||
|------|--------|----------|
|
||||
| QR code scanner (JS) | 2 days | P0 |
|
||||
| Real-time updates (WebSocket) | 1 day | P2 |
|
||||
| Receipt printing | 1 day | P3 |
|
||||
| POS integration hooks | 2 days | P3 |
|
||||
|
||||
---
|
||||
|
||||
## Part 5: Technical Specifications
|
||||
|
||||
### Store Terminal Requirements
|
||||
|
||||
- **Responsive**: Works on tablet (primary), desktop, mobile
|
||||
- **Touch-friendly**: Large buttons, numpad for PIN
|
||||
- **Camera access**: For QR code scanning (WebRTC)
|
||||
- **Offline-capable**: Queue operations if network down (future)
|
||||
- **Real-time**: WebSocket for instant updates
|
||||
|
||||
### Frontend Stack
|
||||
|
||||
- **Framework**: React/Vue components (match existing stack)
|
||||
- **QR Scanner**: `html5-qrcode` or `@aspect-sdk/barcode-reader`
|
||||
- **Charts**: Existing charting library (Chart.js or similar)
|
||||
- **Animations**: CSS transitions for stamp animations
|
||||
|
||||
### API Considerations
|
||||
|
||||
- All store endpoints require `store_id` from auth token
|
||||
- Staff PIN passed in request body, not headers
|
||||
- Rate limiting on lookup/scan endpoints
|
||||
- Pagination on card list (default 50)
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Mockup Reference Images
|
||||
|
||||
### Stamp Card Visual (Wallet Pass)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ ☕ Café du Coin │
|
||||
│ │
|
||||
│ ████ ████ ████ ████ ████ │
|
||||
│ ████ ████ ████ ░░░░ ░░░░ │
|
||||
│ │
|
||||
│ 8/10 STAMPS │
|
||||
│ 2 more until FREE COFFEE │
|
||||
│ │
|
||||
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │
|
||||
│ ▓▓▓▓▓▓▓▓ QR CODE ▓▓▓▓▓▓▓▓ │
|
||||
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │
|
||||
│ │
|
||||
│ Card #4821-7493-2841 │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Points Card Visual (Wallet Pass)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ 🍕 Pizza Roma Rewards │
|
||||
│ │
|
||||
│ ★ 750 ★ │
|
||||
│ POINTS │
|
||||
│ │
|
||||
│ ────────────────────── │
|
||||
│ Next reward: 500 pts │
|
||||
│ Free drink │
|
||||
│ ────────────────────── │
|
||||
│ │
|
||||
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │
|
||||
│ ▓▓▓▓▓▓▓▓ QR CODE ▓▓▓▓▓▓▓▓ │
|
||||
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │
|
||||
│ │
|
||||
│ Card #4821-2847-9283 │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Document Version: 1.0*
|
||||
*Created: 2025-01-28*
|
||||
*Author: Orion Engineering*
|
||||
794
app/modules/loyalty/docs/user-journeys.md
Normal file
794
app/modules/loyalty/docs/user-journeys.md
Normal file
@@ -0,0 +1,794 @@
|
||||
# Loyalty Module - User Journeys
|
||||
|
||||
## Personas
|
||||
|
||||
| # | Persona | Role / Auth | Description |
|
||||
|---|---------|-------------|-------------|
|
||||
| 1 | **Platform Admin** | `admin` role | Oversees all merchants' loyalty programs, views platform-wide stats, manages merchant settings |
|
||||
| 2 | **Merchant Owner** | `store` role + owns merchant | Manages their merchant-wide loyalty program via the store interface. There is **no separate merchant owner UI** - loyalty programs are merchant-scoped but managed through any of the merchant's stores |
|
||||
| 3 | **Store Staff / Team Member** | `store` role + store membership | Operates the POS terminal - scans cards, adds stamps/points, redeems rewards |
|
||||
| 4 | **Customer (authenticated)** | Customer login | Views their loyalty card, balance, and transaction history |
|
||||
| 5 | **Customer (anonymous)** | No auth | Browses program info, self-enrolls, downloads wallet passes |
|
||||
|
||||
!!! note "Merchant Owner vs Store Staff"
|
||||
The loyalty module does **not** have a dedicated merchant owner interface. The merchant owner
|
||||
accesses loyalty through the **store interface** (`/store/{store_code}/loyalty/...`). Since the
|
||||
loyalty program is scoped at the merchant level (one program shared by all stores), the owner
|
||||
can manage it from any of their stores. The difference is only in **permissions** - owners have
|
||||
full access, team members have role-based access.
|
||||
|
||||
---
|
||||
|
||||
## Current Dev Database State
|
||||
|
||||
### Merchants & Stores
|
||||
|
||||
| Merchant | Owner | Stores |
|
||||
|----------|-------|--------|
|
||||
| WizaCorp Ltd. (id=1) | john.owner@wizacorp.com | ORION, WIZAGADGETS, WIZAHOME |
|
||||
| Fashion Group S.A. (id=2) | jane.owner@fashiongroup.com | FASHIONHUB, FASHIONOUTLET |
|
||||
| BookWorld Publishing (id=3) | bob.owner@bookworld.com | BOOKSTORE, BOOKDIGITAL |
|
||||
|
||||
### Users
|
||||
|
||||
| Email | Role | Type |
|
||||
|-------|------|------|
|
||||
| admin@orion.lu | admin | Platform admin |
|
||||
| samir.boulahtit@gmail.com | admin | Platform admin |
|
||||
| john.owner@wizacorp.com | store | Owner of WizaCorp (merchant 1) |
|
||||
| jane.owner@fashiongroup.com | store | Owner of Fashion Group (merchant 2) |
|
||||
| bob.owner@bookworld.com | store | Owner of BookWorld (merchant 3) |
|
||||
| alice.manager@wizacorp.com | store | Team member (stores 1, 2) |
|
||||
| charlie.staff@wizacorp.com | store | Team member (store 3) |
|
||||
| diana.stylist@fashiongroup.com | store | Team member (stores 4, 5) |
|
||||
| eric.sales@fashiongroup.com | store | Team member (store 5) |
|
||||
| fiona.editor@bookworld.com | store | Team member (stores 6, 7) |
|
||||
|
||||
### Loyalty Data Status
|
||||
|
||||
| Table | Rows |
|
||||
|-------|------|
|
||||
| loyalty_programs | 0 |
|
||||
| loyalty_cards | 0 |
|
||||
| loyalty_transactions | 0 |
|
||||
| merchant_loyalty_settings | 0 |
|
||||
| staff_pins | 0 |
|
||||
| merchant_subscriptions | 0 |
|
||||
|
||||
!!! warning "No loyalty programs exist yet"
|
||||
All loyalty tables are empty. The first step in testing is to create a loyalty program
|
||||
via the store interface. There are also **no subscriptions** set up, which may gate access
|
||||
to the loyalty module depending on feature-gating configuration.
|
||||
|
||||
---
|
||||
|
||||
## Dev URLs (localhost:9999)
|
||||
|
||||
The dev server uses path-based platform routing: `http://localhost:9999/platforms/loyalty/...`
|
||||
|
||||
### 1. Platform Admin Pages
|
||||
|
||||
Login as: `admin@orion.lu` or `samir.boulahtit@gmail.com`
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Programs Dashboard | `http://localhost:9999/platforms/loyalty/admin/loyalty/programs` |
|
||||
| Analytics | `http://localhost:9999/platforms/loyalty/admin/loyalty/analytics` |
|
||||
| WizaCorp Detail | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1` |
|
||||
| WizaCorp Settings | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1/settings` |
|
||||
| Fashion Group Detail | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/2` |
|
||||
| Fashion Group Settings | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/2/settings` |
|
||||
| BookWorld Detail | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/3` |
|
||||
| BookWorld Settings | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/3/settings` |
|
||||
|
||||
### 2. Merchant Owner / Store Pages
|
||||
|
||||
Login as the store owner, then navigate to any of their stores.
|
||||
|
||||
**WizaCorp (john.owner@wizacorp.com):**
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Terminal | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal` |
|
||||
| Cards | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/cards` |
|
||||
| Settings | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/settings` |
|
||||
| Stats | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/stats` |
|
||||
| Enroll Customer | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/enroll` |
|
||||
|
||||
**Fashion Group (jane.owner@fashiongroup.com):**
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Terminal | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/terminal` |
|
||||
| Cards | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/cards` |
|
||||
| Settings | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/settings` |
|
||||
| Stats | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/stats` |
|
||||
| Enroll Customer | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/enroll` |
|
||||
|
||||
**BookWorld (bob.owner@bookworld.com):**
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Terminal | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/terminal` |
|
||||
| Cards | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/cards` |
|
||||
| Settings | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/settings` |
|
||||
| Stats | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/stats` |
|
||||
| Enroll Customer | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/enroll` |
|
||||
|
||||
### 3. Customer Storefront Pages
|
||||
|
||||
Login as a customer (e.g., `customer1@orion.example.com`).
|
||||
|
||||
!!! note "Store domain required"
|
||||
Storefront pages require a store domain context. Only ORION (`orion.shop`)
|
||||
and FASHIONHUB (`fashionhub.store`) have domains configured. In dev, storefront
|
||||
routes may need to be accessed through the store's domain or platform path.
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Loyalty Dashboard | `http://localhost:9999/platforms/loyalty/account/loyalty` |
|
||||
| Transaction History | `http://localhost:9999/platforms/loyalty/account/loyalty/history` |
|
||||
|
||||
### 4. Public Pages (No Auth)
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Self-Enrollment | `http://localhost:9999/platforms/loyalty/loyalty/join` |
|
||||
| Enrollment Success | `http://localhost:9999/platforms/loyalty/loyalty/join/success` |
|
||||
|
||||
### 5. API Endpoints
|
||||
|
||||
**Admin API** (prefix: `/platforms/loyalty/api/admin/loyalty/`):
|
||||
|
||||
| Method | Dev URL |
|
||||
|--------|---------|
|
||||
| GET | `http://localhost:9999/platforms/loyalty/api/admin/loyalty/programs` |
|
||||
| GET | `http://localhost:9999/platforms/loyalty/api/admin/loyalty/stats` |
|
||||
|
||||
**Store API** (prefix: `/platforms/loyalty/api/store/loyalty/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | program | `http://localhost:9999/platforms/loyalty/api/store/loyalty/program` |
|
||||
| POST | program | `http://localhost:9999/platforms/loyalty/api/store/loyalty/program` |
|
||||
| POST | stamp | `http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp` |
|
||||
| POST | points | `http://localhost:9999/platforms/loyalty/api/store/loyalty/points` |
|
||||
| POST | enroll | `http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/enroll` |
|
||||
| POST | lookup | `http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup` |
|
||||
|
||||
**Storefront API** (prefix: `/platforms/loyalty/api/storefront/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | program | `http://localhost:9999/platforms/loyalty/api/storefront/loyalty/program` |
|
||||
| POST | enroll | `http://localhost:9999/platforms/loyalty/api/storefront/loyalty/enroll` |
|
||||
| GET | card | `http://localhost:9999/platforms/loyalty/api/storefront/loyalty/card` |
|
||||
| GET | transactions | `http://localhost:9999/platforms/loyalty/api/storefront/loyalty/transactions` |
|
||||
|
||||
**Public API** (prefix: `/platforms/loyalty/api/loyalty/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | program | `http://localhost:9999/platforms/loyalty/api/loyalty/programs/ORION` |
|
||||
|
||||
---
|
||||
|
||||
## Production URLs (rewardflow.lu)
|
||||
|
||||
In production, the platform uses **domain-based routing** instead of the `/platforms/loyalty/` path prefix.
|
||||
Store context is detected via **custom domains** (registered in `store_domains` table)
|
||||
or **subdomains** of `rewardflow.lu` (from `Store.subdomain`).
|
||||
|
||||
### URL Routing Summary
|
||||
|
||||
| Routing mode | Priority | Pattern | Example |
|
||||
|-------------|----------|---------|---------|
|
||||
| Platform domain | — | `rewardflow.lu/...` | Admin pages, public API |
|
||||
| Store custom domain | 1 (highest) | `{custom_domain}/...` | Store with its own domain (overrides merchant domain) |
|
||||
| Merchant domain | 2 | `{merchant_domain}/...` | All stores inherit merchant's domain |
|
||||
| Store subdomain | 3 (fallback) | `{store_code}.rewardflow.lu/...` | Default when no custom/merchant domain |
|
||||
|
||||
!!! info "Domain Resolution Priority"
|
||||
When a request arrives, the middleware resolves the store in this order:
|
||||
|
||||
1. **Store custom domain** (`store_domains` table) — highest priority, store-specific override
|
||||
2. **Merchant domain** (`merchant_domains` table) — inherited by all merchant's stores
|
||||
3. **Store subdomain** (`Store.subdomain` + platform domain) — fallback
|
||||
|
||||
### Case 1: Store with custom domain (e.g., `orion.shop`)
|
||||
|
||||
The store has a verified entry in the `store_domains` table. **All** store URLs
|
||||
(storefront, store backend, store APIs) are served from the custom domain.
|
||||
|
||||
**Storefront (customer-facing):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Loyalty Dashboard | `https://orion.shop/account/loyalty` |
|
||||
| Transaction History | `https://orion.shop/account/loyalty/history` |
|
||||
| Self-Enrollment | `https://orion.shop/loyalty/join` |
|
||||
| Enrollment Success | `https://orion.shop/loyalty/join/success` |
|
||||
|
||||
**Storefront API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET card | `https://orion.shop/api/storefront/loyalty/card` |
|
||||
| GET transactions | `https://orion.shop/api/storefront/loyalty/transactions` |
|
||||
| POST enroll | `https://orion.shop/api/storefront/loyalty/enroll` |
|
||||
| GET program | `https://orion.shop/api/storefront/loyalty/program` |
|
||||
|
||||
**Store backend (staff/owner):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Store Login | `https://orion.shop/store/ORION/login` |
|
||||
| Terminal | `https://orion.shop/store/ORION/loyalty/terminal` |
|
||||
| Cards | `https://orion.shop/store/ORION/loyalty/cards` |
|
||||
| Card Detail | `https://orion.shop/store/ORION/loyalty/cards/{card_id}` |
|
||||
| Settings | `https://orion.shop/store/ORION/loyalty/settings` |
|
||||
| Stats | `https://orion.shop/store/ORION/loyalty/stats` |
|
||||
| Enroll Customer | `https://orion.shop/store/ORION/loyalty/enroll` |
|
||||
|
||||
**Store API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET program | `https://orion.shop/api/store/loyalty/program` |
|
||||
| POST program | `https://orion.shop/api/store/loyalty/program` |
|
||||
| POST stamp | `https://orion.shop/api/store/loyalty/stamp` |
|
||||
| POST points | `https://orion.shop/api/store/loyalty/points` |
|
||||
| POST enroll | `https://orion.shop/api/store/loyalty/cards/enroll` |
|
||||
| POST lookup | `https://orion.shop/api/store/loyalty/cards/lookup` |
|
||||
|
||||
### Case 2: Store with merchant domain (e.g., `myloyaltyprogram.lu`)
|
||||
|
||||
The merchant has registered a domain in the `merchant_domains` table. Stores without
|
||||
their own custom domain inherit the merchant domain. The middleware resolves the
|
||||
merchant domain to the merchant's first active store by default, or to a specific
|
||||
store when the URL includes `/store/{store_code}/...`.
|
||||
|
||||
**Storefront (customer-facing):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Loyalty Dashboard | `https://myloyaltyprogram.lu/account/loyalty` |
|
||||
| Transaction History | `https://myloyaltyprogram.lu/account/loyalty/history` |
|
||||
| Self-Enrollment | `https://myloyaltyprogram.lu/loyalty/join` |
|
||||
| Enrollment Success | `https://myloyaltyprogram.lu/loyalty/join/success` |
|
||||
|
||||
**Storefront API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET card | `https://myloyaltyprogram.lu/api/storefront/loyalty/card` |
|
||||
| GET transactions | `https://myloyaltyprogram.lu/api/storefront/loyalty/transactions` |
|
||||
| POST enroll | `https://myloyaltyprogram.lu/api/storefront/loyalty/enroll` |
|
||||
| GET program | `https://myloyaltyprogram.lu/api/storefront/loyalty/program` |
|
||||
|
||||
**Store backend (staff/owner):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Store Login | `https://myloyaltyprogram.lu/store/WIZAGADGETS/login` |
|
||||
| Terminal | `https://myloyaltyprogram.lu/store/WIZAGADGETS/loyalty/terminal` |
|
||||
| Cards | `https://myloyaltyprogram.lu/store/WIZAGADGETS/loyalty/cards` |
|
||||
| Settings | `https://myloyaltyprogram.lu/store/WIZAGADGETS/loyalty/settings` |
|
||||
| Stats | `https://myloyaltyprogram.lu/store/WIZAGADGETS/loyalty/stats` |
|
||||
|
||||
**Store API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET program | `https://myloyaltyprogram.lu/api/store/loyalty/program` |
|
||||
| POST stamp | `https://myloyaltyprogram.lu/api/store/loyalty/stamp` |
|
||||
| POST points | `https://myloyaltyprogram.lu/api/store/loyalty/points` |
|
||||
| POST enroll | `https://myloyaltyprogram.lu/api/store/loyalty/cards/enroll` |
|
||||
| POST lookup | `https://myloyaltyprogram.lu/api/store/loyalty/cards/lookup` |
|
||||
|
||||
!!! note "Merchant domain resolves to first active store"
|
||||
When a customer visits `myloyaltyprogram.lu` without a `/store/{code}/...` path,
|
||||
the middleware resolves to the merchant's **first active store** (ordered by ID).
|
||||
This is ideal for storefront pages like `/loyalty/join` where the customer doesn't
|
||||
need to know which specific store they're interacting with.
|
||||
|
||||
### Case 3: Store without custom domain (uses platform subdomain)
|
||||
|
||||
The store has no entry in `store_domains` and the merchant has no registered domain.
|
||||
**All** store URLs are served via a subdomain of the platform domain: `{store_code}.rewardflow.lu`.
|
||||
|
||||
**Storefront (customer-facing):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Loyalty Dashboard | `https://bookstore.rewardflow.lu/account/loyalty` |
|
||||
| Transaction History | `https://bookstore.rewardflow.lu/account/loyalty/history` |
|
||||
| Self-Enrollment | `https://bookstore.rewardflow.lu/loyalty/join` |
|
||||
| Enrollment Success | `https://bookstore.rewardflow.lu/loyalty/join/success` |
|
||||
|
||||
**Storefront API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET card | `https://bookstore.rewardflow.lu/api/storefront/loyalty/card` |
|
||||
| GET transactions | `https://bookstore.rewardflow.lu/api/storefront/loyalty/transactions` |
|
||||
| POST enroll | `https://bookstore.rewardflow.lu/api/storefront/loyalty/enroll` |
|
||||
| GET program | `https://bookstore.rewardflow.lu/api/storefront/loyalty/program` |
|
||||
|
||||
**Store backend (staff/owner):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Store Login | `https://bookstore.rewardflow.lu/store/BOOKSTORE/login` |
|
||||
| Terminal | `https://bookstore.rewardflow.lu/store/BOOKSTORE/loyalty/terminal` |
|
||||
| Cards | `https://bookstore.rewardflow.lu/store/BOOKSTORE/loyalty/cards` |
|
||||
| Settings | `https://bookstore.rewardflow.lu/store/BOOKSTORE/loyalty/settings` |
|
||||
| Stats | `https://bookstore.rewardflow.lu/store/BOOKSTORE/loyalty/stats` |
|
||||
|
||||
**Store API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET program | `https://bookstore.rewardflow.lu/api/store/loyalty/program` |
|
||||
| POST stamp | `https://bookstore.rewardflow.lu/api/store/loyalty/stamp` |
|
||||
| POST points | `https://bookstore.rewardflow.lu/api/store/loyalty/points` |
|
||||
| POST enroll | `https://bookstore.rewardflow.lu/api/store/loyalty/cards/enroll` |
|
||||
| POST lookup | `https://bookstore.rewardflow.lu/api/store/loyalty/cards/lookup` |
|
||||
|
||||
### Platform Admin & Public API (always on platform domain)
|
||||
|
||||
| Page / Endpoint | Production URL |
|
||||
|-----------------|----------------|
|
||||
| Admin Programs | `https://rewardflow.lu/admin/loyalty/programs` |
|
||||
| Admin Analytics | `https://rewardflow.lu/admin/loyalty/analytics` |
|
||||
| Admin Merchant Detail | `https://rewardflow.lu/admin/loyalty/merchants/{id}` |
|
||||
| Admin Merchant Settings | `https://rewardflow.lu/admin/loyalty/merchants/{id}/settings` |
|
||||
| Admin API - Programs | `GET https://rewardflow.lu/api/admin/loyalty/programs` |
|
||||
| Admin API - Stats | `GET https://rewardflow.lu/api/admin/loyalty/stats` |
|
||||
| Public API - Program | `GET https://rewardflow.lu/api/loyalty/programs/ORION` |
|
||||
| Apple Wallet Pass | `GET https://rewardflow.lu/api/loyalty/passes/apple/{serial}.pkpass` |
|
||||
|
||||
### Domain configuration per store (current DB state)
|
||||
|
||||
**Merchant domains** (`merchant_domains` table):
|
||||
|
||||
| Merchant | Merchant Domain | Status |
|
||||
|----------|-----------------|--------|
|
||||
| WizaCorp Ltd. | _(none yet)_ | — |
|
||||
| Fashion Group S.A. | _(none yet)_ | — |
|
||||
| BookWorld Publishing | _(none yet)_ | — |
|
||||
|
||||
**Store domains** (`store_domains` table) and effective resolution:
|
||||
|
||||
| Store | Merchant | Store Custom Domain | Effective Domain |
|
||||
|-------|----------|---------------------|------------------|
|
||||
| ORION | WizaCorp | `orion.shop` | `orion.shop` (store override) |
|
||||
| FASHIONHUB | Fashion Group | `fashionhub.store` | `fashionhub.store` (store override) |
|
||||
| WIZAGADGETS | WizaCorp | _(none)_ | `wizagadgets.rewardflow.lu` (subdomain fallback) |
|
||||
| WIZAHOME | WizaCorp | _(none)_ | `wizahome.rewardflow.lu` (subdomain fallback) |
|
||||
| FASHIONOUTLET | Fashion Group | _(none)_ | `fashionoutlet.rewardflow.lu` (subdomain fallback) |
|
||||
| BOOKSTORE | BookWorld | _(none)_ | `bookstore.rewardflow.lu` (subdomain fallback) |
|
||||
| BOOKDIGITAL | BookWorld | _(none)_ | `bookdigital.rewardflow.lu` (subdomain fallback) |
|
||||
|
||||
!!! example "After merchant domain registration"
|
||||
If WizaCorp registers `myloyaltyprogram.lu` as their merchant domain, the table becomes:
|
||||
|
||||
| Store | Effective Domain | Reason |
|
||||
|-------|------------------|--------|
|
||||
| ORION | `orion.shop` | Store custom domain takes priority |
|
||||
| WIZAGADGETS | `myloyaltyprogram.lu` | Inherits merchant domain |
|
||||
| WIZAHOME | `myloyaltyprogram.lu` | Inherits merchant domain |
|
||||
|
||||
!!! info "`{store_domain}` in journey URLs"
|
||||
In the journeys below, `{store_domain}` refers to the store's **effective domain**, resolved in priority order:
|
||||
|
||||
1. **Store custom domain**: `orion.shop` (from `store_domains` table) — highest priority
|
||||
2. **Merchant domain**: `myloyaltyprogram.lu` (from `merchant_domains` table) — inherited default
|
||||
3. **Subdomain fallback**: `orion.rewardflow.lu` (from `Store.subdomain` + platform domain)
|
||||
|
||||
---
|
||||
|
||||
## User Journeys
|
||||
|
||||
### Journey 0: Merchant Subscription & Domain Setup
|
||||
|
||||
**Persona:** Merchant Owner (e.g., john.owner@wizacorp.com) + Platform Admin
|
||||
**Goal:** Subscribe to the loyalty platform, register a merchant domain, and optionally configure store domain overrides
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Merchant owner logs in] --> B[Navigate to billing page]
|
||||
B --> C[Choose subscription tier]
|
||||
C --> D[Complete Stripe checkout]
|
||||
D --> E[Subscription active]
|
||||
E --> F{Register merchant domain?}
|
||||
F -->|Yes| G[Admin registers merchant domain]
|
||||
G --> H[Verify DNS ownership]
|
||||
H --> I[Activate merchant domain]
|
||||
I --> J{Store-specific override?}
|
||||
J -->|Yes| K[Register store custom domain]
|
||||
K --> L[Verify & activate store domain]
|
||||
J -->|No| M[All stores inherit merchant domain]
|
||||
F -->|No| N[Stores use subdomain fallback]
|
||||
L --> O[Domain setup complete]
|
||||
M --> O
|
||||
N --> O
|
||||
```
|
||||
|
||||
**Step 1: Subscribe to the platform**
|
||||
|
||||
1. Login as `john.owner@wizacorp.com` and navigate to billing:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/billing`
|
||||
- Prod (custom domain): `https://orion.shop/store/ORION/billing`
|
||||
- Prod (subdomain): `https://orion.rewardflow.lu/store/ORION/billing`
|
||||
2. View available subscription tiers:
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/v1/store/billing/tiers`
|
||||
- API Prod: `GET https://{store_domain}/api/v1/store/billing/tiers`
|
||||
3. Select a tier and initiate Stripe checkout:
|
||||
- API Dev: `POST http://localhost:9999/platforms/loyalty/api/v1/store/billing/checkout`
|
||||
- API Prod: `POST https://{store_domain}/api/v1/store/billing/checkout`
|
||||
4. Complete payment on Stripe checkout page
|
||||
5. Webhook `checkout.session.completed` activates the subscription
|
||||
6. Verify subscription is active:
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/v1/store/billing/subscription`
|
||||
- API Prod: `GET https://{store_domain}/api/v1/store/billing/subscription`
|
||||
|
||||
**Step 2: Register merchant domain (admin action)**
|
||||
|
||||
!!! note "Admin-only operation"
|
||||
Merchant domain registration is currently an admin operation. The platform admin
|
||||
registers the domain on behalf of the merchant via the admin API.
|
||||
|
||||
1. Platform admin registers a merchant domain:
|
||||
- API Dev: `POST http://localhost:9999/platforms/loyalty/api/v1/admin/merchants/{merchant_id}/domains`
|
||||
- API Prod: `POST https://rewardflow.lu/api/v1/admin/merchants/{merchant_id}/domains`
|
||||
- Body: `{"domain": "myloyaltyprogram.lu", "is_primary": true}`
|
||||
2. The API returns a `verification_token` for DNS verification
|
||||
3. Get DNS verification instructions:
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/v1/admin/merchants/domains/merchant/{domain_id}/verification-instructions`
|
||||
- API Prod: `GET https://rewardflow.lu/api/v1/admin/merchants/domains/merchant/{domain_id}/verification-instructions`
|
||||
4. Merchant adds a DNS TXT record: `_orion-verify.myloyaltyprogram.lu TXT {verification_token}`
|
||||
5. Verify the domain:
|
||||
- API Dev: `POST http://localhost:9999/platforms/loyalty/api/v1/admin/merchants/domains/merchant/{domain_id}/verify`
|
||||
- API Prod: `POST https://rewardflow.lu/api/v1/admin/merchants/domains/merchant/{domain_id}/verify`
|
||||
6. Activate the domain:
|
||||
- API Dev: `PUT http://localhost:9999/platforms/loyalty/api/v1/admin/merchants/domains/merchant/{domain_id}`
|
||||
- API Prod: `PUT https://rewardflow.lu/api/v1/admin/merchants/domains/merchant/{domain_id}`
|
||||
- Body: `{"is_active": true}`
|
||||
7. All merchant stores now inherit `myloyaltyprogram.lu` as their effective domain
|
||||
|
||||
**Step 3: (Optional) Register store-specific domain override**
|
||||
|
||||
If a store needs its own domain (e.g., ORION is a major brand and wants `mysuperloyaltyprogram.lu`):
|
||||
|
||||
1. Platform admin registers a store domain:
|
||||
- API Dev: `POST http://localhost:9999/platforms/loyalty/api/v1/admin/stores/{store_id}/domains`
|
||||
- API Prod: `POST https://rewardflow.lu/api/v1/admin/stores/{store_id}/domains`
|
||||
- Body: `{"domain": "mysuperloyaltyprogram.lu", "is_primary": true}`
|
||||
2. Follow the same DNS verification and activation flow as merchant domains
|
||||
3. Once active, this store's effective domain becomes `mysuperloyaltyprogram.lu` (overrides merchant domain)
|
||||
4. Other stores (WIZAGADGETS, WIZAHOME) continue to use `myloyaltyprogram.lu`
|
||||
|
||||
**Result after domain setup for WizaCorp:**
|
||||
|
||||
| Store | Effective Domain | Source |
|
||||
|-------|------------------|--------|
|
||||
| ORION | `mysuperloyaltyprogram.lu` | Store custom domain (override) |
|
||||
| WIZAGADGETS | `myloyaltyprogram.lu` | Merchant domain (inherited) |
|
||||
| WIZAHOME | `myloyaltyprogram.lu` | Merchant domain (inherited) |
|
||||
|
||||
**Expected blockers in current state:**
|
||||
|
||||
- No subscriptions exist yet - create one first via billing page or admin API
|
||||
- No merchant domains registered - admin must register via API
|
||||
- DNS verification requires actual DNS records (mock in tests)
|
||||
|
||||
---
|
||||
|
||||
### Journey 1: Merchant Owner - First-Time Setup
|
||||
|
||||
**Persona:** Merchant Owner (e.g., john.owner@wizacorp.com)
|
||||
**Goal:** Set up a loyalty program for their merchant
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Login as store owner] --> B[Navigate to store loyalty settings]
|
||||
B --> C{Program exists?}
|
||||
C -->|No| D[Create loyalty program]
|
||||
D --> E[Choose type: stamps / points / hybrid]
|
||||
E --> F[Configure program settings]
|
||||
F --> G[Set branding - colors, logo]
|
||||
G --> H[Configure anti-fraud settings]
|
||||
H --> I[Create staff PINs]
|
||||
I --> J[Program is live]
|
||||
C -->|Yes| K[View/edit existing program]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Login as `john.owner@wizacorp.com` at:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/login`
|
||||
- Prod (custom domain): `https://orion.shop/store/ORION/login`
|
||||
- Prod (subdomain): `https://orion.rewardflow.lu/store/ORION/login`
|
||||
2. Navigate to loyalty settings:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/settings`
|
||||
- Prod (custom domain): `https://orion.shop/store/ORION/loyalty/settings`
|
||||
- Prod (subdomain): `https://orion.rewardflow.lu/store/ORION/loyalty/settings`
|
||||
3. Create a new loyalty program:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/program`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/program`
|
||||
4. Choose loyalty type (stamps, points, or hybrid)
|
||||
5. Configure program parameters (stamp target, points-per-euro, rewards)
|
||||
6. Set branding (card color, logo, hero image)
|
||||
7. Configure anti-fraud (cooldown, daily limits, PIN requirements)
|
||||
8. Create staff PINs:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/pins`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/pins`
|
||||
9. Verify program is live - check from another store (same merchant):
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/WIZAGADGETS/loyalty/settings`
|
||||
- Prod (subdomain): `https://wizagadgets.rewardflow.lu/store/WIZAGADGETS/loyalty/settings`
|
||||
|
||||
**Expected blockers in current state:**
|
||||
|
||||
- No loyalty programs exist - this is the first journey to test
|
||||
|
||||
!!! note "Subscription is not required for program creation"
|
||||
The loyalty module currently has **no feature gating** — program creation works
|
||||
without an active subscription. Journey 0 (subscription & domain setup) is
|
||||
independent and can be done before or after program creation. However, in production
|
||||
you would typically subscribe first to get a custom domain for your loyalty URLs.
|
||||
|
||||
---
|
||||
|
||||
### Journey 2: Store Staff - Daily Operations (Stamps)
|
||||
|
||||
**Persona:** Store Staff (e.g., alice.manager@wizacorp.com)
|
||||
**Goal:** Process customer loyalty stamp transactions
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Open terminal] --> B[Customer presents card/QR]
|
||||
B --> C[Scan/lookup card]
|
||||
C --> D[Enter staff PIN]
|
||||
D --> E[Add stamp]
|
||||
E --> F{Target reached?}
|
||||
F -->|Yes| G[Prompt: Redeem reward?]
|
||||
G -->|Yes| H[Redeem stamps for reward]
|
||||
G -->|No| I[Save for later]
|
||||
F -->|No| J[Done - show updated count]
|
||||
H --> J
|
||||
I --> J
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Login as `alice.manager@wizacorp.com` and open the terminal:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/terminal`
|
||||
2. Scan customer QR code or enter card number:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/cards/lookup`
|
||||
3. Enter staff PIN for verification
|
||||
4. Add stamp:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp`
|
||||
5. If target reached, redeem reward:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp/redeem`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp/redeem`
|
||||
6. View updated card:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/cards/{card_id}`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/cards/{card_id}`
|
||||
7. Browse all cards:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/cards`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/cards`
|
||||
|
||||
**Anti-fraud scenarios to test:**
|
||||
|
||||
- Cooldown rejection (stamp within 15 min)
|
||||
- Daily limit hit (max 5 stamps/day)
|
||||
- PIN lockout (5 failed attempts)
|
||||
|
||||
---
|
||||
|
||||
### Journey 3: Store Staff - Daily Operations (Points)
|
||||
|
||||
**Persona:** Store Staff (e.g., alice.manager@wizacorp.com)
|
||||
**Goal:** Process customer loyalty points from purchase
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Open terminal] --> B[Customer presents card]
|
||||
B --> C[Scan/lookup card]
|
||||
C --> D[Enter purchase amount]
|
||||
D --> E[Enter staff PIN]
|
||||
E --> F[Points calculated & added]
|
||||
F --> G{Enough for reward?}
|
||||
G -->|Yes| H[Offer redemption]
|
||||
G -->|No| I[Done - show balance]
|
||||
H --> I
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Open the terminal:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/terminal`
|
||||
2. Lookup card:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/cards/lookup`
|
||||
3. Enter purchase amount (e.g., 25.00 EUR)
|
||||
4. Earn points (auto-calculated at 10 pts/EUR = 250 points):
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/points`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/points`
|
||||
5. If enough balance, redeem points for reward:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/points/redeem`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/points/redeem`
|
||||
6. Check store-level stats:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/stats`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/stats`
|
||||
|
||||
---
|
||||
|
||||
### Journey 4: Customer Self-Enrollment
|
||||
|
||||
**Persona:** Anonymous Customer
|
||||
**Goal:** Join a merchant's loyalty program
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[See QR code at store counter] --> B[Scan QR / visit enrollment page]
|
||||
B --> C[Fill in details - email, name]
|
||||
C --> D[Submit enrollment]
|
||||
D --> E[Receive card number]
|
||||
E --> F[Optional: Add to Apple/Google Wallet]
|
||||
F --> G[Start collecting stamps/points]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Visit the public enrollment page:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/loyalty/join`
|
||||
- Prod (custom domain): `https://orion.shop/loyalty/join`
|
||||
- Prod (subdomain): `https://bookstore.rewardflow.lu/loyalty/join`
|
||||
2. Fill in enrollment form (email, name)
|
||||
3. Submit enrollment:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/storefront/loyalty/enroll`
|
||||
- Prod (custom domain): `POST https://orion.shop/api/storefront/loyalty/enroll`
|
||||
- Prod (subdomain): `POST https://bookstore.rewardflow.lu/api/storefront/loyalty/enroll`
|
||||
4. Redirected to success page:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/loyalty/join/success?card=XXXX-XXXX-XXXX`
|
||||
- Prod (custom domain): `https://orion.shop/loyalty/join/success?card=XXXX-XXXX-XXXX`
|
||||
- Prod (subdomain): `https://bookstore.rewardflow.lu/loyalty/join/success?card=XXXX-XXXX-XXXX`
|
||||
5. Optionally download Apple Wallet pass:
|
||||
- Dev: `GET http://localhost:9999/platforms/loyalty/api/loyalty/passes/apple/{serial_number}.pkpass`
|
||||
- Prod: `GET https://rewardflow.lu/api/loyalty/passes/apple/{serial_number}.pkpass`
|
||||
|
||||
---
|
||||
|
||||
### Journey 5: Customer - View Loyalty Status
|
||||
|
||||
**Persona:** Authenticated Customer (e.g., `customer1@orion.example.com`)
|
||||
**Goal:** Check loyalty balance and history
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Login as customer at the storefront
|
||||
2. View loyalty dashboard (card balance, available rewards):
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/account/loyalty`
|
||||
- Prod (custom domain): `https://orion.shop/account/loyalty`
|
||||
- Prod (subdomain): `https://bookstore.rewardflow.lu/account/loyalty`
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/storefront/loyalty/card`
|
||||
- API Prod: `GET https://orion.shop/api/storefront/loyalty/card`
|
||||
3. View full transaction history:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/account/loyalty/history`
|
||||
- Prod (custom domain): `https://orion.shop/account/loyalty/history`
|
||||
- Prod (subdomain): `https://bookstore.rewardflow.lu/account/loyalty/history`
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/storefront/loyalty/transactions`
|
||||
- API Prod: `GET https://orion.shop/api/storefront/loyalty/transactions`
|
||||
|
||||
---
|
||||
|
||||
### Journey 6: Platform Admin - Oversight
|
||||
|
||||
**Persona:** Platform Admin (`admin@orion.lu` or `samir.boulahtit@gmail.com`)
|
||||
**Goal:** Monitor all loyalty programs across merchants
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Login as admin
|
||||
2. View all programs:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/programs`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/programs`
|
||||
3. View platform-wide analytics:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/analytics`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/analytics`
|
||||
4. Drill into WizaCorp's program:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/merchants/1`
|
||||
5. Manage WizaCorp's merchant-level settings:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1/settings`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/merchants/1/settings`
|
||||
- API Dev: `PATCH http://localhost:9999/platforms/loyalty/api/admin/loyalty/merchants/1/settings`
|
||||
- API Prod: `PATCH https://rewardflow.lu/api/admin/loyalty/merchants/1/settings`
|
||||
6. Adjust settings: PIN policy, self-enrollment toggle, void permissions
|
||||
7. Check other merchants:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/2`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/merchants/2`
|
||||
|
||||
---
|
||||
|
||||
### Journey 7: Void / Return Flow
|
||||
|
||||
**Persona:** Store Staff (e.g., alice.manager@wizacorp.com)
|
||||
**Goal:** Reverse a loyalty transaction (customer return)
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Open terminal and lookup card:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/terminal`
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/cards/lookup`
|
||||
2. View the card's transaction history to find the transaction to void:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/cards/{card_id}`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/cards/{card_id}`
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/{card_id}/transactions`
|
||||
- API Prod: `GET https://{store_domain}/api/store/loyalty/cards/{card_id}/transactions`
|
||||
3. Void a stamp transaction:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp/void`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp/void`
|
||||
4. Or void a points transaction:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/points/void`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/points/void`
|
||||
5. Verify: original and void transactions are linked in the audit log
|
||||
|
||||
---
|
||||
|
||||
### Journey 8: Cross-Store Redemption
|
||||
|
||||
**Persona:** Customer + Store Staff at two different stores
|
||||
**Goal:** Customer earns at Store A, redeems at Store B (same merchant)
|
||||
|
||||
**Precondition:** Cross-location redemption must be enabled in merchant settings:
|
||||
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1/settings`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/merchants/1/settings`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Staff at ORION adds stamps to customer's card:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/terminal`
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp`
|
||||
2. Customer visits WIZAGADGETS
|
||||
3. Staff at WIZAGADGETS looks up the same card:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/WIZAGADGETS/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/WIZAGADGETS/loyalty/terminal`
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/cards/lookup`
|
||||
4. Card is found (same merchant) with accumulated stamps
|
||||
5. Staff at WIZAGADGETS redeems the reward:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp/redeem`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp/redeem`
|
||||
6. Verify transaction history shows both stores:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/WIZAGADGETS/loyalty/cards/{card_id}`
|
||||
- Prod: `https://{store_domain}/store/WIZAGADGETS/loyalty/cards/{card_id}`
|
||||
|
||||
---
|
||||
|
||||
## Recommended Test Order
|
||||
|
||||
1. **Journey 1** - Create a program first (nothing else works without this)
|
||||
2. **Journey 0** - Subscribe and set up domains (independent, but needed for custom domain URLs)
|
||||
3. **Journey 4** - Enroll a test customer
|
||||
4. **Journey 2 or 3** - Process stamps/points
|
||||
5. **Journey 5** - Verify customer can see their data
|
||||
6. **Journey 7** - Test void/return
|
||||
7. **Journey 8** - Test cross-store (enroll via ORION, redeem via WIZAGADGETS)
|
||||
8. **Journey 6** - Admin overview (verify data appears correctly)
|
||||
|
||||
!!! tip "Journey 0 and Journey 1 are independent"
|
||||
There is no feature gating on loyalty program creation — you can test them in
|
||||
either order. Journey 0 is listed second because domain setup is about URL
|
||||
presentation, not a functional prerequisite for the loyalty module.
|
||||
Reference in New Issue
Block a user