# fluent-cart/1.3.21/app/Modules/Subscriptions/Services/SubscriptionService.php

FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler, version 1.3.21. 387 lines.

- Page: https://pluginprobe.com/plugins/fluent-cart/1.3.21/code/app/Modules/Subscriptions/Services/SubscriptionService.php
- Raw: https://pluginprobe.com/plugins/fluent-cart/1.3.21/raw/app/Modules/Subscriptions/Services/SubscriptionService.php
- Modified: 2026-04-07T13:41:30+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.3.21/code/app/Modules/Subscriptions/Services/SubscriptionService.php#L10-L20`.

```php
<?php

namespace FluentCart\App\Modules\Subscriptions\Services;

use FluentCart\App\Events\Subscription\SubscriptionEOT;
use FluentCart\App\Events\Subscription\SubscriptionRenewed;
use FluentCart\App\Events\Subscription\SubscriptionValidityExpired;
use FluentCart\App\Helpers\Status;
use FluentCart\App\Helpers\StatusHelper;
use FluentCart\App\Models\Order;
use FluentCart\App\Models\OrderItem;
use FluentCart\App\Models\OrderTaxRate;
use FluentCart\App\Models\OrderTransaction;
use FluentCart\App\Models\Subscription;
use FluentCart\App\Modules\PaymentMethods\PayPalGateway\API\API;
use FluentCart\App\Services\DateTime\DateTime;
use FluentCart\App\Services\Payments\PaymentHelper;
use FluentCart\Framework\Support\Arr;

class SubscriptionService
{
    public static function recordRenewalPayment($transactionData, $subscriptionModel = null, $subscriptionUpdateArgs = [])
    {
        if (!$subscriptionModel) {
            $subscriptionModel = Subscription::query()->find($transactionData['subscription_id']);
        }

        if (!$subscriptionModel) {
            return new \WP_Error('subscription_not_found', __('Subscription not found.', 'fluent-cart'));
        }

        $vendorTransactionId = $transactionData['vendor_charge_id'] ?? null;

        if ($vendorTransactionId) {
            if (OrderTransaction::query()->where('vendor_charge_id', $vendorTransactionId)->exists()) {
                return new \WP_Error('transaction_exists', __('This transaction already exists for this subscription.', 'fluent-cart'));
            }
        }

        $parentOrder = $subscriptionModel->order;

        if (!$parentOrder) {
            return new \WP_Error('parent_order_not_found', __('Parent order not found for this subscription.', 'fluent-cart'));
        }

        $transactionDefaults = [
            'order_id' => $parentOrder->id,
            'subscription_id' => $subscriptionModel->id,
            'order_type' => Status::ORDER_TYPE_RENEWAL,
            'transaction_type' => Status::TRANSACTION_TYPE_CHARGE,
            'payment_method' => $subscriptionModel->current_payment_method,
            'payment_mode' => $parentOrder->mode,
            'status' => Status::TRANSACTION_SUCCEEDED,
            'currency' => $parentOrder->currency,
            'total' => $subscriptionModel->recurring_total,
            'meta' => Arr::get($transactionData, 'meta', [])
        ];

        $transactionData = wp_parse_args($transactionData, $transactionDefaults);

        $createdAt = Arr::get($transactionData, 'created_at', DateTime::now()->format('Y-m-d H:i:s'));

        // Let's create the order item first
        $variation = $subscriptionModel->variation;
        $product = $subscriptionModel->product;

        $parentOrderItem = OrderItem::query()
            ->where('order_id', $parentOrder->id)
            ->where('payment_type', Status::ORDER_TYPE_SUBSCRIPTION)
            ->first();

        $taxTotal = Arr::get($transactionData, 'tax_total', 0);
        if (!$taxTotal && $subscriptionModel->recurring_tax_total) {
            $taxTotal = $subscriptionModel->recurring_tax_total;
        }
        
        if (!$taxTotal && $parentOrder->tax_behavior == 2 && $parentOrderItem) {
            $taxTotal = (int) Arr::get($parentOrderItem->other_info, 'recurring_tax', 0);
        }

        $subtotal = $transactionData['total'];
        if ($taxTotal) {
            $subtotal = $transactionData['total'] - $taxTotal;
        }

        $orderItem = [
            'post_id' => $subscriptionModel->product_id,
            'object_id' => $subscriptionModel->variation_id,
            'payment_type' => Status::ORDER_TYPE_SUBSCRIPTION,
            'post_title' => $product && $product->post_title ? $product->post_title : $subscriptionModel->item_name,
            'title' => $product && $variation ? $variation->variation_title : '',
            'quantity' => 1,
            'fulfillment_type' => $parentOrderItem ? $parentOrderItem->fulfillment_type : 'digital',
            'unit_price' => $subtotal,
            'subtotal' => $subtotal,
            'tax_amount' => $taxTotal,
            'line_total' => $transactionData['total'],
            'line_meta' => [],
            'other_info' => []
        ];

        $bundleItemIds = Arr::get($parentOrderItem->line_meta, 'bundle_item_ids', []);

        $isBundleOrder = false;
        if ($bundleItemIds) {
            $isBundleOrder = true;
            $orderItem['line_meta'] = array_merge(
                $orderItem['line_meta'],
                [
                    'bundle_item_ids' => $bundleItemIds
                ]
            );
        }

        $fulfillmentType = $orderItem['fulfillment_type'];

        // Let's create the order first
        $childOrderData = [
            'parent_id' => $parentOrder->id,
            'fulfillment_type' => $fulfillmentType,
            'status' => $fulfillmentType === 'physical' ? Status::ORDER_PROCESSING : Status::ORDER_COMPLETED,
            'type' => Status::ORDER_TYPE_RENEWAL,
            'mode' => $transactionData['payment_mode'],
            'shipping_status' => $fulfillmentType === 'physical' ? Status::SHIPPING_UNSHIPPED : '',
            'customer_id' => $subscriptionModel->customer_id,
            'payment_method' => $transactionData['payment_method'],
            'payment_status' => $transactionData['status'] === Status::TRANSACTION_SUCCEEDED ? Status::PAYMENT_PAID : Status::PAYMENT_PENDING,
            'currency' => $transactionData['currency'],
            'tax_behavior' => $parentOrder->tax_behavior,
            'subtotal' => $subtotal,
            'tax_total' => $taxTotal,
            'total_amount' => $transactionData['total'],
            'total_paid' => $transactionData['status'] === Status::TRANSACTION_SUCCEEDED ? $transactionData['total'] : 0,
            'completed_at' => DateTime::now()->format('Y-m-d H:i:s'),
            'created_at' => $createdAt,
            'config' => []
        ];

        $childOrder = Order::query()->create($childOrderData);

        if (!$childOrder) {
            return new \WP_Error('order_creation_failed', __('Failed to create child order for the subscription renewal.', 'fluent-cart'));
        }

        $billingAddress = $parentOrder->billing_address;
        $shippingAddress = $parentOrder->shipping_address;

        $customer = $parentOrder->customer;

        $fullName = '';
        $email = '';
        $firstName = '';
        $lastName = '';
        if ($customer) {
            $fullName = $customer->first_name . ' ' . $customer->last_name;
            $email = $customer->email;
            $firstName = $customer->first_name;
            $lastName = $customer->last_name;
        }

        $billingAddressData = $billingAddress ? [
            'type' => 'billing',
            'full_name' => $fullName,
            'address_1' => $billingAddress->address_1,
            'address_2' => $billingAddress->address_2,
            'city' => $billingAddress->city,
            'state' => $billingAddress->state,
            'postcode' => $billingAddress->postcode,
            'country' => $billingAddress->country,
            'email' => $email,
            'first_name' => $firstName,
            'last_name' => $lastName
        ] : [];

        $shippingAddressData = $shippingAddress ? [
            'type' => 'shipping',
            'full_name' => $fullName,
            'address_1' => $shippingAddress->address_1,
            'address_2' => $shippingAddress->address_2,
            'city' => $shippingAddress->city,
            'state' => $shippingAddress->state,
            'postcode' => $shippingAddress->postcode,
            'country' => $shippingAddress->country,
            'email' => $email,
            'first_name' => $firstName,
            'last_name' => $lastName
        ] : [];

        \FluentCart\App\Helpers\AddressHelper::insertOrderAddresses(
            $childOrder->id,
            $billingAddressData,
            $shippingAddressData
        );

        // Copy tax ID meta from parent order if exists
        $parentTaxId = $parentOrder->getMeta('tax_id', '');
        if ($parentTaxId) {
            $childOrder->updateMeta('tax_id', $parentTaxId);
        }

        // Copy order tax rates from parent order
        $parentTaxRates = $parentOrder->orderTaxRates;
        foreach ($parentTaxRates as $taxRate) {
            OrderTaxRate::query()->create([
                'order_id'    => $childOrder->id,
                'tax_rate_id' => $taxRate->tax_rate_id,
                'shipping_tax' => $taxRate->shipping_tax,
                'order_tax'   => $taxRate->order_tax,
                'total_tax'   => $taxRate->total_tax,
                'meta'        => $taxRate->meta,
            ]);
        }

        //  Create Order Item
        $orderItem['order_id'] = $childOrder->id;
        $orderItem['created_at'] = $createdAt;
        OrderItem::query()->create($orderItem);

        // let's create the transaction
        $transactionData['order_id'] = $childOrder->id;

        $createdTransaction = OrderTransaction::query()->create($transactionData);

        $subscriptionModel = self::syncSubscriptionStates($subscriptionModel, $subscriptionUpdateArgs);

        (new SubscriptionRenewed($subscriptionModel, $childOrder, $parentOrder, $childOrder->customer))->dispatch();

        return $createdTransaction;
    }

    /**
     * @param $subscriptionModel
     * @param $subscriptionUpdateArgs
     *      - next_billing_date - You must provide this if you want to update the next billing date.
     * *    - Accepts all other filliable attributes of the Subscription model.
     * @return mixed
     */
    public static function syncSubscriptionStates(Subscription $subscriptionModel, $subscriptionUpdateArgs = [])
    {
        $billsCount = OrderTransaction::query()
            ->where('subscription_id', $subscriptionModel->id)
            ->where('transaction_type', Status::TRANSACTION_TYPE_CHARGE)
            ->where('status', Status::TRANSACTION_SUCCEEDED)
            ->where('total', '>', 0)
            ->count();


        $earlyPaymentHistory = $subscriptionModel->getMeta('early_payment_history', []);
        foreach ($earlyPaymentHistory as $earlyPayment) {
            $paidCount = (int) Arr::get($earlyPayment, 'count', 1);
            if ($paidCount > 1) {
                $billsCount += ($paidCount - 1);
            }
        }

        $subscriptionUpdateArgs['bill_count'] = $billsCount;
        $billTimes = $subscriptionModel->bill_times;
        $oldStatus = $subscriptionModel->status;

        $subscriptionUpdateArgs['bill_count'] = $billsCount;
        $isEot = $billTimes > 0 && $billsCount >= $billTimes;

        if ($isEot) {
            $subscriptionUpdateArgs['status'] = 'completed';
            $subscriptionUpdateArgs['next_billing_date'] = NULL;
            $subscriptionUpdateArgs['canceled_at'] = NULL;
        } else if (!$subscriptionModel->next_billing_date && empty($subscriptionUpdateArgs['next_billing_date'])) {
            $subscriptionUpdateArgs['next_billing_date'] = $subscriptionModel->guessNextBillingDate();
        }

        if (Arr::get($subscriptionUpdateArgs, 'status') === Status::SUBSCRIPTION_ACTIVE) {
            $subscriptionUpdateArgs['recurring_total'] = Arr::get($subscriptionUpdateArgs, 'recurring_total', $subscriptionModel->recurring_total);
        }

        $givenSubscriptionStatus = Arr::get($subscriptionUpdateArgs, 'status');
        if ($givenSubscriptionStatus === Status::SUBSCRIPTION_CANCELED && empty($subscriptionUpdateArgs['canceled_at'])) {
            $subscriptionUpdateArgs['canceled_at'] = gmdate('Y-m-d H:i:s');
        }

        $subscriptionModel->fill($subscriptionUpdateArgs);
        $dirtyData = $subscriptionModel->getDirty();
        $subscriptionModel->save();

        $meta = array_filter(Arr::get($subscriptionUpdateArgs, 'meta', []));

        foreach ($meta as $key => $value) {
            $subscriptionModel->updateMeta($key, $value);
        }

        if ($oldStatus === $subscriptionModel->status) {
            if ($dirtyData) {
                do_action('fluent_cart/subscription/data_updated', [
                    'subscription' => $subscriptionModel,
                    'updated_data' => $dirtyData
                ]);
            }

            return $subscriptionModel; // No change in status
        }

        if ($isEot) {
            (new SubscriptionEOT($subscriptionModel, $subscriptionModel->order))->dispatch();
        }

        do_action('fluent_cart/payments/subscription_status_changed', [
            'subscription' => $subscriptionModel,
            'order' => $subscriptionModel->order,
            'customer' => $subscriptionModel->customer,
            'old_status' => $oldStatus,
            'new_status' => $subscriptionModel->status
        ]);

        /**
         * lists of hooks for this action
         * fluent_cart/payments/subscription_canceled
         * fluent_cart/payments/subscription_active
         * fluent_cart/payments/subscription_paused
         * fluent_cart/payments/subscription_expired
         * fluent_cart/payments/subscription_failing
         * fluent_cart/payments/subscription_expiring
         * fluent_cart/payments/subscription_completed
         **/
        do_action('fluent_cart/payments/subscription_' . $subscriptionModel->status, [
            'subscription' => $subscriptionModel,
            'order' => $subscriptionModel->order,
            'customer' => $subscriptionModel->customer,
            'old_status' => $oldStatus,
            'new_status' => $subscriptionModel->status
        ]);

        // note: we needed this event, currently being used in integrations
        if ($subscriptionModel->status === Status::SUBSCRIPTION_EXPIRED) {
            $subscriptionModel->updateMeta('validity_expired_at', DateTime::now()->format('Y-m-d H:i:s'));
            (new SubscriptionValidityExpired($subscriptionModel,$subscriptionModel->order,$subscriptionModel->customer))->dispatch();
        }

        return $subscriptionModel;
    }


    /**
     *
     * Use this method when you are reactivating a expired subscription manually by creating order, transaction etc.
     * Make sure you already handle your transaction statuses!
     *
     * @param \FluentCart\App\Models\Subscription $subscriptionModel
     * @param \FluentCart\App\Models\OrderTransaction $transaction
     * @param $args
     * @return mixed
     */
    public static function recordManualRenewal(Subscription $subscriptionModel, OrderTransaction $transaction, $args = [])
    {
        $renewalOrder = $transaction->order;

        $orderUpdateData = [
            'status' => $renewalOrder->fulfillment_type === 'physical' ? Status::ORDER_PROCESSING : Status::ORDER_COMPLETED,
            'type' => Status::ORDER_TYPE_RENEWAL,
            'payment_method' => $transaction->payment_method,
            'payment_status' => Status::PAYMENT_PAID,
            'total_paid' => $transaction->total,
            'completed_at' => DateTime::now()->format('Y-m-d H:i:s')
        ];

        $renewalOrder->fill($orderUpdateData);
        $renewalOrder->save();

        if ($billingInfo = Arr::get($args, 'billing_info', [])) {
            $subscriptionModel->updateMeta('active_payment_method', $billingInfo);
        }

        $updateData = wp_parse_args(Arr::get($args, 'subscription_args', []), [
            'status' => Status::SUBSCRIPTION_ACTIVE,
            'current_payment_method' => $transaction->payment_method,
        ]);

        $subscriptionModel = self::syncSubscriptionStates($subscriptionModel, $updateData);

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

        if ($transaction->total > 0) {
            (new SubscriptionRenewed($subscriptionModel, $renewalOrder, $subscriptionModel->order, $renewalOrder->customer))->dispatch();
        }

        return $subscriptionModel;
    }
}

```
