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:
136
scripts/google_wallet_status.py
Normal file
136
scripts/google_wallet_status.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Query Google Wallet API for class and object status.
|
||||
|
||||
Usage:
|
||||
python scripts/google_wallet_status.py # List all classes
|
||||
python scripts/google_wallet_status.py --class-id ID # Check specific class
|
||||
python scripts/google_wallet_status.py --object-id ID # Check specific object
|
||||
python scripts/google_wallet_status.py --fix-draft # Patch DRAFT classes to UNDER_REVIEW
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from app.modules.loyalty.config import config
|
||||
from app.modules.loyalty.services.google_wallet_service import GoogleWalletService
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Query Google Wallet API")
|
||||
parser.add_argument("--class-id", help="Check a specific class ID")
|
||||
parser.add_argument("--object-id", help="Check a specific object ID")
|
||||
parser.add_argument(
|
||||
"--fix-draft",
|
||||
action="store_true",
|
||||
help="Patch all DRAFT classes to UNDER_REVIEW for approval",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
svc = GoogleWalletService()
|
||||
|
||||
if not svc.is_configured:
|
||||
print("Google Wallet is not configured.")
|
||||
print(f" LOYALTY_GOOGLE_ISSUER_ID: {config.google_issuer_id or 'NOT SET'}")
|
||||
print(f" LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON: {config.google_service_account_json or 'NOT SET'}")
|
||||
sys.exit(1)
|
||||
|
||||
http = svc._get_http_client()
|
||||
issuer_id = config.google_issuer_id
|
||||
print(f"Issuer ID: {issuer_id}")
|
||||
print()
|
||||
|
||||
if args.object_id:
|
||||
_check_object(http, args.object_id)
|
||||
return
|
||||
|
||||
if args.class_id:
|
||||
_check_class(http, args.class_id)
|
||||
return
|
||||
|
||||
# List all classes for this issuer
|
||||
print("=== Loyalty Classes ===")
|
||||
response = http.get(
|
||||
"https://walletobjects.googleapis.com/walletobjects/v1/loyaltyClass",
|
||||
params={"issuerId": issuer_id},
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"Error: {response.status_code} - {response.text}")
|
||||
sys.exit(1)
|
||||
|
||||
data = response.json()
|
||||
classes = data.get("resources", [])
|
||||
|
||||
if not classes:
|
||||
print("No loyalty classes found.")
|
||||
return
|
||||
|
||||
for cls in classes:
|
||||
status = cls.get("reviewStatus", "UNKNOWN")
|
||||
class_id = cls.get("id")
|
||||
program_name = cls.get("programName", "—")
|
||||
issuer_name = cls.get("issuerName", "—")
|
||||
color = cls.get("hexBackgroundColor", "—")
|
||||
|
||||
status_icon = {
|
||||
"approved": "✅",
|
||||
"draft": "⚠️",
|
||||
"rejected": "❌",
|
||||
"underReview": "🔄",
|
||||
}.get(status, "❓")
|
||||
|
||||
print(f" {status_icon} {class_id}")
|
||||
print(f" Program: {program_name}")
|
||||
print(f" Issuer: {issuer_name}")
|
||||
print(f" Status: {status}")
|
||||
print(f" Color: {color}")
|
||||
print()
|
||||
|
||||
if args.fix_draft and status == "draft":
|
||||
print(" Patching to UNDER_REVIEW...")
|
||||
patch_resp = http.patch(
|
||||
f"https://walletobjects.googleapis.com/walletobjects/v1/loyaltyClass/{class_id}",
|
||||
json={"reviewStatus": "UNDER_REVIEW"},
|
||||
)
|
||||
if patch_resp.status_code == 200:
|
||||
new_status = patch_resp.json().get("reviewStatus", "unknown")
|
||||
print(f" ✅ Updated! New status: {new_status}")
|
||||
else:
|
||||
print(f" ❌ Failed: {patch_resp.status_code} - {patch_resp.text}")
|
||||
print()
|
||||
|
||||
|
||||
def _check_class(http, class_id):
|
||||
print(f"=== Class: {class_id} ===")
|
||||
response = http.get(
|
||||
f"https://walletobjects.googleapis.com/walletobjects/v1/loyaltyClass/{class_id}",
|
||||
)
|
||||
if response.status_code == 200:
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
elif response.status_code == 404:
|
||||
print("Class not found.")
|
||||
else:
|
||||
print(f"Error: {response.status_code} - {response.text}")
|
||||
|
||||
|
||||
def _check_object(http, object_id):
|
||||
print(f"=== Object: {object_id} ===")
|
||||
response = http.get(
|
||||
f"https://walletobjects.googleapis.com/walletobjects/v1/loyaltyObject/{object_id}",
|
||||
)
|
||||
if response.status_code == 200:
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
elif response.status_code == 404:
|
||||
print("Object not found.")
|
||||
else:
|
||||
print(f"Error: {response.status_code} - {response.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2236,6 +2236,9 @@ class ArchitectureValidator:
|
||||
for i, line in enumerate(lines, 1):
|
||||
# Check for .all() queries that might not be scoped
|
||||
if ".query(" in line and ".all()" in line:
|
||||
# Skip if noqa comment present
|
||||
if "noqa: SVC-005" in line or "noqa: SVC005" in line:
|
||||
continue
|
||||
# Check context for store filtering
|
||||
context_start = max(0, i - 5)
|
||||
context_end = min(len(lines), i + 3)
|
||||
@@ -4112,7 +4115,7 @@ class ArchitectureValidator:
|
||||
)
|
||||
|
||||
def _validate_modules(self, target_path: Path):
|
||||
"""Validate module structure rules (MOD-001 to MOD-012)"""
|
||||
"""Validate module structure rules (MOD-001 to MOD-012, MOD-026)"""
|
||||
print("📦 Validating module structure...")
|
||||
|
||||
modules_path = target_path / "app" / "modules"
|
||||
@@ -4353,6 +4356,47 @@ class ArchitectureValidator:
|
||||
suggestion=f"Add translation files: {', '.join(missing_langs)}",
|
||||
)
|
||||
|
||||
# MOD-026: Check config.py uses env_file for .env loading
|
||||
config_file = module_dir / "config.py"
|
||||
if config_file.exists():
|
||||
config_content = config_file.read_text()
|
||||
if "model_config" in config_content:
|
||||
if '"env_file"' not in config_content and "'env_file'" not in config_content:
|
||||
# Find the model_config line number
|
||||
config_line = next(
|
||||
(i for i, line in enumerate(config_content.split("\n"), 1) if "model_config" in line),
|
||||
1,
|
||||
)
|
||||
self._add_violation(
|
||||
rule_id="MOD-026",
|
||||
rule_name="Module config must read from .env file",
|
||||
severity=Severity.ERROR,
|
||||
file_path=config_file,
|
||||
line_number=config_line,
|
||||
message=f"Module '{module_name}' config has model_config without env_file=\".env\" — "
|
||||
"settings won't load from .env when running outside Docker",
|
||||
context="model_config missing env_file",
|
||||
suggestion='Add env_file and extra to model_config: '
|
||||
'{"env_prefix": "...", "env_file": ".env", "extra": "ignore"}',
|
||||
)
|
||||
if '"extra"' not in config_content and "'extra'" not in config_content:
|
||||
config_line = next(
|
||||
(i for i, line in enumerate(config_content.split("\n"), 1) if "model_config" in line),
|
||||
1,
|
||||
)
|
||||
self._add_violation(
|
||||
rule_id="MOD-026",
|
||||
rule_name="Module config must ignore extra .env fields",
|
||||
severity=Severity.ERROR,
|
||||
file_path=config_file,
|
||||
line_number=config_line,
|
||||
message=f"Module '{module_name}' config has model_config without extra=\"ignore\" — "
|
||||
"will fail when .env contains vars for other modules",
|
||||
context="model_config missing extra=ignore",
|
||||
suggestion='Add extra to model_config: '
|
||||
'{"env_prefix": "...", "env_file": ".env", "extra": "ignore"}',
|
||||
)
|
||||
|
||||
# MOD-007: Validate definition paths match directory structure
|
||||
self._validate_module_definition_paths(module_dir, module_name, definition_file, definition_content)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user