File "StripeProcessor.php"

Full Path: /home/romayxjt/public_html/wp-content/plugins/fluentform/app/Modules/Payments/PaymentMethods/Stripe/StripeProcessor.php
File size: 25.51 KB
MIME-type: text/x-php
Charset: utf-8

<?php

namespace FluentForm\App\Modules\Payments\PaymentMethods\Stripe;

if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly.
}

use FluentForm\App\Helpers\Helper;
use FluentForm\App\Services\FormBuilder\ShortCodeParser;
use FluentForm\Framework\Helpers\ArrayHelper;
use FluentForm\App\Modules\Payments\PaymentHelper;
use FluentForm\App\Modules\Payments\PaymentMethods\BaseProcessor;
use FluentForm\App\Modules\Payments\PaymentMethods\Stripe\API\CheckoutSession;
use FluentForm\App\Modules\Payments\PaymentMethods\Stripe\API\Plan;

class StripeProcessor extends BaseProcessor
{
    public $method = 'stripe';

    protected $form;

    public function init()
    {
        add_action('fluentform/process_payment_stripe_hosted', array($this, 'handlePaymentAction'), 10, 6);
        add_action('fluentform/payment_frameless_' . $this->method, array($this, 'handleSessionRedirectBack'));
    }

    public function handlePaymentAction($submissionId, $submissionData, $form, $methodSettings, $hasSubscriptions, $totalPayable)
    {
        $this->setSubmissionId($submissionId);
        $this->form = $form;
        $submission = $this->getSubmission();
        $paymentTotal = $this->getAmountTotal();

        if (!$paymentTotal && !$hasSubscriptions) {
            return false;
        }

        // Create the initial transaction here
        $transaction = $this->createInitialPendingTransaction($submission, $hasSubscriptions);

        $this->handleCheckoutSession($transaction, $submission, $form, $methodSettings);
    }

