subscriptions = new SubscriptionsManager(); } /** * @throws \Exception */ public function switchPayMethod($data, $subscriptionId) { if (!$this->validateRequest($data, $subscriptionId)) { throw new \Exception(__('Invalid request', 'fluent-cart')); } $subscriptionModel = Subscription::query()->where('id', $subscriptionId)->first(); $currentPaymentMethod = sanitize_text_field(Arr::get($data, 'currentPaymentMethod')); $pm = Arr::get($data, 'vendorPaymentMethod'); $verificationStatus = sanitize_text_field(Arr::get($data, 'verification_status', '')); $customerId = sanitize_text_field(Arr::get($data, 'customer_id')); if (!$customerId) { // this is part of preventing duplicating customer if ('stripe' === $subscriptionModel->current_payment_method) { $customerId = $subscriptionModel->vendor_customer_id; } if (!$customerId) { $customerId = $this->subscriptions->getOrCreateStripeCustomer($pm); } } $paymentMethodId = Arr::get($pm, 'id'); if ('verify' === $verificationStatus) { $this->subscriptions::verifyPaymentMethod($paymentMethodId, $customerId, true); } $this->attachPaymentMethodToCustomer($customerId, $paymentMethodId); $order = Order::query()->where('id', $subscriptionModel->parent_order_id)->first(); $variation = ProductVariation::query()->findOrFail($subscriptionModel->variation_id); $processedSubscriptionItem = $this->getSubscriptionItem($subscriptionModel, $variation); $data = wp_parse_args($processedSubscriptionItem, [ 'order_id' => $subscriptionModel->parent_order_id, 'product_id' => $subscriptionModel->product_id, 'variation_id' => $subscriptionModel->variation_id, 'billing_interval' => $subscriptionModel->billing_interval, 'recurring_total' => $subscriptionModel->recurring_total, 'currency' => $subscriptionModel->order->currency, 'trial_days' => (int)$subscriptionModel->trial_days, 'interval_count' => 1 // per month / year / week ]); $plan = Plan::getStripePricing($data); if (is_wp_error($plan)) { $this->sendError($plan->get_error_message()); } $newSub = $this->createStripeSubscription($customerId, $paymentMethodId, $plan, $processedSubscriptionItem, $order); if (is_wp_error($newSub)) { $this->sendError($newSub->get_error_message()); } $newSubStatus = StripeHelper::transformSubscriptionStatus($newSub); if ($newSubStatus == 'incomplete') { wp_send_json([ 'status' => 'failed', 'message' => __('Could not switch payment method, please try again later', 'fluent-cart'), 'data' => Arr::get($newSub, 'id') ], 200); } $oldVendorSubId = $subscriptionModel->vendor_subscription_id; $oldVendorCusId = $subscriptionModel->vendor_customer_id; $oldData = [ 'vendor_subscription_id' => $subscriptionModel->vendor_subscription_id, 'vendor_customer_id' => $subscriptionModel->vendor_customer_id, 'vendor_plan_id' => $subscriptionModel->vendor_plan_id, 'old_payment_gateway' => $currentPaymentMethod, 'payment_source' => SubscriptionMeta::query() ->where('subscription_id', $subscriptionModel->id) ->where('meta_key', 'active_payment_method') ->value('meta_value'), 'reason' => 'switch_payment_method', 'canceled_at' => DateTime::gmtNow(), ]; $this->updateSubscription($subscriptionId, $newSub, $plan, $customerId); $this->updateBillingInfo($subscriptionId, $pm); $this->handleOldSubscription($oldData, $newSub, $subscriptionModel); wp_send_json([ 'status' => 'success', 'message' => __('Payment Method updated successfully', 'fluent-cart'), 'data' => Arr::get($newSub, 'id') ], 200); } private function validateRequest($data, $subscriptionId): bool { $currentPaymentMethod = sanitize_text_field(Arr::get($data, 'currentPaymentMethod')); if (!$currentPaymentMethod || !$subscriptionId) { return false; } return true; } private function attachPaymentMethodToCustomer($customerId, $paymentMethodId) { // check if payment method is already attached $existingPaymentMethods = (new API())->getStripeObject('customers/' . $customerId . '/payment_methods'); if (is_wp_error($existingPaymentMethods)) { $this->sendError($existingPaymentMethods->get_error_message()); } foreach ($existingPaymentMethods['data'] as $method) { if (Arr::get($method, 'id') === $paymentMethodId) { return; } } $response = (new API())->createStripeObject('payment_methods/' . $paymentMethodId . '/attach', [ 'customer' => $customerId ]); if (is_wp_error($response)) { $this->sendError($response->get_error_message()); } } /** * @throws \Exception */ private function createStripeSubscription($customerId, $paymentMethodId, $plan, $processedSubscriptionItem, $order) { $stripeSubscriptionData = [ 'customer' => $customerId, 'payment_behavior' => 'default_incomplete', 'payment_settings' => [ 'save_default_payment_method' => 'on_subscription' ], 'items' => [ [ 'plan' => Arr::get($plan, 'id'), 'quantity' => 1, ] ], 'default_payment_method' => $paymentMethodId, 'expand' => [ 'latest_invoice.confirmation_secret', 'pending_setup_intent' ], 'metadata' => [ 'fct_ref_id' => $order->uuid, 'email' => $order->customer->email, 'name' => $order->full_name, 'order_reference' => 'fct_order_id_' . $order->id, ] ]; if (!empty($processedSubscriptionItem['expire_at'])) { // $stripeSubscriptionData['cancel_at'] = $processedSubscriptionItem['expire_at']; } if (!empty($processedSubscriptionItem['trial_end'])) { $stripeSubscriptionData['trial_end'] = $processedSubscriptionItem['trial_end']; } $newSub = (new API())->createStripeObject('subscriptions', $stripeSubscriptionData); if (is_wp_error($newSub)) { throw new \Exception(esc_html($newSub->get_error_message())); } return $newSub; } private function updateSubscription($subscriptionId, $newSub, $plan, $customerId) { $subscriptionModel = Subscription::query()->where('id', $subscriptionId)->first(); $subscriptionModel->update([ 'vendor_subscription_id' => Arr::get($newSub, 'id'), 'vendor_plan_id' => Arr::get($newSub, 'id'), 'current_payment_method' => 'stripe', 'vendor_customer_id' => $customerId, 'status' => StripeHelper::transformSubscriptionStatus($newSub), ]); $subscriptionModel->mergeConfig(['is_trial_days_simulated' => 'yes']); } private function updateBillingInfo($subscriptionId, $pm) { $billingInfo = PaymentHelper::parsePaymentMethodDetails('stripe', $pm); SubscriptionMeta::updateOrCreate([ 'subscription_id' => $subscriptionId, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_key' => 'active_payment_method' ], [ //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'meta_value' => json_encode($billingInfo) ]); } private function handleOldSubscription($oldData, $newSub, $subscriptionModel) { SubscriptionsManager::addOldSubscriptionMeta($subscriptionModel->id, $oldData); $gateway = App::gateway(Arr::get($oldData, 'old_payment_gateway')); if ($gateway && $gateway->subscriptions) { $gateway->subscriptions->cancel(Arr::get($oldData, 'vendor_subscription_id'), [ 'mode' => $subscriptionModel->order->mode ]); } } public function getSubscriptionItem($subscription, $variation) { $trialDays = 0; $nextBillingTimestamp = null; // trial days is the difference between the next billing date and the current date in days $nextBillingDate = Arr::get($subscription, 'next_billing_date'); if ($nextBillingDate) { $now = DateTime::gmtNow()->getTimestamp(); $nextBillingTimestamp = DateTime::anyTimeToGmt($nextBillingDate)->getTimestamp(); if ($nextBillingTimestamp <= $now) { $trialDays = 0; } else { $trialDays = ceil(($nextBillingTimestamp - $now) / 86400); } } // there is loophole for getting 1 day trial and continue the subscription in loop, so we need to check for that $trialDays = SubscriptionHelper::checkTrailDaysLoopHole($subscription, $trialDays); $billTimes = Arr::get($subscription, 'bill_times'); $billCount = $subscription->calculateBillCount(); if ($billTimes && $billCount) { $billTimes = $billTimes - $billCount; } else { $billTimes = 0; } $processedSubscriptionItem = [ 'billing_interval' => Arr::get($subscription, 'billing_interval'), 'recurring_amount' => intval(Arr::get($subscription, 'recurring_amount')), 'trial_days' => $trialDays, 'bill_times' => $billTimes, 'product_id' => Arr::get($subscription, 'product_id'), 'parent_order_id' => Arr::get($subscription, 'parent_order_id'), 'item_name' => Arr::get($subscription, 'item_name'), 'expire_at' => null, ]; if ($trialDays > 0) { $processedSubscriptionItem['trial_end'] = $nextBillingTimestamp; } return $processedSubscriptionItem; } public function sendError($message, $code = 423) { wp_send_json([ 'status' => 'failed', 'message' => $message ], $code); } }