PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.20
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.20
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 / Helpers / StatusHelper.php

StatusHelper.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.20, at app/Helpers/StatusHelper.php

251 lines 10.0 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\Helpers;
4
5 use FluentCart\App\Events\Order\OrderPaid;
6 use FluentCart\App\Events\Order\OrderStatusUpdated;
7 use FluentCart\App\Models\Cart;
8 use FluentCart\App\Models\Order;
9 use FluentCart\App\Models\OrderTransaction;
10 use FluentCart\App\Services\DateTime\DateTime;
11 use FluentCart\Framework\Support\Arr;
12
13
14 class StatusHelper
15 {
16 protected $order;
17
18 public function __construct($order = null)
19 {
20 $this->order = $order;
21 }
22
23 public function setOrder($order)
24 {
25 $this->order = $order;
26 return $this;
27 }
28
29 public function changeOrderStatus($orderStatus, $paymentStatus, $title, $slug)
30 {
31 $oldStatus = $this->order->status;
32 if ($orderStatus === $oldStatus) {
33 return $this;
34 }
35 if ($orderStatus === Status::ORDER_COMPLETED) {
36 $this->order->completed_at = DateTime::gmtNow();
37 }
38 if ($paymentStatus === Status::PAYMENT_REFUNDED) {
39 $this->order->refunded_at = DateTime::gmtNow();
40 }
41 $this->order->status = $orderStatus;
42 $this->order->payment_method = $slug;
43 $this->order->payment_status = $paymentStatus;
44 $this->order->payment_method_title = $title;
45 $this->order->save();
46
47 $actionActivity = [
48 'title' => __('Order status updated', 'fluent-cart'),
49 'content' => sprintf(
50 /* translators: 1: old status, 2: new status */
51 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), $oldStatus, $orderStatus)
52 ];
53
54 (new OrderStatusUpdated($this->order, $oldStatus, $orderStatus, true, $actionActivity, 'order_status'))->dispatch();
55
56 return $this;
57 }
58
59 public function updateTotalPaid($amount)
60 {
61 $this->order->total_paid = intval($amount) + intval($this->order->total_paid);
62 if ($this->order->total_paid >= $this->order->total_amount) {
63 $this->order->payment_status = Status::PAYMENT_PAID;
64 $this->triggerPaymentStatusActions($this->order, Status::PAYMENT_PAID);
65 } else if ($this->order->total_paid < $this->order->total_amount && Status::PAYMENT_PARTIALLY_PAID !== $this->order->payment_status) {
66 $this->order->payment_status = Status::PAYMENT_PARTIALLY_PAID;
67 }
68 $this->order->save();
69 return $this;
70 }
71
72 public function triggerPaymentStatusActions($order, $paymentStatus)
73 {
74 if (Status::PAYMENT_PAID === $paymentStatus) {
75 $transaction = OrderTransaction::query()->where('order_id', $order->id)
76 ->where('status', Status::TRANSACTION_SUCCEEDED)
77 ->first();
78 (new OrderPaid($order, $this->order->customer, $transaction))->dispatch();
79 }
80
81 // Trigger any other payment status actions based on the payment status
82 }
83
84 public function updateTransactionData($updateData = [], $transaction = null)
85 {
86 if ($transaction) {
87 $transaction->update($updateData);
88 return $this;
89 }
90
91 OrderTransaction::query()->where('order_id', $this->order->id)
92 ->where('order_type', 'payment')
93 ->update($updateData);
94
95 return $this;
96 }
97
98 public function syncOrderStatuses($latestTransaction = null)
99 {
100 // Change the order status depends on payment paid
101 // Change the paid total depends on the order total amount
102 // Change the payment status depends on the order total paid
103
104 $successStatuses = Status::getTransactionSuccessStatuses();
105
106 $transactionPaidTotal = OrderTransaction::query()
107 ->where('order_id', $this->order->id)
108 ->whereIn('status', $successStatuses)
109 ->sum('total');
110
111 $refundedTotal = OrderTransaction::query()
112 ->where('order_id', $this->order->id)
113 ->where('status', Status::TRANSACTION_REFUNDED)
114 ->sum('total');
115
116 $isFullyPaid = $this->order->total_amount <= ($transactionPaidTotal - $refundedTotal);
117
118 $orderPaymentStatus = $this->order->payment_status;
119 if ($isFullyPaid) {
120 $orderPaymentStatus = Status::PAYMENT_PAID;
121 } else if ($refundedTotal) {
122 $orderPaymentStatus = Status::PAYMENT_PARTIALLY_REFUNDED;
123 }
124
125 $orderStatus = $this->order->status;
126 if (!in_array($orderStatus, Status::getOrderSuccessStatuses())) {
127 if ($orderPaymentStatus == Status::PAYMENT_PAID) {
128 $orderStatus = Status::ORDER_PROCESSING;
129 }
130 }
131
132 $oldOrderStatus = $this->order->status;
133 $oldPaymentStatus = $this->order->payment_status;
134
135 $this->order->status = $orderStatus;
136 $this->order->payment_status = $orderPaymentStatus;
137 $this->order->total_paid = $transactionPaidTotal;
138 $this->order->total_refund = $refundedTotal;
139
140 // When transitioning to PAID, use an atomic UPDATE to prevent concurrent requests
141 // (e.g., payment gateway webhook + browser confirmation) from both processing
142 // the same payment — which would dispatch OrderPaid twice, generating duplicate
143 // invoice numbers and sending duplicate emails.
144 // The WHERE condition ensures only one process can claim the transition.
145 if ($orderPaymentStatus == Status::PAYMENT_PAID && $oldPaymentStatus != Status::PAYMENT_PAID) {
146 $claimed = Order::query()
147 ->where('id', $this->order->id)
148 ->where(function ($q) {
149 $q->whereNull('payment_status')
150 ->orWhere('payment_status', '!=', Status::PAYMENT_PAID);
151 })
152 ->update([
153 'status' => $orderStatus,
154 'payment_status' => $orderPaymentStatus,
155 'total_paid' => $transactionPaidTotal,
156 'total_refund' => $refundedTotal,
157 ]);
158
159 if (!$claimed) {
160 // Another process already transitioned this order to paid
161 $this->order = Order::query()->where('id', $this->order->id)->first();
162 return $this->order;
163 }
164
165 // Refresh in-memory model to stay in sync with the DB after query-builder update
166 $this->order = Order::query()->where('id', $this->order->id)->first();
167 } else {
168 $this->order->save();
169 }
170
171 if (($this->order->type === 'renewal') || ($oldPaymentStatus != $this->order->payment_status && $this->order->payment_status == Status::PAYMENT_PAID)) {
172 if (!$latestTransaction) {
173 $latestTransaction = OrderTransaction::query()
174 ->where('order_id', $this->order->id)
175 ->orderBy('id', 'desc')
176 ->first();
177 }
178
179 $relatedCart = Cart::query()->where('order_id', $this->order->id)
180 ->where('stage', '!=', 'completed')
181 ->first();
182
183 if ($relatedCart) {
184 $relatedCart->stage = 'completed';
185 $relatedCart->completed_at = DateTime::now()->format('Y-m-d H:i:s');
186 $relatedCart->save();
187
188 do_action('fluent_cart/cart_completed', [
189 'cart' => $relatedCart,
190 'order' => $this->order,
191 ]);
192
193 $onSuccessActions = Arr::get($relatedCart->checkout_data, '__on_success_actions__', []);
194
195 if ($onSuccessActions) {
196 foreach ($onSuccessActions as $onSuccessAction) {
197 $onSuccessAction = (string)$onSuccessAction;
198 if (has_action($onSuccessAction)) {
199 do_action($onSuccessAction, [
200 'cart' => $relatedCart,
201 'order' => $this->order,
202 'transaction' => $latestTransaction
203 ]);
204 }
205 }
206 }
207 }
208
209 if ($this->order->type !== 'renewal' && $oldPaymentStatus != $this->order->payment_status && $this->order->payment_status == Status::PAYMENT_PAID) {
210 (new OrderPaid($this->order, $this->order->customer, $latestTransaction))->dispatch();
211 }
212 }
213
214 if ($oldOrderStatus != $this->order->status) {
215 $actionActivity = [
216 'title' => __('Order status updated', 'fluent-cart'),
217 'content' => sprintf(
218 /* translators: 1: old status, 2: new status */
219 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), $oldOrderStatus, $this->order->status)
220 ];
221 (new OrderStatusUpdated($this->order, $oldOrderStatus, $this->order->status, true, $actionActivity, 'order_status'))->dispatch();
222 }
223
224 $autoCompleteDigitalOrder = apply_filters('fluent_cart/order_status/auto_complete_digital_order', true, [
225 'order' => $this->order,
226 ]);
227
228 // Now if it's a digital product so we will make it auto completed
229 if ($this->order->fulfillment_type == 'digital' && $autoCompleteDigitalOrder
230 && !$refundedTotal && ($oldOrderStatus != $this->order->status)
231 && ($this->order->status != Status::ORDER_COMPLETED)
232 ) {
233 $oldOrderStatus = $this->order->status;
234 $this->order->status = Status::ORDER_COMPLETED;
235 $this->order->completed_at = DateTime::gmtNow();
236 $this->order->save();
237
238 $actionActivity = [
239 'title' => __('Order status updated', 'fluent-cart'),
240 'content' => sprintf(
241 /* translators: 1: old status, 2: new status */
242 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), $oldOrderStatus, $this->order->status)
243 ];
244
245 (new OrderStatusUpdated($this->order, $oldOrderStatus, $this->order->status, true, $actionActivity, 'order_status'))->dispatch();
246 }
247
248 return $this->order;
249 }
250 }
251