Transactions - Pickup System

Location: api/nextango/apps/transactions/services/pickup_service.py, viewsets/pickup_viewset.py Last Updated: 2026-07-08

Overview

The Pickup System provides comprehensive pickup scheduling and management for web commerce orders. It handles the complete pickup lifecycle from scheduling through customer handoff, with support for pay-at-pickup, partial payments, and order modification.


Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Web Storefront                          │
│        (Checkout with fulfillment_method='pickup')          │
└──────────────────────┬──────────────────────────────────────┘

┌──────────────────────▼──────────────────────────────────────┐
│                 PickupViewSet                               │
│                                                             │
│   GET  /transactions/pickup/available-slots/                             │
│   POST /transactions/pickup/schedule/                                    │
│   POST /transactions/pickup/{id}/mark-ready/                             │
│   POST /transactions/pickup/{id}/complete/                               │
│   POST /transactions/pickup/{id}/cancel/                                 │
│   GET  /transactions/pickup/{id}/for-modification/                       │
│   GET  /transactions/pickup/store/{store_id}/pending/                    │
│   GET  /transactions/pickup/store/{store_id}/today/                      │
└──────────────────────┬──────────────────────────────────────┘

┌──────────────────────▼──────────────────────────────────────┐
│                 PickupService                               │
│                                                             │
│   • generate_available_time_slots()                         │
│   • create_pickup_schedule()                                │
│   • mark_ready_for_pickup()                                 │
│   • complete_pickup()                                       │
│   • cancel_pickup_order()                                   │
│   • get_pending_pickups_for_store()                         │
│   • get_pickups_for_store()                                 │
│   • get_pickup_for_modification()                           │
└──────────────────────┬──────────────────────────────────────┘

┌──────────────────────▼──────────────────────────────────────┐
│              PickupSchedule Model                           │
│                                                             │
│   Status: PENDING → READY → PICKED_UP                       │
│           PENDING → EXPIRED                                 │
│           PENDING/READY → CANCELLED                         │
└─────────────────────────────────────────────────────────────┘

Pickup Lifecycle

┌─────────────────────────────────────────────────────────────┐
│                    Order Placed                             │
│              (fulfillmentMethod='pickup')                   │
└──────────────────────┬──────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                     PENDING                                 │
│                                                             │
│   • Order waiting to be prepared                            │
│   • Inventory is RESERVED (not committed)                   │
│   • Customer can cancel                                     │
│   • Staff can mark as ready                                 │
└──────────────────────┬──────────────────────────────────────┘

          ┌────────────┼────────────┐
          │            │            │
          ▼            ▼            ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│    READY    │ │   EXPIRED   │ │  CANCELLED  │
│             │ │             │ │             │
│ • Prepared  │ │ • Past      │ │ • Customer  │
│ • Waiting   │ │   scheduled │ │   requested │
│   for       │ │   time      │ │ • Inventory │
│   customer  │ │ • Inventory │ │   released  │
│ • Email     │ │   released  │ │             │
│   sent      │ │             │ │             │
└──────┬──────┘ └─────────────┘ └─────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    PICKED_UP                                │
│                                                             │
│   • Customer received order                                 │
│   • Inventory COMMITTED (deducted)                          │
│   • Transaction completed                                   │
│   • Payment balance collected (if pay-at-pickup)            │
└─────────────────────────────────────────────────────────────┘

API Reference

PickupViewSet

Source: viewsets/pickup_viewset.py

Authentication: WebSessionAuthentication, CustomTokenAuthentication

Permission: AllowAny (session-based authentication)


GET /transactions/pickup/available-slots/

Get available pickup time slots for a store.

Query Parameters:

ParameterTypeRequiredDescription
store_idUUIDYesStore ID
start_datedateNoStart date (ISO format, defaults to today)
num_daysintegerNoDays to generate slots for (default: 7)

Response:

