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:
| Parameter | Type | Required | Description |
|---|---|---|---|
store_id | UUID | Yes | Store ID |
start_date | date | No | Start date (ISO format, defaults to today) |
num_days | integer | No | Days 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
date | date | No | Filter 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
range | string | today | Date 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
| Method | Description |
|---|---|
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
PENDINGtoEXPIRED - Releases reserved inventory
- Does NOT trigger refunds (customer didn't cancel)
Celery Task: Can be scheduled via beat for automatic expiration
Related Documentation
- Services - Transaction and fulfillment services
- Models - PickupSchedule and Fulfillment models
- Web Commerce Signals - Reservation cleanup
- Stores Views - Store pickup configuration