    public function handleCheckoutSession($transaction, $submission, $form, $methodSettings)
    {

        $formSettings = PaymentHelper::getFormSettings($form->id);

        $args = [
            'fluentform_payment' => $submission->id,
            'payment_method'     => $this->method,
            'transaction_hash'   => $transaction->transaction_hash,
            'type'               => 'success'
        ];


        $successUrl = add_query_arg($args, site_url('index.php'));

        $cancelUrl = $submission->source_url;

        if (!wp_http_validate_url($cancelUrl)) {
            $cancelUrl = site_url($cancelUrl);
        }
        

        $checkoutArgs = [
            'cancel_url'                 => wp_sanitize_redirect($cancelUrl),
            'success_url'                => wp_sanitize_redirect($successUrl),
            'locale'                     => 'auto',
            'billing_address_collection' => 'auto',
            'client_reference_id'        => $submission->id,
            'customer_email'             => $transaction->payer_email,
            'metadata'                   => [
                'submission_id'  => $submission->id,
                'form_id'        => $form->id,
                'transaction_id' => ($transaction) ? $transaction->id : ''
            ]
        ];

        if (ArrayHelper::get($methodSettings, 'settings.require_billing_info.value') == 'yes') {
            $checkoutArgs['billing_address_collection'] = 'required';
        }

        if (ArrayHelper::get($methodSettings, 'settings.require_shipping_info.value') == 'yes') {
            $checkoutArgs['shipping_address_collection'] = [
                'allowed_countries' => StripeSettings::supportedShippingCountries()
            ];
        }

        if ($lineItems = $this->getFormattedItems($submission->currency)) {
            $checkoutArgs['line_items'] = $lineItems;
        }

        $receiptEmail = PaymentHelper::getCustomerEmail($submission, $form);

        if ($receiptEmail) {
            $checkoutArgs['customer_email'] = $receiptEmail;
        }

        if ($transaction->transaction_type == 'subscription') {
            unset($checkoutArgs['submit_type']);

            $subscriptions = $this->getSubscriptions();
            $subscription = $subscriptions[0];

            if ($subscription->initial_amount) {
                if (!isset($checkoutArgs['line_items'])) {
                    $checkoutArgs['line_items'] = [];
                }
                $price = $subscription->initial_amount;
                if (PaymentHelper::isZeroDecimal($transaction->currency)) {
                    $price = intval($price / 100);
                }
                $checkoutArgs['line_items'][] = [
                    'amount'   => $price,
                    'currency' => $transaction->currency,
                    'name'     => __('Signup fee for ', 'fluentform') . $subscription->plan_name,
                    'quantity' => 1
                ];
            }

            $stripePlan = Plan::getSubscriptionPlanBySubscription($subscription, $transaction->currency);

            if(is_wp_error($stripePlan)) {
                $this->handlePaymentChargeError($stripePlan->get_error_message(), $submission, $transaction);
            }

            $this->updateSubscription($subscription->id, [
                'vendor_plan_id' => $stripePlan->id
            ]);

            $subsArgs = [
                'items' => [
                    [
                        'plan'     => $stripePlan->id,
                        'quantity' => $subscription->quantity ? $subscription->quantity : 1
                    ]
                ]
            ];

            if ($subscription->trial_days) {
                $subsArgs['trial_period_days'] = $subscription->trial_days;
            }

            $subsArgs['metadata'] = $this->getIntentMetaData($submission, $form, $transaction);

            $checkoutArgs['subscription_data'] = $subsArgs;

        } else {
            $checkoutArgs['submit_type'] = 'auto';
            $checkoutArgs['payment_intent_data'] = $this->getPaymentIntentData($transaction, $submission, $form);
            if ($receiptEmail && ArrayHelper::get($formSettings, 'disable_stripe_payment_receipt') != 'yes') {
                $checkoutArgs['payment_intent_data']['receipt_email'] = $receiptEmail;
            }
        }
        if ($formSettings['transaction_type'] == 'donation' && $transaction->transaction_type != 'subscription') {
            $checkoutArgs['submit_type'] = 'donate';
        }

        $checkoutArgs = apply_filters_deprecated(
            'fluentform_stripe_checkout_args',
            [
                $checkoutArgs,
                $submission,
                $transaction,
                $form
            ],
            FLUENTFORM_FRAMEWORK_UPGRADE,
            'fluentform/stripe_checkout_args',
            'Use fluentform/stripe_checkout_args instead of fluentform_stripe_checkout_args.'
        );

        $checkoutArgs = apply_filters('fluentform/stripe_checkout_args', $checkoutArgs, $submission, $transaction, $form);

        // If FluentForm Pro is not installed, apply the fee 1.9%
        if (!Helper::hasPro()) {
            if ($transaction->transaction_type == 'subscription') {
                $checkoutArgs['subscription_data']['application_fee_percent'] = 1.9; // 1.9%
            } else {
                // Total amount of 1.9%
                $applicationFeeAmount = (int) ($transaction->payment_total * 0.019);
                $checkoutArgs['payment_intent_data']['application_fee_amount'] = $applicationFeeAmount;
            }
        }

        $session = CheckoutSession::create($checkoutArgs);

        if (!empty($session->error) || is_wp_error($session)) {
            $error = __('Something is wrong', 'fluentform');
            if (is_wp_error($session)) {
                $error = $session->get_error_message();
            } else if (!empty($session->error->message)) {
                $error = $session->error->message;
            }
            wp_send_json([
                'errors' => __('Stripe Error: ', 'fluentform') . $error
            ], 423);
        }

        $this->setMetaData('stripe_session_id', $session->id);

        $logData = [
            'parent_source_id' => $submission->form_id,
            'source_type'      => 'submission_item',
            'source_id'        => $submission->id,
            'component'        => 'Payment',
            'status'           => 'info',
            'title'            => __('Redirect to Stripe', 'fluentform'),
            'description'      => __('User redirect to Stripe for completing the payment', 'fluentform')
        ];
        do_action('fluentform/log_data', $logData);

        wp_send_json_success([
            'nextAction' => 'payment',
            'actionName' => 'stripeRedirectToCheckout',
            'sessionId'  => $session->id,
            'message'    => __('You are redirecting to stripe.com to complete the purchase. Please wait while you are redirecting....', 'fluentform'),
            'result'     => [
                'insert_id' => $submission->id
            ]
        ], 200);
    }

