PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.26
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.26
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 / Subscriptions / Services / SubscriptionService.php

SubscriptionService.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.26, at app/Modules/Subscriptions/Services/SubscriptionService.php

387 lines 15.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\Subscriptions\Services;
4
5 use FluentCart\App\Events\Subscription\SubscriptionEOT;
6 use FluentCart\App\Events\Subscription\SubscriptionRenewed;
7 use FluentCart\App\Events\Subscription\SubscriptionValidityExpired;
8 use FluentCart\App\Helpers\Status;
9 use FluentCart\App\Helpers\StatusHelper;
10 use FluentCart\App\Models\Order;
11 use FluentCart\App\Models\OrderItem;
12 use FluentCart\App\Models\OrderTaxRate;
13 use FluentCart\App\Models\OrderTransaction;
14 use FluentCart\App\Models\Subscription;
15 use FluentCart\App\Modules\PaymentMethods\PayPalGateway\API\API;
16 use FluentCart\App\Services\DateTime\DateTime;
17 use FluentCart\App\Services\Payments\PaymentHelper;
18 use FluentCart\Framework\Support\Arr;
19
20 class SubscriptionService
21 {
22 public static function recordRenewalPayment($transactionData, $subscriptionModel = null, $subscriptionUpdateArgs = [])
23 {
24 if (!$subscriptionModel) {
25 $subscriptionModel = Subscription::query()->find($transactionData['subscription_id']);
26 }
27
28 if (!$subscriptionModel) {
29 return new \WP_Error('subscription_not_found', __('Subscription not found.', 'fluent-cart'));
30 }
31
32 $vendorTransactionId = $transactionData['vendor_charge_id'] ?? null;
33
34 if ($vendorTransactionId) {
35 if (OrderTransaction::query()->where('vendor_charge_id', $vendorTransactionId)->exists()) {
36 return new \WP_Error('transaction_exists', __('This transaction already exists for this subscription.', 'fluent-cart'));
37 }
38 }
39
40 $parentOrder = $subscriptionModel->order;
41
42 if (!$parentOrder) {
43 return new \WP_Error('parent_order_not_found', __('Parent order not found for this subscription.', 'fluent-cart'));
44 }
45
46 $transactionDefaults = [
47 'order_id' => $parentOrder->id,
48 'subscription_id' => $subscriptionModel->id,
49 'order_type' => Status::ORDER_TYPE_RENEWAL,
50 'transaction_type' => Status::TRANSACTION_TYPE_CHARGE,
51 'payment_method' => $subscriptionModel->current_payment_method,
52 'payment_mode' => $parentOrder->mode,
53 'status' => Status::TRANSACTION_SUCCEEDED,
54 'currency' => $parentOrder->currency,
55 'total' => $subscriptionModel->recurring_total,
56 'meta' => Arr::get($transactionData, 'meta', [])
57 ];
58
59 $transactionData = wp_parse_args($transactionData, $transactionDefaults);
60
61 $createdAt = Arr::get($transactionData, 'created_at', DateTime::now()->format('Y-m-d H:i:s'));
62
63 // Let's create the order item first
64 $variation = $subscriptionModel->variation;
65 $product = $subscriptionModel->product;
66
67 $parentOrderItem = OrderItem::query()
68 ->where('order_id', $parentOrder->id)
69 ->where('payment_type', Status::ORDER_TYPE_SUBSCRIPTION)
70 ->first();
71
72 $taxTotal = Arr::get($transactionData, 'tax_total', 0);
73 if (!$taxTotal && $subscriptionModel->recurring_tax_total) {
74 $taxTotal = $subscriptionModel->recurring_tax_total;
75 }
76
77 if (!$taxTotal && $parentOrder->tax_behavior == 2 && $parentOrderItem) {
78 $taxTotal = (int) Arr::get($parentOrderItem->other_info, 'recurring_tax', 0);
79 }
80
81 $subtotal = $transactionData['total'];
82 if ($taxTotal) {
83 $subtotal = $transactionData['total'] - $taxTotal;
84 }
85
86 $orderItem = [
87 'post_id' => $subscriptionModel->product_id,
88 'object_id' => $subscriptionModel->variation_id,
89 'payment_type' => Status::ORDER_TYPE_SUBSCRIPTION,
90 'post_title' => $product && $product->post_title ? $product->post_title : $subscriptionModel->item_name,
91 'title' => $product && $variation ? $variation->variation_title : '',
92 'quantity' => 1,
93 'fulfillment_type' => $parentOrderItem ? $parentOrderItem->fulfillment_type : 'digital',
94 'unit_price' => $subtotal,
95 'subtotal' => $subtotal,
96 'tax_amount' => $taxTotal,
97 'line_total' => $transactionData['total'],
98 'line_meta' => [],
99 'other_info' => []
100 ];
101
102 $bundleItemIds = Arr::get($parentOrderItem->line_meta, 'bundle_item_ids', []);
103
104 $isBundleOrder = false;
105 if ($bundleItemIds) {
106 $isBundleOrder = true;
107 $orderItem['line_meta'] = array_merge(
108 $orderItem['line_meta'],
109 [
110 'bundle_item_ids' => $bundleItemIds
111 ]
112 );
113 }
114
115 $fulfillmentType = $orderItem['fulfillment_type'];
116
117 // Let's create the order first
118 $childOrderData = [
119 'parent_id' => $parentOrder->id,
120 'fulfillment_type' => $fulfillmentType,
121 'status' => $fulfillmentType === 'physical' ? Status::ORDER_PROCESSING : Status::ORDER_COMPLETED,
122 'type' => Status::ORDER_TYPE_RENEWAL,
123 'mode' => $transactionData['payment_mode'],
124 'shipping_status' => $fulfillmentType === 'physical' ? Status::SHIPPING_UNSHIPPED : '',
125 'customer_id' => $subscriptionModel->customer_id,
126 'payment_method' => $transactionData['payment_method'],
127 'payment_status' => $transactionData['status'] === Status::TRANSACTION_SUCCEEDED ? Status::PAYMENT_PAID : Status::PAYMENT_PENDING,
128 'currency' => $transactionData['currency'],
129 'tax_behavior' => $parentOrder->tax_behavior,
130 'subtotal' => $subtotal,
131 'tax_total' => $taxTotal,
132 'total_amount' => $transactionData['total'],
133 'total_paid' => $transactionData['status'] === Status::TRANSACTION_SUCCEEDED ? $transactionData['total'] : 0,
134 'completed_at' => DateTime::now()->format('Y-m-d H:i:s'),
135 'created_at' => $createdAt,
136 'config' => []
137 ];
138
139 $childOrder = Order::query()->create($childOrderData);
140
141 if (!$childOrder) {
142 return new \WP_Error('order_creation_failed', __('Failed to create child order for the subscription renewal.', 'fluent-cart'));
143 }
144
145 $billingAddress = $parentOrder->billing_address;
146 $shippingAddress = $parentOrder->shipping_address;
147
148 $customer = $parentOrder->customer;
149
150 $fullName = '';
151 $email = '';
152 $firstName = '';
153 $lastName = '';
154 if ($customer) {
155 $fullName = $customer->first_name . ' ' . $customer->last_name;
156 $email = $customer->email;
157 $firstName = $customer->first_name;
158 $lastName = $customer->last_name;
159 }
160
161 $billingAddressData = $billingAddress ? [
162 'type' => 'billing',
163 'full_name' => $fullName,
164 'address_1' => $billingAddress->address_1,
165 'address_2' => $billingAddress->address_2,
166 'city' => $billingAddress->city,
167 'state' => $billingAddress->state,
168 'postcode' => $billingAddress->postcode,
169 'country' => $billingAddress->country,
170 'email' => $email,
171 'first_name' => $firstName,
172 'last_name' => $lastName
173 ] : [];
174
175 $shippingAddressData = $shippingAddress ? [
176 'type' => 'shipping',
177 'full_name' => $fullName,
178 'address_1' => $shippingAddress->address_1,
179 'address_2' => $shippingAddress->address_2,
180 'city' => $shippingAddress->city,
181 'state' => $shippingAddress->state,
182 'postcode' => $shippingAddress->postcode,
183 'country' => $shippingAddress->country,
184 'email' => $email,
185 'first_name' => $firstName,
186 'last_name' => $lastName
187 ] : [];
188
189 \FluentCart\App\Helpers\AddressHelper::insertOrderAddresses(
190 $childOrder->id,
191 $billingAddressData,
192 $shippingAddressData
193 );
194
195 // Copy tax ID meta from parent order if exists
196 $parentTaxId = $parentOrder->getMeta('tax_id', '');
197 if ($parentTaxId) {
198 $childOrder->updateMeta('tax_id', $parentTaxId);
199 }
200
201 // Copy order tax rates from parent order
202 $parentTaxRates = $parentOrder->orderTaxRates;
203 foreach ($parentTaxRates as $taxRate) {
204 OrderTaxRate::query()->create([
205 'order_id' => $childOrder->id,
206 'tax_rate_id' => $taxRate->tax_rate_id,
207 'shipping_tax' => $taxRate->shipping_tax,
208 'order_tax' => $taxRate->order_tax,
209 'total_tax' => $taxRate->total_tax,
210 'meta' => $taxRate->meta,
211 ]);
212 }
213
214 // Create Order Item
215 $orderItem['order_id'] = $childOrder->id;
216 $orderItem['created_at'] = $createdAt;
217 OrderItem::query()->create($orderItem);
218
219 // let's create the transaction
220 $transactionData['order_id'] = $childOrder->id;
221
222 $createdTransaction = OrderTransaction::query()->create($transactionData);
223
224 $subscriptionModel = self::syncSubscriptionStates($subscriptionModel, $subscriptionUpdateArgs);
225
226 (new SubscriptionRenewed($subscriptionModel, $childOrder, $parentOrder, $childOrder->customer))->dispatch();
227
228 return $createdTransaction;
229 }
230
231 /**
232 * @param $subscriptionModel
233 * @param $subscriptionUpdateArgs
234 * - next_billing_date - You must provide this if you want to update the next billing date.
235 * * - Accepts all other filliable attributes of the Subscription model.
236 * @return mixed
237 */
238 public static function syncSubscriptionStates(Subscription $subscriptionModel, $subscriptionUpdateArgs = [])
239 {
240 $billsCount = OrderTransaction::query()
241 ->where('subscription_id', $subscriptionModel->id)
242 ->where('transaction_type', Status::TRANSACTION_TYPE_CHARGE)
243 ->where('status', Status::TRANSACTION_SUCCEEDED)
244 ->where('total', '>', 0)
245 ->count();
246
247
248 $earlyPaymentHistory = $subscriptionModel->getMeta('early_payment_history', []);
249 foreach ($earlyPaymentHistory as $earlyPayment) {
250 $paidCount = (int) Arr::get($earlyPayment, 'count', 1);
251 if ($paidCount > 1) {
252 $billsCount += ($paidCount - 1);
253 }
254 }
255
256 $subscriptionUpdateArgs['bill_count'] = $billsCount;
257 $billTimes = $subscriptionModel->bill_times;
258 $oldStatus = $subscriptionModel->status;
259
260 $subscriptionUpdateArgs['bill_count'] = $billsCount;
261 $isEot = $billTimes > 0 && $billsCount >= $billTimes;
262
263 if ($isEot) {
264 $subscriptionUpdateArgs['status'] = 'completed';
265 $subscriptionUpdateArgs['next_billing_date'] = NULL;
266 $subscriptionUpdateArgs['canceled_at'] = NULL;
267 } else if (!$subscriptionModel->next_billing_date && empty($subscriptionUpdateArgs['next_billing_date'])) {
268 $subscriptionUpdateArgs['next_billing_date'] = $subscriptionModel->guessNextBillingDate();
269 }
270
271 if (Arr::get($subscriptionUpdateArgs, 'status') === Status::SUBSCRIPTION_ACTIVE) {
272 $subscriptionUpdateArgs['recurring_total'] = Arr::get($subscriptionUpdateArgs, 'recurring_total', $subscriptionModel->recurring_total);
273 }
274
275 $givenSubscriptionStatus = Arr::get($subscriptionUpdateArgs, 'status');
276 if ($givenSubscriptionStatus === Status::SUBSCRIPTION_CANCELED && empty($subscriptionUpdateArgs['canceled_at'])) {
277 $subscriptionUpdateArgs['canceled_at'] = gmdate('Y-m-d H:i:s');
278 }
279
280 $subscriptionModel->fill($subscriptionUpdateArgs);
281 $dirtyData = $subscriptionModel->getDirty();
282 $subscriptionModel->save();
283
284 $meta = array_filter(Arr::get($subscriptionUpdateArgs, 'meta', []));
285
286 foreach ($meta as $key => $value) {
287 $subscriptionModel->updateMeta($key, $value);
288 }
289
290 if ($oldStatus === $subscriptionModel->status) {
291 if ($dirtyData) {
292 do_action('fluent_cart/subscription/data_updated', [
293 'subscription' => $subscriptionModel,
294 'updated_data' => $dirtyData
295 ]);
296 }
297
298 return $subscriptionModel; // No change in status
299 }
300
301 if ($isEot) {
302 (new SubscriptionEOT($subscriptionModel, $subscriptionModel->order))->dispatch();
303 }
304
305 do_action('fluent_cart/payments/subscription_status_changed', [
306 'subscription' => $subscriptionModel,
307 'order' => $subscriptionModel->order,
308 'customer' => $subscriptionModel->customer,
309 'old_status' => $oldStatus,
310 'new_status' => $subscriptionModel->status
311 ]);
312
313 /**
314 * lists of hooks for this action
315 * fluent_cart/payments/subscription_canceled
316 * fluent_cart/payments/subscription_active
317 * fluent_cart/payments/subscription_paused
318 * fluent_cart/payments/subscription_expired
319 * fluent_cart/payments/subscription_failing
320 * fluent_cart/payments/subscription_expiring
321 * fluent_cart/payments/subscription_completed
322 **/
323 do_action('fluent_cart/payments/subscription_' . $subscriptionModel->status, [
324 'subscription' => $subscriptionModel,
325 'order' => $subscriptionModel->order,
326 'customer' => $subscriptionModel->customer,
327 'old_status' => $oldStatus,
328 'new_status' => $subscriptionModel->status
329 ]);
330
331 // note: we needed this event, currently being used in integrations
332 if ($subscriptionModel->status === Status::SUBSCRIPTION_EXPIRED) {
333 $subscriptionModel->updateMeta('validity_expired_at', DateTime::now()->format('Y-m-d H:i:s'));
334 (new SubscriptionValidityExpired($subscriptionModel,$subscriptionModel->order,$subscriptionModel->customer))->dispatch();
335 }
336
337 return $subscriptionModel;
338 }
339
340
341 /**
342 *
343 * Use this method when you are reactivating a expired subscription manually by creating order, transaction etc.
344 * Make sure you already handle your transaction statuses!
345 *
346 * @param \FluentCart\App\Models\Subscription $subscriptionModel
347 * @param \FluentCart\App\Models\OrderTransaction $transaction
348 * @param $args
349 * @return mixed
350 */
351 public static function recordManualRenewal(Subscription $subscriptionModel, OrderTransaction $transaction, $args = [])
352 {
353 $renewalOrder = $transaction->order;
354
355 $orderUpdateData = [
356 'status' => $renewalOrder->fulfillment_type === 'physical' ? Status::ORDER_PROCESSING : Status::ORDER_COMPLETED,
357 'type' => Status::ORDER_TYPE_RENEWAL,
358 'payment_method' => $transaction->payment_method,
359 'payment_status' => Status::PAYMENT_PAID,
360 'total_paid' => $transaction->total,
361 'completed_at' => DateTime::now()->format('Y-m-d H:i:s')
362 ];
363
364 $renewalOrder->fill($orderUpdateData);
365 $renewalOrder->save();
366
367 if ($billingInfo = Arr::get($args, 'billing_info', [])) {
368 $subscriptionModel->updateMeta('active_payment_method', $billingInfo);
369 }
370
371 $updateData = wp_parse_args(Arr::get($args, 'subscription_args', []), [
372 'status' => Status::SUBSCRIPTION_ACTIVE,
373 'current_payment_method' => $transaction->payment_method,
374 ]);
375
376 $subscriptionModel = self::syncSubscriptionStates($subscriptionModel, $updateData);
377
378 (new StatusHelper($transaction->order))->syncOrderStatuses($transaction);
379
380 if ($transaction->total > 0) {
381 (new SubscriptionRenewed($subscriptionModel, $renewalOrder, $subscriptionModel->order, $renewalOrder->customer))->dispatch();
382 }
383
384 return $subscriptionModel;
385 }
386 }
387