# fluent-cart/1.6.4/app/Modules/PaymentMethods/Core/AbstractPaymentGateway.php

FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler, version 1.6.4. 603 lines.

- Page: https://pluginprobe.com/plugins/fluent-cart/1.6.4/code/app/Modules/PaymentMethods/Core/AbstractPaymentGateway.php
- Raw: https://pluginprobe.com/plugins/fluent-cart/1.6.4/raw/app/Modules/PaymentMethods/Core/AbstractPaymentGateway.php
- Modified: 2026-09-11T13:28:26+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.6.4/code/app/Modules/PaymentMethods/Core/AbstractPaymentGateway.php#L10-L20`.

```php
<?php

namespace FluentCart\App\Modules\PaymentMethods\Core;

use FluentCart\Api\Helper;
use FluentCart\Api\Orders;
use FluentCart\Api\Resource\FrontendResource\CartResource;
use FluentCart\Api\StoreSettings;
use FluentCart\App\App;
use FluentCart\App\Helpers\CartCheckoutHelper;
use FluentCart\App\Helpers\Status;
use FluentCart\App\Helpers\StatusHelper;
use FluentCart\App\Models\OrderTransaction;
use FluentCart\Api\Resource\ActivityResource;
use FluentCart\App\Services\Payments\PaymentHelper;
use FluentCart\App\Services\Renderer\Receipt\ReceiptRenderer;
use FluentCart\Framework\Support\Arr;

abstract class AbstractPaymentGateway implements PaymentGatewayInterface
{

    private $methodSlug = '';

    public array $supportedFeatures = [];

    public StoreSettings $storeSettings;

    public ?AbstractSubscriptionModule $subscriptions;

    public BaseGatewaySettings $settings;

    public function __construct(BaseGatewaySettings $settings, ?AbstractSubscriptionModule $subscriptions = null)
    {
        $this->settings = $settings;
        $this->methodSlug = $this->getMeta('slug');

        if ($subscriptions) {
            $this->supportedFeatures[] = 'subscriptions';
        }
        $this->subscriptions = $subscriptions;

        // register global hooks
        $this->init();
    }

    public function init(): void
    {
        add_filter('fluent_cart/transaction/url_' . $this->methodSlug, [$this, 'getTransactionUrl'], 10, 2);
        add_filter('fluent_cart/subscription/url_' . $this->methodSlug, [$this, 'getSubscriptionUrl'], 10, 2);
    }

    public function has(string $feature): bool
    {
        return in_array($feature, $this->supportedFeatures);
    }

    public function getMeta($key = '')
    {
        $meta = $this->meta();

        $gatewaySettings = $this->settings->get();

        if (isset($gatewaySettings['checkout_label']) && !empty($gatewaySettings['checkout_label'])) {
            $meta['title'] = $gatewaySettings['checkout_label'];
        }

        if (isset($gatewaySettings['checkout_logo']) && !empty($gatewaySettings['checkout_logo'])) {
            $meta['logo'] = $gatewaySettings['checkout_logo'];
        }

        if (!empty($gatewaySettings['checkout_instructions'])) {
            $instructionsText = trim(wp_strip_all_tags(html_entity_decode($gatewaySettings['checkout_instructions'], ENT_QUOTES)));
            // Nbsp survives strip_tags/html_entity_decode as a literal
            // non-breaking space, so a rich-text editor can save "<p>&nbsp;</p>"
            // and still look non-empty to a bare !empty() check.
            $instructionsText = str_replace("\xC2\xA0", '', $instructionsText);

            if (trim($instructionsText) !== '') {
                $meta['instructions'] = $gatewaySettings['checkout_instructions'];
            }
        }

        if ($key !== '') {
            return Arr::get($meta, $key, '');
        }
        return $meta;
    }

    public function isUpcoming(): bool
    {
        return $this->getMeta('upcoming');
    }

    public function setStoreSettings(StoreSettings $settings): void
    {
        $this->storeSettings = $settings;
    }

    public function storeSettings(): StoreSettings
    {
        return $this->storeSettings;
    }

    public function isCurrencySupported(): bool
    {
        return true;
    }

    public function isEnabled(): bool
    {
        return $this->settings->get('is_active') === 'yes';
    }

    public static function validateSettings($data): array
    {
        return $data;
    }

    public static function beforeSettingsUpdate($data, $oldSettings): array
    {
        return $data;
    }

    public function updateSettings($data)
    {
        if ($this->isUpcoming()) {
            wp_send_json([
                'status'  => 'failed',
                'message' => __('Payment method is upcoming! Not available for right now!', 'fluent-cart')
            ], 422);
        }

        $oldSettings = $this->settings->get();
        $settings = wp_parse_args($data, $oldSettings);
        $settings = Helper::sanitize($settings, $this->fields());
        $is_active = Arr::get($settings, 'is_active', 'no');
        // validate if the settings/credentials are correct
        if ('yes' === $is_active) {
            $response = static::validateSettings($settings);
            if (isset($response['status']) && $response['status'] === 'failed') {
                wp_send_json(
                    [
                        'status'  => 'failed',
                        'message' => $response['message'] ? $response['message'] : __('Invalid credentials!', 'fluent-cart'),
                        'data'    => []
                    ],
                    422
                );
            }
        }

        $settings = static::beforeSettingsUpdate($settings, $oldSettings);
//        unset($settings['payment_mode']);
        unset($settings['provider']);
        fluent_cart_update_option($this->settings->methodHandler, $settings);

        return $settings;
    }

    /**
     * Back-compat wrapper — the canonical entry point is
     * OrderTransaction::getSuccessUrl(); call that directly.
     */
    public function getSuccessUrl($transaction, $args = [])
    {
        return $transaction->getSuccessUrl($args);
    }

    public static function getCancelUrl(): string
    {
        $checkoutPage = (new StoreSettings())->getCheckoutPage();
        // get cart hash from url
        $cartHash = App::request()->get('fct_cart_hash', '');
        if ($cartHash) {
            return add_query_arg([
                'fct_cart_hash' => $cartHash
            ], $checkoutPage);
        }
        return $checkoutPage;
    }

    public function paymentFailedNote($content, $data)
    {
        $request = Arr::get($data, 'request');
        $trx_hash = $request->getSafe('trx_hash', 'sanitize_text_field');
        $transaction = OrderTransaction::query()->where('uuid', $trx_hash)->first();
        if (!$transaction) {
            return __('Transaction not found!', 'fluent-cart');
        }

        $order = (new Orders())->getById($transaction->order_id);

        if (!$order || $transaction->status === Status::TRANSACTION_SUCCEEDED) {
            return '';
        }

        $failedtitle = __('Payment Failed', 'fluent-cart');
        $hasLog = ActivityResource::getQuery()->where('module_id', $order->id)
            ->where('module_name', 'Order')
            ->where('status', 'error')
            ->where('title', $failedtitle)
            ->count();

        if (!$hasLog) {
            $content = 'Payment Failed Reason: ' . $request->getSafe('reason', 'sanitize_text_field');
            fluent_cart_error_log($failedtitle, $content, [
                'module_id'   => $order->id,
                'module_name' => 'Order'
            ]);
        }

        ob_start();
        (new ReceiptRenderer())->renderConfirmationError([
            'order'              => $order,
            'failed_reason'      => $request->getSafe('reason', 'sanitize_text_field'),
            'custom_payment_url' => PaymentHelper::getCustomPaymentLink($order->uuid)
        ]);
        return ob_get_clean();
    }

    protected function getListenerUrl($args = null)
    {
        return (new PaymentHelper($this->getMeta('route')))->listenerUrl($args);
    }

    public function getOrderByHash($orderHash)
    {
        return (new Orders())->getByHash($orderHash);
    }

    public function validateSubscriptions($items): bool
    {
        return (new CartResource())->hasSubscriptionProduct($items);
    }

    /**
     * Whether this gateway can save a payment method WITHOUT an accompanying
     * charge (SetupIntent-style), enabling `system` subscriptions on carts with
     * nothing payable now (free trials). Gateways declaring `system_subscription`
     * SHOULD override this when their API supports zero-amount setup.
     */
    public function supportsSetupWithoutCharge(): bool
    {
        return false;
    }

    /**
     * Charge a system (auto-charged, store-billed) subscription's renewal invoice
     * off-session using the stored token. Gateways declaring the
     * `system_subscription` capability MUST override this. A successful charge must
     * flow through the gateway's normal charge-confirmation path (so
     * syncOrderStatuses / handleRenewalPaid run). Return WP_Error on failure.
     *
     * @param \FluentCart\App\Services\Payments\PaymentInstance $paymentInstance
     * @param array $args ['attempt' => int] — attempt number, used for idempotency keys
     * @return true|'processing'|\WP_Error true = payment confirmed; 'processing' =
     *                                     charge accepted, the gateway webhook will
     *                                     confirm it (invoice stays scheduled)
     */
    public function chargeRenewal(\FluentCart\App\Services\Payments\PaymentInstance $paymentInstance, $args = [])
    {
        return new \WP_Error('not_supported', __('This payment method cannot charge saved payment methods.', 'fluent-cart'));
    }

    /**
     * Re-check an async (processing) renewal charge against the gateway. Called by
     * the SystemChargeService reconciliation loop when a charge was accepted but
     * the confirming webhook has not arrived. A settled payment must be confirmed
     * through the gateway's normal charge-confirmation path before returning true.
     *
     * @param \FluentCart\App\Services\Payments\PaymentInstance $paymentInstance
     * @return true|'processing'|\WP_Error true = settled and confirmed;
     *                                     'processing' = still settling, check again
     *                                     later; WP_Error = definitively failed
     */
    public function reconcileRenewalCharge(\FluentCart\App\Services\Payments\PaymentInstance $paymentInstance)
    {
        return new \WP_Error('not_supported', __('This payment method cannot reconcile pending charges.', 'fluent-cart'));
    }

    /**
     * Store-managed subscription mode: a manual subscription's payment — the first
     * order or a renewal invoice — must be taken as a plain one-time charge. No
     * vendor subscription may be created and no manual→automatic conversion may
     * happen. Every gateway with a subscription branch in
     * makePaymentFromPaymentInstance() must consult this BEFORE its conversion /
     * vendor-subscription logic and route to its single-payment path when true.
     */
    protected function shouldChargeSubscriptionAsOneTime(\FluentCart\App\Services\Payments\PaymentInstance $paymentInstance): bool
    {
        $subscription = $paymentInstance->subscription;

        // Manual and system subscriptions are both store-billed — any interactive
        // payment against them (first order or renewal invoice) is a one-time charge.
        if (!$subscription || !in_array($subscription->collection_method, ['manual', 'system'], true)) {
            return false;
        }

        // Stamped at creation: a subscription born under store-managed mode must
        // never be converted to automatic billing, even after the merchant switches
        // the store setting back to gateway-managed.
        if (\FluentCart\App\Modules\Subscriptions\Services\SubscriptionManagementMode::isSubscriptionStoreManaged($subscription)) {
            return true;
        }

        // Unstamped manual subscriptions (gateway-managed fallback / pre-feature)
        // charge one-time only while the store is currently store-managed; under
        // gateway-managed they keep today's manual→automatic conversion behavior.
        return \FluentCart\App\Modules\Subscriptions\Services\SubscriptionManagementMode::isStoreManaged();
    }

    //technically not reachable , as conversion to manual subscription is not possible as of now.
    protected function maybeConvertToManualSubscription(\FluentCart\App\Services\Payments\PaymentInstance $paymentInstance): void
    {
        $subscription = $paymentInstance->subscription;
        // Manual AND system are already store-billed — nothing to convert. Without
        // the system check, a COD payment against a system renewal invoice would
        // silently downgrade the subscription to manual and disable auto-charging.
        if (!$subscription || in_array($subscription->collection_method, ['manual', 'system'], true)) {
            return;
        }

        if ($subscription->vendor_subscription_id) {
            $oldGateway = App::gateway($subscription->current_payment_method);
            if ($oldGateway && $oldGateway->has('subscriptions') && $oldGateway->subscriptions) {
                $cancelResult = $oldGateway->subscriptions->cancel($subscription->vendor_subscription_id, [
                    'subscription_id' => $subscription->id,
                    'parent_order_id' => $subscription->parent_order_id,
                ]);
                if (is_wp_error($cancelResult)) {
                    fluent_cart_error_log(
                        'Failed to cancel vendor subscription during conversion to manual',
                        $cancelResult->get_error_message()
                    );
                    return;
                }
            }
        }

        $subscription->collection_method = 'manual';
        $subscription->current_payment_method = $this->getMeta('route');
        $subscription->vendor_subscription_id = null;
        $subscription->save();

        $subscription->addLog(
            'Converted to manual billing',
            sprintf('Subscription converted from automatic to manual — paid via %s', $this->getMeta('label')),
            'warning'
        );

        do_action('fluent_cart/subscription_converted_to_manual', [
            'subscription'   => $subscription,
            'payment_method' => $this->getMeta('route'),
        ]);
    }

    public function validatePaymentMethod($data)
    {
        $isZeroPayment = Arr::get($data, 'isZeroPayment', false);
        if (!$this->isEnabled() && !$isZeroPayment) {
            return [
                'isValid' => false,
                'reason'  => sprintf(
                    /* translators: %s is the payment method name */
                    __('Selected payment method %s is not active!', 'fluent-cart'),
                    $this->getMeta('route')
                )
            ];
        }

        return [
            'isValid' => true,
        ];
    }

    public function updateOrderDataByOrder($order, $transactionData, $transaction)
    {
        if ($order == null) {
            return;
        }

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


        $paymentStatus = Status::syncPaymentStatus(Arr::get($transactionData, 'status'));
        $orderStatus = !in_array($paymentStatus, [Status::TRANSACTION_SUCCEEDED, Status::PAYMENT_PAID]) ? $paymentStatus : Status::ORDER_PROCESSING;

        $statusHelper = (new StatusHelper())->setOrder($order);
        $statusHelper->updateTransactionData($transactionData, $transaction);

        if ($amount = Arr::get($transactionData, 'total')) {
            $statusHelper->updateTotalPaid($amount);
        }

        $statusHelper->changeOrderStatus($orderStatus, $paymentStatus, $this->getMeta('title'), $this->getMeta('slug'));

        //If product is digital and processing then trigger status to completed
        if ($order->fulfillment_type == 'digital' && $orderStatus === Status::ORDER_PROCESSING && $order->total_amount <= $order->total_paid) {
            $statusHelper->changeOrderStatus(Status::ORDER_COMPLETED, $paymentStatus, $this->getMeta('title'), $this->getMeta('slug'));
        }

        do_action('fluent_cart/payments/after_payment_' . $paymentStatus, [
            'order' => $order
        ]);
    }

    public function getCheckoutItems(): array
    {
        return (CartCheckoutHelper::make())->getItems();
    }

    public function getSettings(): BaseGatewaySettings
    {
        return $this->settings;
    }

    public function renderStoreModeNotice(): string
    {
        if ((new StoreSettings())->get('order_mode') == 'test') {
            return '<div class="mt-5"><span class="text-warning-500">' . __('Your Store is in test mode, change Store\'s \'Order Mode\' to \'Live\' and update related settings to enable live payment!', 'fluent-cart') . '</span></div>';
        }
        return '<div class="mt-5"><span class="text-success-500">' . __('Your Store is in Live mode', 'fluent-cart') . '</span></div>';
    }

    public function beforeRenderPaymentMethod($hasSubscription): void
    {
        $this->enqueue($hasSubscription);
    }

    public function getEnqueueVersion()
    {
        return FLUENTCART_VERSION;
    }

    public function getEnqueueScriptSrc($hasSubscription): array
    {
        return [];
    }

    public function getEnqueueStyleSrc(): array
    {
        return [];
    }

    public function getTransactionUrl($url, $data)
    {
        return $url;
    }

    public function getSubscriptionUrl($url, $data)
    {
        return $url;
    }

    public function processRefund($transaction, $amount, $args)
    {
        return new \WP_Error('not_implemented', __('Refund process is not implemented for this payment gateway.', 'fluent-cart'));
    }

    public function syncRemoteTransaction(OrderTransaction $transaction)
    {
        return new \WP_Error('not_implemented', __('Remote transaction sync is not available for this payment method.', 'fluent-cart'));
    }

    public function enqueue($hasSubscription): void
    {
        $styles = $this->getEnqueueStyleSrc();
        $scripts = $this->getEnqueueScriptSrc($hasSubscription);

        foreach ($styles as $style) {
            wp_enqueue_style(
                Arr::get($style, 'handle'),
                Arr::get($style, 'src'),
                Arr::get($style, 'deps', null),
                Arr::get($style, 'version', $this->getEnqueueVersion())
            );
        }

        $handleToEnqueue = '';
        $scriptHandles = [];
        foreach ($scripts as $script) {
            $handle = Arr::get($script, 'handle');
            if (empty($handleToEnqueue)) {
                $handleToEnqueue = $handle;
            }
            $scriptHandles[] = $handle;
            wp_enqueue_script(
                $handle,
                Arr::get($script, 'src'),
                Arr::get($script, 'deps', null),
                Arr::get($script, 'version', $this->getEnqueueVersion()),
                Arr::get($script, 'in_footer', false),
            );
        }

        // Add filter to prevent consent plugins from blocking payment gateway scripts
        if (!empty($scriptHandles) && is_array($scriptHandles)) {
            $gatewayInstance = $this;
        
            add_filter('script_loader_tag', function($tag, $handle, $src) use ($scriptHandles, $gatewayInstance) {
        
                if (!in_array($handle, $scriptHandles, true)) {
                    return $tag;
                }

                // This makes payment scripts load as "necessary" cookies
                $attributes = [
                    'data-category="necessary"',
                    'data-consent-category="necessary"',
                    'data-cookieconsent="ignore"',
                    'data-no-optimize="1"',
                    'data-cfasync="false"',
                ];
        
                $serviceName = '';
        
                if (is_object($gatewayInstance)) {
                    if (method_exists($gatewayInstance, 'getConsentServiceName')) {
                        $serviceName = (string) $gatewayInstance->getConsentServiceName();
                    }
        
                    if (empty($serviceName) && method_exists($gatewayInstance, 'getMeta')) {
                        $serviceName = (string) $gatewayInstance->getMeta('title');
                    }
                }
        
                if (!empty($serviceName)) {
                    $serviceName = esc_attr($serviceName);
                    $attributes[] = 'data-usercentrics="' . $serviceName . '"';
                    $attributes[] = 'data-service="' . $serviceName . '"';
                }
        
                $tag = preg_replace(
                    '/<script(\s+)/',
                    '<script$1' . implode(' ', $attributes) . ' ',
                    $tag,
                    1
                );
        
                return $tag;
            }, 10, 3);
        }

        if (!empty($handleToEnqueue)) {
            foreach ($this->getLocalizeData() as $key => $val) {
                wp_localize_script($handleToEnqueue, $key, $val);
            }
        }


    }

    public function prepare($mode, $hasSubscription)
    {
        $this->beforeRenderPaymentMethod($hasSubscription);
        $this->render($mode);
        $route = $this->getMeta('route');
        do_action('fluent_cart/after_render_payment_method_' . $route);
    }

    public function render($mode = 'logo')
    {
        $content = '';
        if ($mode === 'logo') {
            $content .= '<img src="' . esc_url($this->getMeta('logo') ?? '') . '"alt="' . esc_attr($this->getMeta('title')) . '"/>';
        } elseif ($mode === 'radio') {
            $content .= '<span class="checkmark">' . '</span>';
        } else {
            $content .= '<span>' . $this->getMeta('title') . '</span>';
        }
        echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    }

    public function getLocalizeData(): array
    {
//        $example = [
//            'var_name' => [
//                'key' => 'value'
//            ],
//
//            'var_name_two' => [
//                'key' => 'value'
//            ],
//        ];
        return [];
    }

    /**
     * Get the consent service name for this payment gateway
     * Used by consent management plugins (Usercentrics, CookieYes, etc.)
     * Override in child classes to specify the service name
     * 
     * @return string|null Service name or null to use gateway slug
     */
    public function getConsentServiceName(): ?string
    {
        // By default, return null and let the system use gateway meta title
        return null;
    }

}

```