    protected function getPaymentIntentData($transaction, $submission, $form)
    {
        $data = [
            'capture_method'              => 'automatic',
            'description'                 => $form->title,
            'statement_descriptor_suffix' => StripeSettings::getPaymentDescriptor($form),
        ];

        $paymentSettings = PaymentHelper::getFormSettings($form->id, 'admin');
        $intentMeta = $this->getIntentMetaData($submission, $form, $transaction, $paymentSettings);

        $data['metadata'] = $intentMeta;
        return $data;
    }

    public function getFormattedItems($currency)
    {
        $orderItems = $this->getOrderItems();

        $discountItems = $this->getDiscountItems();

        $discountTotal = 0;
        foreach ($discountItems as $item) {
            $discountTotal += $item->line_total;
        }

        $orderTotal = 0;
        foreach ($orderItems as $orderItem) {
            $orderTotal += $orderItem->line_total;
        }

        $formattedItems = [];

        foreach ($orderItems as $item) {
            $price = $item->item_price;

            if($discountTotal) {
                $price = intval($price - ($discountTotal / $orderTotal) * $price);
            }

            if (PaymentHelper::isZeroDecimal($currency)) {
                $price = intval($price / 100);
            }

            $quantity = $item->quantity ?: 1;

            $stripeLine = [
                'amount'   => $price,
                'currency' => $currency,
                'name'     => $item->item_name,
                'quantity' => $quantity
            ];

            $formattedItems[] = $stripeLine;
        }

        return $formattedItems;
    }

    public function handleSessionRedirectBack($data)
    {
        $type = sanitize_text_field($data['type']);
        $submissionId = intval($data['fluentform_payment']);
        $this->setSubmissionId($submissionId);

        $submission = $this->getSubmission();

        if (!$submission) {
            return;
        }

        if ($type == 'success') {
            if ($this->getMetaData('is_form_action_fired') == 'yes') {
                $returnData = $this->getReturnData();
            } else {
                $sessionId = $this->getMetaData('stripe_session_id');
                $session = CheckoutSession::retrieve($sessionId, [
                    'expand' => [
                        'subscription.latest_invoice.payment_intent',
                        'payment_intent'
                    ]
                ], $submission->form_id);

                if ($session && !is_wp_error($session) && $session->customer) {
                    $transactionHash = sanitize_text_field($data['transaction_hash']);
                    $transaction = $this->getTransaction($transactionHash, 'transaction_hash');
                    $returnData = $this->processStripeSession($session, $submission, $transaction);
                } else {
                    $error = __('Sorry! No Valid payment session found. Please try again');
                    if (is_wp_error($session)) {
                        $error = $session->get_error_message();
                    }
                    $returnData = [
                        'insert_id' => $submission->id,
                        'title'     => __('Failed to retrieve session data', 'fluentform'),
                        'result'    => false,
                        'error'     => $error
                    ];
                }
            }
        } else {
            $returnData = [
                'insert_id' => $submission->id,
                'result'    => false,
                'error'     => __('Looks like you have cancelled the payment. Please try again!', 'fluentform')
            ];
        }

        $returnData['type'] = $type;

        if (!isset($returnData['is_new'])) {
            $returnData['is_new'] = false;
        }

        $this->showPaymentView($returnData);
    }

