Transactions - Celery Tasks
Location: api/nextango/apps/transactions/tasks.py
Last Updated: 2026-07-08
Overview
Background tasks for the transactions domain. They fall into three groups: end-of-day pickup lifecycle management (auto-expiry of uncollected orders), abandoned-payment cleanup (stale partial payments and stale 3D Secure holds), and post-completion deferral work (loyalty points, promo usage, campaign conversion, inventory analytics, A/B test conversions, checkout-session cleanup) dispatched via transaction.on_commit from TransactionService.complete_transaction() and the checkout flow so the customer-perceived completion boundary is not blocked on non-critical writes. A separate revenue/inventory rollup group maintains the daily pre-aggregation tables (see Analytics Pre-Aggregation). Pickup tasks are timezone-aware and respect per-store operating hours.
Task Summary
| Task | Schedule | Purpose | Key Service Call |
|---|---|---|---|
process_end_of_day_pickups | Hourly | Expire pending/ready pickups at store closing time | PickupSchedule.mark_expired() |
expire_stale_pickups | Daily | Safety-net expiry for pickups >24 h past scheduled time | PickupSchedule.mark_expired() |
cleanup_stale_partial_payments | On demand | Scan PARTIALLY_PAID transactions past threshold, dispatch a per-row void sub-task | void_stale_partial_payment_for_transaction.delay() |
void_stale_partial_payment_for_transaction | Per row | Void a single stale partially-paid transaction (skips if already resolved) | TransactionService.void_transaction() |
cleanup_stale_3ds_payments | Hourly | Scan PAYMENT_REQUIRES_ACTION transactions past a 1-hour threshold, dispatch a per-row void sub-task | void_stale_3ds_payment_for_transaction.delay() |
void_stale_3ds_payment_for_transaction | Per row | Void a single stale 3D Secure hold (skips if already resolved) | InventoryAdjustmentService |
send_order_ready_email | On demand | Send "order ready" notification to customer | OrderEmailService.send_order_ready_notification() |
send_order_confirmation_email | On demand | Send order confirmation email (pickup or shipping) | OrderEmailService.send_order_confirmation() |
clear_cart_after_checkout | Post-commit | Empty the source Cart after checkout completes | none, direct model update |
commit_checkout_reservations | Post-commit | Commit inventory reservations for a completed checkout session | ReservationService.commit_reservations() |
award_loyalty_points_for_transaction | Post-commit | Award loyalty points for a completed transaction | LoyaltyLedger.objects.create() |
increment_promo_usage_for_transaction | Post-commit | Increment usage counters for applied promo codes | promotions app |
track_campaign_conversion_for_transaction | Post-commit | Record a campaign conversion for applied promo codes | promotions app |
update_inventory_analytics_for_transaction | Post-commit | Update per-InventoryLevel revenue and transaction-count counters | InventoryAnalyticsLedger.objects.create() |
update_ab_test_conversions_for_transaction | Post-commit | Record an A/B test conversion for the transaction's assigned variant | ABTest.record_conversion() |
rollup_daily_revenue | Daily at 00:05 | Recompute previous day's DailyRevenueSummary for all stores | see Analytics Pre-Aggregation |
refresh_today_revenue_summary | Every 15 min | Recompute today's DailyRevenueSummary for all stores | see Analytics Pre-Aggregation |
reconcile_voided_inventory | Scheduled, gated | Retry inventory operations that failed during a void, or raise an alert audit log for human triage | TransactionAuditLog |
Task Reference
process_end_of_day_pickups
Source: tasks.py:49-184
Schedule: Hourly
Purpose: Iterates every active store that has pickup enabled. For each store, checks whether the current local time matches the store's configured closing hour (from Store.get_pickup_hours_for_day()). If it does, finds all PickupSchedule records in PENDING or READY status scheduled for today and expires them:
- Paid transactions (
status='completed'): Always expired; refund is triggered bymark_expired(). - Unpaid transactions past scheduled time: Expired and inventory released.
- Unpaid transactions not yet past scheduled time: Left untouched.
Signature:
@shared_task(name='transactions.process_end_of_day_pickups')
def process_end_of_day_pickups():
...
Returns:
{
'processed': int,
'errors': int,
'status': 'success' | 'partial_success',
}
Retry Policy: None configured.
Timezone handling: Uses store.timezone (falls back to 'UTC') to convert UTC now into the store's local time before comparing with closing hour.
expire_stale_pickups
Source: tasks.py:188-252
Schedule: Daily
Purpose: Safety net for pickups that the hourly task may have missed. Finds all PENDING or READY pickups whose scheduled_time is more than hours_past_scheduled (default: 24) hours in the past and calls mark_expired() on each. Warns separately for paid pickups (where a refund was issued).
Signature:
@shared_task(name='transactions.expire_stale_pickups')
def expire_stale_pickups(hours_past_scheduled: int = 24):
...
Returns:
{
'expired': int,
'errors': int,
'cutoff_time': str, # ISO 8601 timestamp used as the cutoff
}
Retry Policy: None configured.
cleanup_stale_partial_payments
Source: tasks.py:685-716
Schedule: On demand (also callable from management commands or beat schedule)
Purpose: In a POS environment, split payments complete within minutes. This task scans for SaleTransaction rows still in PARTIALLY_PAID status for more than hours_stale hours (default: 24) and dispatches one void_stale_partial_payment_for_transaction sub-task per row; that sub-task performs the void via TransactionService.void_transaction() with an automated reason string (tasks.py:749-773). Gated by the CLEANUP_STALE_PAYMENTS_WORKER_ENABLED setting; each run is capped by CLEANUP_STALE_MAX_ROWS_PER_RUN (default 1000). Sub-tasks are idempotent: a transaction no longer in PARTIALLY_PAID is skipped.
Signature:
@shared_task(name='transactions.cleanup_stale_partial_payments')
def cleanup_stale_partial_payments(hours_stale: int = 24):
...
Returns:
{
'status': 'ok',
'rows_dispatched': int,
}
Returns {'status': 'gated_off'} when disabled by settings.
Retry Policy: None on the scan task; the per-row void sub-task retries up to 3 times.
send_order_ready_email
Source: tasks.py:818-862
Schedule: On demand, dispatched by pickup_service.py when staff marks a pickup as ready.
Purpose: Sends an "Order Ready for Pickup" email to the customer. On success, sets PickupSchedule.customer_notified_at to the current timestamp.
Signature:
@shared_task(name='transactions.send_order_ready_email')
def send_order_ready_email(pickup_schedule_id: str):
...
Parameters:
| Param | Type | Description |
|---|---|---|
pickup_schedule_id | str | UUID of the PickupSchedule |
Returns:
{'success': True, 'pickup_id': str}
# or on failure:
{'success': False, 'error': 'pickup_not_found' | 'email_send_failed' | str}
Retry Policy: None configured.
send_order_confirmation_email
Source: tasks.py:863-989
Schedule: On demand, dispatched after a successful checkout.
Purpose: Sends an order confirmation email. Dispatches the pickup-specific template for fulfillmentMethod='pickup' orders, and the standard confirmation template for shipping orders.
Signature:
@shared_task(name='transactions.send_order_confirmation_email')
def send_order_confirmation_email(
transaction_id: str,
fulfillment_id: str,
pickup_schedule_id: str = None,
):
...
Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
transaction_id | str | Yes | UUID of the SaleTransaction |
fulfillment_id | str | Yes | UUID of the Fulfillment |
pickup_schedule_id | str | No | UUID of PickupSchedule (pickup orders only) |
Returns:
{'success': True, 'transaction_id': str}
# or on failure:
{'success': False, 'error': 'transaction_not_found' | 'fulfillment_not_found' | 'email_send_failed' | str}
Retry Policy: None configured.