{
    "success": true,
    "slots": [
        {
            "datetime": "2024-01-15T10:00:00-05:00",
            "display": "10:00 AM - 11:00 AM",
            "date": "2024-01-15",
            "day": "Monday",
            "available": true
        }
    ],
    "store_timezone": "America/New_York",
    "interval_minutes": 60
}

Slot Generation Logic:

  • Uses store's get_pickup_hours_for_day() method
  • Forces 60-minute interval slots
  • Starts at first full hour >= opening time
  • Only includes future time slots
  • Respects store timezone

POST /transactions/pickup/schedule/

Create pickup schedule for a fulfillment.

Request Body:

{
    "fulfillment_id": "uuid",
    "scheduled_time": "2024-01-15T10:00:00-05:00",
    "customer_notes": "Please include extra napkins"
}

Response:

{
    "success": true,
    "pickup_schedule_id": "uuid",
    "scheduled_time": "2024-01-15T10:00:00-05:00",
    "status": "pending"
}

Validation:

  • Scheduled time must be in the future
  • Fulfillment must have fulfillmentMethod='pickup'

POST /transactions/pickup/{id}/mark-ready/

Mark order as ready for customer pickup (staff endpoint).

Response:

{
    "success": true,
    "status": "ready",
    "ready_at": "2024-01-15T14:30:00-05:00",
    "prepared_by": "Jane Smith"
}

Side Effects:

  • Updates PickupSchedule.ready_at
  • Updates Fulfillment.status
  • Triggers async email notification (if enabled via ENABLE_ORDER_READY_EMAIL=true)

POST /transactions/pickup/{id}/complete/

Complete pickup - customer received their order.

Response:

{
    "success": true,
    "status": "picked_up",
    "picked_up_at": "2024-01-15T15:00:00-05:00",
    "handed_off_by": "John Doe"
}

Side Effects:

  • Commits inventory (reserved → committed)
  • Completes transaction
  • Collects remaining payment balance (if pay-at-pickup)

POST /transactions/pickup/{id}/cancel/

Cancel a pickup order.

Request Body:

{
    "cancellation_reason": "Customer changed mind"
}

Response:

{
    "success": true,
    "status": "cancelled",
    "pickup_id": "uuid",
    "transaction_id": "TXN-123",
    "transaction_status": "cancelled",
    "cancellation_reason": "Customer changed mind",
    "was_refunded": false
}

Behavior:

  • Only available for pickup fulfillment method
  • Releases reserved inventory
  • Triggers refund if payment was made

GET /transactions/pickup/{id}/for-modification/

Get pickup details in cart-compatible format for order modification.

Response:

{
    "success": true,
    "pickup": {
        "id": "uuid",
        "status": "ready",
        "scheduled_time": "2024-01-15T10:00:00-05:00",
        "customer_notes": ""
    },
    "transaction": {
        "transactionId": "TXN-123",
        "totalAmount": "49.99",
        "totalPaid": "25.00",
        "paymentBalance": "24.99",
        "isFullyPaid": false,
        "status": "pending_pickup"
    },
    "lineItems": [
        {
            "id": "uuid",
            "productVariant": "variant-uuid",
            "quantity": 2,
            "unitPrice": "24.99",
            "totalPrice": "49.98",
            "sku": "SKU-001",
            "name": "Product Name - Variant",
            "productName": "Product Name",
            "variantName": "Variant",
            "imageUrls": ["https://..."],
            "isOriginal": true
        }
    ],
    "verification": {
        "customerEmail": "[email protected]",
        "customerPhone": "555-1234",
        "customerName": "John Doe"
    },
    "appliedPromo": {
        "code": "SAVE10",
        "discountAmount": "5.00",
        "discountPercentage": 10.0,
        "description": "10% off discount"
    }
}

Allowed Statuses: PENDING, READY


GET /transactions/pickup/store/{store_id}/pending/

Get pending pickups for a store.

Query Parameters:

ParameterTypeRequiredDescription
datedateNoFilter by scheduled date

Response:

