PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 2.0.11 All 82 releases
yatra / app / Controllers / BookingsController.php

BookingsController.php in Yatra – Travel Booking & Tour Operator Software 3.0.3, at app/Controllers/BookingsController.php

902 lines 33.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Controllers;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_Error;
10 use Yatra\Helpers\FormatHelper;
11 use Yatra\Repositories\TripRepository;
12 use Yatra\Services\BookingService;
13 use Yatra\Services\PaymentService;
14 use Yatra\Services\PdfService;
15 use Yatra\Services\SettingsService;
16 use Yatra\Validators\BookingValidator;
17 use Yatra\Exceptions\ValidationException;
18 use Yatra\Utils\Logger;
19
20 /**
21 * Bookings REST API Controller
22 *
23 * Handles HTTP requests only - delegates business logic to BookingService.
24 *
25 * RESPONSIBILITIES:
26 * - Extract request parameters
27 * - Permission checks
28 * - Call service methods
29 * - Return WP_REST_Response
30 *
31 * NO DATABASE QUERIES OR BUSINESS LOGIC IN THIS FILE.
32 *
33 * @package Yatra\Controllers
34 */
35 class BookingsController extends BaseController
36 {
37 /**
38 * REST API namespace
39 */
40 protected string $namespace = 'yatra/v1';
41
42 /**
43 * Booking service instance
44 */
45 private BookingService $bookingService;
46
47 /**
48 * Payment service instance
49 */
50 private PaymentService $paymentService;
51
52 /**
53 * Constructor - Initialize services
54 */
55 public function __construct()
56 {
57 $this->bookingService = new BookingService();
58 $this->paymentService = new PaymentService();
59 }
60
61 /**
62 * Register REST API routes
63 */
64 public function register_routes(): void
65 {
66 // =====================
67 // BOOKINGS ROUTES
68 // =====================
69
70 // List bookings
71 register_rest_route($this->namespace, '/bookings', [
72 'methods' => 'GET',
73 'callback' => [$this, 'getBookings'],
74 'permission_callback' => [$this, 'checkAdminPermission'],
75 ]);
76
77 // Get single booking
78 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)', [
79 'methods' => 'GET',
80 'callback' => [$this, 'getBooking'],
81 'permission_callback' => [$this, 'checkAdminPermission'],
82 'args' => [
83 'id' => [
84 'required' => true,
85 'type' => 'integer',
86 'sanitize_callback' => 'absint',
87 ],
88 ],
89 ]);
90
91 // Create booking
92 register_rest_route($this->namespace, '/bookings', [
93 'methods' => 'POST',
94 'callback' => [$this, 'createBooking'],
95 'permission_callback' => [$this, 'checkAdminPermission'],
96 ]);
97
98 // Update booking
99 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)', [
100 'methods' => 'PUT',
101 'callback' => [$this, 'updateBooking'],
102 'permission_callback' => [$this, 'checkAdminPermission'],
103 ]);
104
105 // Delete booking
106 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)', [
107 'methods' => 'DELETE',
108 'callback' => [$this, 'deleteBooking'],
109 'permission_callback' => [$this, 'checkAdminPermission'],
110 ]);
111
112 // Update booking status
113 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/status', [
114 'methods' => 'PUT',
115 'callback' => [$this, 'updateBookingStatus'],
116 'permission_callback' => [$this, 'checkAdminPermission'],
117 ]);
118
119 // Get booking statistics
120 register_rest_route($this->namespace, '/bookings/stats', [
121 'methods' => 'GET',
122 'callback' => [$this, 'getBookingStats'],
123 'permission_callback' => [$this, 'checkAdminPermission'],
124 ]);
125
126 // Send booking email
127 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/send-email', [
128 'methods' => 'POST',
129 'callback' => [$this, 'sendBookingEmail'],
130 'permission_callback' => [$this, 'checkAdminPermission'],
131 ]);
132
133 // =====================
134 // PAYMENTS ROUTES
135 // =====================
136
137 // Get booking payments
138 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/payments', [
139 'methods' => 'GET',
140 'callback' => [$this, 'getBookingPayments'],
141 'permission_callback' => [$this, 'checkAdminPermission'],
142 ]);
143
144 // Add payment to booking
145 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/payments', [
146 'methods' => 'POST',
147 'callback' => [$this, 'addPayment'],
148 'permission_callback' => [$this, 'checkAdminPermission'],
149 ]);
150
151 // NOTE: Payment CRUD operations moved to PaymentController
152 // This keeps BookingsController focused on booking operations only
153
154 // =====================
155 // TRAVELERS ROUTES
156 // =====================
157
158 register_rest_route($this->namespace, '/travelers', [
159 'methods' => 'GET',
160 'callback' => [$this, 'getTravelers'],
161 'permission_callback' => [$this, 'checkAdminPermission'],
162 ]);
163
164 // Traveler bulk actions
165 register_rest_route($this->namespace, '/travelers/bulk', [
166 'methods' => 'PUT',
167 'callback' => [$this, 'bulkTravelers'],
168 'permission_callback' => [$this, 'checkAdminPermission'],
169 ]);
170
171 // Download travel voucher for a booking
172 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/voucher', [
173 'methods' => 'GET',
174 'callback' => [$this, 'downloadVoucher'],
175 'permission_callback' => '__return_true', // Auth checked inside callback
176 ]);
177
178 // Download travel itinerary for a booking
179 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/itinerary', [
180 'methods' => 'GET',
181 'callback' => [$this, 'downloadItinerary'],
182 'permission_callback' => '__return_true', // Auth checked inside callback
183 ]);
184 }
185
186 /**
187 * Check admin permission
188 */
189 public function checkAdminPermission(): bool
190 {
191 // Allow custom booking capability or fallback to manage_options
192 if (current_user_can('yatra_view_bookings')) {
193 return true;
194 }
195 return current_user_can('manage_options');
196 }
197
198 // =========================================================================
199 // BOOKING ENDPOINTS
200 // =========================================================================
201
202 /**
203 * GET /bookings - List all bookings
204 */
205 public function getBookings(WP_REST_Request $request): WP_REST_Response
206 {
207 // Extract filters from request
208 $filters = [
209 'page' => (int) ($request->get_param('page') ?: 1),
210 'per_page' => (int) ($request->get_param('per_page') ?: 20),
211 'status' => $request->get_param('status') ?: '',
212 'payment_status' => $request->get_param('payment_status') ?: '',
213 'trip_id' => (int) $request->get_param('trip_id'),
214 'search' => $request->get_param('search') ?: '',
215 'date_from' => $request->get_param('date_from') ?: '',
216 'date_to' => $request->get_param('date_to') ?: '',
217 ];
218
219 // Delegate to service
220 $result = $this->bookingService->getBookings($filters);
221
222 return new WP_REST_Response([
223 'success' => true,
224 'data' => $result['data'],
225 'meta' => [
226 'total' => $result['total'],
227 'page' => $result['page'],
228 'per_page' => $result['per_page'],
229 'total_pages' => $result['total_pages'],
230 ],
231 ]);
232 }
233
234 /**
235 * GET /bookings/{id} - Get single booking
236 */
237 public function getBooking(WP_REST_Request $request)
238 {
239 try {
240 $id = (int) $request->get_param('id');
241
242 if ($id <= 0) {
243 throw new ValidationException('Invalid booking ID', ['id' => ['Booking ID must be a positive integer']]);
244 }
245
246 Logger::apiRequest("/bookings/{$id}", 'GET');
247
248 $booking = $this->bookingService->getBooking($id);
249
250 if (!$booking) {
251 Logger::warning("Booking not found", ['booking_id' => $id]);
252 return $this->not_found(__('Booking not found', 'yatra'));
253 }
254
255 Logger::info("Booking retrieved successfully", ['booking_id' => $id]);
256 return $this->success_response($booking);
257
258 } catch (\Exception $e) {
259 Logger::error("Failed to get booking", ['booking_id' => $id ?? 0, 'error' => $e->getMessage()]);
260 return $this->handle_exception($e);
261 }
262 }
263
264 /**
265 * POST /bookings - Create booking
266 */
267 public function createBooking(WP_REST_Request $request)
268 {
269 try {
270 $data = $request->get_json_params();
271
272 // Validate and sanitize input data
273 BookingValidator::validateCreate($data);
274 $data = BookingValidator::sanitize($data);
275
276 Logger::apiRequest('/bookings', 'POST', $data);
277
278 $result = $this->bookingService->createBooking($data);
279
280 if (!$result['success']) {
281 Logger::warning("Booking creation failed", ['data' => $data, 'result' => $result]);
282 return $this->error_response($result['message'] ?? 'Failed to create booking', 400);
283 }
284
285 Logger::info("Booking created successfully", ['booking_id' => $result['data']['id'] ?? null]);
286 return $this->success_response($result['data'], 201);
287
288 } catch (\Exception $e) {
289 Logger::error("Failed to create booking", ['data' => $data ?? [], 'error' => $e->getMessage()]);
290 return $this->handle_exception($e);
291 }
292 }
293
294 /**
295 * PUT /bookings/{id} - Update booking
296 */
297 public function updateBooking(WP_REST_Request $request)
298 {
299 try {
300 $id = (int) $request->get_param('id');
301 $data = $request->get_json_params();
302
303 // Validate and sanitize input data
304 BookingValidator::validateUpdate($data, $id);
305 $data = BookingValidator::sanitize($data);
306
307 Logger::apiRequest("/bookings/{$id}", 'PUT', $data);
308
309 $result = $this->bookingService->updateBooking($id, $data);
310
311 if (!$result['success']) {
312 Logger::warning("Booking update failed", ['booking_id' => $id, 'data' => $data, 'result' => $result]);
313 return $this->error_response($result['message'] ?? 'Failed to update booking', 400);
314 }
315
316 Logger::info("Booking updated successfully", ['booking_id' => $id]);
317 return $this->success_response($result['data']);
318
319 } catch (\Exception $e) {
320 Logger::error("Failed to update booking", ['booking_id' => $id ?? 0, 'data' => $data ?? [], 'error' => $e->getMessage()]);
321 return $this->handle_exception($e);
322 }
323 }
324
325 /**
326 * DELETE /bookings/{id} - Delete booking
327 */
328 public function deleteBooking(WP_REST_Request $request): WP_REST_Response
329 {
330 $id = (int) $request->get_param('id');
331
332 $result = $this->bookingService->deleteBooking($id);
333
334 if (!$result['success']) {
335 return new WP_REST_Response($result, 400);
336 }
337
338 return new WP_REST_Response($result);
339 }
340
341 /**
342 * PUT /bookings/{id}/status - Update booking status
343 */
344 public function updateBookingStatus(WP_REST_Request $request): WP_REST_Response
345 {
346 $id = (int) $request->get_param('id');
347 $data = $request->get_json_params();
348 $status = $data['status'] ?? '';
349
350 if (empty($status)) {
351 return new WP_REST_Response([
352 'success' => false,
353 'message' => __('Status is required.', 'yatra'),
354 ], 400);
355 }
356
357 $result = $this->bookingService->updateStatus($id, $status);
358
359 if (!$result['success']) {
360 return new WP_REST_Response($result, 400);
361 }
362
363 return new WP_REST_Response($result);
364 }
365
366 /**
367 * GET /bookings/stats - Get booking statistics
368 */
369 public function getBookingStats(WP_REST_Request $request): WP_REST_Response
370 {
371 $stats = $this->bookingService->getStats();
372
373 return new WP_REST_Response($stats ?? []);
374
375 }
376
377 /**
378 * POST /bookings/{id}/send-email - Send booking email
379 */
380 public function sendBookingEmail(WP_REST_Request $request): WP_REST_Response
381 {
382 $id = (int) $request->get_param('id');
383 $data = $request->get_json_params();
384 $emailType = $data['type'] ?? 'confirmation';
385
386 $result = $this->bookingService->sendEmail($id, $emailType);
387
388 if (!$result['success']) {
389 return new WP_REST_Response($result, 400);
390 }
391
392 return new WP_REST_Response($result);
393 }
394
395 // =========================================================================
396 // PAYMENT ENDPOINTS
397 // =========================================================================
398
399 /**
400 * GET /bookings/{id}/payments - Get booking payments
401 */
402 public function getBookingPayments(WP_REST_Request $request): WP_REST_Response
403 {
404 $bookingId = (int) $request->get_param('id');
405
406 $payments = $this->paymentService->getBookingPayments($bookingId);
407
408 return new WP_REST_Response([
409 'success' => true,
410 'data' => $payments,
411 ]);
412 }
413
414 /**
415 * POST /bookings/{id}/payments - Add payment to booking
416 */
417 public function addPayment(WP_REST_Request $request): WP_REST_Response
418 {
419 $bookingId = (int) $request->get_param('id');
420 $data = $request->get_json_params();
421 $data['booking_id'] = $bookingId;
422
423 $result = $this->paymentService->createPayment($data);
424
425 if (!$result['success']) {
426 return new WP_REST_Response($result, 400);
427 }
428
429 return new WP_REST_Response($result, 201);
430 }
431
432 /**
433 * GET /payments - List all payments
434 */
435 public function getPayments(WP_REST_Request $request): WP_REST_Response
436 {
437 $filters = [
438 'page' => (int) ($request->get_param('page') ?: 1),
439 'per_page' => (int) ($request->get_param('per_page') ?: 20),
440 'booking_id' => (int) $request->get_param('booking_id'),
441 'status' => $request->get_param('status') ?: '',
442 'gateway' => $request->get_param('gateway') ?: '',
443 'search' => $request->get_param('search') ?: '',
444 'date_from' => $request->get_param('date_from') ?: '',
445 'date_to' => $request->get_param('date_to') ?: '',
446 ];
447
448 $result = $this->paymentService->getPayments($filters);
449
450 return new WP_REST_Response([
451 'success' => true,
452 'data' => $result['data'],
453 'meta' => [
454 'total' => $result['total'],
455 'page' => $result['page'],
456 'per_page' => $result['per_page'],
457 'total_pages' => $result['total_pages'],
458 ],
459 ]);
460 }
461
462 /**
463 * POST /payments - Create payment
464 */
465 public function createPayment(WP_REST_Request $request): WP_REST_Response
466 {
467 $data = $request->get_json_params();
468
469 $result = $this->paymentService->createPayment($data);
470
471 if (!$result['success']) {
472 return new WP_REST_Response($result, 400);
473 }
474
475 return new WP_REST_Response($result, 201);
476 }
477
478 /**
479 * GET /payments/{id} - Get single payment
480 */
481 public function getPayment(WP_REST_Request $request): WP_REST_Response
482 {
483 $id = (int) $request->get_param('id');
484
485 $payment = $this->paymentService->getPayment($id);
486
487 if (!$payment) {
488 return new WP_REST_Response([
489 'success' => false,
490 'message' => __('Payment not found.', 'yatra'),
491 ], 404);
492 }
493
494 return new WP_REST_Response([
495 'success' => true,
496 'data' => $payment,
497 ]);
498 }
499
500 /**
501 * PUT /payments/{id} - Update payment
502 */
503 public function updatePayment(WP_REST_Request $request): WP_REST_Response
504 {
505 $id = (int) $request->get_param('id');
506 $data = $request->get_json_params();
507
508 $result = $this->paymentService->updatePayment($id, $data);
509
510 if (!$result['success']) {
511 return new WP_REST_Response($result, 400);
512 }
513
514 return new WP_REST_Response($result);
515 }
516
517 /**
518 * DELETE /payments/{id} - Delete payment
519 */
520 public function deletePayment(WP_REST_Request $request): WP_REST_Response
521 {
522 $id = (int) $request->get_param('id');
523
524 $result = $this->paymentService->deletePayment($id);
525
526 if (!$result['success']) {
527 return new WP_REST_Response($result, 400);
528 }
529
530 return new WP_REST_Response($result);
531 }
532
533 // =========================================================================
534 // TRAVELERS ENDPOINTS
535 // =========================================================================
536
537 /**
538 * GET /travelers - Get all travelers
539 */
540 public function getTravelers(WP_REST_Request $request): WP_REST_Response
541 {
542 $filters = [
543 'page' => (int) ($request->get_param('page') ?: 1),
544 'per_page' => (int) ($request->get_param('per_page') ?: 20),
545 'search' => $request->get_param('search') ?: '',
546 'trip_id' => (int) $request->get_param('trip_id'),
547 ];
548
549 $result = $this->bookingService->getTravelers($filters);
550
551 return new WP_REST_Response([
552 'success' => true,
553 'data' => $result['data'],
554 'meta' => $result['meta'] ?? [],
555 ]);
556 }
557
558 /**
559 * PUT /travelers/bulk - Bulk traveler actions
560 */
561 public function bulkTravelers(WP_REST_Request $request): WP_REST_Response
562 {
563 $data = $request->get_json_params();
564 $action = $data['action'] ?? '';
565 $ids = $data['ids'] ?? [];
566
567 if (empty($action) || empty($ids) || !is_array($ids)) {
568 return new WP_REST_Response([
569 'success' => false,
570 'message' => __('Action and IDs are required.', 'yatra'),
571 ], 400);
572 }
573
574 $ids = array_filter(array_map('intval', $ids));
575
576 if (empty($ids)) {
577 return new WP_REST_Response([
578 'success' => false,
579 'message' => __('No valid traveler IDs provided.', 'yatra'),
580 ], 400);
581 }
582
583 $result = $this->bookingService->bulkTravelers($ids, (string) $action);
584
585 return new WP_REST_Response($result, $result['success'] ? 200 : 400);
586 }
587
588 /**
589 * GET /bookings/{id}/voucher - Download travel voucher for a booking
590 */
591 public function downloadVoucher(WP_REST_Request $request)
592 {
593 $bookingId = (int) $request->get_param('id');
594 $isPreview = $request->get_param('preview') === '1';
595 $isDownload = $request->get_param('download') === '1';
596
597 if ($bookingId <= 0) {
598 return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]);
599 }
600
601 // Get booking details
602 $booking = $this->bookingService->getBooking($bookingId);
603
604 if (!$booking) {
605 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
606 }
607
608 // Verify user is logged in and owns this booking (or is admin)
609 $currentUserId = get_current_user_id();
610 $bookingUserId = (int) ($booking['user_id'] ?? 0);
611
612 // Must be logged in
613 if (!$currentUserId) {
614 return new WP_Error('unauthorized', __('You must be logged in to download vouchers.', 'yatra'), ['status' => 401]);
615 }
616
617 // Must own the booking or be admin
618 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
619 return new WP_Error('forbidden', __('You do not have permission to access this voucher.', 'yatra'), ['status' => 403]);
620 }
621
622 // Get payment for this booking
623 $payments = $this->paymentService->getBookingPayments($bookingId);
624
625 if (empty($payments)) {
626 return $this->renderVoucherFromBookingData($booking, $isPreview);
627 }
628
629 // Use the first payment (or you could use the latest payment)
630 $payment = $payments[0];
631 $paymentId = (int) ($payment['id'] ?? 0);
632
633 if ($paymentId <= 0) {
634 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
635 }
636
637 // Delegate to PaymentGatewayController's download_voucher method
638 $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController();
639
640 // Create a new request with the payment ID
641 $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/voucher");
642 $paymentRequest->set_param('payment_id', $paymentId);
643 $paymentRequest->set_param('preview', $isPreview ? '1' : '');
644 $paymentRequest->set_param('download', $isDownload ? '1' : '');
645
646 return $paymentGatewayController->download_voucher($paymentRequest);
647 }
648
649 /**
650 * GET /bookings/{id}/itinerary - Download travel itinerary for a booking
651 */
652 public function downloadItinerary(WP_REST_Request $request)
653 {
654 $bookingId = (int) $request->get_param('id');
655 $isPreview = $request->get_param('preview') === '1';
656 $isDownload = $request->get_param('download') === '1';
657
658 if ($bookingId <= 0) {
659 return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]);
660 }
661
662 // Get booking details
663 $booking = $this->bookingService->getBooking($bookingId);
664
665 if (!$booking) {
666 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
667 }
668
669 // Verify user is logged in and owns this booking (or is admin)
670 $currentUserId = get_current_user_id();
671 $bookingUserId = (int) ($booking['user_id'] ?? 0);
672
673 // Must be logged in
674 if (!$currentUserId) {
675 return new WP_Error('unauthorized', __('You must be logged in to download itineraries.', 'yatra'), ['status' => 401]);
676 }
677
678 // Must own the booking or be admin
679 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
680 return new WP_Error('forbidden', __('You do not have permission to access this itinerary.', 'yatra'), ['status' => 403]);
681 }
682
683 // Get payment for this booking
684 $payments = $this->paymentService->getBookingPayments($bookingId);
685
686 if (empty($payments)) {
687 return $this->renderItineraryFromBookingData($booking, $isPreview);
688 }
689
690 // Use the first payment (or you could use the latest payment)
691 $payment = $payments[0];
692 $paymentId = (int) ($payment['id'] ?? 0);
693
694 if ($paymentId <= 0) {
695 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
696 }
697
698 // Delegate to PaymentGatewayController's download_itinerary method
699 $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController();
700
701 // Create a new request with the payment ID
702 $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/itinerary");
703 $paymentRequest->set_param('payment_id', $paymentId);
704 $paymentRequest->set_param('preview', $isPreview ? '1' : '');
705 $paymentRequest->set_param('download', $isDownload ? '1' : '');
706
707 return $paymentGatewayController->download_itinerary($paymentRequest);
708 }
709
710 /**
711 * Voucher PDF when the booking has no payment rows yet (matches payment-based voucher layout).
712 *
713 * @param array<string,mixed> $booking From BookingService::getBooking()
714 */
715 private function renderVoucherFromBookingData(array $booking, bool $isPreview)
716 {
717 $tripRepository = new TripRepository();
718 $trip = null;
719 $tripId = (int) ($booking['trip_id'] ?? 0);
720 if ($tripId > 0) {
721 $trip = $tripRepository->find($tripId);
722 }
723
724 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
725 $companyAddress = SettingsService::get('company_address', '');
726 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
727 $companyPhone = SettingsService::get('company_phone', '');
728 $currency = SettingsService::getCurrency();
729 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
730
731 $createdAt = $booking['created_at'] ?? $booking['booking_date'] ?? '';
732 $bookingDate = !empty($createdAt) ? date_i18n(get_option('date_format'), strtotime((string) $createdAt)) : '';
733 $travelDateRaw = $booking['travel_date'] ?? '';
734 $travelDate = !empty($travelDateRaw) ? date_i18n(get_option('date_format'), strtotime((string) $travelDateRaw)) : '';
735
736 $returnDate = '';
737 if (!empty($travelDateRaw) && $trip && !empty($trip->duration)) {
738 $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . (int) $trip->duration . ' days');
739 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
740 }
741
742 $statusRaw = (string) ($booking['booking_status'] ?? $booking['status'] ?? '');
743 $bookingRef = (string) ($booking['booking_number'] ?? $booking['reference'] ?? (string) ($booking['id'] ?? ''));
744 $filename = 'Travel Voucher #' . $bookingRef . '.pdf';
745
746 $customerName = trim(
747 (string) ($booking['contact_first_name'] ?? '') . ' ' . (string) ($booking['contact_last_name'] ?? '')
748 ) ?: (string) ($booking['customer_name'] ?? __('Customer', 'yatra'));
749
750 $templateData = [
751 'company_name' => $companyName,
752 'company_address' => $companyAddress,
753 'company_email' => $companyEmail,
754 'company_phone' => $companyPhone,
755 'customer_name' => $customerName,
756 'customer_email' => (string) ($booking['contact_email'] ?? $booking['customer_email'] ?? ''),
757 'booking_ref' => $bookingRef,
758 'booking_date' => $bookingDate,
759 'booking_status' => ucfirst($statusRaw ?: 'pending'),
760 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
761 (in_array(strtolower($statusRaw), ['cancelled'], true) ? 'cancelled' : 'pending'),
762 'trip_title' => $trip ? ($trip->title ?? $booking['trip_title'] ?? __('Trip Booking', 'yatra')) : ($booking['trip_title'] ?? __('Trip Booking', 'yatra')),
763 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
764 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
765 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
766 'destination' => $trip ? ($trip->destination ?? '') : '',
767 'travel_date' => $travelDate,
768 'return_date' => $returnDate,
769 'currency_symbol' => $currencySymbol,
770 'total_amount' => number_format((float) ($booking['total_amount'] ?? 0), 2),
771 'amount_paid' => number_format((float) ($booking['amount_paid'] ?? 0), 2),
772 'amount_due' => number_format((float) ($booking['amount_due'] ?? 0), 2),
773 'traveler_count' => (int) ($booking['travelers_count'] ?? $booking['travelers'] ?? 1),
774 ];
775
776 $pdfService = new PdfService();
777 if (!$pdfService->isAvailable()) {
778 return new WP_Error(
779 'pdf_engine_missing',
780 __('Voucher PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
781 ['status' => 500]
782 );
783 }
784
785 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/voucher.php', $templateData, [
786 'paper' => 'A4',
787 'orientation' => 'portrait',
788 'default_font' => 'DejaVu Sans',
789 ]);
790
791 if ($isPreview) {
792 return new WP_REST_Response([
793 'success' => true,
794 'pdf_data' => base64_encode($pdfBinary),
795 'filename' => $filename,
796 ]);
797 }
798
799 $pdfService->outputPdfDownload($pdfBinary, $filename);
800 exit;
801 }
802
803 /**
804 * Itinerary PDF when the booking has no payment rows yet.
805 *
806 * @param array<string,mixed> $booking From BookingService::getBooking()
807 */
808 private function renderItineraryFromBookingData(array $booking, bool $isPreview)
809 {
810 $tripRepository = new TripRepository();
811 $trip = null;
812 $tripId = (int) ($booking['trip_id'] ?? 0);
813 if ($tripId > 0) {
814 $trip = $tripRepository->find($tripId);
815 }
816
817 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
818 $companyAddress = SettingsService::get('company_address', '');
819 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
820 $companyPhone = SettingsService::get('company_phone', '');
821 $currency = SettingsService::getCurrency();
822 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
823
824 $createdAt = $booking['created_at'] ?? $booking['booking_date'] ?? '';
825 $bookingDate = !empty($createdAt) ? date_i18n(get_option('date_format'), strtotime((string) $createdAt)) : '';
826 $travelDateRaw = $booking['travel_date'] ?? '';
827 $travelDate = !empty($travelDateRaw) ? date_i18n(get_option('date_format'), strtotime((string) $travelDateRaw)) : '';
828
829 $returnDate = '';
830 if (!empty($travelDateRaw) && $trip && !empty($trip->duration)) {
831 $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . (int) $trip->duration . ' days');
832 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
833 }
834
835 $statusRaw = (string) ($booking['booking_status'] ?? $booking['status'] ?? '');
836 $bookingId = (int) ($booking['id'] ?? 0);
837 $bookingRef = 'YTR-' . strtoupper(str_pad((string) $bookingId, 8, '0', STR_PAD_LEFT));
838
839 $customerName = trim(
840 (string) ($booking['contact_first_name'] ?? '') . ' ' . (string) ($booking['contact_last_name'] ?? '')
841 ) ?: (string) ($booking['customer_name'] ?? __('Customer', 'yatra'));
842
843 $pdfService = new PdfService();
844 if (!$pdfService->isAvailable()) {
845 return new WP_Error(
846 'pdf_engine_missing',
847 __('Itinerary PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
848 ['status' => 500]
849 );
850 }
851
852 $filename = 'Travel-Itinerary-' . $bookingRef . '.pdf';
853
854 $templateData = [
855 'company_name' => $companyName,
856 'company_address' => $companyAddress,
857 'company_email' => $companyEmail,
858 'company_phone' => $companyPhone,
859 'customer_name' => $customerName,
860 'customer_email' => (string) ($booking['contact_email'] ?? $booking['customer_email'] ?? ''),
861 'booking_ref' => $bookingRef,
862 'booking_date' => $bookingDate,
863 'booking_status' => ucfirst($statusRaw ?: 'pending'),
864 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
865 (in_array(strtolower($statusRaw), ['cancelled'], true) ? 'cancelled' : 'pending'),
866 'trip_title' => $trip ? ($trip->title ?? $booking['trip_title'] ?? __('Trip Booking', 'yatra')) : ($booking['trip_title'] ?? __('Trip Booking', 'yatra')),
867 'trip_description' => $trip ? ($trip->description ?? $trip->content ?? '') : '',
868 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
869 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
870 'trip_highlights' => $trip ? ($trip->highlights ?? $trip->trip_highlights ?? '') : '',
871 'trip_includes' => $trip ? ($trip->includes ?? $trip->trip_includes ?? '') : '',
872 'trip_excludes' => $trip ? ($trip->excludes ?? $trip->trip_excludes ?? '') : '',
873 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
874 'destination' => $trip ? ($trip->destination ?? '') : '',
875 'travel_date' => $travelDate,
876 'return_date' => $returnDate,
877 'currency_symbol' => $currencySymbol,
878 'total_amount' => number_format((float) ($booking['total_amount'] ?? 0), 2),
879 'amount_paid' => number_format((float) ($booking['amount_paid'] ?? 0), 2),
880 'amount_due' => number_format((float) ($booking['amount_due'] ?? 0), 2),
881 'traveler_count' => (int) ($booking['travelers_count'] ?? $booking['travelers'] ?? 1),
882 ];
883
884 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/itinerary.php', $templateData, [
885 'paper' => 'A4',
886 'orientation' => 'portrait',
887 'default_font' => 'DejaVu Sans',
888 ]);
889
890 if ($isPreview) {
891 return new WP_REST_Response([
892 'success' => true,
893 'pdf_data' => base64_encode($pdfBinary),
894 'filename' => $filename,
895 ]);
896 }
897
898 $pdfService->outputPdfDownload($pdfBinary, $filename);
899 exit;
900 }
901 }
902