PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.25
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.25
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Http / Controllers / OrderController.php

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

1,233 lines 45.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\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\OrderBulkAction;
10 use FluentCart\App\Events\Order\OrderCreated;
11 use FluentCart\App\Events\Order\OrderDeleting;
12 use FluentCart\App\Events\Order\OrderDeleted;
13 use FluentCart\App\Events\Order\OrderPaid;
14 use FluentCart\App\Events\Order\OrderStatusUpdated;
15 use FluentCart\App\Events\Order\RenewalOrderDeleted;
16 use FluentCart\App\Helpers\CartHelper;
17 use FluentCart\App\Helpers\Helper;
18 use FluentCart\App\Helpers\OrderItemHelper;
19 use FluentCart\App\Helpers\Status;
20 use FluentCart\App\Helpers\StatusHelper;
21 use FluentCart\App\Http\Requests\CustomerRequest;
22 use FluentCart\App\Http\Requests\OrderRequest;
23 use FluentCart\App\Models\Customer;
24 use FluentCart\App\Models\CustomerAddresses;
25 use FluentCart\App\Models\CustomerMeta;
26 use FluentCart\App\Models\Order;
27 use FluentCart\App\Models\OrderAddress;
28 use FluentCart\App\Models\OrderItem;
29 use FluentCart\App\Models\OrderMeta;
30 use FluentCart\App\Models\OrderOperation;
31 use FluentCart\App\Models\OrderTaxRate;
32 use FluentCart\App\Models\OrderTransaction;
33 use FluentCart\App\Models\ProductVariation;
34 use FluentCart\App\Models\ShippingMethod;
35 use FluentCart\App\Models\Activity;
36 use FluentCart\App\Models\AppliedCoupon;
37 use FluentCart\App\Models\Cart;
38 use FluentCart\App\Models\LabelRelationship;
39 use FluentCart\App\Models\OrderDownloadPermission;
40 use FluentCart\App\Models\Subscription;
41 use FluentCart\App\Models\SubscriptionMeta;
42 use FluentCart\App\Services\Filter\OrderFilter;
43 use FluentCart\App\Services\Payments\PaymentHelper;
44 use FluentCart\App\Services\Reminders\ReminderService;
45 use FluentCart\App\Services\DateTime\DateTime;
46 use FluentCart\App\Services\Payments\Refund;
47 use FluentCart\App\Services\URL;
48 use FluentCart\Framework\Http\Request\Request;
49 use FluentCart\Framework\Support\Arr;
50 use FluentCart\Framework\Support\Collection;
51 use FluentCart\Framework\Validator\ValidationException;
52 use FluentCartPro\App\Hooks\Handlers\OrderActionsHandler;
53
54 class OrderController extends Controller
55 {
56 protected function getTestOrdersQuery()
57 {
58 return Order::query()
59 ->where('mode', Status::ORDER_MODE_TEST);
60 }
61
62 public function index(Request $request): \WP_REST_Response
63 {
64 $orders = OrderFilter::fromRequest($request)->paginate();
65
66 $orders = apply_filters('fluent_cart/orders_list', $orders);
67
68 return $this->sendSuccess(
69 [
70 'orders' => $orders,
71 ]
72 );
73 }
74
75 /**
76 * @throws \Exception
77 */
78 public function store(OrderRequest $request)
79 {
80 $data = $request->getSafe($request->sanitize());
81 $type = 'payment';
82 $hasSubscription = static::hasSubscription(Arr::get($data, 'order_items', []));
83 if ($hasSubscription) {
84 $type = 'subscription';
85 // right now we don't support subscription with manual order
86 $isSubscriptionAllowedInManualOrder = apply_filters('fluent_cart/order/is_subscription_allowed_in_manual_order', false, [
87 'order_items' => Arr::get($data, 'order_items', [])
88 ]);
89
90 if (!$isSubscriptionAllowedInManualOrder) {
91 return $this->sendError([
92 'message' => __('Subscription order with Manual Order is not supported yet!', 'fluent-cart')
93 ], 400);
94 }
95
96 }
97
98
99 $data['type'] = apply_filters('fluent_cart/order/type', $type, []);
100 $order = OrderResource::updatedPlaceOrder($data);
101
102
103 if (is_wp_error($order)) {
104 return $order;
105 }
106
107 // isCreated is an orderHelper instance
108 (new OrderCreated($order, null, $order->customer))->dispatch();
109
110 return $this->response->sendSuccess([
111 'message' => __('Order created successfully!', 'fluent-cart'),
112 'order_id' => $order->id,
113 'uuid' => $order->uuid
114 ]);
115 }
116
117
118 public static function hasSubscription($orderItems): bool
119 {
120 // check order items for subscription, payment_type == subscription
121 foreach ($orderItems as $item) {
122 if (Arr::get($item, 'payment_type') == 'subscription' || Arr::get($item, 'other_info.payment_type') == 'subscription') {
123 return true;
124 }
125 }
126 return false;
127 }
128
129 public function updateOrder(OrderRequest $request, $order_id)
130 {
131 $order = Order::query()->find($order_id);
132
133 if ($order->isSubscription()) {
134 return $this->sendError([
135 'message' => __('Subscription Order cannot be edited.', 'fluent-cart')
136 ], 400);
137 }
138
139
140 $requestData = $request->getSafe($request->sanitize());
141
142 $totalPaid = Arr::get($request->all(), 'total_paid');
143 $updatedTotal = Arr::get($requestData, 'total_amount');
144
145 if ($totalPaid > 0 && floatval($updatedTotal > $totalPaid)
146 && isset($requestData['payment_status'])
147 && $requestData['payment_status'] !== Status::PAYMENT_PARTIALLY_REFUNDED
148 ) {
149 $requestData['payment_status'] = Status::PAYMENT_PARTIALLY_PAID;
150 }
151
152 $status = Arr::get($requestData, 'status');
153 if ($status == Status::ORDER_COMPLETED) {
154 return $this->sendError([
155 'message' => esc_html__('Completed status can not be updated', 'fluent-cart')
156 ], 400);
157 }
158
159 // if new shipping total is already adjusted in total amount, then no need to adjust again, right now not adjusted before
160 // ToDo: adjust changed shipping total in total amount prior to this
161 $shippingTotal = Arr::get($requestData, 'shipping_total', 0);
162 $oldShippingTotal = Arr::get($order, 'shipping_total', 0);
163 if ($shippingTotal != $oldShippingTotal) {
164 $diff = $shippingTotal - $oldShippingTotal;
165 if ($diff < 0) {
166 $requestData['total_amount'] = $updatedTotal - abs($diff);
167 } else {
168 $requestData['total_amount'] = $updatedTotal + $diff;
169 }
170 }
171
172 $data = [
173 'orderData' => $requestData,
174 'deletedItems' => Arr::get($requestData, 'deletedItems', []),
175 'discount' => Arr::get($requestData, 'discount', ''),
176 'shipping' => Arr::get($requestData, 'shipping', ''),
177 'couponCalculation' => Arr::get($requestData, 'couponCalculation', []),
178 ];
179
180 $isUpdated = OrderResource::update($data, $order->id);
181
182 if (is_wp_error($isUpdated)) {
183 return $isUpdated;
184 }
185
186 return $this->response->sendSuccess($isUpdated);
187 }
188
189 public function updateOrderAddressId(Request $request, $order_id)
190 {
191 $order = Order::query()->find($order_id);
192
193
194 if (!$order) {
195 return $this->sendError([
196 'message' => __('Order not found', 'fluent-cart')
197 ], 404);
198 }
199 $data = OrderResource::updateOrderAddressId($request->only([
200 'address_id',
201 'address_type'
202 ]), $order);
203
204 if (is_wp_error($data)) {
205 return $this->sendError($data->get_error_message());
206 }
207 return $this->sendSuccess([
208 'message' => 'Address updated successfully'
209 ]);
210
211
212 }
213
214 public function generateMissingLicenses(Request $request, Order $order)
215 {
216 if (!$order) {
217 return $this->sendError([
218 'message' => __('Order not found', 'fluent-cart')
219 ], 404);
220 }
221
222 $generatedLicenseCount = $order->licenses->count();
223 $expectedLicenseCount = apply_filters('fluent_cart/order/expected_license_count', 0, [
224 'order_items' => $order->order_items
225 ]);
226
227 if ($generatedLicenseCount >= $expectedLicenseCount) {
228 return $this->sendError([
229 'message' => __('No missing licenses found!', 'fluent-cart')
230 ], 400);
231 }
232
233 do_action('fluent_cart/order/generateMissingLicenses', ['order' => $order]);
234
235 }
236
237 /**
238 * @throws ValidationException
239 */
240 public function refundOrder(Request $request, $orderId)
241 {
242 $order = Order::query()->findOrFail($orderId);
243
244 if (!$order->canBeRefunded()) {
245 return $this->sendError([
246 'message' => __('Order can not be refunded.', 'fluent-cart')
247 ], 400);
248 }
249
250 $refundInfo = $request->get('refund_info', []);
251
252 $this->validate($refundInfo, [
253 'transaction_id' => 'required',
254 'amount' => 'required',
255 ], [
256 'transaction_id.required' => __('Transaction ID is required', 'fluent-cart'),
257 'amount.required' => __('Refund amount is required', 'fluent-cart'),
258 ]);
259
260 $transaction = OrderTransaction::query()->where('order_id', $orderId)->findOrFail($refundInfo['transaction_id']);
261 $refundAmount = Helper::toCent($refundInfo['amount']);
262
263 // refund on our end
264 $result = (new Refund())->processRefund($transaction, $refundAmount, $refundInfo);
265
266 if (is_wp_error($result)) {
267 return $this->sendError([
268 'message' => $result->get_error_message()
269 ]);
270 }
271
272 $vendorRefundId = $result['vendor_refund_id'];
273 $isManuallyRefunded = Arr::get($result, 'manual_refund.status', 'no');
274
275 $responseData = [
276 'fluent_cart_refund' => [
277 'status' => 'success',
278 'message' => __('Refund processed on FluentCart.', 'fluent-cart')
279 ],
280 ];
281
282
283 if ($isManuallyRefunded === 'yes') {
284 $message = __('Refund processed manually.', 'fluent-cart');
285 $source = Arr::get($result, 'manual_refund.source');
286 if ($source) {
287 $message = sprintf(
288 __('Refund processed manually. source: %s', 'fluent-cart'),
289 $source
290 );
291 }
292 $responseData['gateway_refund'] = [
293 'status' => 'success',
294 'message' => $message
295 ];
296 } else {
297 $responseData['gateway_refund'] = [
298 'status' => is_wp_error($vendorRefundId) ? 'failed' : 'success',
299 'message' => !is_wp_error($vendorRefundId)
300 ? sprintf(__('Refund processed on %s', 'fluent-cart'), ucfirst($transaction->payment_method))
301 : sprintf(__('ERROR processing refund on %s: %s', 'fluent-cart'), ucfirst($transaction->payment_method), $vendorRefundId->get_error_message())
302 ];
303
304 if (is_wp_error($vendorRefundId)) {
305 fluent_cart_warning_log('Refund failed on ' . ucfirst($transaction->payment_method), $vendorRefundId->get_error_message(), [
306 'module_name' => 'order',
307 'module_id' => $order->id,
308 'log_type' => 'api'
309 ]);
310 }
311 }
312
313 $cancelSubscription = Arr::get($refundInfo, 'cancelSubscription') == 'true';
314
315 if ($cancelSubscription && $transaction->subscription_id && $transaction->subscription) {
316 $vendorSubscriptionCancelled = $transaction->subscription->cancelRemoteSubscription([
317 'reason' => 'refunded',
318 'effective_from' => 'immediately'
319 ]);
320 if (is_wp_error($vendorSubscriptionCancelled)) {
321 $responseData['subscription_cancel']['status'] = 'failed';
322 $responseData['subscription_cancel']['message'] = $vendorSubscriptionCancelled->get_error_message();
323 } else {
324 $vendorResult = $vendorSubscriptionCancelled['vendor_result'];
325 $responseData['subscription_cancel']['status'] = is_wp_error($vendorResult) ? 'failed' : 'success';
326 $responseData['subscription_cancel']['message'] = is_wp_error($vendorResult)
327 ? $vendorResult->get_error_message()
328 : __('Subscription cancelled successfully', 'fluent-cart');
329 }
330 }
331
332 return $this->sendSuccess(
333 $responseData
334 );
335 }
336
337
338 public function createAndChangeCustomer(CustomerRequest $request, $order_id)
339 {
340
341 $data = $request->getSafe($request->sanitize());
342 $isCreated = CustomerResource::create($data);
343
344 if (is_wp_error($isCreated)) {
345 return $this->sendError(
346 [
347 'message' => __('Failed to attach customer', 'fluent-cart')
348 ]
349 );
350 }
351
352 $customerId = Arr::get($isCreated, 'data.id');
353
354 $isChanged = $this->updateOrderCustomer($customerId, $order_id);
355 if (is_wp_error($isChanged)) {
356 return $this->sendError(
357 [
358 'message' => $isChanged->get_error_message()
359 ]
360 );
361 }
362 return $this->sendSuccess($isChanged);
363
364 }
365
366 public function changeCustomer(Request $request, $order_id)
367 {
368 $customerId = $request->get('customer_id');
369 $customerId = sanitize_text_field($customerId);
370
371 if (!$customerId) {
372 return $this->sendError([
373 'message' => __('Customer id is required', 'fluent-cart')
374 ], 423);
375 }
376
377 $isChanged = $this->updateOrderCustomer($customerId, $order_id);
378 if (is_wp_error($isChanged)) {
379 return $this->sendError(
380 [
381 'message' => $isChanged->get_error_message()
382 ]
383 );
384 }
385 return $this->sendSuccess($isChanged);
386
387 }
388
389 private function updateOrderCustomer($customerId, $orderId)
390 {
391
392 /**
393 * 1. Check if it's a different customer
394 * 2. Update main order.customer_id
395 * 3. update subscription.customer_id
396 * 4. update license.customer_id
397 *
398 *
399 * // critical thinking
400 * 5. If it's a renewal then change the parent order's data as well it's all child resources
401 * 6. Recount Customer's stat (New as well as the old one!)
402 */
403
404 $order = Order::query()->findOrFail($orderId);
405
406 if ($order->customer_id == $customerId) {
407 return [
408 'message' => __('Customer is already attached to this order', 'fluent-cart')
409 ];
410 }
411 $newCustomer = Customer::query()->findOrFail($customerId);
412 $oldCustomerId = $order->customer_id;
413
414 CustomerAddresses::query()->where('customer_id', $oldCustomerId)->update(['customer_id' => $customerId]);
415 CustomerMeta::query()->where('customer_id', $oldCustomerId)->update(['customer_id' => $customerId]);
416
417 $connectedOrderIds = [$order->id];
418 if ($order->parent_id && $order->type == 'renewal') {
419 $connectedOrderIds[] = $order->parent_id;
420 $parentOrderIdsOrders = Order::query()->where('parent_id', $order->parent_id)->get()->pluck('id')->toArray();
421 $connectedOrderIds = array_merge($parentOrderIdsOrders, $connectedOrderIds);
422
423 } else if ($order->type == 'subscription') {
424 $childOrderIds = Order::query()->where('parent_id', $order->id)->pluck('id')->toArray();
425 $connectedOrderIds = array_merge($childOrderIds, $connectedOrderIds);
426 }
427 Order::query()->whereIn('id', $connectedOrderIds)->update(['customer_id' => $customerId]);
428 Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->update(['customer_id' => $customerId]);
429
430 $newCustomer->recountStat();
431 $oldCustomer = Customer::query()->find($oldCustomerId);
432 if (!empty($oldCustomer)) {
433 $oldCustomer->recountStat();
434 }
435
436 do_action('fluent_cart/order_customer_changed', [
437 'order' => $order,
438 'old_customer' => $oldCustomer,
439 'new_customer' => $newCustomer,
440 'connected_order_ids' => $connectedOrderIds
441 ]);
442
443 fluent_cart_success_log(
444 __('Customer changed', 'fluent-cart'),
445 sprintf(
446 /* translators: 1: old customer name, 2: new customer name */
447 __('Customer changed from %1$s to %2$s', 'fluent-cart'), $oldCustomer->full_name, $newCustomer->full_name),
448 [
449 'module_name' => 'order',
450 'module_id' => $orderId,
451 'log_type' => 'activity'
452 ]);
453
454 return [
455 'message' => __('Customer changed successfully', 'fluent-cart')
456 ];
457 }
458
459 public function deleteOrder(Request $request, $order_id)
460 {
461
462 $order = Order::query()->find($order_id);
463
464 if (empty($order)) {
465 return $this->sendError([
466 'message' => __('Order not found', 'fluent-cart'),
467 'data' => [
468 'order_id' => $order_id,
469 'status' => 'error'
470 ],
471 'errors' => []
472 ], 404);
473 }
474
475 $order_id = $order->id; // Get the single order ID
476 // Find the order with additional details
477
478 $canBeDeleted = $order->canBeDeleted();
479 if (is_wp_error($canBeDeleted)) {
480
481 return $this->sendError([
482 'message' => $canBeDeleted->get_error_message(),
483 'data' => [
484 'order_id' => $order_id,
485 'invoice_no' => $order->invoice_no,
486 'status' => 'error',
487 'reason' => $canBeDeleted->get_error_code()
488 ],
489 'errors' => [
490 $canBeDeleted->get_error_message()
491 ]
492 ], 400);
493 }
494
495
496 $DB = \FluentCart\App\App::db();
497 $connectedOrderIds = [$order->id];
498 $isTestMode = $order->mode === Status::ORDER_MODE_TEST;
499
500 if ($order->type === 'subscription') {
501 $childOrderIds = Order::query()->where('parent_id', $order->id)->pluck('id')->toArray();
502 $connectedOrderIds = array_merge($childOrderIds, $connectedOrderIds);
503 }
504
505 try {
506 $DB->beginTransaction();
507
508 if ($order->type === 'subscription') {
509 $subscriptionIds = Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->pluck('id')->toArray();
510 if ($subscriptionIds) {
511 SubscriptionMeta::query()->whereIn('subscription_id', $subscriptionIds)->delete();
512 }
513
514 Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->delete();
515 }
516
517 // Dispatch inside transaction so stock restore is atomic with deletion.
518 // Must run before deleteOrderRelatedData() which removes stock_movement meta and order items.
519 (new OrderDeleting($order, $connectedOrderIds, $isTestMode, $order->type))->dispatch();
520
521 // Pre-load relations before cleanup so the delete events have address data
522 $order->load('customer', 'shipping_address', 'billing_address');
523
524 $this->deleteOrderRelatedData($connectedOrderIds, $isTestMode);
525 $DB->commit();
526 } catch (\Exception $e) {
527 $DB->rollBack();
528 return $this->sendError([
529 'message' => __('Failed to delete order', 'fluent-cart'),
530 ], 400);
531 }
532
533 if ($order->type === 'renewal') {
534 (new RenewalOrderDeleted($order))->dispatch();
535 } else {
536 (new OrderDeleted($order, $connectedOrderIds))->dispatch();
537 }
538
539 return $this->sendSuccess([
540 'message' => sprintf(
541 /* translators: %s is the order/invoice number */
542 __('Order %s deleted successfully', 'fluent-cart'), $order_id),
543 'data' => [
544 'order_id' => $order_id,
545 'invoice_no' => $order->invoice_no,
546 'status' => 'success'
547 ],
548 'errors' => []
549 ]);
550
551 }
552
553 /**
554 * Delete order related data (transactions, items, meta, addresses, orders)
555 */
556 private function deleteOrderRelatedData(array $orderIds, bool $isTestMode = false): void
557 {
558 OrderTransaction::query()->whereIn('order_id', $orderIds)->delete();
559 OrderAddress::query()->whereIn('order_id', $orderIds)->delete();
560 OrderItem::query()->whereIn('order_id', $orderIds)->delete();
561 OrderMeta::query()->whereIn('order_id', $orderIds)->delete();
562 OrderTaxRate::query()->whereIn('order_id', $orderIds)->delete();
563 OrderOperation::query()->whereIn('order_id', $orderIds)->delete();
564 AppliedCoupon::query()->whereIn('order_id', $orderIds)->delete();
565 Cart::query()->whereIn('order_id', $orderIds)->delete();
566 OrderDownloadPermission::query()->whereIn('order_id', $orderIds)->delete();
567 LabelRelationship::query()->where('labelable_type', Order::class)
568 ->whereIn('labelable_id', $orderIds)->delete();
569
570 if ($isTestMode) {
571 Activity::query()->where('module_type', Order::class)
572 ->whereIn('module_id', $orderIds)->delete();
573 }
574
575 Order::query()->whereIn('id', $orderIds)->delete();
576 }
577
578
579 public function getDetails($orderId)
580 {
581 $data = OrderResource::view($orderId);
582
583 if (is_wp_error($data) || empty($data['order'])) {
584 return $this->entityNotFoundError(
585 __('Order not found', 'fluent-cart'),
586 __('Back to orders', 'fluent-cart'),
587 '/orders'
588 );
589 }
590
591 $data['order'] = apply_filters('fluent_cart/order/view', $data['order'], []);
592
593 // check if the order has generated license
594 $data['order']['has_missing_licenses'] = false;
595
596 $expectedLicenseCount = apply_filters('fluent_cart/order/expected_license_count', 0, [
597 'order_items' => Arr::get($data, 'order.order_items', [])
598 ]);
599
600 $generatedLicenseCount = count(Arr::get($data, 'order.licenses', []));
601 if ($expectedLicenseCount && ($expectedLicenseCount > $generatedLicenseCount)) {
602 $data['order']['has_missing_licenses'] = true;
603 }
604
605 $data['order']['order_operation'] = OrderOperation::query()->where('order_id', $orderId)
606 ->first();
607
608 $url = URL::appendQueryParams(
609 (new StoreSettings())->getReceiptPage(),
610 [
611 'order_hash' => Arr::get($data, 'order.uuid')
612 ]
613 );
614
615 if (empty($data['order']['receipt_url'])) {
616 $data['order']['receipt_url'] = $url;
617 }
618 $meta = OrderMeta::query()->where('order_id', $orderId)
619 ->where('meta_key', 'vat_tax_id')
620 ->first();
621
622 if ($meta) {
623 $data['tax_id'] = $meta->meta_value;
624 }
625
626 $data['can_send_payment_reminder'] = (new ReminderService())->canSendPaymentReminder($data['order']);
627
628 return $data;
629 }
630
631 public function createCustom(Request $request, OrderItemHelper $orderItemHelper, Order $order)
632 {
633 try {
634 return $orderItemHelper->processCustom(
635 $request->product,
636 $order->id
637 );
638
639 } catch (\Exception $e) {
640 return $this->sendError([
641 'message' => $e->getMessage()
642 ], 423);
643 }
644
645 }
646
647 // public function calculate(Request $request, OrderHelper $orderHelper)
648 // {
649 // return $orderHelper->calculate($request->order);
650 // }
651
652 public function updateStatuses(Request $request, Order $order)
653 {
654
655 $data = [
656 'order' => $order,
657 'statuses' => $request->get('statuses', []),
658 'manage_stock' => $request->get('manage_stock'),
659 'action' => $request->get('action')
660 ];
661 $isUpdated = OrderResource::updateStatuses($data);
662
663 if (is_wp_error($isUpdated)) {
664 return $isUpdated;
665 }
666 return $this->response->sendSuccess($isUpdated);
667 }
668
669 public function updateOrderAddress(Request $request, $orderId, $addressId)
670 {
671
672 $data = $request->all();
673
674 $isUpdated = OrderResource::updateOrderAddress($data);
675
676 if (is_wp_error($isUpdated)) {
677 return $isUpdated;
678 }
679 return $this->response->sendSuccess($isUpdated);
680 }
681
682
683 public function markAsPaid(Request $request, Order $order)
684 {
685 $dueAmount = intval($order->total_amount - $order->total_paid);
686
687 if ($dueAmount <= 0) {
688 return $this->sendError([
689 'message' => __('Order has already been paid', 'fluent-cart')
690 ], 423);
691 }
692
693 if (Arr::get($order, 'status') === 'canceled') {
694 return $this->sendError([
695 'message' => __('Unable to mark paid for canceled order', 'fluent-cart')
696 ], 423);
697 }
698
699 $transaction = $order->transactions->where('status', Status::TRANSACTION_PENDING)
700 ->where('payment_method', 'offline_payment')
701 ->first();
702
703 $newTransactionData = [
704 'total' => $dueAmount,
705 'status' => Status::TRANSACTION_SUCCEEDED,
706 'payment_method' => sanitize_text_field($request->payment_method),
707 'vendor_charge_id' => sanitize_text_field($request->vendor_charge_id),
708 'payment_mode' => sanitize_text_field($order->mode),
709 'payment_method_type' => sanitize_text_field($request->payment_method),
710 'order_type' => sanitize_text_field($order->type),
711 'transaction_type' => sanitize_text_field($request->transaction_type),
712 'currency' => sanitize_text_field($order->currency),
713 ];
714
715 if ($transaction) {
716 $transaction->update($newTransactionData);
717 } else {
718 $transaction = OrderTransaction::query()->create(
719 array_merge($newTransactionData, [
720 'order_id' => $order->id
721 ])
722 );
723 }
724
725 $order->note = sanitize_text_field($request->get('mark_paid_note', ''));
726
727 $oldStatus = $order->status;
728
729 if ($order->payment_status !== 'partially_refunded') {
730 $order->payment_status = Status::PAYMENT_PAID;
731 }
732
733 $order->status = Status::ORDER_PROCESSING;
734 $order->total_paid = $order->total_amount;
735 $order->save();
736
737 $actionActivity = [
738 'title' => __('Order status updated', 'fluent-cart'),
739 'content' => sprintf(
740 /* translators: 1: old status, 2: new status */
741 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), $oldStatus, $order->status)
742 ];
743
744 // dispatching events related to order status update and payment paid
745 (new OrderPaid($order, $order->customer, $transaction))->dispatch();
746
747 (new OrderStatusUpdated($order, $oldStatus, $order->status, true, $actionActivity, 'order_status'))->dispatch();
748
749 // if digital
750 if ($order->fulfillment_type == 'digital' && $order->status === Status::ORDER_PROCESSING) {
751 $order->status = Status::ORDER_COMPLETED;
752 $order->completed_at = DateTime::gmtNow();
753 $order->save();
754
755 $actionActivity = [
756 'title' => __('Order status updated', 'fluent-cart'),
757 'content' => sprintf(
758 /* translators: 1: old status, 2: new status */
759 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), Status::ORDER_PROCESSING, $order->status)
760 ];
761
762 (new OrderStatusUpdated($order, Status::ORDER_PROCESSING, $order->status, true, $actionActivity, 'order_status'))->dispatch();
763 }
764
765 return $this->response->sendSuccess([
766 'message' => __('Order has been marked as paid', 'fluent-cart')
767 ]);
768 }
769
770 public function handleBulkActions(Request $request)
771 {
772
773 $action = sanitize_text_field($request->get('action', ''));
774 $orderIds = $request->get('order_ids', []);
775
776 if ($action == 'delete_test_orders') {
777 return $this->handleDeleteTestOrdersBulkAction($request);
778 }
779
780 $orderIds = array_map(function ($id) {
781 return (int)$id;
782 }, $orderIds);
783
784 $orderIds = array_filter($orderIds);
785
786 if (!$orderIds) {
787 return $this->sendError([
788 'message' => __('Orders selection is required', 'fluent-cart')
789 ]);
790 }
791
792 $orders = Order::query()->whereIn('id', $orderIds)->get();
793
794
795 if ($action == 'delete_orders') {
796
797 $isDeleted = OrderResource::bulkDeleteByOrderIds($orderIds);
798
799 if (is_wp_error($isDeleted)) {
800 return $isDeleted;
801 }
802 return $this->response->sendSuccess($isDeleted);
803
804 // $DB = App::db();
805 // $DB->beginTransaction();
806
807 // try {
808
809 // OrderResource::bulkDeleteByOrderIds($orderIds);
810 // OrderTransaction::bulkDeleteByOrderIds($orderIds);
811 // OrderMetaResource::bulkDeleteByOrderIds($orderIds);
812 // OrderItemResource::bulkDeleteByOrderIds($orderIds);
813
814 // $DB->commit();
815
816 // return [
817 // 'message' => __('Selected orders and their associated resources have been deleted permanently', 'fluent-cart')
818 // ];
819 // } catch (\Exception $e) {
820 // $DB->rollBack();
821 // return static::makeErrorResponse([
822 // ['code' => 400, 'message' => __('Failed to delete orders and their associated resources', 'fluent-cart')]
823 // ]);
824 // }
825
826
827 }
828 if ($action == 'change_shipping_status') {
829 $newStatus = sanitize_text_field($request->get('new_status', ''));
830 if (!$newStatus) {
831 return $this->sendError([
832 'message' => __('Please select status', 'fluent-cart')
833 ]);
834 }
835
836 $validStatuses = Helper::getShippingStatuses();
837 if (!isset($validStatuses[$newStatus])) {
838 return $this->sendError([
839 'message' => __('Provided shipping status is not valid', 'fluent-cart')
840 ]);
841 }
842
843 // foreach ($orders as $order) {
844 // $order->updateShippingStatus($newStatus);
845 // }
846
847 return [
848 'message' => __('Shipping Status has been changed for the selected orders', 'fluent-cart')
849 ];
850
851 }
852 if ($action == 'change_order_status') {
853
854 $newStatus = sanitize_text_field($request->get('new_status', ''));
855 if (!$newStatus) {
856 return $this->sendError([
857 'message' => __('Please select status', 'fluent-cart')
858 ]);
859 }
860
861 $validStatuses = Status::getEditableOrderStatuses();
862 if (!isset($validStatuses[$newStatus])) {
863 return $this->sendError([
864 'message' => __('Provided order status is not valid', 'fluent-cart')
865 ]);
866 }
867
868 $failedOrderIds = [];
869 $updatedOrderIds = [];
870
871 foreach ($orders as $order) {
872 // $order->updateStatus('status', $newStatus);
873 $isUpdated = OrderResource::updateStatuses([
874 'order' => $order,
875 'action' => 'change_order_status',
876 'statuses.order_status' => $newStatus,
877 'manage_stock' => sanitize_text_field($request->get('manage_stock')),
878 ]);
879
880 if (is_wp_error($isUpdated)) {
881 $failedOrderIds[] = $order->id;
882 } else {
883 $updatedOrderIds[] = $order->id;
884 }
885 }
886
887 if (count($failedOrderIds) > 0) {
888 $failedOrderIds = implode(' , ', $failedOrderIds);
889 return count($updatedOrderIds) > 0
890 ? $this->sendSuccess([
891 'message' => sprintf(
892 /* translators: %s is the order ids */
893 __("The order ID - %s cannot be updated because they are either already cancelled or have the same status. And remaining order status has been successfully changed", 'fluent-cart'), $failedOrderIds)
894 ])
895 :
896 $this->sendError([
897 'message' => sprintf(
898 /* translators: %s is the order ids */
899 __("The order ID - %s cannot be updated because they are either already cancelled or have the same status.", 'fluent-cart'), $failedOrderIds)
900 ], 423);
901 }
902
903 if (count($updatedOrderIds) > 0 && count($failedOrderIds) < 1) {
904 return $this->sendSuccess([
905 'message' => __('Order Status has been changed for the selected orders', 'fluent-cart')
906 ]);
907 }
908 }
909
910 if ($action == 'capture_payments') {
911 foreach ($orders as $order) {
912 $order->capturePayments();
913 }
914
915 return [
916 'message' => __('Selected payments has been successfully captured', 'fluent-cart')
917 ];
918 }
919
920 if ($action == 'change_payment_status') {
921 $newStatus = sanitize_text_field($request->get('new_status', ''));
922 if (!$newStatus) {
923 return $this->sendError([
924 'message' => __('Please select status', 'fluent-cart')
925 ]);
926 }
927
928 $validStatuses = Status::getEditableTransactionStatuses();
929 if (!isset($validStatuses[$newStatus])) {
930 return $this->sendError([
931 'message' => __('Provided payment status is not valid', 'fluent-cart')
932 ]);
933 }
934
935 $failedOrderIds = [];
936 $updatedOrderIds = [];
937 $count = 0;
938 $customerIds = [];
939
940 foreach ($orders as $order) {
941 $transaction = $order->latest_transaction;
942 $isUpdated = OrderResource::updatePaymentStatus([
943 'order' => $order,
944 'status' => $newStatus,
945 'transaction' => $transaction,
946 ]);
947
948 if (is_wp_error($isUpdated)) {
949 $failedOrderIds[] = $order->id;
950 } else {
951 $updatedOrderIds[] = $order->id;
952 $count++;
953 $customerIds[] = $order->customer_id;
954 }
955 }
956
957 if ($count > 0 && count($customerIds) > 0) {
958 (new OrderBulkAction($customerIds))->dispatch();
959 }
960
961 if (count($failedOrderIds) > 0) {
962 $failedOrderIds = implode(' , ', $failedOrderIds);
963 return count($updatedOrderIds) > 0
964 ? $this->sendSuccess([
965 'message' => sprintf(
966 /* translators: %s is the order ids */
967 __("The order ID - %s cannot be updated at the moment because the transaction either already has the same status or does not match the provided order. The remaining order statuses have been updated successfully.", 'fluent-cart'), $failedOrderIds)
968 ])
969 :
970 $this->sendError([
971 'message' => sprintf(
972 /* translators: %s is the order ids */
973 __("The order ID - %s cannot be updated at the moment because its payment status is either the same as before or has already been refunded.", 'fluent-cart'), $failedOrderIds)
974 ], 423);
975 }
976
977 if (count($updatedOrderIds) > 0 && count($failedOrderIds) < 1) {
978 return $this->sendSuccess([
979 'message' => sprintf(
980 /* translators: %s is the payment status */
981 __("Selected orders payment status has been marked as %s", 'fluent-cart'),
982 $newStatus
983 )
984 ]);
985 }
986 }
987
988 return $this->sendError([
989 'message' => __('Selected action is invalid', 'fluent-cart')
990 ]);
991
992 }
993
994 protected function handleDeleteTestOrdersBulkAction(Request $request)
995 {
996 $batchSize = max(1, (int)apply_filters('fluent_cart/order/delete_test_orders_batch_size', 50));
997 $lastOrderId = max(0, (int)$request->get('last_order_id', 0));
998 $totalCount = (int)$this->getTestOrdersQuery()->count();
999
1000 $testOrders = $this->getTestOrdersQuery()
1001 ->select('id')
1002 ->orderBy('id')
1003 ->when($lastOrderId > 0, function ($query) use ($lastOrderId) {
1004 return $query->where('id', '>', $lastOrderId);
1005 })
1006 ->limit($batchSize)
1007 ->get();
1008
1009 $batchOrderIds = $testOrders->pluck('id')->map(function ($id) {
1010 return (int)$id;
1011 })->toArray();
1012
1013 if (!$batchOrderIds) {
1014 return $this->sendSuccess([
1015 'message' => $lastOrderId
1016 ? __('Test order deletion completed.', 'fluent-cart')
1017 : __('No test orders found to delete', 'fluent-cart'),
1018 'total_count' => $totalCount,
1019 'batch_size' => $batchSize,
1020 'batch_count' => 0,
1021 'deleted_count' => 0,
1022 'failed_count' => 0,
1023 'deleted_order_ids' => [],
1024 'failed_order_ids' => [],
1025 'last_attempted_order_id' => $lastOrderId,
1026 'has_more' => false
1027 ]);
1028 }
1029
1030 $isDeleted = OrderResource::bulkDeleteByOrderIds($batchOrderIds);
1031
1032 if (is_wp_error($isDeleted)) {
1033 $deletedOrderIds = [];
1034 $failedOrderIds = $batchOrderIds;
1035 } else {
1036 $deletedOrderIds = array_values(array_unique(array_map('intval', Arr::get($isDeleted, 'data.deleted_order_ids', []))));
1037 $failedOrderIds = array_values(array_unique(array_map('intval', Arr::get($isDeleted, 'data.failed_order_ids', []))));
1038 }
1039
1040 $deletedCount = count($deletedOrderIds);
1041 $failedCount = count($failedOrderIds);
1042 $lastAttemptedOrderId = (int)end($batchOrderIds);
1043 $hasMore = $this->getTestOrdersQuery()
1044 ->where('id', '>', $lastAttemptedOrderId)
1045 ->exists();
1046
1047 return $this->sendSuccess([
1048 'message' => $hasMore
1049 ? __('Deleting test orders...', 'fluent-cart')
1050 : __('Test order deletion completed.', 'fluent-cart'),
1051 'total_count' => $totalCount,
1052 'batch_size' => $batchSize,
1053 'batch_count' => count($batchOrderIds),
1054 'deleted_count' => $deletedCount,
1055 'failed_count' => $failedCount,
1056 'deleted_order_ids' => $deletedOrderIds,
1057 'failed_order_ids' => $failedOrderIds,
1058 'last_attempted_order_id' => $lastAttemptedOrderId,
1059 'has_more' => $hasMore
1060 ]);
1061 }
1062
1063 public function updateTransactionStatus(Request $request, $order, OrderTransaction $transaction)
1064 {
1065
1066 $order = Order::query()->find($order);
1067 $newStatus = $request->get('status');
1068 if ($transaction->status == $newStatus) {
1069 return $this->sendError([
1070 'reload' => true,
1071 'message' => __('Transaction already has the same status', 'fluent-cart')
1072 ]);
1073 }
1074
1075 if ($transaction->order_id != $order->id) {
1076 return $this->sendError([
1077 'message' => __('The selected transaction does not match with the provided order', 'fluent-cart')
1078 ]);
1079 }
1080
1081 $transaction->updateStatus($newStatus);
1082 $order->updatePaymentStatus($newStatus);
1083
1084 return [
1085 'transaction' => $transaction,
1086 'message' => __('Payment status has been successfully updated', 'fluent-cart')
1087 ];
1088 }
1089
1090 public function getStats($orderUuid): \WP_REST_Response
1091 {
1092 $order = OrderResource::find($orderUuid);
1093 return $this->sendSuccess([
1094 'widgets' => apply_filters('fluent_cart/widgets/single_order', [], $order)
1095 ]);
1096 }
1097
1098 public function getShippingMethods(Request $request): \WP_REST_Response
1099 {
1100 $countryCode = $request->get('country_code');
1101 $state = $request->get('state') ?? '';
1102 $orderItems = $this->prepareOrderItemsWithVariations($request->get('order_items'));
1103
1104 $enabledMethods = $this->getEnabledShippingMethodsWithCharges($orderItems);
1105
1106 if (empty($countryCode)) {
1107 return $this->sendSuccess([
1108 'shipping_methods' => [],
1109 'other_shipping_methods' => $enabledMethods,
1110 ]);
1111 }
1112
1113 $applicableMethods = ShippingMethod::getApplicableForCountry($countryCode, $state);
1114
1115 $applicableIds = $applicableMethods->pluck('id')->toArray();
1116
1117 return $this->sendSuccess([
1118 'shipping_methods' => $enabledMethods->whereIn('id', $applicableIds)->values(),
1119 'other_shipping_methods' => $enabledMethods->whereNotIn('id', $applicableIds)->values(),
1120 ]);
1121 }
1122
1123 protected function getEnabledShippingMethodsWithCharges(array $orderItems)
1124 {
1125 return ShippingMethod::query()
1126 ->where('is_enabled', '1')
1127 ->get()
1128 ->each(function ($method) use ($orderItems) {
1129 $method->shipping_charge = CartHelper::calculateShippingMethodCharge($method, $orderItems);
1130 });
1131 }
1132
1133 public function updateShipping(Request $request)
1134 {
1135 $orderItems = $request->get('order_items');
1136 $shippingMethodId = $request->get('shipping_id');
1137
1138 $orderItems = $this->prepareOrderItemsWithVariations($orderItems);
1139
1140
1141 $method = ShippingMethod::query()->find($shippingMethodId);
1142
1143 $totalShippingCharge = CartHelper::calculateShippingMethodCharge($method, $orderItems);
1144
1145 return $this->sendSuccess([
1146 'message' => __('Shipping updated', 'fluent-cart'),
1147 'shipping_charge' => $totalShippingCharge,
1148 'order_items' => $orderItems
1149 ]);
1150
1151 }
1152
1153 protected function prepareOrderItemsWithVariations($orderItems)
1154 {
1155 $itemCollection = (new Collection($orderItems))->keyBy('id');
1156 $ids = $itemCollection->keys()->toArray();
1157 $variations = ProductVariation::query()->with('shippingClass')->whereIn('id', $ids)->get();
1158 $orderItems = $itemCollection->toArray();
1159
1160 foreach ($variations as &$variation) {
1161 $shippingCharge = CartHelper::calculateShippingCharge($variation, $itemCollection->get($variation->id)['quantity']);
1162 $variation->quantity = Arr::get($orderItems, $variation->id . '.' . 'quantity', 1);
1163 $variation->discount_total = Arr::get($orderItems, $variation->id . '.' . 'discount_total', 0);
1164 $variation->shipping_charge = $shippingCharge;
1165 $variation->unit_price = $variation->item_price;
1166
1167 }
1168
1169 $orderItems = $variations->mapWithKeys(function ($item) {
1170 return [
1171 $item->id => [
1172 'id' => $item->id,
1173 'quantity' => $item->quantity,
1174 'shipping_charge' => $item->shipping_charge,
1175 'unit_price' => $item->unit_price,
1176 'other_info' => $item->other_info,
1177 'discount_total' => $item->discount_total,
1178 'fulfillment_type' => $item->fulfillment_type,
1179 ]
1180 ];
1181 });
1182 $orderItems = $orderItems->toArray();
1183
1184 return $orderItems;
1185 }
1186
1187 public function acceptDispute(Request $request, $order, $transaction)
1188 {
1189 $order = Order::query()->find($order);
1190 $transaction = OrderTransaction::query()->find($transaction);
1191 $response = $transaction->acceptDispute([
1192 'dispute_note' => $request->getSafe('dispute_note', 'sanitize_text_field'),
1193 ]);
1194
1195 if (is_wp_error($response)) {
1196 return $this->sendError([
1197 'message' => $response->get_error_message()
1198 ]);
1199 }
1200
1201 return $this->sendSuccess([
1202 'message' => __('Dispute accepted!', 'fluent-cart')
1203 ]);
1204 }
1205
1206 public function syncOrderStatuses(Request $request, Order $order)
1207 {
1208 $latestTransaction = OrderTransaction::query()
1209 ->where('order_id', $order->id)
1210 ->orderBy('id', 'desc')
1211 ->first();
1212
1213 if (!$latestTransaction) {
1214 return $this->sendError([
1215 'message' => __('No transaction found for this order', 'fluent-cart')
1216 ], 404);
1217 }
1218
1219 (new StatusHelper($order))->syncOrderStatuses($latestTransaction);
1220
1221 // Reload order to get updated data
1222 $order = Order::query()->find($order->id);
1223
1224 return $this->sendSuccess([
1225 'message' => __('Order statuses synced successfully', 'fluent-cart'),
1226 'order' => $order,
1227 'payment_status' => $order->payment_status,
1228 'status' => $order->status,
1229 ]);
1230 }
1231
1232 }
1233