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

908 lines 33.1 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 // Column sorting from the table headers — whitelisted in the repository.
264 'orderby' => $request->get_param('orderby') ?: '',
265 'order' => $request->get_param('order') ?: '',
266 ];
267
268 // Delegate to service
269 $result = $this->bookingService->getBookings($filters);
270
271 return new WP_REST_Response([
272 'success' => true,
273 'data' => $result['data'],
274 'meta' => [
275 'total' => $result['total'],
276 'page' => $result['page'],
277 'per_page' => $result['per_page'],
278 'total_pages' => $result['total_pages'],
279 ],
280 ]);
281 }
282
283 /**
284 * GET /bookings/{id} - Get single booking
285 */
286 public function getBooking(WP_REST_Request $request)
287 {
288 try {
289 $id = (int) $request->get_param('id');
290
291 if ($id <= 0) {
292 throw new ValidationException('Invalid booking ID', ['id' => ['Booking ID must be a positive integer']]);
293 }
294
295 Logger::apiRequest("/bookings/{$id}", 'GET');
296
297 $booking = $this->bookingService->getBooking($id);
298
299 if (!$booking) {
300 Logger::warning("Booking not found", ['booking_id' => $id]);
301 return $this->not_found(__('Booking not found', 'yatra'));
302 }
303
304 Logger::info("Booking retrieved successfully", ['booking_id' => $id]);
305 return $this->success_response($booking);
306
307 } catch (\Exception $e) {
308 Logger::error("Failed to get booking", ['booking_id' => $id ?? 0, 'error' => $e->getMessage()]);
309 return $this->handle_exception($e);
310 }
311 }
312
313 /**
314 * POST /bookings - Create booking
315 */
316 public function createBooking(WP_REST_Request $request)
317 {
318 try {
319 $data = $request->get_json_params();
320
321 // Validate and sanitize input data
322 BookingValidator::validateCreate($data);
323 $data = BookingValidator::sanitize($data);
324
325 Logger::apiRequest('/bookings', 'POST', $data);
326
327 $result = $this->bookingService->createBooking($data);
328
329 if (!$result['success']) {
330 Logger::warning("Booking creation failed", ['data' => $data, 'result' => $result]);
331 return $this->error_response($result['message'] ?? 'Failed to create booking', 400);
332 }
333
334 Logger::info("Booking created successfully", ['booking_id' => $result['data']['id'] ?? null]);
335 return $this->success_response($result['data'], 201);
336
337 } catch (\Exception $e) {
338 Logger::error("Failed to create booking", ['data' => $data ?? [], 'error' => $e->getMessage()]);
339 return $this->handle_exception($e);
340 }
341 }
342
343 /**
344 * PUT /bookings/{id} - Update booking
345 */
346 public function updateBooking(WP_REST_Request $request)
347 {
348 try {
349 $id = (int) $request->get_param('id');
350 $data = $request->get_json_params();
351
352 // Validate and sanitize input data
353 BookingValidator::validateUpdate($data, $id);
354 $data = BookingValidator::sanitize($data);
355
356 Logger::apiRequest("/bookings/{$id}", 'PUT', $data);
357
358 $result = $this->bookingService->updateBooking($id, $data);
359
360 if (!$result['success']) {
361 Logger::warning("Booking update failed", ['booking_id' => $id, 'data' => $data, 'result' => $result]);
362 return $this->error_response($result['message'] ?? 'Failed to update booking', 400);
363 }
364
365 Logger::info("Booking updated successfully", ['booking_id' => $id]);
366 return $this->success_response($result['data'] ?? null);
367
368 } catch (\Exception $e) {
369 Logger::error("Failed to update booking", ['booking_id' => $id ?? 0, 'data' => $data ?? [], 'error' => $e->getMessage()]);
370 return $this->handle_exception($e);
371 }
372 }
373
374 /**
375 * DELETE /bookings/{id} - Delete booking
376 */
377 public function deleteBooking(WP_REST_Request $request): WP_REST_Response
378 {
379 $id = (int) $request->get_param('id');
380
381 $result = $this->bookingService->deleteBooking($id);
382
383 if (!$result['success']) {
384 return new WP_REST_Response($result, 400);
385 }
386
387 return new WP_REST_Response($result);
388 }
389
390 /**
391 * PUT /bookings/{id}/status - Update booking status
392 */
393 public function updateBookingStatus(WP_REST_Request $request): WP_REST_Response
394 {
395 $id = (int) $request->get_param('id');
396 $data = $request->get_json_params();
397 $status = $data['status'] ?? '';
398
399 if (empty($status)) {
400 return new WP_REST_Response([
401 'success' => false,
402 'message' => __('Status is required.', 'yatra'),
403 ], 400);
404 }
405
406 $result = $this->bookingService->updateStatus($id, $status);
407
408 if (!$result['success']) {
409 return new WP_REST_Response($result, 400);
410 }
411
412 return new WP_REST_Response($result);
413 }
414
415 /**
416 * GET /bookings/stats - Get booking statistics
417 */
418 public function getBookingStats(WP_REST_Request $request): WP_REST_Response
419 {
420 $stats = $this->bookingService->getStats();
421
422 return new WP_REST_Response($stats ?? []);
423
424 }
425
426 /**
427 * POST /bookings/{id}/send-email - Send booking email
428 */
429 public function sendBookingEmail(WP_REST_Request $request): WP_REST_Response
430 {
431 $id = (int) $request->get_param('id');
432 $data = $request->get_json_params();
433 $emailType = $data['type'] ?? 'confirmation';
434
435 $result = $this->bookingService->sendEmail($id, $emailType);
436
437 if (!$result['success']) {
438 return new WP_REST_Response($result, 400);
439 }
440
441 return new WP_REST_Response($result);
442 }
443
444 // =========================================================================
445 // PAYMENT ENDPOINTS
446 // =========================================================================
447
448 /**
449 * GET /bookings/{id}/payments - Get booking payments
450 */
451 public function getBookingPayments(WP_REST_Request $request): WP_REST_Response
452 {
453 $bookingId = (int) $request->get_param('id');
454
455 $payments = $this->paymentService->getBookingPayments($bookingId);
456
457 return new WP_REST_Response([
458 'success' => true,
459 'data' => $payments,
460 ]);
461 }
462
463 /**
464 * POST /bookings/{id}/payments - Add payment to booking
465 */
466 public function addPayment(WP_REST_Request $request): WP_REST_Response
467 {
468 $bookingId = (int) $request->get_param('id');
469 $data = $request->get_json_params();
470 $data['booking_id'] = $bookingId;
471
472 $result = $this->paymentService->createPayment($data);
473
474 if (!$result['success']) {
475 return new WP_REST_Response($result, 400);
476 }
477
478 return new WP_REST_Response($result, 201);
479 }
480
481 /**
482 * GET /payments - List all payments
483 */
484 public function getPayments(WP_REST_Request $request): WP_REST_Response
485 {
486 $filters = [
487 'page' => (int) ($request->get_param('page') ?: 1),
488 'per_page' => (int) ($request->get_param('per_page') ?: 20),
489 'booking_id' => (int) $request->get_param('booking_id'),
490 'status' => $request->get_param('status') ?: '',
491 'gateway' => $request->get_param('gateway') ?: '',
492 'search' => $request->get_param('search') ?: '',
493 'date_from' => $request->get_param('date_from') ?: '',
494 'date_to' => $request->get_param('date_to') ?: '',
495 ];
496
497 $result = $this->paymentService->getPayments($filters);
498
499 return new WP_REST_Response([
500 'success' => true,
501 'data' => $result['data'],
502 'meta' => [
503 'total' => $result['total'],
504 'page' => $result['page'],
505 'per_page' => $result['per_page'],
506 'total_pages' => $result['total_pages'],
507 ],
508 ]);
509 }
510
511 /**
512 * POST /payments - Create payment
513 */
514 public function createPayment(WP_REST_Request $request): WP_REST_Response
515 {
516 $data = $request->get_json_params();
517
518 $result = $this->paymentService->createPayment($data);
519
520 if (!$result['success']) {
521 return new WP_REST_Response($result, 400);
522 }
523
524 return new WP_REST_Response($result, 201);
525 }
526
527 /**
528 * GET /payments/{id} - Get single payment
529 */
530 public function getPayment(WP_REST_Request $request): WP_REST_Response
531 {
532 $id = (int) $request->get_param('id');
533
534 $payment = $this->paymentService->getPayment($id);
535
536 if (!$payment) {
537 return new WP_REST_Response([
538 'success' => false,
539 'message' => __('Payment not found.', 'yatra'),
540 ], 404);
541 }
542
543 return new WP_REST_Response([
544 'success' => true,
545 'data' => $payment,
546 ]);
547 }
548
549 /**
550 * PUT /payments/{id} - Update payment
551 */
552 public function updatePayment(WP_REST_Request $request): WP_REST_Response
553 {
554 $id = (int) $request->get_param('id');
555 $data = $request->get_json_params();
556
557 $result = $this->paymentService->updatePayment($id, $data);
558
559 if (!$result['success']) {
560 return new WP_REST_Response($result, 400);
561 }
562
563 return new WP_REST_Response($result);
564 }
565
566 /**
567 * DELETE /payments/{id} - Delete payment
568 */
569 public function deletePayment(WP_REST_Request $request): WP_REST_Response
570 {
571 $id = (int) $request->get_param('id');
572
573 $result = $this->paymentService->deletePayment($id);
574
575 if (!$result['success']) {
576 return new WP_REST_Response($result, 400);
577 }
578
579 return new WP_REST_Response($result);
580 }
581
582 // =========================================================================
583 // TRAVELERS ENDPOINTS
584 // =========================================================================
585
586 /**
587 * GET /travelers - Get all travelers
588 */
589 public function getTravelers(WP_REST_Request $request): WP_REST_Response
590 {
591 $filters = [
592 'page' => (int) ($request->get_param('page') ?: 1),
593 'per_page' => (int) ($request->get_param('per_page') ?: 20),
594 'search' => $request->get_param('search') ?: '',
595 'trip_id' => (int) $request->get_param('trip_id'),
596 ];
597
598 $result = $this->bookingService->getTravelers($filters);
599
600 return new WP_REST_Response([
601 'success' => true,
602 'data' => $result['data'],
603 'meta' => $result['meta'] ?? [],
604 ]);
605 }
606
607 /**
608 * PUT /travelers/bulk - Bulk traveler actions
609 */
610 public function bulkTravelers(WP_REST_Request $request): WP_REST_Response
611 {
612 $data = $request->get_json_params();
613 $action = $data['action'] ?? '';
614 $ids = $data['ids'] ?? [];
615
616 if (empty($action) || empty($ids) || !is_array($ids)) {
617 return new WP_REST_Response([
618 'success' => false,
619 'message' => __('Action and IDs are required.', 'yatra'),
620 ], 400);
621 }
622
623 $ids = array_filter(array_map('intval', $ids));
624
625 if (empty($ids)) {
626 return new WP_REST_Response([
627 'success' => false,
628 'message' => __('No valid traveler IDs provided.', 'yatra'),
629 ], 400);
630 }
631
632 $result = $this->bookingService->bulkTravelers($ids, (string) $action);
633
634 return new WP_REST_Response($result, $result['success'] ? 200 : 400);
635 }
636
637 /**
638 * GET /bookings/{id}/voucher - Download travel voucher for a booking
639 */
640 public function downloadVoucher(WP_REST_Request $request)
641 {
642 $bookingId = (int) $request->get_param('id');
643 $isPreview = $request->get_param('preview') === '1';
644 $isDownload = $request->get_param('download') === '1';
645
646 if ($bookingId <= 0) {
647 return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]);
648 }
649
650 // Get booking details
651 $booking = $this->bookingService->getBooking($bookingId);
652
653 if (!$booking) {
654 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
655 }
656
657 // Verify user is logged in and owns this booking (or is admin)
658 $currentUserId = get_current_user_id();
659 $bookingUserId = (int) ($booking['user_id'] ?? 0);
660
661 // Must be logged in
662 if (!$currentUserId) {
663 return new WP_Error('unauthorized', __('You must be logged in to download vouchers.', 'yatra'), ['status' => 401]);
664 }
665
666 // Must own the booking or be admin
667 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
668 return new WP_Error('forbidden', __('You do not have permission to access this voucher.', 'yatra'), ['status' => 403]);
669 }
670
671 // Get payment for this booking
672 $payments = $this->paymentService->getBookingPayments($bookingId);
673
674 if (empty($payments)) {
675 return $this->renderVoucherFromBookingData($booking, $isPreview);
676 }
677
678 // Use the first payment (or you could use the latest payment)
679 $payment = $payments[0];
680 $paymentId = (int) ($payment['id'] ?? 0);
681
682 if ($paymentId <= 0) {
683 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
684 }
685
686 // Delegate to PaymentGatewayController's download_voucher method
687 $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController();
688
689 // Create a new request with the payment ID
690 $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/voucher");
691 $paymentRequest->set_param('payment_id', $paymentId);
692 $paymentRequest->set_param('preview', $isPreview ? '1' : '');
693 $paymentRequest->set_param('download', $isDownload ? '1' : '');
694
695 return $paymentGatewayController->download_voucher($paymentRequest);
696 }
697
698 /**
699 * GET /bookings/{id}/itinerary - Download travel itinerary for a booking
700 */
701 public function downloadItinerary(WP_REST_Request $request)
702 {
703 $bookingId = (int) $request->get_param('id');
704 $isPreview = $request->get_param('preview') === '1';
705 $isDownload = $request->get_param('download') === '1';
706
707 if ($bookingId <= 0) {
708 return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]);
709 }
710
711 // Get booking details
712 $booking = $this->bookingService->getBooking($bookingId);
713
714 if (!$booking) {
715 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
716 }
717
718 // Verify user is logged in and owns this booking (or is admin)
719 $currentUserId = get_current_user_id();
720 $bookingUserId = (int) ($booking['user_id'] ?? 0);
721
722 // Must be logged in
723 if (!$currentUserId) {
724 return new WP_Error('unauthorized', __('You must be logged in to download itineraries.', 'yatra'), ['status' => 401]);
725 }
726
727 // Must own the booking or be admin
728 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
729 return new WP_Error('forbidden', __('You do not have permission to access this itinerary.', 'yatra'), ['status' => 403]);
730 }
731
732 // Get payment for this booking
733 $payments = $this->paymentService->getBookingPayments($bookingId);
734
735 if (empty($payments)) {
736 return $this->renderItineraryFromBookingData($booking, $isPreview);
737 }
738
739 // Use the first payment (or you could use the latest payment)
740 $payment = $payments[0];
741 $paymentId = (int) ($payment['id'] ?? 0);
742
743 if ($paymentId <= 0) {
744 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
745 }
746
747 // Delegate to PaymentGatewayController's download_itinerary method
748 $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController();
749
750 // Create a new request with the payment ID
751 $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/itinerary");
752 $paymentRequest->set_param('payment_id', $paymentId);
753 $paymentRequest->set_param('preview', $isPreview ? '1' : '');
754 $paymentRequest->set_param('download', $isDownload ? '1' : '');
755
756 return $paymentGatewayController->download_itinerary($paymentRequest);
757 }
758
759 /**
760 * Voucher PDF when the booking has no payment rows yet (matches payment-based voucher layout).
761 *
762 * @param array<string,mixed> $booking From BookingService::getBooking()
763 */
764 private function renderVoucherFromBookingData(array $booking, bool $isPreview)
765 {
766 $tripRepository = new TripRepository();
767 $trip = null;
768 $tripId = (int) ($booking['trip_id'] ?? 0);
769 if ($tripId > 0) {
770 $trip = $tripRepository->find($tripId);
771 }
772
773 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
774 $companyAddress = SettingsService::get('company_address', '');
775 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
776 $companyPhone = SettingsService::get('company_phone', '');
777 $currency = SettingsService::getCurrency();
778 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
779
780 $createdAt = $booking['created_at'] ?? $booking['booking_date'] ?? '';
781 $bookingDate = !empty($createdAt) ? date_i18n(get_option('date_format'), strtotime((string) $createdAt)) : '';
782 $travelDateRaw = $booking['travel_date'] ?? '';
783 $travelDate = !empty($travelDateRaw) ? date_i18n(get_option('date_format'), strtotime((string) $travelDateRaw)) : '';
784
785 // Return date. Prefer the booking's STORED end_date — the actual booked
786 // return (accounts for a flexible window or a trip duration changed after
787 // booking). Fall back to the trip duration only when no end is stored:
788 // duration_days is INCLUSIVE, so the return is travel_date + (days - 1)
789 // (matches BookingRepository::calculateEndDate; a bare "+ duration_days"
790 // was one day too far and implied an extra night — see ItineraryPdfBuilder).
791 $returnDate = '';
792 $storedEnd = (string) ($booking['end_date'] ?? '');
793 if ($storedEnd !== '' && ($travelDateRaw === '' || $storedEnd >= $travelDateRaw)) {
794 $returnDate = date_i18n(get_option('date_format'), strtotime($storedEnd));
795 } elseif (!empty($travelDateRaw) && $trip && !empty($trip->duration_days)) {
796 $returnOffset = max(0, (int) $trip->duration_days - 1);
797 $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . $returnOffset . ' days');
798 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
799 }
800
801 $statusRaw = (string) ($booking['booking_status'] ?? $booking['status'] ?? '');
802 $bookingRef = (string) ($booking['booking_number'] ?? $booking['reference'] ?? (string) ($booking['id'] ?? ''));
803 $filename = 'Travel Voucher #' . $bookingRef . '.pdf';
804
805 $customerName = trim(
806 (string) ($booking['contact_first_name'] ?? '') . ' ' . (string) ($booking['contact_last_name'] ?? '')
807 ) ?: (string) ($booking['customer_name'] ?? __('Customer', 'yatra'));
808
809 $templateData = [
810 'company_name' => $companyName,
811 'company_address' => $companyAddress,
812 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(),
813 'company_email' => $companyEmail,
814 'company_phone' => $companyPhone,
815 'customer_name' => $customerName,
816 'customer_email' => (string) ($booking['contact_email'] ?? $booking['customer_email'] ?? ''),
817 'customer_address_lines' => FormatHelper::customerAddressLines($booking),
818 'booking_ref' => $bookingRef,
819 'booking_date' => $bookingDate,
820 'booking_status' => ucfirst($statusRaw ?: 'pending'),
821 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
822 (in_array(strtolower($statusRaw), ['cancelled'], true) ? 'cancelled' : 'pending'),
823 'trip_title' => $trip ? ($trip->title ?? $booking['trip_title'] ?? __('Trip Booking', 'yatra')) : ($booking['trip_title'] ?? __('Trip Booking', 'yatra')),
824 // Trip duration comes from duration_days/duration_nights (there is no
825 // `duration` column — accessing it caused a blank value + PHP notice).
826 'trip_duration' => $trip
827 ? yatra_format_duration((int) ($trip->duration_days ?? 0), isset($trip->duration_nights) ? (int) $trip->duration_nights : null)
828 : '',
829 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
830 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
831 'destination' => $trip ? ($trip->destination ?? '') : '',
832 'travel_date' => $travelDate,
833 'return_date' => $returnDate,
834 'currency_symbol' => $currencySymbol,
835 'total_amount' => yatra_format_price((float) ($booking['total_amount'] ?? 0), $currency, false),
836 'amount_paid' => yatra_format_price((float) ($booking['amount_paid'] ?? 0), $currency, false),
837 'amount_due' => yatra_format_price((float) ($booking['amount_due'] ?? 0), $currency, false),
838 'traveler_count' => (int) ($booking['travelers_count'] ?? $booking['travelers'] ?? 1),
839 ];
840
841 $pdfService = new PdfService();
842 if (!$pdfService->isAvailable()) {
843 return new WP_Error(
844 'pdf_engine_missing',
845 __('Voucher PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
846 ['status' => 500]
847 );
848 }
849
850 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/voucher.php', $templateData, [
851 'paper' => 'A4',
852 'orientation' => 'portrait',
853 'default_font' => 'DejaVu Sans',
854 ]);
855
856 if ($isPreview) {
857 return new WP_REST_Response([
858 'success' => true,
859 'pdf_data' => base64_encode($pdfBinary),
860 'filename' => $filename,
861 ]);
862 }
863
864 $pdfService->outputPdfDownload($pdfBinary, $filename);
865 exit;
866 }
867
868 /**
869 * Itinerary PDF when the booking has no payment rows yet.
870 *
871 * @param array<string,mixed> $booking From BookingService::getBooking()
872 */
873 private function renderItineraryFromBookingData(array $booking, bool $isPreview)
874 {
875 $builder = new \Yatra\Services\ItineraryPdfBuilder();
876 if (!$builder->pdfService()->isAvailable()) {
877 return new WP_Error(
878 'pdf_engine_missing',
879 __('Itinerary PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
880 ['status' => 500]
881 );
882 }
883
884 $bookingId = (int) ($booking['id'] ?? 0);
885 $bookingRef = $bookingId > 0
886 ? 'YTR-' . strtoupper(str_pad((string) $bookingId, 8, '0', STR_PAD_LEFT))
887 : 'PENDING';
888 $filename = 'Travel-Itinerary-' . $bookingRef . '.pdf';
889
890 // The builder accepts the booking array shape directly — just
891 // forward `id` as `booking_id` so the reference resolves the
892 // same as the legacy code, and let it normalise everything else.
893 $source = $booking + ['booking_id' => $bookingId];
894 $pdfBinary = $builder->build($source);
895
896 if ($isPreview) {
897 return new WP_REST_Response([
898 'success' => true,
899 'pdf_data' => base64_encode($pdfBinary),
900 'filename' => $filename,
901 ]);
902 }
903
904 $builder->pdfService()->outputPdfDownload($pdfBinary, $filename);
905 exit;
906 }
907 }
908