{
    "success": true,
    "pickups": [
        {
            "id": "uuid",
            "scheduled_time": "2024-01-15T10:00:00-05:00",
            "status": "pending",
            "customer_notes": "",
            "transaction_id": "TXN-123",
            "customer_email": "[email protected]",
            "customer_phone": "555-1234",
            "total_amount": "49.99",
            "amount_paid": "25.00",
            "payment_balance": "24.99",
            "is_fully_paid": false
        }
    ],
    "count": 1
}

GET /transactions/pickup/store/{store_id}/today/

Get pickups for a store, grouped by status.

Query Parameters:

ParameterTypeDefaultDescription
rangestringtodayDate range: today, 72h, week, month, all

Response:

{
    "success": true,
    "pending": [...],
    "ready": [...],
    "picked_up": [...],
    "expired": [...],
    "cancelled": [...],
    "summary": {
        "pending_count": 5,
        "ready_count": 2,
        "picked_up_count": 10,
        "expired_count": 1,
        "cancelled_count": 0
    },
    "date_range": "today"
}

PickupService

Source: services/pickup_service.py

Static service class for pickup business logic.

Key Methods

MethodDescription
generate_available_time_slots()Generate time slots based on store hours
create_pickup_schedule()Create pickup for fulfillment
mark_ready_for_pickup()Mark order as prepared
complete_pickup()Complete handoff, commit inventory
cancel_pickup_order()Cancel and release inventory
get_pending_pickups_for_store()Query pending pickups
get_pickups_for_store()Query pickups by date range
get_pickup_for_modification()Get cart-format data for modification

Pay-at-Pickup Flow

The pickup system supports partial payments and pay-at-pickup:

┌─────────────────────────────────────────────────────────────┐
│              Online Checkout                                │
│                                                             │
│   payment_timing: 'at_pickup'                               │
│   Total: $100.00                                            │
│   Deposit: $0.00 (or optional deposit)                      │
└──────────────────────┬──────────────────────────────────────┘

┌──────────────────────▼──────────────────────────────────────┐
│              Pickup Created                                 │
│                                                             │
│   totalAmount: $100.00                                      │
│   totalPaid: $0.00                                          │
│   paymentBalance: $100.00                                   │
│   isFullyPaid: false                                        │
└──────────────────────┬──────────────────────────────────────┘

┌──────────────────────▼──────────────────────────────────────┐
│              POS: Complete Pickup                           │
│                                                             │
│   1. Load pickup via /transactions/pickup/{id}/for-modification/         │
│   2. Collect remaining $100.00 via POS payment              │
│   3. Call /transactions/pickup/{id}/complete/                            │
│   4. Inventory committed, transaction completed             │
└─────────────────────────────────────────────────────────────┘

Inventory Integration

Reservation Flow

# On checkout completion (pickup method)
# Inventory is RESERVED, not committed

InventoryLevel.quantity_reserved += item.quantity
# quantity_on_hand unchanged
# quantity_available = quantity_on_hand - quantity_reserved

Completion Flow

# On pickup completion
# Inventory is COMMITTED

InventoryLevel.quantity_reserved -= item.quantity
InventoryLevel.quantity_on_hand -= item.quantity
# Reserved becomes committed

Cancellation/Expiration Flow

# On cancel or expire
# Inventory is RELEASED

InventoryLevel.quantity_reserved -= item.quantity
# quantity_on_hand unchanged
# Inventory returns to available

Email Notifications

Order ready notifications are sent asynchronously when enabled:

# Environment variable
ENABLE_ORDER_READY_EMAIL=true

# On mark_ready_for_pickup()
if os.environ.get('ENABLE_ORDER_READY_EMAIL', 'false').lower() == 'true':
    send_order_ready_email.delay(str(pickup_schedule_id))

Celery Task: transactions.tasks.send_order_ready_email


Expiration

Pickups can expire when the scheduled time passes without completion.

Management Command:

python manage.py expire_pickup

Behavior:

  • Changes status from PENDING to EXPIRED
  • Releases reserved inventory
  • Does NOT trigger refunds (customer didn't cancel)

Celery Task: Can be scheduled via beat for automatic expiration


Was this page helpful?