feat(subscriptions): migrate subscription management to merchant level and seed tiers

Move subscription create/edit from store detail (broken endpoint) to merchant
detail page with proper modal UI. Seed 4 subscription tiers (Essential,
Professional, Business, Enterprise) in init_production.py. Also includes
cross-module dependency declarations, store domain platform_id migration,
platform context middleware, CMS route fixes, and migration backups.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 21:04:04 +01:00
parent 7feacd5af8
commit 68493dc6cb
97 changed files with 13286 additions and 77 deletions

View File

@@ -0,0 +1,609 @@
# 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 | WIZAMART, 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@wizamart.com | 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@wizamart.com` 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/WIZAMART/loyalty/terminal` |
| Cards | `http://localhost:9999/platforms/loyalty/store/WIZAMART/loyalty/cards` |
| Settings | `http://localhost:9999/platforms/loyalty/store/WIZAMART/loyalty/settings` |
| Stats | `http://localhost:9999/platforms/loyalty/store/WIZAMART/loyalty/stats` |
| Enroll Customer | `http://localhost:9999/platforms/loyalty/store/WIZAMART/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@wizamart.example.com`).
!!! note "Store domain required"
Storefront pages require a store domain context. Only WIZAMART (`wizamart.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/WIZAMART` |
---
## Production URLs (loyalty.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 `loyalty.lu` (from `Store.subdomain`).
### URL Routing Summary
| Routing mode | Pattern | Example |
|-------------|---------|---------|
| Platform domain | `loyalty.lu/...` | Admin pages, public API |
| Custom domain | `{custom_domain}/...` | All store pages (store has custom domain) |
| Store subdomain | `{store_code}.loyalty.lu/...` | All store pages (no custom domain) |
### Case 1: Store with custom domain (e.g., `wizamart.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://wizamart.shop/account/loyalty` |
| Transaction History | `https://wizamart.shop/account/loyalty/history` |
| Self-Enrollment | `https://wizamart.shop/loyalty/join` |
| Enrollment Success | `https://wizamart.shop/loyalty/join/success` |
**Storefront API:**
| Method | Production URL |
|--------|----------------|
| GET card | `https://wizamart.shop/api/storefront/loyalty/card` |
| GET transactions | `https://wizamart.shop/api/storefront/loyalty/transactions` |
| POST enroll | `https://wizamart.shop/api/storefront/loyalty/enroll` |
| GET program | `https://wizamart.shop/api/storefront/loyalty/program` |
**Store backend (staff/owner):**
| Page | Production URL |
|------|----------------|
| Store Login | `https://wizamart.shop/store/WIZAMART/login` |
| Terminal | `https://wizamart.shop/store/WIZAMART/loyalty/terminal` |
| Cards | `https://wizamart.shop/store/WIZAMART/loyalty/cards` |
| Card Detail | `https://wizamart.shop/store/WIZAMART/loyalty/cards/{card_id}` |
| Settings | `https://wizamart.shop/store/WIZAMART/loyalty/settings` |
| Stats | `https://wizamart.shop/store/WIZAMART/loyalty/stats` |
| Enroll Customer | `https://wizamart.shop/store/WIZAMART/loyalty/enroll` |
**Store API:**
| Method | Production URL |
|--------|----------------|
| GET program | `https://wizamart.shop/api/store/loyalty/program` |
| POST program | `https://wizamart.shop/api/store/loyalty/program` |
| POST stamp | `https://wizamart.shop/api/store/loyalty/stamp` |
| POST points | `https://wizamart.shop/api/store/loyalty/points` |
| POST enroll | `https://wizamart.shop/api/store/loyalty/cards/enroll` |
| POST lookup | `https://wizamart.shop/api/store/loyalty/cards/lookup` |
### Case 2: Store without custom domain (uses platform subdomain)
The store has no entry in `store_domains`. **All** store URLs are served via a
subdomain of the platform domain: `{store_code}.loyalty.lu`.
**Storefront (customer-facing):**
| Page | Production URL |
|------|----------------|
| Loyalty Dashboard | `https://bookstore.loyalty.lu/account/loyalty` |
| Transaction History | `https://bookstore.loyalty.lu/account/loyalty/history` |
| Self-Enrollment | `https://bookstore.loyalty.lu/loyalty/join` |
| Enrollment Success | `https://bookstore.loyalty.lu/loyalty/join/success` |
**Storefront API:**
| Method | Production URL |
|--------|----------------|
| GET card | `https://bookstore.loyalty.lu/api/storefront/loyalty/card` |
| GET transactions | `https://bookstore.loyalty.lu/api/storefront/loyalty/transactions` |
| POST enroll | `https://bookstore.loyalty.lu/api/storefront/loyalty/enroll` |
| GET program | `https://bookstore.loyalty.lu/api/storefront/loyalty/program` |
**Store backend (staff/owner):**
| Page | Production URL |
|------|----------------|
| Store Login | `https://bookstore.loyalty.lu/store/BOOKSTORE/login` |
| Terminal | `https://bookstore.loyalty.lu/store/BOOKSTORE/loyalty/terminal` |
| Cards | `https://bookstore.loyalty.lu/store/BOOKSTORE/loyalty/cards` |
| Settings | `https://bookstore.loyalty.lu/store/BOOKSTORE/loyalty/settings` |
| Stats | `https://bookstore.loyalty.lu/store/BOOKSTORE/loyalty/stats` |
**Store API:**
| Method | Production URL |
|--------|----------------|
| GET program | `https://bookstore.loyalty.lu/api/store/loyalty/program` |
| POST stamp | `https://bookstore.loyalty.lu/api/store/loyalty/stamp` |
| POST points | `https://bookstore.loyalty.lu/api/store/loyalty/points` |
| POST enroll | `https://bookstore.loyalty.lu/api/store/loyalty/cards/enroll` |
| POST lookup | `https://bookstore.loyalty.lu/api/store/loyalty/cards/lookup` |
### Platform Admin & Public API (always on platform domain)
| Page / Endpoint | Production URL |
|-----------------|----------------|
| Admin Programs | `https://loyalty.lu/admin/loyalty/programs` |
| Admin Analytics | `https://loyalty.lu/admin/loyalty/analytics` |
| Admin Merchant Detail | `https://loyalty.lu/admin/loyalty/merchants/{id}` |
| Admin Merchant Settings | `https://loyalty.lu/admin/loyalty/merchants/{id}/settings` |
| Admin API - Programs | `GET https://loyalty.lu/api/admin/loyalty/programs` |
| Admin API - Stats | `GET https://loyalty.lu/api/admin/loyalty/stats` |
| Public API - Program | `GET https://loyalty.lu/api/loyalty/programs/WIZAMART` |
| Apple Wallet Pass | `GET https://loyalty.lu/api/loyalty/passes/apple/{serial}.pkpass` |
### Domain configuration per store (current DB state)
| Store | Custom Domain | Production URL |
|-------|---------------|----------------|
| WIZAMART | `wizamart.shop` | `https://wizamart.shop/...` |
| FASHIONHUB | `fashionhub.store` | `https://fashionhub.store/...` |
| WIZAGADGETS | _(none)_ | `https://wizagadgets.loyalty.lu/...` |
| WIZAHOME | _(none)_ | `https://wizahome.loyalty.lu/...` |
| FASHIONOUTLET | _(none)_ | `https://fashionoutlet.loyalty.lu/...` |
| BOOKSTORE | _(none)_ | `https://bookstore.loyalty.lu/...` |
| BOOKDIGITAL | _(none)_ | `https://bookdigital.loyalty.lu/...` |
!!! info "`{store_domain}` in journey URLs"
In the journeys below, `{store_domain}` refers to the store's resolved domain:
- **Custom domain**: `wizamart.shop` (from `store_domains` table)
- **Subdomain fallback**: `wizamart.loyalty.lu` (from `Store.subdomain` + platform domain)
---
## User Journeys
### 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/WIZAMART/login`
- Prod (custom domain): `https://wizamart.shop/store/WIZAMART/login`
- Prod (subdomain): `https://wizamart.loyalty.lu/store/WIZAMART/login`
2. Navigate to loyalty settings:
- Dev: `http://localhost:9999/platforms/loyalty/store/WIZAMART/loyalty/settings`
- Prod (custom domain): `https://wizamart.shop/store/WIZAMART/loyalty/settings`
- Prod (subdomain): `https://wizamart.loyalty.lu/store/WIZAMART/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.loyalty.lu/store/WIZAGADGETS/loyalty/settings`
**Expected blockers in current state:**
- No subscriptions exist - feature gating may prevent program creation
- No loyalty programs exist - this is the first journey to test
---
### 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/WIZAMART/loyalty/terminal`
- Prod: `https://{store_domain}/store/WIZAMART/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/WIZAMART/loyalty/cards/{card_id}`
- Prod: `https://{store_domain}/store/WIZAMART/loyalty/cards/{card_id}`
7. Browse all cards:
- Dev: `http://localhost:9999/platforms/loyalty/store/WIZAMART/loyalty/cards`
- Prod: `https://{store_domain}/store/WIZAMART/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/WIZAMART/loyalty/terminal`
- Prod: `https://{store_domain}/store/WIZAMART/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/WIZAMART/loyalty/stats`
- Prod: `https://{store_domain}/store/WIZAMART/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://wizamart.shop/loyalty/join`
- Prod (subdomain): `https://bookstore.loyalty.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://wizamart.shop/api/storefront/loyalty/enroll`
- Prod (subdomain): `POST https://bookstore.loyalty.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://wizamart.shop/loyalty/join/success?card=XXXX-XXXX-XXXX`
- Prod (subdomain): `https://bookstore.loyalty.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://loyalty.lu/api/loyalty/passes/apple/{serial_number}.pkpass`
---
### Journey 5: Customer - View Loyalty Status
**Persona:** Authenticated Customer (e.g., `customer1@wizamart.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://wizamart.shop/account/loyalty`
- Prod (subdomain): `https://bookstore.loyalty.lu/account/loyalty`
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/storefront/loyalty/card`
- API Prod: `GET https://wizamart.shop/api/storefront/loyalty/card`
3. View full transaction history:
- Dev: `http://localhost:9999/platforms/loyalty/account/loyalty/history`
- Prod (custom domain): `https://wizamart.shop/account/loyalty/history`
- Prod (subdomain): `https://bookstore.loyalty.lu/account/loyalty/history`
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/storefront/loyalty/transactions`
- API Prod: `GET https://wizamart.shop/api/storefront/loyalty/transactions`
---
### Journey 6: Platform Admin - Oversight
**Persona:** Platform Admin (`admin@wizamart.com` 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://loyalty.lu/admin/loyalty/programs`
3. View platform-wide analytics:
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/analytics`
- Prod: `https://loyalty.lu/admin/loyalty/analytics`
4. Drill into WizaCorp's program:
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1`
- Prod: `https://loyalty.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://loyalty.lu/admin/loyalty/merchants/1/settings`
- API Dev: `PATCH http://localhost:9999/platforms/loyalty/api/admin/loyalty/merchants/1/settings`
- API Prod: `PATCH https://loyalty.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://loyalty.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/WIZAMART/loyalty/terminal`
- Prod: `https://{store_domain}/store/WIZAMART/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/WIZAMART/loyalty/cards/{card_id}`
- Prod: `https://{store_domain}/store/WIZAMART/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://loyalty.lu/admin/loyalty/merchants/1/settings`
**Steps:**
1. Staff at WIZAMART adds stamps to customer's card:
- Dev: `http://localhost:9999/platforms/loyalty/store/WIZAMART/loyalty/terminal`
- Prod: `https://{store_domain}/store/WIZAMART/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 4** - Enroll a test customer
3. **Journey 2 or 3** - Process stamps/points
4. **Journey 5** - Verify customer can see their data
5. **Journey 7** - Test void/return
6. **Journey 8** - Test cross-store (enroll via WIZAMART, redeem via WIZAGADGETS)
7. **Journey 6** - Admin overview (verify data appears correctly)

View File

@@ -0,0 +1,239 @@
# Session Note: IMPORT-002 Cross-Module Dependency Cleanup
**Date:** 2026-02-09
**Status:** Part A complete, Part B deferred
**Priority:** Medium — architecture cleanup (no runtime crashes, but undeclared dependencies)
**Follows:** SESSION_NOTE_2026-02-03_module-dependency-redesign.md
---
## Context
After fixing all IMPORT-001 violations (core → optional) in previous sessions, the architecture validator now passes with **0 errors and 40 IMPORT-002 warnings**. These are all optional → optional cross-module imports without declared dependencies.
Billing module is fully clean — zero violations.
---
## Current `requires=[]` Declarations
| Module | Currently declares |
|--------|--------------------|
| catalog | `requires=["inventory"]` |
| marketplace | `requires=["inventory"]` |
| orders | `requires=["payments"]` |
| inventory | *(nothing)* |
| cart | `requires=["inventory"]` |
| analytics | *(nothing)* |
---
## Part A: Declare Dependencies (30 warnings — trivial fixes)
These are **hard dependencies** where the source module fundamentally cannot function without the target. Fix is adding to `requires=[]` in each module's `definition.py`.
### A1. marketplace → add `catalog`, `orders`
**Change:** `requires=["inventory"]``requires=["inventory", "catalog", "orders"]`
**File:** `app/modules/marketplace/definition.py`
**Warnings resolved:** 9
| File | Imports from | What |
|------|-------------|------|
| `services/marketplace_product_service.py:862-863` | catalog | `Product` model |
| `services/letzshop_export_service.py:16` | catalog | `Product` model |
| `services/letzshop/order_service.py:26-27` | catalog, orders | `Order`, `OrderItem`, `Product` models |
| `services/letzshop/order_service.py:17` | orders | `order_service` |
| `services/marketplace_product_service.py:1006` | orders | order-related import |
| `routes/api/admin_letzshop.py:23-24` | orders | `OrderHasUnresolvedExceptionsException`, `order_item_exception_service` |
| `routes/api/store_letzshop.py:24-25` | orders | `OrderHasUnresolvedExceptionsException`, `order_item_exception_service` |
**Rationale:** Marketplace syncs products and imports orders from Letzshop. No products or orders = no marketplace.
---
### A2. orders → add `catalog`, `inventory`
**Change:** `requires=["payments"]``requires=["payments", "catalog", "inventory"]`
**File:** `app/modules/orders/definition.py`
**Warnings resolved:** 9
| File | Imports from | What |
|------|-------------|------|
| `services/order_item_exception_service.py:22,26` | catalog | `ProductNotFoundException`, `Product` |
| `services/order_service.py:51` | catalog | `Product` model |
| `services/order_inventory_service.py:17-28` | inventory | `Inventory`, `InventoryTransaction`, `TransactionType`, exceptions, schemas, service (6 imports) |
| `services/order_service.py:29` | inventory | `InsufficientInventoryException` |
**Rationale:** Order line items reference products. Order fulfillment manages stock via `order_inventory_service.py`. Both are fundamental.
---
### A3. inventory → add `catalog`
**Change:** `requires=[]``requires=["catalog"]`
**File:** `app/modules/inventory/definition.py`
**Warnings resolved:** 7
| File | Imports from | What |
|------|-------------|------|
| `services/inventory_service.py:15,33` | catalog | `ProductNotFoundException`, `Product` |
| `services/inventory_transaction_service.py:14,19` | catalog | `ProductNotFoundException`, `Product` |
| `services/inventory_import_service.py:27` | catalog | `Product` |
**Rationale:** Every inventory record tracks stock for a product. No products = no inventory.
---
### A4. cart → add `catalog`
**Change:** `requires=["inventory"]``requires=["inventory", "catalog"]`
**File:** `app/modules/cart/definition.py`
**Warnings resolved:** 2
| File | Imports from | What |
|------|-------------|------|
| `services/cart_service.py:24,27` | catalog | `ProductNotFoundException`, `Product` |
**Rationale:** Cart items are products. Can't add to cart without the Product model.
---
### Part A Subtotal: 4 one-line edits → 27 warnings resolved
*(Note: 3 remaining "declare dependency" warnings are covered by marketplace→analytics which is categorized under Part B as provider pattern instead.)*
---
## Part B: Provider Pattern (10 warnings — moderate refactoring)
These are **optional enrichment** where the source module works without the target. Need actual refactoring to conditionally load/call.
### B1. catalog → marketplace (3 warnings)
| File | What |
|------|------|
| `schemas/product.py:14` | `MarketplaceProductResponse` |
| `schemas/catalog.py:14` | `MarketplaceProductResponse` |
| `services/product_service.py:21` | `MarketplaceProduct` model |
**Why optional:** Products exist independently. Marketplace sync status is display enrichment.
**Fix approach:** Make marketplace fields `Optional` in schemas, populate via provider if marketplace is enabled.
---
### B2. marketplace → analytics (2 warnings)
| File | What |
|------|------|
| `routes/api/admin_marketplace.py:17` | `stats_service` |
| `routes/api/admin_marketplace.py:29` | `ImportStatsResponse` |
**Why optional:** Marketplace import/sync works without analytics. Stats on admin page are dashboard decoration.
**Fix approach:** Conditionally call stats aggregator, return empty stats if analytics disabled.
---
### B3. orders → marketplace (1 warning)
| File | What |
|------|------|
| `services/order_service.py:50` | `MarketplaceProduct`, `MarketplaceProductTranslation` |
**Why optional:** Orders work without marketplace. Enriches order display with Letzshop product info.
**Fix approach:** Conditionally join marketplace data when rendering, skip if module disabled.
---
### B4. inventory → orders (2 warnings)
| File | What |
|------|------|
| `services/inventory_transaction_service.py:15,18` | `OrderNotFoundException`, `Order` |
**Why optional:** Inventory tracks stock independently. Order reference on transactions is an audit back-reference, not functional.
**Fix approach:** Store `order_id` as nullable FK, resolve order details via provider for display.
---
### B5. inventory → marketplace (1 warning)
| File | What |
|------|------|
| `services/inventory_service.py:606` | marketplace import |
**Why optional:** Optional sync enrichment.
**Fix approach:** Conditional import or provider call.
---
### B6. analytics → catalog, orders, inventory, marketplace (4 warnings)
| File | What |
|------|------|
| `services/stats_service.py:23` | `Inventory` model |
| `services/stats_service.py:24` | `MarketplaceImportJob`, `MarketplaceProduct` models |
| `services/stats_service.py:25` | `Order` model |
| `services/stats_service.py:26` | `Product` model |
**Why optional:** Analytics aggregates everything — should report on whatever modules are enabled, not crash if one is disabled. Every module already exposes a `metrics_provider`.
**Fix approach:** Refactor `stats_service` to use module `metrics_provider` pattern instead of direct model imports. Cleanest candidate for provider pattern.
---
### Part B Subtotal: 6 refactors → 13 warnings resolved
---
## Resulting Dependency Tree
After all fixes, the clean module dependency graph:
```
catalog (foundational — products)
├── inventory (requires: catalog)
├── cart (requires: catalog, inventory)
├── orders (requires: catalog, inventory, payments)
├── marketplace (requires: catalog, orders, inventory)
└── analytics (no hard deps — all via providers)
```
---
## Execution Log
### Part A — Completed 2026-02-09
- [x] **A1** — marketplace: declare `catalog`, `orders`
- [x] **A2** — orders: declare `catalog`, `inventory`
- [x] **A3** — inventory: declare `catalog`
- [x] **A4** — cart: declare `catalog`
**Result:** 40 warnings → 13 warnings (27 resolved)
### Part B — Deferred (provider pattern refactors)
Remaining 13 warnings require provider pattern refactoring. To be tackled in a future session.
- [ ] **B6** — analytics: provider pattern (cleanest, biggest impact — 4 warnings)
- [ ] **B1** — catalog: provider pattern for marketplace enrichment (3 warnings)
- [ ] **B2** — marketplace: provider pattern for analytics stats (2 warnings)
- [ ] **B4** — inventory: provider pattern for order back-references (2 warnings)
- [ ] **B3** — orders: provider pattern for marketplace enrichment (1 warning)
- [ ] **B5** — inventory: provider pattern for marketplace sync (1 warning)
---
## Validation Target
Current state:
```
$ python scripts/validate_architecture.py
→ 0 errors, 13 warnings (all IMPORT-002 — provider pattern candidates)
```
After Part B complete:
```
$ python scripts/validate_architecture.py
→ 0 errors, 0 warnings
```