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

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

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