# Runbook: Point Expiration Task ## Overview The `loyalty.expire_points` Celery task runs daily at 02:00 (configured in `definition.py`). It processes all active programs with `points_expiration_days > 0`. ## What it does 1. **Warning emails** (14 days before expiry): finds cards whose last activity is past the warning threshold but not yet past the full expiration threshold. Sends `loyalty_points_expiring` email. Tracked via `last_expiration_warning_at` to prevent duplicates. 2. **Point expiration**: finds cards with `points_balance > 0` and `last_activity_at` older than `points_expiration_days`. Zeros the balance, creates `POINTS_EXPIRED` transaction, sends `loyalty_points_expired` email. Processing is **chunked** (500 cards per batch with `FOR UPDATE SKIP LOCKED`) to avoid long-held row locks. ## Manual execution ```bash # Run directly (outside Celery) python -m app.modules.loyalty.tasks.point_expiration # Via Celery celery -A app.core.celery_config call loyalty.expire_points ``` ## Partial failure handling - Each chunk commits independently — if the task crashes mid-run, already-processed chunks are committed - `SKIP LOCKED` means concurrent workers won't block on the same rows - Notification failures are caught per-card and logged but don't stop the expiration ## Re-run for a specific merchant Not currently supported via CLI. To expire points for a single merchant: ```python from app.core.database import SessionLocal from app.modules.loyalty.services.program_service import program_service from app.modules.loyalty.tasks.point_expiration import _process_program db = SessionLocal() program = program_service.get_program_by_merchant(db, merchant_id=2) cards, points, warnings = _process_program(db, program) print(f"Expired {cards} cards, {points} points, {warnings} warnings") db.close() ``` ## Manual point restore If points were expired incorrectly, use the admin API: ``` POST /api/v1/admin/loyalty/cards/{card_id}/restore-points { "points": 500, "reason": "Incorrectly expired — customer was active" } ``` This creates an `ADMIN_ADJUSTMENT` transaction and restores the balance. ## Monitoring - Alert if `loyalty.expire_points` hasn't succeeded in 26 hours - Check Celery flower for task status and execution time - Expected runtime: < 1 minute for < 10k cards, scales linearly with chunk count