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

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