diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index 223a83c5..530fa36b 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -3,6 +3,6 @@ API Version 1 - All endpoints """ -from . import admin, shop, vendor +from . import admin, storefront, vendor -__all__ = ["admin", "vendor", "shop"] +__all__ = ["admin", "vendor", "storefront"] diff --git a/app/api/v1/admin/code_quality.py b/app/api/v1/admin/code_quality.py index 5d76f8d5..cac3cfe1 100644 --- a/app/api/v1/admin/code_quality.py +++ b/app/api/v1/admin/code_quality.py @@ -21,7 +21,7 @@ from app.services.code_quality_service import ( from app.tasks.code_quality_tasks import execute_code_quality_scan from models.database.architecture_scan import ArchitectureScan from models.database.user import User -from models.schema.stats import CodeQualityDashboardStatsResponse +from app.modules.analytics.schemas import CodeQualityDashboardStatsResponse router = APIRouter() diff --git a/app/api/v1/admin/dashboard.py b/app/api/v1/admin/dashboard.py index ffcdf9d5..1be31d38 100644 --- a/app/api/v1/admin/dashboard.py +++ b/app/api/v1/admin/dashboard.py @@ -13,7 +13,7 @@ from app.core.database import get_db from app.services.admin_service import admin_service from app.services.stats_service import stats_service from models.database.user import User -from models.schema.stats import ( +from app.modules.analytics.schemas import ( AdminDashboardResponse, ImportStatsResponse, MarketplaceStatsResponse, diff --git a/app/api/v1/admin/marketplace.py b/app/api/v1/admin/marketplace.py index fbc78801..620a4c08 100644 --- a/app/api/v1/admin/marketplace.py +++ b/app/api/v1/admin/marketplace.py @@ -24,7 +24,7 @@ from models.schema.marketplace_import_job import ( MarketplaceImportJobRequest, MarketplaceImportJobResponse, ) -from models.schema.stats import ImportStatsResponse +from app.modules.analytics.schemas import ImportStatsResponse router = APIRouter(prefix="/marketplace-import-jobs") logger = logging.getLogger(__name__) diff --git a/app/api/v1/admin/notifications.py b/app/api/v1/admin/notifications.py index 0d8193d6..656aaae6 100644 --- a/app/api/v1/admin/notifications.py +++ b/app/api/v1/admin/notifications.py @@ -29,7 +29,7 @@ from models.schema.admin import ( PlatformAlertResolve, PlatformAlertResponse, ) -from models.schema.notification import ( +from app.modules.messaging.schemas import ( AlertStatisticsResponse, MessageResponse, UnreadCountResponse, diff --git a/app/api/v1/admin/vendors.py b/app/api/v1/admin/vendors.py index e326af24..0f9e1439 100644 --- a/app/api/v1/admin/vendors.py +++ b/app/api/v1/admin/vendors.py @@ -20,7 +20,7 @@ from app.services.admin_service import admin_service from app.services.stats_service import stats_service from app.services.vendor_service import vendor_service from models.database.user import User -from models.schema.stats import VendorStatsResponse +from app.modules.analytics.schemas import VendorStatsResponse from models.schema.vendor import ( LetzshopExportRequest, LetzshopExportResponse, diff --git a/app/api/v1/vendor/analytics.py b/app/api/v1/vendor/analytics.py index b3b9ca76..a4c79db3 100644 --- a/app/api/v1/vendor/analytics.py +++ b/app/api/v1/vendor/analytics.py @@ -21,7 +21,7 @@ from app.core.feature_gate import RequireFeature from app.services.stats_service import stats_service from models.database.feature import FeatureCode from models.database.user import User -from models.schema.stats import ( +from app.modules.analytics.schemas import ( VendorAnalyticsCatalog, VendorAnalyticsImports, VendorAnalyticsInventory, diff --git a/app/api/v1/vendor/dashboard.py b/app/api/v1/vendor/dashboard.py index 346a2a31..a2827d19 100644 --- a/app/api/v1/vendor/dashboard.py +++ b/app/api/v1/vendor/dashboard.py @@ -17,7 +17,7 @@ from app.exceptions import VendorNotActiveException from app.services.stats_service import stats_service from app.services.vendor_service import vendor_service from models.database.user import User -from models.schema.stats import ( +from app.modules.analytics.schemas import ( VendorCustomerStats, VendorDashboardStatsResponse, VendorInfo, diff --git a/app/api/v1/vendor/notifications.py b/app/api/v1/vendor/notifications.py index a6983fc1..c9c2545e 100644 --- a/app/api/v1/vendor/notifications.py +++ b/app/api/v1/vendor/notifications.py @@ -15,7 +15,7 @@ from app.api.deps import get_current_vendor_api from app.core.database import get_db from app.services.vendor_service import vendor_service from models.database.user import User -from models.schema.notification import ( +from app.modules.messaging.schemas import ( MessageResponse, NotificationListResponse, NotificationSettingsResponse, diff --git a/app/modules/customers/routes/api/storefront.py b/app/modules/customers/routes/api/storefront.py index ae853c16..d6c3f442 100644 --- a/app/modules/customers/routes/api/storefront.py +++ b/app/modules/customers/routes/api/storefront.py @@ -31,7 +31,7 @@ from app.modules.customers.services import ( ) from app.services.auth_service import AuthService 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 ( LogoutResponse, PasswordResetRequestResponse, diff --git a/app/modules/monitoring/models/__init__.py b/app/modules/monitoring/models/__init__.py index 3f0b5682..e7618e71 100644 --- a/app/modules/monitoring/models/__init__.py +++ b/app/modules/monitoring/models/__init__.py @@ -9,10 +9,8 @@ Re-exports monitoring-related models from their source locations. from app.modules.billing.models import CapacitySnapshot # Admin notification and logging models -from models.database.admin import ( - AdminNotification, - PlatformAlert, -) +from app.modules.messaging.models import AdminNotification +from models.database.admin import PlatformAlert __all__ = [ "CapacitySnapshot", diff --git a/app/modules/orders/schemas/__init__.py b/app/modules/orders/schemas/__init__.py index 8b9119f2..8876ff34 100644 --- a/app/modules/orders/schemas/__init__.py +++ b/app/modules/orders/schemas/__init__.py @@ -43,6 +43,18 @@ from app.modules.orders.schemas.order import ( 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 ( # Invoice settings schemas VendorInvoiceSettingsCreate, @@ -79,6 +91,16 @@ __all__ = [ "OrderItemCreate", "OrderItemExceptionBrief", "OrderItemResponse", + # Order item exception schemas + "OrderItemExceptionResponse", + "OrderItemExceptionBriefResponse", + "OrderItemExceptionListResponse", + "OrderItemExceptionStats", + "ResolveExceptionRequest", + "IgnoreExceptionRequest", + "BulkResolveRequest", + "BulkResolveResponse", + "AutoMatchResult", # Customer schemas "CustomerSnapshot", "CustomerSnapshotResponse", diff --git a/app/modules/orders/schemas/order_item_exception.py b/app/modules/orders/schemas/order_item_exception.py new file mode 100644 index 00000000..19c7bd29 --- /dev/null +++ b/app/modules/orders/schemas/order_item_exception.py @@ -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] diff --git a/docs/proposals/SESSION_NOTE_2026-01-28_module-config-migrations.md b/docs/proposals/SESSION_NOTE_2026-01-28_module-config-migrations.md new file mode 100644 index 00000000..759fcbb9 --- /dev/null +++ b/docs/proposals/SESSION_NOTE_2026-01-28_module-config-migrations.md @@ -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) diff --git a/docs/proposals/temp-loyalty b/docs/proposals/temp-loyalty new file mode 100644 index 00000000..c4406440 --- /dev/null +++ b/docs/proposals/temp-loyalty @@ -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) \ No newline at end of file diff --git a/loyatly-ideas.html b/loyatly-ideas.html new file mode 100644 index 00000000..33f7eeed --- /dev/null +++ b/loyatly-ideas.html @@ -0,0 +1,678 @@ + + + + + Loyalty Platform Architecture – Google Wallet & Apple Wallet + + + + +
+

Loyalty Platform Architecture

+

Digital stamp & points system with Google Wallet and Apple Wallet support

+
+ +
+
+

1. High-Level Overview

+

+ This document describes a modular loyalty platform that supports: +

+ +

+ The architecture is designed to be extensible: you can add tiers, referrals, coupons, gift cards, and more without redesigning the core. +

+
+ +
+

2. Backend Architecture

+ +

2.1 Core Modules

+ + +

2.2 Folder Structure

+
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
+
+ +
+

3. Data Model

+ +

3.1 Merchant vs Store

+

+ Merchant = business entity (e.g., “CoffeeLux”) + Store = specific location/branch (e.g., “CoffeeLux Gare”). +

+ +

Merchant

+ + + + + + + +
FieldType
idUUID
namestring
emailstring
password_hashstring
created_atdatetime
+ +

Store

+ + + + + + + + +
FieldType
idUUID
merchant_idFK → Merchant
namestring
qr_code_urlstring
loyalty_type"stamps" | "points" | "hybrid"
staff_pin_hashstring (hashed)
+ +

Customer

+ + + + + + + + +
FieldType
idUUID
emailstring (nullable)
phonestring (nullable)
email_consentbool
email_consent_atdatetime (nullable)
created_atdatetime
+ +

PassObject

+ + + + + + + + + + + + + +
FieldType
idUUID
customer_idFK → Customer
store_idFK → Store
google_pass_idstring (nullable)
apple_serialstring (nullable)
apple_auth_tokenstring (nullable)
stamp_countint
points_balanceint
reward_unlockedbool
last_stamp_atdatetime (nullable)
last_points_atdatetime (nullable)
+ +

StampEvent

+ + + + + + +
FieldType
idUUID
pass_idFK → PassObject
store_idFK → Store
timestampdatetime
+ +

PointsEvent

+ + + + + + + + +
FieldType
idUUID
pass_idFK → PassObject
store_idFK → Store
points_addedint
amountdecimal (optional)
timestampdatetime
+ +

AppleDeviceRegistration

+ + + + + + + +
FieldType
idUUID
device_idstring
pass_serialstring
push_tokenstring
updated_atdatetime
+
+ +
+

4. Core Flows

+ +

4.1 Creating a Pass (Onboarding)

+
    +
  1. Customer scans a QR code or visits a link.
  2. +
  3. Page asks for optional data: +
      +
    • Name (optional)
    • +
    • Email (optional, with consent checkbox)
    • +
    • Phone (optional)
    • +
    +
  4. +
  5. Backend: +
      +
    • Creates or finds Customer.
    • +
    • Creates PassObject for the chosen store.
    • +
    • Generates Google Wallet link and/or Apple Wallet .pkpass.
    • +
    +
  6. +
  7. Customer taps “Add to Google Wallet” or “Add to Apple Wallet”.
  8. +
+ +

4.2 Stamping a Pass (Visits)

+

Endpoint: POST /stamp/{store_id}

+
    +
  1. Customer scans store QR: https://yourdomain.com/stamp/{store_id}.
  2. +
  3. Page identifies the customer’s pass (via login, token, or Wallet ID).
  4. +
  5. Staff enters a PIN on the customer’s device.
  6. +
  7. Backend: +
      +
    • Validates staff PIN.
    • +
    • Checks cooldown (e.g., 1 stamp per X minutes).
    • +
    • Increments stamp_count.
    • +
    • Logs a StampEvent.
    • +
    • Updates Google Wallet and Apple Wallet passes.
    • +
    +
  8. +
  9. Customer sees updated stamp count.
  10. +
+ +

4.3 Adding Points (Purchases)

+

Endpoint: POST /points/{store_id}

+
{
+  "pass_id": "abc123",
+  "amount": 59.90,
+  "staff_pin": "4821"
+}
+
    +
  1. Staff enters purchase amount and PIN.
  2. +
  3. Backend: +
      +
    • Validates staff PIN.
    • +
    • Converts amount → points (e.g., €1 = 1 point).
    • +
    • Updates points_balance.
    • +
    • Logs a PointsEvent.
    • +
    • Updates Wallet passes.
    • +
    +
  4. +
+ +

4.4 Reward Logic

+

+ Implemented in loyalty_engine/stamps.py and loyalty_engine/points.py. +

+ +
+ +
+

5. Staff PIN System & Anti-Fraud

+ +

5.1 Staff PIN Concept

+

+ Each store has one or more staff PINs. A stamp or points addition is only valid if a correct PIN is provided. +

+

Example request:

+
POST /stamp/{store_id}
+{
+  "pass_id": "abc123",
+  "staff_pin": "4821"
+}
+ +

5.2 Implementation

+ + +

5.3 Additional Anti-Fraud Layers

+ + +
+ The QR code itself never directly grants a stamp or points. It only opens a URL; the backend decides whether to award anything. +
+
+ +
+

6. Email Collection & GDPR Considerations

+ +

6.1 When to Collect Email

+ + +

6.2 Data Model

+ + +

6.3 GDPR-Friendly Flow (EU/Luxembourg)

+ + +

6.4 Usage

+ +
+ +
+

7. Features Beyond Stamps & Points

+ +

7.1 Reward Types

+ + +

7.2 Coupons, Vouchers, Gift Cards

+ + +

7.3 Marketing & Communication

+ + +

7.4 Analytics & CRM

+ + +

7.5 Advanced Options

+ +
+ +
+

8. Google Wallet Integration

+ +

8.1 Core Concepts

+ + +

8.2 Required Setup

+ + +

8.3 Backend Responsibilities

+ + +

8.4 Example Endpoint Sketch

+
@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}
+
+ +
+

9. Apple Wallet Integration

+ +

9.1 Core Difference

+

+ Apple Wallet uses signed .pkpass files instead of a JSON API. + A .pkpass is a ZIP containing: +

+ + +

9.2 Requirements

+ + +

9.3 Pass Template (pass.json)

+
{
+  "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}}"
+}
+ +

9.4 pkpass Generation Flow

+
    +
  1. Load loyalty_pass.json template.
  2. +
  3. Inject dynamic values (serial, points, stamps, auth token, etc.).
  4. +
  5. Add required images (icon, logo, strip).
  6. +
  7. Create manifest.json with SHA-1 hashes of all files.
  8. +
  9. Sign manifest with your .p12 certificate and Apple’s WWDR cert.
  10. +
  11. Zip everything into a .pkpass file.
  12. +
  13. Return .pkpass from endpoint GET /passes/apple/{pass_id}.
  14. +
+ +

9.5 Device Registration & Updates

+

Apple Wallet uses a push + pull model for updates:

+
    +
  1. Customer adds pass → Apple sends device registration to your backend.
  2. +
  3. You store device ID, pass serial, and push token in AppleDeviceRegistration.
  4. +
  5. When stamps/points change: +
      +
    • You send a push notification to Apple using the push token.
    • +
    • Apple notifies the device; Wallet calls your webServiceURL.
    • +
    • Device fetches updated pass data from GET /apple/updates/{pass_type}/{serial}.
    • +
    +
  6. +
+ +

9.6 Data Model Additions

+ + +

9.7 What Stays the Same

+ +
+ +
+

10. Multi-Wallet Strategy

+

+ Your platform supports both Google Wallet and Apple Wallet by treating them as two output formats for the same underlying PassObject. +

+ +

+ Whenever stamps or points change, you: +

+ +

+ The merchant and store don’t need to care about the technical differences—your backend abstracts it away. +

+
+ +
+

11. Deployment & Stack

+ +
+ +
+

12. MVP vs Future Phases

+ +

12.1 MVP Scope

+ + +

12.2 Phase 2 Ideas

+ +
+ +
+

13. Closing Notes

+

+ The key design choice is that you do not redesign the system when adding: +

+ +

+ You extend the same core models and flows with additional modules and fields, keeping the architecture modular and future-proof. +

+
+
+ + diff --git a/main.py b/main.py index 9cf6edbc..ea0d4e7d 100644 --- a/main.py +++ b/main.py @@ -64,6 +64,9 @@ from app.exceptions.handler import setup_exception_handlers # Import page routers (legacy routes - will be migrated to modules) 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 from app.modules.routes import discover_module_routes, get_vendor_page_routes from app.utils.i18n import get_jinja2_globals diff --git a/models/__init__.py b/models/__init__.py index 85fac465..a4c6d20f 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -6,13 +6,15 @@ from . import schema # Database models (SQLAlchemy) from .database.base import Base -from .database.inventory import Inventory from .database.marketplace_import_job import MarketplaceImportJob from .database.marketplace_product import MarketplaceProduct from .database.product import Product from .database.user import User from .database.vendor import Vendor +# Module-based models +from app.modules.inventory.models import Inventory + # Export database models for Alembic __all__ = [ "Base", diff --git a/models/database/__init__.py b/models/database/__init__.py index 42848464..8b738f05 100644 --- a/models/database/__init__.py +++ b/models/database/__init__.py @@ -25,11 +25,11 @@ logger = logging.getLogger(__name__) from .admin import ( AdminAuditLog, - AdminNotification, AdminSession, AdminSetting, PlatformAlert, ) +from app.modules.messaging.models import AdminNotification from .admin_menu_config import AdminMenuConfig, FrontendType, MANDATORY_MENU_ITEMS from .admin_platform import AdminPlatform from .architecture_scan import ( @@ -43,15 +43,15 @@ from .company import Company from .platform import Platform from .platform_module import PlatformModule from .vendor_platform import VendorPlatform -from .customer import Customer, CustomerAddress -from .password_reset_token import PasswordResetToken +from app.modules.customers.models import Customer, CustomerAddress +from app.modules.customers.models import PasswordResetToken from .email import EmailCategory, EmailLog, EmailStatus, EmailTemplate from .vendor_email_template import VendorEmailTemplate from .vendor_email_settings import EmailProvider, VendorEmailSettings, PREMIUM_EMAIL_PROVIDERS from .feature import Feature, FeatureCategory, FeatureCode, FeatureUILocation -from .inventory import Inventory -from .inventory_transaction import InventoryTransaction, TransactionType -from .invoice import ( +from app.modules.inventory.models import Inventory +from app.modules.inventory.models import InventoryTransaction, TransactionType +from app.modules.orders.models import ( Invoice, InvoiceStatus, VATRegime, @@ -64,7 +64,7 @@ from .letzshop import ( VendorLetzshopCredentials, ) from .marketplace_import_job import MarketplaceImportError, MarketplaceImportJob -from .message import ( +from app.modules.messaging.models import ( Conversation, ConversationParticipant, ConversationType, @@ -80,8 +80,8 @@ from .marketplace_product import ( from .media import MediaFile, ProductMedia from .marketplace_product_translation import MarketplaceProductTranslation from .onboarding import OnboardingStatus, OnboardingStep, VendorOnboarding -from .order import Order, OrderItem -from .order_item_exception import OrderItemException +from app.modules.orders.models import Order, OrderItem +from app.modules.orders.models import OrderItemException from .product import Product from .product_translation import ProductTranslation from .subscription import ( diff --git a/models/database/admin.py b/models/database/admin.py index d5c05d26..32f99d49 100644 --- a/models/database/admin.py +++ b/models/database/admin.py @@ -59,37 +59,8 @@ class AdminAuditLog(Base, TimestampMixin): return f"" -class AdminNotification(Base, TimestampMixin): - """ - 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 has been moved to app/modules/messaging/models/admin_notification.py +# It's re-exported via models/database/__init__.py for backwards compatibility class AdminSetting(Base, TimestampMixin): diff --git a/models/schema/__init__.py b/models/schema/__init__.py index cdb71410..94e9508c 100644 --- a/models/schema/__init__.py +++ b/models/schema/__init__.py @@ -1,18 +1,22 @@ # 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 ( auth, base, email, - inventory, - invoice, marketplace_import_job, marketplace_product, - message, onboarding, - stats, vendor, ) @@ -23,12 +27,8 @@ __all__ = [ "base", "auth", "email", - "invoice", "marketplace_product", - "message", - "inventory", "onboarding", "vendor", "marketplace_import_job", - "stats", ] diff --git a/tests/integration/api/v1/storefront/test_password_reset.py b/tests/integration/api/v1/storefront/test_password_reset.py index abf3f9cf..894cfbc3 100644 --- a/tests/integration/api/v1/storefront/test_password_reset.py +++ b/tests/integration/api/v1/storefront/test_password_reset.py @@ -10,7 +10,7 @@ from unittest.mock import MagicMock, patch import pytest 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 diff --git a/tests/unit/services/test_admin_notification_service.py b/tests/unit/services/test_admin_notification_service.py index bb4f160d..19a71daa 100644 --- a/tests/unit/services/test_admin_notification_service.py +++ b/tests/unit/services/test_admin_notification_service.py @@ -15,7 +15,8 @@ from app.services.admin_notification_service import ( Priority, Severity, ) -from models.database.admin import AdminNotification, PlatformAlert +from app.modules.messaging.models import AdminNotification +from models.database.admin import PlatformAlert @pytest.fixture