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

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

1,088 lines 44.4 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
714 if ($paymentId <= 0) {
715 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
716 }
717
718 // Get payment with booking details
719 $payment = $this->paymentRepository->findWithBooking($paymentId);
720
721 if (!$payment) {
722 return new WP_Error('payment_not_found', __('Payment not found.', 'yatra'), ['status' => 404]);
723 }
724
725 // Verify user is logged in and owns this payment (or is admin)
726 $currentUserId = get_current_user_id();
727 $bookingUserId = (int) ($payment->booking_user_id ?? $payment->user_id ?? 0);
728
729 // Must be logged in
730 if (!$currentUserId) {
731 return new WP_Error('unauthorized', __('You must be logged in to download invoices.', 'yatra'), ['status' => 401]);
732 }
733
734 // Must own the booking or be admin
735 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
736 return new WP_Error('forbidden', __('You do not have permission to access this invoice.', 'yatra'), ['status' => 403]);
737 }
738
739 // Get trip details if available
740 $trip = null;
741 if (!empty($payment->trip_id)) {
742 $trip = $this->tripRepository->find((int) $payment->trip_id);
743 }
744
745 // Get company settings
746 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
747 $companyAddress = SettingsService::get('company_address', '');
748 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
749 $companyPhone = SettingsService::get('company_phone', '');
750 $currency = SettingsService::getCurrency();
751 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
752
753 // Format dates
754 $paymentDate = !empty($payment->created_at) ? date_i18n(get_option('date_format'), strtotime($payment->created_at)) : '';
755 $travelDate = !empty($payment->travel_date) ? date_i18n(get_option('date_format'), strtotime($payment->travel_date)) : '';
756
757
758 $bookingRef = (string) ($payment->booking_reference ?? $payment->booking_number ?? $payment->reference ?? (string) $paymentId);
759 $filename = 'Invoice #' . $bookingRef . '.pdf';
760
761
762 $pdfService = new PdfService();
763 if (!$pdfService->isAvailable()) {
764 return new WP_Error(
765 'pdf_engine_missing',
766 __('Invoice PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
767 ['status' => 500]
768 );
769 }
770
771 // Get tax breakdown for invoice
772 $tax_breakdown = [];
773 $tax_amount = 0;
774 $subtotal = (float) ($payment->booking_total_amount ?? $payment->amount ?? 0);
775
776 if (!empty($payment->tax_details)) {
777 $taxes = json_decode($payment->tax_details, true) ?: [];
778 foreach ($taxes as $tax) {
779 $tax_amount += (float) ($tax['amount'] ?? 0);
780 $tax_breakdown[] = [
781 'name' => $tax['name'] ?? 'Tax',
782 'rate' => $tax['rate'] ?? 0,
783 'amount' => $tax['amount'] ?? 0
784 ];
785 }
786 // Adjust subtotal for tax-exclusive pricing
787 if (!empty($payment->tax_inclusive) && $payment->tax_inclusive) {
788 $subtotal = (float) ($payment->subtotal ?? $subtotal);
789 }
790 } elseif (!empty($payment->tax_amount) && $payment->tax_amount > 0) {
791 // Single tax fallback
792 $tax_amount = (float) $payment->tax_amount;
793 $tax_breakdown[] = [
794 'name' => __('Tax', 'yatra'),
795 'rate' => (float) ($payment->tax_rate ?? 0),
796 'amount' => $tax_amount
797 ];
798 // Adjust subtotal for tax-exclusive pricing
799 if (!empty($payment->tax_inclusive) && $payment->tax_inclusive) {
800 $subtotal = (float) ($payment->subtotal ?? $subtotal);
801 } else {
802 $subtotal = (float) ($payment->subtotal ?? ($subtotal - $tax_amount));
803 }
804 }
805
806 $templateData = [
807 'company_name' => $companyName,
808 'company_address' => $companyAddress,
809 'company_email' => $companyEmail,
810 'company_phone' => $companyPhone,
811 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
812 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
813 'payment_ref' => $payment->reference ?? '',
814 'payment_date' => $paymentDate,
815 'payment_status' => ucfirst($payment->status ?? 'paid'),
816 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['paid', 'completed', 'success'], true) ? 'paid' : 'pending',
817 'trip_title' => $trip->title ?? $payment->trip_title ?? __('Trip Booking', 'yatra'),
818 'payment_method' => ucfirst($payment->gateway ?? $payment->payment_method ?? 'Online'),
819 'booking_ref' => $payment->booking_reference ?? $payment->booking_number ?? '',
820 'travel_date' => $travelDate,
821 'currency_symbol' => $currencySymbol,
822 'amount' => number_format((float) ($payment->amount ?? 0), 2),
823 'booking_total' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
824 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
825 'amount_due' => number_format((float) ($payment->booking_amount_due ?? 0), 2),
826 'tax_breakdown' => $tax_breakdown,
827 'tax_amount' => number_format($tax_amount, 2),
828 'subtotal' => number_format($subtotal, 2),
829 ];
830
831 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/invoice.php', $templateData, [
832 'paper' => 'A4',
833 'orientation' => 'portrait',
834 'default_font' => 'DejaVu Sans',
835 ]);
836
837 if ($isPreview) {
838 // For preview, return PDF as inline display
839 return new WP_REST_Response([
840 'success' => true,
841 'pdf_data' => base64_encode($pdfBinary),
842 'filename' => $filename,
843 ]);
844 } else {
845 // For download, output PDF as download
846 $pdfService->outputPdfDownload($pdfBinary, $filename);
847 exit;
848 }
849 }
850
851 /**
852 * Download travel voucher PDF for a booking
853 */
854 public function download_voucher(WP_REST_Request $request)
855 {
856 $paymentId = (int) $request->get_param('payment_id');
857 $isPreview = $request->get_param('preview') === '1';
858 $isDownload = $request->get_param('download') === '1';
859
860 if ($paymentId <= 0) {
861 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
862 }
863
864 // Get payment with booking details
865 $payment = $this->paymentRepository->findWithBooking($paymentId);
866
867 if (!$payment) {
868 return new WP_Error('payment_not_found', __('Payment not found.', 'yatra'), ['status' => 404]);
869 }
870
871 // Verify user is logged in and owns this payment (or is admin)
872 $currentUserId = get_current_user_id();
873 $bookingUserId = (int) ($payment->booking_user_id ?? $payment->user_id ?? 0);
874
875 // Must be logged in
876 if (!$currentUserId) {
877 return new WP_Error('unauthorized', __('You must be logged in to download vouchers.', 'yatra'), ['status' => 401]);
878 }
879
880 // Must own the booking or be admin
881 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
882 return new WP_Error('forbidden', __('You do not have permission to access this voucher.', 'yatra'), ['status' => 403]);
883 }
884
885 // Get trip details if available
886 $trip = null;
887 if (!empty($payment->trip_id)) {
888 $trip = $this->tripRepository->find((int) $payment->trip_id);
889 }
890
891 // Get company settings
892 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
893 $companyAddress = SettingsService::get('company_address', '');
894 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
895 $companyPhone = SettingsService::get('company_phone', '');
896 $currency = SettingsService::getCurrency();
897 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
898
899 // Format dates
900 $bookingDate = !empty($payment->created_at) ? date_i18n(get_option('date_format'), strtotime($payment->created_at)) : '';
901 $travelDate = !empty($payment->travel_date) ? date_i18n(get_option('date_format'), strtotime($payment->travel_date)) : '';
902
903 // Calculate return date if duration is available
904 $returnDate = '';
905 if (!empty($payment->travel_date) && !empty($trip->duration ?? 0)) {
906 $returnTimestamp = strtotime($payment->travel_date . ' +' . (int) ($trip->duration ?? 0) . ' days');
907 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
908 }
909
910 $bookingRef = (string) ($payment->booking_reference ?? $payment->booking_number ?? $payment->reference ?? (string) $paymentId);
911 $filename = 'Travel Voucher #' . $bookingRef . '.pdf';
912
913 $pdfService = new PdfService();
914 if (!$pdfService->isAvailable()) {
915 return new WP_Error(
916 'pdf_engine_missing',
917 __('Voucher PDF generator is not installed. Please run composer install to install dompdf/dompdf.', 'yatra'),
918 ['status' => 500]
919 );
920 }
921
922 $templateData = [
923 'company_name' => $companyName,
924 'company_address' => $companyAddress,
925 'company_email' => $companyEmail,
926 'company_phone' => $companyPhone,
927 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
928 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
929 'booking_ref' => $bookingRef,
930 'booking_date' => $bookingDate,
931 'booking_status' => ucfirst($payment->status ?? 'confirmed'),
932 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
933 (in_array(strtolower((string) ($payment->status ?? '')), ['cancelled'], true) ? 'cancelled' : 'pending'),
934 'trip_title' => $trip ? ($trip->title ?? $payment->trip_title ?? __('Trip Booking', 'yatra')) : ($payment->trip_title ?? __('Trip Booking', 'yatra')),
935 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
936 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
937 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
938 'destination' => $trip ? ($trip->destination ?? $payment->destination ?? '') : ($payment->destination ?? ''),
939 'travel_date' => $travelDate,
940 'return_date' => $returnDate,
941 'currency_symbol' => $currencySymbol,
942 'total_amount' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
943 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
944 'amount_due' => number_format((float) ($payment->booking_amount_due ?? 0), 2),
945 'traveler_count' => (int) ($payment->traveler_count ?? 1),
946 ];
947
948 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/voucher.php', $templateData, [
949 'paper' => 'A4',
950 'orientation' => 'portrait',
951 'default_font' => 'DejaVu Sans',
952 ]);
953
954 if ($isPreview) {
955 // For preview, return PDF as inline display
956 return new WP_REST_Response([
957 'success' => true,
958 'pdf_data' => base64_encode($pdfBinary),
959 'filename' => $filename,
960 ]);
961 } else {
962 // For download, output PDF as download
963 $pdfService->outputPdfDownload($pdfBinary, $filename);
964 exit;
965 }
966 }
967
968 /**
969 * GET /payments/{payment_id}/itinerary - Download travel itinerary for a payment
970 */
971 public function download_itinerary(WP_REST_Request $request)
972 {
973 $paymentId = (int) $request->get_param('payment_id');
974 $isPreview = $request->get_param('preview') === '1';
975 $isDownload = $request->get_param('download') === '1';
976
977 if ($paymentId <= 0) {
978 return new WP_Error('invalid_payment', __('Invalid payment ID.', 'yatra'), ['status' => 400]);
979 }
980
981 // Get payment with booking details
982 $payment = $this->paymentRepository->findWithBooking($paymentId);
983
984 if (!$payment) {
985 return new WP_Error('payment_not_found', __('Payment not found.', 'yatra'), ['status' => 404]);
986 }
987
988 // Verify user is logged in and owns this payment (or is admin)
989 $currentUserId = get_current_user_id();
990 $bookingUserId = (int) ($payment->booking_user_id ?? $payment->user_id ?? 0);
991
992 // Must be logged in
993 if (!$currentUserId) {
994 return new WP_Error('unauthorized', __('You must be logged in to download itineraries.', 'yatra'), ['status' => 401]);
995 }
996
997 // Must own the booking or be admin
998 if ($bookingUserId && $currentUserId !== $bookingUserId && !current_user_can('manage_options')) {
999 return new WP_Error('forbidden', __('You do not have permission to access this itinerary.', 'yatra'), ['status' => 403]);
1000 }
1001
1002 // Get trip details if available
1003 $trip = null;
1004 if (!empty($payment->trip_id)) {
1005 $trip = $this->tripRepository->find((int) $payment->trip_id);
1006 }
1007
1008 // Get company settings
1009 $companyName = SettingsService::get('company_name', get_bloginfo('name'));
1010 $companyAddress = SettingsService::get('company_address', '');
1011 $companyEmail = SettingsService::get('company_email', get_option('admin_email'));
1012 $companyPhone = SettingsService::get('company_phone', '');
1013 $currency = SettingsService::getCurrency();
1014 $currencySymbol = FormatHelper::getCurrencySymbol($currency);
1015
1016 // Format dates
1017 $bookingDate = !empty($payment->created_at) ? date_i18n(get_option('date_format'), strtotime($payment->created_at)) : '';
1018 $travelDate = !empty($payment->travel_date) ? date_i18n(get_option('date_format'), strtotime($payment->travel_date)) : '';
1019
1020 // Calculate return date if duration is available
1021 $returnDate = '';
1022 if (!empty($payment->travel_date) && !empty($trip->duration ?? 0)) {
1023 $returnTimestamp = strtotime($payment->travel_date . ' +' . (int) ($trip->duration ?? 0) . ' days');
1024 $returnDate = date_i18n(get_option('date_format'), $returnTimestamp);
1025 }
1026
1027 // Generate booking reference
1028 $bookingRef = '';
1029 if (!empty($payment->booking_id)) {
1030 $bookingRef = 'YTR-' . strtoupper(str_pad((string) $payment->booking_id, 8, '0', STR_PAD_LEFT));
1031 }
1032
1033 // Generate PDF using PDF service
1034 $pdfService = new PdfService();
1035 $filename = 'Travel-Itinerary-' . $bookingRef . '.pdf';
1036
1037 // Prepare template data with null-safe access
1038 $templateData = [
1039 'company_name' => $companyName,
1040 'company_address' => $companyAddress,
1041 'company_email' => $companyEmail,
1042 'company_phone' => $companyPhone,
1043 'customer_name' => trim(($payment->contact_first_name ?? '') . ' ' . ($payment->contact_last_name ?? '')) ?: ($payment->customer_name ?? __('Customer', 'yatra')),
1044 'customer_email' => $payment->contact_email ?? $payment->customer_email ?? '',
1045 'booking_ref' => $bookingRef,
1046 'booking_date' => $bookingDate,
1047 'booking_status' => ucfirst($payment->status ?? 'confirmed'),
1048 'status_class' => in_array(strtolower((string) ($payment->status ?? '')), ['confirmed', 'completed', 'success'], true) ? 'confirmed' :
1049 (in_array(strtolower((string) ($payment->status ?? '')), ['cancelled'], true) ? 'cancelled' : 'pending'),
1050 'trip_title' => $trip ? ($trip->title ?? $payment->trip_title ?? __('Trip Booking', 'yatra')) : ($payment->trip_title ?? __('Trip Booking', 'yatra')),
1051 'trip_description' => $trip ? ($trip->description ?? $trip->content ?? '') : '',
1052 'trip_duration' => $trip && $trip->duration ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
1053 'trip_difficulty' => $trip ? ($trip->difficulty_name ?? '') : '',
1054 'trip_highlights' => $trip ? ($trip->highlights ?? $trip->trip_highlights ?? '') : '',
1055 'trip_includes' => $trip ? ($trip->includes ?? $trip->trip_includes ?? '') : '',
1056 'trip_excludes' => $trip ? ($trip->excludes ?? $trip->trip_excludes ?? '') : '',
1057 'departure_location' => $trip ? ($trip->departure_location ?? '') : '',
1058 'destination' => $trip ? ($trip->destination ?? $payment->destination ?? '') : ($payment->destination ?? ''),
1059 'travel_date' => $travelDate,
1060 'return_date' => $returnDate,
1061 'currency_symbol' => $currencySymbol,
1062 'total_amount' => number_format((float) ($payment->booking_total_amount ?? $payment->amount ?? 0), 2),
1063 'amount_paid' => number_format((float) ($payment->booking_amount_paid ?? $payment->amount ?? 0), 2),
1064 'amount_due' => number_format((float) ($payment->booking_amount_due ?? 0), 2),
1065 'traveler_count' => (int) ($payment->traveler_count ?? 1),
1066 ];
1067
1068 $pdfBinary = $pdfService->renderTemplateToPdfSafely('pdf/itinerary.php', $templateData, [
1069 'paper' => 'A4',
1070 'orientation' => 'portrait',
1071 'default_font' => 'DejaVu Sans',
1072 ]);
1073
1074 if ($isPreview) {
1075 // For preview, return PDF as inline display
1076 return new WP_REST_Response([
1077 'success' => true,
1078 'pdf_data' => base64_encode($pdfBinary),
1079 'filename' => $filename,
1080 ]);
1081 } else {
1082 // For download, output PDF as download
1083 $pdfService->outputPdfDownload($pdfBinary, $filename);
1084 exit;
1085 }
1086 }
1087 }
1088