    public function processStripeSession($session, $submission, $transaction)
    {
        $this->setSubmissionId($submission->id);

        if ($transaction->status == 'paid' && $submission->payment_status == 'paid') {
            return $this->getReturnData();
        }

        $invoice = empty($session->subscription->latest_invoice) ? $session : $session->subscription->latest_invoice;

        $paymentStatus = $this->getIntentSuccessName($invoice->payment_intent);

        $this->changeSubmissionPaymentStatus($paymentStatus);

        if($transaction->transaction_type == 'subscription') {
            $subscriptions = $this->getSubscriptions();

            if($subscriptions) {
                $this->processSubscriptionSuccess($subscriptions, $invoice, $submission);

                $vendorSubscription = $session->subscription;
                if($vendorSubscription) {
                    Plan::maybeSetCancelAt($subscriptions[0], $vendorSubscription);
                }
            }
        }

        $this->processOneTimeSuccess($invoice, $transaction, $paymentStatus);

        $returnData = $this->completePaymentSubmission(false);
        $this->recalculatePaidTotal();
        $returnData['is_new'] = $this->getMetaData('is_form_action_fired') === 'yes';

        return $returnData;
    }

    protected function getIntentSuccessName($intent)
    {
        if (!$intent || !$intent->status) {
            return false;
        }

        $successStatuses = [
            'succeeded'        => 'paid',
            'requires_capture' => 'requires_capture'
        ];

        if (isset($successStatuses[$intent->status])) {
            return $successStatuses[$intent->status];
        }

        return false;
    }

    protected function getDescriptor($title)
    {
        $illegal = array('<', '>', '"', "'");
        // Remove slashes
        $descriptor = stripslashes($title);
        // Remove illegal characters
        $descriptor = str_replace($illegal, '', $descriptor);
        // Trim to 22 characters max
        return substr($descriptor, 0, 22);
    }

    protected function formatAddress($address)
    {
        $addressArray = [
            'line1'       => $address->line1,
            'line2'       => $address->line2,
            'city'        => $address->city,
            'state'       => $address->state,
            'postal_code' => $address->postal_code,
            'country'     => $address->country,
        ];
        return implode(', ', array_filter($addressArray));
    }

    public function handleRefund($event)
    {
        $data = $event->data->object;

        $chargeId = $data->payment_intent;
        // Get the Transaction from database
        $transaction = wpFluent()->table('fluentform_transactions')
            ->where('charge_id', $chargeId)
            ->where('payment_method', 'stripe')
            ->first();

        if (!$transaction) {
            // Not our transaction
            return;
        }

        $submission = wpFluent()->table('fluentform_submissions')
            ->find($transaction->submission_id);

        if (!$submission) {
            return;
        }

        $this->setSubmissionId($submission->id);
        $amountRefunded = $data->amount_refunded;
        if (PaymentHelper::isZeroDecimal($data->currency)) {
            $amountRefunded = $amountRefunded * 100;
        }

        // Remove All Existing Refunds
        wpFluent()->table('fluentform_transactions')
            ->where('submission_id', $submission->id)
            ->where('transaction_type', 'refund')
            ->delete();

        $this->refund($amountRefunded, $transaction, $submission, 'stripe', $chargeId, 'Refund from Stripe');

    }

    public function getPaymentMode()
    {
        $stripeSettings = StripeSettings::getSettings();
        return $stripeSettings['payment_mode'];
    }

