PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.23
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.23
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.23, at app/Http/Controllers/OrderController.php

1,232 lines 44.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\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()->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 ]);
319 if (is_wp_error($vendorSubscriptionCancelled)) {
320 $responseData['subscription_cancel']['status'] = 'failed';
321 $responseData['subscription_cancel']['message'] = $vendorSubscriptionCancelled->get_error_message();
322 } else {
323 $vendorResult = $vendorSubscriptionCancelled['vendor_result'];
324 $responseData['subscription_cancel']['status'] = is_wp_error($vendorResult) ? 'failed' : 'success';
325 $responseData['subscription_cancel']['message'] = is_wp_error($vendorResult)
326 ? $vendorResult->get_error_message()
327 : __('Subscription cancelled successfully', 'fluent-cart');
328 }
329 }
330
331 return $this->sendSuccess(
332 $responseData
333 );
334 }
335
336
337 public function createAndChangeCustomer(CustomerRequest $request, $order_id)
338 {
339
340 $data = $request->getSafe($request->sanitize());
341 $isCreated = CustomerResource::create($data);
342
343 if (is_wp_error($isCreated)) {
344 return $this->sendError(
345 [
346 'message' => __('Failed to attach customer', 'fluent-cart')
347 ]
348 );
349 }
350
351 $customerId = Arr::get($isCreated, 'data.id');
352
353 $isChanged = $this->updateOrderCustomer($customerId, $order_id);
354 if (is_wp_error($isChanged)) {
355 return $this->sendError(
356 [
357 'message' => $isChanged->get_error_message()
358 ]
359 );
360 }
361 return $this->sendSuccess($isChanged);
362
363 }
364
365 public function changeCustomer(Request $request, $order_id)
366 {
367 $customerId = $request->get('customer_id');
368 $customerId = sanitize_text_field($customerId);
369
370 if (!$customerId) {
371 return $this->sendError([
372 'message' => __('Customer id is required', 'fluent-cart')
373 ], 423);
374 }
375
376 $isChanged = $this->updateOrderCustomer($customerId, $order_id);
377 if (is_wp_error($isChanged)) {
378 return $this->sendError(
379 [
380 'message' => $isChanged->get_error_message()
381 ]
382 );
383 }
384 return $this->sendSuccess($isChanged);
385
386 }
387
388 private function updateOrderCustomer($customerId, $orderId)
389 {
390
391 /**
392 * 1. Check if it's a different customer
393 * 2. Update main order.customer_id
394 * 3. update subscription.customer_id
395 * 4. update license.customer_id
396 *
397 *
398 * // critical thinking
399 * 5. If it's a renewal then change the parent order's data as well it's all child resources
400 * 6. Recount Customer's stat (New as well as the old one!)
401 */
402
403 $order = Order::query()->findOrFail($orderId);
404
405 if ($order->customer_id == $customerId) {
406 return [
407 'message' => __('Customer is already attached to this order', 'fluent-cart')
408 ];
409 }
410 $newCustomer = Customer::query()->findOrFail($customerId);
411 $oldCustomerId = $order->customer_id;
412
413 CustomerAddresses::query()->where('customer_id', $oldCustomerId)->update(['customer_id' => $customerId]);
414 CustomerMeta::query()->where('customer_id', $oldCustomerId)->update(['customer_id' => $customerId]);
415
416 $connectedOrderIds = [$order->id];
417 if ($order->parent_id && $order->type == 'renewal') {
418 $connectedOrderIds[] = $order->parent_id;
419 $parentOrderIdsOrders = Order::query()->where('parent_id', $order->parent_id)->get()->pluck('id')->toArray();
420 $connectedOrderIds = array_merge($parentOrderIdsOrders, $connectedOrderIds);
421
422 } else if ($order->type == 'subscription') {
423 $childOrderIds = Order::query()->where('parent_id', $order->id)->pluck('id')->toArray();
424 $connectedOrderIds = array_merge($childOrderIds, $connectedOrderIds);
425 }
426 Order::query()->whereIn('id', $connectedOrderIds)->update(['customer_id' => $customerId]);
427 Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->update(['customer_id' => $customerId]);
428
429 $newCustomer->recountStat();
430 $oldCustomer = Customer::query()->find($oldCustomerId);
431 if (!empty($oldCustomer)) {
432 $oldCustomer->recountStat();
433 }
434
435 do_action('fluent_cart/order_customer_changed', [
436 'order' => $order,
437 'old_customer' => $oldCustomer,
438 'new_customer' => $newCustomer,
439 'connected_order_ids' => $connectedOrderIds
440 ]);
441
442 fluent_cart_success_log(
443 __('Customer changed', 'fluent-cart'),
444 sprintf(
445 /* translators: 1: old customer name, 2: new customer name */
446 __('Customer changed from %1$s to %2$s', 'fluent-cart'), $oldCustomer->full_name, $newCustomer->full_name),
447 [
448 'module_name' => 'order',
449 'module_id' => $orderId,
450 'log_type' => 'activity'
451 ]);
452
453 return [
454 'message' => __('Customer changed successfully', 'fluent-cart')
455 ];
456 }
457
458 public function deleteOrder(Request $request, $order_id)
459 {
460
461 $order = Order::query()->find($order_id);
462
463 if (empty($order)) {
464 return $this->sendError([
465 'message' => __('Order not found', 'fluent-cart'),
466 'data' => [
467 'order_id' => $order_id,
468 'status' => 'error'
469 ],
470 'errors' => []
471 ], 404);
472 }
473
474 $order_id = $order->id; // Get the single order ID
475 // Find the order with additional details
476
477 $canBeDeleted = $order->canBeDeleted();
478 if (is_wp_error($canBeDeleted)) {
479
480 return $this->sendError([
481 'message' => $canBeDeleted->get_error_message(),
482 'data' => [
483 'order_id' => $order_id,
484 'invoice_no' => $order->invoice_no,
485 'status' => 'error',
486 'reason' => $canBeDeleted->get_error_code()
487 ],
488 'errors' => [
489 $canBeDeleted->get_error_message()
490 ]
491 ], 400);
492 }
493
494
495 $DB = \FluentCart\App\App::db();
496 $connectedOrderIds = [$order->id];
497 $isTestMode = $order->mode === Status::ORDER_MODE_TEST;
498
499 if ($order->type === 'subscription') {
500 $childOrderIds = Order::query()->where('parent_id', $order->id)->pluck('id')->toArray();
501 $connectedOrderIds = array_merge($childOrderIds, $connectedOrderIds);
502 }
503
504 try {
505 $DB->beginTransaction();
506
507 if ($order->type === 'subscription') {
508 $subscriptionIds = Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->pluck('id')->toArray();
509 if ($subscriptionIds) {
510 SubscriptionMeta::query()->whereIn('subscription_id', $subscriptionIds)->delete();
511 }
512
513 Subscription::query()->whereIn('parent_order_id', $connectedOrderIds)->delete();
514 }
515
516 // Dispatch inside transaction so stock restore is atomic with deletion.
517 // Must run before deleteOrderRelatedData() which removes stock_movement meta and order items.
518 (new OrderDeleting($order, $connectedOrderIds, $isTestMode, $order->type))->dispatch();
519
520 // Pre-load relations before cleanup so the delete events have address data
521 $order->load('customer', 'shipping_address', 'billing_address');
522
523 $this->deleteOrderRelatedData($connectedOrderIds, $isTestMode);
524 $DB->commit();
525 } catch (\Exception $e) {
526 $DB->rollBack();
527 return $this->sendError([
528 'message' => __('Failed to delete order', 'fluent-cart'),
529 ], 400);
530 }
531
532 if ($order->type === 'renewal') {
533 (new RenewalOrderDeleted($order))->dispatch();
534 } else {
535 (new OrderDeleted($order, $connectedOrderIds))->dispatch();
536 }
537
538 return $this->sendSuccess([
539 'message' => sprintf(
540 /* translators: %s is the order/invoice number */
541 __('Order %s deleted successfully', 'fluent-cart'), $order_id),
542 'data' => [
543 'order_id' => $order_id,
544 'invoice_no' => $order->invoice_no,
545 'status' => 'success'
546 ],
547 'errors' => []
548 ]);
549
550 }
551
552 /**
553 * Delete order related data (transactions, items, meta, addresses, orders)
554 */
555 private function deleteOrderRelatedData(array $orderIds, bool $isTestMode = false): void
556 {
557 OrderTransaction::query()->whereIn('order_id', $orderIds)->delete();
558 OrderAddress::query()->whereIn('order_id', $orderIds)->delete();
559 OrderItem::query()->whereIn('order_id', $orderIds)->delete();
560 OrderMeta::query()->whereIn('order_id', $orderIds)->delete();
561 OrderTaxRate::query()->whereIn('order_id', $orderIds)->delete();
562 OrderOperation::query()->whereIn('order_id', $orderIds)->delete();
563 AppliedCoupon::query()->whereIn('order_id', $orderIds)->delete();
564 Cart::query()->whereIn('order_id', $orderIds)->delete();
565 OrderDownloadPermission::query()->whereIn('order_id', $orderIds)->delete();
566 LabelRelationship::query()->where('labelable_type', Order::class)
567 ->whereIn('labelable_id', $orderIds)->delete();
568
569 if ($isTestMode) {
570 Activity::query()->where('module_type', Order::class)
571 ->whereIn('module_id', $orderIds)->delete();
572 }
573
574 Order::query()->whereIn('id', $orderIds)->delete();
575 }
576
577
578 public function getDetails($orderId)
579 {
580 $data = OrderResource::view($orderId);
581
582 if (is_wp_error($data) || empty($data['order'])) {
583 return $this->entityNotFoundError(
584 __('Order not found', 'fluent-cart'),
585 __('Back to orders', 'fluent-cart'),
586 '/orders'
587 );
588 }
589
590 $data['order'] = apply_filters('fluent_cart/order/view', $data['order'], []);
591
592 // check if the order has generated license
593 $data['order']['has_missing_licenses'] = false;
594
595 $expectedLicenseCount = apply_filters('fluent_cart/order/expected_license_count', 0, [
596 'order_items' => Arr::get($data, 'order.order_items', [])
597 ]);
598
599 $generatedLicenseCount = count(Arr::get($data, 'order.licenses', []));
600 if ($expectedLicenseCount && ($expectedLicenseCount > $generatedLicenseCount)) {
601 $data['order']['has_missing_licenses'] = true;
602 }
603
604 $data['order']['order_operation'] = OrderOperation::query()->where('order_id', $orderId)
605 ->first();
606
607 $url = URL::appendQueryParams(
608 (new StoreSettings())->getReceiptPage(),
609 [
610 'order_hash' => Arr::get($data, 'order.uuid')
611 ]
612 );
613
614 if (empty($data['order']['receipt_url'])) {
615 $data['order']['receipt_url'] = $url;
616 }
617 $meta = OrderMeta::query()->where('order_id', $orderId)
618 ->where('meta_key', 'vat_tax_id')
619 ->first();
620
621 if ($meta) {
622 $data['tax_id'] = $meta->meta_value;
623 }
624
625 $data['can_send_payment_reminder'] = (new ReminderService())->canSendPaymentReminder($data['order']);
626
627 return $data;
628 }
629
630 public function createCustom(Request $request, OrderItemHelper $orderItemHelper, Order $order)
631 {
632 try {
633 return $orderItemHelper->processCustom(
634 $request->product,
635 $order->id
636 );
637
638 } catch (\Exception $e) {
639 return $this->sendError([
640 'message' => $e->getMessage()
641 ], 423);
642 }
643
644 }
645
646 // public function calculate(Request $request, OrderHelper $orderHelper)
647 // {
648 // return $orderHelper->calculate($request->order);
649 // }
650
651 public function updateStatuses(Request $request, Order $order)
652 {
653
654 $data = [
655 'order' => $order,
656 'statuses' => $request->get('statuses', []),
657 'manage_stock' => $request->get('manage_stock'),
658 'action' => $request->get('action')
659 ];
660 $isUpdated = OrderResource::updateStatuses($data);
661
662 if (is_wp_error($isUpdated)) {
663 return $isUpdated;
664 }
665 return $this->response->sendSuccess($isUpdated);
666 }
667
668 public function updateOrderAddress(Request $request, $orderId, $addressId)
669 {
670
671 $data = $request->all();
672
673 $isUpdated = OrderResource::updateOrderAddress($data);
674
675 if (is_wp_error($isUpdated)) {
676 return $isUpdated;
677 }
678 return $this->response->sendSuccess($isUpdated);
679 }
680
681
682 public function markAsPaid(Request $request, Order $order)
683 {
684 $dueAmount = intval($order->total_amount - $order->total_paid);
685
686 if ($dueAmount <= 0) {
687 return $this->sendError([
688 'message' => __('Order has already been paid', 'fluent-cart')
689 ], 423);
690 }
691
692 if (Arr::get($order, 'status') === 'canceled') {
693 return $this->sendError([
694 'message' => __('Unable to mark paid for canceled order', 'fluent-cart')
695 ], 423);
696 }
697
698 $transaction = $order->transactions->where('status', Status::TRANSACTION_PENDING)
699 ->where('payment_method', 'offline_payment')
700 ->first();
701
702 $newTransactionData = [
703 'total' => $dueAmount,
704 'status' => Status::TRANSACTION_SUCCEEDED,
705 'payment_method' => sanitize_text_field($request->payment_method),
706 'vendor_charge_id' => sanitize_text_field($request->vendor_charge_id),
707 'payment_mode' => sanitize_text_field($order->mode),
708 'payment_method_type' => sanitize_text_field($request->payment_method),
709 'order_type' => sanitize_text_field($order->type),
710 'transaction_type' => sanitize_text_field($request->transaction_type),
711 'currency' => sanitize_text_field($order->currency),
712 ];
713
714 if ($transaction) {
715 $transaction->update($newTransactionData);
716 } else {
717 $transaction = OrderTransaction::query()->create(
718 array_merge($newTransactionData, [
719 'order_id' => $order->id
720 ])
721 );
722 }
723
724 $order->note = sanitize_text_field($request->get('mark_paid_note', ''));
725
726 $oldStatus = $order->status;
727
728 if ($order->payment_status !== 'partially_refunded') {
729 $order->payment_status = Status::PAYMENT_PAID;
730 }
731
732 $order->status = Status::ORDER_PROCESSING;
733 $order->total_paid = $order->total_amount;
734 $order->save();
735
736 $actionActivity = [
737 'title' => __('Order status updated', 'fluent-cart'),
738 'content' => sprintf(
739 /* translators: 1: old status, 2: new status */
740 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), $oldStatus, $order->status)
741 ];
742
743 // dispatching events related to order status update and payment paid
744 (new OrderPaid($order, $order->customer, $transaction))->dispatch();
745
746 (new OrderStatusUpdated($order, $oldStatus, $order->status, true, $actionActivity, 'order_status'))->dispatch();
747
748 // if digital
749 if ($order->fulfillment_type == 'digital' && $order->status === Status::ORDER_PROCESSING) {
750 $order->status = Status::ORDER_COMPLETED;
751 $order->completed_at = DateTime::gmtNow();
752 $order->save();
753
754 $actionActivity = [
755 'title' => __('Order status updated', 'fluent-cart'),
756 'content' => sprintf(
757 /* translators: 1: old status, 2: new status */
758 __('Order status has been updated from %1$s to %2$s', 'fluent-cart'), Status::ORDER_PROCESSING, $order->status)
759 ];
760
761 (new OrderStatusUpdated($order, Status::ORDER_PROCESSING, $order->status, true, $actionActivity, 'order_status'))->dispatch();
762 }
763
764 return $this->response->sendSuccess([
765 'message' => __('Order has been marked as paid', 'fluent-cart')
766 ]);
767 }
768
769 public function handleBulkActions(Request $request)
770 {
771
772 $action = sanitize_text_field($request->get('action', ''));
773 $orderIds = $request->get('order_ids', []);
774
775 if ($action == 'delete_test_orders') {
776 return $this->handleDeleteTestOrdersBulkAction($request);
777 }
778
779 $orderIds = array_map(function ($id) {
780 return (int)$id;
781 }, $orderIds);
782
783 $orderIds = array_filter($orderIds);
784
785 if (!$orderIds) {
786 return $this->sendError([
787 'message' => __('Orders selection is required', 'fluent-cart')
788 ]);
789 }
790
791 $orders = Order::query()->whereIn('id', $orderIds)->get();
792
793
794 if ($action == 'delete_orders') {
795
796 $isDeleted = OrderResource::bulkDeleteByOrderIds($orderIds);
797
798 if (is_wp_error($isDeleted)) {
799 return $isDeleted;
800 }
801 return $this->response->sendSuccess($isDeleted);
802
803 // $DB = App::db();
804 // $DB->beginTransaction();
805
806 // try {
807
808 // OrderResource::bulkDeleteByOrderIds($orderIds);
809 // OrderTransaction::bulkDeleteByOrderIds($orderIds);
810 // OrderMetaResource::bulkDeleteByOrderIds($orderIds);
811 // OrderItemResource::bulkDeleteByOrderIds($orderIds);
812
813 // $DB->commit();
814
815 // return [
816 // 'message' => __('Selected orders and their associated resources have been deleted permanently', 'fluent-cart')
817 // ];
818 // } catch (\Exception $e) {
819 // $DB->rollBack();
820 // return static::makeErrorResponse([
821 // ['code' => 400, 'message' => __('Failed to delete orders and their associated resources', 'fluent-cart')]
822 // ]);
823 // }
824
825
826 }
827 if ($action == 'change_shipping_status') {
828 $newStatus = sanitize_text_field($request->get('new_status', ''));
829 if (!$newStatus) {
830 return $this->sendError([
831 'message' => __('Please select status', 'fluent-cart')
832 ]);
833 }
834
835 $validStatuses = Helper::getShippingStatuses();
836 if (!isset($validStatuses[$newStatus])) {
837 return $this->sendError([
838 'message' => __('Provided shipping status is not valid', 'fluent-cart')
839 ]);
840 }
841
842 // foreach ($orders as $order) {
843 // $order->updateShippingStatus($newStatus);
844 // }
845
846 return [
847 'message' => __('Shipping Status has been changed for the selected orders', 'fluent-cart')
848 ];
849
850 }
851 if ($action == 'change_order_status') {
852
853 $newStatus = sanitize_text_field($request->get('new_status', ''));
854 if (!$newStatus) {
855 return $this->sendError([
856 'message' => __('Please select status', 'fluent-cart')
857 ]);
858 }
859
860 $validStatuses = Status::getEditableOrderStatuses();
861 if (!isset($validStatuses[$newStatus])) {
862 return $this->sendError([
863 'message' => __('Provided order status is not valid', 'fluent-cart')
864 ]);
865 }
866
867 $failedOrderIds = [];
868 $updatedOrderIds = [];
869
870 foreach ($orders as $order) {
871 // $order->updateStatus('status', $newStatus);
872 $isUpdated = OrderResource::updateStatuses([
873 'order' => $order,
874 'action' => 'change_order_status',
875 'statuses.order_status' => $newStatus,
876 'manage_stock' => sanitize_text_field($request->get('manage_stock')),
877 ]);
878
879 if (is_wp_error($isUpdated)) {
880 $failedOrderIds[] = $order->id;
881 } else {
882 $updatedOrderIds[] = $order->id;
883 }
884 }
885
886 if (count($failedOrderIds) > 0) {
887 $failedOrderIds = implode(' , ', $failedOrderIds);
888 return count($updatedOrderIds) > 0
889 ? $this->sendSuccess([
890 'message' => sprintf(
891 /* translators: %s is the order ids */
892 __("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)
893 ])
894 :
895 $this->sendError([
896 'message' => sprintf(
897 /* translators: %s is the order ids */
898 __("The order ID - %s cannot be updated because they are either already cancelled or have the same status.", 'fluent-cart'), $failedOrderIds)
899 ], 423);
900 }
901
902 if (count($updatedOrderIds) > 0 && count($failedOrderIds) < 1) {
903 return $this->sendSuccess([
904 'message' => __('Order Status has been changed for the selected orders', 'fluent-cart')
905 ]);
906 }
907 }
908
909 if ($action == 'capture_payments') {
910 foreach ($orders as $order) {
911 $order->capturePayments();
912 }
913
914 return [
915 'message' => __('Selected payments has been successfully captured', 'fluent-cart')
916 ];
917 }
918
919 if ($action == 'change_payment_status') {
920 $newStatus = sanitize_text_field($request->get('new_status', ''));
921 if (!$newStatus) {
922 return $this->sendError([
923 'message' => __('Please select status', 'fluent-cart')
924 ]);
925 }
926
927 $validStatuses = Status::getEditableTransactionStatuses();
928 if (!isset($validStatuses[$newStatus])) {
929 return $this->sendError([
930 'message' => __('Provided payment status is not valid', 'fluent-cart')
931 ]);
932 }
933
934 $failedOrderIds = [];
935 $updatedOrderIds = [];
936 $count = 0;
937 $customerIds = [];
938
939 foreach ($orders as $order) {
940 $transaction = $order->latest_transaction;
941 $isUpdated = OrderResource::updatePaymentStatus([
942 'order' => $order,
943 'status' => $newStatus,
944 'transaction' => $transaction,
945 ]);
946
947 if (is_wp_error($isUpdated)) {
948 $failedOrderIds[] = $order->id;
949 } else {
950 $updatedOrderIds[] = $order->id;
951 $count++;
952 $customerIds[] = $order->customer_id;
953 }
954 }
955
956 if ($count > 0 && count($customerIds) > 0) {
957 (new OrderBulkAction($customerIds))->dispatch();
958 }
959
960 if (count($failedOrderIds) > 0) {
961 $failedOrderIds = implode(' , ', $failedOrderIds);
962 return count($updatedOrderIds) > 0
963 ? $this->sendSuccess([
964 'message' => sprintf(
965 /* translators: %s is the order ids */
966 __("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)
967 ])
968 :
969 $this->sendError([
970 'message' => sprintf(
971 /* translators: %s is the order ids */
972 __("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)
973 ], 423);
974 }
975
976 if (count($updatedOrderIds) > 0 && count($failedOrderIds) < 1) {
977 return $this->sendSuccess([
978 'message' => sprintf(
979 /* translators: %s is the payment status */
980 __("Selected orders payment status has been marked as %s", 'fluent-cart'),
981 $newStatus
982 )
983 ]);
984 }
985 }
986
987 return $this->sendError([
988 'message' => __('Selected action is invalid', 'fluent-cart')
989 ]);
990
991 }
992
993 protected function handleDeleteTestOrdersBulkAction(Request $request)
994 {
995 $batchSize = max(1, (int)apply_filters('fluent_cart/order/delete_test_orders_batch_size', 50));
996 $lastOrderId = max(0, (int)$request->get('last_order_id', 0));
997 $totalCount = (int)$this->getTestOrdersQuery()->count();
998
999 $testOrders = $this->getTestOrdersQuery()
1000 ->select('id')
1001 ->orderBy('id')
1002 ->when($lastOrderId > 0, function ($query) use ($lastOrderId) {
1003 return $query->where('id', '>', $lastOrderId);
1004 })
1005 ->limit($batchSize)
1006 ->get();
1007
1008 $batchOrderIds = $testOrders->pluck('id')->map(function ($id) {
1009 return (int)$id;
1010 })->toArray();
1011
1012 if (!$batchOrderIds) {
1013 return $this->sendSuccess([
1014 'message' => $lastOrderId
1015 ? __('Test order deletion completed.', 'fluent-cart')
1016 : __('No test orders found to delete', 'fluent-cart'),
1017 'total_count' => $totalCount,
1018 'batch_size' => $batchSize,
1019 'batch_count' => 0,
1020 'deleted_count' => 0,
1021 'failed_count' => 0,
1022 'deleted_order_ids' => [],
1023 'failed_order_ids' => [],
1024 'last_attempted_order_id' => $lastOrderId,
1025 'has_more' => false
1026 ]);
1027 }
1028
1029 $isDeleted = OrderResource::bulkDeleteByOrderIds($batchOrderIds);
1030
1031 if (is_wp_error($isDeleted)) {
1032 $deletedOrderIds = [];
1033 $failedOrderIds = $batchOrderIds;
1034 } else {
1035 $deletedOrderIds = array_values(array_unique(array_map('intval', Arr::get($isDeleted, 'data.deleted_order_ids', []))));
1036 $failedOrderIds = array_values(array_unique(array_map('intval', Arr::get($isDeleted, 'data.failed_order_ids', []))));
1037 }
1038
1039 $deletedCount = count($deletedOrderIds);
1040 $failedCount = count($failedOrderIds);
1041 $lastAttemptedOrderId = (int)end($batchOrderIds);
1042 $hasMore = $this->getTestOrdersQuery()
1043 ->where('id', '>', $lastAttemptedOrderId)
1044 ->exists();
1045
1046 return $this->sendSuccess([
1047 'message' => $hasMore
1048 ? __('Deleting test orders...', 'fluent-cart')
1049 : __('Test order deletion completed.', 'fluent-cart'),
1050 'total_count' => $totalCount,
1051 'batch_size' => $batchSize,
1052 'batch_count' => count($batchOrderIds),
1053 'deleted_count' => $deletedCount,
1054 'failed_count' => $failedCount,
1055 'deleted_order_ids' => $deletedOrderIds,
1056 'failed_order_ids' => $failedOrderIds,
1057 'last_attempted_order_id' => $lastAttemptedOrderId,
1058 'has_more' => $hasMore
1059 ]);
1060 }
1061
1062 public function updateTransactionStatus(Request $request, $order, OrderTransaction $transaction)
1063 {
1064
1065 $order = Order::query()->find($order);
1066 $newStatus = $request->get('status');
1067 if ($transaction->status == $newStatus) {
1068 return $this->sendError([
1069 'reload' => true,
1070 'message' => __('Transaction already has the same status', 'fluent-cart')
1071 ]);
1072 }
1073
1074 if ($transaction->order_id != $order->id) {
1075 return $this->sendError([
1076 'message' => __('The selected transaction does not match with the provided order', 'fluent-cart')
1077 ]);
1078 }
1079
1080 $transaction->updateStatus($newStatus);
1081 $order->updatePaymentStatus($newStatus);
1082
1083 return [
1084 'transaction' => $transaction,
1085 'message' => __('Payment status has been successfully updated', 'fluent-cart')
1086 ];
1087 }
1088
1089 public function getStats($orderUuid): \WP_REST_Response
1090 {
1091 $order = OrderResource::find($orderUuid);
1092 return $this->sendSuccess([
1093 'widgets' => apply_filters('fluent_cart/widgets/single_order', [], $order)
1094 ]);
1095 }
1096
1097 public function getShippingMethods(Request $request): \WP_REST_Response
1098 {
1099 $countryCode = $request->get('country_code');
1100 $state = $request->get('state') ?? '';
1101 $orderItems = $this->prepareOrderItemsWithVariations($request->get('order_items'));
1102
1103 $enabledMethods = $this->getEnabledShippingMethodsWithCharges($orderItems);
1104
1105 if (empty($countryCode)) {
1106 return $this->sendSuccess([
1107 'shipping_methods' => [],
1108 'other_shipping_methods' => $enabledMethods,
1109 ]);
1110 }
1111
1112 $applicableMethods = ShippingMethod::getApplicableForCountry($countryCode, $state);
1113
1114 $applicableIds = $applicableMethods->pluck('id')->toArray();
1115
1116 return $this->sendSuccess([
1117 'shipping_methods' => $enabledMethods->whereIn('id', $applicableIds)->values(),
1118 'other_shipping_methods' => $enabledMethods->whereNotIn('id', $applicableIds)->values(),
1119 ]);
1120 }
1121
1122 protected function getEnabledShippingMethodsWithCharges(array $orderItems)
1123 {
1124 return ShippingMethod::query()
1125 ->where('is_enabled', '1')
1126 ->get()
1127 ->each(function ($method) use ($orderItems) {
1128 $method->shipping_charge = CartHelper::calculateShippingMethodCharge($method, $orderItems);
1129 });
1130 }
1131
1132 public function updateShipping(Request $request)
1133 {
1134 $orderItems = $request->get('order_items');
1135 $shippingMethodId = $request->get('shipping_id');
1136
1137 $orderItems = $this->prepareOrderItemsWithVariations($orderItems);
1138
1139
1140 $method = ShippingMethod::query()->find($shippingMethodId);
1141
1142 $totalShippingCharge = CartHelper::calculateShippingMethodCharge($method, $orderItems);
1143
1144 return $this->sendSuccess([
1145 'message' => __('Shipping updated', 'fluent-cart'),
1146 'shipping_charge' => $totalShippingCharge,
1147 'order_items' => $orderItems
1148 ]);
1149
1150 }
1151
1152 protected function prepareOrderItemsWithVariations($orderItems)
1153 {
1154 $itemCollection = (new Collection($orderItems))->keyBy('id');
1155 $ids = $itemCollection->keys()->toArray();
1156 $variations = ProductVariation::query()->with('shippingClass')->whereIn('id', $ids)->get();
1157 $orderItems = $itemCollection->toArray();
1158
1159 foreach ($variations as &$variation) {
1160 $shippingCharge = CartHelper::calculateShippingCharge($variation, $itemCollection->get($variation->id)['quantity']);
1161 $variation->quantity = Arr::get($orderItems, $variation->id . '.' . 'quantity', 1);
1162 $variation->discount_total = Arr::get($orderItems, $variation->id . '.' . 'discount_total', 0);
1163 $variation->shipping_charge = $shippingCharge;
1164 $variation->unit_price = $variation->item_price;
1165
1166 }
1167
1168 $orderItems = $variations->mapWithKeys(function ($item) {
1169 return [
1170 $item->id => [
1171 'id' => $item->id,
1172 'quantity' => $item->quantity,
1173 'shipping_charge' => $item->shipping_charge,
1174 'unit_price' => $item->unit_price,
1175 'other_info' => $item->other_info,
1176 'discount_total' => $item->discount_total,
1177 'fulfillment_type' => $item->fulfillment_type,
1178 ]
1179 ];
1180 });
1181 $orderItems = $orderItems->toArray();
1182
1183 return $orderItems;
1184 }
1185
1186 public function acceptDispute(Request $request, $order, $transaction)
1187 {
1188 $order = Order::query()->find($order);
1189 $transaction = OrderTransaction::query()->find($transaction);
1190 $response = $transaction->acceptDispute([
1191 'dispute_note' => $request->getSafe('dispute_note', 'sanitize_text_field'),
1192 ]);
1193
1194 if (is_wp_error($response)) {
1195 return $this->sendError([
1196 'message' => $response->get_error_message()
1197 ]);
1198 }
1199
1200 return $this->sendSuccess([
1201 'message' => __('Dispute accepted!', 'fluent-cart')
1202 ]);
1203 }
1204
1205 public function syncOrderStatuses(Request $request, Order $order)
1206 {
1207 $latestTransaction = OrderTransaction::query()
1208 ->where('order_id', $order->id)
1209 ->orderBy('id', 'desc')
1210 ->first();
1211
1212 if (!$latestTransaction) {
1213 return $this->sendError([
1214 'message' => __('No transaction found for this order', 'fluent-cart')
1215 ], 404);
1216 }
1217
1218 (new StatusHelper($order))->syncOrderStatuses($latestTransaction);
1219
1220 // Reload order to get updated data
1221 $order = Order::query()->find($order->id);
1222
1223 return $this->sendSuccess([
1224 'message' => __('Order statuses synced successfully', 'fluent-cart'),
1225 'order' => $order,
1226 'payment_status' => $order->payment_status,
1227 'status' => $order->status,
1228 ]);
1229 }
1230
1231 }
1232