# fluent-cart/1.5.0/app/Modules/PaymentMethods/PayPalGateway/Processor.php

FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler, version 1.5.0. 453 lines.

- Page: https://pluginprobe.com/plugins/fluent-cart/1.5.0/code/app/Modules/PaymentMethods/PayPalGateway/Processor.php
- Raw: https://pluginprobe.com/plugins/fluent-cart/1.5.0/raw/app/Modules/PaymentMethods/PayPalGateway/Processor.php
- Modified: 2026-06-11T12:21:58+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/fluent-cart/1.5.0/code/app/Modules/PaymentMethods/PayPalGateway/Processor.php#L10-L20`.

```php
<?php

namespace FluentCart\App\Modules\PaymentMethods\PayPalGateway;

use FluentCart\App\Events\Subscription\SubscriptionActivated;
use FluentCart\App\Helpers\Helper;
use FluentCart\App\Helpers\Status;
use FluentCart\App\Modules\PaymentMethods\PayPalGateway\API\API;
use FluentCart\App\Helpers\StatusHelper;
use FluentCart\App\Models\Order;
use FluentCart\App\Models\OrderTransaction;
use FluentCart\App\Models\Subscription;
use FluentCart\App\Modules\Subscriptions\Services\SubscriptionService;
use FluentCart\App\Services\DateTime\DateTime;
use FluentCart\App\Services\Payments\PaymentHelper;
use FluentCart\App\Services\Payments\PaymentInstance;
use FluentCart\Framework\Support\Arr;

class Processor
{
    public function handleSinglePayment(PaymentInstance $paymentInstance, $args = [])
    {
        $transaction = $paymentInstance->transaction;
        $order = $paymentInstance->order;

        $itemsSubTotal = 0;
        $formattedItems = [];

        foreach ($order->order_items as $item) {
            $quantity = $item->quantity ?? 1;
            $perQuantity = $this->toDecimal($item->line_total / $quantity);
            $title = $item->post_title . ' ' . $item->title;

            $formattedItems[] = [
                'name'        => strlen($title) > 127 ? substr($title, 0, 120) . '...' : $title,
                'description' => strlen($title) > 4000 ? substr($title, 0, 3997) . '...' : $title,
                'unit_amount' => [
                    'currency_code' => $transaction->currency,
                    'value'         => $perQuantity,
                ],
                'quantity'    => $quantity,
            ];

            $itemsSubTotal += $perQuantity * $quantity;
        }

        $chargingAmount = $this->toDecimal($transaction->total);
        $pushedTotal = $itemsSubTotal;


        // Learn more at: https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit
        $purchaseUnits = [
            'reference_id' => $transaction->uuid, // This is the order UUID
            'amount'       => [ // https://developer.paypal.com/docs/api/orders/v2/#definition-amount_breakdown
                'currency_code' => $transaction->currency,
                'value'         => $chargingAmount,
                'breakdown'     => [
                    'item_total' => [
                        'currency_code' => $transaction->currency,
                        'value'         => number_format($itemsSubTotal, 2, '.', ''),
                    ]
                ]
            ],
            'items'        => $formattedItems
        ];

        // if there is no defined credential for specific mode,
        // then add merchantId as it's a partner app connection
        $payPalSettings = new PayPalSettingsBase();
        if ($merchantId = $payPalSettings->getMerchantId()) {
            if ($payPalSettings->getProviderType() === 'api_keys') {
                $purchaseUnits['payee'] = [
                    "merchant_id" => $merchantId
                ];
            }
        }

        if ($order->shipping_total > 0) {
            $shippingAmount = $this->toDecimal($order->shipping_total);
            $purchaseUnits['amount']['breakdown']['shipping'] = [
                'currency_code' => $transaction->currency,
                'value'         => $shippingAmount,
            ];
            $pushedTotal += $shippingAmount;
        }



        $taxBehavior       = (int) $order->tax_behavior;
        $exclusiveTaxTotal = (int) $order->getMeta('exclusive_tax_total');
        $storeTaxBehavior  = (int) $order->getMeta('store_tax_behavior');
        $feeTax            = (int) $order->getMeta('fee_tax');

        // Fallback: if meta missing (old order), use tax_behavior as store_tax_behavior
        if (empty($storeTaxBehavior) && $taxBehavior > 0) {
            $storeTaxBehavior = $taxBehavior;
        }

        if ($taxBehavior === 1) {
            // Pure exclusive: all tax is additive on top of item prices.
            // tax_total includes product + fee tax (both exclusive).
            $taxTotal = $this->toDecimal($order->tax_total) + $this->toDecimal($order->shipping_tax);
        } elseif ($taxBehavior === 3) {
            // Mixed: only exclusive product + fee tax is additive; shipping conditional.
            $taxTotal = $this->toDecimal($exclusiveTaxTotal);
            if ($storeTaxBehavior === 1) {
                // Store is exclusive: fees and shipping are also exclusive.
                $taxTotal += $this->toDecimal($order->shipping_tax);
                $taxTotal += $this->toDecimal($feeTax);
            }
        } else {
            $taxTotal = 0;
        }

        if ($taxTotal > 0) {
            $purchaseUnits['amount']['breakdown']['tax_total'] = [
                'currency_code' => $transaction->currency,
                'value'         => number_format($taxTotal, 2, '.', ''),
            ];
            $pushedTotal += $taxTotal;
        }

        if ($chargingAmount < $pushedTotal) {
            $discount = $pushedTotal - $chargingAmount;
            $purchaseUnits['amount']['breakdown']['discount'] = [
                'currency_code' => $transaction->currency,
                'value'         => number_format($discount, 2, '.', ''),
            ];
        } else if ($chargingAmount > $pushedTotal) {
            $extraChargeNeedToBeAdded = $chargingAmount - $pushedTotal;
            $formattedItems[] = [
                'name'        => __('Adjustment Amount', 'fluent-cart'),
                'unit_amount' => [
                    'currency_code' => $transaction->currency,
                    'value'         => number_format($extraChargeNeedToBeAdded, 2, '.', ''),
                ],
                'quantity'    => 1,
            ];

            $purchaseUnits['items'] = $formattedItems;

            //now the total amount need to be adjusted with item total value
            $adjustedItemTotal = $itemsSubTotal + $extraChargeNeedToBeAdded;
            $purchaseUnits['amount']['breakdown']['item_total']['value'] = number_format($adjustedItemTotal, 2, '.', '');
        }

        $paypalOrder = API::createOrder($purchaseUnits);

        if (is_wp_error($paypalOrder)) {
            return $paypalOrder;
        }

        $paypalOrderId = Arr::get($paypalOrder, 'id');

        $transaction->update([
            'meta' => array_merge($transaction->meta ?? [], ['paypal_order_id' => $paypalOrderId])
        ]);

        return [
            'nextAction'         => 'paypal',
            'actionName'         => 'custom',
            'status'             => 'success',
            'data'               => [
                'order'       => [
                    'uuid' => $order->uuid,
                ],
                'transaction' => [
                    'uuid' => $transaction->uuid,
                ]
            ],
            'message'            => __('Order has been placed successfully', 'fluent-cart'),
            'custom_payment_url' => PaymentHelper::getCustomPaymentLink($order->uuid),
            'response'           => [
                'paypalOrderId' => $paypalOrderId,
            ]
        ];
    }

    public function handleSubscriptionPaymentFromPaymentInstance(PaymentInstance $paymentInstance, $args = [])
    {
        $orderType = $paymentInstance->order->type;
        $subscription = $paymentInstance->subscription;
        $feeTotal = $orderType !== 'renewal' ? (int)$paymentInstance->order->fee_total : 0;
        $initialAmount = (int)$subscription->signup_fee + $paymentInstance->getExtraAddonAmount() + $feeTotal;
        $status = Status::SUBSCRIPTION_INTENDED;

        if ($orderType == 'renewal') {
            $requiredBillTimes = $subscription->getRequiredBillTimes();

            if ($requiredBillTimes === -1) {
                return new \WP_Error('already_completed', __('Invalid bill times for the subscription.', 'fluent-cart'));
            }

            $data = [
                'order_id'         => $subscription->parent_order_id,
                'product_id'       => $subscription->product_id,
                'variation_id'     => $subscription->variation_id,
                'trial_days'       => $subscription->getReactivationTrialDays(), // trial days for reactivation
                'billing_interval' => $subscription->billing_interval,
                'currency'         => $paymentInstance->order->currency,
                'interval_count'   => 1, // 1
                'recurring_amount' => $subscription->getCurrentRenewalAmount(), // default recurring total in cents
                'signup_fee'       => 0, // default setup fee in cents ($0.00)
                'bill_times'       => $requiredBillTimes, // 0 for unlimited
            ];
            $status = $subscription->status;
        } else {
            $data = [
                'order_id'         => $subscription->parent_order_id,
                'product_id'       => $subscription->product_id,
                'variation_id'     => $subscription->variation_id,
                'trial_days'       => $subscription->trial_days,
                'billing_interval' => $subscription->billing_interval,
                'currency'         => $paymentInstance->order->currency,
                'interval_count'   => 1, // 1
                'recurring_amount' => $subscription->recurring_total, // default recurring total in cents
                'signup_fee'       => $initialAmount, // default setup fee in cents ($0.00)
                'bill_times'       => (int)$subscription->bill_times, // 0 for unlimited
            ];

        }

        $paypalPlan = PayPalHelper::getPayPalPlan($data);

        if (is_wp_error($paypalPlan)) {
            return $paypalPlan;
        }

        $subscription->update([
            'status'          => $status,
            'vendor_plan_id'  => Arr::get($paypalPlan, 'id'),
            'vendor_response' => json_encode($paypalPlan, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
        ]);

        return [
            'status'     => 'success',
            'nextAction' => 'paypal',
            'actionName' => 'custom',
            'message'    => __('Order has been placed successfully', 'fluent-cart'),
            'data'       => [
                'order'        => [
                    'uuid' => $paymentInstance->order->uuid,
                ],
                'transaction'  => [
                    'uuid' => $paymentInstance->transaction->uuid,
                ],
                'subscription' => [
                    'uuid' => $subscription->uuid,
                ]
            ],
            'response'   => [
                'planId' => Arr::get($paypalPlan, 'id')
            ]
        ];
    }

    /**
     * Confirm payment success
     * Currently used by:
     * @param OrderTransaction $transaction
     * @param array $args
     * @param array $transactionArgs
     *      string vendor_charge_id - The intent_id from paypal
     *      string total - The amount charged in cents
     *      string status - The status of the transaction ('succeeded', 'pending', etc.))
     *      array payer - The payer information from PayPal.
     *      array payment_source - The payment source information from PayPal.
     *
     * @param string $args ['intent_id'] - The intent ID from Stripe.
     * @return Order
     */
    public function confirmPaymentSuccessByCharge(OrderTransaction $transaction, $transactionArgs = [])
    {
        $transactionUpdateData = array_filter([
            'vendor_charge_id'    => Arr::get($transactionArgs, 'vendor_charge_id', ''),
            'payment_method'      => 'paypal',
            'status'              => Arr::get($transactionArgs, 'status', Status::TRANSACTION_SUCCEEDED),
            'total'               => (int)Arr::get($transactionArgs, 'total', 0),
            // payment_method_type: this is the intent ID. We may need that later In case we don't have the vendor_charge_id
            'payment_method_type' => Arr::get($transactionArgs, 'payment_method_type', ''),
        ]);

        $order = Order::query()->where('id', $transaction->order_id)->first();
        // in race conditions between webhook and AJAX confirmation
        $transaction = OrderTransaction::query()->where('id', $transaction->id)->first();
        if ($transaction->status === Status::TRANSACTION_SUCCEEDED || $transactionUpdateData['status'] !== Status::TRANSACTION_SUCCEEDED) {
            if (!$transaction->vendor_charge_id && !empty($transactionUpdateData['vendor_charge_id'])) {
                $transaction->update(['vendor_charge_id' => $transactionUpdateData['vendor_charge_id']]);
            }
            return $order; // already confirmed or not needed to confirm
        }

        // handle payment source
        $cardData = Arr::get($transactionArgs, 'payment_source.card', []);
        if ($cardData) {
            $transactionUpdateData['card_last_4'] = strlen(Arr::get($cardData, 'last_digits')) > 4 ? substr(Arr::get($cardData, 'last_digits'), -4) : Arr::get($cardData, 'last_digits');
            $transactionUpdateData['card_brand'] = Arr::get($cardData, 'brand');
        }

        $transactionUpdateData['meta'] = array_merge($transaction->meta ?? [], Arr::get($transactionArgs, 'meta', []));

        $transaction->fill($transactionUpdateData);
        $transaction->save();

        fluent_cart_add_log(__('PayPal Payment Confirmation', 'fluent-cart'), __('Payment confirmation received from PayPal. Transaction ID: ', 'fluent-cart') . Arr::get($transactionArgs, 'vendor_charge_id', ''), 'info', [
            'module_name' => 'order',
            'module_id'   => $order->id,
        ]);

        // Maybe we have to save the billing details

        // We are assuming. This is only for one time payment. No subscription or renewal will be here!

        return (new StatusHelper($order))->syncOrderStatuses($transaction);
    }


    // This should be only used from the ajax call for the very first time subscription activation
    public function activateSubscription($paypalSubscription, OrderTransaction $transaction, $subscriptionModel = null)
    {
        $order = $transaction->order;

        if (!$subscriptionModel) {
            $subscriptionModel = Subscription::query()->where('id', $transaction->subscription_id)->first();
        }

        if (!$subscriptionModel || $subscriptionModel->status === Status::SUBSCRIPTION_ACTIVE) {
            return $subscriptionModel; // already active or invalid
        }

        // Verify the PayPal subscription's plan matches the expected plan
        if ($subscriptionModel->vendor_plan_id) {
            $paypalPlanId = Arr::get($paypalSubscription, 'plan_id', '');
            if ($paypalPlanId && $paypalPlanId !== $subscriptionModel->vendor_plan_id) {
                fluent_cart_add_log(
                    __('PayPal Subscription Plan Mismatch', 'fluent-cart'),
                    sprintf(
                        /* translators: %1$s: expected plan ID, %2$s: received plan ID */
                        __('PayPal subscription plan mismatch. Expected: %1$s, Received: %2$s. Subscription not activated.', 'fluent-cart'),
                        $subscriptionModel->vendor_plan_id,
                        $paypalPlanId
                    ),
                    'error',
                    [
                        'module_name' => 'order',
                        'module_id'   => $order->id,
                        'log_type'    => 'api'
                    ]
                );
                return $subscriptionModel; // Do not activate
            }
        }

        $nextBillingDate = Arr::get($paypalSubscription, 'billing_info.next_billing_time') ?? null;
        if ($nextBillingDate) {
            $nextBillingDate = gmdate('Y-m-d H:i:s', strtotime($nextBillingDate));
        } else {
            // calculate the next billing date, as PayPal has not been charged yet
            $billingIntervalDays = PaymentHelper::getIntervalDays($subscriptionModel->billing_interval) + (int) $subscriptionModel->trial_days;
            $nextBillingDate = DateTime::gmtNow()->addDays($billingIntervalDays)->format('Y-m-d H:i:s');
        }

        $subscriptionUpdateData = array_filter([
            'next_billing_date'      => $nextBillingDate,
            'status'                 => Status::SUBSCRIPTION_ACTIVE,
            'vendor_subscription_id' => $paypalSubscription['id'],
            'vendor_customer_id'     => Arr::get($paypalSubscription, 'subscriber.payer_id', ''),
            'current_payment_method' => 'paypal',
        ]);

        $transactionUpdateData = [];
        $lastTransactionAmount = Helper::toCent(Arr::get($paypalSubscription, 'billing_info.last_payment.amount.value', 0));

        if (($lastTransactionAmount && $transaction->total == $lastTransactionAmount) || $transaction->total == 0) {
            $transactionUpdateData = [
                'order_id'       => $order->id,
                'status'         => Status::TRANSACTION_SUCCEEDED,
                'payment_method' => 'paypal'
            ];
        }

        if ($transactionUpdateData) {
            $transactionUpdateData = array_filter([
                'order_id'       => $order->id,
                'status'         => Status::TRANSACTION_SUCCEEDED,
                'payment_method' => 'paypal',
            ]);

            $transaction->fill($transactionUpdateData);
            $transaction->save();
        }


        if ($order->type === Status::ORDER_TYPE_RENEWAL) {
            $subscriptionUpdateData['canceled_at'] = null;
            $billingInfo = PaymentHelper::parsePaymentMethodDetails('paypal', [
                'email'    => Arr::get($paypalSubscription, 'subscriber.email_address'),
                'payer_id' => Arr::get($paypalSubscription, 'subscriber.payer_id'),
                'name'     => Arr::get($paypalSubscription, 'subscriber.name.given_name') . ' ' . Arr::get($paypalSubscription, 'subscriber.name.surname'),
                'address'  => Arr::get($paypalSubscription, 'subscriber.shipping_address.address')
            ]);

            SubscriptionService::recordManualRenewal($subscriptionModel, $transaction, [
                'billing_info'      => $billingInfo,
                'subscription_args' => $subscriptionUpdateData
            ]);

        } else {
            // This can be a trialing subscription
            if ($subscriptionModel->trial_days > 0) {
                $subscriptionUpdateData['status'] = Status::SUBSCRIPTION_TRIALING;
            }

            $oldStatus = $subscriptionModel->status;

            $subscriptionModel->fill($subscriptionUpdateData);
            $subscriptionModel->save();

            $subscriptionModel->updateMeta('active_payment_method', PaymentHelper::parsePaymentMethodDetails('paypal', [
                'email'    => Arr::get($paypalSubscription, 'subscriber.email_address'),
                'payer_id' => Arr::get($paypalSubscription, 'subscriber.payer_id'),
                'name'     => Arr::get($paypalSubscription, 'subscriber.name.given_name') . ' ' . Arr::get($paypalSubscription, 'subscriber.name.surname'),
                'address'  => Arr::get($paypalSubscription, 'subscriber.shipping_address.address')
            ]));

            if ($oldStatus != $subscriptionModel->status && (Status::SUBSCRIPTION_ACTIVE === $subscriptionModel->status || Status::SUBSCRIPTION_TRIALING === $subscriptionModel->status)) {
                (new SubscriptionActivated($subscriptionModel, $order, $order->customer))->dispatch();
            }
        }

        if ($transaction->status === Status::TRANSACTION_SUCCEEDED) {
            (new StatusHelper($order))->syncOrderStatuses($transaction);
        } else {
            fluent_cart_add_log('PayPal Subscription Activated', 'Subscription activated, transaction & order statuses will be synced on webhook receive.', [
                'module_name' => 'order',
                'module_id'   => $order->id,
            ]);
            if ($subscriptionModel) {
                $subscriptionModel->addLog('PayPal Subscription Activated', 'Subscription activated, transaction & order statuses will be synced on webhook receive.');
            }
        }

        return $subscriptionModel;
    }


    private function toDecimal($cents)
    {
        return Helper::toDecimalWithoutComma($cents);
    }

}

```
