PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.8
Yatra – Travel Booking & Tour Operator Software v3.0.2.8
3.0.15 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 All 83 releases
yatra / app / Controllers / BookingsController.php

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

1,035 lines 37.5 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 // =====================
172 // SCHEDULED PAYMENTS
173 // =====================
174
175 register_rest_route($this->namespace, '/scheduled-payments', [
176 'methods' => 'GET',
177 'callback' => [$this, 'getScheduledPayments'],
178 'permission_callback' => [$this, 'checkAdminPermission'],
179 ]);
180
181 register_rest_route($this->namespace, '/scheduled-payments/(?P<id>\d+)', [
182 'methods' => 'GET',
183 'callback' => [$this, 'getScheduledPayment'],
184 'permission_callback' => [$this, 'checkAdminPermission'],
185 ]);
186
187 register_rest_route($this->namespace, '/scheduled-payments/(?P<id>\d+)', [
188 'methods' => 'PUT',
189 'callback' => [$this, 'updateScheduledPayment'],
190 'permission_callback' => [$this, 'checkAdminPermission'],
191 ]);
192
193 register_rest_route($this->namespace, '/scheduled-payments/(?P<id>\d+)/cancel', [
194 'methods' => 'POST',
195 'callback' => [$this, 'cancelScheduledPayment'],
196 'permission_callback' => [$this, 'checkAdminPermission'],
197 ]);
198
199 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/scheduled-payments', [
200 'methods' => 'GET',
201 'callback' => [$this, 'getBookingScheduledPayments'],
202 'permission_callback' => [$this, 'checkAdminPermission'],
203 ]);
204
205 // Download travel voucher for a booking
206 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/voucher', [
207 'methods' => 'GET',
208 'callback' => [$this, 'downloadVoucher'],
209 'permission_callback' => '__return_true', // Auth checked inside callback
210 ]);
211
212 // Download travel itinerary for a booking
213 register_rest_route($this->namespace, '/bookings/(?P<id>\d+)/itinerary', [
214 'methods' => 'GET',
215 'callback' => [$this, 'downloadItinerary'],
216 'permission_callback' => '__return_true', // Auth checked inside callback
217 ]);
218 }
219
220 /**
221 * Check admin permission
222 */
223 public function checkAdminPermission(): bool
224 {
225 // Allow custom booking capability or fallback to manage_options
226 if (current_user_can('yatra_view_bookings')) {
227 return true;
228 }
229 return current_user_can('manage_options');
230 }
231
232 // =========================================================================
233 // BOOKING ENDPOINTS
234 // =========================================================================
235
236 /**
237 * GET /bookings - List all bookings
238 */
239 public function getBookings(WP_REST_Request $request): WP_REST_Response
240 {
241 // Extract filters from request
242 $filters = [
243 'page' => (int) ($request->get_param('page') ?: 1),
244 'per_page' => (int) ($request->get_param('per_page') ?: 20),
245 'status' => $request->get_param('status') ?: '',
246 'payment_status' => $request->get_param('payment_status') ?: '',
247 'trip_id' => (int) $request->get_param('trip_id'),
248 'search' => $request->get_param('search') ?: '',
249 'date_from' => $request->get_param('date_from') ?: '',
250 'date_to' => $request->get_param('date_to') ?: '',
251 ];
252
253 // Delegate to service
254 $result = $this->bookingService->getBookings($filters);
255
256 return new WP_REST_Response([
257 'success' => true,
258 'data' => $result['data'],
259 'meta' => [
260 'total' => $result['total'],
261 'page' => $result['page'],
262 'per_page' => $result['per_page'],
263 'total_pages' => $result['total_pages'],
264 ],
265 ]);
266 }
267
268 /**
269 * GET /bookings/{id} - Get single booking
270 */
271 public function getBooking(WP_REST_Request $request)
272 {
273 try {
274 $id = (int) $request->get_param('id');
275
276 if ($id <= 0) {
277 throw new ValidationException('Invalid booking ID', ['id' => ['Booking ID must be a positive integer']]);
278 }
279
280 Logger::apiRequest("/bookings/{$id}", 'GET');
281
282 $booking = $this->bookingService->getBooking($id);
283
284 if (!$booking) {
285 Logger::warning("Booking not found", ['booking_id' => $id]);
286 return $this->not_found(__('Booking not found', 'yatra'));
287 }
288
289 Logger::info("Booking retrieved successfully", ['booking_id' => $id]);
290 return $this->success_response($booking);
291
292 } catch (\Exception $e) {
293 Logger::error("Failed to get booking", ['booking_id' => $id ?? 0, 'error' => $e->getMessage()]);
294 return $this->handle_exception($e);
295 }
296 }
297
298 /**
299 * POST /bookings - Create booking
300 */
301 public function createBooking(WP_REST_Request $request)
302 {
303 try {
304 $data = $request->get_json_params();
305
306 // Validate and sanitize input data
307 BookingValidator::validateCreate($data);
308 $data = BookingValidator::sanitize($data);
309
310 Logger::apiRequest('/bookings', 'POST', $data);
311
312 $result = $this->bookingService->createBooking($data);
313
314 if (!$result['success']) {
315 Logger::warning("Booking creation failed", ['data' => $data, 'result' => $result]);
316 return $this->error_response($result['message'] ?? 'Failed to create booking', 400);
317 }
318
319 Logger::info("Booking created successfully", ['booking_id' => $result['data']['id'] ?? null]);
320 return $this->success_response($result['data'], 201);
321
322 } catch (\Exception $e) {
323 Logger::error("Failed to create booking", ['data' => $data ?? [], 'error' => $e->getMessage()]);
324 return $this->handle_exception($e);
325 }
326 }
327
328 /**
329 * PUT /bookings/{id} - Update booking
330 */
331 public function updateBooking(WP_REST_Request $request)
332 {
333 try {
334 $id = (int) $request->get_param('id');
335 $data = $request->get_json_params();
336
337 // Validate and sanitize input data
338 BookingValidator::validateUpdate($data, $id);
339 $data = BookingValidator::sanitize($data);
340
341 Logger::apiRequest("/bookings/{$id}", 'PUT', $data);
342
343 $result = $this->bookingService->updateBooking($id, $data);
344
345 if (!$result['success']) {
346 Logger::warning("Booking update failed", ['booking_id' => $id, 'data' => $data, 'result' => $result]);
347 return $this->error_response($result['message'] ?? 'Failed to update booking', 400);
348 }
349
350 Logger::info("Booking updated successfully", ['booking_id' => $id]);
351 return $this->success_response($result['data']);
352
353 } catch (\Exception $e) {
354 Logger::error("Failed to update booking", ['booking_id' => $id ?? 0, 'data' => $data ?? [], 'error' => $e->getMessage()]);
355 return $this->handle_exception($e);
356 }
357 }
358
359 /**
360 * DELETE /bookings/{id} - Delete booking
361 */
362 public function deleteBooking(WP_REST_Request $request): WP_REST_Response
363 {
364 $id = (int) $request->get_param('id');
365
366 $result = $this->bookingService->deleteBooking($id);
367
368 if (!$result['success']) {
369 return new WP_REST_Response($result, 400);
370 }
371
372 return new WP_REST_Response($result);
373 }
374
375 /**
376 * PUT /bookings/{id}/status - Update booking status
377 */
378 public function updateBookingStatus(WP_REST_Request $request): WP_REST_Response
379 {
380 $id = (int) $request->get_param('id');
381 $data = $request->get_json_params();
382 $status = $data['status'] ?? '';
383
384 if (empty($status)) {
385 return new WP_REST_Response([
386 'success' => false,
387 'message' => __('Status is required.', 'yatra'),
388 ], 400);
389 }
390
391 $result = $this->bookingService->updateStatus($id, $status);
392
393 if (!$result['success']) {
394 return new WP_REST_Response($result, 400);
395 }
396
397 return new WP_REST_Response($result);
398 }
399
400 /**
401 * GET /bookings/stats - Get booking statistics
402 */
403 public function getBookingStats(WP_REST_Request $request): WP_REST_Response
404 {
405 $stats = $this->bookingService->getStats();
406
407 return new WP_REST_Response($stats ?? []);
408
409 }
410
411 /**
412 * POST /bookings/{id}/send-email - Send booking email
413 */
414 public function sendBookingEmail(WP_REST_Request $request): WP_REST_Response
415 {
416 $id = (int) $request->get_param('id');
417 $data = $request->get_json_params();
418 $emailType = $data['type'] ?? 'confirmation';
419
420 $result = $this->bookingService->sendEmail($id, $emailType);
421
422 if (!$result['success']) {
423 return new WP_REST_Response($result, 400);
424 }
425
426 return new WP_REST_Response($result);
427 }
428
429 // =========================================================================
430 // PAYMENT ENDPOINTS
431 // =========================================================================
432
433 /**
434 * GET /bookings/{id}/payments - Get booking payments
435 */
436 public function getBookingPayments(WP_REST_Request $request): WP_REST_Response
437 {
438 $bookingId = (int) $request->get_param('id');
439
440 $payments = $this->paymentService->getBookingPayments($bookingId);
441
442 return new WP_REST_Response([
443 'success' => true,
444 'data' => $payments,
445 ]);
446 }
447
448 /**
449 * POST /bookings/{id}/payments - Add payment to booking
450 */
451 public function addPayment(WP_REST_Request $request): WP_REST_Response
452 {
453 $bookingId = (int) $request->get_param('id');
454 $data = $request->get_json_params();
455 $data['booking_id'] = $bookingId;
456
457 $result = $this->paymentService->createPayment($data);
458
459 if (!$result['success']) {
460 return new WP_REST_Response($result, 400);
461 }
462
463 return new WP_REST_Response($result, 201);
464 }
465
466 /**
467 * GET /payments - List all payments
468 */
469 public function getPayments(WP_REST_Request $request): WP_REST_Response
470 {
471 $filters = [
472 'page' => (int) ($request->get_param('page') ?: 1),
473 'per_page' => (int) ($request->get_param('per_page') ?: 20),
474 'booking_id' => (int) $request->get_param('booking_id'),
475 'status' => $request->get_param('status') ?: '',
476 'gateway' => $request->get_param('gateway') ?: '',
477 'search' => $request->get_param('search') ?: '',
478 'date_from' => $request->get_param('date_from') ?: '',
479 'date_to' => $request->get_param('date_to') ?: '',
480 ];
481
482 $result = $this->paymentService->getPayments($filters);
483
484 return new WP_REST_Response([
485 'success' => true,
486 'data' => $result['data'],
487 'meta' => [
488 'total' => $result['total'],
489 'page' => $result['page'],
490 'per_page' => $result['per_page'],
491 'total_pages' => $result['total_pages'],
492 ],
493 ]);
494 }
495
496 /**
497 * POST /payments - Create payment
498 */
499 public function createPayment(WP_REST_Request $request): WP_REST_Response
500 {
501 $data = $request->get_json_params();
502
503 $result = $this->paymentService->createPayment($data);
504
505 if (!$result['success']) {
506 return new WP_REST_Response($result, 400);
507 }
508
509 return new WP_REST_Response($result, 201);
510 }
511
512 /**
513 * GET /payments/{id} - Get single payment
514 */
515 public function getPayment(WP_REST_Request $request): WP_REST_Response
516 {
517 $id = (int) $request->get_param('id');
518
519 $payment = $this->paymentService->getPayment($id);
520
521 if (!$payment) {
522 return new WP_REST_Response([
523 'success' => false,
524 'message' => __('Payment not found.', 'yatra'),
525 ], 404);
526 }
527
528 return new WP_REST_Response([
529 'success' => true,
530 'data' => $payment,
531 ]);
532 }
533
534 /**
535 * PUT /payments/{id} - Update payment
536 */
537 public function updatePayment(WP_REST_Request $request): WP_REST_Response
538 {
539 $id = (int) $request->get_param('id');
540 $data = $request->get_json_params();
541
542 $result = $this->paymentService->updatePayment($id, $data);
543
544 if (!$result['success']) {
545 return new WP_REST_Response($result, 400);
546 }
547
548 return new WP_REST_Response($result);
549 }
550
551 /**
552 * DELETE /payments/{id} - Delete payment
553 */
554 public function deletePayment(WP_REST_Request $request): WP_REST_Response
555 {
556 $id = (int) $request->get_param('id');
557
558 $result = $this->paymentService->deletePayment($id);
559
560 if (!$result['success']) {
561 return new WP_REST_Response($result, 400);
562 }
563
564 return new WP_REST_Response($result);
565 }
566
567 // =========================================================================
568 // TRAVELERS ENDPOINTS
569 // =========================================================================
570
571 /**
572 * GET /travelers - Get all travelers
573 */
574 public function getTravelers(WP_REST_Request $request): WP_REST_Response
575 {
576 $filters = [
577 'page' => (int) ($request->get_param('page') ?: 1),
578 'per_page' => (int) ($request->get_param('per_page') ?: 20),
579 'search' => $request->get_param('search') ?: '',
580 'trip_id' => (int) $request->get_param('trip_id'),
581 ];
582
583 $result = $this->bookingService->getTravelers($filters);
584
585 return new WP_REST_Response([
586 'success' => true,
587 'data' => $result['data'],
588 'meta' => $result['meta'] ?? [],
589 ]);
590 }
591
592 /**
593 * PUT /travelers/bulk - Bulk traveler actions
594 */
595 public function bulkTravelers(WP_REST_Request $request): WP_REST_Response
596 {
597 $data = $request->get_json_params();
598 $action = $data['action'] ?? '';
599 $ids = $data['ids'] ?? [];
600
601 if (empty($action) || empty($ids) || !is_array($ids)) {
602 return new WP_REST_Response([
603 'success' => false,
604 'message' => __('Action and IDs are required.', 'yatra'),
605 ], 400);
606 }
607
608 $ids = array_filter(array_map('intval', $ids));
609
610 if (empty($ids)) {
611 return new WP_REST_Response([
612 'success' => false,
613 'message' => __('No valid traveler IDs provided.', 'yatra'),
614 ], 400);
615 }
616
617 $result = $this->bookingService->bulkTravelers($ids, (string) $action);
618
619 return new WP_REST_Response($result, $result['success'] ? 200 : 400);
620 }
621
622 // =========================================================================
623 // SCHEDULED PAYMENTS ENDPOINTS
624 // =========================================================================
625
626 /**
627 * GET /scheduled-payments - List scheduled payments
628 */
629 public function getScheduledPayments(WP_REST_Request $request): WP_REST_Response
630 {
631 $filters = [
632 'page' => (int) ($request->get_param('page') ?: 1),
633 'per_page' => (int) ($request->get_param('per_page') ?: 20),
634 'status' => $request->get_param('status') ?: '',
635 ];
636
637 $result = $this->paymentService->getScheduledPayments($filters);
638
639 return new WP_REST_Response([
640 'success' => true,
641 'data' => $result['data'],
642 'meta' => [
643 'total' => $result['total'],
644 'page' => $result['page'],
645 'per_page' => $result['per_page'],
646 'total_pages' => $result['total_pages'],
647 ],
648 ]);
649 }
650
651 /**
652 * GET /scheduled-payments/{id} - Get single scheduled payment
653 */
654 public function getScheduledPayment(WP_REST_Request $request): WP_REST_Response
655 {
656 $id = (int) $request->get_param('id');
657
658 $payment = $this->paymentService->getScheduledPayment($id);
659
660 if (!$payment) {
661 return new WP_REST_Response([
662 'success' => false,
663 'message' => __('Scheduled payment not found.', 'yatra'),
664 ], 404);
665 }
666
667 return new WP_REST_Response([
668 'success' => true,
669 'data' => $payment,
670 ]);
671 }
672
673 /**
674 * PUT /scheduled-payments/{id} - Update scheduled payment
675 */
676 public function updateScheduledPayment(WP_REST_Request $request): WP_REST_Response
677 {
678 $id = (int) $request->get_param('id');
679 $data = $request->get_json_params();
680
681 $result = $this->paymentService->updateScheduledPayment($id, $data);
682
683 if (!$result['success']) {
684 return new WP_REST_Response($result, 400);
685 }
686
687 return new WP_REST_Response($result);
688 }
689
690 /**
691 * POST /scheduled-payments/{id}/cancel - Cancel scheduled payment
692 */
693 public function cancelScheduledPayment(WP_REST_Request $request): WP_REST_Response
694 {
695 $id = (int) $request->get_param('id');
696
697 $result = $this->paymentService->cancelScheduledPayment($id);
698
699 if (!$result['success']) {
700 return new WP_REST_Response($result, 400);
701 }
702
703 return new WP_REST_Response($result);
704 }
705
706 /**
707 * GET /bookings/{id}/scheduled-payments - Get booking's scheduled payments
708 */
709 public function getBookingScheduledPayments(WP_REST_Request $request): WP_REST_Response
710 {
711 $bookingId = (int) $request->get_param('id');
712
713 $payments = $this->paymentService->getBookingScheduledPayments($bookingId);
714
715 return new WP_REST_Response([
716 'success' => true,
717 'data' => $payments,
718 ]);
719 }
720
721 /**
722 * GET /bookings/{id}/voucher - Download travel voucher for a booking
723 */
724 public function downloadVoucher(WP_REST_Request $request)
725 {
726 $bookingId = (int) $request->get_param('id');
727 $isPreview = $request->get_param('preview') === '1';
728 $isDownload = $request->get_param('download') === '1';
729
730 if ($bookingId <= 0) {
731 return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]);
732 }
733
734 // Get booking details
735 $booking = $this->bookingService->getBooking($bookingId);
736
737 if (!$booking) {
738 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
739 }
740
741 // Verify user is logged in and owns this booking (or is admin)
742 $currentUserId = get_current_user_id();
743 $bookingUserId = (int) ($booking['user_id'] ?? 0);
744
745 // Must be logged in
746 if (!$currentUserId) {
747 return new WP_Error('unauthorized', __('You must be logged in to download vouchers.', 'yatra'), ['status' => 401]);
748 }
749
750 // Must own the booking or be admin
751 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
752 return new WP_Error('forbidden', __('You do not have permission to access this voucher.', 'yatra'), ['status' => 403]);
753 }
754
755 // Get payment for this booking
756 $payments = $this->paymentService->getBookingPayments($bookingId);
757
758 if (empty($payments)) {
759 return $this->renderVoucherFromBookingData($booking, $isPreview);
760 }
761
762 // Use the first payment (or you could use the latest payment)
763 $payment = $payments[0];
764 $paymentId = (int) ($payment['id'] ?? 0);
765
766 if ($paymentId <= 0) {
767 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
768 }
769
770 // Delegate to PaymentGatewayController's download_voucher method
771 $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController();
772
773 // Create a new request with the payment ID
774 $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/voucher");
775 $paymentRequest->set_param('payment_id', $paymentId);
776 $paymentRequest->set_param('preview', $isPreview ? '1' : '');
777 $paymentRequest->set_param('download', $isDownload ? '1' : '');
778
779 return $paymentGatewayController->download_voucher($paymentRequest);
780 }
781
782 /**
783 * GET /bookings/{id}/itinerary - Download travel itinerary for a booking
784 */
785 public function downloadItinerary(WP_REST_Request $request)
786 {
787 $bookingId = (int) $request->get_param('id');
788 $isPreview = $request->get_param('preview') === '1';
789 $isDownload = $request->get_param('download') === '1';
790
791 if ($bookingId <= 0) {
792 return new WP_Error('invalid_booking', __('Invalid booking ID.', 'yatra'), ['status' => 400]);
793 }
794
795 // Get booking details
796 $booking = $this->bookingService->getBooking($bookingId);
797
798 if (!$booking) {
799 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
800 }
801
802 // Verify user is logged in and owns this booking (or is admin)
803 $currentUserId = get_current_user_id();
804 $bookingUserId = (int) ($booking['user_id'] ?? 0);
805
806 // Must be logged in
807 if (!$currentUserId) {
808 return new WP_Error('unauthorized', __('You must be logged in to download itineraries.', 'yatra'), ['status' => 401]);
809 }
810
811 // Must own the booking or be admin
812 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
813 return new WP_Error('forbidden', __('You do not have permission to access this itinerary.', 'yatra'), ['status' => 403]);
814 }
815
816 // Get payment for this booking
817 $payments = $this->paymentService->getBookingPayments($bookingId);
818
819 if (empty($payments)) {
820 return $this->renderItineraryFromBookingData($booking, $isPreview);
821 }
822
823 // Use the first payment (or you could use the latest payment)
824 $payment = $payments[0];
825 $paymentId = (int) ($payment['id'] ?? 0);
826
827 if ($paymentId <= 0) {
828 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
829 }
830
831 // Delegate to PaymentGatewayController's download_itinerary method
832 $paymentGatewayController = new \Yatra\Controllers\PaymentGatewayController();
833
834 // Create a new request with the payment ID
835 $paymentRequest = new WP_REST_Request('GET', "/payments/{$paymentId}/itinerary");
836 $paymentRequest->set_param('payment_id', $paymentId);
837 $paymentRequest->set_param('preview', $isPreview ? '1' : '');
838 $paymentRequest->set_param('download', $isDownload ? '1' : '');
839
840 return $paymentGatewayController->download_itinerary($paymentRequest);
841 }
842
843 /**
844 * Voucher PDF when the booking has no payment rows yet (matches payment-based voucher layout).
845 *
846 * @param array<string,mixed> $booking From BookingService::getBooking()
847 */
848 private function renderVoucherFromBookingData(array $booking, bool $isPreview)
849 {
850 $tripRepository = new TripRepository();
851 $trip = null;
852 $tripId = (int) ($booking['trip_id'] ?? 0);
853 if ($tripId > 0) {
854 $trip = $tripRepository->find($tripId);
855 }
856
857 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
858 $companyAddress = SettingsService::get('company_address', '');
859 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
860 $companyPhone = SettingsService::get('company_phone', '');
861 $currency = SettingsService::getCurrency();
862 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
863
864 $createdAt = $booking['created_at'] ?? $booking['booking_date'] ?? '';
865 $bookingDate = !empty($createdAt) ? date_i18n(get_option('date_format'), strtotime((string) $createdAt)) : '';
866 $travelDateRaw = $booking['travel_date'] ?? '';
867 $travelDate = !empty($travelDateRaw) ? date_i18n(get_option('date_format'), strtotime((string) $travelDateRaw)) : '';
868
869 $returnDate = '';
870 if (!empty($travelDateRaw) && $trip && !empty($trip->duration)) {
871 $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . (int) $trip->duration . ' days');
872 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
873 }
874
875 $statusRaw = (string) ($booking['booking_status'] ?? $booking['status'] ?? '');
876 $bookingRef = (string) ($booking['booking_number'] ?? $booking['reference'] ?? (string) ($booking['id'] ?? ''));
877 $filename = 'Travel Voucher #' . $bookingRef . '.pdf';
878
879 $customerName = trim(
880 (string) ($booking['contact_first_name'] ?? '') . ' ' . (string) ($booking['contact_last_name'] ?? '')
881 ) ?: (string) ($booking['customer_name'] ?? __('Customer', 'yatra'));
882
883 $templateData = [
884 'company_name' => $companyName,
885 'company_address' => $companyAddress,
886 'company_email' => $companyEmail,
887 'company_phone' => $companyPhone,
888 'customer_name' => $customerName,
889 'customer_email' => (string) ($booking['contact_email'] ?? $booking['customer_email'] ?? ''),
890 'booking_ref' => $bookingRef,
891 'booking_date' => $bookingDate,
892 'booking_status' => ucfirst($statusRaw ?: 'pending'),
893 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
894 (in_array(strtolower($statusRaw), ['cancelled'], true) ? 'cancelled' : 'pending'),
895 'trip_title' => $trip ? ($trip->title ?? $booking['trip_title'] ?? __('Trip Booking', 'yatra')) : ($booking['trip_title'] ?? __('Trip Booking', 'yatra')),
896 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
897 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
898 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
899 'destination' => $trip ? ($trip->destination ?? '') : '',
900 'travel_date' => $travelDate,
901 'return_date' => $returnDate,
902 'currency_symbol' => $currencySymbol,
903 'total_amount' => number_format((float) ($booking['total_amount'] ?? 0), 2),
904 'amount_paid' => number_format((float) ($booking['amount_paid'] ?? 0), 2),
905 'amount_due' => number_format((float) ($booking['amount_due'] ?? 0), 2),
906 'traveler_count' => (int) ($booking['travelers_count'] ?? $booking['travelers'] ?? 1),
907 ];
908
909 $pdfService = new PdfService();
910 if (!$pdfService->isAvailable()) {
911 return new WP_Error(
912 'pdf_engine_missing',
913 __('Voucher PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
914 ['status' => 500]
915 );
916 }
917
918 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/voucher.php', $templateData, [
919 'paper' => 'A4',
920 'orientation' => 'portrait',
921 'default_font' => 'DejaVu Sans',
922 ]);
923
924 if ($isPreview) {
925 return new WP_REST_Response([
926 'success' => true,
927 'pdf_data' => base64_encode($pdfBinary),
928 'filename' => $filename,
929 ]);
930 }
931
932 $pdfService->outputPdfDownload($pdfBinary, $filename);
933 exit;
934 }
935
936 /**
937 * Itinerary PDF when the booking has no payment rows yet.
938 *
939 * @param array<string,mixed> $booking From BookingService::getBooking()
940 */
941 private function renderItineraryFromBookingData(array $booking, bool $isPreview)
942 {
943 $tripRepository = new TripRepository();
944 $trip = null;
945 $tripId = (int) ($booking['trip_id'] ?? 0);
946 if ($tripId > 0) {
947 $trip = $tripRepository->find($tripId);
948 }
949
950 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
951 $companyAddress = SettingsService::get('company_address', '');
952 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
953 $companyPhone = SettingsService::get('company_phone', '');
954 $currency = SettingsService::getCurrency();
955 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
956
957 $createdAt = $booking['created_at'] ?? $booking['booking_date'] ?? '';
958 $bookingDate = !empty($createdAt) ? date_i18n(get_option('date_format'), strtotime((string) $createdAt)) : '';
959 $travelDateRaw = $booking['travel_date'] ?? '';
960 $travelDate = !empty($travelDateRaw) ? date_i18n(get_option('date_format'), strtotime((string) $travelDateRaw)) : '';
961
962 $returnDate = '';
963 if (!empty($travelDateRaw) && $trip && !empty($trip->duration)) {
964 $returnTimestamp = strtotime((string) $travelDateRaw . ' +' . (int) $trip->duration . ' days');
965 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
966 }
967
968 $statusRaw = (string) ($booking['booking_status'] ?? $booking['status'] ?? '');
969 $bookingId = (int) ($booking['id'] ?? 0);
970 $bookingRef = 'YTR-' . strtoupper(str_pad((string) $bookingId, 8, '0', STR_PAD_LEFT));
971
972 $customerName = trim(
973 (string) ($booking['contact_first_name'] ?? '') . ' ' . (string) ($booking['contact_last_name'] ?? '')
974 ) ?: (string) ($booking['customer_name'] ?? __('Customer', 'yatra'));
975
976 $pdfService = new PdfService();
977 if (!$pdfService->isAvailable()) {
978 return new WP_Error(
979 'pdf_engine_missing',
980 __('Itinerary PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
981 ['status' => 500]
982 );
983 }
984
985 $filename = 'Travel-Itinerary-' . $bookingRef . '.pdf';
986
987 $templateData = [
988 'company_name' => $companyName,
989 'company_address' => $companyAddress,
990 'company_email' => $companyEmail,
991 'company_phone' => $companyPhone,
992 'customer_name' => $customerName,
993 'customer_email' => (string) ($booking['contact_email'] ?? $booking['customer_email'] ?? ''),
994 'booking_ref' => $bookingRef,
995 'booking_date' => $bookingDate,
996 'booking_status' => ucfirst($statusRaw ?: 'pending'),
997 'status_class' => in_array(strtolower($statusRaw), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
998 (in_array(strtolower($statusRaw), ['cancelled'], true) ? 'cancelled' : 'pending'),
999 'trip_title' => $trip ? ($trip->title ?? $booking['trip_title'] ?? __('Trip Booking', 'yatra')) : ($booking['trip_title'] ?? __('Trip Booking', 'yatra')),
1000 'trip_description' => $trip ? ($trip->description ?? $trip->content ?? '') : '',
1001 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
1002 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
1003 'trip_highlights' => $trip ? ($trip->highlights ?? $trip->trip_highlights ?? '') : '',
1004 'trip_includes' => $trip ? ($trip->includes ?? $trip->trip_includes ?? '') : '',
1005 'trip_excludes' => $trip ? ($trip->excludes ?? $trip->trip_excludes ?? '') : '',
1006 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
1007 'destination' => $trip ? ($trip->destination ?? '') : '',
1008 'travel_date' => $travelDate,
1009 'return_date' => $returnDate,
1010 'currency_symbol' => $currencySymbol,
1011 'total_amount' => number_format((float) ($booking['total_amount'] ?? 0), 2),
1012 'amount_paid' => number_format((float) ($booking['amount_paid'] ?? 0), 2),
1013 'amount_due' => number_format((float) ($booking['amount_due'] ?? 0), 2),
1014 'traveler_count' => (int) ($booking['travelers_count'] ?? $booking['travelers'] ?? 1),
1015 ];
1016
1017 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/itinerary.php', $templateData, [
1018 'paper' => 'A4',
1019 'orientation' => 'portrait',
1020 'default_font' => 'DejaVu Sans',
1021 ]);
1022
1023 if ($isPreview) {
1024 return new WP_REST_Response([
1025 'success' => true,
1026 'pdf_data' => base64_encode($pdfBinary),
1027 'filename' => $filename,
1028 ]);
1029 }
1030
1031 $pdfService->outputPdfDownload($pdfBinary, $filename);
1032 exit;
1033 }
1034 }
1035