feat(loyalty): fix Google Wallet integration and improve enrollment flow

- Fix Google Wallet class creation: add required issuerName field (merchant name),
  programLogo with default logo fallback, hexBackgroundColor default
- Add default loyalty logo assets (200px + 512px) for programs without custom logos
- Smart retry: skip retries on 400/401/403/404 client errors (not transient)
- Fix enrollment success page: use sessionStorage for wallet URLs instead of
  authenticated API call (self-enrolled customers have no session)
- Hide wallet section on success page when no wallet URLs available
- Wire up T&C modal on enrollment page with program.terms_text
- Add startup validation for Google/Apple Wallet configs in lifespan
- Add admin wallet status dashboard endpoint and UI (moved to service layer)
- Fix Apple Wallet push notifications with real APNs HTTP/2 implementation
- Fix docs: correct enrollment URLs (port, path segments, /v1 prefix)
- Fix test assertion: !loyalty-enroll! → !enrollment!

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 17:32:55 +01:00
parent f766a72480
commit 8c8975239a
15 changed files with 828 additions and 239 deletions

View File

@@ -833,6 +833,62 @@ class ProgramService:
"estimated_liability_cents": estimated_liability,
}
def get_wallet_integration_status(self, db: Session) -> dict:
"""Get wallet integration status for admin dashboard."""
from app.modules.loyalty.models import LoyaltyCard
from app.modules.loyalty.services.apple_wallet_service import (
apple_wallet_service,
)
from app.modules.loyalty.services.google_wallet_service import (
google_wallet_service,
)
# Google Wallet
google_config = google_wallet_service.validate_config()
google_classes = []
if google_config["credentials_valid"]:
programs_with_class = (
db.query(LoyaltyProgram)
.filter(LoyaltyProgram.google_class_id.isnot(None))
.all()
)
for prog in programs_with_class:
status = google_wallet_service.get_class_status(
prog.google_class_id,
)
google_classes.append({
"program_id": prog.id,
"program_name": prog.display_name,
"class_id": prog.google_class_id,
"review_status": status["review_status"] if status else "UNKNOWN",
})
google_objects = (
db.query(LoyaltyCard)
.filter(LoyaltyCard.google_object_id.isnot(None))
.count()
)
# Apple Wallet
apple_config = apple_wallet_service.validate_config()
apple_passes = (
db.query(LoyaltyCard)
.filter(LoyaltyCard.apple_serial_number.isnot(None))
.count()
)
return {
"google_wallet": {
**google_config,
"classes": google_classes,
"total_objects": google_objects,
},
"apple_wallet": {
**apple_config,
"total_passes": apple_passes,
},
}
def get_merchant_stats(self, db: Session, merchant_id: int) -> dict:
"""
Get statistics for a merchant's loyalty program across all locations.