fix(loyalty): use Code 128 barcode for retail scanner compatibility
Switch wallet pass barcodes from QR to Code 128 format using the card_number (digits only), so standard retail barcode scanners can read loyalty cards. Apple Wallet keeps QR as fallback in barcodes array. Also fix stale Vendor.loyalty_program relationship (now company-based), add parent init calls in vendor JS components, and update module docs to reflect Phase 2 completion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 ===');
|
||||
},
|
||||
|
||||
@@ -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 ===');
|
||||
},
|
||||
|
||||
@@ -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 ===');
|
||||
},
|
||||
|
||||
@@ -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 ===');
|
||||
|
||||
@@ -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"<Vendor(id={self.id}, vendor_code='{self.vendor_code}', name='{self.name}', subdomain='{self.subdomain}')>"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user