Async Booking Workflow
Queue-based asynchronous flight booking workflow with real-time status updates and automatic ticket issuance.
Overview
Section titled “Overview”The async booking system provides:
- Immediate API response (<1s vs 60-90s synchronous)
- Queue-based processing with automatic retries
- Real-time notifications via FilamentPHP
- Instant ticket order support with 2-minute delay
- Status polling endpoint for frontend integration
Quick Start
Section titled “Quick Start”Dispatch Booking Job
Section titled “Dispatch Booking Job”use App\Jobs\AerticketCreateBookingJob;
AerticketCreateBookingJob::dispatch( fareId: 'fare-123', instantTicketOrder: true, // 2-minute delay for ticket issuance userId: auth()->id())->onQueue('aerticket-bookings');
return response()->json([ 'success' => true, 'message' => 'Booking is being created asynchronously', 'status' => 'processing',]);Poll Booking Status
Section titled “Poll Booking Status”GET /api/bookings/{ref}/retrieve-status
Response:{ "retrieve_status": "completed", "is_ready": true, "pnr_status": "TICKETED"}Architecture
Section titled “Architecture”Job Flow
Section titled “Job Flow”User Request │ ▼AerticketCreateBookingJob (aerticket-bookings queue) │ ├── Success ──► FlightBooking created │ │ │ ▼ │ AerticketRetrieveBookingJob (2-min delay if instant ticket) │ │ │ ▼ │ PNR status updated │ └── Failure ──► Retry (3 attempts) ──► Failed notificationQueue Configuration
Section titled “Queue Configuration”| Queue | Purpose | Priority |
|---|---|---|
aerticket-tickets |
Ticket issuance | Highest |
aerticket-bookings |
Booking creation | High |
aerticket-fastlane |
Fastlane ticketing | High |
default |
General tasks | Normal |
Job Configuration
Section titled “Job Configuration”AerticketCreateBookingJob
Section titled “AerticketCreateBookingJob”public int $tries = 3;public int $timeout = 330;public array $backoff = [60, 120, 300]; // 1m, 2m, 5mProperties:
fareId- The fare identifier from search/verifyinstantTicketOrder- Whether to auto-issue ticketsuserId- User who initiated the bookingpassengerIds- Array of passenger IDs for the bookingfareRulesData- Fare rules data from verification steptravelDocumentIsRequired- Whether travel documents are required
Behavior:
- Takes a cache lock on
aerticket-booking:{fareId}, held for the whole run - Inside that lock, skips the run if the fare already has a reservation from the last 24h that still stands
- Creates booking via AerTicket API
- Stores FlightBooking record, including the
fare_idthe guard reads back - Dispatches retrieve job (with delay if instant ticket)
- Sends success/failure notification
Why both a lock and a lookup: $timeout (330s) is above the connection’s
retry_after (240s), so the queue can hand the same payload to a second worker
while the first is still booking — the lock stops that pair from placing two
PNRs. It does nothing for sequential re-execution, which this worker makes
routine (--max-time=3600 --memory=384, plus a SIGALRM at $timeout): the
process can die mid-booking and the payload is picked up again after the lock
has expired. The recent-fare lookup covers that second case, and is bounded to
24h because Aerticket can re-emit a fareId for the same fare-family on a later
search — an unbounded check would refuse a legitimate sale. For the same reason
it ignores reservations that no longer stand: an operator who voids a booking and
rebooks the same fare must not be turned away.
What counts as “still stands” is deliberately narrow. A ticket that is
issued or ticketing holds the fare whatever the booking status says, because
the two columns are independent: voiding only ever writes ticket_status, and
the Cancel Booking action lets an operator cancel straight over issued tickets —
it warns and asks them to void first, but does not require it. Treating
booking_status = 'cancelled' as released on its own would therefore let a
re-delivery place a second PNR against a ticket the airline still holds. With no
live ticket, the fare stays held unless it was voided or cancelled; any status
the guard does not recognise counts as held, since erring that way costs one
logged, recoverable refusal while erring the other way books a second PNR.
AerticketRetrieveBookingJob
Section titled “AerticketRetrieveBookingJob”public int $tries = 3;public int $timeout = 60;public array $backoff = [60, 120, 300]; // 1m, 2m, 5mDelay Logic:
- Instant ticket order: 2-minute delay before retrieve
- Manual ticketing: Immediate retrieve
Purpose:
- Updates PNR status from AerTicket
- Syncs ticket numbers if issued
- Updates
retrieve_statusfor polling
Status Tracking
Section titled “Status Tracking”Booking Status Values
Section titled “Booking Status Values”| Status | Description |
|---|---|
pending |
Booking in progress |
confirmed |
PNR created successfully |
ticketed |
Tickets issued |
cancelled |
Booking cancelled |
failed |
Booking failed |
Retrieve Status Values
Section titled “Retrieve Status Values”| Status | Description |
|---|---|
pending |
Retrieve not started |
in_progress |
Retrieve job running |
completed |
Retrieve successful |
failed |
Retrieve failed |
Frontend Integration
Section titled “Frontend Integration”JavaScript Polling
Section titled “JavaScript Polling”async function pollBookingStatus(bookingRef) { const maxAttempts = 30; const interval = 5000; // 5 seconds
for (let attempt = 0; attempt < maxAttempts; attempt++) { const response = await fetch(`/api/bookings/${bookingRef}/retrieve-status`); const data = await response.json();
if (data.is_ready) { return data; }
await new Promise(resolve => setTimeout(resolve, interval)); }
throw new Error('Booking status check timed out');}
// Usagetry { const status = await pollBookingStatus('ABC123'); console.log('PNR Status:', status.pnr_status);} catch (error) { console.error('Failed to get booking status');}React Integration
Section titled “React Integration”function useBookingStatus(bookingRef: string) { const [status, setStatus] = useState<BookingStatus | null>(null); const [loading, setLoading] = useState(true);
useEffect(() => { const poll = async () => { try { const response = await fetch(`/api/bookings/${bookingRef}/retrieve-status`); const data = await response.json();
setStatus(data);
if (!data.is_ready) { setTimeout(poll, 5000); } else { setLoading(false); } } catch (error) { setLoading(false); } };
poll(); }, [bookingRef]);
return { status, loading };}Error Handling
Section titled “Error Handling”Job Failures
Section titled “Job Failures”public function failed(\Throwable $exception): void{ Log::error('Booking creation failed', [ 'fare_id' => $this->fareId, 'user_id' => $this->userId, 'error' => $exception->getMessage(), ]);
Notification::make() ->title('Booking Failed') ->body($exception->getMessage()) ->danger() ->sendToDatabase(User::find($this->userId));}Retry Logic
Section titled “Retry Logic”Jobs retry with exponential backoff:
- First retry: 1 minute
- Second retry: 2 minutes
- Third retry: 5 minutes
After all retries exhausted, job moves to failed_jobs table.
Monitoring
Section titled “Monitoring”Queue Status
Section titled “Queue Status”# Check queue status./vendor/bin/sail artisan queue:monitor
# View failed jobs./vendor/bin/sail artisan queue:failed
# Retry failed job./vendor/bin/sail artisan queue:retry {job_id}# Real-time logs./vendor/bin/sail artisan pail --filter="Aerticket"
# View booking-specific logs./vendor/bin/sail artisan pail --filter="AerticketCreateBookingJob"Database Schema
Section titled “Database Schema”flight_bookings Table
Section titled “flight_bookings Table”| Column | Type | Description |
|---|---|---|
booking_reference |
VARCHAR | PNR locator |
booking_status |
ENUM | confirmed, pending, etc. |
retrieve_status |
ENUM | pending, completed, etc. |
pnr_status |
VARCHAR | AerTicket PNR status |
ticket_status |
ENUM | not_issued, issued, voided |
total_amount |
DECIMAL | Total booking amount |
travel_date |
DATE | Departure date |
return_date |
DATE | Return date (nullable) |
Troubleshooting
Section titled “Troubleshooting”Booking Stuck in Pending
Section titled “Booking Stuck in Pending”- Check queue worker is running:
docker ps | grep queue- Check for failed jobs:
./vendor/bin/sail artisan queue:failed- View job exception:
./vendor/bin/sail artisan tinker>>> DB::table('failed_jobs')->latest()->first()->exception;Retrieve Not Completing
Section titled “Retrieve Not Completing”- Check retrieve job was dispatched:
./vendor/bin/sail artisan pail --filter="AerticketRetrieveBookingJob"- Manually trigger retrieve:
use App\Jobs\AerticketRetrieveBookingJob;
AerticketRetrieveBookingJob::dispatch( bookingReference: 'ABC123', bookingId: 123)->onQueue('aerticket-bookings');Related Documentation
Section titled “Related Documentation”- AerTicket Integration - Complete API reference
- Queue System - Queue infrastructure