PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
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 / PaymentGatewayController.php

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

1,142 lines 47.0 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\PaymentGateways\PaymentGatewayRegistry;
11 use Yatra\Repositories\BookingRepository;
12 use Yatra\Repositories\PaymentRepository;
13 use Yatra\Repositories\ScheduledPaymentRepository;
14 use Yatra\Repositories\TripRepository;
15 use Yatra\Helpers\FormatHelper;
16 use Yatra\Services\PdfService;
17 use Yatra\Services\SettingsService;
18
19 /**
20 * Payment Gateway REST API Controller
21 *
22 * Handles payment gateway operations and payment processing
23 */
24 class PaymentGatewayController extends BaseController
25 {
26 private PaymentGatewayRegistry $registry;
27 private BookingRepository $bookingRepository;
28 private PaymentRepository $paymentRepository;
29 private ScheduledPaymentRepository $scheduledPaymentRepository;
30 private TripRepository $tripRepository;
31
32 public function __construct()
33 {
34 $this->registry = PaymentGatewayRegistry::getInstance();
35 $this->bookingRepository = new BookingRepository();
36 $this->paymentRepository = new PaymentRepository();
37 $this->scheduledPaymentRepository = new ScheduledPaymentRepository();
38 $this->tripRepository = new TripRepository();
39 }
40
41 public function register_routes(): void
42 {
43 $namespace = 'yatra/v1';
44 $base = 'payment';
45
46 // Get gateway definitions for admin settings
47 register_rest_route($namespace, '/' . $base . '/gateways/definitions', [
48 [
49 'methods' => \WP_REST_Server::READABLE,
50 'callback' => [$this, 'get_gateway_definitions'],
51 'permission_callback' => [$this, 'check_admin_permission'],
52 ],
53 ]);
54
55 // Get available gateways for checkout
56 register_rest_route($namespace, '/' . $base . '/gateways', [
57 [
58 'methods' => \WP_REST_Server::READABLE,
59 'callback' => [$this, 'get_available_gateways'],
60 'permission_callback' => '__return_true',
61 ],
62 ]);
63
64 // Save gateway config
65 register_rest_route($namespace, '/' . $base . '/gateways/(?P<gateway_id>[a-z_]+)/config', [
66 [
67 'methods' => \WP_REST_Server::CREATABLE,
68 'callback' => [$this, 'save_gateway_config'],
69 'permission_callback' => [$this, 'check_admin_permission'],
70 ],
71 ]);
72
73 // Create payment intent
74 register_rest_route($namespace, '/' . $base . '/create-intent', [
75 [
76 'methods' => \WP_REST_Server::CREATABLE,
77 'callback' => [$this, 'create_payment_intent'],
78 'permission_callback' => '__return_true',
79 ],
80 ]);
81
82 // Confirm payment
83 register_rest_route($namespace, '/' . $base . '/confirm', [
84 [
85 'methods' => \WP_REST_Server::CREATABLE,
86 'callback' => [$this, 'confirm_payment'],
87 'permission_callback' => '__return_true',
88 ],
89 ]);
90
91 // Webhook handlers
92 register_rest_route($namespace, '/' . $base . '/webhook/(?P<gateway>[a-z_]+)', [
93 [
94 'methods' => \WP_REST_Server::CREATABLE,
95 'callback' => [$this, 'handle_webhook'],
96 'permission_callback' => '__return_true',
97 ],
98 ]);
99
100 // Payment callback (for redirect-based payments)
101 register_rest_route($namespace, '/' . $base . '/callback/(?P<gateway>[a-z_]+)', [
102 [
103 'methods' => \WP_REST_Server::READABLE,
104 'callback' => [$this, 'handle_callback'],
105 'permission_callback' => '__return_true',
106 ],
107 ]);
108
109 // Get payment status
110 register_rest_route($namespace, '/' . $base . '/status/(?P<booking_id>[\d]+)', [
111 [
112 'methods' => \WP_REST_Server::READABLE,
113 'callback' => [$this, 'get_payment_status'],
114 'permission_callback' => '__return_true',
115 ],
116 ]);
117
118 register_rest_route($namespace, '/' . $base . '/remaining', [
119 [
120 'methods' => \WP_REST_Server::CREATABLE,
121 'callback' => [$this, 'create_remaining_balance_intent'],
122 'permission_callback' => [$this, 'check_customer_permission'],
123 ],
124 ]);
125
126 register_rest_route($namespace, '/' . $base . '/remaining/session', [
127 [
128 'methods' => \WP_REST_Server::CREATABLE,
129 'callback' => [$this, 'start_remaining_payment_session'],
130 'permission_callback' => [$this, 'check_customer_permission'],
131 ],
132 ]);
133
134 // Download invoice for a payment
135 register_rest_route($namespace, '/' . $base . '/(?P<payment_id>[\d]+)/invoice', [
136 [
137 'methods' => \WP_REST_Server::READABLE,
138 'callback' => [$this, 'download_invoice'],
139 'permission_callback' => '__return_true', // Auth checked inside callback
140 ],
141 ]);
142
143 // Download travel voucher for a payment
144 register_rest_route($namespace, '/' . $base . '/(?P<payment_id>[\d]+)/voucher', [
145 [
146 'methods' => \WP_REST_Server::READABLE,
147 'callback' => [$this, 'download_voucher'],
148 'permission_callback' => '__return_true', // Auth checked inside callback
149 ],
150 ]);
151 }
152
153 /**
154 * Check admin permission
155 */
156 public function check_admin_permission(): bool
157 {
158 return current_user_can('manage_options');
159 }
160
161 public function check_customer_permission(): bool
162 {
163 return is_user_logged_in();
164 }
165
166 /**
167 * Get gateway definitions for admin settings
168 */
169 public function get_gateway_definitions(WP_REST_Request $request): WP_REST_Response
170 {
171 return new WP_REST_Response([
172 'gateways' => $this->registry->getDefinitions(),
173 'currency' => get_option('yatra_currency', 'USD'),
174 ], 200);
175 }
176
177 public function create_remaining_balance_intent(WP_REST_Request $request)
178 {
179 $bookingId = (int) $request->get_param('booking_id');
180 $method = sanitize_text_field($request->get_param('method') ?: 'stripe');
181
182 if ($bookingId <= 0) {
183 return new WP_Error('invalid_booking', __('Invalid booking ID provided.', 'yatra'), ['status' => 400]);
184 }
185
186 $booking = $this->bookingRepository->find($bookingId);
187
188 if (!$booking) {
189 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
190 }
191
192 $currentUser = get_current_user_id();
193 if (!$currentUser || (int) $booking->user_id !== $currentUser) {
194 return new WP_Error('forbidden', __('You do not have permission to pay for this booking.', 'yatra'), ['status' => 403]);
195 }
196
197 $remainingAmount = (float) ($booking->amount_due ?? ($booking->total_amount - $booking->amount_paid));
198
199 if ($remainingAmount <= 0) {
200 return new WP_Error('no_balance_due', __('This booking is already fully paid.', 'yatra'), ['status' => 400]);
201 }
202
203 $customerEmail = $booking->contact_email ?? ($booking->customer_email ?? '');
204 $customerName = trim(($booking->contact_first_name ?? '') . ' ' . ($booking->contact_last_name ?? ''));
205
206 $paymentData = [
207 'amount' => $remainingAmount,
208 'currency' => $booking->currency ?? get_option('yatra_currency', 'USD'),
209 'booking_id' => $bookingId,
210 'customer_email' => $customerEmail,
211 'customer_name' => $customerName ?: $customerEmail,
212 'return_url' => $this->getConfirmationUrl($booking->reference ?? (string) $bookingId),
213 'description' => sprintf(__('Remaining balance for Booking #%s', 'yatra'), $booking->reference ?? $bookingId),
214 'cancel_url' => home_url('/my-account?tab=payments&payment=cancelled'),
215 ];
216
217 $result = $this->registry->processPayment($method, $paymentData);
218
219 if (!$result['success']) {
220 $message = $result['error'] ?? $result['message'] ?? __('Unable to initiate payment.', 'yatra');
221 return new WP_Error('payment_error', $message, ['status' => 400]);
222 }
223
224 return new WP_REST_Response([ 'success' => true, 'data' => $result ], 200);
225 }
226
227 public function start_remaining_payment_session(WP_REST_Request $request)
228 {
229 if (!function_exists('yatra_start_session')) {
230 return new WP_Error('session_unavailable', __('Booking session helpers not loaded.', 'yatra'), ['status' => 500]);
231 }
232
233 yatra_start_session();
234
235 $bookingId = (int) $request->get_param('booking_id');
236
237 if ($bookingId <= 0) {
238 return new WP_Error('invalid_booking', __('Invalid booking ID provided.', 'yatra'), ['status' => 400]);
239 }
240
241 $booking = $this->bookingRepository->findWithTrip($bookingId);
242
243 if (!$booking) {
244 return new WP_Error('booking_not_found', __('Booking not found.', 'yatra'), ['status' => 404]);
245 }
246
247 $currentUser = get_current_user_id();
248
249 if (!$currentUser || (int) $booking->user_id !== $currentUser) {
250 return new WP_Error('forbidden', __('You do not have permission to pay for this booking.', 'yatra'), ['status' => 403]);
251 }
252
253 $remainingAmount = (float) ($booking->amount_due ?? ($booking->total_amount - $booking->amount_paid));
254
255 if ($remainingAmount <= 0) {
256 return new WP_Error('no_balance_due', __('This booking is already fully paid.', 'yatra'), ['status' => 400]);
257 }
258
259 $trip = $this->tripRepository->findPublished((int) $booking->trip_id);
260
261 if (!$trip) {
262 return new WP_Error('trip_not_found', __('Trip associated with this booking is unavailable.', 'yatra'), ['status' => 400]);
263 }
264
265 $currency = $booking->currency ?? SettingsService::getCurrency();
266 $travelersCount = (int) ($booking->travelers_count ?? $booking->travelers ?? 1);
267 $travelersCount = max(1, $travelersCount);
268 $pricePerPerson = $travelersCount > 0 ? ((float) $booking->total_amount / $travelersCount) : (float) $trip->sale_price;
269
270 // Use dedicated remaining session (separate from booking session)
271 $remainingSessionData = [
272 'booking_id' => (int) $booking->id,
273 'booking_reference' => $booking->reference ?? '',
274 'trip_id' => (int) $trip->id,
275 'trip_title' => $trip->title,
276 'trip_slug' => $trip->slug,
277 'trip_price' => $pricePerPerson,
278 'trip_featured_image' => $trip->featured_image ?? '',
279 'currency' => $currency,
280 'travel_date' => $booking->travel_date,
281 'travelers' => $travelersCount,
282 'remaining_amount' => $remainingAmount,
283 'amount_paid' => (float) ($booking->amount_paid ?? 0),
284 'total_amount' => (float) ($booking->total_amount ?? 0),
285 'contact_first_name' => $booking->contact_first_name ?? '',
286 'contact_last_name' => $booking->contact_last_name ?? '',
287 'contact_email' => $booking->contact_email ?? $booking->customer_email ?? '',
288 'contact_phone' => $booking->contact_phone ?? $booking->customer_phone ?? '',
289 ];
290
291 // Clear any existing booking session to avoid confusion
292 yatra_clear_booking_session();
293 // Set the remaining payment session
294 yatra_set_remaining_session($remainingSessionData);
295
296 $checkoutUrl = yatra_get_checkout_url();
297 // Custom booking page is a normal WP page: pass trip slug so embedded booking UI can resolve the trip.
298 if (!empty($trip->slug) && SettingsService::useCustomBookingPage()) {
299 $checkoutUrl = add_query_arg('trip', rawurlencode((string) $trip->slug), $checkoutUrl);
300 }
301
302 return new WP_REST_Response([
303 'success' => true,
304 'data' => [
305 'checkout_url' => $checkoutUrl,
306 'booking_reference' => $booking->reference ?? '',
307 'return_url' => $this->getConfirmationUrl($booking->reference ?? ''),
308 ],
309 ]);
310 }
311
312 /**
313 * Get available gateways for checkout
314 */
315 public function get_available_gateways(WP_REST_Request $request): WP_REST_Response
316 {
317 return new WP_REST_Response([
318 'gateways' => $this->registry->getForCheckout(),
319 'currency' => get_option('yatra_currency', 'USD'),
320 ], 200);
321 }
322
323 /**
324 * Save gateway configuration
325 */
326 public function save_gateway_config(WP_REST_Request $request)
327 {
328 $gatewayId = $request->get_param('gateway_id');
329 $config = $request->get_json_params();
330
331 $gateway = $this->registry->get($gatewayId);
332 if (!$gateway) {
333 return new WP_Error('invalid_gateway', __('Gateway not found', 'yatra'), ['status' => 404]);
334 }
335
336 $saved = $gateway->saveConfig($config);
337
338 if ($saved) {
339 /**
340 * Fires after a payment gateway configuration is saved (telemetry / integrations).
341 *
342 * @param string $gatewayId Gateway id.
343 * @param array<string, mixed> $config Sanitized-bound request body.
344 */
345 do_action('yatra_payment_gateway_config_saved', (string) $gatewayId, is_array($config) ? $config : []);
346 }
347
348 return new WP_REST_Response([
349 'success' => $saved,
350 'message' => $saved ? __('Gateway configuration saved', 'yatra') : __('Failed to save configuration', 'yatra'),
351 ], $saved ? 200 : 500);
352 }
353
354 /**
355 * Create payment intent
356 */
357 public function create_payment_intent(WP_REST_Request $request)
358 {
359 $gatewayId = sanitize_text_field($request->get_param('gateway'));
360 $paymentData = [
361 'amount' => (float) $request->get_param('amount'),
362 'currency' => sanitize_text_field($request->get_param('currency') ?: get_option('yatra_currency', 'USD')),
363 'booking_id' => (int) $request->get_param('booking_id'),
364 'trip_id' => (int) $request->get_param('trip_id'),
365 'trip_date' => sanitize_text_field($request->get_param('trip_date') ?? ''),
366 'customer_email' => sanitize_email($request->get_param('customer_email')),
367 'customer_name' => sanitize_text_field($request->get_param('customer_name')),
368 'return_url' => esc_url_raw($request->get_param('return_url')),
369 ];
370
371 // Enrich payment data with booking context (reference, trip title, cancel URL)
372 if ($paymentData['booking_id'] > 0) {
373 $booking = $this->bookingRepository->find($paymentData['booking_id']);
374 if ($booking) {
375 $paymentData['reference'] = $booking->reference ?? '';
376 $paymentData['trip_title'] = $booking->trip_title ?? '';
377 if (empty($paymentData['trip_id'])) {
378 $paymentData['trip_id'] = (int) ($booking->trip_id ?? 0);
379 }
380 }
381 }
382
383 if (empty($paymentData['return_url'])) {
384 $reference = $paymentData['reference'] ?? (string) $paymentData['booking_id'];
385 $paymentData['return_url'] = add_query_arg('payment', 'success', $this->getConfirmationUrl($reference));
386 }
387
388 $cancelParam = esc_url_raw($request->get_param('cancel_url'));
389 $paymentData['cancel_url'] = $cancelParam ?: home_url('/book/?payment=cancelled&ref=' . ($paymentData['reference'] ?? $paymentData['booking_id']));
390
391 if ($paymentData['amount'] <= 0) {
392 return new WP_Error('invalid_amount', __('Invalid payment amount', 'yatra'), ['status' => 400]);
393 }
394
395 $result = $this->registry->processPayment($gatewayId, $paymentData);
396
397 if (!$result['success']) {
398 $errorMessage = $result['error'] ?? $result['message'] ?? __('Payment failed', 'yatra');
399 return new WP_Error('payment_error', $errorMessage, ['status' => 400]);
400 }
401
402 return new WP_REST_Response($result, 200);
403 }
404
405 private function getConfirmationUrl(string $reference): string
406 {
407 return yatra_get_booking_confirmation_url($reference);
408 }
409
410 /**
411 * Confirm payment
412 */
413 public function confirm_payment(WP_REST_Request $request)
414 {
415 $gatewayId = sanitize_text_field($request->get_param('gateway'));
416 $transactionId = sanitize_text_field($request->get_param('transaction_id'));
417 $bookingId = (int) $request->get_param('booking_id');
418 $saveCard = !empty($request->get_param('save_card'));
419
420 $gateway = $this->registry->get($gatewayId);
421 if (!$gateway) {
422 return new WP_Error('invalid_gateway', __('Gateway not found', 'yatra'), ['status' => 404]);
423 }
424
425 $result = $gateway->verifyPayment($transactionId);
426
427 if ($result['success']) {
428 // Get customer and payment method from result
429 $customerId = $result['customer_id'] ?? null;
430 $paymentMethodId = $result['payment_method_id'] ?? $result['token_id'] ?? $result['vault_id'] ?? null;
431
432 $this->handle_successful_payment(
433 $bookingId,
434 $gatewayId,
435 $transactionId,
436 $result['amount'] ?? null,
437 $result['currency'] ?? null,
438 $saveCard ? $customerId : null,
439 $saveCard ? $paymentMethodId : null
440 );
441 }
442
443 return new WP_REST_Response($result, 200);
444 }
445
446 /**
447 * Handle webhook
448 */
449 public function handle_webhook(WP_REST_Request $request)
450 {
451 $gatewayId = $request->get_param('gateway');
452 $gateway = $this->registry->get($gatewayId);
453
454 if (!$gateway) {
455 return new WP_Error('invalid_gateway', __('Gateway not found', 'yatra'), ['status' => 404]);
456 }
457
458 $data = $request->get_json_params() ?: [];
459 $data['raw_body'] = $request->get_body();
460 $data['headers'] = $request->get_headers();
461 $data['post_data'] = $request->get_body_params(); // For form-encoded data (like PayPal IPN)
462
463 $result = $gateway->handleWebhook($data);
464
465 return new WP_REST_Response($result, 200);
466 }
467
468 /**
469 * Handle callback (for redirect-based payments)
470 */
471 public function handle_callback(WP_REST_Request $request): void
472 {
473 $gatewayId = $request->get_param('gateway');
474 $bookingId = (int) $request->get_param('booking_id');
475 $status = $request->get_param('status');
476
477 $gateway = $this->registry->get($gatewayId);
478
479 if (!$gateway) {
480 wp_redirect(home_url('/booking-failed/'));
481 exit;
482 }
483
484 // Get transaction ID from request (varies by gateway)
485 $transactionId = $request->get_param('refId')
486 ?? $request->get_param('pidx')
487 ?? $request->get_param('transaction_id')
488 ?? '';
489
490 if ($status === 'success' && !empty($transactionId)) {
491 $result = $gateway->verifyPayment($transactionId);
492
493 if ($result['success']) {
494 $this->handle_successful_payment($bookingId, $gatewayId, $transactionId);
495 wp_redirect(home_url('/booking-success/?booking_id=' . $bookingId));
496 exit;
497 }
498 }
499
500 wp_redirect(home_url('/booking-failed/'));
501 exit;
502 }
503
504 /**
505 * Get payment status
506 */
507 public function get_payment_status(WP_REST_Request $request)
508 {
509 $bookingId = (int) $request->get_param('booking_id');
510
511 $payment = $this->paymentRepository->findLatestByBookingId($bookingId);
512
513 if (!$payment) {
514 return new WP_Error('payment_not_found', __('Payment not found', 'yatra'), ['status' => 404]);
515 }
516
517 return new WP_REST_Response([
518 'status' => $payment->status,
519 'amount' => (float) $payment->amount,
520 'currency' => $payment->currency,
521 'gateway' => $payment->payment_gateway ?? $payment->gateway ?? '',
522 'transaction_id' => $payment->transaction_id,
523 'created_at' => $payment->created_at,
524 ], 200);
525 }
526
527 /**
528 * Record a completed charge against an existing booking (initial or remaining balance).
529 * Does not create bookings — only PaymentRepository::create + booking amount/status updates.
530 */
531 private function handle_successful_payment(
532 int $bookingId,
533 string $gateway,
534 string $transactionId,
535 ?float $amount = null,
536 ?string $currency = null,
537 ?string $customerId = null,
538 ?string $paymentMethodId = null
539 ): void {
540 if ($bookingId <= 0) {
541 return;
542 }
543
544 // Get booking details
545 $booking = $this->bookingRepository->find($bookingId);
546
547 if (!$booking) {
548 return;
549 }
550
551 $paid_amount = $amount ?? (float) $booking->amount_due;
552 $payment_currency = $currency ?? $booking->currency;
553
554 $payment_data = [
555 'booking_id' => $bookingId,
556 'gateway' => $gateway,
557 'transaction_id' => $transactionId,
558 'amount' => $paid_amount,
559 'currency' => $payment_currency,
560 'status' => 'completed',
561 'customer_id' => $booking->customer_id ? (int) $booking->customer_id : null,
562 ];
563
564 // Create or update payment record
565 $this->paymentRepository->create($payment_data);
566
567 // Calculate new amounts
568 $new_amount_paid = (float) $booking->amount_paid + $paid_amount;
569 $new_amount_due = max(0, (float) $booking->total_amount - $new_amount_paid);
570
571 // Determine payment status
572 $payment_status = 'paid';
573 if ($new_amount_due > 0) {
574 $payment_status = 'partial';
575 }
576
577 // Update booking
578 $this->bookingRepository->update($bookingId, [
579 'amount_paid' => $new_amount_paid,
580 'amount_due' => $new_amount_due,
581 'payment_status' => $payment_status,
582 'status' => 'confirmed',
583 ]);
584
585 // Handle scheduled payments for remaining balance
586 if ($new_amount_due > 0 && $customerId && $paymentMethodId) {
587 $this->createScheduledPaymentsForBooking(
588 $bookingId,
589 $gateway,
590 $customerId,
591 $paymentMethodId,
592 $new_amount_due,
593 $payment_currency
594 );
595 }
596
597 // Clear remaining payment session if this was a remaining payment
598 if (function_exists('yatra_has_remaining_session') && yatra_has_remaining_session()) {
599 yatra_clear_remaining_session();
600 }
601
602 do_action('yatra_payment_completed', $bookingId, $gateway, $transactionId, [
603 'amount' => $paid_amount,
604 'remaining' => $new_amount_due,
605 'customer_id' => $customerId,
606 'payment_method_id' => $paymentMethodId,
607 ]);
608 }
609
610 /**
611 * Create scheduled payments for remaining balance
612 */
613 private function createScheduledPaymentsForBooking(
614 int $bookingId,
615 string $gateway,
616 string $customerId,
617 string $paymentMethodId,
618 float $remainingAmount,
619 string $currency
620 ): void {
621 // Get scheduled payment settings
622 $settings = \Yatra\Services\SettingsService::getAll();
623
624 // Check if auto-scheduled payments is enabled
625 if (empty($settings['enable_scheduled_payments'])) {
626 return;
627 }
628
629 // Save the payment token first
630 $tokenId = $this->savePaymentToken($bookingId, $gateway, $customerId, $paymentMethodId);
631
632 if (!$tokenId) {
633 return;
634 }
635
636 // Get schedule configuration from settings
637 $schedule = [
638 'type' => $settings['scheduled_payment_type'] ?? 'single', // single, installments
639 'days_until' => (int) ($settings['scheduled_payment_days'] ?? 15),
640 'installments' => (int) ($settings['scheduled_payment_installments'] ?? 1),
641 'interval_days' => (int) ($settings['scheduled_payment_interval'] ?? 30),
642 ];
643
644 // Create scheduled payments
645 \Yatra\Services\ScheduledPaymentService::createScheduledPayments(
646 $bookingId,
647 $gateway,
648 $customerId,
649 $tokenId,
650 $remainingAmount,
651 $currency,
652 $schedule
653 );
654 }
655
656 /**
657 * Save payment token for future charges
658 */
659 private function savePaymentToken(
660 int $bookingId,
661 string $gateway,
662 string $customerId,
663 string $paymentMethodId
664 ): ?int {
665 // Get booking for customer info
666 $booking = $this->bookingRepository->find($bookingId);
667
668 if (!$booking) {
669 return null;
670 }
671
672 $userId = get_current_user_id() ?: 0;
673
674 // Get payment method details from gateway
675 $gatewayInstance = $this->registry->get($gateway);
676 $cardInfo = [];
677
678 if ($gatewayInstance) {
679 $methods = $gatewayInstance->getPaymentMethods($customerId);
680 foreach ($methods as $method) {
681 if ($method['id'] === $paymentMethodId) {
682 $cardInfo = $method;
683 break;
684 }
685 }
686 }
687
688 // Create token via repository
689 $tokenId = $this->scheduledPaymentRepository->createPaymentToken([
690 'customer_id' => $userId,
691 'user_id' => $userId,
692 'gateway' => $gateway,
693 'token' => $paymentMethodId,
694 'payment_method_id' => $paymentMethodId,
695 'card_brand' => $cardInfo['brand'] ?? null,
696 'card_last4' => $cardInfo['last4'] ?? null,
697 'card_exp_month' => $cardInfo['exp_month'] ?? null,
698 'card_exp_year' => $cardInfo['exp_year'] ?? null,
699 'is_default' => 1,
700 ]);
701
702 return $tokenId ?: null;
703 }
704
705 /**
706 * Download invoice PDF for a payment
707 */
708 public function download_invoice(WP_REST_Request $request)
709 {
710 $paymentId = (int) $request->get_param('payment_id');
711 $isPreview = $request->get_param('preview') === '1';
712 $isDownload = $request->get_param('download') === '1';
713 $bookingToken = sanitize_text_field((string) ($request->get_param('booking_token') ?? ''));
714 $invoiceToken = sanitize_text_field((string) ($request->get_param('invoice_token') ?? ''));
715
716 if ($paymentId <= 0) {
717 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
718 }
719
720 // Get payment with booking details
721 $payment = $this->paymentRepository->findWithBooking($paymentId);
722
723 if (!$payment) {
724 return new WP_Error('payment_not_found', __('Payment not found.', 'yatra'), ['status' => 404]);
725 }
726
727 // Authorisation:
728 // 1. Administrators can always access (no further checks).
729 // 2. Logged-in owner of the booking can access.
730 // 3. Anyone with a valid signed `invoice_token` (HMAC) can access — used on the
731 // booking-confirmation page so guest checkouts and post-session views work.
732 // 4. Legacy guest path: `booking_token` (active checkout transient) — kept for BC.
733 $currentUserId = (int) get_current_user_id();
734 $bookingUserId = (int) ($payment->booking_user_id ?? $payment->user_id ?? 0);
735 $paymentBookingId = (int) ($payment->booking_id ?? 0);
736 $isAdmin = current_user_can('manage_options');
737 $authorised = false;
738
739 if ($isAdmin) {
740 $authorised = true;
741 } elseif ($currentUserId && $bookingUserId && $currentUserId === $bookingUserId) {
742 $authorised = true;
743 } elseif ($invoiceToken !== '' && self::verifyInvoiceToken($invoiceToken, (int) $payment->id, $paymentBookingId)) {
744 $authorised = true;
745 } elseif ($bookingToken !== '') {
746 $guestEnabled = (bool) SettingsService::get('allow_guest_checkout', true);
747 if ($guestEnabled) {
748 $session = get_transient($bookingToken);
749 if (is_array($session)) {
750 $sessionBookingId = (int) ($session['booking_id'] ?? 0);
751 if ($sessionBookingId > 0 && $paymentBookingId > 0 && $sessionBookingId === $paymentBookingId) {
752 $authorised = true;
753 }
754 }
755 }
756 }
757
758 if (!$authorised) {
759 if ($currentUserId) {
760 return new WP_Error('forbidden', __('You do not have permission to access this invoice.', 'yatra'), ['status' => 403]);
761 }
762 return new WP_Error('unauthorized', __('You must be logged in to download invoices.', 'yatra'), ['status' => 401]);
763 }
764
765 // Get trip details if available
766 $trip = null;
767 if (!empty($payment->trip_id)) {
768 $trip = $this->tripRepository->find((int) $payment->trip_id);
769 }
770
771 // Get company settings
772 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
773 $companyAddress = SettingsService::get('company_address', '');
774 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
775 $companyPhone = SettingsService::get('company_phone', '');
776 $currency = SettingsService::getCurrency();
777 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
778
779 // Format dates
780 $paymentDate = !empty($payment->created_at) ? date_i18n(get_option('date_format'), strtotime($payment->created_at)) : '';
781 $travelDate = !empty($payment->travel_date) ? date_i18n(get_option('date_format'), strtotime($payment->travel_date)) : '';
782
783
784 $bookingRef = (string) ($payment->booking_reference ?? $payment->booking_number ?? $payment->reference ?? (string) $paymentId);
785 $filename = 'Invoice #' . $bookingRef . '.pdf';
786
787
788 $pdfService = new PdfService();
789 if (!$pdfService->isAvailable()) {
790 return new WP_Error(
791 'pdf_engine_missing',
792 __('Invoice PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
793 ['status' => 500]
794 );
795 }
796
797 // Get tax breakdown for invoice
798 $tax_breakdown = [];
799 $tax_amount = 0;
800 $subtotal = (float) ($payment->booking_total_amount ?? $payment->amount ?? 0);
801
802 if (!empty($payment->tax_details)) {
803 $taxes = json_decode($payment->tax_details, true) ?: [];
804 foreach ($taxes as $tax) {
805 $tax_amount += (float) ($tax['amount'] ?? 0);
806 $tax_breakdown[] = [
807 'name' => $tax['name'] ?? 'Tax',
808 'rate' => $tax['rate'] ?? 0,
809 'amount' => $tax['amount'] ?? 0
810 ];
811 }
812 // Adjust subtotal for tax-exclusive pricing
813 if (!empty($payment->tax_inclusive) && $payment->tax_inclusive) {
814 $subtotal = (float) ($payment->subtotal ?? $subtotal);
815 }
816 } elseif (!empty($payment->tax_amount) && $payment->tax_amount > 0) {
817 // Single tax fallback
818 $tax_amount = (float) $payment->tax_amount;
819 $tax_breakdown[] = [
820 'name' => __('Tax', 'yatra'),
821 'rate' => (float) ($payment->tax_rate ?? 0),
822 'amount' => $tax_amount
823 ];
824 // Adjust subtotal for tax-exclusive pricing
825 if (!empty($payment->tax_inclusive) && $payment->tax_inclusive) {
826 $subtotal = (float) ($payment->subtotal ?? $subtotal);
827 } else {
828 $subtotal = (float) ($payment->subtotal ?? ($subtotal - $tax_amount));
829 }
830 }
831
832 $templateData = [
833 'company_name' => $companyName,
834 'company_address' => $companyAddress,
835 'company_email' => $companyEmail,
836 'company_phone' => $companyPhone,
837 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
838 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
839 'payment_ref' => $payment->reference ?? '',
840 'payment_date' => $paymentDate,
841 'payment_status' => ucfirst($payment->status ?? 'paid'),
842 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['paid', 'completed', 'success'], true) ? 'paid' : 'pending',
843 'trip_title' => $trip->title ?? $payment->trip_title ?? __('Trip Booking', 'yatra'),
844 'payment_method' => ucfirst($payment->gateway ?? $payment->payment_method ?? 'Online'),
845 'booking_ref' => $payment->booking_reference ?? $payment->booking_number ?? '',
846 'travel_date' => $travelDate,
847 'currency_symbol' => $currencySymbol,
848 'amount' => number_format((float) ($payment->amount ?? 0), 2),
849 'booking_total' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
850 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
851 'amount_due' => number_format((float) ($payment->booking_amount_due ?? 0), 2),
852 'tax_breakdown' => $tax_breakdown,
853 'tax_amount' => number_format($tax_amount, 2),
854 'subtotal' => number_format($subtotal, 2),
855 ];
856
857 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/invoice.php', $templateData, [
858 'paper' => 'A4',
859 'orientation' => 'portrait',
860 'default_font' => 'DejaVu Sans',
861 ]);
862
863 if ($isPreview) {
864 // For preview, return PDF as inline display
865 return new WP_REST_Response([
866 'success' => true,
867 'pdf_data' => base64_encode($pdfBinary),
868 'filename' => $filename,
869 ]);
870 } else {
871 // For download, output PDF as download
872 $pdfService->outputPdfDownload($pdfBinary, $filename);
873 exit;
874 }
875 }
876
877 /**
878 * Download travel voucher PDF for a booking
879 */
880 public function download_voucher(WP_REST_Request $request)
881 {
882 $paymentId = (int) $request->get_param('payment_id');
883 $isPreview = $request->get_param('preview') === '1';
884 $isDownload = $request->get_param('download') === '1';
885
886 if ($paymentId <= 0) {
887 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
888 }
889
890 // Get payment with booking details
891 $payment = $this->paymentRepository->findWithBooking($paymentId);
892
893 if (!$payment) {
894 return new WP_Error('payment_not_found', __('Payment not found.', 'yatra'), ['status' => 404]);
895 }
896
897 // Verify user is logged in and owns this payment (or is admin)
898 $currentUserId = get_current_user_id();
899 $bookingUserId = (int) ($payment->booking_user_id ?? $payment->user_id ?? 0);
900
901 // Must be logged in
902 if (!$currentUserId) {
903 return new WP_Error('unauthorized', __('You must be logged in to download vouchers.', 'yatra'), ['status' => 401]);
904 }
905
906 // Must own the booking or be admin
907 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
908 return new WP_Error('forbidden', __('You do not have permission to access this voucher.', 'yatra'), ['status' => 403]);
909 }
910
911 // Get trip details if available
912 $trip = null;
913 if (!empty($payment->trip_id)) {
914 $trip = $this->tripRepository->find((int) $payment->trip_id);
915 }
916
917 // Get company settings
918 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
919 $companyAddress = SettingsService::get('company_address', '');
920 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
921 $companyPhone = SettingsService::get('company_phone', '');
922 $currency = SettingsService::getCurrency();
923 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
924
925 // Format dates
926 $bookingDate = !empty($payment->created_at) ? date_i18n(get_option('date_format'), strtotime($payment->created_at)) : '';
927 $travelDate = !empty($payment->travel_date) ? date_i18n(get_option('date_format'), strtotime($payment->travel_date)) : '';
928
929 // Calculate return date if duration is available
930 $returnDate = '';
931 if (!empty($payment->travel_date) && !empty($trip->duration ?? 0)) {
932 $returnTimestamp = strtotime($payment->travel_date . ' +' . (int) ($trip->duration ?? 0) . ' days');
933 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
934 }
935
936 $bookingRef = (string) ($payment->booking_reference ?? $payment->booking_number ?? $payment->reference ?? (string) $paymentId);
937 $filename = 'Travel Voucher #' . $bookingRef . '.pdf';
938
939 $pdfService = new PdfService();
940 if (!$pdfService->isAvailable()) {
941 return new WP_Error(
942 'pdf_engine_missing',
943 __('Voucher PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
944 ['status' => 500]
945 );
946 }
947
948 $templateData = [
949 'company_name' => $companyName,
950 'company_address' => $companyAddress,
951 'company_email' => $companyEmail,
952 'company_phone' => $companyPhone,
953 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
954 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
955 'booking_ref' => $bookingRef,
956 'booking_date' => $bookingDate,
957 'booking_status' => ucfirst($payment->status ?? 'confirmed'),
958 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
959 (in_array(strtolower((string) ($payment->status ?? '')), ['cancelled'], true) ? 'cancelled' : 'pending'),
960 'trip_title' => $trip ? ($trip->title ?? $payment->trip_title ?? __('Trip Booking', 'yatra')) : ($payment->trip_title ?? __('Trip Booking', 'yatra')),
961 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
962 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
963 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
964 'destination' => $trip ? ($trip->destination ?? $payment->destination ?? '') : ($payment->destination ?? ''),
965 'travel_date' => $travelDate,
966 'return_date' => $returnDate,
967 'currency_symbol' => $currencySymbol,
968 'total_amount' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
969 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
970 'amount_due' => number_format((float) ($payment->booking_amount_due ?? 0), 2),
971 'traveler_count' => (int) ($payment->traveler_count ?? 1),
972 ];
973
974 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/voucher.php', $templateData, [
975 'paper' => 'A4',
976 'orientation' => 'portrait',
977 'default_font' => 'DejaVu Sans',
978 ]);
979
980 if ($isPreview) {
981 // For preview, return PDF as inline display
982 return new WP_REST_Response([
983 'success' => true,
984 'pdf_data' => base64_encode($pdfBinary),
985 'filename' => $filename,
986 ]);
987 } else {
988 // For download, output PDF as download
989 $pdfService->outputPdfDownload($pdfBinary, $filename);
990 exit;
991 }
992 }
993
994 /**
995 * GET /payments/{payment_id}/itinerary - Download travel itinerary for a payment
996 */
997 public function download_itinerary(WP_REST_Request $request)
998 {
999 $paymentId = (int) $request->get_param('payment_id');
1000 $isPreview = $request->get_param('preview') === '1';
1001 $isDownload = $request->get_param('download') === '1';
1002
1003 if ($paymentId <= 0) {
1004 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
1005 }
1006
1007 // Get payment with booking details
1008 $payment = $this->paymentRepository->findWithBooking($paymentId);
1009
1010 if (!$payment) {
1011 return new WP_Error('payment_not_found', __('Payment not found.', 'yatra'), ['status' => 404]);
1012 }
1013
1014 // Verify user is logged in and owns this payment (or is admin)
1015 $currentUserId = get_current_user_id();
1016 $bookingUserId = (int) ($payment->booking_user_id ?? $payment->user_id ?? 0);
1017
1018 // Must be logged in
1019 if (!$currentUserId) {
1020 return new WP_Error('unauthorized', __('You must be logged in to download itineraries.', 'yatra'), ['status' => 401]);
1021 }
1022
1023 // Must own the booking or be admin
1024 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
1025 return new WP_Error('forbidden', __('You do not have permission to access this itinerary.', 'yatra'), ['status' => 403]);
1026 }
1027
1028 // Get trip details if available
1029 $trip = null;
1030 if (!empty($payment->trip_id)) {
1031 $trip = $this->tripRepository->find((int) $payment->trip_id);
1032 }
1033
1034 // Get company settings
1035 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
1036 $companyAddress = SettingsService::get('company_address', '');
1037 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
1038 $companyPhone = SettingsService::get('company_phone', '');
1039 $currency = SettingsService::getCurrency();
1040 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
1041
1042 // Format dates
1043 $bookingDate = !empty($payment->created_at) ? date_i18n(get_option('date_format'), strtotime($payment->created_at)) : '';
1044 $travelDate = !empty($payment->travel_date) ? date_i18n(get_option('date_format'), strtotime($payment->travel_date)) : '';
1045
1046 // Calculate return date if duration is available
1047 $returnDate = '';
1048 if (!empty($payment->travel_date) && !empty($trip->duration ?? 0)) {
1049 $returnTimestamp = strtotime($payment->travel_date . ' +' . (int) ($trip->duration ?? 0) . ' days');
1050 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
1051 }
1052
1053 // Generate booking reference
1054 $bookingRef = '';
1055 if (!empty($payment->booking_id)) {
1056 $bookingRef = 'YTR-' . strtoupper(str_pad((string) $payment->booking_id, 8, '0', STR_PAD_LEFT));
1057 }
1058
1059 // Generate PDF using PDF service
1060 $pdfService = new PdfService();
1061 $filename = 'Travel-Itinerary-' . $bookingRef . '.pdf';
1062
1063 // Prepare template data with null-safe access
1064 $templateData = [
1065 'company_name' => $companyName,
1066 'company_address' => $companyAddress,
1067 'company_email' => $companyEmail,
1068 'company_phone' => $companyPhone,
1069 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
1070 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
1071 'booking_ref' => $bookingRef,
1072 'booking_date' => $bookingDate,
1073 'booking_status' => ucfirst($payment->status ?? 'confirmed'),
1074 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
1075 (in_array(strtolower((string) ($payment->status ?? '')), ['cancelled'], true) ? 'cancelled' : 'pending'),
1076 'trip_title' => $trip ? ($trip->title ?? $payment->trip_title ?? __('Trip Booking', 'yatra')) : ($payment->trip_title ?? __('Trip Booking', 'yatra')),
1077 'trip_description' => $trip ? ($trip->description ?? $trip->content ?? '') : '',
1078 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
1079 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
1080 'trip_highlights' => $trip ? ($trip->highlights ?? $trip->trip_highlights ?? '') : '',
1081 'trip_includes' => $trip ? ($trip->includes ?? $trip->trip_includes ?? '') : '',
1082 'trip_excludes' => $trip ? ($trip->excludes ?? $trip->trip_excludes ?? '') : '',
1083 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
1084 'destination' => $trip ? ($trip->destination ?? $payment->destination ?? '') : ($payment->destination ?? ''),
1085 'travel_date' => $travelDate,
1086 'return_date' => $returnDate,
1087 'currency_symbol' => $currencySymbol,
1088 'total_amount' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
1089 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
1090 'amount_due' => number_format((float) ($payment->booking_amount_due ?? 0), 2),
1091 'traveler_count' => (int) ($payment->traveler_count ?? 1),
1092 ];
1093
1094 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/itinerary.php', $templateData, [
1095 'paper' => 'A4',
1096 'orientation' => 'portrait',
1097 'default_font' => 'DejaVu Sans',
1098 ]);
1099
1100 if ($isPreview) {
1101 // For preview, return PDF as inline display
1102 return new WP_REST_Response([
1103 'success' => true,
1104 'pdf_data' => base64_encode($pdfBinary),
1105 'filename' => $filename,
1106 ]);
1107 } else {
1108 // For download, output PDF as download
1109 $pdfService->outputPdfDownload($pdfBinary, $filename);
1110 exit;
1111 }
1112 }
1113
1114 /**
1115 * Issue a stateless, signed token that grants access to a single payment's invoice.
1116 *
1117 * The token is bound to the payment id + booking id and signed with the WP auth salt,
1118 * so it cannot be forged without the site secret. It is safe to embed in the
1119 * confirmation page link so guests (or users who logged out after checkout) can still
1120 * download their invoice without a session.
1121 */
1122 public static function issueInvoiceToken(int $paymentId, int $bookingId): string
1123 {
1124 if ($paymentId <= 0 || $bookingId <= 0) {
1125 return '';
1126 }
1127 return hash_hmac('sha256', $paymentId . '|' . $bookingId, wp_salt('auth') . '|yatra_invoice');
1128 }
1129
1130 /**
1131 * Verify a token previously issued by self::issueInvoiceToken().
1132 */
1133 public static function verifyInvoiceToken(string $token, int $paymentId, int $bookingId): bool
1134 {
1135 if ($token === '' || $paymentId <= 0 || $bookingId <= 0) {
1136 return false;
1137 }
1138 $expected = self::issueInvoiceToken($paymentId, $bookingId);
1139 return $expected !== '' && hash_equals($expected, $token);
1140 }
1141 }
1142