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

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