PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 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 All 51 releases
fluent-cart / app / Http / Controllers / OrderController.php

OrderController.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.1, at app/Http/Controllers/OrderController.php

1,361 lines 49.9 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\Http\Controllers;
4
5
6 use FluentCart\Api\Resource\CustomerResource;
7 use FluentCart\Api\Resource\OrderResource;
8 use FluentCart\Api\StoreSettings;
9 use FluentCart\App\Events\Order\OrderCreated;
10 use FluentCart\App\Events\Order\OrderDeleting;
11 use FluentCart\App\Events\Order\OrderDeleted;
12 use FluentCart\App\Events\Order\RenewalOrderDeleted;
13 use FluentCart\App\Helpers\CartHelper;
14 use FluentCart\App\Helpers\Helper;
15 use FluentCart\App\Helpers\OrderItemHelper;
16 use FluentCart\App\Helpers\Status;
17 use FluentCart\App\Helpers\StatusHelper;
18 use FluentCart\App\Http\Requests\CustomerRequest;
19 use FluentCart\App\Http\Requests\OrderRequest;
20 use FluentCart\App\Models\Customer;
21 use FluentCart\App\Models\CustomerAddresses;
22 use FluentCart\App\Models\CustomerMeta;
23 use FluentCart\App\Models\Order;
24 use FluentCart\App\Models\OrderAddress;
25 use FluentCart\App\Models\OrderItem;
26 use FluentCart\App\Models\OrderMeta;
27 use FluentCart\App\Models\OrderOperation;
28 use FluentCart\App\Models\OrderTaxRate;
29 use FluentCart\App\Models\OrderTransaction;
30 use FluentCart\App\Models\ProductVariation;
31 use FluentCart\App\Models\ShippingMethod;
32 use FluentCart\App\Models\Activity;
33 use FluentCart\App\Models\AppliedCoupon;
34 use FluentCart\App\Models\Cart;
35 use FluentCart\App\Models\LabelRelationship;
36 use FluentCart\App\Models\OrderDownloadPermission;
37 use FluentCart\App\Models\Subscription;
38 use FluentCart\App\Models\SubscriptionMeta;
39 use FluentCart\App\Services\Filter\OrderFilter;
40 use FluentCart\App\Services\Payments\PaymentHelper;
41 use FluentCart\App\Services\Reminders\ReminderService;
42 use FluentCart\App\Services\Payments\Refund;
43 use FluentCart\App\Services\URL;
44 use FluentCart\Framework\Http\Request\Request;
45 use FluentCart\Framework\Support\Arr;
46 use FluentCart\Framework\Support\Collection;
47 use FluentCart\Framework\Validator\ValidationException;
48 use FluentCartPro\App\Hooks\Handlers\OrderActionsHandler;
49
50 class OrderController extends Controller
51 {
52 protected function getTestOrdersQuery()
53 {
54 return Order::query()
55 ->where('mode', Status::ORDER_MODE_TEST);
56 }
57
58 public function index(Request $request): \WP_REST_Response
59 {
60 $orders = OrderFilter::fromRequest($request)->paginate();
61
62 $orders = apply_filters('fluent_cart/orders_list', $orders);
63
64 return $this->sendSuccess(
65 [
66 'orders' => $orders,
67 ]
68 );
69 }
70
71 /**
72 * @throws \Exception
73 */
74 public function store(OrderRequest $request)
75 {
76 $data = $request->getSafe($request->sanitize());
77 $type = 'payment';
78 $orderItems = Arr::get($data, 'order_items', []);
79
80 $variationPaymentTypes = static::getVariationPaymentTypes($orderItems);
81
82 foreach ($orderItems as $item) {
83 $paymentTypeError = static::getPaymentTypeConflict($item, $variationPaymentTypes);
84 if ($paymentTypeError) {
85 return $this->sendError([
86 'message' => $paymentTypeError
87 ], 400);
88 }
89 }
90
91 $hasSubscription = static::hasSubscription($orderItems);
92 if ($hasSubscription) {
93 $type = 'subscription';
94 // right now we don't support subscription with manual order
95 $isSubscriptionAllowedInManualOrder = apply_filters('fluent_cart/order/is_subscription_allowed_in_manual_order', true, [
96 'order_items' => $orderItems
97 ]);
98
99 if (!$isSubscriptionAllowedInManualOrder) {
100 return $this->sendError([
101 'message' => __('Subscription order with Manual Order is not supported yet!', 'fluent-cart')
102 ], 400);
103 }
104
105 }
106
107
108 $data['type'] = apply_filters('fluent_cart/order/type', $type, []);
109 $order = OrderResource::updatedPlaceOrder($data);
110
111
112 if (is_wp_error($order)) {
113 return $order;
114 }
115
116 // isCreated is an orderHelper instance
117 (new OrderCreated($order, null, $order->customer))->dispatch();
118
119 return $this->response->sendSuccess([
120 'message' => __('Order created successfully!', 'fluent-cart'),
121 'order_id' => $order->id,
122 'uuid' => $order->uuid
123 ]);
124 }
125
126 public static function hasSubscription($orderItems): bool
127 {
128 foreach ($orderItems as $item) {
129 if (Arr::get($item, 'payment_type') == 'subscription' || Arr::get($item, 'other_info.payment_type') == 'subscription') {
130 return true;
131 }
132 }
133
134 return false;
135 }
136
137 /**
138 * The variation row decides whether a line is recurring, so every label the payload
139 * carries has to agree with it. A recurring line must additionally be labelled in
140 * other_info: that is the only copy AdminOrderProcessor reads, and the interval and
141 * installment count travel beside it.
142 *
143 * @return string empty when the line is consistent, else the rejection message
144 */
145 protected static function getPaymentTypeConflict($item, $variationPaymentTypes): string
146 {
147 $variationId = (int)Arr::get($item, 'object_id', 0);
148
149 if (!isset($variationPaymentTypes[$variationId])) {
150 return '';
151 }
152
153 $isSubscriptionVariation = $variationPaymentTypes[$variationId] === 'subscription';
154
155 foreach (['payment_type', 'other_info.payment_type'] as $labelKey) {
156 $label = Arr::get($item, $labelKey);
157 if (is_null($label) || $label === '') {
158 continue;
159 }
160
161 if (($label === 'subscription') !== $isSubscriptionVariation) {
162 return $isSubscriptionVariation
163 ? __('Subscription product cannot be placed as a one time item.', 'fluent-cart')
164 : __('One time product cannot be placed as a subscription item.', 'fluent-cart');
165 }
166 }
167
168 if ($isSubscriptionVariation && Arr::get($item, 'other_info.payment_type') !== 'subscription') {
169 return __('Subscription product must be placed as a subscription item.', 'fluent-cart');
170 }
171
172 return '';
173 }
174
175 /**
176 * @return array variation id => stored payment_type, for the lines that resolve
177 */
178 protected static function getVariationPaymentTypes($orderItems): array
179 {
180 $variationIds = [];
181 foreach ($orderItems as $item) {
182 $variationId = (int)Arr::get($item, 'object_id', 0);
183 if ($variationId > 0) {
184 $variationIds[$variationId] = $variationId;
185 }
186 }
187
188 if (!$variationIds) {
189 return [];
190 }
191
192 $variations = ProductVariation::query()
193 ->whereIn('id', $variationIds)
194 ->get(['id', 'payment_type']);
195
196 $paymentTypes = [];
197 foreach ($variations as $variation) {
198 $paymentTypes[(int)$variation->id] = $variation->payment_type;
199 }
200
201 return $paymentTypes;
202 }
203
204 public function updateOrder(OrderRequest $request, $order_id)
205 {
206 $order = Order::query()->find($order_id);
207
208 if ($order->isSubscription()) {
209 return $this->sendError([
210 'message' => __('Subscription Order cannot be edited.', 'fluent-cart')
211 ], 400);
212 }
213
214
215 $requestData = array_intersect_key(
216 $request->getSafe($request->sanitize()),
217 $request->all()
218 );
219
220 $totalPaid = Arr::get($request->all(), 'total_paid');
221 $updatedTotal = Arr::get($requestData, 'total_amount');
222
223 if ($totalPaid > 0 && floatval($updatedTotal > $totalPaid)
224 && isset($requestData['payment_status'])
225 && $requestData['payment_status'] !== Status::PAYMENT_PARTIALLY_REFUNDED
226 ) {
227 $requestData['payment_status'] = Status::PAYMENT_PARTIALLY_PAID;
228 }
229
230 $status = Arr::get($requestData, 'status');
231 if ($status == Status::ORDER_COMPLETED) {
232 return $this->sendError([
233 'message' => esc_html__('Completed status can not be updated', 'fluent-cart')
234 ], 400);
235 }
236
237 // if new shipping total is already adjusted in total amount, then no need to adjust again, right now not adjusted before
238 // ToDo: adjust changed shipping total in total amount prior to this
239 $shippingTotal = Arr::get($requestData, 'shipping_total', 0);
240 $oldShippingTotal = Arr::get($order, 'shipping_total', 0);
241 if (array_key_exists('shipping_total', $requestData) && $shippingTotal != $oldShippingTotal) {
242 $diff = $shippingTotal - $oldShippingTotal;
243 if ($diff < 0) {
244 $requestData['total_amount'] = $updatedTotal - abs($diff);
245 } else {
246 $requestData['total_amount'] = $updatedTotal + $diff;
247 }
248 }
249
250 $data = [
251 'orderData' => $requestData,
252 'deletedItems' => Arr::get($requestData, 'deletedItems', []),
253 'discount' => Arr::get($requestData, 'discount', ''),
254 'shipping' => Arr::get($requestData, 'shipping', ''),
255 'couponCalculation' => Arr::get($requestData, 'couponCalculation', []),
256 ];
257
258 $isUpdated = OrderResource::update($data, $order->id);
259
260 if (is_wp_error($isUpdated)) {
261 return $isUpdated;
262 }
263
264 return $this->response->sendSuccess($isUpdated);
265 }
266
267 public function updateOrderAddressId(Request $request, $order_id)
268 {
269 $order = Order::query()->find($order_id);
270
271
272 if (!$order) {
273 return $this->sendError([
274 'message' => __('Order not found', 'fluent-cart')
275 ], 404);
276 }
277 $data = OrderResource::updateOrderAddressId($request->only([
278 'address_id',
279 'address_type'
280 ]), $order);
281
282 if (is_wp_error($data)) {
283 return $this->sendError($data->get_error_message());
284 }
285
286 // Changing the order address recalculates tax server-side
287 // (OrderResource::updateOrderAddressId → reapplyTaxAfterUpdate). Return the
288 // refreshed order with the tax appends so the admin UI can update the tax
289 // summary, totals and payment status without a full page reload.
290 $freshOrder = Order::query()->where('id', $order_id)
291 ->addAppends([
292 'business_info',
293 'customer_tax_number',
294 'is_b2b_order',
295 'display_tax_lines',
296 'display_shipping_tax_lines',
297 'is_reverse_charge_tax_order',
298 'tax_summary',
299 ])
300 ->first();
301
302 return $this->sendSuccess([
303 'message' => __('Address updated successfully', 'fluent-cart'),
304 'order' => $freshOrder,
305 ]);
306 }
307
308 public function generateMissingLicenses(Request $request, Order $order)
309 {
310 if (!$order) {
311 return $this->sendError([
312 'message' => __('Order not found', 'fluent-cart')
313 ], 404);
314 }
315
316 $generatedLicenseCount = $order->licenses->count();
317 $expectedLicenseCount = apply_filters('fluent_cart/order/expected_license_count', 0, [
318 'order_items' => $order->order_items
319 ]);
320
321 if ($generatedLicenseCount >= $expectedLicenseCount) {
322 return $this->sendError([
323 'message' => __('No missing licenses found!', 'fluent-cart')
324 ], 400);
325 }
326
327 do_action('fluent_cart/order/generateMissingLicenses', ['order' => $order]);
328
329 }
330
331 /**
332 * Refund against an order transaction.
333 *
334 * `refund_info.amount` is in CENTS, matching every money value in a read response and
335 * the stored column. So {"amount": 2500} refunds $25.00. roundCent() below only
336 * normalizes float artifacts; it does not scale. See dev-docs/PRICING-AND-TAX.md §6.
337 *
338 * @throws ValidationException
339 */
340 public function refundOrder(Request $request, $orderId)
341 {
342 $order = Order::query()->findOrFail($orderId);
343
344 if (!$order->canBeRefunded()) {
345 return $this->sendError([
346 'message' => __('Order can not be refunded.', 'fluent-cart')
347 ], 400);
348 }
349
350 $refundInfo = (array)$request->get('refund_info', []);
351
352 // $this->validate() reports failures only by exception, and outside a
353 // REST_REQUEST context the framework swallows that exception (no
354 // handle_exception listener) — execution would continue and crash on
355 // $refundInfo['transaction_id'] below. Fail closed: run the validator
356 // directly and return the per-field 422 payload in every context.
357 $validator = $this->app->validator->make($refundInfo, [
358 'transaction_id' => 'required',
359 'amount' => 'required',
360 ], [
361 'transaction_id.required' => __('Transaction ID is required', 'fluent-cart'),
362 'amount.required' => __('Refund amount is required', 'fluent-cart'),
363 ]);
364
365 if ($validator->validate()->fails()) {
366 return $this->sendError($validator->errors(), 422);
367 }
368
369 $transaction = OrderTransaction::query()->where('order_id', $orderId)->findOrFail($refundInfo['transaction_id']);
370 $refundAmount = Helper::roundCent($refundInfo['amount']);
371
372 // refund on our end
373 $result = (new Refund())->processRefund($transaction, $refundAmount, $refundInfo);
374
375 if (is_wp_error($result)) {
376 return $this->sendError([
377 'message' => $result->get_error_message()
378 ]);
379 }
380
381 $vendorRefundId = $result['vendor_refund_id'];
382 $isManuallyRefunded = Arr::get($result, 'manual_refund.status', 'no');
383
384 $responseData = [
385 'fluent_cart_refund' => [
386 'status' => 'success',
387 'message' => __('Refund processed on FluentCart.', 'fluent-cart')
388 ],
389 ];
390
391
392 if ($isManuallyRefunded === 'yes') {
393 $message = __('Refund processed manually.', 'fluent-cart');
394 $source = Arr::get($result, 'manual_refund.source');
395 if ($source) {
396 $message = sprintf(
397 __('Refund processed manually. source: %s', 'fluent-cart'),
398 $source
399 );
400 }
401 $responseData['gateway_refund'] = [
402 'status' => 'success',
403 'message' => $message
404 ];
405 } else {
406 $responseData['gateway_refund'] = [
407 'status' => is_wp_error($vendorRefundId) ? 'failed' : 'success',
408 'message' => !is_wp_error($vendorRefundId)
409 ? sprintf(__('Refund processed on %s', 'fluent-cart'), ucfirst($transaction->payment_method))
410 : sprintf(__('ERROR processing refund on %s: %s', 'fluent-cart'), ucfirst($transaction->payment_method), $vendorRefundId->get_error_message())
411 ];
412
413 if (is_wp_error($vendorRefundId)) {
414 fluent_cart_warning_log('Refund failed on ' . ucfirst($transaction->payment_method), $vendorRefundId->get_error_message(), [
415 'module_name' => 'order',
416 'module_id' => $order->id,
417 'log_type' => 'api'
418 ]);
419 }
420 }
421
422 $cancelSubscription = Arr::get($refundInfo, 'cancelSubscription') == 'true';
423
424 if ($cancelSubscription && $transaction->subscription_id && $transaction->subscription) {
425 $vendorSubscriptionCancelled = $transaction->subscription->cancelRemoteSubscription([
426 'reason' => 'refunded',
427 'effective_from' => 'immediately'
428 ]);
429 if (is_wp_error($vendorSubscriptionCancelled)) {
430 $responseData['subscription_cancel']['status'] = 'failed';
431 $responseData['subscription_cancel']['message'] = $vendorSubscriptionCancelled->get_error_message();
432 } else {
433 $vendorResult = $vendorSubscriptionCancelled['vendor_result'];
434 $responseData['subscription_cancel']['status'] = is_wp_error($vendorResult) ? 'failed' : 'success';
435 $responseData['subscription_cancel']['message'] = is_wp_error($vendorResult)
436 ? $vendorResult->get_error_message()
437 : __('Subscription cancelled successfully', 'fluent-cart');
438 }
439 }
440
441 return $this->sendSuccess(
442 $responseData
443 );
444 }
445
446
447 public function createAndChangeCustomer(CustomerRequest $request, $order_id)
448 {
449
450 $data = $request->getSafe($request->sanitize());
451 $isCreated = CustomerResource::create($data);
452
453 if (is_wp_error($isCreated)) {
454 return $this->sendError(
455 [
456 'message' => __('Failed to attach customer', 'fluent-cart')
457 ]
458 );
459 }
460
461 $customerId = Arr::get($isCreated, 'data.id');
462
463 $isChanged = $this->updateOrderCustomer($customerId, $order_id);
464 if (is_wp_error($isChanged)) {
465 return $this->sendError(
466 [
467 'message' => $isChanged->get_error_message()
468 ]
469 );
470 }
471 return $this->sendSuccess($isChanged);
472
473 }
474
475 public function changeCustomer(Request $request, $order_id)
476 {
477 $customerId = $request->get('customer_id');
478 $customerId = sanitize_text_field($customerId);
479
480 if (!$customerId) {
481 return $this->sendError([
482 'message' => __('Customer id is required', 'fluent-cart')
483 ], 423);
484 }
485
486 $isChanged = $this->updateOrderCustomer($customerId, $order_id);
487 if (is_wp_error($isChanged)) {
488 return $this->sendError(
489 [
490 'message' => $isChanged->get_error_message()
491 ]
492 );
493 }
494 return $this->sendSuccess($isChanged);
495
496 }
497
498 private function updateOrderCustomer($customerId, $orderId)
499 {
500
501 /**
502 * 1. Check if it's a different customer
503 * 2. Update main order.customer_id
504 * 3. update subscription.customer_id
505 * 4. update license.customer_id
506 *
507 *
508 * // critical thinking
509 * 5. If it's a renewal then change the parent order's data as well it's all child resources
510 * 6. Recount Customer's stat (New as well as the old one!)
511 */
512
513 $order = Order::query()->findOrFail($orderId);
514
515 if ($order->customer_id == $customerId) {
516 return [
517 'message' => __('Customer is already attached to this order', 'fluent-cart')
518 ];
519 }
520 $newCustomer = Customer::query()->findOrFail($customerId);
521 $oldCustomerId = $order->customer_id;
522
523 CustomerAddresses::query()->where('customer_id', $oldCustomerId)->update(['customer_id' => $customerId]);
524 CustomerMeta::query()->where('customer_id', $oldCustomerId)->update(['customer_id' => $customerId]);
525
526 $connectedOrderIds = [$order->id];
527 if ($order->parent_id && $order->type == 'renewal') {
528 $connectedOrderIds[] = $order->parent_id;
529 $parentOrderIdsOrders = Order::query()->where('parent_id', $order->parent_id)->get()->pluck('id')->toArray();
530 $connectedOrderIds = array_merge($parentOrderIdsOrders, $connectedOrderIds);
531
532 } else if ($order->type == 'subscription') {
533 $childOrderIds = Order::query()->where('parent_id', $order->id)->pluck('id')->toArray();
534 $connectedOrderIds = array_merge($childOrderIds, $connectedOrderIds);
535 }
536 Order::query()->whereIn('id', $connectedOrderIds)->update(['customer_id' => $customerId]);
537 Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->update(['customer_id' => $customerId]);
538
539 $newCustomer->recountStat();
540 $oldCustomer = Customer::query()->find($oldCustomerId);
541 if (!empty($oldCustomer)) {
542 $oldCustomer->recountStat();
543 }
544
545 do_action('fluent_cart/order_customer_changed', [
546 'order' => $order,
547 'old_customer' => $oldCustomer,
548 'new_customer' => $newCustomer,
549 'connected_order_ids' => $connectedOrderIds
550 ]);
551
552 fluent_cart_success_log(
553 __('Customer changed', 'fluent-cart'),
554 sprintf(
555 /* translators: 1: old customer name, 2: new customer name */
556 __('Customer changed from %1$s to %2$s', 'fluent-cart'), $oldCustomer->full_name, $newCustomer->full_name),
557 [
558 'module_name' => 'order',
559 'module_id' => $orderId,
560 'log_type' => 'activity'
561 ]);
562
563 return [
564 'message' => __('Customer changed successfully', 'fluent-cart')
565 ];
566 }
567
568 public function deleteOrder(Request $request, $order_id)
569 {
570
571 $order = Order::query()->find($order_id);
572
573 if (empty($order)) {
574 return $this->sendError([
575 'message' => __('Order not found', 'fluent-cart'),
576 'data' => [
577 'order_id' => $order_id,
578 'status' => 'error'
579 ],
580 'errors' => []
581 ], 404);
582 }
583
584 $order_id = $order->id; // Get the single order ID
585 // Find the order with additional details
586
587 $canBeDeleted = $order->canBeDeleted();
588 if (is_wp_error($canBeDeleted)) {
589
590 return $this->sendError([
591 'message' => $canBeDeleted->get_error_message(),
592 'data' => [
593 'order_id' => $order_id,
594 'invoice_no' => $order->invoice_no,
595 'status' => 'error',
596 'reason' => $canBeDeleted->get_error_code()
597 ],
598 'errors' => [
599 $canBeDeleted->get_error_message()
600 ]
601 ], 400);
602 }
603
604
605 $DB = \FluentCart\App\App::db();
606 $connectedOrderIds = [$order->id];
607 $isTestMode = $order->mode === Status::ORDER_MODE_TEST;
608
609 if ($order->type === 'subscription') {
610 $childOrderIds = Order::query()->where('parent_id', $order->id)->pluck('id')->toArray();
611 $connectedOrderIds = array_merge($childOrderIds, $connectedOrderIds);
612 }
613
614 try {
615 $DB->beginTransaction();
616
617 if ($order->type === 'subscription') {
618 $subscriptionIds = Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->pluck('id')->toArray();
619 if ($subscriptionIds) {
620 SubscriptionMeta::query()->whereIn('subscription_id', $subscriptionIds)->delete();
621 }
622
623 Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->delete();
624 }
625
626 // Dispatch inside transaction so stock restore is atomic with deletion.
627 // Must run before deleteOrderRelatedData() which removes stock_movement meta and order items.
628 (new OrderDeleting($order, $connectedOrderIds, $isTestMode, $order->type))->dispatch();
629
630 // Pre-load relations before cleanup so the delete events have address data
631 $order->load(['customer', 'shipping_address', 'billing_address']);
632
633 $this->deleteOrderRelatedData($connectedOrderIds, $isTestMode);
634 $DB->commit();
635 } catch (\Exception $e) {
636 $DB->rollBack();
637 return $this->sendError([
638 'message' => __('Failed to delete order', 'fluent-cart'),
639 ], 400);
640 }
641
642 if ($order->type === 'renewal') {
643 (new RenewalOrderDeleted($order))->dispatch();
644 } else {
645 (new OrderDeleted($order, $connectedOrderIds))->dispatch();
646 }
647
648 return $this->sendSuccess([
649 'message' => sprintf(
650 /* translators: %s is the order/invoice number */
651 __('Order %s deleted successfully', 'fluent-cart'), $order_id),
652 'data' => [
653 'order_id' => $order_id,
654 'invoice_no' => $order->invoice_no,
655 'status' => 'success'
656 ],
657 'errors' => []
658 ]);
659
660 }
661
662 /**
663 * Delete order related data (transactions, items, meta, addresses, orders)
664 */
665 private function deleteOrderRelatedData(array $orderIds, bool $isTestMode = false): void
666 {
667 OrderTransaction::query()->whereIn('order_id', $orderIds)->delete();
668 OrderAddress::query()->whereIn('order_id', $orderIds)->delete();
669 OrderItem::query()->whereIn('order_id', $orderIds)->delete();
670 OrderMeta::query()->whereIn('order_id', $orderIds)->delete();
671 OrderTaxRate::query()->whereIn('order_id', $orderIds)->delete();
672 OrderOperation::query()->whereIn('order_id', $orderIds)->delete();
673 AppliedCoupon::query()->whereIn('order_id', $orderIds)->delete();
674 Cart::query()->whereIn('order_id', $orderIds)->delete();
675 OrderDownloadPermission::query()->whereIn('order_id', $orderIds)->delete();
676 LabelRelationship::query()->where('labelable_type', Order::class)
677 ->whereIn('labelable_id', $orderIds)->delete();
678
679 if ($isTestMode) {
680 Activity::query()->where('module_type', Order::class)
681 ->whereIn('module_id', $orderIds)->delete();
682 }
683
684 Order::query()->whereIn('id', $orderIds)->delete();
685 }
686
687
688 public function getDetails($orderId)
689 {
690 $data = OrderResource::view($orderId);
691
692 if (is_wp_error($data) || empty($data['order'])) {
693 return $this->entityNotFoundError(
694 __('Order not found', 'fluent-cart'),
695 __('Back to orders', 'fluent-cart'),
696 '/orders'
697 );
698 }
699
700 $data['order'] = apply_filters('fluent_cart/order/view', $data['order'], []);
701
702 // check if the order has generated license
703 $data['order']['has_missing_licenses'] = false;
704
705 $expectedLicenseCount = apply_filters('fluent_cart/order/expected_license_count', 0, [
706 'order_items' => Arr::get($data, 'order.order_items', [])
707 ]);
708
709 $generatedLicenseCount = count(Arr::get($data, 'order.licenses', []));
710 if ($expectedLicenseCount && ($expectedLicenseCount > $generatedLicenseCount)) {
711 $data['order']['has_missing_licenses'] = true;
712 }
713
714 $data['order']['order_operation'] = OrderOperation::query()->where('order_id', $orderId)
715 ->first();
716
717 $url = URL::appendQueryParams(
718 (new StoreSettings())->getReceiptPage(),
719 [
720 'order_hash' => Arr::get($data, 'order.uuid')
721 ]
722 );
723
724 if (empty($data['order']['receipt_url'])) {
725 $data['order']['receipt_url'] = $url;
726 }
727 $taxNumber = Arr::get($data, 'order.customer_tax_number', '');
728 if (!empty($taxNumber)) {
729 $data['tax_id'] = $taxNumber;
730 }
731 unset($data['order']['customer_tax_number']);
732
733 $data['can_send_payment_reminder'] = (new ReminderService())->canSendPaymentReminder($data['order']);
734
735 return $data;
736 }
737
738 public function getTransactionDetails($orderId, $transactionId)
739 {
740 $orderId = (int)$orderId;
741 $transactionId = (int)$transactionId;
742
743 $belongsToOrder = OrderTransaction::query()
744 ->where('id', $transactionId)
745 ->where('order_id', $orderId)
746 ->exists();
747
748 if (!$belongsToOrder) {
749 return $this->entityNotFoundError(
750 __('Transaction not found', 'fluent-cart'),
751 __('Back to orders', 'fluent-cart'),
752 '/orders'
753 );
754 }
755
756 $data = $this->getDetails($orderId);
757
758 if (!is_array($data) || empty($data['order'])) {
759 return $data;
760 }
761
762 // The path names one transaction, so the sibling rows on the same order
763 // are not part of this response.
764 $data['order']['transactions'] = array_values(array_filter(
765 (array)Arr::get($data, 'order.transactions', []),
766 function ($transaction) use ($transactionId) {
767 return (int)Arr::get($transaction, 'id') === $transactionId;
768 }
769 ));
770
771 return $data;
772 }
773
774 public function createCustom(Request $request, OrderItemHelper $orderItemHelper, Order $order)
775 {
776 try {
777 $orderItem = $orderItemHelper->processCustom(
778 $request->product,
779 $order->id
780 );
781
782 return $this->sendSuccess([
783 'message' => __('Custom item has been added to the order!', 'fluent-cart'),
784 'order_item' => $orderItem
785 ]);
786
787 } catch (\Exception $e) {
788 return $this->sendError([
789 'message' => $e->getMessage()
790 ], 423);
791 }
792
793 }
794
795 // public function calculate(Request $request, OrderHelper $orderHelper)
796 // {
797 // return $orderHelper->calculate($request->order);
798 // }
799
800 public function updateStatuses(Request $request, Order $order)
801 {
802
803 $data = [
804 'order' => $order,
805 'statuses' => $request->get('statuses', []),
806 'manage_stock' => $request->get('manage_stock'),
807 'action' => $request->get('action')
808 ];
809 $isUpdated = OrderResource::updateStatuses($data);
810
811 if (is_wp_error($isUpdated)) {
812 return $isUpdated;
813 }
814 return $this->response->sendSuccess($isUpdated);
815 }
816
817 public function updateOrderAddress(Request $request, $orderId, $addressId)
818 {
819
820 $data = $request->all();
821
822 $isUpdated = OrderResource::updateOrderAddress($data);
823
824 if (is_wp_error($isUpdated)) {
825 return $isUpdated;
826 }
827 return $this->response->sendSuccess($isUpdated);
828 }
829
830
831 public function markAsPaid(Request $request, Order $order)
832 {
833 $db = Order::query()->getConnection();
834 $db->beginTransaction();
835
836 try {
837 $locked = Order::query()
838 ->where('id', $order->id)
839 ->lockForUpdate()
840 ->first();
841
842 if (!$locked) {
843 $db->rollBack();
844 return $this->sendError([
845 'message' => __('Order not found', 'fluent-cart')
846 ], 404);
847 }
848
849 $dueAmount = intval($locked->total_amount - $locked->total_paid);
850
851 if ($dueAmount <= 0) {
852 $db->rollBack();
853 return $this->sendError([
854 'message' => __('Order has already been paid', 'fluent-cart')
855 ], 423);
856 }
857
858 if ($locked->status === Status::ORDER_CANCELED) {
859 $db->rollBack();
860 return $this->sendError([
861 'message' => __('Unable to mark paid for canceled order', 'fluent-cart')
862 ], 423);
863 }
864
865 // Reuse an existing pending transaction without vendor_charge_id instead of
866 // creating a new one. Queried fresh (not via the route-bound relation) so it
867 // reflects the state under the lock.
868 $transaction = OrderTransaction::query()
869 ->where('order_id', $locked->id)
870 ->where('status', Status::TRANSACTION_PENDING)
871 ->where(function ($query) {
872 $query->whereNull('vendor_charge_id')
873 ->orWhere('vendor_charge_id', '');
874 })
875 ->orderBy('id', 'asc')
876 ->lockForUpdate()
877 ->first();
878
879 $newTransactionData = [
880 'total' => $dueAmount,
881 'status' => Status::TRANSACTION_SUCCEEDED,
882 'payment_method' => sanitize_text_field($request->payment_method),
883 'vendor_charge_id' => sanitize_text_field($request->vendor_charge_id),
884 'payment_mode' => sanitize_text_field($locked->mode),
885 'payment_method_type' => sanitize_text_field($request->payment_method),
886 'order_type' => sanitize_text_field($locked->type),
887 'currency' => sanitize_text_field($locked->currency),
888 ];
889
890 if ($transaction) {
891 // Don't include transaction_type in the update — the existing value is always 'charge'
892 // and overwriting it with the request value would break syncSubscriptionStates bill_count.
893 $transaction->update($newTransactionData);
894 } else {
895 $transaction = OrderTransaction::query()->create(
896 array_merge($newTransactionData, [
897 'order_id' => $locked->id,
898 'transaction_type' => Status::TRANSACTION_TYPE_CHARGE,
899 ])
900 );
901 }
902
903 // Persist the settled balance while the row lock is held so the next request
904 // to acquire it computes due = 0. payment_status is deliberately left alone:
905 // syncOrderStatuses() owns the atomic pending → paid claim that dispatches
906 // OrderPaid exactly once, and it runs after commit so third-party hook
907 // callbacks (emails, integrations, subscription activation) never execute
908 // while the order row is locked.
909 $locked->total_paid = (int) OrderTransaction::query()
910 ->where('order_id', $locked->id)
911 ->whereIn('status', Status::getTransactionSuccessStatuses())
912 ->sum('total');
913
914 $locked->save();
915
916 $db->commit();
917 } catch (\Throwable $e) {
918 $db->rollBack();
919 throw $e;
920 }
921
922 (new StatusHelper($locked))->syncOrderStatuses($transaction);
923
924 $paymentNote = sanitize_textarea_field($request->get('mark_paid_note', ''));
925 if ($paymentNote) {
926 $locked->addLog(
927 __('Payment note', 'fluent-cart'),
928 nl2br(esc_html($paymentNote)),
929 'info',
930 wp_get_current_user()->display_name
931 );
932 }
933
934 return $this->response->sendSuccess([
935 'message' => __('Order has been marked as paid', 'fluent-cart')
936 ]);
937 }
938
939 public function handleBulkActions(Request $request)
940 {
941
942 $action = sanitize_text_field($request->get('action', ''));
943 $orderIds = $request->get('order_ids', []);
944
945 if ($action == 'delete_test_orders') {
946 return $this->handleDeleteTestOrdersBulkAction($request);
947 }
948
949 $orderIds = array_map(function ($id) {
950 return (int)$id;
951 }, $orderIds);
952
953 $orderIds = array_filter($orderIds);
954
955 if (!$orderIds) {
956 return $this->sendError([
957 'message' => __('Orders selection is required', 'fluent-cart')
958 ]);
959 }
960
961 if ($action == 'delete_orders') {
962
963 $isDeleted = OrderResource::bulkDeleteByOrderIds($orderIds);
964
965 if (is_wp_error($isDeleted)) {
966 return $isDeleted;
967 }
968 return $this->response->sendSuccess($isDeleted);
969
970 // $DB = App::db();
971 // $DB->beginTransaction();
972
973 // try {
974
975 // OrderResource::bulkDeleteByOrderIds($orderIds);
976 // OrderTransaction::bulkDeleteByOrderIds($orderIds);
977 // OrderMetaResource::bulkDeleteByOrderIds($orderIds);
978 // OrderItemResource::bulkDeleteByOrderIds($orderIds);
979
980 // $DB->commit();
981
982 // return [
983 // 'message' => __('Selected orders and their associated resources have been deleted permanently', 'fluent-cart')
984 // ];
985 // } catch (\Exception $e) {
986 // $DB->rollBack();
987 // return static::makeErrorResponse([
988 // ['code' => 400, 'message' => __('Failed to delete orders and their associated resources', 'fluent-cart')]
989 // ]);
990 // }
991
992
993 }
994
995 // The capture_payments branch was removed: it called
996 // $order->capturePayments(), a method that has never existed anywhere
997 // in the codebase, so the action fataled on the first order (audit
998 // item #43). No UI sends it — the orders bulk bar submits
999 // delete_test_orders only. Bulk payment capture, if wanted, is a
1000 // gateway feature to design (authorize/capture per gateway), not a
1001 // branch to resurrect as-is.
1002
1003 return $this->sendError([
1004 'message' => __('Selected action is invalid', 'fluent-cart')
1005 ]);
1006
1007 }
1008
1009 protected function handleDeleteTestOrdersBulkAction(Request $request)
1010 {
1011 $batchSize = max(1, (int)apply_filters('fluent_cart/order/delete_test_orders_batch_size', 50));
1012 $lastOrderId = max(0, (int)$request->get('last_order_id', 0));
1013 $totalCount = (int)$this->getTestOrdersQuery()->count();
1014
1015 $testOrders = $this->getTestOrdersQuery()
1016 ->select('id')
1017 ->orderBy('id')
1018 ->when($lastOrderId > 0, function ($query) use ($lastOrderId) {
1019 return $query->where('id', '>', $lastOrderId);
1020 })
1021 ->limit($batchSize)
1022 ->get();
1023
1024 $batchOrderIds = $testOrders->pluck('id')->map(function ($id) {
1025 return (int)$id;
1026 })->toArray();
1027
1028 if (!$batchOrderIds) {
1029 return $this->sendSuccess([
1030 'message' => $lastOrderId
1031 ? __('Test order deletion completed.', 'fluent-cart')
1032 : __('No test orders found to delete', 'fluent-cart'),
1033 'total_count' => $totalCount,
1034 'batch_size' => $batchSize,
1035 'batch_count' => 0,
1036 'deleted_count' => 0,
1037 'failed_count' => 0,
1038 'deleted_order_ids' => [],
1039 'failed_order_ids' => [],
1040 'last_attempted_order_id' => $lastOrderId,
1041 'has_more' => false
1042 ]);
1043 }
1044
1045 $isDeleted = OrderResource::bulkDeleteByOrderIds($batchOrderIds);
1046
1047 if (is_wp_error($isDeleted)) {
1048 $deletedOrderIds = [];
1049 $failedOrderIds = $batchOrderIds;
1050 } else {
1051 $deletedOrderIds = array_values(array_unique(array_map('intval', Arr::get($isDeleted, 'data.deleted_order_ids', []))));
1052 $failedOrderIds = array_values(array_unique(array_map('intval', Arr::get($isDeleted, 'data.failed_order_ids', []))));
1053 }
1054
1055 $deletedCount = count($deletedOrderIds);
1056 $failedCount = count($failedOrderIds);
1057 $lastAttemptedOrderId = (int)end($batchOrderIds);
1058 $hasMore = $this->getTestOrdersQuery()
1059 ->where('id', '>', $lastAttemptedOrderId)
1060 ->exists();
1061
1062 return $this->sendSuccess([
1063 'message' => $hasMore
1064 ? __('Deleting test orders...', 'fluent-cart')
1065 : __('Test order deletion completed.', 'fluent-cart'),
1066 'total_count' => $totalCount,
1067 'batch_size' => $batchSize,
1068 'batch_count' => count($batchOrderIds),
1069 'deleted_count' => $deletedCount,
1070 'failed_count' => $failedCount,
1071 'deleted_order_ids' => $deletedOrderIds,
1072 'failed_order_ids' => $failedOrderIds,
1073 'last_attempted_order_id' => $lastAttemptedOrderId,
1074 'has_more' => $hasMore
1075 ]);
1076 }
1077
1078 public function updateTransactionStatus(Request $request, $order, OrderTransaction $transaction)
1079 {
1080
1081 $order = Order::query()->find($order);
1082 $newStatus = sanitize_text_field($request->get('status', ''));
1083
1084 $validStatuses = Status::getEditableTransactionStatuses();
1085 if (!isset($validStatuses[$newStatus])) {
1086 return $this->sendError([
1087 'message' => __('Provided transaction status is not valid', 'fluent-cart')
1088 ]);
1089 }
1090
1091 if ($transaction->status == $newStatus) {
1092 return $this->sendError([
1093 'reload' => true,
1094 'message' => __('Transaction already has the same status', 'fluent-cart')
1095 ]);
1096 }
1097
1098 if ($transaction->order_id != $order->id) {
1099 return $this->sendError([
1100 'message' => __('The selected transaction does not match with the provided order', 'fluent-cart')
1101 ]);
1102 }
1103
1104 // Money already counted into the order cannot be changed from a dropdown; returning
1105 // it is a refund, which records a refund transaction through the refund action (FC-SEC-09).
1106 if ($transaction->status === Status::TRANSACTION_SUCCEEDED) {
1107 return $this->sendError([
1108 'message' => __('A succeeded transaction cannot be changed here. Use the refund action to return the payment.', 'fluent-cart')
1109 ], 422);
1110 }
1111
1112 $transaction->updateStatus($newStatus);
1113
1114 if ($newStatus === Status::TRANSACTION_SUCCEEDED) {
1115 // 'succeeded' is a transaction word, not an order payment status: derive the
1116 // order's paid state and total_paid from its transactions, as mark-as-paid does.
1117 (new StatusHelper($order))->syncOrderStatuses($transaction);
1118 } else {
1119 $order->updatePaymentStatus($newStatus);
1120 }
1121
1122 return [
1123 'transaction' => $transaction,
1124 'message' => __('Payment status has been successfully updated', 'fluent-cart')
1125 ];
1126 }
1127
1128 public function syncPendingTransaction(Request $request, $order, OrderTransaction $transaction)
1129 {
1130 $order = Order::query()->find($order);
1131
1132 if (!$order || $transaction->order_id != $order->id) {
1133 return $this->sendError([
1134 'message' => __('The selected transaction does not match with the provided order', 'fluent-cart')
1135 ]);
1136 }
1137
1138 $result = $transaction->syncPendingTransaction();
1139
1140 if (is_wp_error($result)) {
1141 return $this->sendError([
1142 'message' => $result->get_error_message()
1143 ]);
1144 }
1145
1146 return $this->sendSuccess([
1147 'message' => __('Transaction has been synced from the payment gateway successfully!', 'fluent-cart'),
1148 'transaction' => $result
1149 ]);
1150 }
1151
1152 public function getStats($orderUuid): \WP_REST_Response
1153 {
1154 $order = OrderResource::find($orderUuid);
1155 return $this->sendSuccess([
1156 'widgets' => apply_filters('fluent_cart/widgets/single_order', [], $order)
1157 ]);
1158 }
1159
1160 public function getShippingMethods(Request $request): \WP_REST_Response
1161 {
1162 $countryCode = $request->get('country_code');
1163 $state = $request->get('state') ?? '';
1164 $orderItems = $this->prepareOrderItemsWithVariations($request->get('order_items'));
1165
1166 $enabledMethods = $this->getEnabledShippingMethodsWithCharges($orderItems);
1167
1168 if (empty($countryCode)) {
1169 return $this->sendSuccess([
1170 'shipping_methods' => [],
1171 'other_shipping_methods' => $enabledMethods,
1172 ]);
1173 }
1174
1175 $applicableMethods = ShippingMethod::getApplicableForCountry($countryCode, $state);
1176
1177 $applicableIds = $applicableMethods->pluck('id')->toArray();
1178
1179 return $this->sendSuccess([
1180 'shipping_methods' => $enabledMethods->whereIn('id', $applicableIds)->values(),
1181 'other_shipping_methods' => $enabledMethods->whereNotIn('id', $applicableIds)->values(),
1182 ]);
1183 }
1184
1185 protected function getEnabledShippingMethodsWithCharges(array $orderItems)
1186 {
1187 return ShippingMethod::query()
1188 ->where('is_enabled', '1')
1189 ->get()
1190 ->each(function ($method) use ($orderItems) {
1191 $method->shipping_charge = CartHelper::calculateShippingMethodCharge($method, $orderItems);
1192 });
1193 }
1194
1195 public function updateShipping(Request $request)
1196 {
1197 $orderItems = $request->get('order_items');
1198 $shippingMethodId = $request->get('shipping_id');
1199
1200 $orderItems = $this->prepareOrderItemsWithVariations($orderItems);
1201
1202
1203 $method = ShippingMethod::query()->find($shippingMethodId);
1204
1205 $totalShippingCharge = CartHelper::calculateShippingMethodCharge($method, $orderItems);
1206
1207 return $this->sendSuccess([
1208 'message' => __('Shipping updated', 'fluent-cart'),
1209 'shipping_charge' => $totalShippingCharge,
1210 'order_items' => $orderItems
1211 ]);
1212
1213 }
1214
1215 /**
1216 * Calculate tax for an admin order given an address and item list.
1217 * No DB writes — pure calculation helper.
1218 *
1219 * Accepts: { country, state, city, postcode, items: [{post_id, object_id, subtotal, discount_total}] }
1220 * Returns: { tax_total, shipping_tax, tax_lines, tax_behavior, tax_country }
1221 */
1222 public function calculateTax(Request $request)
1223 {
1224 $address = [
1225 'country' => sanitize_text_field($request->get('country', '')),
1226 'state' => sanitize_text_field($request->get('state', '')),
1227 'city' => sanitize_text_field($request->get('city', '')),
1228 'postcode' => sanitize_text_field($request->get('postcode', '')),
1229 ];
1230
1231 $rawItems = $request->get('items', []);
1232 if (!is_array($rawItems)) {
1233 $rawItems = [];
1234 } elseif (count($rawItems) > 100) {
1235 $rawItems = array_slice($rawItems, 0, 100);
1236 }
1237
1238 $items = [];
1239 foreach ($rawItems as $item) {
1240 $items[] = [
1241 'post_id' => (int) Arr::get($item, 'post_id', 0),
1242 'object_id' => (int) Arr::get($item, 'object_id', 0),
1243 'subtotal' => (int) Arr::get($item, 'subtotal', 0),
1244 'discount_total' => (int) Arr::get($item, 'discount_total', 0),
1245 'shipping_charge' => (int) Arr::get($item, 'shipping_charge', 0),
1246 'quantity' => max(1, (int) Arr::get($item, 'quantity', 1)),
1247 ];
1248 }
1249
1250 $result = \FluentCart\App\Services\Tax\AdminOrderTaxService::calculate($items, $address);
1251
1252 if ($result === null) {
1253 return $this->sendSuccess([
1254 'tax_total' => 0,
1255 'shipping_tax' => 0,
1256 'tax_lines' => [],
1257 'tax_behavior' => 0,
1258 'tax_country' => '',
1259 ]);
1260 }
1261
1262 $taxLines = Arr::get($result, 'tax_lines', []);
1263 $strippedTaxLines = array_values(array_map(function ($line) {
1264 return [
1265 'label' => Arr::get($line, 'label', ''),
1266 'rate_percent' => Arr::get($line, 'rate_percent', 0),
1267 'tax_amount' => (int) Arr::get($line, 'tax_amount', 0),
1268 'inclusive' => (bool) Arr::get($line, 'inclusive', false),
1269 ];
1270 }, $taxLines));
1271
1272 return $this->sendSuccess([
1273 'tax_total' => (int) Arr::get($result, 'tax_total', 0),
1274 'shipping_tax' => (int) Arr::get($result, 'shipping_tax', 0),
1275 'tax_behavior' => (int) Arr::get($result, 'tax_behavior', 0),
1276 'tax_country' => Arr::get($result, 'tax_country', ''),
1277 'tax_lines' => $strippedTaxLines,
1278 ]);
1279 }
1280
1281 protected function prepareOrderItemsWithVariations($orderItems)
1282 {
1283 $itemCollection = (new Collection($orderItems))->keyBy('id');
1284 $ids = $itemCollection->keys()->toArray();
1285 $variations = ProductVariation::query()->with('shippingClass')->whereIn('id', $ids)->get();
1286 $orderItems = $itemCollection->toArray();
1287
1288 foreach ($variations as &$variation) {
1289 $shippingCharge = CartHelper::calculateShippingCharge($variation, $itemCollection->get($variation->id)['quantity']);
1290 $variation->quantity = Arr::get($orderItems, $variation->id . '.' . 'quantity', 1);
1291 $variation->discount_total = Arr::get($orderItems, $variation->id . '.' . 'discount_total', 0);
1292 $variation->shipping_charge = $shippingCharge;
1293 $variation->unit_price = $variation->item_price;
1294
1295 }
1296
1297 $orderItems = $variations->mapWithKeys(function ($item) {
1298 return [
1299 $item->id => [
1300 'id' => $item->id,
1301 'quantity' => $item->quantity,
1302 'shipping_charge' => $item->shipping_charge,
1303 'unit_price' => $item->unit_price,
1304 'other_info' => $item->other_info,
1305 'discount_total' => $item->discount_total,
1306 'fulfillment_type' => $item->fulfillment_type,
1307 ]
1308 ];
1309 });
1310 $orderItems = $orderItems->toArray();
1311
1312 return $orderItems;
1313 }
1314
1315 public function acceptDispute(Request $request, $order, $transaction)
1316 {
1317 $order = Order::query()->find($order);
1318 $transaction = OrderTransaction::query()->find($transaction);
1319 $response = $transaction->acceptDispute([
1320 'dispute_note' => $request->getSafe('dispute_note', 'sanitize_text_field'),
1321 ]);
1322
1323 if (is_wp_error($response)) {
1324 return $this->sendError([
1325 'message' => $response->get_error_message()
1326 ]);
1327 }
1328
1329 return $this->sendSuccess([
1330 'message' => __('Dispute accepted!', 'fluent-cart')
1331 ]);
1332 }
1333
1334 public function syncOrderStatuses(Request $request, Order $order)
1335 {
1336 $latestTransaction = OrderTransaction::query()
1337 ->where('order_id', $order->id)
1338 ->orderBy('id', 'desc')
1339 ->first();
1340
1341 if (!$latestTransaction) {
1342 return $this->sendError([
1343 'message' => __('No transaction found for this order', 'fluent-cart')
1344 ], 404);
1345 }
1346
1347 (new StatusHelper($order))->syncOrderStatuses($latestTransaction);
1348
1349 // Reload order to get updated data
1350 $order = Order::query()->find($order->id);
1351
1352 return $this->sendSuccess([
1353 'message' => __('Order statuses synced successfully', 'fluent-cart'),
1354 'order' => $order,
1355 'payment_status' => $order->payment_status,
1356 'status' => $order->status,
1357 ]);
1358 }
1359
1360 }
1361