Some checks failed
- 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>
119 lines
3.6 KiB
Python
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()
|