PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.6
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.6
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
← All changes | app/Modules/PaymentMethods/StripeGateway/Webhook/Webhook.php +48 -3 1.6.1 → 1.6.6 View file →
@@ -10,8 +10,9 @@
10 10 use FluentCart\App\Modules\PaymentMethods\StripeGateway\API\API;
11 11 use FluentCart\App\Modules\PaymentMethods\StripeGateway\Confirmations;
12 12 use FluentCart\App\Modules\PaymentMethods\StripeGateway\StripeHelper;
13 13 use FluentCart\App\Modules\Subscriptions\Services\SubscriptionService;
14 +use FluentCart\App\Services\DateTime\DateTime;
14 15 use FluentCart\Framework\Support\Arr;
15 16
16 17 class Webhook
17 18 {
@@ -103,8 +104,9 @@
103 104
104 105 public function processAndInsertOrderByEvent($event)
105 106 {
106 107 $eventType = $event->type;
108 + $eventLivemode = isset($event->livemode) ? (bool)$event->livemode : null;
107 109
108 110 $this->unresolvedReason = '';
109 111
110 112 $metaDataEvents = [
@@ -116,8 +118,9 @@
116 118 'checkout.session.completed',
117 119 'customer.subscription.deleted',
118 120 'customer.subscription.updated',
119 121 'setup_intent.succeeded', // recovers zero-payable system-subscription vaulting if the AJAX confirm is lost
122 + 'invoice.payment_failed',
120 123 ];
121 124
122 125 if (!in_array($eventType, $metaDataEvents)) {
123 126 $this->unresolvedReason = __('Event type has no order resolver.', 'fluent-cart');
@@ -128,9 +131,9 @@
128 131
129 132 if ($eventType === 'setup_intent.succeeded') {
130 133 $setupIntentId = Arr::get((array)$vendorDataObject, 'id');
131 134 if ($setupIntentId) {
132 - $result = (new Confirmations())->confirmSetupIntent($setupIntentId);
135 + $result = (new Confirmations())->confirmSetupIntent($setupIntentId, null, StripeHelper::modeFromLivemode($eventLivemode));
133 136 if (!is_wp_error($result)) {
134 137 wp_send_json([
135 138 'message' => 'Setup intent confirmed successfully.',
136 139 ], 200);
@@ -145,9 +148,9 @@
145 148 //check if subscription billing_cycle invoice paid or failed
146 149 $isSubscriptionCycle = $vendorDataObject->billing_reason === 'subscription_cycle';
147 150 if ($isSubscriptionCycle) {
148 151 if ($eventType === 'invoice.paid') {
149 - $vendorDataObject = (new API())->getStripeObject('invoices/' . $vendorDataObject->id, ['expand' => ['payment_intent']]);
152 + $vendorDataObject = (new API())->getStripeObject('invoices/' . $vendorDataObject->id, ['expand' => ['payment_intent']], StripeHelper::modeFromLivemode($eventLivemode));
150 153 $createdOrder = $this->processSubscriptionRenewal($vendorDataObject);
151 154 if ($createdOrder) {
152 155 wp_send_json([
153 156 'message' => 'Subscription renewal processed successfully. Order ID: ' . $createdOrder->id,
@@ -159,8 +162,25 @@
159 162 return false;
160 163 }
161 164 }
162 165
166 + if ($eventType === 'invoice.payment_failed') {
167 + $isSubscriptionCycle = $vendorDataObject->billing_reason === 'subscription_cycle';
168 + if ($isSubscriptionCycle) {
169 + $invoice = (new API())->getStripeObject('invoices/' . $vendorDataObject->id, [], StripeHelper::modeFromLivemode($eventLivemode));
170 + if (!is_wp_error($invoice)) {
171 + list($subscription, $parentOrder) = $this->resolveSubscriptionAndOrder($invoice);
172 +
173 + if ($subscription && $parentOrder && $subscription->current_payment_method === 'stripe') {
174 + return $parentOrder;
175 + }
176 + }
177 + }
178 +
179 + $this->unresolvedReason = __('Subscription renewal-failure invoice resolved to no order.', 'fluent-cart');
180 + return false;
181 + }
182 +
163 183 if ($eventType === 'charge.refunded' || $eventType === 'charge.succeeded') {
164 184 $paymentIntent = $vendorDataObject->payment_intent;
165 185 $orderTransaction = OrderTransaction::query()->where('vendor_charge_id', $paymentIntent)
166 186 ->where('transaction_type', 'charge')
@@ -240,9 +260,16 @@
240 260 $this->unresolvedReason = __('Event carries no reference to a local order.', 'fluent-cart');
241 261 return null;
242 262 }
243 263
244 - public function processSubscriptionRenewal($vendorInvoiceObject)
264 + /**
265 + * Resolve the local Subscription + parent Order for a Stripe invoice payload,
266 + * shared by renewal-success (invoice.paid) and renewal-failure
267 + * (invoice.payment_failed) handling.
268 + *
269 + * @return array{0: Subscription|null, 1: Order|null}
270 + */
271 + protected function resolveSubscriptionAndOrder($vendorInvoiceObject)
245 272 {
246 273 $subscription = null;
247 274 $parentOrder = null;
248 275
@@ -273,8 +300,18 @@
273 300 ->orderBy('id', 'DESC')
274 301 ->first();
275 302 }
276 303
304 + return [$subscription, $parentOrder];
305 + }
306 +
307 + public function processSubscriptionRenewal($vendorInvoiceObject)
308 + {
309 + list($subscription, $parentOrder) = $this->resolveSubscriptionAndOrder($vendorInvoiceObject);
310 +
311 + $vendorSubscriptionId = Arr::get($vendorInvoiceObject, 'subscription', null)
312 + ?: (Arr::get($vendorInvoiceObject, 'parent.subscription_details.subscription', null) ?? null);
313 +
277 314 if (!$parentOrder || !$subscription || $subscription->current_payment_method !== 'stripe') {
278 315 fluent_cart_error_log('Stripe Webhook Error: Subscription Renewal - Order or Subscription not found.', 'Vendor Subscription ID: ' . $vendorSubscriptionId);
279 316 return false; // this is not our order
280 317 };
@@ -316,8 +353,16 @@
316 353 if (!is_wp_error($paymentIntent)) {
317 354 $transactionData['card_last_4'] = Arr::get($paymentIntent, 'latest_charge.payment_method_details.card.last4', '');
318 355 $transactionData['card_brand'] = (string)Arr::get($paymentIntent, 'latest_charge.payment_method_details.card.brand', '');
319 356 $transactionData['payment_method_type'] = (string)Arr::get($paymentIntent, 'latest_charge.payment_method_details.type', '');
357 +
358 + // The charge's own `created` is the settlement moment; without it the
359 + // model hook would stamp the webhook-processing time, which drifts on
360 + // delayed deliveries.
361 + $chargeCreatedAt = (int)Arr::get($paymentIntent, 'latest_charge.created', 0);
362 + if ($chargeCreatedAt) {
363 + $transactionData['meta'] = array_merge($transactionData['meta'] ?? [], ['settled_at' => DateTime::anyTimeToGmt($chargeCreatedAt)->format('Y-m-d H:i:s')]);
364 + }
320 365 } else {
321 366 $activePaymentMethod = $subscription->getMeta('active_payment_method', []);
322 367 if (!$activePaymentMethod || !is_array($activePaymentMethod)) {
323 368 $activePaymentMethod = [];