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

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

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