Some checks failed
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>
795 lines
36 KiB
Markdown
795 lines
36 KiB
Markdown
# 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.
|