Clean up 28 backward compatibility instances identified in the codebase. The app is not live, so all shims are replaced with the target architecture: - Remove legacy Inventory.location column (use bin_location exclusively) - Remove dashboard _extract_metric_value helper (use flat metrics dict) - Remove legacy stat field duplicates (total_stores, total_imports, etc.) - Remove 13 re-export shims and class aliases across modules - Remove module-enabling JSON fallback (use PlatformModule junction table) - Remove menu_to_legacy_format() conversion (return dataclasses directly) - Remove title/description from MarketplaceProductBase schema - Clean billing convenience method docstrings - Clean test fixtures and backward-compat comments - Add PlatformModule seeding to init_production.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
36 KiB
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
| 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
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
- Login as
john.owner@wizacorp.comand 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
- Dev:
- 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
- API Dev:
- 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
- API Dev:
- Complete payment on Stripe checkout page
- Webhook
checkout.session.completedactivates the subscription - 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
- API Dev:
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.
- 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}
- API Dev:
- The API returns a
verification_tokenfor DNS verification - 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
- API Dev:
- Merchant adds a DNS TXT record:
_orion-verify.myloyaltyprogram.lu TXT {verification_token} - 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
- API Dev:
- 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}
- API Dev:
- All merchant stores now inherit
myloyaltyprogram.luas 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):
- 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}
- API Dev:
- Follow the same DNS verification and activation flow as merchant domains
- Once active, this store's effective domain becomes
mysuperloyaltyprogram.lu(overrides merchant domain) - 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
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:
- Login as
john.owner@wizacorp.comat:- 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
- Dev:
- 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
- Dev:
- 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
- Dev:
- Choose loyalty type (stamps, points, or hybrid)
- Configure program parameters (stamp target, points-per-euro, rewards)
- Set branding (card color, logo, hero image)
- Configure anti-fraud (cooldown, daily limits, PIN requirements)
- Create staff PINs:
- Dev:
POST http://localhost:9999/platforms/loyalty/api/store/loyalty/pins - Prod:
POST https://{store_domain}/api/store/loyalty/pins
- Dev:
- 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
- Dev:
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
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:
- Login as
alice.manager@wizacorp.comand open the terminal:- Dev:
http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal - Prod:
https://{store_domain}/store/ORION/loyalty/terminal
- Dev:
- 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
- Dev:
- Enter staff PIN for verification
- Add stamp:
- Dev:
POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp - Prod:
POST https://{store_domain}/api/store/loyalty/stamp
- Dev:
- 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
- Dev:
- 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}
- Dev:
- Browse all cards:
- Dev:
http://localhost:9999/platforms/loyalty/store/ORION/loyalty/cards - Prod:
https://{store_domain}/store/ORION/loyalty/cards
- Dev:
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
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:
- Open the terminal:
- Dev:
http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal - Prod:
https://{store_domain}/store/ORION/loyalty/terminal
- Dev:
- Lookup card:
- Dev:
POST http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup - Prod:
POST https://{store_domain}/api/store/loyalty/cards/lookup
- Dev:
- Enter purchase amount (e.g., 25.00 EUR)
- 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
- Dev:
- 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
- Dev:
- Check store-level stats:
- Dev:
http://localhost:9999/platforms/loyalty/store/ORION/loyalty/stats - Prod:
https://{store_domain}/store/ORION/loyalty/stats
- Dev:
Journey 4: Customer Self-Enrollment
Persona: Anonymous Customer Goal: Join a merchant's loyalty program
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:
- 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
- Dev:
- Fill in enrollment form (email, name)
- 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
- Dev:
- 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
- Dev:
- 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
- Dev:
Journey 5: Customer - View Loyalty Status
Persona: Authenticated Customer (e.g., customer1@orion.example.com)
Goal: Check loyalty balance and history
Steps:
- Login as customer at the storefront
- 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
- Dev:
- 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
- Dev:
Journey 6: Platform Admin - Oversight
Persona: Platform Admin (admin@orion.lu or samir.boulahtit@gmail.com)
Goal: Monitor all loyalty programs across merchants
Steps:
- Login as admin
- View all programs:
- Dev:
http://localhost:9999/platforms/loyalty/admin/loyalty/programs - Prod:
https://rewardflow.lu/admin/loyalty/programs
- Dev:
- View platform-wide analytics:
- Dev:
http://localhost:9999/platforms/loyalty/admin/loyalty/analytics - Prod:
https://rewardflow.lu/admin/loyalty/analytics
- Dev:
- Drill into WizaCorp's program:
- Dev:
http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1 - Prod:
https://rewardflow.lu/admin/loyalty/merchants/1
- Dev:
- 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
- Dev:
- Adjust settings: PIN policy, self-enrollment toggle, void permissions
- Check other merchants:
- Dev:
http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/2 - Prod:
https://rewardflow.lu/admin/loyalty/merchants/2
- Dev:
Journey 7: Void / Return Flow
Persona: Store Staff (e.g., alice.manager@wizacorp.com) Goal: Reverse a loyalty transaction (customer return)
Steps:
- 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
- Dev:
- 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
- Dev:
- 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
- Dev:
- 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
- Dev:
- 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:
- 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
- Dev:
- Customer visits WIZAGADGETS
- 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
- Dev:
- Card is found (same merchant) with accumulated stamps
- 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
- Dev:
- 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}
- Dev:
Recommended Test Order
- Journey 1 - Create a program first (nothing else works without this)
- Journey 0 - Subscribe and set up domains (independent, but needed for custom domain URLs)
- Journey 4 - Enroll a test customer
- Journey 2 or 3 - Process stamps/points
- Journey 5 - Verify customer can see their data
- Journey 7 - Test void/return
- Journey 8 - Test cross-store (enroll via ORION, redeem via WIZAGADGETS)
- 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.