PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.1
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Modules / PaymentMethods / PayPalGateway / PayPal.php

PayPal.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.4.1, at app/Modules/PaymentMethods/PayPalGateway/PayPal.php

753 lines 27.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\PaymentMethods\PayPalGateway;
4
5 use FluentCart\Api\CurrencySettings;
6 use FluentCart\Api\Orders;
7 use FluentCart\App\App;
8 use FluentCart\App\Helpers\CartCheckoutHelper;
9 use FluentCart\App\Helpers\CartHelper;
10 use FluentCart\App\Helpers\Helper;
11 use FluentCart\App\Helpers\Status;
12 use FluentCart\App\Hooks\Cart\WebCheckoutHandler;
13 use FluentCart\App\Models\OrderTransaction;
14 use FluentCart\App\Models\Subscription;
15 use FluentCart\App\Modules\PaymentMethods\Core\AbstractPaymentGateway;
16 use FluentCart\App\Modules\PaymentMethods\PayPalGateway\API\API;
17 use FluentCart\App\Modules\PaymentMethods\PayPalGateway\API\Webhook;
18 use FluentCart\App\Services\Payments\PaymentInstance;
19 use FluentCart\App\Vite;
20 use FluentCart\Framework\Support\Arr;
21
22 class PayPal extends AbstractPaymentGateway
23 {
24
25 private $methodSlug = 'paypal';
26
27 public array $supportedFeatures = ['payment', 'refund', 'webhook', 'custom_payment', 'card_update', 'switch_payment_method' => [
28 'supported_gateways' => ['stripe', 'paypal'],
29 ], 'dispute_handler', 'subscriptions'];
30
31
32 public function __construct()
33 {
34 parent::__construct(
35 new PayPalSettingsBase(),
36 new PayPalSubscriptions()
37 );
38
39 add_filter('fluent_cart/payment_methods_with_custom_checkout_buttons', function ($methods) {
40 $methods[] = 'paypal';
41 return $methods;
42 });
43 }
44
45 public function meta(): array
46 {
47 return [
48 'title' => 'PayPal',
49 'route' => 'paypal',
50 'slug' => 'paypal',
51 'label' => 'PayPal',
52 'description' => __('PayPal is the faster, safer way to send and receive money or make an online payment. Get started or create a merchant account to accept payments.', 'fluent-cart'),
53 'logo' => Vite::getAssetUrl("images/payment-methods/paypal-icon.svg"),
54 'icon' => Vite::getAssetUrl("images/payment-methods/paypal-icon.svg"),
55 'brand_color' => '#4f94d4',
56 'status' => $this->settings->get('is_active') === 'yes',
57 'upcoming' => false,
58 'supported_features' => $this->supportedFeatures
59 ];
60 }
61
62 public function boot()
63 {
64 (new IPN())->init();
65
66 add_action('wp_ajax_nopriv_fluent_cart_confirm_paypal_payment', [$this, 'confirmPayPalSinglePayment']);
67 add_action('wp_ajax_fluent_cart_confirm_paypal_payment', [$this, 'confirmPayPalSinglePayment']);
68
69 add_action('wp_ajax_nopriv_fluent_cart_confirm_paypal_subscription', [$this, 'confirmPayPalSubscription']);
70 add_action('wp_ajax_fluent_cart_confirm_paypal_subscription', [$this, 'confirmPayPalSubscription']);
71
72 add_filter('fluent_cart/payment_methods/paypal_client_id', [$this, 'getClientId'], 10, 2);
73
74 // add PayPal partner tags
75 add_filter('script_loader_tag', function ($tag, $handle) {
76 if ($handle === 'fluent-cart-checkout-sdk-paypal') {
77 $tag = str_replace(
78 '<script ',
79 '<script data-partner-attribution-id="FLUENTCART_SP_PPCP" ', $tag
80 );
81 }
82 return $tag;
83 }, 1, 2);
84
85 }
86
87 public function makePaymentFromPaymentInstance(PaymentInstance $paymentInstance)
88 {
89 if ($paymentInstance->subscription) {
90 return (new Processor())->handleSubscriptionPaymentFromPaymentInstance($paymentInstance, []);
91 }
92
93 return (new Processor())->handleSinglePayment($paymentInstance, []);
94 }
95
96 public function confirmPayPalSinglePayment()
97 {
98 if (empty(App::request()->get('payId')) || empty(App::request()->get('ref_id'))) {
99 wp_send_json([
100 'status' => 'failed',
101 'message' => __('No payId ID!', 'fluent-cart')
102 ], 422);
103 }
104
105 $payPalReferenceId = sanitize_text_field(App::request()->get('payId'));
106 $transactionHash = sanitize_text_field(App::request()->get('ref_id'));
107
108 $payment_intent = API::verifyPayment($payPalReferenceId);
109
110 if (is_wp_error($payment_intent)) {
111 wp_send_json([
112 'status' => 'failed',
113 'message' => $payment_intent->get_error_message(),
114 ], 422);
115 }
116
117 $transaction = null;
118
119 $intendedTransactionHash = Arr::get($payment_intent, 'purchase_units.0.reference_id', '');
120 if ($intendedTransactionHash) {
121 $transaction = OrderTransaction::query()
122 ->where('uuid', $intendedTransactionHash)
123 ->where('transaction_type', Status::TRANSACTION_TYPE_CHARGE)
124 ->first();
125 }
126
127 if (!$transaction) {
128 $transaction = OrderTransaction::query()
129 ->where('uuid', $transactionHash)
130 ->where('transaction_type', Status::TRANSACTION_TYPE_CHARGE)
131 ->first();
132 }
133
134 if (!$transaction) {
135 wp_send_json([
136 'status' => 'failed',
137 'message' => __('Transaction not found!', 'fluent-cart')
138 ], 423);
139 }
140
141 $isPaid = Arr::get($payment_intent, 'status') === 'COMPLETED' || Arr::get($payment_intent, 'status') === 'APPROVED';
142
143 if (!$isPaid) {
144 wp_send_json([
145 'status' => 'failed',
146 'message' => __('Payment not completed!', 'fluent-cart')
147 ], 422);
148 }
149
150 $paidAmount = 0;
151 $paidCurrency = '';
152 foreach (Arr::get($payment_intent, 'purchase_units', []) as $unit) {
153 $paidAmount += Helper::toCent(Arr::get($unit, 'amount.value', 0));
154 if (!$paidCurrency) {
155 $paidCurrency = strtoupper(Arr::get($unit, 'amount.currency_code', ''));
156 }
157 }
158
159 if ($paidAmount != $transaction->total) {
160 fluent_cart_warning_log(
161 __('PayPal Amount Mismatch Attempt', 'fluent-cart'),
162 sprintf(
163 /* translators: %1$s: expected amount, %2$s: received amount */
164 __('Payment amount mismatch detected. Expected: %1$s, Received: %2$s. This may indicate payment tampering.', 'fluent-cart'),
165 Helper::toDecimal($transaction->total),
166 Helper::toDecimal($paidAmount)
167 ),
168 [
169 'module_name' => 'order',
170 'module_id' => $transaction->order_id,
171 'log_type' => 'api'
172 ]
173 );
174 wp_send_json([
175 'status' => 'failed',
176 'message' => __('Paid amount does not match with transaction amount!', 'fluent-cart')
177 ], 422);
178 }
179
180 if ($paidCurrency && $transaction->currency && strtoupper($transaction->currency) !== $paidCurrency) {
181 fluent_cart_warning_log(
182 __('PayPal Currency Mismatch Attempt', 'fluent-cart'),
183 sprintf(
184 /* translators: %1$s: expected currency, %2$s: received currency */
185 __('Payment currency mismatch detected. Expected: %1$s, Received: %2$s. This may indicate payment tampering.', 'fluent-cart'),
186 $transaction->currency,
187 $paidCurrency
188 ),
189 [
190 'module_name' => 'order',
191 'module_id' => $transaction->order_id,
192 'log_type' => 'api'
193 ]
194 );
195 wp_send_json([
196 'status' => 'failed',
197 'message' => __('Payment currency does not match with transaction currency!', 'fluent-cart')
198 ], 422);
199 }
200
201 $chargeId = Arr::get($payment_intent, 'purchase_units.0.payments.captures.0.id', '');
202
203 // All Verified! Let's update the transaction and order
204 (new Processor())->confirmPaymentSuccessByCharge($transaction, [
205 'vendor_charge_id' => $chargeId,
206 'status' => Status::TRANSACTION_SUCCEEDED,
207 'total' => $paidAmount,
208 'payment_method_type' => 'PayPal',
209 'meta' => [
210 'payer' => Arr::get($payment_intent, 'payer', [])
211 ],
212 'payment_source' => Arr::get($payment_intent, 'payment_source', []),
213 ]);
214
215
216 wp_send_json([
217 'status' => 'success',
218 'redirect_url' => $transaction->getReceiptPageUrl(true),
219 'order' => [
220 'uuid' => $transaction->order->uuid
221 ],
222 'message' => __('Payment has been paid successfully! Redirecting...', 'fluent-cart')
223 ]);
224 }
225
226 public function confirmPayPalSubscription()
227 {
228 if (empty(App::request()->get('subscription_id')) || empty(App::request()->get('ref_id'))) {
229 wp_send_json([
230 'status' => 'failed',
231 'message' => __('No Subscription ID!', 'fluent-cart')
232 ], 423);
233 }
234
235 $subscriptionId = sanitize_text_field(App::request()->get('subscription_id'));
236
237 $paypalSubscription = API::getResource('billing/subscriptions/' . $subscriptionId);
238
239 if (is_wp_error($paypalSubscription)) {
240 wp_send_json([
241 'message' => $paypalSubscription->get_error_message(),
242 'status' => 'failed',
243 ], 422);
244 }
245
246
247 $status = Arr::get($paypalSubscription, 'status', '');
248
249 if ($status != 'ACTIVE') {
250 wp_send_json([
251 'status' => 'failed',
252 'message' => __('Subscription is not active', 'fluent-cart')
253 ], 423);
254 }
255
256 $transaction = OrderTransaction::query()->where('uuid', sanitize_text_field(App::request()->get('ref_id')))->first();
257
258 if (!$transaction) {
259 wp_send_json([
260 'status' => 'failed',
261 'message' => __('Transaction not found!', 'fluent-cart')
262 ], 404);
263 }
264
265 // Verify the PayPal subscription's plan matches the expected plan
266 $localSubscription = Subscription::query()->where('id', $transaction->subscription_id)->first();
267
268 if ($localSubscription && $localSubscription->vendor_plan_id) {
269 $paypalPlanId = Arr::get($paypalSubscription, 'plan_id', '');
270 if ($paypalPlanId && $paypalPlanId !== $localSubscription->vendor_plan_id) {
271 fluent_cart_add_log(
272 'PayPal Subscription Plan Mismatch',
273 'The PayPal subscription plan ID does not match the expected plan ID for this subscription. This may indicate a configuration issue or potential tampering.',
274 [
275 'module_name' => 'subscription',
276 'module_id' => $localSubscription->id,
277 'log_type' => 'api'
278 ]
279 );
280
281 wp_send_json([
282 'status' => 'failed',
283 'message' => __('PayPal subscription plan does not match the expected plan.', 'fluent-cart')
284 ], 422);
285 }
286 }
287
288 $subscriptionModel = (new Processor())->activateSubscription($paypalSubscription, $transaction);
289
290 if (!$subscriptionModel || !in_array($subscriptionModel->status, [Status::SUBSCRIPTION_ACTIVE, Status::SUBSCRIPTION_TRIALING], true)) {
291 wp_send_json([
292 'status' => 'failed',
293 'message' => __('Subscription activation failed.', 'fluent-cart')
294 ], 422);
295 }
296
297 wp_send_json([
298 'status' => 'success',
299 'message' => __('Subscription has been activated successfully!', 'fluent-cart'),
300 'redirect_url' => $transaction->getReceiptPageUrl(true),
301 'order' => [
302 'uuid' => $transaction->order->uuid
303 ],
304 ], 200);
305 }
306
307 public function getClientId($value, $args)
308 {
309 return $this->settings->getPublicKey();
310 }
311
312 public function handleIPN()
313 {
314 if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] != 'POST') {
315 return;
316 }
317
318 (new IPN())->processWebhook();
319 exit(200);
320 }
321
322 public function getTransactionUrl($url, $data)
323 {
324 if (Arr::get($data, 'payment_mode') === 'test') {
325 return 'https://www.sandbox.paypal.com/activity/payment/' . Arr::get($data, 'vendor_charge_id');
326 }
327
328 return 'https://www.paypal.com/activity/payment/' . Arr::get($data, 'vendor_charge_id');
329 }
330
331 public function appAuthenticator($request)
332 {
333 ConnectConfig::parseConnectInfos($request);
334 }
335
336 public function getSubscriptionUrl($url, $data)
337 {
338 if (Arr::get($data, 'payment_mode') === 'test') {
339 return 'https://www.sandbox.paypal.com/billing/subscriptions/' . Arr::get($data, 'vendor_subscription_id');
340 }
341
342 return 'https://www.paypal.com/billing/subscriptions' . Arr::get($data, 'vendor_subscription_id');
343 }
344
345 public static function beforeSettingsUpdate($data, $oldSettings): array
346 {
347 if (Arr::get($data, 'payment_mode') === 'live') {
348 $data['live_client_secret'] = Helper::encryptKey($data['live_client_secret']);
349 } else {
350 $data['test_client_secret'] = Helper::encryptKey($data['test_client_secret']);
351 }
352
353 if (isset($data['define_test_keys'])) {
354 unset($data['define_test_keys']);
355 }
356 if (isset($data['define_live_keys'])) {
357 unset($data['define_live_keys']);
358 }
359 //clean existing access token if exist, fix for: api key change authentication error
360 fluent_cart_update_option('_paypal_access_token_' . Arr::get($data, 'payment_mode'), []);
361
362 return $data;
363 }
364
365 public function isEnabled(): bool
366 {
367 return $this->settings->isActive();
368 }
369
370 /**
371 * Connect configuration should return
372 */
373 public function getConnectInfo()
374 {
375 return ConnectConfig::getConnectConfig();
376 }
377
378 public function disconnect($data)
379 {
380 return ConnectConfig::disconnect($data);
381 }
382
383 public function getWebhookInfo($mode = 'test')
384 {
385 $webhookId = $this->settings->get($mode . '_webhook_id');
386 $webhookEvents = $this->settings->get($mode . '_webhook_events');
387
388 if (!$webhookId || !$webhookEvents) {
389 return false;
390 }
391
392 /**
393 * return string
394 * webhook url also in code formatted and add copy button
395 * webhook id
396 * webhook events (list of events, and every list item should be code formatted), if not empty
397 * $webhookUrl = home_url('/wp-json/fluent-cart/v2/webhook?fct_payment_listener=1&method=paypal')
398 */
399
400 $webhookInfo = '';
401 if ($webhookId) {
402 $webhookInfo .= '<p><b>' . __('Webhook (No further setup needed) :', 'fluent-cart') . '</b><span style="color:green;">Your webhook <code class="copyable-content">' . $webhookId . '</code> is connected!</span> </p>';
403 }
404 if ($webhookEvents) {
405 $webhookInfo .= '<p>' . __('and now watching for Webhook Events listed bellow:', 'fluent-cart') . '</p><p style="word-wrap: break-word;
406 font-size: 12px;" class="copyable-content">';
407 foreach ($webhookEvents as $event) {
408 $webhookInfo .= $event['name'] . ' | ';
409 }
410 $webhookInfo .= '</p>';
411 }
412
413 return $webhookInfo;
414 }
415
416 public function fields()
417 {
418 $testSchema = [
419 'webhook_instruction' => [
420 'value' => Webhook::webhookInstruction(),
421 'label' => __('Webhook Setup', 'fluent-cart'),
422 'type' => 'html_attr'
423 ],
424 'test_webhook_id' => [
425 'value' => '',
426 'placeholder' => 'Webhook ID',
427 'required' => true,
428 'label' => __('Test Webhook ID (Copy the webhook id and paste bellow)', 'fluent-cart'),
429 'type' => 'text'
430 ],
431 ];
432
433 $liveSchema = [
434 'webhook_instruction' => [
435 'value' => Webhook::webhookInstruction(),
436 'label' => __('Webhook Setup', 'fluent-cart'),
437 'type' => 'html_attr'
438 ],
439 'live_webhook_id' => [
440 'value' => '',
441 'placeholder' => 'Webhook ID',
442 'required' => true,
443 'label' => __('Live Webhook ID (Copy the webhook id and paste bellow)', 'fluent-cart'),
444 'type' => 'text'
445 ],
446 ];
447
448 // if not defined property then no need to show webhook instruction
449 if ($this->settings->getProviderType() !== 'api_keys') {
450 $testSchema = [];
451 $liveSchema = [];
452 }
453
454 $payPalFields = array(
455 'notice' => [
456 'value' => $this->renderStoreModeNotice(),
457 'label' => __('PayPal', 'fluent-cart'),
458 'type' => 'notice'
459 ],
460 'payment_mode' => [
461 'type' => 'tabs',
462 'schema' => [
463 [
464 'type' => 'tab',
465 'label' => __('Live credentials', 'fluent-cart'),
466 'value' => 'live',
467 'schema' => $liveSchema
468 ],
469 [
470 'type' => 'tab',
471 'label' => __('Test credentials', 'fluent-cart'),
472 'value' => 'test',
473 'schema' => $testSchema
474 ]
475 ]
476 ],
477 'provider' => array(
478 'value' => $this->settings->getProviderType(),
479 'label' => __('Provider', 'fluent-cart'),
480 'type' => 'provider'
481 ),
482 'webhook_info_test' => array(
483 'info' => $this->getWebhookInfo('test'),
484 'label' => __('Webhook Info', 'fluent-cart'),
485 'type' => 'webhook_info',
486 'mode' => 'test'
487 ),
488 'webhook_info_live' => array(
489 'info' => $this->getWebhookInfo('live'),
490 'label' => __('Webhook Info', 'fluent-cart'),
491 'type' => 'webhook_info',
492 'mode' => 'live'
493 ),
494 'is_pro_item' => array(
495 'value' => 'no',
496 'label' => __('PayPal', 'fluent-cart'),
497 'type' => 'validate'
498 ),
499 );
500
501 return $payPalFields;
502 }
503
504 public function webHookPaymentMethodName()
505 {
506 return $this->methodSlug;
507 }
508
509 public static function validateSettings($data): array
510 {
511 $mode = Arr::get($data, 'payment_mode', 'test');
512 $provider = Arr::get($data, 'provider', 'connect');
513
514 if ($provider === 'api_keys') {
515 if ($mode === 'live') {
516 $clientId = defined('FCT_PAYPAL_LIVE_PUBLIC_KEY') ? FCT_PAYPAL_LIVE_PUBLIC_KEY : Arr::get($data, 'live_client_id');
517 $clientSecret = defined('FCT_PAYPAL_LIVE_SECRET_KEY') ? FCT_PAYPAL_LIVE_SECRET_KEY : Arr::get($data, 'live_client_secret');
518 } else {
519 $clientId = defined('FCT_PAYPAL_TEST_PUBLIC_KEY') ? FCT_PAYPAL_TEST_PUBLIC_KEY : Arr::get($data, 'test_client_id');
520 $clientSecret = defined('FCT_PAYPAL_TEST_SECRET_KEY') ? FCT_PAYPAL_TEST_SECRET_KEY : Arr::get($data, 'test_client_secret');
521 }
522
523 return static::validateApiCredentials($clientId, $clientSecret, $mode);
524
525 }
526
527 $clientId = Arr::get($data, "{$mode}_client_id");
528 $clientSecret = Arr::get($data, "{$mode}_client_secret");
529
530 if (!$clientId || !$clientSecret) {
531 return [
532 'status' => 'failed',
533 'message' => $mode === 'live' ? __('PayPal live credentials are required!', 'fluent-cart') : __('PayPal test credentials are required!', 'fluent-cart'),
534 ];
535 }
536
537 return [
538 'status' => 'success',
539 'message' => __('Credentials are valid!', 'fluent-cart')
540 ];
541
542 }
543
544 private static function validateApiCredentials($clientId, $clientSecret, $mode): array
545 {
546 $result = API::validateCredentials($clientId, $clientSecret, $mode);
547
548 if (is_wp_error($result)) {
549 return [
550 'status' => 'failed',
551 'message' => $result->get_error_message()
552 ];
553 }
554
555 return [
556 'status' => 'success',
557 'message' => __('Credentials are valid!', 'fluent-cart')
558 ];
559
560 }
561
562 /*
563 * Default sdk enqueue version is the plugin version
564 * if any sdk require a specific version, then override this method
565 * or to remove a version, return null
566 */
567 public function getEnqueueVersion()
568 {
569 return null;
570 }
571
572 public function getEnqueueScriptSrc($hasSubscription = 'no'): array
573 {
574 if ($this->settings->get('checkout_mode') !== 'paypal_pro') {
575 return [];
576 }
577
578 $clientId = $this->settings->getPublicKey();
579 $clientId = sanitize_text_field($clientId);
580
581 $sdkSrc = 'https://www.paypal.com/sdk/js?client-id=' . $clientId;
582
583 if ('yes' == $hasSubscription) {
584 $sdkSrc = add_query_arg(array('vault' => 'true', 'intent' => 'subscription'), $sdkSrc);
585 } else {
586 $sdkSrc = add_query_arg(array('currency' => strtoupper(CurrencySettings::get('currency')), 'intent' => 'capture'), $sdkSrc);
587 }
588 $sdkSrc = apply_filters('fluent_cart/payments/paypal_sdk_src', $sdkSrc, []);
589
590 return [
591 [
592 'handle' => 'fluent-cart-checkout-sdk-paypal',
593 'src' => $sdkSrc,
594 ],
595 [
596 'handle' => 'fluent-cart-checkout-handler-paypal',
597 'src' => Vite::getEnqueuePath('public/payment-methods/paypal-checkout.js'),
598 'deps' => ['fluent-cart-checkout-sdk-paypal']
599 ]
600 ];
601 }
602
603 public function getLocalizeData(): array
604 {
605 return [
606 'fct_paypal_data' => [
607 'translations' => [
608 'uuid not found' => __('uuid not found', 'fluent-cart'),
609 'Choose any option to continue' => __('Choose any option to continue', 'fluent-cart'),
610 'An unknown error occurred' => __('An unknown error occurred', 'fluent-cart'),
611 'An error occurred while loading PayPal.' => __('An error occurred while loading PayPal.', 'fluent-cart'),
612 'Loading Payment Processor...' => __('Loading Payment Processor...', 'fluent-cart'),
613 'Order creation failed' => __('Order creation failed', 'fluent-cart'),
614 'Not proper order handler' => __('Not proper order handler', 'fluent-cart'),
615 'No Subscription ID' => __('No Subscription ID', 'fluent-cart'),
616 'no processing' => __('no processing', 'fluent-cart'),
617 'not proper order handler' => __('not proper order handler', 'fluent-cart'),
618 'Payment confirmation failed' => __('Payment confirmation failed', 'fluent-cart'),
619 ]
620 ]
621 ];
622 }
623
624 public function processRefund($transaction, $amount, $args)
625 {
626 if (!$amount) {
627 return new \WP_Error(
628 'fluent_cart_stripe_refund_error',
629 __('Refund amount is required.', 'fluent-cart')
630 );
631 }
632
633 return PayPalHelper::processRemoteRefund($transaction, $amount, $args);
634 }
635
636 public function getOrderInfo($data)
637 {
638 $cart = CartHelper::getCart();
639 $checkOutHelper = CartCheckoutHelper::make();
640 $shippingChargeData = (new WebCheckoutHandler())->getShippingChargeData($cart);
641 $shippingCharge = Arr::get($shippingChargeData, 'charge');
642 $totalPrice = $checkOutHelper->getItemsAmountTotal(false) + $shippingCharge;
643
644 $tax = $checkOutHelper->getCart()->checkout_data['tax_data'] ?? [];
645 $taxBehavior = (int) Arr::get($tax, 'tax_behavior', 0);
646 $storeTaxBehavior = (int) Arr::get($tax, 'store_tax_behavior', $taxBehavior);
647
648 if ($taxBehavior === 1) {
649 // Pure exclusive — add all tax including fee tax (tax_total contains both).
650 $totalPrice = $totalPrice + (int) Arr::get($tax, 'tax_total', 0)
651 + (int) Arr::get($tax, 'shipping_tax', 0);
652 } elseif ($taxBehavior === 3) {
653 // Mixed — add only exclusive product tax + fee/shipping if store is exclusive.
654 $totalPrice = $totalPrice + (int) Arr::get($tax, 'exclusive_tax_total', 0);
655 if ($storeTaxBehavior === 1) {
656 $totalPrice = $totalPrice + (int) Arr::get($tax, 'fee_tax', 0)
657 + (int) Arr::get($tax, 'shipping_tax', 0);
658 }
659 }
660
661 $items = $checkOutHelper->getItems();
662 $hasSubscription = $this->validateSubscriptions($items);
663
664 $clientId = $this->settings->getPublicKey();
665
666 if (empty($clientId)) {
667 $message = __('Please provide a valid Client Id!', 'fluent-cart');
668 fluent_cart_add_log('PayPal Credential Validation', $message, 'error', ['log_type' => 'payment']);
669 wp_send_json([
670 'status' => 'failed',
671 'message' => __('No valid Client ID found!', 'fluent-cart')
672 ], 422);
673 }
674
675 $paymentArgs['public_key'] = $clientId;
676
677 $paymentDetails = [
678 'mode' => 'payment',
679 'amount' => Helper::toDecimalWithoutComma($totalPrice),
680 'currency' => strtoupper(CurrencySettings::get('currency')),
681 ];
682
683 if ($hasSubscription) {
684 $paymentDetails['mode'] = 'subscription';
685 }
686
687 $this->checkCurrencySupport();
688
689 wp_send_json(
690 [
691 'data' => [],
692 'payment_args' => $paymentArgs,
693 'message' => __('Order info retrieved!', 'fluent-cart'),
694 'intent' => $paymentDetails,
695 ],
696 200
697 );
698
699 }
700
701 public function checkCurrencySupport()
702 {
703 $currency = CurrencySettings::get('currency');
704
705 if (!in_array(strtoupper($currency), self::getPaypalSupportedCurrency())) {
706 wp_send_json([
707 'status' => 'failed',
708 'message' => __('PayPal does not support the currency you are using!', 'fluent-cart')
709 ], 422);
710 }
711 }
712
713 public function isCurrencySupported(): bool
714 {
715 $currency = CurrencySettings::get('currency');
716 return in_array(strtoupper($currency), self::getPaypalSupportedCurrency());
717 }
718
719 public static function getPaypalSupportedCurrency(): array
720 {
721 return [
722 'USD', 'EUR', 'GBP', 'AUD', 'CAD', 'JPY', 'NZD', 'CHF', 'HKD', 'SGD', 'SEK', 'DKK', 'PLN', 'NOK', 'HUF', 'CZK', 'ILS', 'MXN', 'MYR', 'BRL', 'PHP', 'TWD', 'THB'
723 ];
724 }
725
726 public function acceptRemoteDispute($transaction, $args = [])
727 {
728 $disputeId = Arr::get($transaction->meta, 'dispute_id');
729 $dispute = (new API())->getResource('customer/disputes/'. $disputeId);
730
731 if (!$disputeId) {
732 $dispute = (new API())->getResource('customer/disputes/'. $disputeId);
733
734 if (is_wp_error($dispute) || empty($dispute['dispute_id'])) {
735 new \WP_Error('No dispute ID found!', __('Please check PayPal if the dispute is already accepted or not!', 'fluent-cart'));
736 }
737
738 $disputeId = Arr::get($dispute, 'dispute_id');
739 }
740
741 $note = Arr::get($args, 'dispute_note', 'Accepted full dispute claim!');
742
743 $closeDispute = (new API())->createResource('customer/disputes/' . $disputeId . '/accept-claim', ['note' => $note]);
744
745 if (is_wp_error($closeDispute)) {
746 return $closeDispute;
747 }
748
749 return $closeDispute;
750 }
751
752 }
753