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.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.5.4, at app/Helpers/StatusHelper.php

278 lines 10.8 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 if (in_array($orderStatus, Status::getOrderSuccessStatuses())) {
57 // Without this, the cart stays reusable, gets resurrected by the
58 // logged-in user lookup and permanently blocks checkout with
59 // "You have already completed this order."
60 $this->completeRelatedCart();
61 }
62
63 return $this;
64 }
65
66 protected function completeRelatedCart()
67 {
68 $relatedCart = Cart::query()->where('order_id', $this->order->id)
69 ->where('stage', '!=', 'completed')
70 ->first();
71
72 if (!$relatedCart) {
73 return;
74 }
75
76 $relatedCart->stage = 'completed';
77 $relatedCart->completed_at = DateTime::now()->format('Y-m-d H:i:s');
78 $relatedCart->save();
79
80 do_action('fluent_cart/cart_completed', [
81 'cart' => $relatedCart,
82 'order' => $this->order,
83 ]);
84 }
85
86 public function updateTotalPaid($amount)
87 {
88 $this->order->total_paid = intval($amount) + intval($this->order->total_paid);
89 if ($this->order->total_paid >= $this->order->total_amount) {
90 $this->order->payment_status = Status::PAYMENT_PAID;
91 $this->triggerPaymentStatusActions($this->order, Status::PAYMENT_PAID);
92 } else if ($this->order->total_paid < $this->order->total_amount && Status::PAYMENT_PARTIALLY_PAID !== $this->order->payment_status) {
93 $this->order->payment_status = Status::PAYMENT_PARTIALLY_PAID;
94 }
95 $this->order->save();
96 return $this;
97 }
98
99 public function triggerPaymentStatusActions($order, $paymentStatus)
100 {
101 if (Status::PAYMENT_PAID === $paymentStatus) {
102 $transaction = OrderTransaction::query()->where('order_id', $order->id)
103 ->where('status', Status::TRANSACTION_SUCCEEDED)
104 ->first();
105 (new OrderPaid($order, $this->order->customer, $transaction))->dispatch();
106 }
107
108 // Trigger any other payment status actions based on the payment status
109 }
110
111 public function updateTransactionData($updateData = [], $transaction = null)
112 {
113 if ($transaction) {
114 $transaction->update($updateData);
115 return $this;
116 }
117
118 OrderTransaction::query()->where('order_id', $this->order->id)
119 ->where('order_type', 'payment')
120 ->update($updateData);
121
122 return $this;
123 }
124
125 public function syncOrderStatuses($latestTransaction = null)
126 {
127 // Change the order status depends on payment paid
128 // Change the paid total depends on the order total amount
129 // Change the payment status depends on the order total paid
130
131 $successStatuses = Status::getTransactionSuccessStatuses();
132
133 $transactionPaidTotal = OrderTransaction::query()
134 ->where('order_id', $this->order->id)
135 ->whereIn('status', $successStatuses)
136 ->sum('total');
137
138 $refundedTotal = OrderTransaction::query()
139 ->where('order_id', $this->order->id)
140 ->where('status', Status::TRANSACTION_REFUNDED)
141 ->sum('total');
142
143 $isFullyPaid = $this->order->total_amount <= ($transactionPaidTotal - $refundedTotal);
144
145 $orderPaymentStatus = $this->order->payment_status;
146 if ($isFullyPaid) {
147 $orderPaymentStatus = Status::PAYMENT_PAID;
148 } else if ($refundedTotal) {
149 $orderPaymentStatus = Status::PAYMENT_PARTIALLY_REFUNDED;
150 }
151
152 $orderStatus = $this->order->status;
153 if (!in_array($orderStatus, Status::getOrderSuccessStatuses())) {
154 if ($orderPaymentStatus == Status::PAYMENT_PAID) {
155 $orderStatus = Status::ORDER_PROCESSING;
156 }
157 }
158
159 $oldOrderStatus = $this->order->status;
160 $oldPaymentStatus = $this->order->payment_status;
161
162 $this->order->status = $orderStatus;
163 $this->order->payment_status = $orderPaymentStatus;
164 $this->order->total_paid = $transactionPaidTotal;
165 $this->order->total_refund = $refundedTotal;
166
167 // When transitioning to PAID, use an atomic UPDATE to prevent concurrent requests
168 // (e.g., payment gateway webhook + browser confirmation) from both processing
169 // the same payment — which would dispatch OrderPaid twice, generating duplicate
170 // invoice numbers and sending duplicate emails.
171 // The WHERE condition ensures only one process can claim the transition.
172 if ($orderPaymentStatus == Status::PAYMENT_PAID && $oldPaymentStatus != Status::PAYMENT_PAID) {
173 $claimed = Order::query()
174 ->where('id', $this->order->id)
175 ->where(function ($q) {
176 $q->whereNull('payment_status')
177 ->orWhere('payment_status', '!=', Status::PAYMENT_PAID);
178 })
179 ->update([
180 'status' => $orderStatus,
181 'payment_status' => $orderPaymentStatus,
182 'total_paid' => $transactionPaidTotal,
183 'total_refund' => $refundedTotal,
184 ]);
185
186 if (!$claimed) {
187 // Another process already transitioned this order to paid
188 $this->order = Order::query()->where('id', $this->order->id)->first();
189 return $this->order;
190 }
191
192 // Refresh in-memory model to stay in sync with the DB after query-builder update
193 $this->order = Order::query()->where('id', $this->order->id)->first();
194 } else {
195 $this->order->save();
196 }
197
198 if (($this->order->type === 'renewal') || ($oldPaymentStatus != $this->order->payment_status && $this->order->payment_status == Status::PAYMENT_PAID)) {
199 if (!$latestTransaction) {
200 $latestTransaction = OrderTransaction::query()
201 ->where('order_id', $this->order->id)
202 ->orderBy('id', 'desc')
203 ->first();
204 }
205
206 $relatedCart = Cart::query()->where('order_id', $this->order->id)
207 ->where('stage', '!=', 'completed')
208 ->first();
209
210 if ($relatedCart) {
211 $relatedCart->stage = 'completed';
212 $relatedCart->completed_at = DateTime::now()->format('Y-m-d H:i:s');
213 $relatedCart->save();
214
215 do_action('fluent_cart/cart_completed', [
216 'cart' => $relatedCart,
217 'order' => $this->order,
218 ]);
219
220 $onSuccessActions = Arr::get($relatedCart->checkout_data, '__on_success_actions__', []);
221
222 if ($onSuccessActions) {
223 foreach ($onSuccessActions as $onSuccessAction) {
224 $onSuccessAction = (string)$onSuccessAction;
225 if (has_action($onSuccessAction)) {
226 do_action($onSuccessAction, [
227 'cart' => $relatedCart,
228 'order' => $this->order,
229 'transaction' => $latestTransaction
230 ]);
231 }
232 }
233 }
234 }
235
236 if ($this->order->type !== 'renewal' && $oldPaymentStatus != $this->order->payment_status && $this->order->payment_status == Status::PAYMENT_PAID) {
237 (new OrderPaid($this->order, $this->order->customer, $latestTransaction))->dispatch();
238 }
239 }
240
241 if ($oldOrderStatus != $this->order->status) {
242 $actionActivity = [
243 'title' => __('Order status updated', 'fluent-cart'),
244 'content' => sprintf(
245 /* translators: 1: old status, 2: new status */
246 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), $oldOrderStatus, $this->order->status)
247 ];
248 (new OrderStatusUpdated($this->order, $oldOrderStatus, $this->order->status, true, $actionActivity, 'order_status'))->dispatch();
249 }
250
251 $autoCompleteDigitalOrder = apply_filters('fluent_cart/order_status/auto_complete_digital_order', true, [
252 'order' => $this->order,
253 ]);
254
255 // Now if it's a digital product so we will make it auto completed
256 if ($this->order->fulfillment_type == 'digital' && $autoCompleteDigitalOrder
257 && !$refundedTotal && ($oldOrderStatus != $this->order->status)
258 && ($this->order->status != Status::ORDER_COMPLETED)
259 ) {
260 $oldOrderStatus = $this->order->status;
261 $this->order->status = Status::ORDER_COMPLETED;
262 $this->order->completed_at = DateTime::gmtNow();
263 $this->order->save();
264
265 $actionActivity = [
266 'title' => __('Order status updated', 'fluent-cart'),
267 'content' => sprintf(
268 /* translators: 1: old status, 2: new status */
269 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), $oldOrderStatus, $this->order->status)
270 ];
271
272 (new OrderStatusUpdated($this->order, $oldOrderStatus, $this->order->status, true, $actionActivity, 'order_status'))->dispatch();
273 }
274
275 return $this->order;
276 }
277 }
278