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 / Subscriptions / Services / SubscriptionService.php

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

398 lines 16.2 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 = OrderTransaction::query()
247 ->where('subscription_id', $subscriptionModel->id)
248 ->where('transaction_type', Status::TRANSACTION_TYPE_CHARGE)
249 ->where('status', Status::TRANSACTION_SUCCEEDED)
250 ->where('total', '>', 0)
251 ->count();
252
253
254 $earlyPaymentHistory = $subscriptionModel->getMeta('early_payment_history', []);
255 foreach ($earlyPaymentHistory as $earlyPayment) {
256 $paidCount = (int) Arr::get($earlyPayment, 'count', 1);
257 if ($paidCount > 1) {
258 $billsCount += ($paidCount - 1);
259 }
260 }
261
262 $subscriptionUpdateArgs['bill_count'] = $billsCount;
263 $billTimes = $subscriptionModel->bill_times;
264 $oldStatus = $subscriptionModel->status;
265
266 $subscriptionUpdateArgs['bill_count'] = $billsCount;
267 $isEot = $billTimes > 0 && $billsCount >= $billTimes;
268
269 if ($isEot) {
270 $subscriptionUpdateArgs['status'] = 'completed';
271 $subscriptionUpdateArgs['next_billing_date'] = NULL;
272 $subscriptionUpdateArgs['canceled_at'] = NULL;
273 } else if (!$subscriptionModel->next_billing_date && empty($subscriptionUpdateArgs['next_billing_date'])) {
274 $subscriptionUpdateArgs['next_billing_date'] = $subscriptionModel->guessNextBillingDate();
275 }
276
277 if (Arr::get($subscriptionUpdateArgs, 'status') === Status::SUBSCRIPTION_ACTIVE) {
278 $subscriptionUpdateArgs['recurring_total'] = Arr::get($subscriptionUpdateArgs, 'recurring_total', $subscriptionModel->recurring_total);
279 }
280
281 $givenSubscriptionStatus = Arr::get($subscriptionUpdateArgs, 'status');
282 if ($givenSubscriptionStatus === Status::SUBSCRIPTION_CANCELED && empty($subscriptionUpdateArgs['canceled_at'])) {
283 $subscriptionUpdateArgs['canceled_at'] = gmdate('Y-m-d H:i:s');
284 }
285
286 $subscriptionModel->fill($subscriptionUpdateArgs);
287 $dirtyData = $subscriptionModel->getDirty();
288 $subscriptionModel->save();
289
290 $meta = array_filter(Arr::get($subscriptionUpdateArgs, 'meta', []));
291
292 foreach ($meta as $key => $value) {
293 $subscriptionModel->updateMeta($key, $value);
294 }
295
296 // validity_expired_at should only exist when status IS expired
297 if ($subscriptionModel->status !== Status::SUBSCRIPTION_EXPIRED) {
298 $subscriptionModel->deleteMeta('validity_expired_at');
299 }
300
301 if ($oldStatus === $subscriptionModel->status) {
302 if ($dirtyData) {
303 do_action('fluent_cart/subscription/data_updated', [
304 'subscription' => $subscriptionModel,
305 'updated_data' => $dirtyData
306 ]);
307 }
308
309 return $subscriptionModel; // No change in status
310 }
311
312 if ($isEot) {
313 (new SubscriptionEOT($subscriptionModel, $subscriptionModel->order))->dispatch();
314 }
315
316 do_action('fluent_cart/payments/subscription_status_changed', [
317 'subscription' => $subscriptionModel,
318 'order' => $subscriptionModel->order,
319 'customer' => $subscriptionModel->customer,
320 'old_status' => $oldStatus,
321 'new_status' => $subscriptionModel->status
322 ]);
323
324 /**
325 * lists of hooks for this action
326 * fluent_cart/payments/subscription_canceled
327 * fluent_cart/payments/subscription_active
328 * fluent_cart/payments/subscription_paused
329 * fluent_cart/payments/subscription_expired
330 * fluent_cart/payments/subscription_failing
331 * fluent_cart/payments/subscription_expiring
332 * fluent_cart/payments/subscription_completed
333 **/
334 do_action('fluent_cart/payments/subscription_' . $subscriptionModel->status, [
335 'subscription' => $subscriptionModel,
336 'order' => $subscriptionModel->order,
337 'customer' => $subscriptionModel->customer,
338 'old_status' => $oldStatus,
339 'new_status' => $subscriptionModel->status
340 ]);
341
342 // note: we needed this event, currently being used in integrations
343 if ($subscriptionModel->status === Status::SUBSCRIPTION_EXPIRED) {
344 $subscriptionModel->updateMeta('validity_expired_at', DateTime::now()->format('Y-m-d H:i:s'));
345 (new SubscriptionValidityExpired($subscriptionModel,$subscriptionModel->order,$subscriptionModel->customer))->dispatch();
346 }
347
348 return $subscriptionModel;
349 }
350
351
352 /**
353 *
354 * Use this method when you are reactivating a expired subscription manually by creating order, transaction etc.
355 * Make sure you already handle your transaction statuses!
356 *
357 * @param \FluentCart\App\Models\Subscription $subscriptionModel
358 * @param \FluentCart\App\Models\OrderTransaction $transaction
359 * @param $args
360 * @return mixed
361 */
362 public static function recordManualRenewal(Subscription $subscriptionModel, OrderTransaction $transaction, $args = [])
363 {
364 $renewalOrder = $transaction->order;
365
366 $orderUpdateData = [
367 'status' => $renewalOrder->fulfillment_type === 'physical' ? Status::ORDER_PROCESSING : Status::ORDER_COMPLETED,
368 'type' => Status::ORDER_TYPE_RENEWAL,
369 'payment_method' => $transaction->payment_method,
370 'payment_status' => Status::PAYMENT_PAID,
371 'total_paid' => $transaction->total,
372 'completed_at' => DateTime::now()->format('Y-m-d H:i:s')
373 ];
374
375 $renewalOrder->fill($orderUpdateData);
376 $renewalOrder->save();
377
378 if ($billingInfo = Arr::get($args, 'billing_info', [])) {
379 $subscriptionModel->updateMeta('active_payment_method', $billingInfo);
380 }
381
382 $updateData = wp_parse_args(Arr::get($args, 'subscription_args', []), [
383 'status' => Status::SUBSCRIPTION_ACTIVE,
384 'current_payment_method' => $transaction->payment_method,
385 ]);
386
387 $subscriptionModel = self::syncSubscriptionStates($subscriptionModel, $updateData);
388
389 (new StatusHelper($transaction->order))->syncOrderStatuses($transaction);
390
391 if ($transaction->total > 0) {
392 (new SubscriptionRenewed($subscriptionModel, $renewalOrder, $subscriptionModel->order, $renewalOrder->customer))->dispatch();
393 }
394
395 return $subscriptionModel;
396 }
397 }
398