PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.0
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
fluent-cart / app / Modules / PaymentMethods / StripeGateway / Webhook / Webhook.php

Webhook.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.0, at app/Modules/PaymentMethods/StripeGateway/Webhook/Webhook.php

330 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\PaymentMethods\StripeGateway\Webhook;
4
5 use FluentCart\App\Helpers\Status;
6 use FluentCart\App\Helpers\CurrenciesHelper;
7 use FluentCart\App\Models\Order;
8 use FluentCart\App\Models\OrderTransaction;
9 use FluentCart\App\Models\Subscription;
10 use FluentCart\App\Modules\PaymentMethods\StripeGateway\API\API;
11 use FluentCart\App\Modules\PaymentMethods\StripeGateway\Confirmations;
12 use FluentCart\App\Modules\PaymentMethods\StripeGateway\StripeHelper;
13 use FluentCart\App\Modules\Subscriptions\Services\SubscriptionService;
14 use FluentCart\Framework\Support\Arr;
15
16 class Webhook
17 {
18 const WEBHOOK_ENDPOINT = '?fluent-cart=fct_payment_listener_ipn&method=stripe';
19
20 public static function getURL(): string
21 {
22 return trailingslashit(site_url()) . self::WEBHOOK_ENDPOINT;
23 }
24
25 public static function getEvents(): array
26 {
27 return [
28 'checkout.session.completed',
29 'charge.refunded',
30 'charge.refund.updated',
31 'charge.succeeded',
32 'invoice.paid',
33 'customer.subscription.deleted',
34 'customer.subscription.updated',
35 'invoice.payment_failed',
36 'setup_intent.succeeded'
37 ];
38 }
39
40 public static function webhookInstruction(): array
41 {
42 $events = 'checkout.session.completed%2Ccharge.refunded%2Ccharge.refund.updated%2Ccharge.succeeded%2Cinvoice.paid%2Ccustomer.subscription.deleted%2Ccustomer.subscription.updated%2Cinvoice.payment_failed%2Ccharge.captured%2Ccharge.dispute.closed%2Ccharge.dispute.created%2Cinvoice_payment.paid%2Cpayment_intent.succeeded%2Csetup_intent.succeeded';
43
44 $svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z"></path></svg>';
45
46 /* translators: %1$s: "Add endpoint" link with icon */
47 $step = fn($class, $url) => \sprintf(
48 '<p class="%s">%s</p>',
49 $class,
50 \sprintf(
51 __('Click %1$s and paste the webhook URL above', 'fluent-cart'),
52 \sprintf('<a href="%s" target="_blank">%s %s</a>', $url, __('Add endpoint', 'fluent-cart'), $svg)
53 )
54 );
55
56 return [
57 'title' => __('Webhook URL', 'fluent-cart'),
58 'webhook_url' => static::getURL(),
59 'description' => __('You should configure your Stripe webhooks to get all updates of your payments remotely.', 'fluent-cart'),
60 'steps' => [
61 'title' => __('How to configure?', 'fluent-cart'),
62 'list' => [
63 'live' => [
64 __('In your Stripe Dashboard, go to Developers → Webhooks', 'fluent-cart'),
65 $step('fct_hide_on_test', \sprintf('https://dashboard.stripe.com/webhooks/create?events=%s', $events)),
66 ],
67 'test' => [
68 __('In your Stripe Dashboard, go to Developers → Webhooks', 'fluent-cart'),
69 $step('fct_hide_on_live', \sprintf('https://dashboard.stripe.com/test/webhooks/create?events=%s', $events)),
70 ],
71 ],
72 ],
73 'events' => [
74 'title' => __('Select these events', 'fluent-cart'),
75 'list' => [
76 'checkout.session.completed',
77 'charge.refunded',
78 'charge.refund.updated',
79 'charge.succeeded',
80 'invoice.paid',
81 'invoice.payment_failed',
82 'customer.subscription.deleted',
83 'customer.subscription.updated',
84 'setup_intent.succeeded',
85 ],
86 ],
87 ];
88 }
89
90 public function processAndInsertOrderByEvent($event)
91 {
92 $eventType = $event->type;
93
94 $metaDataEvents = [
95 'invoice.paid', // Reviewed for subscription cycle
96 'charge.refunded', // reviewed
97 'charge.succeeded', // reviewed
98 'charge.dispute.created',
99 'charge.dispute.closed',
100 'checkout.session.completed',
101 'customer.subscription.deleted',
102 'customer.subscription.updated',
103 'setup_intent.succeeded', // recovers zero-payable system-subscription vaulting if the AJAX confirm is lost
104 ];
105
106 if (!in_array($eventType, $metaDataEvents)) {
107 return false;
108 }
109
110 $vendorDataObject = $event->data->object;
111
112 if ($eventType === 'setup_intent.succeeded') {
113 $setupIntentId = Arr::get((array)$vendorDataObject, 'id');
114 if ($setupIntentId) {
115 $result = (new Confirmations())->confirmSetupIntent($setupIntentId);
116 if (!is_wp_error($result)) {
117 wp_send_json([
118 'message' => 'Setup intent confirmed successfully.',
119 ], 200);
120 }
121 }
122
123 return false;
124 }
125
126 if ($eventType == 'invoice.paid') {
127 //check if subscription billing_cycle invoice paid or failed
128 $isSubscriptionCycle = $vendorDataObject->billing_reason === 'subscription_cycle';
129 if ($isSubscriptionCycle) {
130 if ($eventType === 'invoice.paid') {
131 $vendorDataObject = (new API())->getStripeObject('invoices/' . $vendorDataObject->id, ['expand' => ['payment_intent']]);
132 $createdOrder = $this->processSubscriptionRenewal($vendorDataObject);
133 if ($createdOrder) {
134 wp_send_json([
135 'message' => 'Subscription renewal processed successfully. Order ID: ' . $createdOrder->id,
136 ], 200);
137 }
138 }
139
140 return false;
141 }
142 }
143
144 if ($eventType === 'charge.refunded' || $eventType === 'charge.succeeded') {
145 $paymentIntent = $vendorDataObject->payment_intent;
146 $orderTransaction = OrderTransaction::query()->where('vendor_charge_id', $paymentIntent)
147 ->where('transaction_type', 'charge')
148 ->first();
149
150 if (!$orderTransaction) {
151 $orderTransaction = apply_filters('fluent_cart/stripe/fallback_order_transaction', null, $vendorDataObject);
152 if (!$orderTransaction || $orderTransaction instanceof OrderTransaction) {
153 $orderTransaction = null;
154 }
155 }
156
157 if ($orderTransaction) {
158 $order = Order::where('id', $orderTransaction->order_id)->first();
159 $order->current_transaction = $orderTransaction;
160 return $order;
161 }
162 }
163
164 if ($eventType === 'customer.subscription.deleted' || $eventType === 'customer.subscription.updated') {
165
166 $vendorSubscriptionId = $vendorDataObject->id;
167
168 $subscription = Subscription::query()
169 ->where('vendor_subscription_id', $vendorSubscriptionId)
170 ->first();
171
172 if ($subscription) {
173 $order = Order::where('id', $subscription->parent_order_id)->first();
174 if ($order) {
175 $order->current_subscription = $subscription;
176 }
177 return $order;
178 }
179 }
180
181 if ($eventType === 'charge.dispute.created' || $eventType === 'charge.dispute.closed') {
182 $paymentIntent = $vendorDataObject->payment_intent;
183 $orderTransaction = OrderTransaction::query()->where('vendor_charge_id', $paymentIntent)
184 ->first();
185
186 if ($orderTransaction) {
187 return $orderTransaction->order;
188 }
189 return null;
190 }
191
192 // Handle checkout.session.completed for hosted checkout
193 if ($eventType === 'checkout.session.completed') {
194 $sessionId = $vendorDataObject->id;
195 return StripeHelper::validateBySession($sessionId);
196 }
197
198 $metaData = (array)$vendorDataObject->metadata;
199 $orderHash = Arr::get($metaData, 'fct_ref_id', false);
200
201 if ($orderHash) {
202 return Order::query()->where('uuid', $orderHash)->first();
203 }
204
205 return null;
206 }
207
208 public function processSubscriptionRenewal($vendorInvoiceObject)
209 {
210 $subscription = null;
211 $parentOrder = null;
212
213 $vendorSubscriptionId = Arr::get($vendorInvoiceObject, 'subscription', null)
214 ?: (Arr::get($vendorInvoiceObject, 'parent.subscription_details.subscription', null) ?? null);
215
216 if ($vendorSubscriptionId) {
217 $subscription = Subscription::query()->where('vendor_subscription_id', $vendorSubscriptionId)
218 ->orderBy('id', 'DESC')
219 ->first();
220 }
221
222 if ($subscription) {
223 $parentOrder = Order::query()->where('id', $subscription->parent_order_id)->first();
224 }
225
226 if (!$parentOrder) {
227 // let's try to find from the meta ref id
228 $refId = Arr::get($vendorInvoiceObject, 'subscription_details.metadata.fct_ref_id', null);
229 if ($refId) {
230 $parentOrder = Order::query()->where('uuid', $refId)->first();
231 }
232 }
233
234 if ($parentOrder && !$subscription) {
235 $subscription = Subscription::query()
236 ->where('parent_order_id', $parentOrder->id)
237 ->orderBy('id', 'DESC')
238 ->first();
239 }
240
241 if (!$parentOrder || !$subscription || $subscription->current_payment_method !== 'stripe') {
242 fluent_cart_error_log('Stripe Webhook Error: Subscription Renewal - Order or Subscription not found.', 'Vendor Subscription ID: ' . $vendorSubscriptionId);
243 return false; // this is not our order
244 };
245 $paymentIntent = Arr::get($vendorInvoiceObject, 'payment_intent', null);
246 if (is_array($paymentIntent)) {
247 $paymentIntentId = Arr::get($paymentIntent, 'id', null);
248 } else {
249 $paymentIntentId = $paymentIntent;
250 }
251
252 if ($paymentIntent) {
253 $alreadyRecorded = OrderTransaction::query()
254 ->where('subscription_id', $subscription->id)
255 ->where('vendor_charge_id', $paymentIntentId)
256 ->exists();
257
258 if ($alreadyRecorded) {
259 return null; // already recorded
260 }
261 }
262
263 $amountPaid = Arr::get($vendorInvoiceObject, 'amount_paid', 0);
264 $chargeCurrency = Arr::get($vendorInvoiceObject, 'currency', null);
265 if ($chargeCurrency && CurrenciesHelper::isZeroDecimal($chargeCurrency)) {
266 $amountPaid = $amountPaid * 100;
267 }
268
269 $transactionData = [
270 'payment_method' => 'stripe',
271 'total' => $amountPaid,
272 'vendor_charge_id' => $paymentIntentId
273 ];
274
275 $paymentIntent = (new API())->getStripeObject('payment_intents/' . $paymentIntentId, [
276 'expand' => ['latest_charge']
277 ], $parentOrder->mode);
278
279 if (!is_wp_error($paymentIntent)) {
280 $transactionData['card_last_4'] = Arr::get($paymentIntent, 'latest_charge.payment_method_details.card.last4', '');
281 $transactionData['card_brand'] = (string)Arr::get($paymentIntent, 'latest_charge.payment_method_details.card.brand', '');
282 $transactionData['payment_method_type'] = (string)Arr::get($paymentIntent, 'latest_charge.payment_method_details.type', '');
283 } else {
284 $activePaymentMethod = $subscription->getMeta('active_payment_method', []);
285 if (!$activePaymentMethod || !is_array($activePaymentMethod)) {
286 $activePaymentMethod = [];
287 }
288 if ($activePaymentMethod) {
289 $transactionData['card_last_4'] = Arr::get($activePaymentMethod, 'details.last_4');
290 $transactionData['card_brand'] = (string)Arr::get($activePaymentMethod, 'details.brand');
291 $transactionData['payment_method_type'] = (string)Arr::get($activePaymentMethod, 'details.type');
292 }
293 }
294
295 $subscriptionUpdateData = array_filter([
296 'current_payment_method' => 'stripe'
297 ]);
298
299 $stripeSubscription = (new API())->getStripeObject('subscriptions/' . $vendorSubscriptionId, [
300 'expand' => ['latest_invoice']
301 ], $parentOrder->mode);
302
303 if (!is_wp_error($stripeSubscription)) {
304 $subscriptionUpdateData = StripeHelper::getSubscriptionUpdateData($stripeSubscription, $subscription);
305 }
306
307 $createdTransaction = SubscriptionService::recordRenewalPayment($transactionData, $subscription, $subscriptionUpdateData);
308
309 $subscription = Subscription::query()->find($subscription->id);
310
311 if ($subscription && $subscription->status === Status::SUBSCRIPTION_COMPLETED) {
312 if (!is_wp_error($stripeSubscription)) {
313 if ($stripeSubscription['status'] === 'active') {
314 $deleted = (new API)->deleteStripeObject('subscriptions/' . $vendorSubscriptionId, [], $parentOrder->mode);
315 if (is_wp_error($deleted)) {
316 fluent_cart_error_log('Stripe Subscription Deletion Error. Subscription ID: ' . $subscription->id, $deleted->get_error_message());
317 }
318 }
319 }
320 }
321
322 if ($createdTransaction) {
323 return $createdTransaction->order;
324 }
325
326 return null;
327 }
328
329 }
330