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

TaskSchedulePurposeKey Service Call
process_end_of_day_pickupsHourlyExpire pending/ready pickups at store closing timePickupSchedule.mark_expired()
expire_stale_pickupsDailySafety-net expiry for pickups >24 h past scheduled timePickupSchedule.mark_expired()
cleanup_stale_partial_paymentsOn demandScan PARTIALLY_PAID transactions past threshold, dispatch a per-row void sub-taskvoid_stale_partial_payment_for_transaction.delay()
void_stale_partial_payment_for_transactionPer rowVoid a single stale partially-paid transaction (skips if already resolved)TransactionService.void_transaction()
cleanup_stale_3ds_paymentsHourlyScan PAYMENT_REQUIRES_ACTION transactions past a 1-hour threshold, dispatch a per-row void sub-taskvoid_stale_3ds_payment_for_transaction.delay()
void_stale_3ds_payment_for_transactionPer rowVoid a single stale 3D Secure hold (skips if already resolved)InventoryAdjustmentService
send_order_ready_emailOn demandSend "order ready" notification to customerOrderEmailService.send_order_ready_notification()
send_order_confirmation_emailOn demandSend order confirmation email (pickup or shipping)OrderEmailService.send_order_confirmation()
clear_cart_after_checkoutPost-commitEmpty the source Cart after checkout completesnone, direct model update
commit_checkout_reservationsPost-commitCommit inventory reservations for a completed checkout sessionReservationService.commit_reservations()
award_loyalty_points_for_transactionPost-commitAward loyalty points for a completed transactionLoyaltyLedger.objects.create()
increment_promo_usage_for_transactionPost-commitIncrement usage counters for applied promo codespromotions app
track_campaign_conversion_for_transactionPost-commitRecord a campaign conversion for applied promo codespromotions app
update_inventory_analytics_for_transactionPost-commitUpdate per-InventoryLevel revenue and transaction-count countersInventoryAnalyticsLedger.objects.create()
update_ab_test_conversions_for_transactionPost-commitRecord an A/B test conversion for the transaction's assigned variantABTest.record_conversion()
rollup_daily_revenueDaily at 00:05Recompute previous day's DailyRevenueSummary for all storessee Analytics Pre-Aggregation
refresh_today_revenue_summaryEvery 15 minRecompute today's DailyRevenueSummary for all storessee Analytics Pre-Aggregation
reconcile_voided_inventoryScheduled, gatedRetry inventory operations that failed during a void, or raise an alert audit log for human triageTransactionAuditLog

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 by mark_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:

ParamTypeDescription
pickup_schedule_idstrUUID 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:

ParamTypeRequiredDescription
transaction_idstrYesUUID of the SaleTransaction
fulfillment_idstrYesUUID of the Fulfillment
pickup_schedule_idstrNoUUID 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.


  • Models: SaleTransaction, PickupSchedule, Fulfillment
  • Services: TransactionService.void_transaction(), PickupService
  • Signals: Transaction lifecycle events

Was this page helpful?