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:
609
docs/features/user-journeys/loyalty.md
Normal file
609
docs/features/user-journeys/loyalty.md
Normal 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)
|
||||
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user