fix: resolve all remaining legacy import issues
- Update models/database/__init__.py to import from module locations - Update models/schema/__init__.py to remove deleted modules - Update models/__init__.py to import Inventory from module - Remove duplicate AdminNotification from models/database/admin.py - Fix monitoring module to import AdminNotification from messaging - Update stats schema imports in admin/vendor API - Update notification schema imports - Add order_item_exception.py schema to orders module - Fix app/api/v1/__init__.py to use storefront instead of shop - Add cms_admin_pages import to main.py - Fix password_reset_token imports - Fix AdminNotification test imports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,6 @@
|
|||||||
API Version 1 - All endpoints
|
API Version 1 - All endpoints
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import admin, shop, vendor
|
from . import admin, storefront, vendor
|
||||||
|
|
||||||
__all__ = ["admin", "vendor", "shop"]
|
__all__ = ["admin", "vendor", "storefront"]
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from app.services.code_quality_service import (
|
|||||||
from app.tasks.code_quality_tasks import execute_code_quality_scan
|
from app.tasks.code_quality_tasks import execute_code_quality_scan
|
||||||
from models.database.architecture_scan import ArchitectureScan
|
from models.database.architecture_scan import ArchitectureScan
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.stats import CodeQualityDashboardStatsResponse
|
from app.modules.analytics.schemas import CodeQualityDashboardStatsResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from app.core.database import get_db
|
|||||||
from app.services.admin_service import admin_service
|
from app.services.admin_service import admin_service
|
||||||
from app.services.stats_service import stats_service
|
from app.services.stats_service import stats_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.stats import (
|
from app.modules.analytics.schemas import (
|
||||||
AdminDashboardResponse,
|
AdminDashboardResponse,
|
||||||
ImportStatsResponse,
|
ImportStatsResponse,
|
||||||
MarketplaceStatsResponse,
|
MarketplaceStatsResponse,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from models.schema.marketplace_import_job import (
|
|||||||
MarketplaceImportJobRequest,
|
MarketplaceImportJobRequest,
|
||||||
MarketplaceImportJobResponse,
|
MarketplaceImportJobResponse,
|
||||||
)
|
)
|
||||||
from models.schema.stats import ImportStatsResponse
|
from app.modules.analytics.schemas import ImportStatsResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/marketplace-import-jobs")
|
router = APIRouter(prefix="/marketplace-import-jobs")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from models.schema.admin import (
|
|||||||
PlatformAlertResolve,
|
PlatformAlertResolve,
|
||||||
PlatformAlertResponse,
|
PlatformAlertResponse,
|
||||||
)
|
)
|
||||||
from models.schema.notification import (
|
from app.modules.messaging.schemas import (
|
||||||
AlertStatisticsResponse,
|
AlertStatisticsResponse,
|
||||||
MessageResponse,
|
MessageResponse,
|
||||||
UnreadCountResponse,
|
UnreadCountResponse,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from app.services.admin_service import admin_service
|
|||||||
from app.services.stats_service import stats_service
|
from app.services.stats_service import stats_service
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.stats import VendorStatsResponse
|
from app.modules.analytics.schemas import VendorStatsResponse
|
||||||
from models.schema.vendor import (
|
from models.schema.vendor import (
|
||||||
LetzshopExportRequest,
|
LetzshopExportRequest,
|
||||||
LetzshopExportResponse,
|
LetzshopExportResponse,
|
||||||
|
|||||||
2
app/api/v1/vendor/analytics.py
vendored
2
app/api/v1/vendor/analytics.py
vendored
@@ -21,7 +21,7 @@ from app.core.feature_gate import RequireFeature
|
|||||||
from app.services.stats_service import stats_service
|
from app.services.stats_service import stats_service
|
||||||
from models.database.feature import FeatureCode
|
from models.database.feature import FeatureCode
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.stats import (
|
from app.modules.analytics.schemas import (
|
||||||
VendorAnalyticsCatalog,
|
VendorAnalyticsCatalog,
|
||||||
VendorAnalyticsImports,
|
VendorAnalyticsImports,
|
||||||
VendorAnalyticsInventory,
|
VendorAnalyticsInventory,
|
||||||
|
|||||||
2
app/api/v1/vendor/dashboard.py
vendored
2
app/api/v1/vendor/dashboard.py
vendored
@@ -17,7 +17,7 @@ from app.exceptions import VendorNotActiveException
|
|||||||
from app.services.stats_service import stats_service
|
from app.services.stats_service import stats_service
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.stats import (
|
from app.modules.analytics.schemas import (
|
||||||
VendorCustomerStats,
|
VendorCustomerStats,
|
||||||
VendorDashboardStatsResponse,
|
VendorDashboardStatsResponse,
|
||||||
VendorInfo,
|
VendorInfo,
|
||||||
|
|||||||
2
app/api/v1/vendor/notifications.py
vendored
2
app/api/v1/vendor/notifications.py
vendored
@@ -15,7 +15,7 @@ from app.api.deps import get_current_vendor_api
|
|||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.notification import (
|
from app.modules.messaging.schemas import (
|
||||||
MessageResponse,
|
MessageResponse,
|
||||||
NotificationListResponse,
|
NotificationListResponse,
|
||||||
NotificationSettingsResponse,
|
NotificationSettingsResponse,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from app.modules.customers.services import (
|
|||||||
)
|
)
|
||||||
from app.services.auth_service import AuthService
|
from app.services.auth_service import AuthService
|
||||||
from app.services.email_service import EmailService
|
from app.services.email_service import EmailService
|
||||||
from models.database.password_reset_token import PasswordResetToken
|
from app.modules.customers.models import PasswordResetToken
|
||||||
from models.schema.auth import (
|
from models.schema.auth import (
|
||||||
LogoutResponse,
|
LogoutResponse,
|
||||||
PasswordResetRequestResponse,
|
PasswordResetRequestResponse,
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ Re-exports monitoring-related models from their source locations.
|
|||||||
from app.modules.billing.models import CapacitySnapshot
|
from app.modules.billing.models import CapacitySnapshot
|
||||||
|
|
||||||
# Admin notification and logging models
|
# Admin notification and logging models
|
||||||
from models.database.admin import (
|
from app.modules.messaging.models import AdminNotification
|
||||||
AdminNotification,
|
from models.database.admin import PlatformAlert
|
||||||
PlatformAlert,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CapacitySnapshot",
|
"CapacitySnapshot",
|
||||||
|
|||||||
@@ -43,6 +43,18 @@ from app.modules.orders.schemas.order import (
|
|||||||
ShippingLabelInfo,
|
ShippingLabelInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from app.modules.orders.schemas.order_item_exception import (
|
||||||
|
OrderItemExceptionResponse,
|
||||||
|
OrderItemExceptionBriefResponse,
|
||||||
|
OrderItemExceptionListResponse,
|
||||||
|
OrderItemExceptionStats,
|
||||||
|
ResolveExceptionRequest,
|
||||||
|
IgnoreExceptionRequest,
|
||||||
|
BulkResolveRequest,
|
||||||
|
BulkResolveResponse,
|
||||||
|
AutoMatchResult,
|
||||||
|
)
|
||||||
|
|
||||||
from app.modules.orders.schemas.invoice import (
|
from app.modules.orders.schemas.invoice import (
|
||||||
# Invoice settings schemas
|
# Invoice settings schemas
|
||||||
VendorInvoiceSettingsCreate,
|
VendorInvoiceSettingsCreate,
|
||||||
@@ -79,6 +91,16 @@ __all__ = [
|
|||||||
"OrderItemCreate",
|
"OrderItemCreate",
|
||||||
"OrderItemExceptionBrief",
|
"OrderItemExceptionBrief",
|
||||||
"OrderItemResponse",
|
"OrderItemResponse",
|
||||||
|
# Order item exception schemas
|
||||||
|
"OrderItemExceptionResponse",
|
||||||
|
"OrderItemExceptionBriefResponse",
|
||||||
|
"OrderItemExceptionListResponse",
|
||||||
|
"OrderItemExceptionStats",
|
||||||
|
"ResolveExceptionRequest",
|
||||||
|
"IgnoreExceptionRequest",
|
||||||
|
"BulkResolveRequest",
|
||||||
|
"BulkResolveResponse",
|
||||||
|
"AutoMatchResult",
|
||||||
# Customer schemas
|
# Customer schemas
|
||||||
"CustomerSnapshot",
|
"CustomerSnapshot",
|
||||||
"CustomerSnapshotResponse",
|
"CustomerSnapshotResponse",
|
||||||
|
|||||||
173
app/modules/orders/schemas/order_item_exception.py
Normal file
173
app/modules/orders/schemas/order_item_exception.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# app/modules/orders/schemas/order_item_exception.py
|
||||||
|
"""
|
||||||
|
Pydantic schemas for order item exception management.
|
||||||
|
|
||||||
|
Handles unmatched products during marketplace order imports.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Exception Response Schemas
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class OrderItemExceptionResponse(BaseModel):
|
||||||
|
"""Schema for order item exception response."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
order_item_id: int
|
||||||
|
vendor_id: int
|
||||||
|
vendor_name: str | None = None # For cross-vendor views
|
||||||
|
|
||||||
|
# Original data from marketplace
|
||||||
|
original_gtin: str | None
|
||||||
|
original_product_name: str | None
|
||||||
|
original_sku: str | None
|
||||||
|
|
||||||
|
# Exception classification
|
||||||
|
exception_type: str # product_not_found, gtin_mismatch, duplicate_gtin
|
||||||
|
|
||||||
|
# Resolution status
|
||||||
|
status: str # pending, resolved, ignored
|
||||||
|
|
||||||
|
# Resolution details
|
||||||
|
resolved_product_id: int | None
|
||||||
|
resolved_at: datetime | None
|
||||||
|
resolved_by: int | None
|
||||||
|
resolution_notes: str | None
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
# Nested order info (populated by service)
|
||||||
|
order_number: str | None = None
|
||||||
|
order_id: int | None = None
|
||||||
|
order_date: datetime | None = None
|
||||||
|
order_status: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_pending(self) -> bool:
|
||||||
|
"""Check if exception is pending resolution."""
|
||||||
|
return self.status == "pending"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_resolved(self) -> bool:
|
||||||
|
"""Check if exception has been resolved."""
|
||||||
|
return self.status == "resolved"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ignored(self) -> bool:
|
||||||
|
"""Check if exception has been ignored."""
|
||||||
|
return self.status == "ignored"
|
||||||
|
|
||||||
|
|
||||||
|
class OrderItemExceptionBriefResponse(BaseModel):
|
||||||
|
"""Brief exception info for embedding in order item responses."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
original_gtin: str | None
|
||||||
|
original_product_name: str | None
|
||||||
|
exception_type: str
|
||||||
|
status: str
|
||||||
|
resolved_product_id: int | None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# List/Stats Response Schemas
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class OrderItemExceptionListResponse(BaseModel):
|
||||||
|
"""Paginated list of exceptions."""
|
||||||
|
|
||||||
|
exceptions: list[OrderItemExceptionResponse]
|
||||||
|
total: int
|
||||||
|
skip: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
|
||||||
|
class OrderItemExceptionStats(BaseModel):
|
||||||
|
"""Exception statistics for a vendor."""
|
||||||
|
|
||||||
|
pending: int = 0
|
||||||
|
resolved: int = 0
|
||||||
|
ignored: int = 0
|
||||||
|
total: int = 0
|
||||||
|
|
||||||
|
# Additional breakdown
|
||||||
|
orders_with_exceptions: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Request Schemas
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class ResolveExceptionRequest(BaseModel):
|
||||||
|
"""Request to resolve an exception by assigning a product."""
|
||||||
|
|
||||||
|
product_id: int = Field(..., description="Product ID to assign to this order item")
|
||||||
|
notes: str | None = Field(
|
||||||
|
None,
|
||||||
|
max_length=1000,
|
||||||
|
description="Optional notes about the resolution"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IgnoreExceptionRequest(BaseModel):
|
||||||
|
"""Request to ignore an exception (still blocks confirmation)."""
|
||||||
|
|
||||||
|
notes: str = Field(
|
||||||
|
...,
|
||||||
|
min_length=1,
|
||||||
|
max_length=1000,
|
||||||
|
description="Reason for ignoring (required)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BulkResolveRequest(BaseModel):
|
||||||
|
"""Request to bulk resolve all pending exceptions for a GTIN."""
|
||||||
|
|
||||||
|
gtin: str = Field(
|
||||||
|
...,
|
||||||
|
min_length=1,
|
||||||
|
max_length=50,
|
||||||
|
description="GTIN to match pending exceptions"
|
||||||
|
)
|
||||||
|
product_id: int = Field(..., description="Product ID to assign")
|
||||||
|
notes: str | None = Field(
|
||||||
|
None,
|
||||||
|
max_length=1000,
|
||||||
|
description="Optional notes about the resolution"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BulkResolveResponse(BaseModel):
|
||||||
|
"""Response from bulk resolve operation."""
|
||||||
|
|
||||||
|
resolved_count: int
|
||||||
|
gtin: str
|
||||||
|
product_id: int
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Auto-Match Response Schemas
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class AutoMatchResult(BaseModel):
|
||||||
|
"""Result of auto-matching after product import."""
|
||||||
|
|
||||||
|
gtin: str
|
||||||
|
product_id: int
|
||||||
|
resolved_count: int
|
||||||
|
resolved_exception_ids: list[int]
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# Session Note: Module Config & Migrations Infrastructure
|
||||||
|
|
||||||
|
**Date:** 2026-01-28
|
||||||
|
**Focus:** Self-contained module configuration and migrations auto-discovery
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Completed the infrastructure for fully self-contained modules with config and migrations auto-discovery. All modules now have placeholder files ready for the final migration phase.
|
||||||
|
|
||||||
|
## What Was Done
|
||||||
|
|
||||||
|
### 1. Module Config Auto-Discovery
|
||||||
|
|
||||||
|
Created `app/modules/config.py` that auto-discovers module configurations:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.modules.config import get_module_config
|
||||||
|
|
||||||
|
marketplace_config = get_module_config("marketplace")
|
||||||
|
```
|
||||||
|
|
||||||
|
Each module now has a `config.py` placeholder:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# app/modules/marketplace/config.py
|
||||||
|
class MarketplaceConfig(BaseSettings):
|
||||||
|
model_config = {"env_prefix": "MARKETPLACE_"}
|
||||||
|
|
||||||
|
config = MarketplaceConfig()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Module Migrations Directories
|
||||||
|
|
||||||
|
Created `migrations/versions/` directories for all 11 self-contained modules:
|
||||||
|
- analytics, billing, cms, customers, dev_tools
|
||||||
|
- inventory, marketplace, messaging, monitoring, orders, payments
|
||||||
|
|
||||||
|
Each with required `__init__.py` files for Alembic discovery.
|
||||||
|
|
||||||
|
### 3. New Architecture Rules
|
||||||
|
|
||||||
|
Added rules MOD-013 to MOD-015:
|
||||||
|
|
||||||
|
| Rule | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| MOD-013 | config.py should export `config` or `config_class` |
|
||||||
|
| MOD-014 | Migrations must follow naming convention `{module}_{seq}_{desc}.py` |
|
||||||
|
| MOD-015 | Migrations directory must have `__init__.py` files |
|
||||||
|
|
||||||
|
### 4. Module Migration Commits
|
||||||
|
|
||||||
|
Committed all pending module migration work:
|
||||||
|
|
||||||
|
| Commit | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `2466dfd` | feat: add module config and migrations auto-discovery infrastructure |
|
||||||
|
| `bd2c99a` | feat: complete analytics module self-containment |
|
||||||
|
| `f79e67d` | feat: complete billing module self-containment |
|
||||||
|
| `b74d134` | feat: complete marketplace module self-containment |
|
||||||
|
| `705d336` | feat: add self-contained structure to remaining modules |
|
||||||
|
| `d987274` | feat: complete dev_tools module self-containment |
|
||||||
|
| `37cf74c` | refactor: update registry and main.py for module auto-discovery |
|
||||||
|
| `3ffa337` | refactor: convert legacy models/schemas to re-exports |
|
||||||
|
| `bf871dc` | refactor: convert legacy services/tasks to re-exports |
|
||||||
|
| `fbcf079` | chore: update API routers, validation, and docs |
|
||||||
|
|
||||||
|
### 5. Documentation Updates
|
||||||
|
|
||||||
|
- `docs/architecture/module-system.md` - Added Module Configuration and Module Migrations sections
|
||||||
|
- `docs/development/creating-modules.md` - Added config.py pattern, updated migrations docs
|
||||||
|
|
||||||
|
## Current Module Status
|
||||||
|
|
||||||
|
| Module | definition.py | config.py | migrations/ | routes/api/ | routes/pages/ | locales/ | Status |
|
||||||
|
|--------|--------------|-----------|-------------|-------------|---------------|----------|--------|
|
||||||
|
| analytics | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | **Complete** |
|
||||||
|
| billing | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | Needs pages |
|
||||||
|
| cms | ✅ | ✅ | ✅ | ✅ | ✅ | - | **Complete** |
|
||||||
|
| customers | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | - | Needs routes |
|
||||||
|
| dev_tools | ✅ | ✅ | ✅ | ✅ | - | - | **Complete** |
|
||||||
|
| inventory | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | - | Needs routes |
|
||||||
|
| marketplace | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | Needs pages |
|
||||||
|
| messaging | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | - | Needs routes |
|
||||||
|
| monitoring | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | - | Needs routes |
|
||||||
|
| orders | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | - | Needs routes |
|
||||||
|
| payments | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | - | Needs routes |
|
||||||
|
| core | ✅ | - | - | - | - | - | Minimal |
|
||||||
|
| tenancy | ✅ | - | - | - | - | - | Minimal |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
1. **Config is environment-based**: Each module uses Pydantic Settings with `{MODULE}_` prefix
|
||||||
|
2. **Migrations stay central for now**: Existing migrations remain in `alembic/versions/`, module directories are for future changes
|
||||||
|
3. **Migration reorganization deferred**: Will move existing migrations to modules before production
|
||||||
|
4. **Legacy files become re-exports**: Original files in `models/database/`, `models/schema/`, `app/services/` re-export from modules
|
||||||
|
|
||||||
|
## Tomorrow's Tasks
|
||||||
|
|
||||||
|
### Phase 1: Module Alignment Audit
|
||||||
|
|
||||||
|
Run architecture validator and fix all modules to comply with rules:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/validate_architecture.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Check each module for:
|
||||||
|
- [ ] MOD-001: Required directories exist
|
||||||
|
- [ ] MOD-002: Services contain actual code (not re-exports)
|
||||||
|
- [ ] MOD-003: Schemas contain actual code (not re-exports)
|
||||||
|
- [ ] MOD-004: Routes import from module, not legacy
|
||||||
|
- [ ] MOD-005: Templates and static exist for UI modules
|
||||||
|
- [ ] MOD-008: exceptions.py exists
|
||||||
|
- [ ] MOD-010: Routes export `router` variable
|
||||||
|
- [ ] MOD-011: Tasks have `__init__.py`
|
||||||
|
|
||||||
|
### Phase 2: Complete Module Routes Migration
|
||||||
|
|
||||||
|
Move remaining routes to self-contained structure:
|
||||||
|
|
||||||
|
1. **customers** - Move routes from `app/api/v1/*/customers.py`
|
||||||
|
2. **inventory** - Move routes from `app/api/v1/*/inventory.py`
|
||||||
|
3. **messaging** - Move routes from `app/api/v1/*/messages.py`
|
||||||
|
4. **monitoring** - Move routes from `app/api/v1/admin/monitoring.py`
|
||||||
|
5. **orders** - Move routes from `app/api/v1/*/orders.py`
|
||||||
|
6. **payments** - Move routes from `app/api/v1/*/payments.py`
|
||||||
|
|
||||||
|
### Phase 3: Migration Reorganization (Pre-Production)
|
||||||
|
|
||||||
|
Move existing migrations to module-specific directories:
|
||||||
|
|
||||||
|
1. Identify which tables belong to which module
|
||||||
|
2. Create baseline migrations in module directories
|
||||||
|
3. Test migration chain works correctly
|
||||||
|
4. Remove/reorganize central migrations
|
||||||
|
|
||||||
|
### Phase 4: Validation & Testing
|
||||||
|
|
||||||
|
1. Run full architecture validation
|
||||||
|
2. Test all routes are accessible
|
||||||
|
3. Test module enable/disable functionality
|
||||||
|
4. Verify Alembic discovers all migrations
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `app/modules/config.py` - Config auto-discovery
|
||||||
|
- `app/modules/*/config.py` - Module config placeholders (11 files)
|
||||||
|
- `app/modules/*/migrations/__init__.py` - Migration package markers (11 modules)
|
||||||
|
- `app/modules/*/migrations/versions/__init__.py` - Version package markers (11 modules)
|
||||||
|
- `.architecture-rules/module.yaml` - Module validation rules
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `docs/architecture/module-system.md` - Added config/migrations sections
|
||||||
|
- `docs/development/creating-modules.md` - Added config pattern, updated migrations
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Module System Architecture](../architecture/module-system.md)
|
||||||
|
- [Creating Modules Guide](../development/creating-modules.md)
|
||||||
|
- [Module Migration Plan](module-migration-plan.md)
|
||||||
229
docs/proposals/temp-loyalty
Normal file
229
docs/proposals/temp-loyalty
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
│ Loyalty Platform & Module Implementation Plan │
|
||||||
|
│ │
|
||||||
|
│ Overview │
|
||||||
|
│ │
|
||||||
|
│ Create a Loyalty Module for Wizamart that provides stamp-based and points-based loyalty programs with Google Wallet and Apple Wallet integration. │
|
||||||
|
│ │
|
||||||
|
│ Entity Mapping │
|
||||||
|
│ ┌────────────────────┬────────────────────┬──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Loyalty Concept │ Wizamart Entity │ Notes │ │
|
||||||
|
│ ├────────────────────┼────────────────────┼──────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Merchant │ Company │ Existing - legal business entity │ │
|
||||||
|
│ ├────────────────────┼────────────────────┼──────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Store │ Vendor │ Existing - brand/location │ │
|
||||||
|
│ ├────────────────────┼────────────────────┼──────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Customer │ Customer │ Existing - has vendor_id, total_spent, marketing_consent │ │
|
||||||
|
│ ├────────────────────┼────────────────────┼──────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Pass Object │ LoyaltyCard │ NEW - links customer to vendor's program │ │
|
||||||
|
│ ├────────────────────┼────────────────────┼──────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Stamp/Points Event │ LoyaltyTransaction │ NEW - records all operations │ │
|
||||||
|
│ ├────────────────────┼────────────────────┼──────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Staff PIN │ StaffPin │ NEW - fraud prevention │ │
|
||||||
|
│ └────────────────────┴────────────────────┴──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ Database Models │
|
||||||
|
│ │
|
||||||
|
│ Core Models (5 tables) │
|
||||||
|
│ │
|
||||||
|
│ 1. loyalty_programs - Vendor's program configuration │
|
||||||
|
│ - vendor_id (unique FK) │
|
||||||
|
│ - loyalty_type: stamps | points | hybrid │
|
||||||
|
│ - Stamps config: stamps_target, stamps_reward_description │
|
||||||
|
│ - Points config: points_per_euro, points_rewards (JSON) │
|
||||||
|
│ - Anti-fraud: cooldown_minutes, max_daily_stamps, require_staff_pin │
|
||||||
|
│ - Branding: card_name, card_color, logo_url │
|
||||||
|
│ - Wallet IDs: google_issuer_id, apple_pass_type_id │
|
||||||
|
│ 2. loyalty_cards - Customer's card (PassObject) │
|
||||||
|
│ - customer_id, program_id, vendor_id │
|
||||||
|
│ - card_number (unique), qr_code_data │
|
||||||
|
│ - Stamps: stamp_count, total_stamps_earned, stamps_redeemed │
|
||||||
|
│ - Points: points_balance, total_points_earned, points_redeemed │
|
||||||
|
│ - Wallet: google_object_id, apple_serial_number, apple_auth_token │
|
||||||
|
│ - Timestamps: last_stamp_at, last_points_at │
|
||||||
|
│ 3. loyalty_transactions - All loyalty events │
|
||||||
|
│ - card_id, vendor_id, staff_pin_id │
|
||||||
|
│ - transaction_type: stamp_earned | stamp_redeemed | points_earned | points_redeemed │
|
||||||
|
│ - stamps_delta, points_delta, purchase_amount_cents │
|
||||||
|
│ - Metadata: ip_address, user_agent, notes │
|
||||||
|
│ 4. staff_pins - Fraud prevention │
|
||||||
|
│ - program_id, vendor_id │
|
||||||
|
│ - name, pin_hash (bcrypt) │
|
||||||
|
│ - failed_attempts, locked_until │
|
||||||
|
│ 5. apple_device_registrations - Apple Wallet push │
|
||||||
|
│ - card_id, device_library_identifier, push_token │
|
||||||
|
│ │
|
||||||
|
│ Module Structure │
|
||||||
|
│ │
|
||||||
|
│ app/modules/loyalty/ │
|
||||||
|
│ ├── __init__.py │
|
||||||
|
│ ├── definition.py # ModuleDefinition (requires: customers) │
|
||||||
|
│ ├── config.py # LOYALTY_ env vars │
|
||||||
|
│ ├── exceptions.py │
|
||||||
|
│ ├── models/ │
|
||||||
|
│ │ ├── loyalty_program.py │
|
||||||
|
│ │ ├── loyalty_card.py │
|
||||||
|
│ │ ├── loyalty_transaction.py │
|
||||||
|
│ │ ├── staff_pin.py │
|
||||||
|
│ │ └── apple_device.py │
|
||||||
|
│ ├── schemas/ │
|
||||||
|
│ │ ├── program.py │
|
||||||
|
│ │ ├── card.py │
|
||||||
|
│ │ ├── stamp.py │
|
||||||
|
│ │ ├── points.py │
|
||||||
|
│ │ └── pin.py │
|
||||||
|
│ ├── services/ │
|
||||||
|
│ │ ├── program_service.py # Program CRUD │
|
||||||
|
│ │ ├── card_service.py # Card enrollment, lookup │
|
||||||
|
│ │ ├── stamp_service.py # Stamp logic + anti-fraud │
|
||||||
|
│ │ ├── points_service.py # Points logic │
|
||||||
|
│ │ ├── pin_service.py # PIN validation │
|
||||||
|
│ │ ├── wallet_service.py # Unified wallet abstraction │
|
||||||
|
│ │ ├── google_wallet_service.py │
|
||||||
|
│ │ └── apple_wallet_service.py │
|
||||||
|
│ ├── routes/ │
|
||||||
|
│ │ ├── api/ │
|
||||||
|
│ │ │ ├── admin.py # Platform admin │
|
||||||
|
│ │ │ ├── vendor.py # Vendor dashboard │
|
||||||
|
│ │ │ └── public.py # Enrollment, Apple web service │
|
||||||
|
│ │ └── pages/ │
|
||||||
|
│ ├── tasks/ │
|
||||||
|
│ │ ├── point_expiration.py │
|
||||||
|
│ │ └── wallet_sync.py │
|
||||||
|
│ ├── migrations/versions/ │
|
||||||
|
│ ├── locales/ │
|
||||||
|
│ └── templates/ │
|
||||||
|
│ │
|
||||||
|
│ Key API Endpoints │
|
||||||
|
│ │
|
||||||
|
│ Public (Customer) │
|
||||||
|
│ │
|
||||||
|
│ - POST /api/v1/loyalty/enroll/{vendor_code} - Enroll in program │
|
||||||
|
│ - GET /api/v1/loyalty/passes/apple/{serial}.pkpass - Download Apple pass │
|
||||||
|
│ - Apple Web Service endpoints for device registration/updates │
|
||||||
|
│ │
|
||||||
|
│ Vendor (Staff) │
|
||||||
|
│ │
|
||||||
|
│ - POST /api/v1/vendor/loyalty/stamp - Add stamp (requires PIN) │
|
||||||
|
│ - POST /api/v1/vendor/loyalty/points - Add points from purchase │
|
||||||
|
│ - POST /api/v1/vendor/loyalty/*/redeem - Redeem for reward │
|
||||||
|
│ - GET /api/v1/vendor/loyalty/cards - List customer cards │
|
||||||
|
│ - GET /api/v1/vendor/loyalty/pins - Manage staff PINs │
|
||||||
|
│ - GET /api/v1/vendor/loyalty/stats - Dashboard analytics │
|
||||||
|
│ │
|
||||||
|
│ Admin │
|
||||||
|
│ │
|
||||||
|
│ - GET /api/v1/admin/loyalty/programs - List all programs │
|
||||||
|
│ - GET /api/v1/admin/loyalty/stats - Platform-wide stats │
|
||||||
|
│ │
|
||||||
|
│ Anti-Fraud System │
|
||||||
|
│ │
|
||||||
|
│ 1. Staff PIN - Required for all stamp/points operations │
|
||||||
|
│ 2. Cooldown - Configurable minutes between stamps (default: 15) │
|
||||||
|
│ 3. Daily Limit - Max stamps per card per day (default: 5) │
|
||||||
|
│ 4. PIN Lockout - Lock after 5 failed attempts for 30 minutes │
|
||||||
|
│ 5. Audit Trail - All transactions logged with IP/user agent │
|
||||||
|
│ │
|
||||||
|
│ Wallet Integration │
|
||||||
|
│ │
|
||||||
|
│ Google Wallet │
|
||||||
|
│ │
|
||||||
|
│ - Create LoyaltyClass when program created │
|
||||||
|
│ - Create LoyaltyObject when customer enrolls │
|
||||||
|
│ - PATCH object on stamp/points change │
|
||||||
|
│ - Generate JWT-based "Add to Wallet" URL │
|
||||||
|
│ │
|
||||||
|
│ Apple Wallet │
|
||||||
|
│ │
|
||||||
|
│ - Generate .pkpass file (pass.json + images + signature) │
|
||||||
|
│ - Implement Apple Web Service for device registration │
|
||||||
|
│ - Send push notification on updates → device fetches new pass │
|
||||||
|
│ │
|
||||||
|
│ Implementation Phases │
|
||||||
|
│ │
|
||||||
|
│ Phase 1: MVP (Target) │
|
||||||
|
│ │
|
||||||
|
│ 1. Core Infrastructure │
|
||||||
|
│ - Module structure, definition, exceptions │
|
||||||
|
│ - Database models and migrations │
|
||||||
|
│ - Program service (CRUD) │
|
||||||
|
│ - Card service (enrollment, lookup) │
|
||||||
|
│ 2. Stamp Loyalty │
|
||||||
|
│ - Staff PIN service with lockout │
|
||||||
|
│ - Stamp service with anti-fraud │
|
||||||
|
│ - Transaction logging │
|
||||||
|
│ - Vendor API routes │
|
||||||
|
│ 3. Points Loyalty │
|
||||||
|
│ - Points service │
|
||||||
|
│ - Purchase-to-points calculation │
|
||||||
|
│ - Redemption flow │
|
||||||
|
│ 4. Wallet Integration │
|
||||||
|
│ - Google Wallet service │
|
||||||
|
│ - Apple .pkpass generation │
|
||||||
|
│ - Apple Web Service endpoints │
|
||||||
|
│ 5. Dashboard │
|
||||||
|
│ - Vendor stats endpoint │
|
||||||
|
│ - Transaction history │
|
||||||
|
│ - QR code generation │
|
||||||
|
│ │
|
||||||
|
│ Phase 2: Future Enhancements │
|
||||||
|
│ │
|
||||||
|
│ - Rewards catalog with configurable tiers │
|
||||||
|
│ - Customer tiers (Bronze/Silver/Gold) │
|
||||||
|
│ - Referral program │
|
||||||
|
│ - Gamification (spin wheel, scratch cards) │
|
||||||
|
│ - POS integration │
|
||||||
|
│ │
|
||||||
|
│ Files to Create │
|
||||||
|
│ │
|
||||||
|
│ app/modules/loyalty/ │
|
||||||
|
│ ├── __init__.py │
|
||||||
|
│ ├── definition.py │
|
||||||
|
│ ├── config.py │
|
||||||
|
│ ├── exceptions.py │
|
||||||
|
│ ├── models/__init__.py │
|
||||||
|
│ ├── models/loyalty_program.py │
|
||||||
|
│ ├── models/loyalty_card.py │
|
||||||
|
│ ├── models/loyalty_transaction.py │
|
||||||
|
│ ├── models/staff_pin.py │
|
||||||
|
│ ├── models/apple_device.py │
|
||||||
|
│ ├── schemas/__init__.py │
|
||||||
|
│ ├── schemas/program.py │
|
||||||
|
│ ├── schemas/card.py │
|
||||||
|
│ ├── schemas/stamp.py │
|
||||||
|
│ ├── schemas/points.py │
|
||||||
|
│ ├── schemas/pin.py │
|
||||||
|
│ ├── services/__init__.py │
|
||||||
|
│ ├── services/program_service.py │
|
||||||
|
│ ├── services/card_service.py │
|
||||||
|
│ ├── services/stamp_service.py │
|
||||||
|
│ ├── services/points_service.py │
|
||||||
|
│ ├── services/pin_service.py │
|
||||||
|
│ ├── services/wallet_service.py │
|
||||||
|
│ ├── services/google_wallet_service.py │
|
||||||
|
│ ├── services/apple_wallet_service.py │
|
||||||
|
│ ├── routes/__init__.py │
|
||||||
|
│ ├── routes/api/__init__.py │
|
||||||
|
│ ├── routes/api/admin.py │
|
||||||
|
│ ├── routes/api/vendor.py │
|
||||||
|
│ ├── routes/api/public.py │
|
||||||
|
│ ├── routes/pages/__init__.py │
|
||||||
|
│ ├── tasks/__init__.py │
|
||||||
|
│ ├── tasks/point_expiration.py │
|
||||||
|
│ ├── migrations/__init__.py │
|
||||||
|
│ ├── migrations/versions/__init__.py │
|
||||||
|
│ └── locales/{en,fr,de,lu}.json │
|
||||||
|
│ │
|
||||||
|
│ Reference Patterns │
|
||||||
|
│ │
|
||||||
|
│ - Module definition: app/modules/billing/definition.py │
|
||||||
|
│ - Models: app/modules/billing/models/subscription.py │
|
||||||
|
│ - Services: app/modules/billing/services/subscription_service.py │
|
||||||
|
│ - Customer integration: models/database/customer.py │
|
||||||
|
│ │
|
||||||
|
│ Verification │
|
||||||
|
│ │
|
||||||
|
│ 1. Run architecture validator: python scripts/validate_architecture.py │
|
||||||
|
│ 2. Run migrations: alembic upgrade head │
|
||||||
|
│ 3. Test enrollment flow via API │
|
||||||
|
│ 4. Test stamp/points operations with PIN │
|
||||||
|
│ 5. Verify wallet pass generation │
|
||||||
|
│ 6. Check anti-fraud (cooldown, limits, lockout)
|
||||||
678
loyatly-ideas.html
Normal file
678
loyatly-ideas.html
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Loyalty Platform Architecture – Google Wallet & Apple Wallet</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #0b1020;
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
background: linear-gradient(135deg, #1f2937, #111827);
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #374151;
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
header p {
|
||||||
|
margin: 0;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
h2, h3, h4 {
|
||||||
|
color: #e5e7eb;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
border-bottom: 1px solid #374151;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: #111827;
|
||||||
|
padding: 0.15rem 0.35rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #111827;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #374151;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #60a5fa;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background: #111827;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
border-left: 3px solid #3b82f6;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Loyalty Platform Architecture</h1>
|
||||||
|
<p>Digital stamp & points system with Google Wallet and Apple Wallet support</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section id="overview">
|
||||||
|
<h2>1. High-Level Overview</h2>
|
||||||
|
<p>
|
||||||
|
This document describes a modular loyalty platform that supports:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Stamp-based loyalty</strong> (e.g., coffee shops, barbers)</li>
|
||||||
|
<li><strong>Points-based loyalty</strong> (e.g., clothing stores, restaurants)</li>
|
||||||
|
<li><strong>Google Wallet passes</strong> (JSON + API)</li>
|
||||||
|
<li><strong>Apple Wallet passes</strong> (<code>.pkpass</code> files)</li>
|
||||||
|
<li><strong>Staff PIN system</strong> for fraud prevention</li>
|
||||||
|
<li><strong>QR-based flows</strong> for visits and purchases</li>
|
||||||
|
<li><strong>Email collection</strong> and basic CRM</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
The architecture is designed to be extensible: you can add tiers, referrals, coupons, gift cards, and more without redesigning the core.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="architecture">
|
||||||
|
<h2>2. Backend Architecture</h2>
|
||||||
|
|
||||||
|
<h3>2.1 Core Modules</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Auth & Merchant Management:</strong> merchants, stores, staff PINs, billing hooks.</li>
|
||||||
|
<li><strong>Customer & Pass Management:</strong> customers, pass objects, Wallet IDs.</li>
|
||||||
|
<li><strong>Loyalty Engine:</strong> stamp logic and points logic.</li>
|
||||||
|
<li><strong>QR Engine:</strong> static QR codes per store, scan endpoints.</li>
|
||||||
|
<li><strong>Dashboard API:</strong> analytics, customer lists, rewards, exports.</li>
|
||||||
|
<li><strong>Wallet Integrations:</strong> Google Wallet and Apple Wallet modules.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>2.2 Folder Structure</h3>
|
||||||
|
<pre><code>backend/
|
||||||
|
app/
|
||||||
|
main.py
|
||||||
|
config.py
|
||||||
|
dependencies.py
|
||||||
|
|
||||||
|
api/
|
||||||
|
merchants.py
|
||||||
|
stores.py
|
||||||
|
passes.py
|
||||||
|
stamps.py
|
||||||
|
points.py
|
||||||
|
qr.py
|
||||||
|
dashboard.py
|
||||||
|
apple_passes.py
|
||||||
|
|
||||||
|
core/
|
||||||
|
google_wallet.py
|
||||||
|
loyalty_engine/
|
||||||
|
stamps.py
|
||||||
|
points.py
|
||||||
|
qr_generator.py
|
||||||
|
apple_wallet/
|
||||||
|
__init__.py
|
||||||
|
signer.py
|
||||||
|
generator.py
|
||||||
|
templates/
|
||||||
|
loyalty_pass.json
|
||||||
|
images/
|
||||||
|
icon.png
|
||||||
|
logo.png
|
||||||
|
strip.png
|
||||||
|
|
||||||
|
models/
|
||||||
|
merchant.py
|
||||||
|
store.py
|
||||||
|
customer.py
|
||||||
|
pass_object.py
|
||||||
|
stamp_event.py
|
||||||
|
points_event.py
|
||||||
|
apple_device_registration.py
|
||||||
|
|
||||||
|
db/
|
||||||
|
base.py
|
||||||
|
session.py
|
||||||
|
migrations/
|
||||||
|
|
||||||
|
utils/
|
||||||
|
security.py
|
||||||
|
id_generator.py
|
||||||
|
validators.py
|
||||||
|
|
||||||
|
requirements.txt</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="data-model">
|
||||||
|
<h2>3. Data Model</h2>
|
||||||
|
|
||||||
|
<h3>3.1 Merchant vs Store</h3>
|
||||||
|
<p>
|
||||||
|
<span class="tag">Merchant</span> = business entity (e.g., “CoffeeLux”)
|
||||||
|
<span class="tag">Store</span> = specific location/branch (e.g., “CoffeeLux Gare”).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Merchant</h4>
|
||||||
|
<table>
|
||||||
|
<tr><th>Field</th><th>Type</th></tr>
|
||||||
|
<tr><td>id</td><td>UUID</td></tr>
|
||||||
|
<tr><td>name</td><td>string</td></tr>
|
||||||
|
<tr><td>email</td><td>string</td></tr>
|
||||||
|
<tr><td>password_hash</td><td>string</td></tr>
|
||||||
|
<tr><td>created_at</td><td>datetime</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>Store</h4>
|
||||||
|
<table>
|
||||||
|
<tr><th>Field</th><th>Type</th></tr>
|
||||||
|
<tr><td>id</td><td>UUID</td></tr>
|
||||||
|
<tr><td>merchant_id</td><td>FK → Merchant</td></tr>
|
||||||
|
<tr><td>name</td><td>string</td></tr>
|
||||||
|
<tr><td>qr_code_url</td><td>string</td></tr>
|
||||||
|
<tr><td>loyalty_type</td><td><code>"stamps"</code> | <code>"points"</code> | <code>"hybrid"</code></td></tr>
|
||||||
|
<tr><td>staff_pin_hash</td><td>string (hashed)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>Customer</h4>
|
||||||
|
<table>
|
||||||
|
<tr><th>Field</th><th>Type</th></tr>
|
||||||
|
<tr><td>id</td><td>UUID</td></tr>
|
||||||
|
<tr><td>email</td><td>string (nullable)</td></tr>
|
||||||
|
<tr><td>phone</td><td>string (nullable)</td></tr>
|
||||||
|
<tr><td>email_consent</td><td>bool</td></tr>
|
||||||
|
<tr><td>email_consent_at</td><td>datetime (nullable)</td></tr>
|
||||||
|
<tr><td>created_at</td><td>datetime</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>PassObject</h4>
|
||||||
|
<table>
|
||||||
|
<tr><th>Field</th><th>Type</th></tr>
|
||||||
|
<tr><td>id</td><td>UUID</td></tr>
|
||||||
|
<tr><td>customer_id</td><td>FK → Customer</td></tr>
|
||||||
|
<tr><td>store_id</td><td>FK → Store</td></tr>
|
||||||
|
<tr><td>google_pass_id</td><td>string (nullable)</td></tr>
|
||||||
|
<tr><td>apple_serial</td><td>string (nullable)</td></tr>
|
||||||
|
<tr><td>apple_auth_token</td><td>string (nullable)</td></tr>
|
||||||
|
<tr><td>stamp_count</td><td>int</td></tr>
|
||||||
|
<tr><td>points_balance</td><td>int</td></tr>
|
||||||
|
<tr><td>reward_unlocked</td><td>bool</td></tr>
|
||||||
|
<tr><td>last_stamp_at</td><td>datetime (nullable)</td></tr>
|
||||||
|
<tr><td>last_points_at</td><td>datetime (nullable)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>StampEvent</h4>
|
||||||
|
<table>
|
||||||
|
<tr><th>Field</th><th>Type</th></tr>
|
||||||
|
<tr><td>id</td><td>UUID</td></tr>
|
||||||
|
<tr><td>pass_id</td><td>FK → PassObject</td></tr>
|
||||||
|
<tr><td>store_id</td><td>FK → Store</td></tr>
|
||||||
|
<tr><td>timestamp</td><td>datetime</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>PointsEvent</h4>
|
||||||
|
<table>
|
||||||
|
<tr><th>Field</th><th>Type</th></tr>
|
||||||
|
<tr><td>id</td><td>UUID</td></tr>
|
||||||
|
<tr><td>pass_id</td><td>FK → PassObject</td></tr>
|
||||||
|
<tr><td>store_id</td><td>FK → Store</td></tr>
|
||||||
|
<tr><td>points_added</td><td>int</td></tr>
|
||||||
|
<tr><td>amount</td><td>decimal (optional)</td></tr>
|
||||||
|
<tr><td>timestamp</td><td>datetime</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>AppleDeviceRegistration</h4>
|
||||||
|
<table>
|
||||||
|
<tr><th>Field</th><th>Type</th></tr>
|
||||||
|
<tr><td>id</td><td>UUID</td></tr>
|
||||||
|
<tr><td>device_id</td><td>string</td></tr>
|
||||||
|
<tr><td>pass_serial</td><td>string</td></tr>
|
||||||
|
<tr><td>push_token</td><td>string</td></tr>
|
||||||
|
<tr><td>updated_at</td><td>datetime</td></tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="flows">
|
||||||
|
<h2>4. Core Flows</h2>
|
||||||
|
|
||||||
|
<h3>4.1 Creating a Pass (Onboarding)</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Customer scans a QR code or visits a link.</li>
|
||||||
|
<li>Page asks for optional data:
|
||||||
|
<ul>
|
||||||
|
<li>Name (optional)</li>
|
||||||
|
<li>Email (optional, with consent checkbox)</li>
|
||||||
|
<li>Phone (optional)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Backend:
|
||||||
|
<ul>
|
||||||
|
<li>Creates or finds Customer.</li>
|
||||||
|
<li>Creates PassObject for the chosen store.</li>
|
||||||
|
<li>Generates Google Wallet link and/or Apple Wallet <code>.pkpass</code>.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Customer taps “Add to Google Wallet” or “Add to Apple Wallet”.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>4.2 Stamping a Pass (Visits)</h3>
|
||||||
|
<p><strong>Endpoint:</strong> <code>POST /stamp/{store_id}</code></p>
|
||||||
|
<ol>
|
||||||
|
<li>Customer scans store QR: <code>https://yourdomain.com/stamp/{store_id}</code>.</li>
|
||||||
|
<li>Page identifies the customer’s pass (via login, token, or Wallet ID).</li>
|
||||||
|
<li>Staff enters a PIN on the customer’s device.</li>
|
||||||
|
<li>Backend:
|
||||||
|
<ul>
|
||||||
|
<li>Validates staff PIN.</li>
|
||||||
|
<li>Checks cooldown (e.g., 1 stamp per X minutes).</li>
|
||||||
|
<li>Increments <code>stamp_count</code>.</li>
|
||||||
|
<li>Logs a StampEvent.</li>
|
||||||
|
<li>Updates Google Wallet and Apple Wallet passes.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Customer sees updated stamp count.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>4.3 Adding Points (Purchases)</h3>
|
||||||
|
<p><strong>Endpoint:</strong> <code>POST /points/{store_id}</code></p>
|
||||||
|
<pre><code>{
|
||||||
|
"pass_id": "abc123",
|
||||||
|
"amount": 59.90,
|
||||||
|
"staff_pin": "4821"
|
||||||
|
}</code></pre>
|
||||||
|
<ol>
|
||||||
|
<li>Staff enters purchase amount and PIN.</li>
|
||||||
|
<li>Backend:
|
||||||
|
<ul>
|
||||||
|
<li>Validates staff PIN.</li>
|
||||||
|
<li>Converts amount → points (e.g., €1 = 1 point).</li>
|
||||||
|
<li>Updates <code>points_balance</code>.</li>
|
||||||
|
<li>Logs a PointsEvent.</li>
|
||||||
|
<li>Updates Wallet passes.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>4.4 Reward Logic</h3>
|
||||||
|
<p>
|
||||||
|
Implemented in <code>loyalty_engine/stamps.py</code> and <code>loyalty_engine/points.py</code>.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Stamps:</strong> reward after N stamps.</li>
|
||||||
|
<li><strong>Points:</strong> reward at thresholds (e.g., 100, 250, 500 points).</li>
|
||||||
|
<li><strong>Hybrid:</strong> store can use both stamps and points.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="staff-pin">
|
||||||
|
<h2>5. Staff PIN System & Anti-Fraud</h2>
|
||||||
|
|
||||||
|
<h3>5.1 Staff PIN Concept</h3>
|
||||||
|
<p>
|
||||||
|
Each store has one or more staff PINs. A stamp or points addition is only valid if a correct PIN is provided.
|
||||||
|
</p>
|
||||||
|
<p><strong>Example request:</strong></p>
|
||||||
|
<pre><code>POST /stamp/{store_id}
|
||||||
|
{
|
||||||
|
"pass_id": "abc123",
|
||||||
|
"staff_pin": "4821"
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<h3>5.2 Implementation</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Store table:</strong> <code>staff_pin_hash</code> (hashed with bcrypt/argon2).</li>
|
||||||
|
<li>Staff enters PIN on customer’s device after a purchase/visit.</li>
|
||||||
|
<li>Backend compares provided PIN with hash.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>5.3 Additional Anti-Fraud Layers</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Cooldown:</strong> 1 stamp per X minutes per pass.</li>
|
||||||
|
<li><strong>Device fingerprinting:</strong> optional (IP, user agent, cookies).</li>
|
||||||
|
<li><strong>Reward state checks:</strong> don’t stamp a full card.</li>
|
||||||
|
<li><strong>Staff PIN rotation:</strong> merchants can change PIN periodically.</li>
|
||||||
|
<li><strong>Attempt limits:</strong> block after N wrong PIN attempts.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
The QR code itself never directly grants a stamp or points. It only opens a URL; the backend decides whether to award anything.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="email">
|
||||||
|
<h2>6. Email Collection & GDPR Considerations</h2>
|
||||||
|
|
||||||
|
<h3>6.1 When to Collect Email</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>During pass creation:</strong> best moment, right after QR scan.</li>
|
||||||
|
<li><strong>On reward redemption:</strong> “Want your next reward reminder by email?”</li>
|
||||||
|
<li><strong>On progress view:</strong> small banner: “Add your email to get reward reminders.”</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>6.2 Data Model</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>Customer.email</code></li>
|
||||||
|
<li><code>Customer.email_consent</code></li>
|
||||||
|
<li><code>Customer.email_consent_at</code></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>6.3 GDPR-Friendly Flow (EU/Luxembourg)</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Use an explicit checkbox:
|
||||||
|
<br><em>“I agree to receive loyalty updates and promotions.”</em>
|
||||||
|
</li>
|
||||||
|
<li>Do not pre-check the box.</li>
|
||||||
|
<li>Store consent timestamp.</li>
|
||||||
|
<li>Provide unsubscribe link in emails.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>6.4 Usage</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Reward notifications (“Your reward is ready”).</li>
|
||||||
|
<li>Progress nudges (“You’re 1 stamp away”).</li>
|
||||||
|
<li>Promotions and new collection announcements.</li>
|
||||||
|
<li>Birthday offers (if DOB is collected).</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="features">
|
||||||
|
<h2>7. Features Beyond Stamps & Points</h2>
|
||||||
|
|
||||||
|
<h3>7.1 Reward Types</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Tiered loyalty levels:</strong> Bronze, Silver, Gold based on spend or visits.</li>
|
||||||
|
<li><strong>Cashback / store credit:</strong> earn balance redeemable in-store.</li>
|
||||||
|
<li><strong>Birthday rewards:</strong> automatic bonuses around customer’s birthday.</li>
|
||||||
|
<li><strong>Referral rewards:</strong> invite friends, both get points or discounts.</li>
|
||||||
|
<li><strong>Hybrid models:</strong> stamps + points + referrals.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>7.2 Coupons, Vouchers, Gift Cards</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Digital coupons (percentage or fixed amount).</li>
|
||||||
|
<li>Digital gift cards with balance.</li>
|
||||||
|
<li>Time-limited offers (weekend-only, seasonal).</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>7.3 Marketing & Communication</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Email campaigns.</li>
|
||||||
|
<li>SMS campaigns.</li>
|
||||||
|
<li>Wallet-based notifications (pass updates, messages).</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>7.4 Analytics & CRM</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Visit frequency, average spend, redemption rates.</li>
|
||||||
|
<li>Customer lifetime value.</li>
|
||||||
|
<li>New vs returning customers.</li>
|
||||||
|
<li>Store-level and staff-level performance.</li>
|
||||||
|
<li>Basic CRM: customer profiles, segmentation.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>7.5 Advanced Options</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Receipt scanning with OCR.</li>
|
||||||
|
<li>POS integration (automatic points and redemptions).</li>
|
||||||
|
<li>Gamification (spin-the-wheel, scratch cards, challenges).</li>
|
||||||
|
<li>Paid memberships / VIP subscriptions.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="google-wallet">
|
||||||
|
<h2>8. Google Wallet Integration</h2>
|
||||||
|
|
||||||
|
<h3>8.1 Core Concepts</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Google Wallet uses <strong>Passes API</strong> with JSON-based classes and objects.</li>
|
||||||
|
<li>You define a loyalty class and create pass objects per customer.</li>
|
||||||
|
<li>Updates are done via API calls (patching fields like points, stamps, messages).</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>8.2 Required Setup</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Google Cloud project.</li>
|
||||||
|
<li>Wallet API enabled.</li>
|
||||||
|
<li>Service account with JSON key.</li>
|
||||||
|
<li>Issuer ID and loyalty class definition.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>8.3 Backend Responsibilities</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Create pass objects for new customers.</li>
|
||||||
|
<li>Store <code>google_pass_id</code> in PassObject.</li>
|
||||||
|
<li>Update pass when stamps/points change.</li>
|
||||||
|
<li>Optionally send messages or promotions via pass updates.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>8.4 Example Endpoint Sketch</h3>
|
||||||
|
<pre><code>@router.post("/passes/create")
|
||||||
|
async def create_pass(data: PassCreateRequest, db: Session = Depends(get_db)):
|
||||||
|
customer = get_or_create_customer(db, data)
|
||||||
|
pass_obj = get_or_create_pass(db, customer, data.store_id)
|
||||||
|
|
||||||
|
google_link = google_wallet.create_pass(pass_obj)
|
||||||
|
return {"add_to_wallet_url": google_link}</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="apple-wallet">
|
||||||
|
<h2>9. Apple Wallet Integration</h2>
|
||||||
|
|
||||||
|
<h3>9.1 Core Difference</h3>
|
||||||
|
<p>
|
||||||
|
Apple Wallet uses <strong>signed <code>.pkpass</code> files</strong> instead of a JSON API.
|
||||||
|
A <code>.pkpass</code> is a ZIP containing:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>pass.json</code> – pass data</li>
|
||||||
|
<li>Images – logo, icon, strip</li>
|
||||||
|
<li><code>manifest.json</code> – SHA-1 hashes of all files</li>
|
||||||
|
<li><code>signature</code> – signed manifest using your certificate</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>9.2 Requirements</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Apple Developer Program membership.</li>
|
||||||
|
<li>Pass Type ID (e.g., <code>pass.com.yourbrand.loyalty</code>).</li>
|
||||||
|
<li>Pass certificate (<code>.p12</code>).</li>
|
||||||
|
<li>Push notification certificate for Wallet updates.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>9.3 Pass Template (pass.json)</h3>
|
||||||
|
<pre><code>{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"passTypeIdentifier": "pass.com.yourbrand.loyalty",
|
||||||
|
"teamIdentifier": "ABCDE12345",
|
||||||
|
"organizationName": "Your Brand",
|
||||||
|
"serialNumber": "{{serial}}",
|
||||||
|
"description": "Loyalty Card",
|
||||||
|
"logoText": "Your Brand",
|
||||||
|
"foregroundColor": "rgb(255,255,255)",
|
||||||
|
"backgroundColor": "rgb(0,0,0)",
|
||||||
|
"storeCard": {
|
||||||
|
"primaryFields": [
|
||||||
|
{
|
||||||
|
"key": "points",
|
||||||
|
"label": "Points",
|
||||||
|
"value": "{{points}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secondaryFields": [
|
||||||
|
{
|
||||||
|
"key": "stamps",
|
||||||
|
"label": "Stamps",
|
||||||
|
"value": "{{stamps}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"barcode": {
|
||||||
|
"format": "PKBarcodeFormatQR",
|
||||||
|
"message": "{{pass_id}}",
|
||||||
|
"messageEncoding": "iso-8859-1"
|
||||||
|
},
|
||||||
|
"webServiceURL": "https://yourdomain.com/apple/updates",
|
||||||
|
"authenticationToken": "{{auth_token}}"
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<h3>9.4 pkpass Generation Flow</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Load <code>loyalty_pass.json</code> template.</li>
|
||||||
|
<li>Inject dynamic values (serial, points, stamps, auth token, etc.).</li>
|
||||||
|
<li>Add required images (icon, logo, strip).</li>
|
||||||
|
<li>Create <code>manifest.json</code> with SHA-1 hashes of all files.</li>
|
||||||
|
<li>Sign manifest with your <code>.p12</code> certificate and Apple’s WWDR cert.</li>
|
||||||
|
<li>Zip everything into a <code>.pkpass</code> file.</li>
|
||||||
|
<li>Return <code>.pkpass</code> from endpoint <code>GET /passes/apple/{pass_id}</code>.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>9.5 Device Registration & Updates</h3>
|
||||||
|
<p>Apple Wallet uses a push + pull model for updates:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Customer adds pass → Apple sends device registration to your backend.</li>
|
||||||
|
<li>You store device ID, pass serial, and push token in <code>AppleDeviceRegistration</code>.</li>
|
||||||
|
<li>When stamps/points change:
|
||||||
|
<ul>
|
||||||
|
<li>You send a push notification to Apple using the push token.</li>
|
||||||
|
<li>Apple notifies the device; Wallet calls your <code>webServiceURL</code>.</li>
|
||||||
|
<li>Device fetches updated pass data from <code>GET /apple/updates/{pass_type}/{serial}</code>.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>9.6 Data Model Additions</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>PassObject.apple_serial</code></li>
|
||||||
|
<li><code>PassObject.apple_auth_token</code></li>
|
||||||
|
<li><code>AppleDeviceRegistration</code> table for device + push tokens.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>9.7 What Stays the Same</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Merchant, Store, Customer, PassObject core structure.</li>
|
||||||
|
<li>Stamp and points logic.</li>
|
||||||
|
<li>QR flows and staff PIN system.</li>
|
||||||
|
<li>Dashboard and analytics.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="multi-wallet">
|
||||||
|
<h2>10. Multi-Wallet Strategy</h2>
|
||||||
|
<p>
|
||||||
|
Your platform supports both Google Wallet and Apple Wallet by treating them as two output formats for the same underlying PassObject.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Google:</strong> store <code>google_pass_id</code>, update via API.</li>
|
||||||
|
<li><strong>Apple:</strong> store <code>apple_serial</code> and <code>apple_auth_token</code>, update via push + web service.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Whenever stamps or points change, you:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Patch the Google Wallet pass (if it exists).</li>
|
||||||
|
<li>Trigger Apple Wallet update (if Apple pass exists).</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
The merchant and store don’t need to care about the technical differences—your backend abstracts it away.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="deployment">
|
||||||
|
<h2>11. Deployment & Stack</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Backend:</strong> FastAPI (Python).</li>
|
||||||
|
<li><strong>Database:</strong> PostgreSQL (or MySQL), with migrations.</li>
|
||||||
|
<li><strong>Caching / rate limiting:</strong> Redis (optional but recommended).</li>
|
||||||
|
<li><strong>Web server:</strong> Nginx or similar reverse proxy.</li>
|
||||||
|
<li><strong>Containerization:</strong> Docker for reproducible deployments.</li>
|
||||||
|
<li><strong>Security:</strong> HTTPS via Cloudflare or Let’s Encrypt.</li>
|
||||||
|
<li><strong>Cloud:</strong> Any provider; Google Cloud is convenient for Wallet API.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="mvp">
|
||||||
|
<h2>12. MVP vs Future Phases</h2>
|
||||||
|
|
||||||
|
<h3>12.1 MVP Scope</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Merchant + Store management.</li>
|
||||||
|
<li>Customer + PassObject models.</li>
|
||||||
|
<li>Stamp-based loyalty with staff PIN.</li>
|
||||||
|
<li>Points-based loyalty for clothing stores.</li>
|
||||||
|
<li>Google Wallet integration.</li>
|
||||||
|
<li>Apple Wallet integration (basic, without advanced segmentation).</li>
|
||||||
|
<li>Basic dashboard: customers, stamps, points, rewards.</li>
|
||||||
|
<li>Email collection with consent.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>12.2 Phase 2 Ideas</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Tiers (Bronze/Silver/Gold).</li>
|
||||||
|
<li>Referrals and invite links.</li>
|
||||||
|
<li>Coupons, vouchers, and gift cards.</li>
|
||||||
|
<li>Gamification (spin-the-wheel, scratch cards).</li>
|
||||||
|
<li>Receipt scanning and POS integration.</li>
|
||||||
|
<li>Paid memberships / VIP programs.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="closing">
|
||||||
|
<h2>13. Closing Notes</h2>
|
||||||
|
<p>
|
||||||
|
The key design choice is that you do <strong>not</strong> redesign the system when adding:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Points on top of stamps.</li>
|
||||||
|
<li>Apple Wallet on top of Google Wallet.</li>
|
||||||
|
<li>New reward types (tiers, referrals, coupons).</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
You extend the same core models and flows with additional modules and fields, keeping the architecture modular and future-proof.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
main.py
3
main.py
@@ -64,6 +64,9 @@ from app.exceptions.handler import setup_exception_handlers
|
|||||||
# Import page routers (legacy routes - will be migrated to modules)
|
# Import page routers (legacy routes - will be migrated to modules)
|
||||||
from app.routes import admin_pages, platform_pages, storefront_pages, vendor_pages
|
from app.routes import admin_pages, platform_pages, storefront_pages, vendor_pages
|
||||||
|
|
||||||
|
# Import CMS module admin pages
|
||||||
|
from app.modules.cms.routes.pages.admin import router as cms_admin_pages
|
||||||
|
|
||||||
# Module route auto-discovery
|
# Module route auto-discovery
|
||||||
from app.modules.routes import discover_module_routes, get_vendor_page_routes
|
from app.modules.routes import discover_module_routes, get_vendor_page_routes
|
||||||
from app.utils.i18n import get_jinja2_globals
|
from app.utils.i18n import get_jinja2_globals
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ from . import schema
|
|||||||
|
|
||||||
# Database models (SQLAlchemy)
|
# Database models (SQLAlchemy)
|
||||||
from .database.base import Base
|
from .database.base import Base
|
||||||
from .database.inventory import Inventory
|
|
||||||
from .database.marketplace_import_job import MarketplaceImportJob
|
from .database.marketplace_import_job import MarketplaceImportJob
|
||||||
from .database.marketplace_product import MarketplaceProduct
|
from .database.marketplace_product import MarketplaceProduct
|
||||||
from .database.product import Product
|
from .database.product import Product
|
||||||
from .database.user import User
|
from .database.user import User
|
||||||
from .database.vendor import Vendor
|
from .database.vendor import Vendor
|
||||||
|
|
||||||
|
# Module-based models
|
||||||
|
from app.modules.inventory.models import Inventory
|
||||||
|
|
||||||
# Export database models for Alembic
|
# Export database models for Alembic
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Base",
|
"Base",
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
from .admin import (
|
from .admin import (
|
||||||
AdminAuditLog,
|
AdminAuditLog,
|
||||||
AdminNotification,
|
|
||||||
AdminSession,
|
AdminSession,
|
||||||
AdminSetting,
|
AdminSetting,
|
||||||
PlatformAlert,
|
PlatformAlert,
|
||||||
)
|
)
|
||||||
|
from app.modules.messaging.models import AdminNotification
|
||||||
from .admin_menu_config import AdminMenuConfig, FrontendType, MANDATORY_MENU_ITEMS
|
from .admin_menu_config import AdminMenuConfig, FrontendType, MANDATORY_MENU_ITEMS
|
||||||
from .admin_platform import AdminPlatform
|
from .admin_platform import AdminPlatform
|
||||||
from .architecture_scan import (
|
from .architecture_scan import (
|
||||||
@@ -43,15 +43,15 @@ from .company import Company
|
|||||||
from .platform import Platform
|
from .platform import Platform
|
||||||
from .platform_module import PlatformModule
|
from .platform_module import PlatformModule
|
||||||
from .vendor_platform import VendorPlatform
|
from .vendor_platform import VendorPlatform
|
||||||
from .customer import Customer, CustomerAddress
|
from app.modules.customers.models import Customer, CustomerAddress
|
||||||
from .password_reset_token import PasswordResetToken
|
from app.modules.customers.models import PasswordResetToken
|
||||||
from .email import EmailCategory, EmailLog, EmailStatus, EmailTemplate
|
from .email import EmailCategory, EmailLog, EmailStatus, EmailTemplate
|
||||||
from .vendor_email_template import VendorEmailTemplate
|
from .vendor_email_template import VendorEmailTemplate
|
||||||
from .vendor_email_settings import EmailProvider, VendorEmailSettings, PREMIUM_EMAIL_PROVIDERS
|
from .vendor_email_settings import EmailProvider, VendorEmailSettings, PREMIUM_EMAIL_PROVIDERS
|
||||||
from .feature import Feature, FeatureCategory, FeatureCode, FeatureUILocation
|
from .feature import Feature, FeatureCategory, FeatureCode, FeatureUILocation
|
||||||
from .inventory import Inventory
|
from app.modules.inventory.models import Inventory
|
||||||
from .inventory_transaction import InventoryTransaction, TransactionType
|
from app.modules.inventory.models import InventoryTransaction, TransactionType
|
||||||
from .invoice import (
|
from app.modules.orders.models import (
|
||||||
Invoice,
|
Invoice,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
VATRegime,
|
VATRegime,
|
||||||
@@ -64,7 +64,7 @@ from .letzshop import (
|
|||||||
VendorLetzshopCredentials,
|
VendorLetzshopCredentials,
|
||||||
)
|
)
|
||||||
from .marketplace_import_job import MarketplaceImportError, MarketplaceImportJob
|
from .marketplace_import_job import MarketplaceImportError, MarketplaceImportJob
|
||||||
from .message import (
|
from app.modules.messaging.models import (
|
||||||
Conversation,
|
Conversation,
|
||||||
ConversationParticipant,
|
ConversationParticipant,
|
||||||
ConversationType,
|
ConversationType,
|
||||||
@@ -80,8 +80,8 @@ from .marketplace_product import (
|
|||||||
from .media import MediaFile, ProductMedia
|
from .media import MediaFile, ProductMedia
|
||||||
from .marketplace_product_translation import MarketplaceProductTranslation
|
from .marketplace_product_translation import MarketplaceProductTranslation
|
||||||
from .onboarding import OnboardingStatus, OnboardingStep, VendorOnboarding
|
from .onboarding import OnboardingStatus, OnboardingStep, VendorOnboarding
|
||||||
from .order import Order, OrderItem
|
from app.modules.orders.models import Order, OrderItem
|
||||||
from .order_item_exception import OrderItemException
|
from app.modules.orders.models import OrderItemException
|
||||||
from .product import Product
|
from .product import Product
|
||||||
from .product_translation import ProductTranslation
|
from .product_translation import ProductTranslation
|
||||||
from .subscription import (
|
from .subscription import (
|
||||||
|
|||||||
@@ -59,37 +59,8 @@ class AdminAuditLog(Base, TimestampMixin):
|
|||||||
return f"<AdminAuditLog(id={self.id}, action='{self.action}', target={self.target_type}:{self.target_id})>"
|
return f"<AdminAuditLog(id={self.id}, action='{self.action}', target={self.target_type}:{self.target_id})>"
|
||||||
|
|
||||||
|
|
||||||
class AdminNotification(Base, TimestampMixin):
|
# AdminNotification has been moved to app/modules/messaging/models/admin_notification.py
|
||||||
"""
|
# It's re-exported via models/database/__init__.py for backwards compatibility
|
||||||
Admin-specific notifications for system alerts and warnings.
|
|
||||||
|
|
||||||
Different from vendor/customer notifications - these are for platform
|
|
||||||
administrators to track system health and issues requiring attention.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "admin_notifications"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
type = Column(
|
|
||||||
String(50), nullable=False, index=True
|
|
||||||
) # system_alert, vendor_issue, import_failure
|
|
||||||
priority = Column(
|
|
||||||
String(20), default="normal", index=True
|
|
||||||
) # low, normal, high, critical
|
|
||||||
title = Column(String(200), nullable=False)
|
|
||||||
message = Column(Text, nullable=False)
|
|
||||||
is_read = Column(Boolean, default=False, index=True)
|
|
||||||
read_at = Column(DateTime, nullable=True)
|
|
||||||
read_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
||||||
action_required = Column(Boolean, default=False, index=True)
|
|
||||||
action_url = Column(String(500)) # Link to relevant admin page
|
|
||||||
notification_metadata = Column(JSON) # Additional contextual data
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
read_by = relationship("User", foreign_keys=[read_by_user_id])
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<AdminNotification(id={self.id}, type='{self.type}', priority='{self.priority}')>"
|
|
||||||
|
|
||||||
|
|
||||||
class AdminSetting(Base, TimestampMixin):
|
class AdminSetting(Base, TimestampMixin):
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
# models/schema/__init__.py
|
# models/schema/__init__.py
|
||||||
"""API models package - Pydantic models for request/response validation."""
|
"""API models package - Pydantic models for request/response validation.
|
||||||
|
|
||||||
# Import API model modules
|
Note: Many schemas have been migrated to their respective modules:
|
||||||
|
- Customer schemas: app.modules.customers.schemas
|
||||||
|
- Order schemas: app.modules.orders.schemas
|
||||||
|
- Inventory schemas: app.modules.inventory.schemas
|
||||||
|
- Message schemas: app.modules.messaging.schemas
|
||||||
|
- Cart schemas: app.modules.cart.schemas
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Import API model modules that remain in legacy location
|
||||||
from . import (
|
from . import (
|
||||||
auth,
|
auth,
|
||||||
base,
|
base,
|
||||||
email,
|
email,
|
||||||
inventory,
|
|
||||||
invoice,
|
|
||||||
marketplace_import_job,
|
marketplace_import_job,
|
||||||
marketplace_product,
|
marketplace_product,
|
||||||
message,
|
|
||||||
onboarding,
|
onboarding,
|
||||||
stats,
|
|
||||||
vendor,
|
vendor,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,12 +27,8 @@ __all__ = [
|
|||||||
"base",
|
"base",
|
||||||
"auth",
|
"auth",
|
||||||
"email",
|
"email",
|
||||||
"invoice",
|
|
||||||
"marketplace_product",
|
"marketplace_product",
|
||||||
"message",
|
|
||||||
"inventory",
|
|
||||||
"onboarding",
|
"onboarding",
|
||||||
"vendor",
|
"vendor",
|
||||||
"marketplace_import_job",
|
"marketplace_import_job",
|
||||||
"stats",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.modules.customers.models.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
from models.database.password_reset_token import PasswordResetToken
|
from app.modules.customers.models import PasswordResetToken
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from app.services.admin_notification_service import (
|
|||||||
Priority,
|
Priority,
|
||||||
Severity,
|
Severity,
|
||||||
)
|
)
|
||||||
from models.database.admin import AdminNotification, PlatformAlert
|
from app.modules.messaging.models import AdminNotification
|
||||||
|
from models.database.admin import PlatformAlert
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
Reference in New Issue
Block a user