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 / Processor.php

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

549 lines 21.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;
4
5 use FluentCart\App\Helpers\CurrenciesHelper;
6 use FluentCart\App\Models\Cart;
7 use FluentCart\App\Modules\PaymentMethods\StripeGateway\API\API;
8 use FluentCart\App\Services\Payments\PaymentInstance;
9 use FluentCart\App\Helpers\Helper;
10 use FluentCart\Framework\Support\Arr;
11
12 class Processor
13 {
14
15 public function handleSubscription(PaymentInstance $paymentInstance, $paymentArgs)
16 {
17 $stripeSettings = new StripeSettingsBase();
18 $checkoutMode = $stripeSettings->get('checkout_mode') ?? 'onsite';
19
20 // If hosted mode, create Checkout Session for subscription
21 if ($checkoutMode === 'hosted') {
22 return $this->handleHostedSubscriptionCheckout($paymentInstance, $paymentArgs);
23 }
24
25 // Original onsite subscription flow
26 $orderType = $paymentInstance->order->type;
27 $fcCustomer = $paymentInstance->order->customer;
28 $billingAddress = $paymentInstance->order->billing_address;
29
30 $subscriptionModel = $paymentInstance->subscription;
31
32 if (!$subscriptionModel) {
33 return new \WP_Error('no_subscription', __('No subscription found.', 'fluent-cart'));
34 }
35
36 $stripeCustomer = StripeHelper::createOrGetStripeCustomer($paymentInstance->order->customer);
37
38 if (is_wp_error($stripeCustomer)) {
39 return $stripeCustomer;
40 }
41
42 $feeTotal = $orderType !== 'renewal' ? (int)$paymentInstance->order->fee_total : 0;
43 $initialAmount = (int)$subscriptionModel->signup_fee + $paymentInstance->getExtraAddonAmount() + $feeTotal;
44
45 if ($orderType == 'renewal') {
46 $stripePlan = Plan::getStripePricing([
47 'order_id' => $subscriptionModel->parent_order_id,
48 'product_id' => $subscriptionModel->product_id,
49 'variation_id' => $subscriptionModel->variation_id,
50 'billing_interval' => $subscriptionModel->billing_interval,
51 'recurring_total' => $subscriptionModel->getCurrentRenewalAmount(),
52 'currency' => $paymentInstance->order->currency,
53 'trial_days' => $subscriptionModel->getReactivationTrialDays(), // No trial for renewals
54 'interval_count' => 1 // per month / year / week
55 ]);
56
57 $initialAmount = 0;
58 } else {
59 $stripePlan = Plan::getStripePricing([
60 'order_id' => $subscriptionModel->parent_order_id,
61 'product_id' => $subscriptionModel->product_id,
62 'variation_id' => $subscriptionModel->variation_id,
63 'billing_interval' => $subscriptionModel->billing_interval,
64 'recurring_total' => $subscriptionModel->recurring_total,
65 'currency' => $paymentInstance->order->currency,
66 'trial_days' => (int)$subscriptionModel->trial_days,
67 'interval_count' => 1 // per month / year / week
68 ]);
69 }
70
71 if (is_wp_error($stripePlan)) {
72 return $stripePlan;
73 }
74
75 $stripeSubscriptionData = [
76 'customer' => Arr::get($stripeCustomer, 'id', ''),
77 'payment_behavior' => 'default_incomplete',
78 'payment_settings' => [
79 'save_default_payment_method' => 'on_subscription'
80 ],
81 'items' => [
82 [
83 'plan' => $stripePlan['id'],
84 'quantity' => $subscriptionModel->quantity ?: 1,
85 ]
86 ],
87 'expand' => [
88 'latest_invoice.confirmation_secret',
89 'pending_setup_intent'
90 ],
91 'metadata' => apply_filters('fluent_cart/payments/stripe_metadata_subscription', [
92 'fct_ref_id' => $paymentInstance->order->uuid,
93 'email' => $paymentInstance->order->customer->email,
94 'name' => $paymentInstance->order->full_name,
95 'subscription_item' => $subscriptionModel->item_name,
96 'order_reference' => 'fct_order_id_' . $paymentInstance->order->id,
97 ], [
98 'order' => $paymentInstance->order,
99 'transaction' => $paymentInstance->transaction,
100 'subscription' => $subscriptionModel
101 ]),
102 ];
103
104 if (Arr::get($stripePlan, 'trial_period_days')) {
105 $stripeSubscriptionData['trial_end'] = strtotime('+' . Arr::get($stripePlan, 'trial_period_days') . ' days');
106 }
107
108 // Maybe we have initial amount
109 if ($initialAmount) {
110 $addonPrice = Plan::getOneTimeAddonPrice([
111 'product_id' => $subscriptionModel->product_id,
112 'currency' => $paymentInstance->order->currency,
113 'amount' => (int)$initialAmount,
114 'variation_id' => $subscriptionModel->variation_id,
115 'order_id' => $subscriptionModel->parent_order_id,
116 ]);
117
118 if (is_wp_error($addonPrice)) {
119 return $addonPrice;
120 }
121
122 $stripeSubscriptionData['add_invoice_items'] = [
123 [
124 'price' => $addonPrice['id'],
125 'quantity' => 1
126 ]
127 ];
128 }
129
130 if ($expireAt = $paymentInstance->getSubscriptionCancelAtTimeStamp()) {
131 // $stripeSubscriptionData['cancel_at'] = $expireAt;
132 }
133
134 $stripeSubscription = (new API())->createStripeObject('subscriptions', $stripeSubscriptionData);
135
136 if (is_wp_error($stripeSubscription)) {
137 return $stripeSubscription;
138 }
139
140 $vendorChargeId = Arr::get($stripeSubscription, 'latest_invoice.payment_intent');
141 if (!$vendorChargeId) {
142 $vendorChargeId = Arr::get($stripeSubscription, 'pending_setup_intent.id');
143 }
144
145 if ($vendorChargeId) {
146 $paymentInstance->transaction->update(['vendor_charge_id' => $vendorChargeId]);
147 }
148
149 $vendorSubscriptionId = Arr::get($stripeSubscription, 'id');
150
151 $subscriptionModel->update([
152 'vendor_subscription_id' => $vendorSubscriptionId,
153 'vendor_customer_id' => $stripeSubscription['customer']
154 ]);
155
156 if ($stripeSubscription['pending_setup_intent'] != null) {
157 $paymentArgs['vendor_subscription_info'] = [
158 'type' => 'setup',
159 'clientSecret' => Arr::get($stripeSubscription, 'pending_setup_intent.client_secret'),
160 'trx_hash' => $paymentInstance->transaction->uuid,
161 ];
162 } else {
163 $paymentArgs['vendor_subscription_info'] = [
164 'type' => 'payment',
165 'clientSecret' => Arr::get($stripeSubscription, 'latest_invoice.confirmation_secret.client_secret')
166 ];
167 }
168
169 $customerData = [
170 'name' => $fcCustomer->first_name . ' ' . $fcCustomer->last_name,
171 'email' => $fcCustomer->email,
172 'address_1' => $billingAddress->address_1,
173 'address_2' => $billingAddress->address_2,
174 'city' => $billingAddress->city,
175 'state' => $billingAddress->state,
176 'postcode' => $billingAddress->postcode,
177 'country' => $billingAddress->country
178 ];
179
180 return [
181 'nextAction' => 'stripe',
182 'actionName' => 'custom',
183 'status' => 'success',
184 'message' => __('Order has been placed successfully', 'fluent-cart'),
185 'payment_args' => $paymentArgs,
186 'response' => $stripeSubscription,
187 'fc_customer' => $customerData
188 ];
189 }
190
191
192 /**
193 * Handle single payment for stripe (onsite or hosted)
194 *
195 * @return \WP_Error|array
196 */
197 public function handleSinglePayment(PaymentInstance $paymentInstance, $paymentArgs = [])
198 {
199 $stripeSettings = new StripeSettingsBase();
200 $checkoutMode = $stripeSettings->get('checkout_mode') ?? 'onsite';
201
202 if ($checkoutMode === 'hosted') {
203 return $this->handleHostedCheckout($paymentInstance, $paymentArgs);
204 }
205
206 // Original onsite payment flow
207 $order = $paymentInstance->order;
208 $transaction = $paymentInstance->transaction;
209 $fcCustomer = $paymentInstance->order->customer;
210 $billingAddress = $order->billing_address;
211
212 $transactionCurrency = $transaction->currency;
213 $intentAmount = (int)$transaction->total;
214
215 if ($transactionCurrency && CurrenciesHelper::isZeroDecimal($transactionCurrency)) {
216 $intentAmount = (int)($intentAmount / 100);
217 }
218
219 $intentData = [
220 'amount' => $intentAmount,
221 'currency' => $transactionCurrency,
222 'automatic_payment_methods' => ['enabled' => 'true'],
223 'metadata' => apply_filters('fluent_cart/payments/stripe_metadata_onetime', [
224 'fct_ref_id' => $order->uuid,
225 'Name' => $order->customer->full_name,
226 'Email' => $order->customer->email,
227 'order_reference' => 'fct_order_id_' . $paymentInstance->order->id,
228 ], [
229 'order' => $order,
230 'transaction' => $transaction
231 ]),
232 ];
233
234 $itemCount = 1;
235 foreach($paymentInstance->order->order_items as $item) {
236 $intentData['metadata']['item ' . $itemCount] = 'Name: ' . $item->title . ', ' . 'Qty: ' . $item->quantity . ', Price: ' . Helper::toDecimal($item->line_total, false, null, true, true, false);
237 if (count($intentData['metadata']) > 49) {
238 break;
239 }
240 $itemCount++;
241 }
242
243 if (!empty($paymentArgs['customer'])) {
244 $intentData['customer'] = $paymentArgs['customer'];
245 } else {
246 $stripeCustomer = StripeHelper::createOrGetStripeCustomer($order->customer);
247 if (is_wp_error($stripeCustomer)) {
248 return $stripeCustomer;
249 }
250 $intentData['customer'] = $stripeCustomer['id'];
251 }
252
253 if (!empty($paymentArgs['setup_future_usage'])) {
254 $intentData['setup_future_usage'] = $paymentArgs['setup_future_usage'];
255 }
256
257 $paymentArgs['public_key'] = (new StripeSettingsBase())->getPublicKey();
258
259 $intentData = apply_filters('fluent_cart/payments/stripe_onetime_intent_args', $intentData, [
260 'order' => $order,
261 'transaction' => $transaction
262 ]);
263
264 $intent = (new API())->createStripeObject('payment_intents', $intentData);
265
266 if (is_wp_error($intent)) {
267 return $intent;
268 }
269
270 $transaction->update([
271 'vendor_charge_id' => $intent['id']
272 ]);
273
274 $customerData = [
275 'name' => $fcCustomer->first_name . ' ' . $fcCustomer->last_name,
276 'email' => $fcCustomer->email,
277 'address_1' => $billingAddress->address_1,
278 'address_2' => $billingAddress->address_2,
279 'city' => $billingAddress->city,
280 'state' => $billingAddress->state,
281 'postcode' => $billingAddress->postcode,
282 'country' => $billingAddress->country
283 ];
284
285 return [
286 'status' => 'success',
287 'nextAction' => 'stripe',
288 'actionName' => 'custom',
289 'message' => __('Order has been placed successfully', 'fluent-cart'),
290 'response' => $intent,
291 'payment_args' => $paymentArgs,
292 'fc_customer' => $customerData
293 ];
294 }
295
296
297 private function handleHostedCheckout(PaymentInstance $paymentInstance, $paymentArgs = [])
298 {
299 $order = $paymentInstance->order;
300 $transaction = $paymentInstance->transaction;
301 $fcCustomer = $order->customer;
302 $billingAddress = $order->billing_address;
303
304 $transactionCurrency = $transaction->currency;
305 $chargeAmount = (int)$transaction->total;
306
307 if ($transactionCurrency && CurrenciesHelper::isZeroDecimal($transactionCurrency)) {
308 $chargeAmount = (int)($chargeAmount / 100);
309 }
310
311 // Create or get Stripe customer
312 $stripeCustomer = StripeHelper::createOrGetStripeCustomer($fcCustomer);
313 if (is_wp_error($stripeCustomer)) {
314 return $stripeCustomer;
315 }
316
317 // Use a single line item with the total amount to avoid complexity
318 // This is simpler and prevents any calculation mismatches
319 $storeName = (new \FluentCart\Api\StoreSettings())->get('store_name');
320 $lineItems = [
321 [
322 'price_data' => [
323 'currency' => strtolower($transactionCurrency),
324 'product_data' => [
325 'name' => $storeName . ' - Order #' . $order->uuid,
326 'description' => sprintf(__('Order total including all items, shipping (If any), and taxes (If any)', 'fluent-cart')),
327 ],
328 'unit_amount' => $chargeAmount,
329 ],
330 'quantity' => 1,
331 ]
332 ];
333
334 $sessionData = [
335 'customer' => $stripeCustomer['id'],
336 'client_reference_id' => $order->uuid,
337 'line_items' => $lineItems,
338 'mode' => 'payment',
339 'success_url' => Arr::get($paymentArgs, 'success_url') . '&fct_stripe_hosted=1&trx_hash=' . $transaction->uuid,
340 'cancel_url' => StripeHelper::getCancelUrl(),
341 'metadata' => [
342 'fct_ref_id' => $order->uuid,
343 'transaction_hash' => $transaction->uuid,
344 'order_reference' => 'fct_order_id_' . $order->id,
345 ],
346 ];
347
348 $itemCount = 1;
349 foreach($order->order_items as $item) {
350 $sessionData['metadata']['item ' . $itemCount] = 'Name: ' . $item->title . ', ' . 'Qty: ' . $item->quantity . ', Price: ' . Helper::toDecimal($item->line_total, false, null, true, true, false);
351 if (count($sessionData['metadata']) > 49) {
352 break;
353 }
354
355 $itemCount++;
356 }
357
358 $sessionData = apply_filters('fluent_cart/payments/stripe_checkout_session_args', $sessionData, [
359 'order' => $order,
360 'transaction' => $transaction
361 ]);
362
363 $session = (new API())->createStripeObject('checkout/sessions', $sessionData);
364
365 if (is_wp_error($session)) {
366 return $session;
367 }
368
369 $transaction->update([
370 'meta' => array_merge($transaction->meta ?? [], [
371 'session_id' => $session['id']
372 ])
373 ]);
374
375 return [
376 'status' => 'success',
377 'nextAction' => 'stripe',
378 'actionName' => 'redirect',
379 'message' => __('Redirecting to Stripe checkout...', 'fluent-cart'),
380 'response' => $session,
381 'payment_args' => array_merge($paymentArgs, [
382 'checkout_url' => $session['url'],
383 'session_id' => $session['id']
384 ])
385 ];
386 }
387
388
389 private function handleHostedSubscriptionCheckout(PaymentInstance $paymentInstance, $paymentArgs = [])
390 {
391 $order = $paymentInstance->order;
392 $transaction = $paymentInstance->transaction;
393 $subscriptionModel = $paymentInstance->subscription;
394 $fcCustomer = $order->customer;
395
396 if (!$subscriptionModel) {
397 return new \WP_Error('no_subscription', __('No subscription found.', 'fluent-cart'));
398 }
399
400 $transactionCurrency = $transaction->currency;
401 $orderType = $order->type;
402
403 // Create or get Stripe customer
404 $stripeCustomer = StripeHelper::createOrGetStripeCustomer($fcCustomer);
405 if (is_wp_error($stripeCustomer)) {
406 return $stripeCustomer;
407 }
408
409 // Get or create Stripe price/plan
410 if ($orderType == 'renewal') {
411 $stripePlan = Plan::getStripePricing([
412 'product_id' => $subscriptionModel->product_id,
413 'variation_id' => $subscriptionModel->variation_id,
414 'billing_interval' => $subscriptionModel->billing_interval,
415 'recurring_total' => $subscriptionModel->getCurrentRenewalAmount(),
416 'currency' => $order->currency,
417 'trial_days' => $subscriptionModel->getReactivationTrialDays(),
418 'interval_count' => 1,
419 'order_id' => $subscriptionModel->parent_order_id,
420 ]);
421 } else {
422 $stripePlan = Plan::getStripePricing([
423 'product_id' => $subscriptionModel->product_id,
424 'variation_id' => $subscriptionModel->variation_id,
425 'billing_interval' => $subscriptionModel->billing_interval,
426 'recurring_total' => $subscriptionModel->recurring_total,
427 'currency' => $order->currency,
428 'trial_days' => (int)$subscriptionModel->trial_days,
429 'interval_count' => 1,
430 'order_id' => $subscriptionModel->parent_order_id,
431 ]);
432 }
433
434 if (is_wp_error($stripePlan)) {
435 return $stripePlan;
436 }
437
438
439 $feeTotal = $orderType !== 'renewal' ? (int)$paymentInstance->order->fee_total : 0;
440 $initialAmount = (int)$subscriptionModel->signup_fee + $paymentInstance->getExtraAddonAmount() + $feeTotal;
441
442 if ($orderType == 'renewal') {
443 $initialAmount = 0;
444 }
445
446 $recurringTotal = (int)$subscriptionModel->recurring_total;
447 if ($transactionCurrency && CurrenciesHelper::isZeroDecimal($transactionCurrency)) {
448 $initialAmount = (int)($initialAmount / 100);
449 $recurringTotal = (int)($recurringTotal / 100);
450 }
451
452 $lineItems = [
453 [
454 'price' => $stripePlan['id'],
455 'quantity' => $subscriptionModel->quantity ?: 1,
456 ]
457 ];
458
459 $subscriptionData = [
460 'metadata' => [
461 'fct_ref_id' => $order->uuid,
462 'email' => $fcCustomer->email,
463 'name' => $order->full_name,
464 'order_reference' => 'fct_order_id_' . $order->id,
465 'subscription_item' => $subscriptionModel->item_name,
466 ],
467 ];
468
469 // Handle trial period if set in plan (same as onsite lines 94-96)
470 if (!empty($stripePlan['trial_period_days'])) {
471 $subscriptionData['trial_period_days'] = $stripePlan['trial_period_days'];
472 }
473
474 if ($initialAmount > 0) {
475 $addonPrice = Plan::getOneTimeAddonPrice([
476 'product_id' => $subscriptionModel->product_id,
477 'currency' => $order->currency,
478 'amount' => (int)$initialAmount,
479 'name' => __('Signup fee / initial payment', 'fluent-cart'),
480 'variation_id' => $subscriptionModel->variation_id,
481 'order_id' => $subscriptionModel->parent_order_id,
482
483 ]);
484
485 if (is_wp_error($addonPrice)) {
486 return $addonPrice;
487 };
488
489 $lineItems[] = [
490 'price' => $addonPrice['id'],
491 'quantity' => 1
492 ];
493 }
494
495 $sessionData = [
496 'customer' => $stripeCustomer['id'],
497 'client_reference_id' => $order->uuid,
498 'line_items' => $lineItems,
499 'mode' => 'subscription',
500 'consent_collection' => ['payment_method_reuse_agreement' => ['position' => 'hidden']],
501 'success_url' => Arr::get($paymentArgs, 'success_url') . '&fct_stripe_hosted=1&trx_hash=' . $transaction->uuid,
502 'cancel_url' => StripeHelper::getCancelUrl(),
503 'subscription_data' => $subscriptionData,
504 'metadata' => [
505 'fct_ref_id' => $order->uuid,
506 'subscription_item' => $subscriptionModel->item_name,
507 'transaction_hash' => $transaction->uuid,
508 'order_reference' => 'fct_order_id_' . $order->id,
509 ],
510 ];
511
512 $sessionData = apply_filters('fluent_cart/payments/stripe_subscription_checkout_session_args', $sessionData, [
513 'order' => $order,
514 'transaction' => $transaction,
515 'subscription' => $subscriptionModel
516 ]);
517
518 $session = (new API())->createStripeObject('checkout/sessions', $sessionData);
519
520 if (is_wp_error($session)) {
521 return $session;
522 }
523
524 $subscriptionModel->update([
525 'vendor_customer_id' => $stripeCustomer['id']
526 ]);
527
528 $transaction->update([
529 'vendor_charge_id' => Arr::get($session, 'payment_intent', Arr::get($session, 'id')),
530 'meta' => array_merge($transaction->meta ?? [], [
531 'session_id' => $session['id']
532 ])
533 ]);
534
535 return [
536 'status' => 'success',
537 'nextAction' => 'stripe',
538 'actionName' => 'redirect',
539 'message' => __('Redirecting to Stripe checkout...', 'fluent-cart'),
540 'response' => $session,
541 'payment_args' => array_merge($paymentArgs, [
542 'checkout_url' => $session['url'],
543 'session_id' => $session['id']
544 ])
545 ];
546 }
547
548 }
549