PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.7
Yatra – Travel Booking & Tour Operator Software v3.0.7
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.7, at app/Controllers/BookingsController.php

893 lines 31.8 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 — view cap.
71 register_rest_route($this->namespace, '/bookings', [
72 'methods' => 'GET',
73 'callback' => [$this, 'getBookings'],
74 'permission_callback' => [$this, 'checkCanView'],
75 ]);
76
77 // Get single booking — view cap.
78 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)', [
79 'methods' => 'GET',
80 'callback' => [$this, 'getBooking'],
81 'permission_callback' => [$this, 'checkCanView'],
82 'args' => [
83 'id' => [
84 'required' => true,
85 'type' => 'integer',
86 'sanitize_callback' => 'absint',
87 ],
88 ],
89 ]);
90
91 // Create booking — create cap.
92 register_rest_route($this->namespace, '/bookings', [
93 'methods' => 'POST',
94 'callback' => [$this, 'createBooking'],
95 'permission_callback' => [$this, 'checkCanCreate'],
96 ]);
97
98 // Update booking — edit cap.
99 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)', [
100 'methods' => 'PUT',
101 'callback' => [$this, 'updateBooking'],
102 'permission_callback' => [$this, 'checkCanEdit'],
103 ]);
104
105 // Delete booking — critical-sensitivity delete cap. Only
106 // Owner role gets this by default.
107 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)', [
108 'methods' => 'DELETE',
109 'callback' => [$this, 'deleteBooking'],
110 'permission_callback' => [$this, 'checkCanDelete'],
111 ]);
112
113 // Update booking status — dedicated change-status cap so
114 // Front Desk (who has this cap but NOT edit) can flip
115 // confirmed → checked-in without being able to mutate other
116 // fields.
117 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/status', [
118 'methods' => 'PUT',
119 'callback' => [$this, 'updateBookingStatus'],
120 'permission_callback' => [$this, 'checkCanChangeStatus'],
121 ]);
122
123 // Get booking statistics — view cap (aggregates only).
124 register_rest_route($this->namespace, '/bookings/stats', [
125 'methods' => 'GET',
126 'callback' => [$this, 'getBookingStats'],
127 'permission_callback' => [$this, 'checkCanView'],
128 ]);
129
130 // Send booking email — edit cap. Sending a transactional
131 // re-confirmation is a write-side operation against the
132 // customer's record.
133 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/send-email', [
134 'methods' => 'POST',
135 'callback' => [$this, 'sendBookingEmail'],
136 'permission_callback' => [$this, 'checkCanEdit'],
137 ]);
138
139 // =====================
140 // PAYMENTS ROUTES
141 // =====================
142
143 // Get booking payments — view cap.
144 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/payments', [
145 'methods' => 'GET',
146 'callback' => [$this, 'getBookingPayments'],
147 'permission_callback' => [$this, 'checkCanView'],
148 ]);
149
150 // Add payment to booking — edit cap (modifies the booking's
151 // payment state). Refunds + payment deletion live on the
152 // dedicated PaymentController with their own high-sensitivity
153 // caps.
154 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/payments', [
155 'methods' => 'POST',
156 'callback' => [$this, 'addPayment'],
157 'permission_callback' => [$this, 'checkCanEdit'],
158 ]);
159
160 // NOTE: Payment CRUD operations moved to PaymentController
161 // This keeps BookingsController focused on booking operations only
162
163 // =====================
164 // TRAVELERS ROUTES
165 // =====================
166
167 // Travelers list — view cap.
168 register_rest_route($this->namespace, '/travelers', [
169 'methods' => 'GET',
170 'callback' => [$this, 'getTravelers'],
171 'permission_callback' => [$this, 'checkCanView'],
172 ]);
173
174 // Traveler bulk actions — edit cap.
175 register_rest_route($this->namespace, '/travelers/bulk', [
176 'methods' => 'PUT',
177 'callback' => [$this, 'bulkTravelers'],
178 'permission_callback' => [$this, 'checkCanEdit'],
179 ]);
180
181 // Download travel voucher for a booking
182 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/voucher', [
183 'methods' => 'GET',
184 'callback' => [$this, 'downloadVoucher'],
185 'permission_callback' => '__return_true', // Auth checked inside callback
186 ]);
187
188 // Download travel itinerary for a booking
189 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/itinerary', [
190 'methods' => 'GET',
191 'callback' => [$this, 'downloadItinerary'],
192 'permission_callback' => '__return_true', // Auth checked inside callback
193 ]);
194 }
195
196 /**
197 * Granular permission checks — one per operation. WP administrators
198 * pass every cap via the Team module's admin-fallback filter
199 * (priority 7 / 8), so an explicit `manage_options` check isn't
200 * needed at this layer — the cap covers it.
201 */
202 public function checkCanView(): bool
203 {
204 return current_user_can('yatra_view_bookings');
205 }
206
207 public function checkCanCreate(): bool
208 {
209 return current_user_can('yatra_create_bookings');
210 }
211
212 public function checkCanEdit(): bool
213 {
214 return current_user_can('yatra_edit_bookings');
215 }
216
217 public function checkCanDelete(): bool
218 {
219 // Critical-sensitivity cap. By default only the Owner role
220 // holds this — Manager, Sales Agent, Front Desk, etc. cannot
221 // delete bookings even when they can edit them.
222 return current_user_can('yatra_delete_bookings');
223 }
224
225 public function checkCanChangeStatus(): bool
226 {
227 // Separate from edit — Front Desk has this without the
228 // broader edit cap so they can confirm/check-in bookings
229 // without being able to mutate other fields.
230 return current_user_can('yatra_change_booking_status');
231 }
232
233 /**
234 * @deprecated Kept for any external code (snippet, integration)
235 * that referenced the old method name. Routes to the view-only
236 * cap — safer than the old `view OR manage_options` shorthand,
237 * and admin users still pass via the admin-fallback layer.
238 */
239 public function checkAdminPermission(): bool
240 {
241 return $this->checkCanView();
242 }
243
244 // =========================================================================
245 // BOOKING ENDPOINTS
246 // =========================================================================
247
248 /**
249 * GET /bookings - List all bookings
250 */
251 public function getBookings(WP_REST_Request $request): WP_REST_Response
252 {
253 // Extract filters from request
254 $filters = [
255 'page' => (int) ($request->get_param('page') ?: 1),
256 'per_page' => (int) ($request->get_param('per_page') ?: 20),
257 'status' => $request->get_param('status') ?: '',
258 'payment_status' => $request->get_param('payment_status') ?: '',
259 'trip_id' => (int) $request->get_param('trip_id'),
260 'search' => $request->get_param('search') ?: '',
261 'date_from' => $request->get_param('date_from') ?: '',
262 'date_to' => $request->get_param('date_to') ?: '',
263 ];
264
265 // Delegate to service
266 $result = $this->bookingService->getBookings($filters);
267
268 return new WP_REST_Response([
269 'success' => true,
270 'data' => $result['data'],
271 'meta' => [
272 'total' => $result['total'],
273 'page' => $result['page'],
274 'per_page' => $result['per_page'],
275 'total_pages' => $result['total_pages'],
276 ],
277 ]);
278 }
279
280 /**
281 * GET /bookings/{id} - Get single booking
282 */
283 public function getBooking(WP_REST_Request $request)
284 {
285 try {
286 $id = (int) $request->get_param('id');
287
288 if ($id <= 0) {
289 throw new ValidationException('Invalid booking ID', ['id' => ['Booking ID must be a positive integer']]);
290 }
291
292 Logger::apiRequest("/bookings/{$id}", 'GET');
293
294 $booking = $this->bookingService->getBooking($id);
295
296 if (!$booking) {
297 Logger::warning("Booking not found", ['booking_id' => $id]);
298 return $this->not_found(__('Booking not found', 'yatra'));
299 }
300
301 Logger::info("Booking retrieved successfully", ['booking_id' => $id]);
302 return $this->success_response($booking);
303
304 } catch (\Exception $e) {
305 Logger::error("Failed to get booking", ['booking_id' => $id ?? 0, 'error' => $e->getMessage()]);
306 return $this->handle_exception($e);
307 }
308 }
309
310 /**
311 * POST /bookings - Create booking
312 */
313 public function createBooking(WP_REST_Request $request)
314 {
315 try {
316 $data = $request->get_json_params();
317
318 // Validate and sanitize input data
319 BookingValidator::validateCreate($data);
320 $data = BookingValidator::sanitize($data);
321
322 Logger::apiRequest('/bookings', 'POST', $data);
323
324 $result = $this->bookingService->createBooking($data);
325
326 if (!$result['success']) {
327 Logger::warning("Booking creation failed", ['data' => $data, 'result' => $result]);
328 return $this->error_response($result['message'] ?? 'Failed to create booking', 400);
329 }
330
331 Logger::info("Booking created successfully", ['booking_id' => $result['data']['id'] ?? null]);
332 return $this->success_response($result['data'], 201);
333
334 } catch (\Exception $e) {
335 Logger::error("Failed to create booking", ['data' => $data ?? [], 'error' => $e->getMessage()]);
336 return $this->handle_exception($e);
337 }
338 }
339
340 /**
341 * PUT /bookings/{id} - Update booking
342 */
343 public function updateBooking(WP_REST_Request $request)
344 {
345 try {
346 $id = (int) $request->get_param('id');
347 $data = $request->get_json_params();
348
349 // Validate and sanitize input data
350 BookingValidator::validateUpdate($data, $id);
351 $data = BookingValidator::sanitize($data);
352
353 Logger::apiRequest("/bookings/{$id}", 'PUT', $data);
354
355 $result = $this->bookingService->updateBooking($id, $data);
356
357 if (!$result['success']) {
358 Logger::warning("Booking update failed", ['booking_id' => $id, 'data' => $data, 'result' => $result]);
359 return $this->error_response($result['message'] ?? 'Failed to update booking', 400);
360 }
361
362 Logger::info("Booking updated successfully", ['booking_id' => $id]);
363 return $this->success_response($result['data']);
364
365 } catch (\Exception $e) {
366 Logger::error("Failed to update booking", ['booking_id' => $id ?? 0, 'data' => $data ?? [], 'error' => $e->getMessage()]);
367 return $this->handle_exception($e);
368 }
369 }
370
371 /**
372 * DELETE /bookings/{id} - Delete booking
373 */
374 public function deleteBooking(WP_REST_Request $request): WP_REST_Response
375 {
376 $id = (int) $request->get_param('id');
377
378 $result = $this->bookingService->deleteBooking($id);
379
380 if (!$result['success']) {
381 return new WP_REST_Response($result, 400);
382 }
383
384 return new WP_REST_Response($result);
385 }
386
387 /**
388 * PUT /bookings/{id}/status - Update booking status
389 */
390 public function updateBookingStatus(WP_REST_Request $request): WP_REST_Response
391 {
392 $id = (int) $request->get_param('id');
393 $data = $request->get_json_params();
394 $status = $data['status'] ?? '';
395
396 if (empty($status)) {
397 return new WP_REST_Response([
398 'success' => false,
399 'message' => __('Status is required.', 'yatra'),
400 ], 400);
401 }
402
403 $result = $this->bookingService->updateStatus($id, $status);
404
405 if (!$result['success']) {
406 return new WP_REST_Response($result, 400);
407 }
408
409 return new WP_REST_Response($result);
410 }
411
412 /**
413 * GET /bookings/stats - Get booking statistics
414 */
415 public function getBookingStats(WP_REST_Request $request): WP_REST_Response
416 {
417 $stats = $this->bookingService->getStats();
418
419 return new WP_REST_Response($stats ?? []);
420
421 }
422
423 /**
424 * POST /bookings/{id}/send-email - Send booking email
425 */
426 public function sendBookingEmail(WP_REST_Request $request): WP_REST_Response
427 {
428 $id = (int) $request->get_param('id');
429 $data = $request->get_json_params();
430 $emailType = $data['type'] ?? 'confirmation';
431
432 $result = $this->bookingService->sendEmail($id, $emailType);
433
434 if (!$result['success']) {
435 return new WP_REST_Response($result, 400);
436 }
437
438 return new WP_REST_Response($result);
439 }
440
441 // =========================================================================
442 // PAYMENT ENDPOINTS
443 // =========================================================================
444
445 /**
446 * GET /bookings/{id}/payments - Get booking payments
447 */
448 public function getBookingPayments(WP_REST_Request $request): WP_REST_Response
449 {
450 $bookingId = (int) $request->get_param('id');
451
452 $payments = $this->paymentService->getBookingPayments($bookingId);
453
454 return new WP_REST_Response([
455 'success' => true,
456 'data' => $payments,
457 ]);
458 }
459
460 /**
461 * POST /bookings/{id}/payments - Add payment to booking
462 */
463 public function addPayment(WP_REST_Request $request): WP_REST_Response
464 {
465 $bookingId = (int) $request->get_param('id');
466 $data = $request->get_json_params();
467 $data['booking_id'] = $bookingId;
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 - List all payments
480 */
481 public function getPayments(WP_REST_Request $request): WP_REST_Response
482 {
483 $filters = [
484 'page' => (int) ($request->get_param('page') ?: 1),
485 'per_page' => (int) ($request->get_param('per_page') ?: 20),
486 'booking_id' => (int) $request->get_param('booking_id'),
487 'status' => $request->get_param('status') ?: '',
488 'gateway' => $request->get_param('gateway') ?: '',
489 'search' => $request->get_param('search') ?: '',
490 'date_from' => $request->get_param('date_from') ?: '',
491 'date_to' => $request->get_param('date_to') ?: '',
492 ];
493
494 $result = $this->paymentService->getPayments($filters);
495
496 return new WP_REST_Response([
497 'success' => true,
498 'data' => $result['data'],
499 'meta' => [
500 'total' => $result['total'],
501 'page' => $result['page'],
502 'per_page' => $result['per_page'],
503 'total_pages' => $result['total_pages'],
504 ],
505 ]);
506 }
507
508 /**
509 * POST /payments - Create payment
510 */
511 public function createPayment(WP_REST_Request $request): WP_REST_Response
512 {
513 $data = $request->get_json_params();
514
515 $result = $this->paymentService->createPayment($data);
516
517 if (!$result['success']) {
518 return new WP_REST_Response($result, 400);
519 }
520
521 return new WP_REST_Response($result, 201);
522 }
523
524 /**
525 * GET /payments/{id} - Get single payment
526 */
527 public function getPayment(WP_REST_Request $request): WP_REST_Response
528 {
529 $id = (int) $request->get_param('id');
530
531 $payment = $this->paymentService->getPayment($id);
532
533 if (!$payment) {
534 return new WP_REST_Response([
535 'success' => false,
536 'message' => __('Payment not found.', 'yatra'),
537 ], 404);
538 }
539
540 return new WP_REST_Response([
541 'success' => true,
542 'data' => $payment,
543 ]);
544 }
545
546 /**
547 * PUT /payments/{id} - Update payment
548 */
549 public function updatePayment(WP_REST_Request $request): WP_REST_Response
550 {
551 $id = (int) $request->get_param('id');
552 $data = $request->get_json_params();
553
554 $result = $this->paymentService->updatePayment($id, $data);
555
556 if (!$result['success']) {
557 return new WP_REST_Response($result, 400);
558 }
559
560 return new WP_REST_Response($result);
561 }
562
563 /**
564 * DELETE /payments/{id} - Delete payment
565 */
566 public function deletePayment(WP_REST_Request $request): WP_REST_Response
567 {
568 $id = (int) $request->get_param('id');
569
570 $result = $this->paymentService->deletePayment($id);
571
572 if (!$result['success']) {
573 return new WP_REST_Response($result, 400);
574 }
575
576 return new WP_REST_Response($result);
577 }
578
579 // =========================================================================
580 // TRAVELERS ENDPOINTS
581 // =========================================================================
582
583 /**
584 * GET /travelers - Get all travelers
585 */
586 public function getTravelers(WP_REST_Request $request): WP_REST_Response
587 {
588 $filters = [
589 'page' => (int) ($request->get_param('page') ?: 1),
590 'per_page' => (int) ($request->get_param('per_page') ?: 20),
591 'search' => $request->get_param('search') ?: '',
592 'trip_id' => (int) $request->get_param('trip_id'),
593 ];
594
595 $result = $this->bookingService->getTravelers($filters);
596
597 return new WP_REST_Response([
598 'success' => true,
599 'data' => $result['data'],
600 'meta' => $result['meta'] ?? [],
601 ]);
602 }
603
604 /**
605 * PUT /travelers/bulk - Bulk traveler actions
606 */
607 public function bulkTravelers(WP_REST_Request $request): WP_REST_Response
608 {
609 $data = $request->get_json_params();
610 $action = $data['action'] ?? '';
611 $ids = $data['ids'] ?? [];
612
613 if (empty($action) || empty($ids) || !is_array($ids)) {
614 return new WP_REST_Response([
615 'success' => false,
616 'message' => __('Action and IDs are required.', 'yatra'),
617 ], 400);
618 }
619
620 $ids = array_filter(array_map('intval', $ids));
621
622 if (empty($ids)) {
623 return new WP_REST_Response([
624 'success' => false,
625 'message' => __('No valid traveler IDs provided.', 'yatra'),
626 ], 400);
627 }
628
629 $result = $this->bookingService->bulkTravelers($ids, (string) $action);
630
631 return new WP_REST_Response($result, $result['success'] ? 200 : 400);
632 }
633
634 /**
635 * GET /bookings/{id}/voucher - Download travel voucher for a booking
636 */
637 public function downloadVoucher(WP_REST_Request $request)
638 {
639 $bookingId = (int) $request->get_param('id');
640 $isPreview = $request->get_param('preview') === '1';
641 $isDownload = $request->get_param('download') === '1';
642
643 if ($bookingId <= 0) {
644 return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]);
645 }
646
647 // Get booking details
648 $booking = $this->bookingService->getBooking($bookingId);
649
650 if (!$booking) {
651 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
652 }
653
654 // Verify user is logged in and owns this booking (or is admin)
655 $currentUserId = get_current_user_id();
656 $bookingUserId = (int) ($booking['user_id'] ?? 0);
657
658 // Must be logged in
659 if (!$currentUserId) {
660 return new WP_Error('unauthorized', __('You must be logged in to download vouchers.', 'yatra'), ['status' => 401]);
661 }
662
663 // Must own the booking or be admin
664 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
665 return new WP_Error('forbidden', __('You do not have permission to access this voucher.', 'yatra'), ['status' => 403]);
666 }
667
668 // Get payment for this booking
669 $payments = $this->paymentService->getBookingPayments($bookingId);
670
671 if (empty($payments)) {
672 return $this->renderVoucherFromBookingData($booking, $isPreview);
673 }
674
675 // Use the first payment (or you could use the latest payment)
676 $payment = $payments[0];
677 $paymentId = (int) ($payment['id'] ?? 0);
678
679 if ($paymentId <= 0) {
680 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
681 }
682
683 // Delegate to PaymentGatewayController's download_voucher method
684 $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController();
685
686 // Create a new request with the payment ID
687 $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/voucher");
688 $paymentRequest->set_param('payment_id', $paymentId);
689 $paymentRequest->set_param('preview', $isPreview ? '1' : '');
690 $paymentRequest->set_param('download', $isDownload ? '1' : '');
691
692 return $paymentGatewayController->download_voucher($paymentRequest);
693 }
694
695 /**
696 * GET /bookings/{id}/itinerary - Download travel itinerary for a booking
697 */
698 public function downloadItinerary(WP_REST_Request $request)
699 {
700 $bookingId = (int) $request->get_param('id');
701 $isPreview = $request->get_param('preview') === '1';
702 $isDownload = $request->get_param('download') === '1';
703
704 if ($bookingId <= 0) {
705 return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]);
706 }
707
708 // Get booking details
709 $booking = $this->bookingService->getBooking($bookingId);
710
711 if (!$booking) {
712 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
713 }
714
715 // Verify user is logged in and owns this booking (or is admin)
716 $currentUserId = get_current_user_id();
717 $bookingUserId = (int) ($booking['user_id'] ?? 0);
718
719 // Must be logged in
720 if (!$currentUserId) {
721 return new WP_Error('unauthorized', __('You must be logged in to download itineraries.', 'yatra'), ['status' => 401]);
722 }
723
724 // Must own the booking or be admin
725 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
726 return new WP_Error('forbidden', __('You do not have permission to access this itinerary.', 'yatra'), ['status' => 403]);
727 }
728
729 // Get payment for this booking
730 $payments = $this->paymentService->getBookingPayments($bookingId);
731
732 if (empty($payments)) {
733 return $this->renderItineraryFromBookingData($booking, $isPreview);
734 }
735
736 // Use the first payment (or you could use the latest payment)
737 $payment = $payments[0];
738 $paymentId = (int) ($payment['id'] ?? 0);
739
740 if ($paymentId <= 0) {
741 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
742 }
743
744 // Delegate to PaymentGatewayController's download_itinerary method
745 $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController();
746
747 // Create a new request with the payment ID
748 $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/itinerary");
749 $paymentRequest->set_param('payment_id', $paymentId);
750 $paymentRequest->set_param('preview', $isPreview ? '1' : '');
751 $paymentRequest->set_param('download', $isDownload ? '1' : '');
752
753 return $paymentGatewayController->download_itinerary($paymentRequest);
754 }
755
756 /**
757 * Voucher PDF when the booking has no payment rows yet (matches payment-based voucher layout).
758 *
759 * @param array<string,mixed> $booking From BookingService::getBooking()
760 */
761 private function renderVoucherFromBookingData(array $booking, bool $isPreview)
762 {
763 $tripRepository = new TripRepository();
764 $trip = null;
765 $tripId = (int) ($booking['trip_id'] ?? 0);
766 if ($tripId > 0) {
767 $trip = $tripRepository->find($tripId);
768 }
769
770 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
771 $companyAddress = SettingsService::get('company_address', '');
772 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
773 $companyPhone = SettingsService::get('company_phone', '');
774 $currency = SettingsService::getCurrency();
775 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
776
777 $createdAt = $booking['created_at'] ?? $booking['booking_date'] ?? '';
778 $bookingDate = !empty($createdAt) ? date_i18n(get_option('date_format'), strtotime((string) $createdAt)) : '';
779 $travelDateRaw = $booking['travel_date'] ?? '';
780 $travelDate = !empty($travelDateRaw) ? date_i18n(get_option('date_format'), strtotime((string) $travelDateRaw)) : '';
781
782 $returnDate = '';
783 if (!empty($travelDateRaw) && $trip && !empty($trip->duration_days)) {
784 $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . (int) $trip->duration_days . ' days');
785 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
786 }
787
788 $statusRaw = (string) ($booking['booking_status'] ?? $booking['status'] ?? '');
789 $bookingRef = (string) ($booking['booking_number'] ?? $booking['reference'] ?? (string) ($booking['id'] ?? ''));
790 $filename = 'Travel Voucher #' . $bookingRef . '.pdf';
791
792 $customerName = trim(
793 (string) ($booking['contact_first_name'] ?? '') . ' ' . (string) ($booking['contact_last_name'] ?? '')
794 ) ?: (string) ($booking['customer_name'] ?? __('Customer', 'yatra'));
795
796 $templateData = [
797 'company_name' => $companyName,
798 'company_address' => $companyAddress,
799 'company_email' => $companyEmail,
800 'company_phone' => $companyPhone,
801 'customer_name' => $customerName,
802 'customer_email' => (string) ($booking['contact_email'] ?? $booking['customer_email'] ?? ''),
803 'booking_ref' => $bookingRef,
804 'booking_date' => $bookingDate,
805 'booking_status' => ucfirst($statusRaw ?: 'pending'),
806 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
807 (in_array(strtolower($statusRaw), ['cancelled'], true) ? 'cancelled' : 'pending'),
808 'trip_title' => $trip ? ($trip->title ?? $booking['trip_title'] ?? __('Trip Booking', 'yatra')) : ($booking['trip_title'] ?? __('Trip Booking', 'yatra')),
809 // Trip duration comes from duration_days/duration_nights (there is no
810 // `duration` column — accessing it caused a blank value + PHP notice).
811 'trip_duration' => $trip
812 ? yatra_format_duration((int) ($trip->duration_days ?? 0), isset($trip->duration_nights) ? (int) $trip->duration_nights : null)
813 : '',
814 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
815 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
816 'destination' => $trip ? ($trip->destination ?? '') : '',
817 'travel_date' => $travelDate,
818 'return_date' => $returnDate,
819 'currency_symbol' => $currencySymbol,
820 'total_amount' => number_format((float) ($booking['total_amount'] ?? 0), 2),
821 'amount_paid' => number_format((float) ($booking['amount_paid'] ?? 0), 2),
822 'amount_due' => number_format((float) ($booking['amount_due'] ?? 0), 2),
823 'traveler_count' => (int) ($booking['travelers_count'] ?? $booking['travelers'] ?? 1),
824 ];
825
826 $pdfService = new PdfService();
827 if (!$pdfService->isAvailable()) {
828 return new WP_Error(
829 'pdf_engine_missing',
830 __('Voucher PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
831 ['status' => 500]
832 );
833 }
834
835 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/voucher.php', $templateData, [
836 'paper' => 'A4',
837 'orientation' => 'portrait',
838 'default_font' => 'DejaVu Sans',
839 ]);
840
841 if ($isPreview) {
842 return new WP_REST_Response([
843 'success' => true,
844 'pdf_data' => base64_encode($pdfBinary),
845 'filename' => $filename,
846 ]);
847 }
848
849 $pdfService->outputPdfDownload($pdfBinary, $filename);
850 exit;
851 }
852
853 /**
854 * Itinerary PDF when the booking has no payment rows yet.
855 *
856 * @param array<string,mixed> $booking From BookingService::getBooking()
857 */
858 private function renderItineraryFromBookingData(array $booking, bool $isPreview)
859 {
860 $builder = new \Yatra\Services\ItineraryPdfBuilder();
861 if (!$builder->pdfService()->isAvailable()) {
862 return new WP_Error(
863 'pdf_engine_missing',
864 __('Itinerary PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
865 ['status' => 500]
866 );
867 }
868
869 $bookingId = (int) ($booking['id'] ?? 0);
870 $bookingRef = $bookingId > 0
871 ? 'YTR-' . strtoupper(str_pad((string) $bookingId, 8, '0', STR_PAD_LEFT))
872 : 'PENDING';
873 $filename = 'Travel-Itinerary-' . $bookingRef . '.pdf';
874
875 // The builder accepts the booking array shape directly — just
876 // forward `id` as `booking_id` so the reference resolves the
877 // same as the legacy code, and let it normalise everything else.
878 $source = $booking + ['booking_id' => $bookingId];
879 $pdfBinary = $builder->build($source);
880
881 if ($isPreview) {
882 return new WP_REST_Response([
883 'success' => true,
884 'pdf_data' => base64_encode($pdfBinary),
885 'filename' => $filename,
886 ]);
887 }
888
889 $builder->pdfService()->outputPdfDownload($pdfBinary, $filename);
890 exit;
891 }
892 }
893