diff --git a/app/modules/loyalty/services/apple_wallet_service.py b/app/modules/loyalty/services/apple_wallet_service.py index bdd47f50..efd81e5e 100644 --- a/app/modules/loyalty/services/apple_wallet_service.py +++ b/app/modules/loyalty/services/apple_wallet_service.py @@ -125,16 +125,23 @@ class AppleWalletService: "authenticationToken": card.apple_auth_token, "webServiceURL": self._get_web_service_url(), "barcode": { - "message": card.qr_code_data, - "format": "PKBarcodeFormatQR", + "message": card.card_number.replace("-", ""), + "format": "PKBarcodeFormatCode128", "messageEncoding": "iso-8859-1", + "altText": card.card_number, }, "barcodes": [ + { + "message": card.card_number.replace("-", ""), + "format": "PKBarcodeFormatCode128", + "messageEncoding": "iso-8859-1", + "altText": card.card_number, + }, { "message": card.qr_code_data, "format": "PKBarcodeFormatQR", "messageEncoding": "iso-8859-1", - } + }, ], } diff --git a/app/modules/loyalty/services/google_wallet_service.py b/app/modules/loyalty/services/google_wallet_service.py index e2c0df59..8606fa6d 100644 --- a/app/modules/loyalty/services/google_wallet_service.py +++ b/app/modules/loyalty/services/google_wallet_service.py @@ -275,8 +275,9 @@ class GoogleWalletService: "accountId": card.card_number, "accountName": card.card_number, "barcode": { - "type": "QR_CODE", - "value": card.qr_code_data, + "type": "CODE_128", + "value": card.card_number.replace("-", ""), + "alternateText": card.card_number, }, } diff --git a/app/modules/loyalty/static/vendor/js/loyalty-card-detail.js b/app/modules/loyalty/static/vendor/js/loyalty-card-detail.js index a16f917e..253c1334 100644 --- a/app/modules/loyalty/static/vendor/js/loyalty-card-detail.js +++ b/app/modules/loyalty/static/vendor/js/loyalty-card-detail.js @@ -20,6 +20,12 @@ function vendorLoyaltyCardDetail() { if (window._loyaltyCardDetailInitialized) return; window._loyaltyCardDetailInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + // Extract card ID from URL const pathParts = window.location.pathname.split('/'); const cardsIndex = pathParts.indexOf('cards'); diff --git a/app/modules/loyalty/static/vendor/js/loyalty-cards.js b/app/modules/loyalty/static/vendor/js/loyalty-cards.js index 90ce4b62..2cec2c41 100644 --- a/app/modules/loyalty/static/vendor/js/loyalty-cards.js +++ b/app/modules/loyalty/static/vendor/js/loyalty-cards.js @@ -41,6 +41,12 @@ function vendorLoyaltyCards() { if (window._loyaltyCardsInitialized) return; window._loyaltyCardsInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } diff --git a/app/modules/loyalty/static/vendor/js/loyalty-enroll.js b/app/modules/loyalty/static/vendor/js/loyalty-enroll.js index a2c3e766..55423fdb 100644 --- a/app/modules/loyalty/static/vendor/js/loyalty-enroll.js +++ b/app/modules/loyalty/static/vendor/js/loyalty-enroll.js @@ -29,6 +29,12 @@ function vendorLoyaltyEnroll() { if (window._loyaltyEnrollInitialized) return; window._loyaltyEnrollInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + await this.loadProgram(); loyaltyEnrollLog.info('=== LOYALTY ENROLL PAGE INITIALIZATION COMPLETE ==='); }, diff --git a/app/modules/loyalty/static/vendor/js/loyalty-settings.js b/app/modules/loyalty/static/vendor/js/loyalty-settings.js index 6b7dfe38..fd286537 100644 --- a/app/modules/loyalty/static/vendor/js/loyalty-settings.js +++ b/app/modules/loyalty/static/vendor/js/loyalty-settings.js @@ -30,6 +30,12 @@ function vendorLoyaltySettings() { if (window._loyaltySettingsInitialized) return; window._loyaltySettingsInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + await this.loadSettings(); loyaltySettingsLog.info('=== LOYALTY SETTINGS PAGE INITIALIZATION COMPLETE ==='); }, diff --git a/app/modules/loyalty/static/vendor/js/loyalty-stats.js b/app/modules/loyalty/static/vendor/js/loyalty-stats.js index 2eebd737..99aa74ba 100644 --- a/app/modules/loyalty/static/vendor/js/loyalty-stats.js +++ b/app/modules/loyalty/static/vendor/js/loyalty-stats.js @@ -29,6 +29,12 @@ function vendorLoyaltyStats() { if (window._loyaltyStatsInitialized) return; window._loyaltyStatsInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + await this.loadStats(); loyaltyStatsLog.info('=== LOYALTY STATS PAGE INITIALIZATION COMPLETE ==='); }, diff --git a/app/modules/loyalty/static/vendor/js/loyalty-terminal.js b/app/modules/loyalty/static/vendor/js/loyalty-terminal.js index b7a2fd22..2a13970f 100644 --- a/app/modules/loyalty/static/vendor/js/loyalty-terminal.js +++ b/app/modules/loyalty/static/vendor/js/loyalty-terminal.js @@ -52,6 +52,12 @@ function vendorLoyaltyTerminal() { } window._loyaltyTerminalInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + await this.loadData(); loyaltyTerminalLog.info('=== LOYALTY TERMINAL INITIALIZATION COMPLETE ==='); diff --git a/app/modules/tenancy/models/vendor.py b/app/modules/tenancy/models/vendor.py index d1719aa9..33c1f4e6 100644 --- a/app/modules/tenancy/models/vendor.py +++ b/app/modules/tenancy/models/vendor.py @@ -253,14 +253,6 @@ class Vendor(Base, TimestampMixin): cascade="all, delete-orphan", ) - # Loyalty program (one-to-one) - loyalty_program = relationship( - "LoyaltyProgram", - back_populates="vendor", - uselist=False, - cascade="all, delete-orphan", - ) - def __repr__(self): """String representation of the Vendor object.""" return f"" diff --git a/docs/modules/loyalty.md b/docs/modules/loyalty.md index 1d8ba34b..00f66fee 100644 --- a/docs/modules/loyalty.md +++ b/docs/modules/loyalty.md @@ -8,7 +8,7 @@ The Loyalty Module provides stamp-based and points-based loyalty programs for Wi |--------|-------------| | Module Code | `loyalty` | | Dependencies | `customers` | -| Status | Phase 1 MVP Complete | +| Status | Phase 2 Complete | ### Key Features @@ -23,36 +23,37 @@ The Loyalty Module provides stamp-based and points-based loyalty programs for Wi ``` ┌─────────────────┐ ┌─────────────────┐ -│ Vendor │───────│ LoyaltyProgram │ +│ Company │───────│ LoyaltyProgram │ └─────────────────┘ 1:1 └─────────────────┘ - │ - ┌───────────┼───────────┐ - │ │ │ - ▼ ▼ ▼ - ┌──────────┐ ┌──────────┐ ┌──────────┐ - │ StaffPin │ │LoyaltyCard│ │ (config)│ - └──────────┘ └──────────┘ └──────────┘ - │ │ - │ ▼ - │ ┌──────────────┐ - └───▶│ Transaction │ - └──────────────┘ - │ - ▼ - ┌──────────────────────┐ - │AppleDeviceRegistration│ - └──────────────────────┘ + │ │ + ▼ ┌────────┼──────────┐ +┌──────────────────┐ │ │ │ +│ CompanyLoyalty │ ▼ ▼ ▼ +│ Settings │┌──────────┐┌──────────┐┌────────┐ +└──────────────────┘│ StaffPin ││LoyaltyCard││(config)│ + └──────────┘└──────────┘└────────┘ + │ │ + │ ▼ + │ ┌──────────────┐ + └──▶│ Transaction │ + └──────────────┘ + │ + ▼ + ┌──────────────────────┐ + │AppleDeviceRegistration│ + └──────────────────────┘ ``` ### Database Tables | Table | Purpose | |-------|---------| -| `loyalty_programs` | Vendor's program configuration (type, targets, branding) | -| `loyalty_cards` | Customer cards with stamp/point balances | +| `loyalty_programs` | Company's program configuration (type, targets, branding) | +| `loyalty_cards` | Customer cards with stamp/point balances (company-scoped) | | `loyalty_transactions` | Immutable audit log of all operations | | `staff_pins` | Hashed PINs for fraud prevention | | `apple_device_registrations` | Apple Wallet push notification tokens | +| `company_loyalty_settings` | Admin-controlled per-company settings | ## Configuration @@ -112,6 +113,14 @@ LOYALTY_APPLE_SIGNER_KEY_PATH=/path/to/signer.key | `GET` | `/programs/{id}/stats` | Get program statistics | | `GET` | `/stats` | Platform-wide statistics | +### Storefront Endpoints (`/api/v1/storefront/loyalty/`) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/card` | Get customer's loyalty card and balance | +| `GET` | `/transactions` | Get customer's transaction history | +| `POST` | `/enroll` | Self-enrollment in loyalty program | + ### Public Endpoints (`/api/v1/loyalty/`) | Method | Endpoint | Description | @@ -254,22 +263,54 @@ result = points_service.earn_points( | Task | Schedule | Description | |------|----------|-------------| | `loyalty.sync_wallet_passes` | Hourly | Sync cards that missed real-time updates | -| `loyalty.expire_points` | Daily 02:00 | Expire old points (future enhancement) | +| `loyalty.expire_points` | Daily 02:00 | Expire points for inactive cards (based on `points_expiration_days`) | + +## UI Pages + +### Admin Pages + +| Page | Path | Description | +|------|------|-------------| +| Programs Dashboard | `/admin/loyalty/programs` | List all loyalty programs with stats | +| Company Detail | `/admin/loyalty/companies/{id}` | Detailed view of a company's program | +| Company Settings | `/admin/loyalty/companies/{id}/settings` | Admin-controlled company settings | +| Analytics | `/admin/loyalty/analytics` | Platform-wide analytics | + +### Vendor Pages + +| Page | Path | Description | +|------|------|-------------| +| Terminal | `/vendor/loyalty/terminal` | Scan card, add stamps/points, redeem | +| Cards List | `/vendor/loyalty/cards` | Browse customer cards | +| Card Detail | `/vendor/loyalty/cards/{id}` | Individual card detail | +| Enroll | `/vendor/loyalty/enroll` | Enroll new customer | +| Settings | `/vendor/loyalty/settings` | Program settings | +| Stats | `/vendor/loyalty/stats` | Vendor-level statistics | + +### Storefront Pages + +| Page | Path | Description | +|------|------|-------------| +| Dashboard | `/loyalty/dashboard` | Customer's card and balance | +| History | `/loyalty/history` | Transaction history | +| Enroll | `/loyalty/enroll` | Self-enrollment page | ## Localization -Available in 4 languages: +Available in 5 languages: - English (`en.json`) - French (`fr.json`) - German (`de.json`) -- Luxembourgish (`lu.json`) +- Luxembourgish (`lu.json`, `lb.json`) -## Future Enhancements (Phase 2) +## Future Enhancements (Phase 3+) - Rewards catalog with configurable tiers - Customer tiers (Bronze/Silver/Gold) +- Promotions engine (bonus points, discounts, free items) - Referral program - Gamification (spin wheel, scratch cards) - POS integration -- Points expiration rules - Batch import of existing loyalty cards +- Real-time WebSocket updates +- Receipt printing