← All changes
|
app/Modules/PaymentMethods/StripeGateway/UpdateCustomerPaymentMethod.php
+83
-1
1.5.2
→
1.6.6
View file →
| @@ -4,8 +4,9 @@ | ||
| 4 | 4 | |
| 5 | 5 | use FluentCart\App\Models\Subscription; |
| 6 | 6 | use FluentCart\App\Models\SubscriptionMeta; |
| 7 | 7 | use FluentCart\App\Modules\PaymentMethods\StripeGateway\API\API; |
| 8 | +use FluentCart\App\Modules\Subscriptions\Services\SystemChargeService; | |
| 8 | 9 | use FluentCart\App\Services\Payments\PaymentHelper; |
| 9 | 10 | use FluentCart\Framework\Support\Arr; |
| 10 | 11 | |
| 11 | 12 | class UpdateCustomerPaymentMethod |
| @@ -26,9 +27,18 @@ | ||
| 26 | 27 | $newPaymentMethod = Arr::get($data, 'newPaymentMethod'); |
| 27 | 28 | $verificationStatus = Arr::get($data, 'verification_status', ''); |
| 28 | 29 | |
| 29 | 30 | if (!$vendorSubscriptionId) { |
| 30 | - return; | |
| 31 | + // System (auto-charged) subscriptions have no vendor subscription — the | |
| 32 | + // stored token IS the subscription's payment method. Update it locally. | |
| 33 | + $localSubscription = Subscription::query()->find($subscriptionId); | |
| 34 | + | |
| 35 | + if ($localSubscription instanceof Subscription && $localSubscription->isSystem()) { | |
| 36 | + $this->updateSystemPaymentMethod($localSubscription, $newPaymentMethod, $verificationStatus); | |
| 37 | + return; | |
| 38 | + } | |
| 39 | + | |
| 40 | + throw new \Exception(esc_html__('This subscription has no vendor subscription to update.', 'fluent-cart'), 423); | |
| 31 | 41 | } |
| 32 | 42 | |
| 33 | 43 | // get customer id |
| 34 | 44 | $subscription = Subscription::query()->where('vendor_subscription_id', $vendorSubscriptionId)->first(); |
| @@ -56,8 +66,80 @@ | ||
| 56 | 66 | static::updateSubscriptionDefaultPaymentMethod($vendorSubscriptionId, $newPaymentMethodId, $newPaymentMethod, $customerId, $subscriptionId); |
| 57 | 67 | |
| 58 | 68 | } |
| 59 | 69 | |
| 70 | + | |
| 71 | + /** | |
| 72 | + * Replace the stored token of a system (auto-charged) subscription. No vendor | |
| 73 | + * subscription exists, so nothing is called on Stripe's subscriptions API: the | |
| 74 | + * new payment method is verified for off-session use (SCA mandate), attached to | |
| 75 | + * the Stripe customer, and written to active_payment_method — which the charge | |
| 76 | + * engine reads at fire time, so the next renewal (or a pending retry) uses it. | |
| 77 | + * | |
| 78 | + * @throws \Exception | |
| 79 | + */ | |
| 80 | + private function updateSystemPaymentMethod(Subscription $subscription, $newPaymentMethod, $verificationStatus) | |
| 81 | + { | |
| 82 | + $newPaymentMethodId = Arr::get($newPaymentMethod, 'id'); | |
| 83 | + $customerId = $subscription->vendor_customer_id; | |
| 84 | + | |
| 85 | + if (!$newPaymentMethodId || !$customerId) { | |
| 86 | + wp_send_json([ | |
| 87 | + 'status' => 'failed', | |
| 88 | + 'message' => __('This subscription has no saved payment profile to update.', 'fluent-cart') | |
| 89 | + ], 423); | |
| 90 | + } | |
| 91 | + | |
| 92 | + if ('verify' === $verificationStatus) { | |
| 93 | + // SCA-compliant off-session mandate (handles requires_action + rate limiting) | |
| 94 | + $this->subscriptions::verifyPaymentMethod($newPaymentMethodId, $customerId, true); | |
| 95 | + } | |
| 96 | + | |
| 97 | + $response = (new API())->createStripeObject('payment_methods/' . $newPaymentMethodId . '/attach', [ | |
| 98 | + 'customer' => $customerId | |
| 99 | + ], $subscription->order ? $subscription->order->mode : 'current'); | |
| 100 | + | |
| 101 | + if (is_wp_error($response)) { | |
| 102 | + wp_send_json([ | |
| 103 | + 'status' => 'failed', | |
| 104 | + 'message' => $response->get_error_message() | |
| 105 | + ], 423); | |
| 106 | + } | |
| 107 | + | |
| 108 | + $billingInfo = PaymentHelper::parsePaymentMethodDetails('stripe', $newPaymentMethod); | |
| 109 | + | |
| 110 | + SubscriptionMeta::updateOrCreate([ | |
| 111 | + 'subscription_id' => $subscription->id, | |
| 112 | + //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key | |
| 113 | + 'meta_key' => 'active_payment_method' | |
| 114 | + ], [ | |
| 115 | + //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value | |
| 116 | + 'meta_value' => json_encode($billingInfo) | |
| 117 | + ]); | |
| 118 | + | |
| 119 | + // The previous decline reason describes the OLD card — the portal and admin | |
| 120 | + // render it verbatim, so drop it. Retry bookkeeping and any in-flight | |
| 121 | + // processing marker survive; the next attempt reads the new token. | |
| 122 | + SystemChargeService::clearFailureState($subscription); | |
| 123 | + | |
| 124 | + $subscription->addLog( | |
| 125 | + 'Payment method updated', | |
| 126 | + __('The saved payment method for automatic renewal charges was updated by the customer.', 'fluent-cart'), | |
| 127 | + 'info' | |
| 128 | + ); | |
| 129 | + | |
| 130 | + do_action('fluent_cart/subscriptions/system_payment_method_updated', [ | |
| 131 | + 'subscription' => $subscription, | |
| 132 | + 'payment_method' => $billingInfo, | |
| 133 | + ]); | |
| 134 | + | |
| 135 | + wp_send_json([ | |
| 136 | + 'status' => 'success', | |
| 137 | + 'message' => __('Payment method updated successfully', 'fluent-cart'), | |
| 138 | + 'action' => 'none', | |
| 139 | + 'data' => $billingInfo | |
| 140 | + ], 200); | |
| 141 | + } | |
| 60 | 142 | |
| 61 | 143 | public static function updateSubscriptionDefaultPaymentMethod($vendorSubscriptionId, $newPaymentMethodId, $paymentMethod, $customerId, $subscriptionId, $customerDefault = false) |
| 62 | 144 | { |
| 63 | 145 | // attach payment method to customer and make it the default payment method for the subscription |