PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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 / StripeHelper.php

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

300 lines 10.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\Helpers\Status;
7 use FluentCart\App\Models\Customer;
8 use FluentCart\App\Models\OrderTransaction;
9 use FluentCart\App\Models\Order;
10 use FluentCart\App\Modules\PaymentMethods\StripeGateway\API\API;
11 use FluentCart\App\Services\Payments\PaymentHelper;
12 use FluentCart\Api\StoreSettings;
13 use FluentCart\App\App;
14 use FluentCart\Framework\Support\Arr;
15
16 class StripeHelper
17 {
18 public static function createOrGetStripeCustomer(Customer $customer)
19 {
20 // check if we already have a stripe_customer_id for this person
21 $existingStripeCustomerId = $customer->getMeta('stripe_customer_id', false);
22 if ($existingStripeCustomerId) {
23 $existingStripeCustomer = (new API())->getStripeObject('customers/' . $existingStripeCustomerId);
24
25 if (!is_wp_error($existingStripeCustomer) && is_array($existingStripeCustomer) && !empty($existingStripeCustomer['id']) && isset($existingStripeCustomer['email']) && $existingStripeCustomer['email'] === $customer->email) {
26 return $existingStripeCustomer;
27 }
28 }
29
30 $customerInfo = array_filter([
31 'name' => $customer->full_name,
32 'email' => $customer->email,
33 'phone' => $customer->phone ?? '',
34 'address' => array_filter([
35 'city' => $customer->city ?? '',
36 'country' => $customer->country ?? '',
37 'postal_code' => $customer->postcode ?? '',
38 'state' => $customer->state ?? '',
39 ])
40 ]);
41
42 $newStripeCustomer = (new API())->createStripeObject('customers', $customerInfo);
43
44 if (is_wp_error($newStripeCustomer)) {
45 return $newStripeCustomer;
46 }
47
48 $id = Arr::get($newStripeCustomer, 'id', false);
49
50 if ($id) {
51 $customer->updateMeta('stripe_customer_id', $id);
52 }
53
54 return $newStripeCustomer;
55 }
56
57 public static function transformSubscriptionStatus($stripeSubscription, $subscriptionModel = null)
58 {
59 $status = strtolower($stripeSubscription['status']);
60
61 if ($status === 'active') {
62 $status = Status::SUBSCRIPTION_ACTIVE;
63 } else if ($status === 'incomplete' || $status === 'incomplete_expired') {
64 $status = Status::SUBSCRIPTION_INTENDED;
65 } else if ($status === 'trialing') {
66 $status = Status::SUBSCRIPTION_TRIALING;
67 } else if ($status === 'canceled') {
68 $status = Status::SUBSCRIPTION_CANCELED;
69 if (Arr::get($stripeSubscription, 'cancellation_details.reason', '') === 'payment_failed') {
70 $status = Status::SUBSCRIPTION_EXPIRED;
71 }
72 } else if ($status === 'unpaid') {
73 $status = Status::SUBSCRIPTION_EXPIRED;
74 } else if ($status === 'paused') {
75 $status = Status::SUBSCRIPTION_PAUSED;
76 } else if ($status === 'past_due') {
77 $status = Status::SUBSCRIPTION_EXPIRING;
78 if ($subscriptionModel && $subscriptionModel->status === 'expired') {
79 $status = Status::SUBSCRIPTION_EXPIRED;
80 }
81 }
82
83 return $status;
84 }
85
86 public static function getSubscriptionUpdateData($stripeSubscription, $subscriptionModel = null)
87 {
88 $stripeStatus = strtolower(Arr::get($stripeSubscription, 'status', ''));
89
90 $status = self::transformSubscriptionStatus($stripeSubscription, $subscriptionModel);
91
92 $amount = Arr::get($stripeSubscription, 'plan.amount', 0);
93 $currency = Arr::get($stripeSubscription, 'plan.currency', null);
94
95 if ($currency && CurrenciesHelper::isZeroDecimal($currency)) {
96 $amount = $amount * 100;
97 }
98
99 $subscriptionUpdateData = array_filter([
100 'current_payment_method' => 'stripe',
101 'status' => $status,
102 'recurring_total' => $amount,
103 ]);
104
105 if ($stripeStatus == Status::SUBSCRIPTION_CANCELED) {
106 $cancelledAt = (int)Arr::get($stripeSubscription, 'canceled_at');
107 if ($cancelledAt) {
108 $subscriptionUpdateData['canceled_at'] = gmdate('Y-m-d H:i:s', $cancelledAt);
109 }
110 }
111
112 $currentPeriodEnds = (int)Arr::get($stripeSubscription, 'current_period_end');
113 // we have to check if the last invoice is paid or not!
114 // If not paid, then we have to use the $stripeSubscription['current_period_start']
115 $latestInvoice = Arr::get($stripeSubscription, 'latest_invoice', null);
116 if ($latestInvoice && !empty($latestInvoice['id'])) {
117 // we have the latest invoice
118 if (Arr::get($latestInvoice, 'status') !== 'paid') {
119 $currentPeriodEnds = (int)Arr::get($stripeSubscription, 'current_period_start');
120 }
121 }
122
123 if ($currentPeriodEnds) {
124 $subscriptionUpdateData['next_billing_date'] = gmdate('Y-m-d H:i:s', $currentPeriodEnds);
125 }
126
127 return $subscriptionUpdateData;
128 }
129
130
131 /**
132 * Convert a Stripe wire amount (invoice/charge/payment_intent) to FluentCart's
133 * internal ×100 unit. Zero-decimal currencies (JPY, KRW, ...) come off the wire
134 * unscaled, so they need the ×100 that non-zero-decimal currencies already have.
135 *
136 * @param int|float $amount
137 * @param string $currency
138 * @return int
139 */
140 public static function toInternalAmount($amount, $currency)
141 {
142 $amount = (int)$amount;
143
144 if ($currency && CurrenciesHelper::isZeroDecimal($currency)) {
145 $amount *= 100;
146 }
147
148 return $amount;
149 }
150
151 /**
152 * Convert a Stripe object's `livemode` flag to a getStripeObject()/getApiKey()
153 * `$mode` argument. The store's global mode toggle ('current') is not a reliable
154 * proxy for which key an already-fetched object needs — use its own livemode
155 * whenever one is available instead of guessing from the store setting.
156 *
157 * @param bool|null $livemode Null when no prior object/event is available yet.
158 * @return string 'live', 'test', or 'current'
159 */
160 public static function modeFromLivemode($livemode)
161 {
162 if (is_null($livemode)) {
163 return 'current';
164 }
165
166 return $livemode ? 'live' : 'test';
167 }
168
169 public static function processRemoteRefund($transaction, $amount, $args)
170 {
171 $intentId = $transaction->vendor_charge_id;
172 if (!$intentId) {
173 return new \WP_Error('invalid_refund', __('Invalid transaction ID for refund.', 'fluent-cart'));
174 }
175
176 $refundAmount = (int)$amount;
177 $refundCurrency = $transaction->currency;
178
179 if ($refundCurrency && CurrenciesHelper::isZeroDecimal($refundCurrency)) {
180 $refundAmount = (int)($refundAmount / 100);
181 }
182
183 $refundData = [
184 'payment_intent' => $intentId,
185 'amount' => $refundAmount,
186 ];
187
188 $reason = Arr::get($args, 'reason', '');
189
190 if ($reason && in_array($reason, ['duplicate', 'fraudulent', 'requested_by_customer'])) {
191 $refundData['reason'] = $reason;
192 }
193
194 $refunded = (new API())->createStripeObject('refunds', $refundData, $transaction->payment_mode);
195
196 if (is_wp_error($refunded)) {
197 return $refunded;
198 }
199
200 $status = Arr::get($refunded, 'status');
201 $acceptedStatus = ['succeeded', 'pending'];
202 if (!in_array($status, $acceptedStatus)) {
203 return new \WP_Error('refund_failed', __('Refund could not be processed in stripe. Please check on your stripe account', 'fluent-cart'));
204 }
205
206 return Arr::get($refunded, 'id');
207 }
208
209 public static function createOrUpdateIpnRefund($refundData, $parentTransaction)
210 {
211 $allRefunds = OrderTransaction::query()
212 ->where('order_id', $refundData['order_id'])
213 ->where('transaction_type', Status::TRANSACTION_TYPE_REFUND)
214 ->orderBy('id', 'DESC')
215 ->get();
216
217 if ($allRefunds->isEmpty()) {
218 // this is the first refund for this order
219 return OrderTransaction::query()->create($refundData);
220 }
221
222 $currentRefundTransactionId = Arr::get($refundData, 'meta.parent_id', '');
223
224 $existingLocalRefund = null;
225 foreach ($allRefunds as $refund) {
226 if ($refund->vendor_charge_id == $refundData['vendor_charge_id']) {
227 if ($refund->total != $refundData['total']) {
228 $refund->fill($refundData);
229 $refund->save();
230 }
231 // this refund already exists
232 return $refund;
233 }
234
235 if (!$refund->vendor_charge_id) { // this is a local redfund without vendor charge id
236 $refundTransactionId = Arr::get($refund->meta, 'parent_id', '');
237 $isTransactionMatched = $refundTransactionId == $currentRefundTransactionId;
238
239 // this is a local refund without vendor charge id, we will update it
240 if ($refund->total == $refundData['total'] && $isTransactionMatched) {
241 // this refund already exists
242 $existingLocalRefund = $refund;
243 }
244 }
245 }
246
247 if ($existingLocalRefund) {
248 $existingLocalRefund->fill($refundData);
249 $existingLocalRefund->save();
250 return $existingLocalRefund;
251 }
252
253 $createdRefund = OrderTransaction::query()->create($refundData);
254
255 PaymentHelper::updateTransactionRefundedTotal($parentTransaction, $createdRefund->total);
256 return $createdRefund;
257 }
258
259 /*
260 * To validate by session, id
261 *
262 */
263 public static function validateBySession($id)
264 {
265 $apiKey = (new StripeSettingsBase())->getApiKey();
266
267 $session = (new API())->makeRequest('checkout/sessions/' . $id, [], $apiKey, 'GET');
268
269 if (!$session || is_wp_error($session)) {
270 return null;
271 }
272
273
274 $order = Order::query()
275 ->where('uuid', Arr::get($session, 'client_reference_id'))
276 ->first();
277
278 if (!$order) {
279 return null;
280 }
281
282 return $order;
283
284 }
285
286 public static function getCancelUrl(): string
287 {
288 $checkoutPage = (new StoreSettings())->getCheckoutPage();
289 // get cart hash from url
290 $cartHash = App::request()->get('fct_cart_hash', '');
291 if ($cartHash) {
292 return add_query_arg([
293 'fct_cart_hash' => $cartHash
294 ], $checkoutPage);
295 }
296 return $checkoutPage;
297 }
298
299 }
300