PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
3.0.16 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 All 84 releases
yatra / app / PaymentGateways / Gateways / PayPal / PayPalGateway.php

PayPalGateway.php in Yatra – Travel Booking & Tour Operator Software 3.0.6, at app/PaymentGateways/Gateways/PayPal/PayPalGateway.php

779 lines 28.9 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\PaymentGateways\Gateways\PayPal;
6
7 use Yatra\Database\Tables\BookingsTable;
8 use Yatra\Database\Tables\BookingPaymentsTable;
9 use Yatra\PaymentGateways\AbstractPaymentGateway;
10 use Yatra\PaymentGateways\GatewayUserMessages;
11
12 class PayPalGateway extends AbstractPaymentGateway
13 {
14 protected string $id = 'paypal';
15 protected string $title = 'PayPal';
16 protected string $description = 'Accept PayPal and credit card payments';
17 protected string $icon = 'paypal.svg';
18 protected string $sandboxUrl = 'https://developer.paypal.com/tools/sandbox/';
19 protected array $supports = ['paypal', 'credit_card', 'refunds', 'recurring', 'tokenization'];
20
21 public function getConfigFields(): array
22 {
23 return [
24 [
25 'id' => 'mode',
26 'type' => 'select',
27 'label' => __('Integration Mode', 'yatra'),
28 'description' => __('Choose how to connect PayPal', 'yatra'),
29 'default' => 'simple',
30 'options' => [
31 'simple' => __('Simple (Email Only) - Quick setup, basic payments', 'yatra'),
32 'advanced' => __('Advanced (API) - Refunds, saved cards, scheduled payments', 'yatra'),
33 ],
34 'help_text' => __('Simple mode: Just enter your PayPal email. Advanced mode: Enables refunds, saved payment methods, and scheduled payments.', 'yatra'),
35 ],
36 [
37 'id' => 'email',
38 'type' => 'email',
39 'label' => __('PayPal Email', 'yatra'),
40 'description' => __('Your PayPal account email address', 'yatra'),
41 'placeholder' => '[email protected]',
42 'default' => '',
43 'help_text' => __('Enter the email address associated with your PayPal Business or Premier account.', 'yatra'),
44 'help_url_live' => 'https://www.paypal.com/businesswallet/settings',
45 'show_when' => ['mode' => 'simple'],
46 ],
47 [
48 'id' => 'client_id',
49 'type' => 'text',
50 'label' => __('Client ID', 'yatra'),
51 'description' => __('Your PayPal application client ID', 'yatra'),
52 'placeholder' => 'AeA1QIZXiflr1...',
53 'default' => '',
54 'help_url_test' => 'https://developer.paypal.com/tools/sandbox/',
55 'help_url_live' => 'https://developer.paypal.com/dashboard/applications/live',
56 'help_text' => __('Create an app in PayPal Developer Dashboard to get Client ID', 'yatra'),
57 'show_when' => ['mode' => 'advanced'],
58 ],
59 [
60 'id' => 'client_secret',
61 'type' => 'password',
62 'label' => __('Client Secret', 'yatra'),
63 'description' => __('Your PayPal application client secret', 'yatra'),
64 'placeholder' => 'EC...',
65 'default' => '',
66 'help_url_test' => 'https://developer.paypal.com/tools/sandbox/',
67 'help_url_live' => 'https://developer.paypal.com/dashboard/applications/live',
68 'help_text' => __('Get Client Secret from the same PayPal app you created', 'yatra'),
69 'show_when' => ['mode' => 'advanced'],
70 ],
71 ];
72 }
73
74 /**
75 * Check if using simple (email-only) mode
76 */
77 private function isSimpleMode(): bool
78 {
79 return ($this->config['mode'] ?? 'simple') === 'simple';
80 }
81
82 public function isProperlyConfigured(): bool
83 {
84 if ($this->isSimpleMode()) {
85 return !empty(trim((string) ($this->config['email'] ?? '')));
86 }
87
88 return !empty(trim((string) ($this->config['client_id'] ?? '')))
89 && !empty(trim((string) ($this->config['client_secret'] ?? '')));
90 }
91
92 private function getBaseUrl(): string
93 {
94 return \Yatra\Services\SettingsService::isPaymentTestMode()
95 ? 'https://api-m.sandbox.paypal.com'
96 : 'https://api-m.paypal.com';
97 }
98
99 private function getAccessToken(): ?string
100 {
101 $response = $this->makeRequest($this->getBaseUrl() . '/v1/oauth2/token', [
102 'method' => 'POST',
103 'headers' => [
104 'Authorization' => 'Basic ' . base64_encode(($this->config['client_id'] ?? '') . ':' . ($this->config['client_secret'] ?? '')),
105 'Content-Type' => 'application/x-www-form-urlencoded',
106 ],
107 'body' => 'grant_type=client_credentials',
108 ]);
109
110 return $response['body']['access_token'] ?? null;
111 }
112
113 private function getHeaders(): array
114 {
115 $accessToken = $this->getAccessToken();
116 return [
117 'Authorization' => 'Bearer ' . $accessToken,
118 'Content-Type' => 'application/json',
119 ];
120 }
121
122 public function processPayment(array $paymentData): array
123 {
124 // Log payment attempt for debugging
125 $this->log('Processing PayPal payment', [
126 'mode' => $this->isSimpleMode() ? 'simple' : 'advanced',
127 'amount' => $paymentData['amount'] ?? 0,
128 'currency' => $paymentData['currency'] ?? 'USD',
129 'booking_id' => $paymentData['booking_id'] ?? 0,
130 'test_mode' => \Yatra\Services\SettingsService::isPaymentTestMode(),
131 ]);
132
133 // Use simple or advanced mode based on configuration
134 if ($this->isSimpleMode()) {
135 return $this->processSimplePayment($paymentData);
136 }
137
138 return $this->processAdvancedPayment($paymentData);
139 }
140
141 /**
142 * Process payment using Simple mode (PayPal Standard - email only)
143 * Redirects to PayPal hosted checkout page
144 */
145 private function processSimplePayment(array $paymentData): array
146 {
147 $email = $this->config['email'] ?? '';
148 if (empty($email)) {
149 return ['success' => false, 'error' => GatewayUserMessages::gatewayNotConfigured($this)];
150 }
151
152 $amount = number_format((float) ($paymentData['amount'] ?? 0), 2, '.', '');
153 $currency = $paymentData['currency'] ?? 'USD';
154 $bookingId = $paymentData['booking_id'] ?? 0;
155 $reference = $paymentData['reference'] ?? $bookingId;
156 $description = $paymentData['description'] ?? sprintf(
157 /* translators: %s: booking reference. */
158 __('Booking #%s', 'yatra'),
159 $reference
160 );
161 $returnUrl = $paymentData['return_url'] ?? yatra_get_booking_confirmation_url((string) $reference);
162 $cancelUrl = $paymentData['cancel_url'] ?? home_url('/book/?payment=cancelled&ref=' . $reference);
163
164 // PayPal Standard base URL
165 $isTestMode = \Yatra\Services\SettingsService::isPaymentTestMode();
166 $paypalUrl = $isTestMode
167 ? 'https://www.sandbox.paypal.com/cgi-bin/webscr'
168 : 'https://www.paypal.com/cgi-bin/webscr';
169
170 // Build PayPal Standard payment URL
171 $params = [
172 'cmd' => '_xclick',
173 'business' => $email,
174 'item_name' => $description,
175 'item_number' => $reference,
176 'amount' => $amount,
177 'currency_code' => $currency,
178 'return' => add_query_arg(['paypal' => 'success', 'booking_id' => $bookingId], $returnUrl),
179 'cancel_return' => $cancelUrl,
180 'notify_url' => rest_url('yatra/v1/payment/webhook/paypal'),
181 'custom' => wp_json_encode(['booking_id' => $bookingId, 'reference' => $reference]),
182 'no_shipping' => '1',
183 'no_note' => '1',
184 'rm' => '2', // POST data back to return URL
185 ];
186
187 $redirectUrl = $paypalUrl . '?' . http_build_query($params);
188
189 $this->log('PayPal Simple mode redirect URL created', [
190 'booking_id' => $bookingId,
191 'amount' => $amount,
192 'test_mode' => $isTestMode,
193 ]);
194
195 return [
196 'success' => true,
197 'redirect_url' => $redirectUrl,
198 'transaction_id' => 'pending_' . $bookingId, // Will be updated via IPN
199 'mode' => 'simple',
200 ];
201 }
202
203 /**
204 * Process payment using Advanced mode (PayPal REST API)
205 * Creates order via API and redirects to approval URL
206 */
207 private function processAdvancedPayment(array $paymentData): array
208 {
209 $accessToken = $this->getAccessToken();
210 if (!$accessToken) {
211 $this->log('PayPal authentication failed', ['client_id' => substr($this->config['client_id'] ?? '', 0, 10) . '...']);
212 return ['success' => false, 'error' => __('Failed to authenticate with PayPal. Please check your API credentials.', 'yatra')];
213 }
214
215 $amount = number_format((float) ($paymentData['amount'] ?? 0), 2, '.', '');
216 $currency = $paymentData['currency'] ?? 'USD';
217 $bookingId = $paymentData['booking_id'] ?? 0;
218 $referenceForReturn = (string) ($paymentData['reference'] ?? $bookingId);
219 $returnUrl = $paymentData['return_url'] ?? yatra_get_booking_confirmation_url($referenceForReturn);
220 $cancelUrl = $paymentData['cancel_url'] ?? home_url('/book/?payment=cancelled');
221 $savePayment = !empty($paymentData['save_payment']);
222
223 $orderData = [
224 'intent' => 'CAPTURE',
225 'purchase_units' => [[
226 'custom_id' => (string) $bookingId,
227 'description' => $paymentData['description'] ?? sprintf(
228 /* translators: %s: booking reference. */
229 __('Booking #%s', 'yatra'),
230 $bookingId
231 ),
232 'amount' => [
233 'currency_code' => $currency,
234 'value' => $amount,
235 ],
236 ]],
237 'application_context' => [
238 'return_url' => add_query_arg(['paypal' => 'success', 'booking_id' => $bookingId], $returnUrl),
239 'cancel_url' => $cancelUrl,
240 'user_action' => 'PAY_NOW',
241 'brand_name' => get_bloginfo('name'),
242 ],
243 ];
244
245 // Enable vault for saving payment method
246 if ($savePayment) {
247 $orderData['payment_source'] = [
248 'paypal' => [
249 'experience_context' => [
250 'payment_method_preference' => 'IMMEDIATE_PAYMENT_REQUIRED',
251 'return_url' => add_query_arg(['paypal' => 'success', 'booking_id' => $bookingId], $returnUrl),
252 'cancel_url' => $cancelUrl,
253 ],
254 'attributes' => [
255 'vault' => [
256 'store_in_vault' => 'ON_SUCCESS',
257 'usage_type' => 'MERCHANT',
258 ],
259 ],
260 ],
261 ];
262 }
263
264 $response = $this->makeRequest($this->getBaseUrl() . '/v2/checkout/orders', [
265 'method' => 'POST',
266 'headers' => [
267 'Authorization' => 'Bearer ' . $accessToken,
268 'Content-Type' => 'application/json',
269 ],
270 'body' => wp_json_encode($orderData),
271 ]);
272
273 if (!$response['success'] || empty($response['body']['id'])) {
274 $errorMessage = $response['body']['message'] ?? $response['body']['error_description'] ?? __('Failed to create PayPal order', 'yatra');
275 $this->log('PayPal order creation failed', [
276 'response' => $response,
277 'error' => $errorMessage,
278 ]);
279 return ['success' => false, 'error' => $errorMessage];
280 }
281
282 // Get approval URL
283 $approvalUrl = null;
284 foreach ($response['body']['links'] ?? [] as $link) {
285 if ($link['rel'] === 'approve') {
286 $approvalUrl = $link['href'];
287 break;
288 }
289 }
290
291 if (empty($approvalUrl)) {
292 $this->log('PayPal order created but no approval URL found', ['order_id' => $response['body']['id']]);
293 return ['success' => false, 'error' => __('PayPal order created but redirect URL not found', 'yatra')];
294 }
295
296 $this->log('PayPal order created successfully', [
297 'order_id' => $response['body']['id'],
298 'approval_url' => $approvalUrl,
299 ]);
300
301 return [
302 'success' => true,
303 'order_id' => $response['body']['id'],
304 'transaction_id' => $response['body']['id'],
305 'redirect_url' => $approvalUrl,
306 'approval_url' => $approvalUrl,
307 'client_id' => $this->config['client_id'] ?? '',
308 'sandbox' => \Yatra\Services\SettingsService::isPaymentTestMode(),
309 'mode' => 'advanced',
310 ];
311 }
312
313 /**
314 * Create PayPal customer (for vault)
315 */
316 public function createCustomer(array $customerData): array
317 {
318 $accessToken = $this->getAccessToken();
319 if (!$accessToken) {
320 return ['success' => false, 'error' => __('Failed to authenticate with PayPal', 'yatra')];
321 }
322
323 // PayPal uses vault tokens associated with merchant, not traditional customers
324 // Generate a unique customer reference
325 $customerId = 'yatra_' . md5($customerData['email'] . time());
326
327 return [
328 'success' => true,
329 'customer_id' => $customerId,
330 ];
331 }
332
333 /**
334 * Save payment token after successful vaulted payment
335 */
336 public function savePaymentMethod(string $customerId, array $paymentMethodData): array
337 {
338 // PayPal vault token is returned after successful order capture
339 $vaultId = $paymentMethodData['vault_id'] ?? '';
340
341 if (empty($vaultId)) {
342 return [
343 'success' => false,
344 'error' => __('Vault ID is required', 'yatra'),
345 ];
346 }
347
348 return [
349 'success' => true,
350 'payment_method_id' => $vaultId,
351 'type' => 'paypal',
352 'card_brand' => 'paypal',
353 'card_last4' => substr($paymentMethodData['email'] ?? '', -4),
354 ];
355 }
356
357 /**
358 * Charge saved PayPal payment token
359 */
360 public function chargePaymentMethod(string $customerId, string $paymentMethodId, array $paymentData): array
361 {
362 $accessToken = $this->getAccessToken();
363 if (!$accessToken) {
364 return ['success' => false, 'error' => __('Failed to authenticate with PayPal', 'yatra')];
365 }
366
367 $amount = number_format((float) ($paymentData['amount'] ?? 0), 2, '.', '');
368 $currency = $paymentData['currency'] ?? 'USD';
369 $bookingId = $paymentData['booking_id'] ?? 0;
370
371 // Create order using vaulted payment source
372 $orderData = [
373 'intent' => 'CAPTURE',
374 'purchase_units' => [[
375 'custom_id' => (string) $bookingId,
376 'description' => $paymentData['description'] ?? 'Scheduled Payment',
377 'amount' => [
378 'currency_code' => $currency,
379 'value' => $amount,
380 ],
381 ]],
382 'payment_source' => [
383 'paypal' => [
384 'vault_id' => $paymentMethodId,
385 ],
386 ],
387 ];
388
389 $response = $this->makeRequest($this->getBaseUrl() . '/v2/checkout/orders', [
390 'method' => 'POST',
391 'headers' => [
392 'Authorization' => 'Bearer ' . $accessToken,
393 'Content-Type' => 'application/json',
394 'PayPal-Request-Id' => uniqid('yatra_', true),
395 ],
396 'body' => wp_json_encode($orderData),
397 ]);
398
399 if (!$response['success'] || empty($response['body']['id'])) {
400 return [
401 'success' => false,
402 'error' => $response['body']['message'] ?? __('Failed to create PayPal order', 'yatra'),
403 ];
404 }
405
406 $orderId = $response['body']['id'];
407
408 // Auto-capture for vaulted payments
409 if ($response['body']['status'] === 'COMPLETED') {
410 $captureId = $response['body']['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
411 return [
412 'success' => true,
413 'transaction_id' => $captureId ?? $orderId,
414 'order_id' => $orderId,
415 'amount' => (float) $amount,
416 'currency' => $currency,
417 'status' => 'completed',
418 ];
419 }
420
421 // If not auto-captured, capture now
422 $captureResponse = $this->makeRequest($this->getBaseUrl() . "/v2/checkout/orders/{$orderId}/capture", [
423 'method' => 'POST',
424 'headers' => [
425 'Authorization' => 'Bearer ' . $accessToken,
426 'Content-Type' => 'application/json',
427 ],
428 ]);
429
430 if ($captureResponse['body']['status'] === 'COMPLETED') {
431 $captureId = $captureResponse['body']['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
432 return [
433 'success' => true,
434 'transaction_id' => $captureId ?? $orderId,
435 'order_id' => $orderId,
436 'amount' => (float) $amount,
437 'currency' => $currency,
438 'status' => 'completed',
439 ];
440 }
441
442 return [
443 'success' => false,
444 'error' => __('Payment capture failed', 'yatra'),
445 ];
446 }
447
448 /**
449 * Get saved payment methods
450 */
451 public function getPaymentMethods(string $customerId): array
452 {
453 // PayPal vault tokens need to be stored locally
454 // as PayPal doesn't provide a list endpoint for merchant-stored tokens
455 return [];
456 }
457
458 /**
459 * Delete saved payment method
460 */
461 public function deletePaymentMethod(string $paymentMethodId): array
462 {
463 $accessToken = $this->getAccessToken();
464 if (!$accessToken) {
465 return ['success' => false, 'error' => __('Failed to authenticate with PayPal', 'yatra')];
466 }
467
468 $response = $this->makeRequest($this->getBaseUrl() . "/v3/vault/payment-tokens/{$paymentMethodId}", [
469 'method' => 'DELETE',
470 'headers' => [
471 'Authorization' => 'Bearer ' . $accessToken,
472 ],
473 ]);
474
475 return [
476 'success' => $response['code'] === 204 || $response['success'],
477 ];
478 }
479
480 public function verifyPayment(string $transactionId): array
481 {
482 $accessToken = $this->getAccessToken();
483 if (!$accessToken) {
484 return ['success' => false, 'error' => 'Authentication failed'];
485 }
486
487 // First try to get order details
488 $orderResponse = $this->makeRequest($this->getBaseUrl() . "/v2/checkout/orders/{$transactionId}", [
489 'method' => 'GET',
490 'headers' => [
491 'Authorization' => 'Bearer ' . $accessToken,
492 ],
493 ]);
494
495 // If already completed, return success
496 if (($orderResponse['body']['status'] ?? '') === 'COMPLETED') {
497 $vaultId = null;
498 $paymentSource = $orderResponse['body']['payment_source']['paypal'] ?? [];
499 if (!empty($paymentSource['attributes']['vault']['id'])) {
500 $vaultId = $paymentSource['attributes']['vault']['id'];
501 }
502
503 return [
504 'success' => true,
505 'status' => 'COMPLETED',
506 'capture_id' => $orderResponse['body']['purchase_units'][0]['payments']['captures'][0]['id'] ?? null,
507 'vault_id' => $vaultId,
508 ];
509 }
510
511 // If approved, capture the order
512 if (($orderResponse['body']['status'] ?? '') === 'APPROVED') {
513 $response = $this->makeRequest($this->getBaseUrl() . "/v2/checkout/orders/{$transactionId}/capture", [
514 'method' => 'POST',
515 'headers' => [
516 'Authorization' => 'Bearer ' . $accessToken,
517 'Content-Type' => 'application/json',
518 ],
519 ]);
520
521 $status = $response['body']['status'] ?? '';
522
523 // Check for vault ID
524 $vaultId = null;
525 $paymentSource = $response['body']['payment_source']['paypal'] ?? [];
526 if (!empty($paymentSource['attributes']['vault']['id'])) {
527 $vaultId = $paymentSource['attributes']['vault']['id'];
528 }
529
530 return [
531 'success' => $status === 'COMPLETED',
532 'status' => $status,
533 'capture_id' => $response['body']['purchase_units'][0]['payments']['captures'][0]['id'] ?? null,
534 'vault_id' => $vaultId,
535 ];
536 }
537
538 return [
539 'success' => false,
540 'status' => $orderResponse['body']['status'] ?? 'UNKNOWN',
541 ];
542 }
543
544 public function processRefund(string $transactionId, float $amount): array
545 {
546 $accessToken = $this->getAccessToken();
547 if (!$accessToken) {
548 return ['success' => false, 'error' => 'Authentication failed'];
549 }
550
551 $response = $this->makeRequest($this->getBaseUrl() . "/v2/payments/captures/{$transactionId}/refund", [
552 'method' => 'POST',
553 'headers' => [
554 'Authorization' => 'Bearer ' . $accessToken,
555 'Content-Type' => 'application/json',
556 ],
557 'body' => wp_json_encode([
558 'amount' => [
559 'value' => number_format($amount, 2, '.', ''),
560 'currency_code' => 'USD',
561 ],
562 ]),
563 ]);
564
565 return [
566 'success' => $response['success'],
567 'refund_id' => $response['body']['id'] ?? null,
568 ];
569 }
570
571 /**
572 * Handle PayPal webhook (supports both IPN for Simple mode and REST webhooks for Advanced mode)
573 */
574 public function handleWebhook(array $data): array
575 {
576 $body = $data['raw_body'] ?? '';
577 $postData = $data['post_data'] ?? [];
578
579 // Check if this is an IPN notification (Simple mode)
580 if (!empty($postData['txn_type']) || !empty($postData['payment_status'])) {
581 return $this->handleIPN($postData);
582 }
583
584 // Otherwise handle as REST API webhook (Advanced mode)
585 $event = json_decode($body, true);
586 $eventType = $event['event_type'] ?? '';
587
588 switch ($eventType) {
589 case 'PAYMENT.CAPTURE.COMPLETED':
590 do_action('yatra_paypal_payment_completed', $event['resource'] ?? []);
591 break;
592
593 case 'PAYMENT.CAPTURE.REFUNDED':
594 do_action('yatra_paypal_payment_refunded', $event['resource'] ?? []);
595 break;
596
597 case 'VAULT.PAYMENT-TOKEN.CREATED':
598 do_action('yatra_paypal_token_created', $event['resource'] ?? []);
599 break;
600 }
601
602 return ['success' => true, 'event_type' => $eventType];
603 }
604
605 /**
606 * Handle PayPal IPN (Instant Payment Notification) for Simple mode
607 */
608 private function handleIPN(array $ipnData): array
609 {
610 $this->log('PayPal IPN received', $ipnData);
611
612 // Verify IPN with PayPal
613 $isTestMode = \Yatra\Services\SettingsService::isPaymentTestMode();
614 $verifyUrl = $isTestMode
615 ? 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr'
616 : 'https://ipnpb.paypal.com/cgi-bin/webscr';
617
618 $verifyData = array_merge(['cmd' => '_notify-validate'], $ipnData);
619
620 $response = wp_remote_post($verifyUrl, [
621 'body' => $verifyData,
622 'timeout' => 60,
623 'httpversion' => '1.1',
624 ]);
625
626 if (is_wp_error($response)) {
627 $this->log('IPN verification failed', ['error' => $response->get_error_message()]);
628 return ['success' => false, 'error' => 'IPN verification failed'];
629 }
630
631 $responseBody = wp_remote_retrieve_body($response);
632
633 if ($responseBody !== 'VERIFIED') {
634 $this->log('IPN not verified', ['response' => $responseBody]);
635 return ['success' => false, 'error' => 'IPN not verified'];
636 }
637
638 // Process the payment
639 $paymentStatus = $ipnData['payment_status'] ?? '';
640 $customData = json_decode($ipnData['custom'] ?? '{}', true);
641 $bookingId = $customData['booking_id'] ?? 0;
642 $transactionId = $ipnData['txn_id'] ?? '';
643 $amount = (float) ($ipnData['mc_gross'] ?? 0);
644 $currency = $ipnData['mc_currency'] ?? 'USD';
645
646 if ($paymentStatus === 'Completed' && $bookingId > 0) {
647 // Fire action for payment completed
648 do_action('yatra_payment_completed', [
649 'booking_id' => $bookingId,
650 'transaction_id' => $transactionId,
651 'amount' => $amount,
652 'currency' => $currency,
653 'gateway' => 'paypal',
654 'mode' => 'simple',
655 ]);
656
657 $this->log('PayPal IPN payment completed', [
658 'booking_id' => $bookingId,
659 'transaction_id' => $transactionId,
660 'amount' => $amount,
661 ]);
662
663 return ['success' => true, 'status' => 'completed', 'booking_id' => $bookingId];
664 }
665
666 return ['success' => true, 'status' => $paymentStatus];
667 }
668
669 /**
670 * Check if this gateway should handle the return request
671 */
672 public function shouldHandleReturn(array $params): bool
673 {
674 return isset($params['paypal']) && $params['paypal'] === 'success';
675 }
676
677 /**
678 * Handle payment return from PayPal (when user is redirected back after payment)
679 * Works for both Simple and Advanced modes
680 */
681 public function handlePaymentReturn($booking, $bookingRepository): void
682 {
683 global $wpdb;
684
685 // Check if payment already processed
686 if ($booking->payment_status === 'paid') {
687 return;
688 }
689
690 $bookingId = (int) $booking->id;
691 $isAdvancedMode = !$this->isSimpleMode();
692
693 $this->log('Handling PayPal return', [
694 'booking_id' => $bookingId,
695 'mode' => $isAdvancedMode ? 'advanced' : 'simple',
696 ]);
697
698 if ($isAdvancedMode) {
699 // Advanced mode: Get token from URL and capture the order
700 $token = sanitize_text_field($_GET['token'] ?? '');
701
702 if (!empty($token)) {
703 $result = $this->verifyPayment($token);
704
705 if ($result['success'] && $result['status'] === 'COMPLETED') {
706 $this->completePayment($booking, $bookingRepository, $result['capture_id'] ?? $token);
707 }
708 }
709 } else {
710 // Simple mode: Payment verification happens via IPN
711 // For now, mark as pending verification - IPN will update it
712 // But we can show success to user since PayPal redirected them back
713 $this->log('Simple mode return - awaiting IPN verification', ['booking_id' => $bookingId]);
714 }
715 }
716
717 /**
718 * Complete the payment and update booking status
719 */
720 private function completePayment($booking, $bookingRepository, string $transactionId): void
721 {
722 global $wpdb;
723
724 $bookingId = (int) $booking->id;
725 $amountDue = (float) ($booking->amount_due ?? ($booking->total_amount - $booking->amount_paid));
726 $previousBookingStatus = (string) ($booking->status ?? 'pending');
727
728 // Update booking payment status
729 $bookings_table = BookingsTable::getTableName();
730 $wpdb->update(
731 $bookings_table,
732 [
733 'payment_status' => 'paid',
734 'amount_paid' => $booking->total_amount,
735 'amount_due' => 0,
736 'status' => 'confirmed',
737 'confirmed_at' => current_time('mysql'),
738 ],
739 ['id' => $bookingId],
740 ['%s', '%f', '%f', '%s', '%s'],
741 ['%d']
742 );
743
744 // Record the payment
745 $payments_table = BookingPaymentsTable::getTableName();
746 $wpdb->insert(
747 $payments_table,
748 [
749 'booking_id' => $bookingId,
750 'amount' => $amountDue,
751 'currency' => $booking->currency ?? 'USD',
752 'payment_gateway' => 'paypal',
753 'transaction_id' => $transactionId,
754 'status' => 'completed',
755 'created_at' => current_time('mysql'),
756 ],
757 ['%d', '%f', '%s', '%s', '%s', '%s', '%s']
758 );
759
760 $this->log('PayPal payment completed', [
761 'booking_id' => $bookingId,
762 'transaction_id' => $transactionId,
763 'amount' => $amountDue,
764 ]);
765
766 \yatra_trigger_booking_confirmed($bookingId, $previousBookingStatus);
767
768 // Fire action for other plugins/services
769 do_action('yatra_payment_completed', [
770 'booking_id' => $bookingId,
771 'transaction_id' => $transactionId,
772 'amount' => $amountDue,
773 'currency' => $booking->currency ?? 'USD',
774 'gateway' => 'paypal',
775 ]);
776 }
777 }
778
779