    public function processSubscriptionSuccess($subscriptions, $invoice, $submission)
    {
        foreach ($subscriptions as $subscription) {
            $subscriptionStatus = 'active';

            if ($subscription->trial_days) {
                $subscriptionStatus = 'trialling';
            }

            $updateData = [
                'vendor_customer_id' => $invoice->customer,
                'vendor_response'    => maybe_serialize($invoice),
            ];

            if(!$subscription->bill_count && $subscriptionStatus == 'active') {
                $updateData['bill_count'] = 1;
            }

            if (!$subscription->vendor_subscription_id) {
                $updateData['vendor_subscription_id'] = $invoice->subscription;
            }

            $this->updateSubscription($subscription->id, $updateData);
            $subscription = fluentFormApi('submissions')->getSubscription($subscription->id);
            $this->updateSubscriptionStatus($subscription, $subscriptionStatus);
        }

        $logData = [
            'parent_source_id' => $submission->form_id,
            'source_type'      => 'submission_item',
            'source_id'        => $submission->id,
            'component'        => 'Payment',
            'status'           => 'success',
            'title'            => __('Stripe Subscription Charged', 'fluentform'),
            'description'      => __('Stripe recurring subscription successfully initiated', 'fluentform')
        ];

        do_action('fluentform/log_data', $logData);

        if (!empty($invoice->payment_intent->charges->data[0])) {
            $charge = $invoice->payment_intent->charges->data[0];
            $this->recordStripeBillingAddress($charge, $submission);
        }
    }

    protected function recordStripeBillingAddress($charge, $submission)
    {
        if(!is_array($submission->response)) {
            $submission->response = json_decode($submission->response, true);
        }

        if (isset($submission->response['__checkout_billing_address_details'])) {
            return;
        }

        if (empty($charge->billing_details)) {
            return;
        }

        $billingDetails = $charge->billing_details;
        if (!empty($billingDetails->address)) {
            $submission->response['__checkout_billing_address_details'] = $billingDetails->address;
        }
        if (!empty($billingDetails->phone)) {
            $submission->response['__stripe_phone'] = $billingDetails->phone;
        }
        if (!empty($billingDetails->name)) {
            $submission->response['__stripe_name'] = $billingDetails->name;
        }
        if (!empty($billingDetails->email)) {
            $submission->response['__stripe_email'] = $billingDetails->email;
        }

        $this->updateSubmission($submission->id, [
            'response' => json_encode($submission->response)
        ]);

        $logData = [
            'parent_source_id' => $submission->form_id,
            'source_type'      => 'submission_item',
            'source_id'        => $submission->id,
            'component'        => 'Payment',
            'status'           => 'info',
            'title'            => __('Stripe Billing Address Logged', 'fluentform'),
            'description'      => __('Billing address from stripe has been logged in the submission data', 'fluentform')
        ];

        do_action('fluentform/log_data', $logData);
    }

    protected function processOneTimeSuccess($invoice, $transaction, $paymentStatus)
    {
        if ($transaction) {
            $updateData = [
                'charge_id'    => $invoice->payment_intent ? $invoice->payment_intent->id : null,
                'status'       => 'paid',
                'payment_note' => maybe_serialize($invoice->payment_intent)
            ];

            $updateData = array_merge($updateData, $this->retrieveCustomerDetailsFromInvoice($invoice));

            $this->updateTransaction($transaction->id, $updateData);

            $this->changeTransactionStatus($transaction->id, $paymentStatus);
        }
    }

    protected function getIntentMetaData($submission, $form, $transaction, $paymentSettings = false)
    {
        if (!$paymentSettings) {
            $paymentSettings = PaymentHelper::getFormSettings($form->id, 'admin');
        }

        $intentMeta = [
            'submission_id'  => $submission->id,
            'form_id'        => $form->id,
            'transaction_id' => $transaction->id,
            'wp_plugin'      => 'Fluent Forms Pro',
            'form_title'     => $form->title
        ];
    
        $metaItems = ArrayHelper::get($paymentSettings, 'stripe_meta_data', []);
        if ((ArrayHelper::get($paymentSettings, 'push_meta_to_stripe') == 'yes') && !empty($metaItems)) {

            foreach ($metaItems as $metaItem) {
                if ($itemValue = ArrayHelper::get($metaItem, 'item_value')) {
                    $metaData[ArrayHelper::get($metaItem, 'label', 'item')] = $itemValue;
                }
            }

            $metaData = ShortCodeParser::parse($metaData, $submission->id, $submission->response);

            $metaData = array_filter($metaData);

            foreach ($metaData as $itemKey => $value) {
                if (is_string($value) || is_numeric($value)) {
                    $metaData[$itemKey] = strip_tags($value);
                }
            }

            $intentMeta = array_merge($intentMeta, $metaData);
        }

        return $intentMeta;
    }

