bookingService = new BookingService(); $this->paymentService = new PaymentService(); } /** * Register REST API routes */ public function register_routes(): void { // ===================== // BOOKINGS ROUTES // ===================== // List bookings — view cap. register_rest_route($this->namespace, '/bookings', [ 'methods' => 'GET', 'callback' => [$this, 'getBookings'], 'permission_callback' => [$this, 'checkCanView'], ]); // Get single booking — view cap. register_rest_route($this->namespace, '/bookings/(?P\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'getBooking'], 'permission_callback' => [$this, 'checkCanView'], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ], ], ]); // Create booking — create cap. register_rest_route($this->namespace, '/bookings', [ 'methods' => 'POST', 'callback' => [$this, 'createBooking'], 'permission_callback' => [$this, 'checkCanCreate'], ]); // Update booking — edit cap. register_rest_route($this->namespace, '/bookings/(?P\d+)', [ 'methods' => 'PUT', 'callback' => [$this, 'updateBooking'], 'permission_callback' => [$this, 'checkCanEdit'], ]); // Delete booking — critical-sensitivity delete cap. Only // Owner role gets this by default. register_rest_route($this->namespace, '/bookings/(?P\d+)', [ 'methods' => 'DELETE', 'callback' => [$this, 'deleteBooking'], 'permission_callback' => [$this, 'checkCanDelete'], ]); // Update booking status — dedicated change-status cap so // Front Desk (who has this cap but NOT edit) can flip // confirmed → checked-in without being able to mutate other // fields. register_rest_route($this->namespace, '/bookings/(?P\d+)/status', [ 'methods' => 'PUT', 'callback' => [$this, 'updateBookingStatus'], 'permission_callback' => [$this, 'checkCanChangeStatus'], ]); // Get booking statistics — view cap (aggregates only). register_rest_route($this->namespace, '/bookings/stats', [ 'methods' => 'GET', 'callback' => [$this, 'getBookingStats'], 'permission_callback' => [$this, 'checkCanView'], ]); // Send booking email — edit cap. Sending a transactional // re-confirmation is a write-side operation against the // customer's record. register_rest_route($this->namespace, '/bookings/(?P\d+)/send-email', [ 'methods' => 'POST', 'callback' => [$this, 'sendBookingEmail'], 'permission_callback' => [$this, 'checkCanEdit'], ]); // ===================== // PAYMENTS ROUTES // ===================== // Get booking payments — view cap. register_rest_route($this->namespace, '/bookings/(?P\d+)/payments', [ 'methods' => 'GET', 'callback' => [$this, 'getBookingPayments'], 'permission_callback' => [$this, 'checkCanView'], ]); // Add payment to booking — edit cap (modifies the booking's // payment state). Refunds + payment deletion live on the // dedicated PaymentController with their own high-sensitivity // caps. register_rest_route($this->namespace, '/bookings/(?P\d+)/payments', [ 'methods' => 'POST', 'callback' => [$this, 'addPayment'], 'permission_callback' => [$this, 'checkCanEdit'], ]); // NOTE: Payment CRUD operations moved to PaymentController // This keeps BookingsController focused on booking operations only // ===================== // TRAVELERS ROUTES // ===================== // Travelers list — view cap. register_rest_route($this->namespace, '/travelers', [ 'methods' => 'GET', 'callback' => [$this, 'getTravelers'], 'permission_callback' => [$this, 'checkCanView'], ]); // Traveler bulk actions — edit cap. register_rest_route($this->namespace, '/travelers/bulk', [ 'methods' => 'PUT', 'callback' => [$this, 'bulkTravelers'], 'permission_callback' => [$this, 'checkCanEdit'], ]); // Download travel voucher for a booking register_rest_route($this->namespace, '/bookings/(?P\d+)/voucher', [ 'methods' => 'GET', 'callback' => [$this, 'downloadVoucher'], 'permission_callback' => '__return_true', // Auth checked inside callback ]); // Download travel itinerary for a booking register_rest_route($this->namespace, '/bookings/(?P\d+)/itinerary', [ 'methods' => 'GET', 'callback' => [$this, 'downloadItinerary'], 'permission_callback' => '__return_true', // Auth checked inside callback ]); } /** * Granular permission checks — one per operation. WP administrators * pass every cap via the Team module's admin-fallback filter * (priority 7 / 8), so an explicit `manage_options` check isn't * needed at this layer — the cap covers it. */ public function checkCanView(): bool { return current_user_can('yatra_view_bookings'); } public function checkCanCreate(): bool { return current_user_can('yatra_create_bookings'); } public function checkCanEdit(): bool { return current_user_can('yatra_edit_bookings'); } public function checkCanDelete(): bool { // Critical-sensitivity cap. By default only the Owner role // holds this — Manager, Sales Agent, Front Desk, etc. cannot // delete bookings even when they can edit them. return current_user_can('yatra_delete_bookings'); } public function checkCanChangeStatus(): bool { // Separate from edit — Front Desk has this without the // broader edit cap so they can confirm/check-in bookings // without being able to mutate other fields. return current_user_can('yatra_change_booking_status'); } /** * @deprecated Kept for any external code (snippet, integration) * that referenced the old method name. Routes to the view-only * cap — safer than the old `view OR manage_options` shorthand, * and admin users still pass via the admin-fallback layer. */ public function checkAdminPermission(): bool { return $this->checkCanView(); } // ========================================================================= // BOOKING ENDPOINTS // ========================================================================= /** * GET /bookings - List all bookings */ public function getBookings(WP_REST_Request $request): WP_REST_Response { // Extract filters from request $filters = [ 'page' => (int) ($request->get_param('page') ?: 1), 'per_page' => (int) ($request->get_param('per_page') ?: 20), 'status' => $request->get_param('status') ?: '', 'payment_status' => $request->get_param('payment_status') ?: '', 'trip_id' => (int) $request->get_param('trip_id'), 'search' => $request->get_param('search') ?: '', 'date_from' => $request->get_param('date_from') ?: '', 'date_to' => $request->get_param('date_to') ?: '', // Column sorting from the table headers — whitelisted in the repository. 'orderby' => $request->get_param('orderby') ?: '', 'order' => $request->get_param('order') ?: '', ]; // Delegate to service $result = $this->bookingService->getBookings($filters); return new WP_REST_Response([ 'success' => true, 'data' => $result['data'], 'meta' => [ 'total' => $result['total'], 'page' => $result['page'], 'per_page' => $result['per_page'], 'total_pages' => $result['total_pages'], ], ]); } /** * GET /bookings/{id} - Get single booking */ public function getBooking(WP_REST_Request $request) { try { $id = (int) $request->get_param('id'); if ($id <= 0) { throw new ValidationException('Invalid booking ID', ['id' => ['Booking ID must be a positive integer']]); } Logger::apiRequest("/bookings/{$id}", 'GET'); $booking = $this->bookingService->getBooking($id); if (!$booking) { Logger::warning("Booking not found", ['booking_id' => $id]); return $this->not_found(__('Booking not found', 'yatra')); } Logger::info("Booking retrieved successfully", ['booking_id' => $id]); return $this->success_response($booking); } catch (\Exception $e) { Logger::error("Failed to get booking", ['booking_id' => $id ?? 0, 'error' => $e->getMessage()]); return $this->handle_exception($e); } } /** * POST /bookings - Create booking */ public function createBooking(WP_REST_Request $request) { try { $data = $request->get_json_params(); // Validate and sanitize input data BookingValidator::validateCreate($data); $data = BookingValidator::sanitize($data); Logger::apiRequest('/bookings', 'POST', $data); $result = $this->bookingService->createBooking($data); if (!$result['success']) { Logger::warning("Booking creation failed", ['data' => $data, 'result' => $result]); return $this->error_response($result['message'] ?? 'Failed to create booking', 400); } Logger::info("Booking created successfully", ['booking_id' => $result['data']['id'] ?? null]); return $this->success_response($result['data'], 201); } catch (\Exception $e) { Logger::error("Failed to create booking", ['data' => $data ?? [], 'error' => $e->getMessage()]); return $this->handle_exception($e); } } /** * PUT /bookings/{id} - Update booking */ public function updateBooking(WP_REST_Request $request) { try { $id = (int) $request->get_param('id'); $data = $request->get_json_params(); // Validate and sanitize input data BookingValidator::validateUpdate($data, $id); $data = BookingValidator::sanitize($data); Logger::apiRequest("/bookings/{$id}", 'PUT', $data); $result = $this->bookingService->updateBooking($id, $data); if (!$result['success']) { Logger::warning("Booking update failed", ['booking_id' => $id, 'data' => $data, 'result' => $result]); return $this->error_response($result['message'] ?? 'Failed to update booking', 400); } Logger::info("Booking updated successfully", ['booking_id' => $id]); return $this->success_response($result['data'] ?? null); } catch (\Exception $e) { Logger::error("Failed to update booking", ['booking_id' => $id ?? 0, 'data' => $data ?? [], 'error' => $e->getMessage()]); return $this->handle_exception($e); } } /** * DELETE /bookings/{id} - Delete booking */ public function deleteBooking(WP_REST_Request $request): WP_REST_Response { $id = (int) $request->get_param('id'); $result = $this->bookingService->deleteBooking($id); if (!$result['success']) { return new WP_REST_Response($result, 400); } return new WP_REST_Response($result); } /** * PUT /bookings/{id}/status - Update booking status */ public function updateBookingStatus(WP_REST_Request $request): WP_REST_Response { $id = (int) $request->get_param('id'); $data = $request->get_json_params(); $status = $data['status'] ?? ''; if (empty($status)) { return new WP_REST_Response([ 'success' => false, 'message' => __('Status is required.', 'yatra'), ], 400); } $result = $this->bookingService->updateStatus($id, $status); if (!$result['success']) { return new WP_REST_Response($result, 400); } return new WP_REST_Response($result); } /** * GET /bookings/stats - Get booking statistics */ public function getBookingStats(WP_REST_Request $request): WP_REST_Response { $stats = $this->bookingService->getStats(); return new WP_REST_Response($stats ?? []); } /** * POST /bookings/{id}/send-email - Send booking email */ public function sendBookingEmail(WP_REST_Request $request): WP_REST_Response { $id = (int) $request->get_param('id'); $data = $request->get_json_params(); $emailType = $data['type'] ?? 'confirmation'; $result = $this->bookingService->sendEmail($id, $emailType); if (!$result['success']) { return new WP_REST_Response($result, 400); } return new WP_REST_Response($result); } // ========================================================================= // PAYMENT ENDPOINTS // ========================================================================= /** * GET /bookings/{id}/payments - Get booking payments */ public function getBookingPayments(WP_REST_Request $request): WP_REST_Response { $bookingId = (int) $request->get_param('id'); $payments = $this->paymentService->getBookingPayments($bookingId); return new WP_REST_Response([ 'success' => true, 'data' => $payments, ]); } /** * POST /bookings/{id}/payments - Add payment to booking */ public function addPayment(WP_REST_Request $request): WP_REST_Response { $bookingId = (int) $request->get_param('id'); $data = $request->get_json_params(); $data['booking_id'] = $bookingId; $result = $this->paymentService->createPayment($data); if (!$result['success']) { return new WP_REST_Response($result, 400); } return new WP_REST_Response($result, 201); } /** * GET /payments - List all payments */ public function getPayments(WP_REST_Request $request): WP_REST_Response { $filters = [ 'page' => (int) ($request->get_param('page') ?: 1), 'per_page' => (int) ($request->get_param('per_page') ?: 20), 'booking_id' => (int) $request->get_param('booking_id'), 'status' => $request->get_param('status') ?: '', 'gateway' => $request->get_param('gateway') ?: '', 'search' => $request->get_param('search') ?: '', 'date_from' => $request->get_param('date_from') ?: '', 'date_to' => $request->get_param('date_to') ?: '', ]; $result = $this->paymentService->getPayments($filters); return new WP_REST_Response([ 'success' => true, 'data' => $result['data'], 'meta' => [ 'total' => $result['total'], 'page' => $result['page'], 'per_page' => $result['per_page'], 'total_pages' => $result['total_pages'], ], ]); } /** * POST /payments - Create payment */ public function createPayment(WP_REST_Request $request): WP_REST_Response { $data = $request->get_json_params(); $result = $this->paymentService->createPayment($data); if (!$result['success']) { return new WP_REST_Response($result, 400); } return new WP_REST_Response($result, 201); } /** * GET /payments/{id} - Get single payment */ public function getPayment(WP_REST_Request $request): WP_REST_Response { $id = (int) $request->get_param('id'); $payment = $this->paymentService->getPayment($id); if (!$payment) { return new WP_REST_Response([ 'success' => false, 'message' => __('Payment not found.', 'yatra'), ], 404); } return new WP_REST_Response([ 'success' => true, 'data' => $payment, ]); } /** * PUT /payments/{id} - Update payment */ public function updatePayment(WP_REST_Request $request): WP_REST_Response { $id = (int) $request->get_param('id'); $data = $request->get_json_params(); $result = $this->paymentService->updatePayment($id, $data); if (!$result['success']) { return new WP_REST_Response($result, 400); } return new WP_REST_Response($result); } /** * DELETE /payments/{id} - Delete payment */ public function deletePayment(WP_REST_Request $request): WP_REST_Response { $id = (int) $request->get_param('id'); $result = $this->paymentService->deletePayment($id); if (!$result['success']) { return new WP_REST_Response($result, 400); } return new WP_REST_Response($result); } // ========================================================================= // TRAVELERS ENDPOINTS // ========================================================================= /** * GET /travelers - Get all travelers */ public function getTravelers(WP_REST_Request $request): WP_REST_Response { $filters = [ 'page' => (int) ($request->get_param('page') ?: 1), 'per_page' => (int) ($request->get_param('per_page') ?: 20), 'search' => $request->get_param('search') ?: '', 'trip_id' => (int) $request->get_param('trip_id'), ]; $result = $this->bookingService->getTravelers($filters); return new WP_REST_Response([ 'success' => true, 'data' => $result['data'], 'meta' => $result['meta'] ?? [], ]); } /** * PUT /travelers/bulk - Bulk traveler actions */ public function bulkTravelers(WP_REST_Request $request): WP_REST_Response { $data = $request->get_json_params(); $action = $data['action'] ?? ''; $ids = $data['ids'] ?? []; if (empty($action) || empty($ids) || !is_array($ids)) { return new WP_REST_Response([ 'success' => false, 'message' => __('Action and IDs are required.', 'yatra'), ], 400); } $ids = array_filter(array_map('intval', $ids)); if (empty($ids)) { return new WP_REST_Response([ 'success' => false, 'message' => __('No valid traveler IDs provided.', 'yatra'), ], 400); } $result = $this->bookingService->bulkTravelers($ids, (string) $action); return new WP_REST_Response($result, $result['success'] ? 200 : 400); } /** * GET /bookings/{id}/voucher - Download travel voucher for a booking */ public function downloadVoucher(WP_REST_Request $request) { $bookingId = (int) $request->get_param('id'); $isPreview = $request->get_param('preview') === '1'; $isDownload = $request->get_param('download') === '1'; if ($bookingId <= 0) { return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]); } // Get booking details $booking = $this->bookingService->getBooking($bookingId); if (!$booking) { return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]); } // Verify user is logged in and owns this booking (or is admin) $currentUserId = get_current_user_id(); $bookingUserId = (int) ($booking['user_id'] ?? 0); // Must be logged in if (!$currentUserId) { return new WP_Error('unauthorized', __('You must be logged in to download vouchers.', 'yatra'), ['status' => 401]); } // Must own the booking or be admin if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) { return new WP_Error('forbidden', __('You do not have permission to access this voucher.', 'yatra'), ['status' => 403]); } // Get payment for this booking $payments = $this->paymentService->getBookingPayments($bookingId); if (empty($payments)) { return $this->renderVoucherFromBookingData($booking, $isPreview); } // Use the first payment (or you could use the latest payment) $payment = $payments[0]; $paymentId = (int) ($payment['id'] ?? 0); if ($paymentId <= 0) { return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]); } // Delegate to PaymentGatewayController's download_voucher method $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController(); // Create a new request with the payment ID $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/voucher"); $paymentRequest->set_param('payment_id', $paymentId); $paymentRequest->set_param('preview', $isPreview ? '1' : ''); $paymentRequest->set_param('download', $isDownload ? '1' : ''); return $paymentGatewayController->download_voucher($paymentRequest); } /** * GET /bookings/{id}/itinerary - Download travel itinerary for a booking */ public function downloadItinerary(WP_REST_Request $request) { $bookingId = (int) $request->get_param('id'); $isPreview = $request->get_param('preview') === '1'; $isDownload = $request->get_param('download') === '1'; if ($bookingId <= 0) { return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]); } // Get booking details $booking = $this->bookingService->getBooking($bookingId); if (!$booking) { return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]); } // Verify user is logged in and owns this booking (or is admin) $currentUserId = get_current_user_id(); $bookingUserId = (int) ($booking['user_id'] ?? 0); // Must be logged in if (!$currentUserId) { return new WP_Error('unauthorized', __('You must be logged in to download itineraries.', 'yatra'), ['status' => 401]); } // Must own the booking or be admin if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) { return new WP_Error('forbidden', __('You do not have permission to access this itinerary.', 'yatra'), ['status' => 403]); } // Get payment for this booking $payments = $this->paymentService->getBookingPayments($bookingId); if (empty($payments)) { return $this->renderItineraryFromBookingData($booking, $isPreview); } // Use the first payment (or you could use the latest payment) $payment = $payments[0]; $paymentId = (int) ($payment['id'] ?? 0); if ($paymentId <= 0) { return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]); } // Delegate to PaymentGatewayController's download_itinerary method $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController(); // Create a new request with the payment ID $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/itinerary"); $paymentRequest->set_param('payment_id', $paymentId); $paymentRequest->set_param('preview', $isPreview ? '1' : ''); $paymentRequest->set_param('download', $isDownload ? '1' : ''); return $paymentGatewayController->download_itinerary($paymentRequest); } /** * Voucher PDF when the booking has no payment rows yet (matches payment-based voucher layout). * * @param array $booking From BookingService::getBooking() */ private function renderVoucherFromBookingData(array $booking, bool $isPreview) { $tripRepository = new TripRepository(); $trip = null; $tripId = (int) ($booking['trip_id'] ?? 0); if ($tripId > 0) { $trip = $tripRepository->find($tripId); } $companyName = SettingsService::get('company_name', get_bloginfo('name')); $companyAddress = SettingsService::get('company_address', ''); $companyEmail = SettingsService::get('company_email', get_option('admin_email')); $companyPhone = SettingsService::get('company_phone', ''); $currency = SettingsService::getCurrency(); $currencySymbol = FormatHelper::getCurrencySymbol($currency); $createdAt = $booking['created_at'] ?? $booking['booking_date'] ?? ''; $bookingDate = !empty($createdAt) ? date_i18n(get_option('date_format'), strtotime((string) $createdAt)) : ''; $travelDateRaw = $booking['travel_date'] ?? ''; $travelDate = !empty($travelDateRaw) ? date_i18n(get_option('date_format'), strtotime((string) $travelDateRaw)) : ''; // Return date. Prefer the booking's STORED end_date — the actual booked // return (accounts for a flexible window or a trip duration changed after // booking). Fall back to the trip duration only when no end is stored: // duration_days is INCLUSIVE, so the return is travel_date + (days - 1) // (matches BookingRepository::calculateEndDate; a bare "+ duration_days" // was one day too far and implied an extra night — see ItineraryPdfBuilder). $returnDate = ''; $storedEnd = (string) ($booking['end_date'] ?? ''); if ($storedEnd !== '' && ($travelDateRaw === '' || $storedEnd >= $travelDateRaw)) { $returnDate = date_i18n(get_option('date_format'), strtotime($storedEnd)); } elseif (!empty($travelDateRaw) && $trip && !empty($trip->duration_days)) { $returnOffset = max(0, (int) $trip->duration_days - 1); $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . $returnOffset . ' days'); $returnDate = date_i18n(get_option('date_format'), $returnTimestamp); } $statusRaw = (string) ($booking['booking_status'] ?? $booking['status'] ?? ''); $bookingRef = (string) ($booking['booking_number'] ?? $booking['reference'] ?? (string) ($booking['id'] ?? '')); $filename = 'Travel Voucher #' . $bookingRef . '.pdf'; $customerName = trim( (string) ($booking['contact_first_name'] ?? '') . ' ' . (string) ($booking['contact_last_name'] ?? '') ) ?: (string) ($booking['customer_name'] ?? __('Customer', 'yatra')); $templateData = [ 'company_name' => $companyName, 'company_address' => $companyAddress, 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(), 'company_email' => $companyEmail, 'company_phone' => $companyPhone, 'customer_name' => $customerName, 'customer_email' => (string) ($booking['contact_email'] ?? $booking['customer_email'] ?? ''), 'customer_address_lines' => FormatHelper::customerAddressLines($booking), 'booking_ref' => $bookingRef, 'booking_date' => $bookingDate, 'booking_status' => ucfirst($statusRaw ?: 'pending'), 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' : (in_array(strtolower($statusRaw), ['cancelled'], true) ? 'cancelled' : 'pending'), 'trip_title' => $trip ? ($trip->title ?? $booking['trip_title'] ?? __('Trip Booking', 'yatra')) : ($booking['trip_title'] ?? __('Trip Booking', 'yatra')), // Trip duration comes from duration_days/duration_nights (there is no // `duration` column — accessing it caused a blank value + PHP notice). 'trip_duration' => $trip ? yatra_format_duration((int) ($trip->duration_days ?? 0), isset($trip->duration_nights) ? (int) $trip->duration_nights : null) : '', 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '', 'departure_location' => $trip ? ($trip->departure_location ?? '') : '', 'destination' => $trip ? ($trip->destination ?? '') : '', 'travel_date' => $travelDate, 'return_date' => $returnDate, 'currency_symbol' => $currencySymbol, 'total_amount' => yatra_format_price((float) ($booking['total_amount'] ?? 0), $currency, false), 'amount_paid' => yatra_format_price((float) ($booking['amount_paid'] ?? 0), $currency, false), 'amount_due' => yatra_format_price((float) ($booking['amount_due'] ?? 0), $currency, false), 'traveler_count' => (int) ($booking['travelers_count'] ?? $booking['travelers'] ?? 1), ]; $pdfService = new PdfService(); if (!$pdfService->isAvailable()) { return new WP_Error( 'pdf_engine_missing', __('Voucher PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'), ['status' => 500] ); } $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/voucher.php', $templateData, [ 'paper' => 'A4', 'orientation' => 'portrait', 'default_font' => 'DejaVu Sans', ]); if ($isPreview) { return new WP_REST_Response([ 'success' => true, 'pdf_data' => base64_encode($pdfBinary), 'filename' => $filename, ]); } $pdfService->outputPdfDownload($pdfBinary, $filename); exit; } /** * Itinerary PDF when the booking has no payment rows yet. * * @param array $booking From BookingService::getBooking() */ private function renderItineraryFromBookingData(array $booking, bool $isPreview) { $builder = new \Yatra\Services\ItineraryPdfBuilder(); if (!$builder->pdfService()->isAvailable()) { return new WP_Error( 'pdf_engine_missing', __('Itinerary PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'), ['status' => 500] ); } $bookingId = (int) ($booking['id'] ?? 0); $bookingRef = $bookingId > 0 ? 'YTR-' . strtoupper(str_pad((string) $bookingId, 8, '0', STR_PAD_LEFT)) : 'PENDING'; $filename = 'Travel-Itinerary-' . $bookingRef . '.pdf'; // The builder accepts the booking array shape directly — just // forward `id` as `booking_id` so the reference resolves the // same as the legacy code, and let it normalise everything else. $source = $booking + ['booking_id' => $bookingId]; $pdfBinary = $builder->build($source); if ($isPreview) { return new WP_REST_Response([ 'success' => true, 'pdf_data' => base64_encode($pdfBinary), 'filename' => $filename, ]); } $builder->pdfService()->outputPdfDownload($pdfBinary, $filename); exit; } }