PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.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.2.6, at app/PaymentGateways/Gateways/PayPal/PayPalGateway.php

768 lines 28.6 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', 'scheduled_payments'];
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(__('Booking #%s', 'yatra'), $reference);
157 $returnUrl = $paymentData['return_url'] ?? yatra_get_booking_confirmation_url((string) $reference);
158 $cancelUrl = $paymentData['cancel_url'] ?? home_url('/book/?payment=cancelled&ref=' . $reference);
159
160 // PayPal Standard base URL
161 $isTestMode = \Yatra\Services\SettingsService::isPaymentTestMode();
162 $paypalUrl = $isTestMode
163 ? 'https://www.sandbox.paypal.com/cgi-bin/webscr'
164 : 'https://www.paypal.com/cgi-bin/webscr';
165
166 // Build PayPal Standard payment URL
167 $params = [
168 'cmd' => '_xclick',
169 'business' => $email,
170 'item_name' => $description,
171 'item_number' => $reference,
172 'amount' => $amount,
173 'currency_code' => $currency,
174 'return' => add_query_arg(['paypal' => 'success', 'booking_id' => $bookingId], $returnUrl),
175 'cancel_return' => $cancelUrl,
176 'notify_url' => rest_url('yatra/v1/payment/webhook/paypal'),
177 'custom' => wp_json_encode(['booking_id' => $bookingId, 'reference' => $reference]),
178 'no_shipping' => '1',
179 'no_note' => '1',
180 'rm' => '2', // POST data back to return URL
181 ];
182
183 $redirectUrl = $paypalUrl . '?' . http_build_query($params);
184
185 $this->log('PayPal Simple mode redirect URL created', [
186 'booking_id' => $bookingId,
187 'amount' => $amount,
188 'test_mode' => $isTestMode,
189 ]);
190
191 return [
192 'success' => true,
193 'redirect_url' => $redirectUrl,
194 'transaction_id' => 'pending_' . $bookingId, // Will be updated via IPN
195 'mode' => 'simple',
196 ];
197 }
198
199 /**
200 * Process payment using Advanced mode (PayPal REST API)
201 * Creates order via API and redirects to approval URL
202 */
203 private function processAdvancedPayment(array $paymentData): array
204 {
205 $accessToken = $this->getAccessToken();
206 if (!$accessToken) {
207 $this->log('PayPal authentication failed', ['client_id' => substr($this->config['client_id'] ?? '', 0, 10) . '...']);
208 return ['success' => false, 'error' => __('Failed to authenticate with PayPal. Please check your API credentials.', 'yatra')];
209 }
210
211 $amount = number_format((float) ($paymentData['amount'] ?? 0), 2, '.', '');
212 $currency = $paymentData['currency'] ?? 'USD';
213 $bookingId = $paymentData['booking_id'] ?? 0;
214 $referenceForReturn = (string) ($paymentData['reference'] ?? $bookingId);
215 $returnUrl = $paymentData['return_url'] ?? yatra_get_booking_confirmation_url($referenceForReturn);
216 $cancelUrl = $paymentData['cancel_url'] ?? home_url('/book/?payment=cancelled');
217 $savePayment = !empty($paymentData['save_payment']);
218
219 $orderData = [
220 'intent' => 'CAPTURE',
221 'purchase_units' => [[
222 'custom_id' => (string) $bookingId,
223 'description' => $paymentData['description'] ?? sprintf(__('Booking #%s', 'yatra'), $bookingId),
224 'amount' => [
225 'currency_code' => $currency,
226 'value' => $amount,
227 ],
228 ]],
229 'application_context' => [
230 'return_url' => add_query_arg(['paypal' => 'success', 'booking_id' => $bookingId], $returnUrl),
231 'cancel_url' => $cancelUrl,
232 'user_action' => 'PAY_NOW',
233 'brand_name' => get_bloginfo('name'),
234 ],
235 ];
236
237 // Enable vault for saving payment method
238 if ($savePayment) {
239 $orderData['payment_source'] = [
240 'paypal' => [
241 'experience_context' => [
242 'payment_method_preference' => 'IMMEDIATE_PAYMENT_REQUIRED',
243 'return_url' => add_query_arg(['paypal' => 'success', 'booking_id' => $bookingId], $returnUrl),
244 'cancel_url' => $cancelUrl,
245 ],
246 'attributes' => [
247 'vault' => [
248 'store_in_vault' => 'ON_SUCCESS',
249 'usage_type' => 'MERCHANT',
250 ],
251 ],
252 ],
253 ];
254 }
255
256 $response = $this->makeRequest($this->getBaseUrl() . '/v2/checkout/orders', [
257 'method' => 'POST',
258 'headers' => [
259 'Authorization' => 'Bearer ' . $accessToken,
260 'Content-Type' => 'application/json',
261 ],
262 'body' => wp_json_encode($orderData),
263 ]);
264
265 if (!$response['success'] || empty($response['body']['id'])) {
266 $errorMessage = $response['body']['message'] ?? $response['body']['error_description'] ?? __('Failed to create PayPal order', 'yatra');
267 $this->log('PayPal order creation failed', [
268 'response' => $response,
269 'error' => $errorMessage,
270 ]);
271 return ['success' => false, 'error' => $errorMessage];
272 }
273
274 // Get approval URL
275 $approvalUrl = null;
276 foreach ($response['body']['links'] ?? [] as $link) {
277 if ($link['rel'] === 'approve') {
278 $approvalUrl = $link['href'];
279 break;
280 }
281 }
282
283 if (empty($approvalUrl)) {
284 $this->log('PayPal order created but no approval URL found', ['order_id' => $response['body']['id']]);
285 return ['success' => false, 'error' => __('PayPal order created but redirect URL not found', 'yatra')];
286 }
287
288 $this->log('PayPal order created successfully', [
289 'order_id' => $response['body']['id'],
290 'approval_url' => $approvalUrl,
291 ]);
292
293 return [
294 'success' => true,
295 'order_id' => $response['body']['id'],
296 'transaction_id' => $response['body']['id'],
297 'redirect_url' => $approvalUrl,
298 'approval_url' => $approvalUrl,
299 'client_id' => $this->config['client_id'] ?? '',
300 'sandbox' => \Yatra\Services\SettingsService::isPaymentTestMode(),
301 'mode' => 'advanced',
302 ];
303 }
304
305 /**
306 * Create PayPal customer (for vault)
307 */
308 public function createCustomer(array $customerData): array
309 {
310 $accessToken = $this->getAccessToken();
311 if (!$accessToken) {
312 return ['success' => false, 'error' => __('Failed to authenticate with PayPal', 'yatra')];
313 }
314
315 // PayPal uses vault tokens associated with merchant, not traditional customers
316 // Generate a unique customer reference
317 $customerId = 'yatra_' . md5($customerData['email'] . time());
318
319 return [
320 'success' => true,
321 'customer_id' => $customerId,
322 ];
323 }
324
325 /**
326 * Save payment token after successful vaulted payment
327 */
328 public function savePaymentMethod(string $customerId, array $paymentMethodData): array
329 {
330 // PayPal vault token is returned after successful order capture
331 $vaultId = $paymentMethodData['vault_id'] ?? '';
332
333 if (empty($vaultId)) {
334 return [
335 'success' => false,
336 'error' => __('Vault ID is required', 'yatra'),
337 ];
338 }
339
340 return [
341 'success' => true,
342 'payment_method_id' => $vaultId,
343 'type' => 'paypal',
344 'card_brand' => 'paypal',
345 'card_last4' => substr($paymentMethodData['email'] ?? '', -4),
346 ];
347 }
348
349 /**
350 * Charge saved PayPal payment token
351 */
352 public function chargePaymentMethod(string $customerId, string $paymentMethodId, array $paymentData): array
353 {
354 $accessToken = $this->getAccessToken();
355 if (!$accessToken) {
356 return ['success' => false, 'error' => __('Failed to authenticate with PayPal', 'yatra')];
357 }
358
359 $amount = number_format((float) ($paymentData['amount'] ?? 0), 2, '.', '');
360 $currency = $paymentData['currency'] ?? 'USD';
361 $bookingId = $paymentData['booking_id'] ?? 0;
362
363 // Create order using vaulted payment source
364 $orderData = [
365 'intent' => 'CAPTURE',
366 'purchase_units' => [[
367 'custom_id' => (string) $bookingId,
368 'description' => $paymentData['description'] ?? 'Scheduled Payment',
369 'amount' => [
370 'currency_code' => $currency,
371 'value' => $amount,
372 ],
373 ]],
374 'payment_source' => [
375 'paypal' => [
376 'vault_id' => $paymentMethodId,
377 ],
378 ],
379 ];
380
381 $response = $this->makeRequest($this->getBaseUrl() . '/v2/checkout/orders', [
382 'method' => 'POST',
383 'headers' => [
384 'Authorization' => 'Bearer ' . $accessToken,
385 'Content-Type' => 'application/json',
386 'PayPal-Request-Id' => uniqid('yatra_', true),
387 ],
388 'body' => wp_json_encode($orderData),
389 ]);
390
391 if (!$response['success'] || empty($response['body']['id'])) {
392 return [
393 'success' => false,
394 'error' => $response['body']['message'] ?? __('Failed to create PayPal order', 'yatra'),
395 ];
396 }
397
398 $orderId = $response['body']['id'];
399
400 // Auto-capture for vaulted payments
401 if ($response['body']['status'] === 'COMPLETED') {
402 $captureId = $response['body']['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
403 return [
404 'success' => true,
405 'transaction_id' => $captureId ?? $orderId,
406 'order_id' => $orderId,
407 'amount' => (float) $amount,
408 'currency' => $currency,
409 'status' => 'completed',
410 ];
411 }
412
413 // If not auto-captured, capture now
414 $captureResponse = $this->makeRequest($this->getBaseUrl() . "/v2/checkout/orders/{$orderId}/capture", [
415 'method' => 'POST',
416 'headers' => [
417 'Authorization' => 'Bearer ' . $accessToken,
418 'Content-Type' => 'application/json',
419 ],
420 ]);
421
422 if ($captureResponse['body']['status'] === 'COMPLETED') {
423 $captureId = $captureResponse['body']['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
424 return [
425 'success' => true,
426 'transaction_id' => $captureId ?? $orderId,
427 'order_id' => $orderId,
428 'amount' => (float) $amount,
429 'currency' => $currency,
430 'status' => 'completed',
431 ];
432 }
433
434 return [
435 'success' => false,
436 'error' => __('Payment capture failed', 'yatra'),
437 ];
438 }
439
440 /**
441 * Get saved payment methods
442 */
443 public function getPaymentMethods(string $customerId): array
444 {
445 // PayPal vault tokens need to be stored locally
446 // as PayPal doesn't provide a list endpoint for merchant-stored tokens
447 return [];
448 }
449
450 /**
451 * Delete saved payment method
452 */
453 public function deletePaymentMethod(string $paymentMethodId): array
454 {
455 $accessToken = $this->getAccessToken();
456 if (!$accessToken) {
457 return ['success' => false, 'error' => __('Failed to authenticate with PayPal', 'yatra')];
458 }
459
460 $response = $this->makeRequest($this->getBaseUrl() . "/v3/vault/payment-tokens/{$paymentMethodId}", [
461 'method' => 'DELETE',
462 'headers' => [
463 'Authorization' => 'Bearer ' . $accessToken,
464 ],
465 ]);
466
467 return [
468 'success' => $response['code'] === 204 || $response['success'],
469 ];
470 }
471
472 public function verifyPayment(string $transactionId): array
473 {
474 $accessToken = $this->getAccessToken();
475 if (!$accessToken) {
476 return ['success' => false, 'error' => 'Authentication failed'];
477 }
478
479 // First try to get order details
480 $orderResponse = $this->makeRequest($this->getBaseUrl() . "/v2/checkout/orders/{$transactionId}", [
481 'method' => 'GET',
482 'headers' => [
483 'Authorization' => 'Bearer ' . $accessToken,
484 ],
485 ]);
486
487 // If already completed, return success
488 if (($orderResponse['body']['status'] ?? '') === 'COMPLETED') {
489 $vaultId = null;
490 $paymentSource = $orderResponse['body']['payment_source']['paypal'] ?? [];
491 if (!empty($paymentSource['attributes']['vault']['id'])) {
492 $vaultId = $paymentSource['attributes']['vault']['id'];
493 }
494
495 return [
496 'success' => true,
497 'status' => 'COMPLETED',
498 'capture_id' => $orderResponse['body']['purchase_units'][0]['payments']['captures'][0]['id'] ?? null,
499 'vault_id' => $vaultId,
500 ];
501 }
502
503 // If approved, capture the order
504 if (($orderResponse['body']['status'] ?? '') === 'APPROVED') {
505 $response = $this->makeRequest($this->getBaseUrl() . "/v2/checkout/orders/{$transactionId}/capture", [
506 'method' => 'POST',
507 'headers' => [
508 'Authorization' => 'Bearer ' . $accessToken,
509 'Content-Type' => 'application/json',
510 ],
511 ]);
512
513 $status = $response['body']['status'] ?? '';
514
515 // Check for vault ID
516 $vaultId = null;
517 $paymentSource = $response['body']['payment_source']['paypal'] ?? [];
518 if (!empty($paymentSource['attributes']['vault']['id'])) {
519 $vaultId = $paymentSource['attributes']['vault']['id'];
520 }
521
522 return [
523 'success' => $status === 'COMPLETED',
524 'status' => $status,
525 'capture_id' => $response['body']['purchase_units'][0]['payments']['captures'][0]['id'] ?? null,
526 'vault_id' => $vaultId,
527 ];
528 }
529
530 return [
531 'success' => false,
532 'status' => $orderResponse['body']['status'] ?? 'UNKNOWN',
533 ];
534 }
535
536 public function processRefund(string $transactionId, float $amount): array
537 {
538 $accessToken = $this->getAccessToken();
539 if (!$accessToken) {
540 return ['success' => false, 'error' => 'Authentication failed'];
541 }
542
543 $response = $this->makeRequest($this->getBaseUrl() . "/v2/payments/captures/{$transactionId}/refund", [
544 'method' => 'POST',
545 'headers' => [
546 'Authorization' => 'Bearer ' . $accessToken,
547 'Content-Type' => 'application/json',
548 ],
549 'body' => wp_json_encode([
550 'amount' => [
551 'value' => number_format($amount, 2, '.', ''),
552 'currency_code' => 'USD',
553 ],
554 ]),
555 ]);
556
557 return [
558 'success' => $response['success'],
559 'refund_id' => $response['body']['id'] ?? null,
560 ];
561 }
562
563 /**
564 * Handle PayPal webhook (supports both IPN for Simple mode and REST webhooks for Advanced mode)
565 */
566 public function handleWebhook(array $data): array
567 {
568 $body = $data['raw_body'] ?? '';
569 $postData = $data['post_data'] ?? [];
570
571 // Check if this is an IPN notification (Simple mode)
572 if (!empty($postData['txn_type']) || !empty($postData['payment_status'])) {
573 return $this->handleIPN($postData);
574 }
575
576 // Otherwise handle as REST API webhook (Advanced mode)
577 $event = json_decode($body, true);
578 $eventType = $event['event_type'] ?? '';
579
580 switch ($eventType) {
581 case 'PAYMENT.CAPTURE.COMPLETED':
582 do_action('yatra_paypal_payment_completed', $event['resource'] ?? []);
583 break;
584
585 case 'PAYMENT.CAPTURE.REFUNDED':
586 do_action('yatra_paypal_payment_refunded', $event['resource'] ?? []);
587 break;
588
589 case 'VAULT.PAYMENT-TOKEN.CREATED':
590 do_action('yatra_paypal_token_created', $event['resource'] ?? []);
591 break;
592 }
593
594 return ['success' => true, 'event_type' => $eventType];
595 }
596
597 /**
598 * Handle PayPal IPN (Instant Payment Notification) for Simple mode
599 */
600 private function handleIPN(array $ipnData): array
601 {
602 $this->log('PayPal IPN received', $ipnData);
603
604 // Verify IPN with PayPal
605 $isTestMode = \Yatra\Services\SettingsService::isPaymentTestMode();
606 $verifyUrl = $isTestMode
607 ? 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr'
608 : 'https://ipnpb.paypal.com/cgi-bin/webscr';
609
610 $verifyData = array_merge(['cmd' => '_notify-validate'], $ipnData);
611
612 $response = wp_remote_post($verifyUrl, [
613 'body' => $verifyData,
614 'timeout' => 60,
615 'httpversion' => '1.1',
616 ]);
617
618 if (is_wp_error($response)) {
619 $this->log('IPN verification failed', ['error' => $response->get_error_message()]);
620 return ['success' => false, 'error' => 'IPN verification failed'];
621 }
622
623 $responseBody = wp_remote_retrieve_body($response);
624
625 if ($responseBody !== 'VERIFIED') {
626 $this->log('IPN not verified', ['response' => $responseBody]);
627 return ['success' => false, 'error' => 'IPN not verified'];
628 }
629
630 // Process the payment
631 $paymentStatus = $ipnData['payment_status'] ?? '';
632 $customData = json_decode($ipnData['custom'] ?? '{}', true);
633 $bookingId = $customData['booking_id'] ?? 0;
634 $transactionId = $ipnData['txn_id'] ?? '';
635 $amount = (float) ($ipnData['mc_gross'] ?? 0);
636 $currency = $ipnData['mc_currency'] ?? 'USD';
637
638 if ($paymentStatus === 'Completed' && $bookingId > 0) {
639 // Fire action for payment completed
640 do_action('yatra_payment_completed', [
641 'booking_id' => $bookingId,
642 'transaction_id' => $transactionId,
643 'amount' => $amount,
644 'currency' => $currency,
645 'gateway' => 'paypal',
646 'mode' => 'simple',
647 ]);
648
649 $this->log('PayPal IPN payment completed', [
650 'booking_id' => $bookingId,
651 'transaction_id' => $transactionId,
652 'amount' => $amount,
653 ]);
654
655 return ['success' => true, 'status' => 'completed', 'booking_id' => $bookingId];
656 }
657
658 return ['success' => true, 'status' => $paymentStatus];
659 }
660
661 /**
662 * Check if this gateway should handle the return request
663 */
664 public function shouldHandleReturn(array $params): bool
665 {
666 return isset($params['paypal']) && $params['paypal'] === 'success';
667 }
668
669 /**
670 * Handle payment return from PayPal (when user is redirected back after payment)
671 * Works for both Simple and Advanced modes
672 */
673 public function handlePaymentReturn($booking, $bookingRepository): void
674 {
675 global $wpdb;
676
677 // Check if payment already processed
678 if ($booking->payment_status === 'paid') {
679 return;
680 }
681
682 $bookingId = (int) $booking->id;
683 $isAdvancedMode = !$this->isSimpleMode();
684
685 $this->log('Handling PayPal return', [
686 'booking_id' => $bookingId,
687 'mode' => $isAdvancedMode ? 'advanced' : 'simple',
688 ]);
689
690 if ($isAdvancedMode) {
691 // Advanced mode: Get token from URL and capture the order
692 $token = sanitize_text_field($_GET['token'] ?? '');
693
694 if (!empty($token)) {
695 $result = $this->verifyPayment($token);
696
697 if ($result['success'] && $result['status'] === 'COMPLETED') {
698 $this->completePayment($booking, $bookingRepository, $result['capture_id'] ?? $token);
699 }
700 }
701 } else {
702 // Simple mode: Payment verification happens via IPN
703 // For now, mark as pending verification - IPN will update it
704 // But we can show success to user since PayPal redirected them back
705 $this->log('Simple mode return - awaiting IPN verification', ['booking_id' => $bookingId]);
706 }
707 }
708
709 /**
710 * Complete the payment and update booking status
711 */
712 private function completePayment($booking, $bookingRepository, string $transactionId): void
713 {
714 global $wpdb;
715
716 $bookingId = (int) $booking->id;
717 $amountDue = (float) ($booking->amount_due ?? ($booking->total_amount - $booking->amount_paid));
718
719 // Update booking payment status
720 $bookings_table = BookingsTable::getTableName();
721 $wpdb->update(
722 $bookings_table,
723 [
724 'payment_status' => 'paid',
725 'amount_paid' => $booking->total_amount,
726 'amount_due' => 0,
727 'status' => 'confirmed',
728 'confirmed_at' => current_time('mysql'),
729 ],
730 ['id' => $bookingId],
731 ['%s', '%f', '%f', '%s', '%s'],
732 ['%d']
733 );
734
735 // Record the payment
736 $payments_table = BookingPaymentsTable::getTableName();
737 $wpdb->insert(
738 $payments_table,
739 [
740 'booking_id' => $bookingId,
741 'amount' => $amountDue,
742 'currency' => $booking->currency ?? 'USD',
743 'payment_gateway' => 'paypal',
744 'transaction_id' => $transactionId,
745 'status' => 'completed',
746 'created_at' => current_time('mysql'),
747 ],
748 ['%d', '%f', '%s', '%s', '%s', '%s', '%s']
749 );
750
751 $this->log('PayPal payment completed', [
752 'booking_id' => $bookingId,
753 'transaction_id' => $transactionId,
754 'amount' => $amountDue,
755 ]);
756
757 // Fire action for other plugins/services
758 do_action('yatra_payment_completed', [
759 'booking_id' => $bookingId,
760 'transaction_id' => $transactionId,
761 'amount' => $amountDue,
762 'currency' => $booking->currency ?? 'USD',
763 'gateway' => 'paypal',
764 ]);
765 }
766 }
767
768