Files
orion/app/modules/loyalty/tasks/wallet_sync.py
Samir Boulahtit b6047f5b7d
Some checks failed
CI / ruff (push) Successful in 12s
CI / validate (push) Successful in 26s
CI / dependency-scanning (push) Successful in 30s
CI / pytest (push) Failing after 3h9m5s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
feat(loyalty): Google Wallet production readiness — 10 hardening items
- Fix rate limiter to extract real client IP and handle sync/async endpoints
- Rate-limit public enrollment (10/min) and program info (30/min) endpoints
- Add 409 Conflict to non-retryable status codes in retry decorator
- Cache private key in get_save_url() to avoid re-reading JSON per call
- Make update_class() return bool success status with error-level logging
- Move Google Wallet config from core to loyalty module config
- Document time.sleep() safety in retry decorator (threadpool execution)
- Add per-card retry (1 retry, 2s delay) to wallet sync task
- Add logo URL reachability check (HEAD request) to validate_config()
- Add 26 comprehensive unit tests for GoogleWalletService

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 00:18:13 +01:00

119 lines
3.6 KiB
Python

# app/modules/loyalty/tasks/wallet_sync.py
"""
Wallet synchronization task.
Handles syncing loyalty card data to Google Wallet and Apple Wallet
for cards that may have missed real-time updates.
"""
import logging
from celery import shared_task
logger = logging.getLogger(__name__)
@shared_task(name="loyalty.sync_wallet_passes")
def sync_wallet_passes() -> dict:
"""
Sync wallet passes for cards that may be out of sync.
This catches any cards that missed real-time updates due to
errors or network issues.
Returns:
Summary of synced passes
"""
from datetime import UTC, datetime, timedelta
from app.core.database import SessionLocal
from app.modules.loyalty.models import LoyaltyCard, LoyaltyTransaction
from app.modules.loyalty.services import wallet_service
db = SessionLocal()
try:
# Find cards with transactions in the last hour that have wallet IDs
one_hour_ago = datetime.now(UTC) - timedelta(hours=1)
# Get card IDs with recent transactions
recent_tx_card_ids = (
db.query(LoyaltyTransaction.card_id)
.filter(LoyaltyTransaction.transaction_at >= one_hour_ago)
.distinct()
.all()
)
card_ids = [row[0] for row in recent_tx_card_ids]
if not card_ids:
logger.info("No cards with recent transactions to sync")
return {
"status": "success",
"cards_checked": 0,
"google_synced": 0,
"apple_synced": 0,
}
# Get cards with wallet integrations
cards = (
db.query(LoyaltyCard)
.filter(
LoyaltyCard.id.in_(card_ids),
(LoyaltyCard.google_object_id.isnot(None))
| (LoyaltyCard.apple_serial_number.isnot(None)),
)
.all()
)
google_synced = 0
apple_synced = 0
failed_card_ids = []
for card in cards:
synced = False
for attempt in range(2): # 1 retry
try:
results = wallet_service.sync_card_to_wallets(db, card)
if results.get("google_wallet"):
google_synced += 1
if results.get("apple_wallet"):
apple_synced += 1
synced = True
break
except Exception as e:
if attempt == 0:
logger.warning(
f"Failed to sync card {card.id} (attempt 1/2), "
f"retrying in 2s: {e}"
)
import time
time.sleep(2)
else:
logger.error(
f"Failed to sync card {card.id} after 2 attempts: {e}"
)
if not synced:
failed_card_ids.append(card.id)
logger.info(
f"Wallet sync complete: {len(cards)} cards checked, "
f"{google_synced} Google, {apple_synced} Apple, "
f"{len(failed_card_ids)} failed"
)
return {
"status": "success",
"cards_checked": len(cards),
"google_synced": google_synced,
"apple_synced": apple_synced,
"failed_card_ids": failed_card_ids,
}
except Exception as e:
logger.error(f"Wallet sync task failed: {e}")
return {
"status": "error",
"error": str(e),
}
finally:
db.close()