feat(loyalty): wallet debug page, Google Wallet fixes, and module config env_file standardization
Some checks failed
Some checks failed
- Add wallet diagnostics page at /admin/loyalty/wallet-debug (super admin only) with explorer-sidebar pattern: config validation, class status, card inspector, save URL tester, recent enrollments, and Apple Wallet status panels - Fix Google Wallet fat JWT: include both loyaltyClasses and loyaltyObjects in payload, use UNDER_REVIEW instead of DRAFT for class reviewStatus - Fix StorefrontProgramResponse schema: accept google_class_id values while keeping exclude=True (was rejecting non-None values) - Standardize all module configs to read from .env file directly (env_file=".env", extra="ignore") matching core Settings pattern - Add MOD-026 architecture rule enforcing env_file in module configs - Add SVC-005 noqa support in architecture validator - Add test files for dev_tools domain_health and isolation_audit services - Add google_wallet_status.py script for querying Google Wallet API - Use table_wrapper macro in wallet-debug.html (FE-005 compliance) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -250,41 +250,8 @@ class GoogleWalletService:
|
||||
if not self.is_configured:
|
||||
raise GoogleWalletNotConfiguredException()
|
||||
|
||||
issuer_id = config.google_issuer_id
|
||||
class_id = f"{issuer_id}.loyalty_program_{program.id}"
|
||||
|
||||
# issuerName is required by Google Wallet API
|
||||
issuer_name = program.merchant.name if program.merchant else program.display_name
|
||||
|
||||
class_data = {
|
||||
"id": class_id,
|
||||
"issuerId": issuer_id,
|
||||
"issuerName": issuer_name,
|
||||
"reviewStatus": "DRAFT",
|
||||
"programName": program.display_name,
|
||||
"hexBackgroundColor": program.card_color or "#4285F4",
|
||||
"localizedProgramName": {
|
||||
"defaultValue": {
|
||||
"language": "en",
|
||||
"value": program.display_name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# programLogo is required by Google Wallet API
|
||||
# Google must be able to fetch the image, so it needs a public URL
|
||||
logo_url = program.logo_url
|
||||
if not logo_url:
|
||||
logo_url = config.default_logo_url
|
||||
class_data["programLogo"] = {
|
||||
"sourceUri": {"uri": logo_url},
|
||||
}
|
||||
|
||||
# Add hero image if configured
|
||||
if program.hero_image_url:
|
||||
class_data["heroImage"] = {
|
||||
"sourceUri": {"uri": program.hero_image_url},
|
||||
}
|
||||
class_id = f"{config.google_issuer_id}.loyalty_program_{program.id}"
|
||||
class_data = self._build_class_data(program, class_id)
|
||||
|
||||
try:
|
||||
http = self._get_http_client()
|
||||
@@ -445,15 +412,50 @@ class GoogleWalletService:
|
||||
except (requests.RequestException, ValueError, AttributeError) as exc:
|
||||
logger.error("Failed to update Google Wallet object: %s", exc)
|
||||
|
||||
def _build_class_data(
|
||||
self, program: LoyaltyProgram, class_id: str,
|
||||
*, review_status: str = "UNDER_REVIEW",
|
||||
) -> dict[str, Any]:
|
||||
"""Build the LoyaltyClass data structure."""
|
||||
issuer_id = config.google_issuer_id
|
||||
issuer_name = program.merchant.name if program.merchant else program.display_name
|
||||
|
||||
class_data: dict[str, Any] = {
|
||||
"id": class_id,
|
||||
"issuerId": issuer_id,
|
||||
"issuerName": issuer_name,
|
||||
"reviewStatus": review_status,
|
||||
"programName": program.display_name,
|
||||
"hexBackgroundColor": program.card_color or "#4285F4",
|
||||
"localizedProgramName": {
|
||||
"defaultValue": {
|
||||
"language": "en",
|
||||
"value": program.display_name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
logo_url = program.logo_url or config.default_logo_url
|
||||
class_data["programLogo"] = {
|
||||
"sourceUri": {"uri": logo_url},
|
||||
}
|
||||
|
||||
if program.hero_image_url:
|
||||
class_data["heroImage"] = {
|
||||
"sourceUri": {"uri": program.hero_image_url},
|
||||
}
|
||||
|
||||
return class_data
|
||||
|
||||
def _build_object_data(
|
||||
self, card: LoyaltyCard, object_id: str
|
||||
self, card: LoyaltyCard, object_id: str, class_id: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Build the LoyaltyObject data structure."""
|
||||
program = card.program
|
||||
|
||||
object_data: dict[str, Any] = {
|
||||
"id": object_id,
|
||||
"classId": program.google_class_id,
|
||||
"classId": class_id or program.google_class_id,
|
||||
"state": "ACTIVE" if card.is_active else "INACTIVE",
|
||||
"accountId": card.card_number,
|
||||
"accountName": card.card_number,
|
||||
@@ -537,9 +539,14 @@ class GoogleWalletService:
|
||||
"loyaltyObjects": [{"id": card.google_object_id}],
|
||||
}
|
||||
else:
|
||||
# Object not created — embed full object data in JWT
|
||||
object_data = self._build_object_data(card, object_id)
|
||||
# Object not created — embed full class + object data in JWT
|
||||
# ("fat JWT"). Both are required for Google to render and save.
|
||||
program = card.program
|
||||
class_id = program.google_class_id or f"{issuer_id}.loyalty_program_{program.id}"
|
||||
class_data = self._build_class_data(program, class_id)
|
||||
object_data = self._build_object_data(card, object_id, class_id=class_id)
|
||||
payload = {
|
||||
"loyaltyClasses": [class_data],
|
||||
"loyaltyObjects": [object_data],
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user