    protected function retrieveCustomerDetailsFromInvoice($invoice)
    {
        $customer = [];

        if (!empty($invoice->payment_intent->charges->data[0])) {
            $charge = $invoice->payment_intent->charges->data[0];

            $customer = $this->retrieveCustomerDetailsFromCharge($charge);

            $customerName = isset($charge->billing_details->name) &&
            !empty($charge->billing_details->name) ?
                $charge->billing_details->name : (
                isset($invoice->customer_name) &&
                !empty($invoice->customer_name) ?
                    $invoice->customer_name : ''
                );

            $customer = array_merge($customer, [
                'payer_email' => $invoice->customer_email,
                'payer_name'  => $customerName,
            ]);
        }

        return $customer;
    }

    protected function retrieveCustomerDetailsFromCharge($charge)
    {
        $customer = [];

        if (!empty($charge->billing_details)) {
            $customer['billing_address'] = $this->formatAddress($charge->billing_details->address);
        }

        if (!empty($charge->shipping) && !empty($charge->shipping->address)) {
            $customer['shipping_address'] = $this->formatAddress($charge->shipping->address);
        }

        if (!empty($charge->payment_method_details->card)) {
            $card = $charge->payment_method_details->card;
            $customer['card_brand'] = $card->brand;
            $customer['card_last_4'] = $card->last4;
        }

        return $customer;
    }

    protected function handlePaymentChargeError($message, $submission, $transaction, $charge = false, $type = 'general')
    {
        do_action_deprecated(
            'fluentform_payment_stripe_failed',
            [
                $submission,
                $transaction,
                $this->form->id,
                $charge,
                $type
            ],
            FLUENTFORM_FRAMEWORK_UPGRADE,
            'fluentform/payment_stripe_failed',
            'Use fluentform/payment_stripe_failed instead of fluentform_payment_stripe_failed.'
        );

        do_action('fluentform/payment_stripe_failed', $submission, $transaction, $this->form->id, $charge, $type);

        do_action_deprecated(
            'fluentform_payment_failed',
            [
                $submission,
                $transaction,
                $this->form->id,
                $charge,
                $type
            ],
            FLUENTFORM_FRAMEWORK_UPGRADE,
            'fluentform/payment_failed',
            'Use fluentform/payment_failed instead of fluentform_payment_failed.'
        );
        do_action('fluentform/payment_failed', $submission, $transaction, $this->form->id, $charge, $type);

        if ($transaction) {
            $this->changeTransactionStatus($transaction->id, 'failed');
        }

        $this->changeSubmissionPaymentStatus('failed');

        if ($message) {
            $logData = [
                'parent_source_id' => $submission->form_id,
                'source_type'      => 'submission_item',
                'source_id'        => $submission->id,
                'component'        => 'Payment',
                'status'           => 'error',
                'title'            => __('Stripe Payment Error', 'fluentform'),
                'description'      => __($message, 'fluentform')
            ];

            do_action('fluentform/log_data', $logData);
        }

        wp_send_json([
            'errors'      => __('Stripe Error: ', 'fluentform') . $message,
            'append_data' => [
                '__entry_intermediate_hash' => Helper::getSubmissionMeta($submission->id, '__entry_intermediate_hash')
            ]
        ], 423);
    }

    public function recordSubscriptionCharge($subscription, $transactionData)
    {
        $this->setSubmissionId($subscription->submission_id);
        return $this->maybeInsertSubscriptionCharge($transactionData);
    }
}