PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.19
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.19
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 / api / Resource / OrderResource.php

OrderResource.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.19, at api/Resource/OrderResource.php

1,337 lines 56.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\Api\Resource;
4
5 use FluentCart\Api\Orders;
6 use FluentCart\Api\StoreSettings;
7 use FluentCart\App\App;
8 use FluentCart\App\Events\Order\OrderDeleting;
9 use FluentCart\App\Events\Order\OrderDeleted;
10 use FluentCart\App\Events\Order\RenewalOrderDeleted;
11 use FluentCart\App\Events\Order\OrderStatusUpdated;
12 use FluentCart\App\Events\Order\OrderUpdated;
13 use FluentCart\App\Events\StockChanged;
14 use FluentCart\App\Helpers\AddressHelper;
15 use FluentCart\App\Helpers\Helper;
16 use FluentCart\App\Helpers\AdminOrderProcessor;
17 use FluentCart\App\Helpers\Status;
18 use FluentCart\App\Models\Activity;
19 use FluentCart\App\Models\AppliedCoupon;
20 use FluentCart\App\Models\Cart;
21 use FluentCart\App\Models\Coupon;
22 use FluentCart\App\Models\CustomerAddresses;
23 use FluentCart\App\Models\LabelRelationship;
24 use FluentCart\App\Models\Order;
25 use FluentCart\App\Models\OrderAddress;
26 use FluentCart\App\Models\OrderItem;
27 use FluentCart\App\Models\OrderDownloadPermission;
28 use FluentCart\App\Models\OrderMeta;
29 use FluentCart\App\Models\OrderOperation;
30 use FluentCart\App\Models\OrderTaxRate;
31 use FluentCart\App\Models\OrderTransaction;
32 use FluentCart\App\Models\Query\QueryParser;
33 use FluentCart\App\Models\Query\Sort;
34 use FluentCart\App\Models\Subscription;
35 use FluentCart\App\Models\SubscriptionMeta;
36 use FluentCart\App\Services\DateTime\DateTime;
37 use FluentCart\App\Services\OrderService;
38 use FluentCart\App\Services\Payments\PaymentHelper;
39 use FluentCart\App\Services\Payments\PaymentInstance;
40 use FluentCart\Framework\Database\Orm\Builder;
41 use FluentCart\Framework\Database\Orm\Collection;
42 use FluentCart\Framework\Support\Arr;
43
44
45 class OrderResource extends BaseResourceApi
46 {
47 public static function getQuery(): Builder
48 {
49 return Order::query();
50 }
51
52 /**
53 * Retrieve orders with additional data based on specified parameters.
54 *
55 * @param array $params Optional. Additional parameters for order retrieval.
56 * $params = [
57 * 'search' => ( string ) Optional. Search Order.
58 * [
59 * 'column name(e.g., first_name|last_name|email|id)' => [
60 * column => 'column name(e.g., first_name|last_name|email|id)',
61 * operator => 'operator (e.g., like_all|rlike|or_rlike|or_like_all)',
62 * value => 'value' ]
63 * ],
64 * 'filters' => ( string ) Optional. Filters order.
65 * [
66 * 'column name(e.g., status|payment_status|payment_method)' => [
67 * column => 'column name(e.g., status|payment_status|payment_method)',
68 * operator => 'operator (e.g., in)',
69 * value => 'value' ]
70 * ],
71 * 'order_by' => ( string ) Optional. Column to order by,
72 * 'order_type' => ( string ) Optional. Order type for sorting ( ASC or DESC ),
73 * 'per_page' => ( int ) Optional. Number of items for per page,
74 * 'page' => ( int ) Optional. Page number for pagination
75 * ]
76 *
77 */
78 public static function get(array $params = [])
79 {
80 $query = static::getQuery();
81 $dynamicConditions = Arr::get($params, 'dynamic_filters') ?? [];
82 QueryParser::make()->parse($query, $dynamicConditions);
83 $sortCriteria = Arr::get($params, 'sort_criteria', []);
84 Sort::make()->apply($query, $sortCriteria);
85
86
87 $with = array_merge(['customer', 'filteredOrderItems'], Arr::get($params, 'with', []));
88
89 return $query->with($with)
90 ->whereHas('customer', function ($query) use ($params) {
91 $query->when(Arr::get($params, 'search'), function ($query) use ($params) {
92 return $query->search(Arr::get($params, 'search', ''));
93 });
94 })
95 ->applyCustomFilters(Arr::get($params, 'filters', []))
96 ->when(!count($sortCriteria), function ($query) use ($params) {
97 $query->orderBy(
98 sanitize_sql_orderby(Arr::get($params, 'order_by', 'id')),
99 sanitize_sql_orderby(Arr::get($params, 'order_type', 'DESC'))
100 );
101 })
102 ->paginate(Arr::get($params, 'per_page'), ['*'], 'page', Arr::get($params, 'page'));
103 }
104
105
106 /**
107 * Find an order by ID with associated customer and address details.
108 *
109 * @param string $id Required. The UUID of the order to find.
110 * @param array $params Optional. Additional parameters for order retrieval.
111 * [
112 * // Include optional parameters, if any.
113 * ]
114 *
115 */
116 public static function find($id, $params = [])
117 {
118 $with = Arr::get($params, 'with', []);
119 return static::getQuery()
120 ->with($with)
121 ->with([
122 'customer' => function ($query) {
123 $query->with([
124 'billing_address' => function ($query) {
125 $query->where('is_primary', '1');
126 }
127 ]);
128 $query->with([
129 'shipping_address' => function ($query) {
130 $query->where('is_primary', '1');
131 }
132 ]);
133 }
134 ])
135 ->where('uuid', $id)
136 ->first();
137 }
138
139 /**
140 * Create an order with the provided data.
141 *
142 * @param array $data Required. Array containing the necessary parameters for order creation.
143 * $data = [
144 * 'status' => ( string ) Required. The status of the order,
145 * // Include additional parameters, if any.
146 * ]
147 * @param array $params Optional. Additional parameters for order creation.
148 * [
149 * // Include optional parameters, if any.
150 * ]
151 *
152 */
153 public static function create($data, $params = [])
154 {
155 $order = $data;
156 $orderItems = Arr::except(Arr::get($order, 'order_items', []), ['*']);
157 $hasPhysicalProduct = false;
158
159 foreach ($orderItems as $item) {
160 if (isset($item['trial_days']) && $item['trial_days'] > 0) {
161 continue;
162 }
163 if (Arr::get($item, 'fulfillment_type') == 'physical') {
164 $hasPhysicalProduct = true;
165 }
166 }
167
168 $subtotal = OrderService::getItemsAmountWithoutDiscount($orderItems); //get order total without a discount
169
170 // because of decimal issue commented this below line, using OrderService::getCouponDiscountTotal instead
171 // $subtotalWithDiscount = OrderService::getItemsAmountTotal($orderItems, false, false); //get order total with discount
172 $coupon_discount_total = OrderService::getCouponDiscountTotal($orderItems);
173 $couponDiscountTotal = $coupon_discount_total;
174
175 $totalAmount = floatVal($subtotal + Arr::get($order, 'tax_total', 0) + Arr::get($order, 'shipping_total', 0) - Arr::get($order, 'manual_discount_total', 0) - $couponDiscountTotal);
176
177 $latestOrder = static::getQuery()->latest()->first();
178 $latestOrderId = Arr::get($latestOrder, 'id', 0);
179
180 $fulfillmentType = $hasPhysicalProduct ? 'physical' : 'digital';
181 $storeSettings = new StoreSettings();
182
183 $shipping_total = Arr::get($order, 'shipping_total', 0);
184 $userTz = Arr::get($order, 'user_tz');
185 $config = [];
186
187 if (!empty($userTz)) {
188 $config['user_tz'] = $userTz;
189 }
190 $orderData = [
191 'subtotal' => $subtotal,
192 'total_amount' => $totalAmount,
193 'payment_status' => $totalAmount == 0 ? Status::PAYMENT_PAID : Status::PAYMENT_PENDING,
194 'status' => Status::ORDER_ON_HOLD,
195 'currency' => Helper::shopConfig('currency'),
196 'mode' => Helper::shopConfig('order_mode'),
197 'receipt_number' => ($latestOrderId + 1),
198 'invoice_no' => $storeSettings->getInvoicePrefix() . ($latestOrderId + 1) . $storeSettings->getInvoiceSuffix(),
199 'ip_address' => AddressHelper::getIpAddress(),
200 'fulfillment_type' => $fulfillmentType,
201 'manual_discount_total' => Arr::get($order, 'manual_discount_total', 0),
202 'coupon_discount_total' => $couponDiscountTotal,
203 'shipping_total' => $shipping_total,
204 'config' => $config
205 ];
206
207 $isPlanChange = Arr::get($params, 'is_plan_change', 'no');
208 $discountApplied = Arr::get($params, 'discount_applied', 'no');
209 if ('yes' == $isPlanChange && 'yes' == $discountApplied) {
210 $orderData['subtotal'] = $subtotal + Arr::get($params, 'discount_amount', 0);
211 $orderData['manual_discount_total'] = Arr::get($params, 'discount_amount', 0);
212 }
213 $orderData += $order;
214
215 $orderData['created_at'] = DateTime::gmtNow();
216 $orderData['updated_at'] = DateTime::gmtNow();
217
218 try {
219 $res = static::getQuery()->create($orderData);;
220 if (!$res || !$res->id) {
221 throw new \Exception(__('Order creation failed.', 'fluent-cart'));
222 }
223 return $res;
224 } catch (\Exception $e) {
225 return static::makeErrorResponse([
226 ['code' => 400, 'message' => $e->getMessage()]
227 ]);
228 }
229 }
230
231 /**
232 * @throws \Exception
233 */
234 public static function updatedPlaceOrder($data, $params = [])
235 {
236 $order = $data;
237 $discount = Arr::get($data, 'discount');
238 $shipping = Arr::get($data, 'shipping');
239 $newLabelIds = Arr::get($data, 'labels');
240 $paymentMethod = sanitize_text_field('offline_payment');
241
242 $items = Arr::except(Arr::get($order, 'order_items'), ['*']);
243 OrderService::validateProducts($items);
244
245 $customer = static::getCustomer($data);
246
247 if (Arr::get($discount, 'value', 0) > 0) {
248 static::distributeManualDiscount($items, Helper::toCent(Arr::get($discount, 'value', 0)));
249 }
250
251 // admin order processor
252 $adminOrderProcessor = new AdminOrderProcessor($items, [
253 'customer_id' => $customer->id,
254 'payment_method' => $paymentMethod,
255 'applied_coupons' => Arr::get($data, 'applied_coupon', []),
256 'shipping_total' => Arr::get($data, 'shipping_total', []),
257 'billing_address' => Arr::get($customer, 'billing_address', []),
258 'shipping_address' => Arr::get($customer, 'shipping_address', []),
259 'user_tz' => Arr::get($data, 'user_tz', ''),
260 ]);
261
262 $order = $adminOrderProcessor->createDraftOrder();
263
264 $data = Arr::except($data, ['order_items', 'customer', 'discount', 'shipping']);
265
266 try {
267 if ($paymentMethod) {
268 static::addOrderMeta($order->id, $discount, $shipping, $newLabelIds);
269
270 static::commitEvents($order);
271
272 static::createOrderAddresses($order->id, $data);
273
274 static::triggerStockChangedEvents($order);
275
276 if ($gateway = App::gateway($paymentMethod)) {
277 $paymentInstance = new PaymentInstance($order);
278 $gateway->makePaymentFromPaymentInstance($paymentInstance);
279 }
280
281 return $order;
282 } else {
283 return static::makeErrorResponse([
284 ['code' => 423, 'message' => __('Please select a payment method first!', 'fluent-cart')]
285 ]);
286 }
287 } catch (\Exception $e) {
288 return static::makeErrorResponse([
289 ['code' => 400, 'message' => $e->getMessage()]
290 ]);
291 }
292 }
293
294 private static function distributeManualDiscount(&$items, $manualDiscountTotal)
295 {
296 $totalSubtotal = array_reduce($items, function ($carry, $item) {
297 return $carry + ((int)Arr::get($item, 'unit_price', 0) * (int)Arr::get($item, 'quantity', 1));
298 }, 0);
299
300 if ($totalSubtotal <= 0) {
301 return;
302 }
303
304 $distributed = 0;
305 foreach ($items as &$checkoutItem) {
306 $unitPrice = (int)Arr::get($checkoutItem, 'unit_price', 0);
307 $quantity = (int)Arr::get($checkoutItem, 'quantity', 1);
308 $itemSubtotal = $unitPrice * $quantity;
309
310 $itemManualDiscount = (int) (($itemSubtotal / $totalSubtotal) * $manualDiscountTotal);
311
312 if ($itemManualDiscount > $itemSubtotal) {
313 $itemManualDiscount = $itemSubtotal;
314 }
315
316 $distributed += $itemManualDiscount;
317
318 Arr::set($checkoutItem, 'manual_discount', $itemManualDiscount);
319
320 }
321
322 $diff = round($manualDiscountTotal - $distributed, 2);
323 // Adjust the first item to account for any precision differences
324 if ($diff != 0) {
325 $items[0]['manual_discount'] = (int) (Arr::get($items[0], 'manual_discount', 0) + $diff);
326 }
327 }
328
329
330 private static function getCustomer($data)
331 {
332 $customer = CustomerResource::find(Arr::get($data, 'customer_id'), [
333 'with' => ['primary_billing_address', 'primary_shipping_address']
334 ]);
335 return Arr::get($customer, 'customer');
336 }
337
338 private static function addOrderMeta($orderId, $discount, $shipping, $newLabelIds)
339 {
340 if (!empty($discount)) {
341 static::addOrUpdateOrderMeta([
342 'order_id' => $orderId,
343 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
344 'meta_key' => 'order_discount',
345 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
346 'meta_value' => $discount
347 ]);
348 }
349
350 if (!empty($shipping)) {
351 static::addOrUpdateOrderMeta([
352 'order_id' => $orderId,
353 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
354 'meta_key' => 'order_shipping',
355 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
356 'meta_value' => $shipping
357 ]);
358 }
359
360 if (!empty($newLabelIds)) {
361 LabelResource::addLabelToLabelRelationships(Order::find($orderId), [
362 'labelable_id' => $orderId,
363 'labelable_type' => Order::class,
364 'new_label_ids' => $newLabelIds,
365 ]);
366 }
367 }
368
369 private static function commitEvents($order)
370 {
371
372 if (!$order) {
373 throw new \Exception(esc_html__('Please process order first', 'fluent-cart'));
374 }
375
376 if (!$order->customer) {
377 throw new \Exception(esc_html__('Please set customer first', 'fluent-cart'));
378 }
379
380 if (!$order->latest_transaction) {
381 throw new \Exception(esc_html__('Please set Transaction First', 'fluent-cart'));
382 }
383
384 $paymentStatus = $order->payment_status;
385
386 $transactionStatus = $order->latest_transaction->status;
387
388 if (in_array($transactionStatus, Status::getTransactionSuccessStatuses())) {
389
390 do_action('fluent_cart/payment_' . $paymentStatus,
391 [
392 'order' => $order,
393 'customer' => $order->customer,
394 'transaction' => $order->latest_transaction
395 ]);
396
397 do_action('fluent_cart/payment_' . $order->latest_transaction->transaction_type . '_' . $paymentStatus, [
398 'order' => $order,
399 'customer' => $order->customer,
400 'transaction' => $order->latest_transaction
401 ]);
402 }
403
404 }
405
406 private static function createOrderAddresses($orderId, $data)
407 {
408
409 $billingAddress = CustomerAddresses::query()->find(
410 Arr::get($data, 'billing_address_id')
411 );
412
413 $shippingAddress = CustomerAddresses::query()->find(
414 Arr::get($data, 'shipping_address_id')
415 );
416
417 if (!empty($billingAddress)) {
418 static::createOrderAddress($billingAddress->toArray(), $orderId);
419 }
420 if (!empty($shippingAddress)) {
421 static::createOrderAddress($shippingAddress->toArray(), $orderId);
422 }
423 }
424
425 private static function triggerStockChangedEvents($order)
426 {
427 $productIds = OrderService::pluckProductIds($order);
428 if (!empty($productIds)) {
429 // (new StockChanged($productIds))->dispatch();
430 }
431 }
432
433 /**
434 * Update an order with the provided data.
435 *
436 * @param array $data Required. Array containing the necessary parameters for order update.
437 * $data = [
438 * 'orderData' => ( array ) Required. Represents the main order details.
439 * [
440 * 'id' => (int) The id for the order.
441 * 'status' => (string) The current status of the order
442 * 'parent_id' => (int) The parent order ID, if applicable.
443 * 'receipt_number' => (int) the unique sequential order number.
444 * 'invoice_no' => (string) The order number assigned to the order.
445 * 'fulfillment_type' => (string) (e.g., 'virtual', 'physical', etc.).
446 * 'type' => (string) Type (e.g., 'sale', 'refund', etc.).
447 * 'customer_id' => (int) The ID of the customer associated with the order.
448 * 'payment_method' => (string) The payment method used for the order.
449 * 'payment_method_title' => (string) The title of the payment method.
450 * 'currency' => (string) The currency used for the order (e.g., 'BDT').
451 * 'subtotal' => (float) The subtotal amount of the order.
452 * 'discount_tax' => (float) The tax amount on discounts.
453 * 'manual_discount_total' => (float) The total discount amount for the order.
454 * 'shipping_tax' => (float) The tax amount on shipping.
455 * 'shipping_total' => (float) The total shipping amount for the order.
456 * 'tax_total' => (float) The total tax amount for the order.
457 * 'total_amount' => (float) The total amount for the order.
458 * 'total_paid' => (float) The total amount paid for the order.
459 * 'rate' => (float) The exchange rate used for currency conversion.
460 * 'ip_address' => (string) The IP address associated with the order.
461 * 'completed_at' => (string|null) date-time order completed|null
462 *  * 'refunded_at' => (string|null) date-time the order was refunded|null
463 *  * 'uuid' => (string) The id for the order.
464 *   * 'created_at' => (string) The date and time the order was created.
465 *  * 'updated_at' => (string) The date and time the order was last updated.
466 *  * 'customer' => (null|array) Info of customer associated with the order.
467 * 'order_items' => (array) Required. Array of order item details.
468 * [
469 * 'id' => ( int ) The id for the order item.
470 * 'order_id' => ( int ) The ID of the order to which the item belongs.
471 * 'post_id' => ( int ) The product ID associated with the order item.
472 * 'object_id' => ( int ) The variation ID of the order item.
473 * 'thumbnail' => ( string ) The URL of the thumbnail of order item.
474 * 'item_price' => ( float ) The price of the item.
475 * 'item_name' => ( string ) The name of the item.
476 * 'quantity' => ( int ) The quantity of the item.
477 * 'type' => ( string ) Type ( e.g., 'simple', 'variable' ).
478 * 'stockStatus' => ( string ) ( e.g., 'in-stock'|'out-of-stock' ).
479 * 'stock' => ( int ) The current stock quantity.
480 * 'tax_amount' => ( float ) The tax amount for the item.
481 * 'manual_discount_total' => ( float ) The total discount amount for the item.
482 * 'item_total' => ( float ) The total amount for the item.
483 * 'line_total' => ( float ) The total amount for the line
484 * ]
485 * ],
486 * 'discount' => ( array ) Optional. Represents the discount details
487 * [
488 * 'type' => ( string ) Required. type of discount ( e.g., 'amount', 'percentage' )
489 * 'label' => ( string ) Optional. The label associated with the discount
490 * 'reason' => ( string ) Optional. The reason for the discount
491 * 'value' => ( float ) Required. The value of the discount
492 * ],
493 * 'shipping' => ( array ) Optional. Represents the shipping details.
494 * [
495 * 'type' => ( string ) Optional. The type of shipping.
496 * 'value' => ( float|null ) Optional. Value associated with shipping|null if not
497 * ],
498 * 'deletedItems' => ( array ) Optional. IDs of items to be deleted.
499 * [
500 * ( e.g., 100, 501 etc )
501 * ]
502 * ]
503 * @param int $id Required. The ID of the order to update.
504 * @param array $params Optional. Additional parameters for order update.
505 * [
506 * // Include optional parameters, if any.
507 * ]
508 *
509 */
510 public static function update($data, $id, $params = [])
511 {
512
513
514 $order = static::getQuery()->with(["order_items", "appliedCoupons", "labels"])->where('id', $id)->first();
515
516 if (empty($order) || $order->status === Status::ORDER_COMPLETED || $order->status === Status::ORDER_CANCELED) {
517 if (empty($order)) {
518 return static::makeErrorResponse([
519 ['code' => 404, 'message' => __('The order information does not match', 'fluent-cart')]
520 ]);
521 }
522
523 return static::makeErrorResponse([
524 ['code' => 404, 'message' => sprintf(
525 /* translators: %s is the order status */
526 __('Your order status is marked as %s and not eligible for any further modifications at this time.', 'fluent-cart'), $order->status)]
527 ]);
528 }
529
530 $orderData = $data['orderData'];
531 $deletedItems = $data['deletedItems'];
532 $appliedCoupons = Arr::get($orderData, 'applied_coupon');
533 $discount = $data['discount'];
534 $shipping = $data['shipping'];
535
536 $orderId = $order->id;
537
538 /**
539 * First delete the deleted items
540 */
541 if (!empty($deletedItems)) {
542 // Filter only the custom items that are in the deleted IDs
543 $customItems = $order->order_items
544 ->filter(fn($item) => $item->is_custom && in_array($item->id, $deletedItems))
545 ->values(); // reset keys
546
547 if ($customItems->isNotEmpty()) {
548 do_action('fluent_cart/order/before_custom_items_deleted', $customItems, $order);
549 }
550
551 OrderItem::destroy($deletedItems);
552
553 if ($customItems->isNotEmpty()) {
554 do_action('fluent_cart/order/after_custom_items_deleted', $customItems, $order);
555 }
556 }
557
558 if (!empty($discount)) {
559 if (!empty($appliedCoupons) && count($appliedCoupons) > 0) {
560 // Remove the custom discount amount if coupon is applied.
561 OrderMetaResource::delete($orderId, [
562 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
563 'meta_key' => 'order_discount',
564 ]);
565 } else {
566 static::addOrUpdateOrderMeta([
567 'order_id' => $orderId,
568 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
569 'meta_key' => 'order_discount',
570 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
571 'meta_value' => $discount
572 ]);
573 }
574 }
575 if (!empty($shipping)) {
576 static::addOrUpdateOrderMeta([
577 'order_id' => $orderId,
578 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
579 'meta_key' => 'order_shipping',
580 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
581 'meta_value' => $shipping
582 ]);
583 }
584
585 $items = Arr::get($orderData, 'order_items');
586 $isUpdatedOrderItems = OrderItemResource::updateOrInsertOrderItems($order, $orderId, Arr::except($items, ['*']));
587
588 if ($isUpdatedOrderItems) {
589 unset($orderData['order_items']);
590 unset($orderData['customer']);
591
592
593 $orderData['currency'] = Helper::shopConfig('currency');
594
595 $oldOrder = clone $order;
596 $isUpdated = $order->update($orderData);
597
598 if ($isUpdated) {
599 $newOrder = $order->refresh();
600
601 if (!empty($appliedCoupons)) {
602 $appliedCoupons = Arr::except($appliedCoupons, ['*']);
603 $couponCodes = array_keys($appliedCoupons);
604 if (!empty($couponCodes)) {
605 $coupons = Coupon::query()->whereIn('code', $couponCodes)->get()
606 ->keyBy('code')
607 ->toArray();
608
609 foreach ($coupons as $code => &$coupon) {
610 $coupon['order_id'] = $orderId;
611 $coupon['coupon_id'] = $appliedCoupons[$code]['id'];
612 $coupon['amount'] = $appliedCoupons[$code]['discount'];
613 $coupon['created_at'] = $order->updated_at;
614 $coupon['updated_at'] = $order->updated_at;
615 }
616 $order->appliedCoupons()->delete();
617 $order->appliedCoupons()->createMany($coupons);
618 Coupon::query()->whereIn('code', $couponCodes)->increment('use_count', 1);
619 }
620 }
621
622 if (empty($appliedCoupons) && count($order->appliedCoupons) > 0) {
623 $order->appliedCoupons()->delete();
624 }
625
626 // $getOrderNoActionableStatuses = ['unshippable'];
627 // if(in_array($newOrder->shipping_status, $getOrderNoActionableStatuses)) {
628 // $newOrder->shipping_status = OrderMetaResource::find($orderId, ['meta_key' => 'shipping_previous_status']);
629 // }
630 (new OrderUpdated($newOrder, $oldOrder))->dispatch();
631
632 $oldOrderItems = json_decode(json_encode(Arr::get($oldOrder, 'order_items', [])), true);
633 $newOrderItems = json_decode(json_encode(Arr::get($newOrder, 'order_items', [])), true);
634 $pluckOldVariationIds = array_column($oldOrderItems, 'object_id');
635 foreach ($newOrderItems as $newItem) {
636 if (!in_array($newItem['object_id'], $pluckOldVariationIds)) {
637 $oldOrderItems[] = $newItem;
638 }
639 }
640
641 static::triggerEventsOnStockChanged($oldOrderItems);
642
643 return static::makeSuccessResponse(
644 $isUpdated,
645 __('Order updated successfully', 'fluent-cart')
646 );
647 }
648 }
649
650 return static::makeErrorResponse([
651 ['code' => 400, 'message' => __('Order update failed.', 'fluent-cart')]
652 ]);
653 }
654
655 public static function updateOrderAddressId($data, Order $order)
656 {
657
658 $addressType = Arr::get($data, 'address_type') ?? 'billing';
659 $addressId = Arr::get($data, 'address_id');
660 $addressRelation = $addressType === 'billing' ? 'billing_address' : 'shipping_address';
661
662 $address = CustomerAddresses::query()->find($addressId);
663 if (!empty($address)) {
664 $order->load($addressRelation);
665 $currentAddress = $order->{$addressRelation};
666 if (empty($currentAddress)) {
667 return static::createOrderAddress($address->toArray(), $order->id);
668 } else {
669 return static::mergeOrderAddress($currentAddress, $address->toArray());
670 }
671 }
672 }
673
674 public static function updateOrderAddress($data)
675 {
676 $orderId = sanitize_text_field(Arr::get($data, 'order_id'));
677 $addressId = sanitize_text_field(Arr::get($data, 'id'));
678 $orderAddress = OrderAddress::query()->where('order_id', $orderId)->where('id', $addressId)->first();
679 if (empty($orderAddress)) {
680 return static::makeErrorResponse([
681 ['code' => 404, 'message' => __('The address information does not match', 'fluent-cart')]
682 ]);
683 }
684
685 $updateData = Arr::only($data, ['name', 'first_name', 'last_name', 'full_name', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country']);
686 // sanitize the data before updating
687 $updateData = array_map('sanitize_text_field', $updateData);
688 return $orderAddress->update($updateData);
689
690 }
691
692 /**
693 * Delete an order and associated data by ID.Including order meta, order items, transactions,
694 *
695 * @param int $id Required. The ID of the order to delete.
696 * @param array $params Optional. Additional parameters for order deletion.
697 * [
698 * // Include optional parameters, if any.
699 * ]
700 *
701 */
702 public static function delete($id, $params = [])
703 {
704 $DB = App::db();
705
706 try {
707 /** @var Order $order */
708 $order = static::getQuery()->with("order_items")->find($id);
709 if (!$order) {
710 return static::makeErrorResponse([
711 ['code' => 404, 'message' => __('Order not found', 'fluent-cart')]
712 ]);
713 }
714
715 $canBeDeleted = $order->canBeDeleted();
716 if (is_wp_error($canBeDeleted)) {
717 return $canBeDeleted;
718 }
719
720 $deletedOrder = clone $order;
721 $deletedOrderItems = json_decode(json_encode(Arr::get($order, 'order_items', [])), true);
722 $connectedOrderIds = [$order->id];
723 $isTestMode = $order->mode === Status::ORDER_MODE_TEST;
724
725 if ($order->type === 'subscription') {
726 $childOrderIds = Order::query()->where('parent_id', $order->id)->pluck('id')->toArray();
727 $connectedOrderIds = array_merge($childOrderIds, $connectedOrderIds);
728 }
729
730 $DB->beginTransaction();
731
732 if ($order->type === 'subscription') {
733 $subscriptionIds = Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->pluck('id')->toArray();
734 if ($subscriptionIds) {
735 SubscriptionMeta::query()->whereIn('subscription_id', $subscriptionIds)->delete();
736 }
737
738 Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->delete();
739 }
740
741 // Dispatch inside transaction so stock restore is atomic with deletion.
742 // Must run before deleteOrderRelatedData() which removes stock_movement meta and order items.
743 (new OrderDeleting($order, $connectedOrderIds, $isTestMode, $order->type))->dispatch();
744
745 // Pre-load relations before cleanup so the OrderDeleted event has address data
746 $deletedOrder->load('customer', 'shipping_address', 'billing_address');
747
748 static::deleteOrderRelatedData($connectedOrderIds, $isTestMode);
749 $DB->commit();
750
751 if (!empty($deletedOrder)) {
752 if ($order->type === 'renewal') {
753 (new RenewalOrderDeleted($deletedOrder))->dispatch();
754 } else {
755 (new OrderDeleted($deletedOrder, $connectedOrderIds))->dispatch();
756 }
757 }
758 if (!empty($deletedOrderItems)) {
759 static::triggerEventsOnStockChanged($deletedOrderItems);
760 }
761
762 return static::makeSuccessResponse(
763 '',
764 __('Selected order and associated data has been deleted', 'fluent-cart')
765 );
766
767 } catch (\Exception $e) {
768 $DB->rollBack();
769 return static::makeErrorResponse([
770 ['code' => 400, 'message' => __('Failed to delete', 'fluent-cart')]
771 ]);
772 }
773 }
774
775 protected static function deleteOrderRelatedData(array $orderIds, bool $isTestMode = false): void
776 {
777 OrderTransaction::query()->whereIn('order_id', $orderIds)->delete();
778 OrderAddress::query()->whereIn('order_id', $orderIds)->delete();
779 OrderItem::query()->whereIn('order_id', $orderIds)->delete();
780 OrderMeta::query()->whereIn('order_id', $orderIds)->delete();
781 OrderTaxRate::query()->whereIn('order_id', $orderIds)->delete();
782 OrderOperation::query()->whereIn('order_id', $orderIds)->delete();
783 AppliedCoupon::query()->whereIn('order_id', $orderIds)->delete();
784 Cart::query()->whereIn('order_id', $orderIds)->delete();
785 OrderDownloadPermission::query()->whereIn('order_id', $orderIds)->delete();
786 LabelRelationship::query()->where('labelable_type', Order::class)
787 ->whereIn('labelable_id', $orderIds)->delete();
788
789 if ($isTestMode) {
790 Activity::query()->where('module_type', Order::class)
791 ->whereIn('module_id', $orderIds)->delete();
792 }
793
794 Order::query()->whereIn('id', $orderIds)->delete();
795 }
796
797 /**
798 * View details of an order by ID.
799 *
800 * This function retrieves details of an order by the specified ID. It includes information about the customer, order items with variants, transactions, discount meta, shipping meta,
801 * and order settings.
802 *
803 * @param int $id Required. The ID of the order to view.
804 *
805 */
806 public static function view(int $id)
807 {
808 $orders = static::search(
809 ['fct_orders.id' => $id],
810 function (Builder $query) {
811 return $query
812 ->with(
813 [
814 'parentOrder' => function ($query) {
815 return $query->select('id')
816 ->with('subscriptions');
817 },
818 'subscriptions',
819 'activities.user',
820 'labels',
821 'customer',
822 'children' => function ($query) {
823 return $query->select('id', 'parent_id', 'created_at');
824 },
825 //'order_items.variants.product_detail',
826 'order_items.variants.media',
827 'transactions',
828 'order_addresses',
829 'billing_address',
830 'shipping_address',
831 'appliedCoupons' => function ($query) {
832 $query->select('*');
833 }
834 ]
835 );
836 }
837 );
838
839 if (empty($orders[0])) {
840 return new \WP_Error('403', __('Order not found!', 'fluent-cart'));
841 }
842
843 $subscriptions = Arr::get($orders, '0.subscriptions');
844
845 if (empty($subscriptions)) {
846 $config = Arr::get($orders, '0.config', null);
847 $upgradedFrom = is_array($config)
848 ? Arr::get($config, 'upgraded_from', null)
849 : (is_string($config) ? Arr::get(json_decode($config, true), 'upgraded_from', null) : null);
850
851 $orders[0]['subscriptions'] = $upgradedFrom
852 ? []
853 : Arr::get($orders, '0.parent_order.subscriptions', []);
854 }
855
856 $data = [];
857
858 if (isset($orders[0])) {
859 $order = $orders[0];
860 $selectedLabels = Collection::make($order['labels'])->pluck('label_id');
861 $order['custom_checkout_url'] = PaymentHelper::getCustomPaymentLink(Arr::get($order, 'uuid'));
862
863 $data = [
864 'order' => $order,
865 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
866 'discount_meta' => OrderMetaResource::find($order['id'], ['meta_key' => 'order_discount']),
867 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
868 'shipping_meta' => OrderMetaResource::find($order['id'], ['meta_key' => 'order_shipping']),
869 'order_settings' => [
870 // 'has_vendor_refund' => PaymentMethodFactory::instance()->hasVendorRefund($order->payment_method)
871 ],
872 'selected_labels' => $selectedLabels,
873 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
874 'tax_id' => OrderMetaResource::find($order['id'], ['meta_key' => 'tax_id'])
875 ];
876 }
877
878 return $data;
879 }
880
881 /**
882 * Retrieve an overview of reports based on specified parameters.
883 *
884 * It calculates total sales, net sales, total discounts, total shipping tax, average order
885 * value, and customer order count based on the reports data.
886 *
887 * @param array $params Required. Additional parameters for report overview.
888 * $params = [
889 * //(Required)
890 * "status" => [
891 * "column" => "status",
892 * "operator" => "in",
893 * "value" => "Order success status e.g. completed,
894 * ],
895 *
896 * //(Required)
897 * "payment_status" => [
898 * "column" => "payment_status",
899 * "operator" => "in",
900 * "value" => "Transaction success status e.g. paid,
901 * ],
902 *
903 * //(Optional)
904 * "created_at" => [
905 * "column" => "created_at",
906 * "operator" => "between"
907 * "value" => "from and to date"
908 * ]
909 * ]
910 *
911 */
912 public static function reportOverview($params = [])
913 {
914 return static::getQuery()->when(
915 $params,
916 function ($query) use ($params) {
917 return $query->search($params);
918 }
919 )
920 ->selectRaw('sum(total_amount) as total_sales')
921 ->selectRaw('sum(total_amount - manual_discount_total - shipping_total - tax_total) as net_sales')
922 ->selectRaw('sum(discount_total) as total_discounts')
923 ->selectRaw('sum(shipping_total) as total_shipping_tax')
924 ->selectRaw('avg(total_amount) as average_order_value')
925 ->selectRaw('count(*) as customer_order_count')
926 ->get()->first();
927 }
928
929 /**
930 * Retrieve order summary based on payment methods and specified parameters.
931 *
932 * This function generates order summary by payment method, applying filters provided in the parameters.
933 *
934 * It retrieves the count of orders, total transactions, and groups the results by payment method.
935 *
936 * @param array $params Required. Additional parameters for order summary generation.
937 * $params = [
938 * //(Required)
939 * "status" => [
940 * "column" => "status",
941 * "operator" => "in",
942 * "value" => "Order success status e.g. completed,
943 * ],
944 *
945 * //(Required)
946 * "payment_status" => [
947 * "column" => "payment_status",
948 * "operator" => "in",
949 * "value" => "Transaction success status e.g. paid,
950 * ],
951 *
952 * //( Optional )
953 * 'created_at' => [
954 * 'column' => 'created_at',
955 * 'operator' => 'between'
956 * 'value' => 'from and to date'
957 * ]
958 * ]
959 *
960 * @return Collection of orders
961 */
962 public static function orderSummaryByPayment(array $params = [])
963 {
964 return static::getQuery()->select('payment_method')
965 ->when(
966 $params,
967 function ($query) use ($params) {
968 return $query->search($params);
969 }
970 )
971 ->selectRaw('COUNT(*) as order_count')
972 ->selectRaw('SUM(total_amount) as transactions')
973 ->groupBy('payment_method')
974 ->get();
975 }
976
977 private static function addOrUpdateOrderMeta($params = [])
978 {
979 $orderId = Arr::get($params, 'order_id', null);
980 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
981 $key = Arr::get($params, 'meta_key', '');
982 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
983 $value = Arr::get($params, 'meta_value', '');
984 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
985 $isExist = OrderMetaResource::find($orderId, ['meta_key' => $key]);
986
987 if ($isExist) {
988 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
989 return OrderMetaResource::update($value, $orderId, ['meta_key' => $key]);
990 }
991 return OrderMetaResource::create($params);
992 }
993
994 private static function triggerEventsOnStockChanged($orderItems)
995 {
996 if (!empty($orderItems)) {
997 $productIds = [];
998 foreach ($orderItems as $orderItem) {
999 $productIds[] = Arr::get($orderItem, 'post_id');
1000 }
1001 if (!empty($productIds)) {
1002 // (new StockChanged($productIds))->dispatch();
1003 }
1004 }
1005 }
1006
1007 public static function updateStatuses(array $params = [])
1008 {
1009
1010 $order = Arr::get($params, 'order');
1011 if (empty($order)) {
1012 return static::makeErrorResponse([
1013 ['code' => 404, 'message' => __('Order not found!', 'fluent-cart')]
1014 ]);
1015 }
1016
1017 $orderId = Arr::get($order, 'id');
1018
1019 $order = static::getQuery()->with("order_items.variants.product_detail")->where('id', $orderId)->first();
1020
1021 $action = Arr::get($params, 'action');
1022
1023 $changeType = $action === 'change_shipping_status' ? 'shipping_status' : 'order_status';
1024 $actionActivity = [];
1025
1026 if ($action === 'change_shipping_status') {
1027 $newStatus = Arr::get($params, 'statuses.shipping_status', null);
1028 $oldStatus = Arr::get($order, 'shipping_status');
1029 $validStatuses = Status::getEditableShippingStatuses();
1030 $actionActivity = [
1031 'title' => __('Shipping status updated', 'fluent-cart'),
1032 'content' => sprintf(
1033 /* translators: %1$s is the old status, %2$s is the new status */
1034 __('Shipping status has been updated from %1$s to %2$s', 'fluent-cart'), $oldStatus, $newStatus)
1035 ];
1036
1037 $orderItems = OrderItem::query()->where('fulfillment_type', 'physical')->where('order_id', $orderId)->get();
1038 $updateData = [];
1039 foreach ($orderItems as $item) {
1040 $updateData[] = [
1041 'id' => $item->id,
1042 'fulfilled_quantity' => in_array($newStatus, ['shipped', 'delivered']) ? $item->quantity : '0'
1043 ];
1044 }
1045 OrderItem::query()->batchUpdate($updateData);
1046 }
1047 if ($action === 'change_order_status') {
1048 $newStatus = Arr::get($params, 'statuses.order_status', null);
1049 $oldStatus = Arr::get($order, 'status');
1050 $validStatuses = Status::getEditableOrderStatuses();
1051 $shippingStatus = Arr::get($order, 'shipping_status');
1052 $actionActivity = [
1053 'title' => __('Order status updated', 'fluent-cart'),
1054 'content' => sprintf(
1055 /* translators: %1$s is the old status, %2$s is the new status */
1056 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), $oldStatus, $newStatus)
1057 ];
1058 }
1059
1060 if ($newStatus !== null) {
1061 if (isset($validStatuses[$newStatus])) {
1062 if ($newStatus != $oldStatus) {
1063
1064 $getOrderNoActionableStatuses = [Status::SHIPPING_UNSHIPPABLE];
1065
1066 if ($action === 'change_order_status') {
1067 if ($oldStatus === Status::ORDER_CANCELED) {
1068 return static::makeErrorResponse([
1069 ['code' => 400, 'message' => __('You cannot change the order status once it has been canceled.', 'fluent-cart')]
1070 ]);
1071 }
1072
1073 $order = $order->updateStatus('status', $newStatus);
1074
1075 if ($newStatus === Status::ORDER_CANCELED) {
1076 if (in_array($shippingStatus, $getOrderNoActionableStatuses)) {
1077 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
1078 $shippingStatus = OrderMetaResource::find($orderId, ['meta_key' => 'shipping_previous_status']);
1079 }
1080 (new OrderStatusUpdated($order, $shippingStatus, $newStatus, Arr::get($params, 'manage_stock', true), $actionActivity, $changeType))->dispatch();
1081 } else {
1082 (new OrderStatusUpdated($order, $oldStatus, $newStatus, false, $actionActivity, $changeType))->dispatch();
1083 }
1084 }
1085
1086 if ($action === 'change_shipping_status') {
1087
1088 if (in_array($newStatus, $getOrderNoActionableStatuses)) {
1089 static::addOrUpdateOrderMeta([
1090 'order_id' => $orderId,
1091 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
1092 'meta_key' => 'shipping_previous_status',
1093 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
1094 'meta_value' => $oldStatus
1095 ]);
1096 }
1097 if (in_array($oldStatus, $getOrderNoActionableStatuses)) {
1098 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
1099 $oldStatus = OrderMetaResource::find($orderId, ['meta_key' => 'shipping_previous_status']);
1100 }
1101
1102 if ($action !== 'change_shipping_status' && Arr::get($params, 'manage_stock') == 'true') {
1103 $validationSucceeded = static::validateStock(Arr::get($order, 'order_items', []));
1104
1105 if (Arr::get($validationSucceeded, 'status') === true) {
1106 return static::makeErrorResponse([
1107 ['code' => 400, 'message' => Arr::get($validationSucceeded, 'message')]
1108 ]);
1109 }
1110 }
1111
1112 $order = $order->updateStatus('shipping_status', $newStatus);
1113
1114 (new OrderStatusUpdated($order, $oldStatus, $newStatus, Arr::get($params, 'manage_stock'), $actionActivity, $changeType))->dispatch();
1115
1116 $orderItems = json_decode(json_encode(Arr::get($order, 'order_items', [])), true);
1117 static::triggerEventsOnStockChanged($orderItems);
1118 }
1119
1120 return static::makeSuccessResponse(
1121 $order,
1122 __('Status has been updated', 'fluent-cart')
1123 );
1124 }
1125 return static::makeErrorResponse([
1126 ['code' => 400, 'message' => __('Order already has the same status', 'fluent-cart')]
1127 ]);
1128 }
1129 return static::makeErrorResponse([
1130 ['code' => 400, 'message' => __('Provided status is not valid', 'fluent-cart')]
1131 ]);
1132 }
1133
1134 return static::makeErrorResponse([
1135 ['code' => 400, 'message' => __('Failed to update status', 'fluent-cart')]
1136 ]);
1137 }
1138
1139 private static function validateStock($orderItems)
1140 {
1141 $outOfStockVariants = [];
1142
1143 foreach ($orderItems as $orderItem) {
1144 $quantity = (int)Arr::get($orderItem, 'quantity', 0);
1145 $stock = (int)Arr::get($orderItem, 'variants.available', 0);
1146 // $manageStock = (int)Arr::get($orderItem, 'variants.product_detail.manage_stock');
1147 $manageStock = (int)Arr::get($orderItem, 'variants.manage_stock');
1148 $variationTitle = Arr::get($orderItem, 'variants.variation_title');
1149
1150 if ($manageStock == 1 && $stock - $quantity < 0) {
1151 $outOfStockVariants[] = $variationTitle;
1152 }
1153 }
1154
1155 if (!empty($outOfStockVariants)) {
1156 $message = (count($outOfStockVariants) > 1)
1157 ? sprintf(
1158 /* translators: %s is the list of out of stock variants */
1159 __('%s are out of stock', 'fluent-cart'), implode(', ', $outOfStockVariants))
1160 : sprintf(
1161 /* translators: %s is the out of stock variant */
1162 __('%s is out of stock', 'fluent-cart'), reset($outOfStockVariants));
1163
1164
1165 return [
1166 'status' => true,
1167 'message' => $message
1168 ];
1169 }
1170
1171 return false;
1172 }
1173
1174 /**
1175 * Delete orders and its associated data.
1176 *
1177 * @param array $orderIds The ids of the order to be deleted.
1178 * @param array $params Additional parameters for the deletion process.
1179 *
1180 */
1181 public static function bulkDeleteByOrderIds($orderIds, $params = [])
1182 {
1183 $failedOrderIds = [];
1184 $deletedOrderIds = [];
1185
1186 foreach ($orderIds as $order) {
1187 $isDeleted = static::delete($order);
1188
1189 if (is_wp_error($isDeleted)) {
1190 $failedOrderIds[] = $order;
1191 } else {
1192 $deletedOrderIds[] = $order;
1193 }
1194 }
1195
1196 if (count($failedOrderIds) > 0) {
1197 $failedOrderIdsString = implode(' , ', $failedOrderIds);
1198 return count($deletedOrderIds) > 0
1199 ? static::makeSuccessResponse([
1200 'deleted_order_ids' => $deletedOrderIds,
1201 'deleted_count' => count($deletedOrderIds),
1202 'failed_order_ids' => $failedOrderIds,
1203 'failed_count' => count($failedOrderIds)
1204 ], sprintf(
1205 /* translators: %s: The order ID(s) that could not be deleted. */
1206 __("The order ID - %s cannot be deleted at the moment as these orders status is not canceled. And remaining order and its associated data have been deleted", 'fluent-cart'), $failedOrderIdsString))
1207 : static::makeErrorResponse([['code' => 400, 'message' => sprintf(
1208 /* translators: %s: The order ID(s) that could not be deleted. */
1209 __("The order ID - %s cannot be deleted at the moment as these orders status is not canceled.", 'fluent-cart'), $failedOrderIdsString)]]);
1210 }
1211
1212 if (count($deletedOrderIds) > 0 && count($failedOrderIds) < 1) {
1213 return static::makeSuccessResponse([
1214 'deleted_order_ids' => $deletedOrderIds,
1215 'deleted_count' => count($deletedOrderIds),
1216 'failed_order_ids' => [],
1217 'failed_count' => 0
1218 ], __('Selected order and associated data have been deleted', 'fluent-cart'));
1219 }
1220
1221 return static::makeSuccessResponse([
1222 'deleted_order_ids' => [],
1223 'deleted_count' => 0,
1224 'failed_order_ids' => [],
1225 'failed_count' => 0
1226 ], __('No orders were deleted', 'fluent-cart'));
1227 }
1228
1229 public static function updatePaymentStatus(array $params = [])
1230 {
1231 $order = Arr::get($params, 'order');
1232 $transaction = Arr::get($params, 'transaction');
1233 $newStatus = Arr::get($params, 'status');
1234
1235 if (empty($transaction)) {
1236 return static::makeErrorResponse([
1237 ['code' => 404, 'message' => __('Transaction not found!', 'fluent-cart')]
1238 ]);
1239 }
1240
1241 if ($transaction->status == $newStatus) {
1242 return static::makeErrorResponse([
1243 ['code' => 400, 'message' => __('Transaction already has the same status', 'fluent-cart')]
1244 ]);
1245 }
1246
1247 if ($transaction->order_id != $order->id) {
1248 return static::makeErrorResponse([
1249 ['code' => 400, 'message' => __('The selected transaction does not match with the provided order', 'fluent-cart')]
1250 ]);
1251 }
1252
1253 $data = [];
1254 $totalPaid = ($order->total_paid - $transaction->total) < 0 ? 0 : $transaction->total;
1255
1256 if ($newStatus == Status::PAYMENT_PAID) {
1257 $data[] = [
1258 'id' => $order->id,
1259 'payment_status' => $newStatus,
1260 'total_paid' => ['+', $transaction->total],
1261 ];
1262 } elseif ($newStatus == Status::PAYMENT_REFUNDED) {
1263 $data[] = [
1264 'id' => $order->id,
1265 'payment_status' => $newStatus,
1266 'refunded_at' => DateTime::gmtNow(),
1267 'total_paid' => ['-', $totalPaid],
1268 'total_refund' => ['+', $transaction->total],
1269 ];
1270 } elseif ($newStatus == (Status::PAYMENT_PENDING || Status::PAYMENT_FAILED)) {
1271 $data[] = [
1272 'id' => $order->id,
1273 'payment_status' => $newStatus,
1274 'total_paid' => ['-', $totalPaid],
1275 ];
1276 }
1277
1278 $updatedStatus = $transaction->updateStatus($newStatus);
1279
1280 if (!empty($data) && $updatedStatus) {
1281 $oldStatus = Arr::get($order, 'payment_status');
1282 $actionActivity = [
1283 'title' => 'Payment status updated',
1284 'content' => sprintf(
1285 /* translators: %1$s is the old status, %2$s is the new status */
1286 __('Payment status has been updated from %1$s to %2$s', 'fluent-cart'), $oldStatus, $newStatus)
1287 ];
1288
1289 static::getQuery()->batchUpdate($data);
1290
1291 (new OrderStatusUpdated($order, $oldStatus, $newStatus, false, $actionActivity, 'payment_status'))->dispatch();
1292
1293 return static::makeSuccessResponse(
1294 $order,
1295 __('Payment Status has been updated', 'fluent-cart')
1296 );
1297
1298 } else {
1299 return static::makeErrorResponse([
1300 ['code' => 400, 'message' => __('Failed to update payment status', 'fluent-cart')]
1301 ]);
1302 }
1303 }
1304
1305 private static function mergeOrderAddress(OrderAddress $address, array $addressData)
1306 {
1307 $keysToInclude = ['type', 'name', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country'];
1308 foreach ($keysToInclude as $key) {
1309 $address->{$key} = $addressData[$key];
1310 }
1311
1312 if ($address->save()) {
1313 return $address;
1314 }
1315 return static::makeErrorResponse([
1316 ['code' => 400, 'message' => __('Failed to update address', 'fluent-cart')]
1317 ]);
1318 }
1319
1320 private static function createOrderAddress(array $address, $orderId)
1321 {
1322 $keysToInclude = ['order_id', 'type', 'name', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country'];
1323 $address = Arr::only($address, $keysToInclude);
1324 $address['order_id'] = $orderId;
1325
1326 if (!empty($address)) {
1327 return OrderAddressResource::create($address);
1328 }
1329 }
1330
1331 public static function getOrderByHash($orderHash)
1332 {
1333 return (new Orders())->getByHash($orderHash);
1334 }
1335
1336 }
1337