PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.1
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 / ProductController.php

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

1,586 lines 52.1 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 use FluentCart\Api\Resource\ProductDetailResource;
6 use FluentCart\Api\Resource\ProductResource;
7 use FluentCart\Api\Resource\ProductVariationResource;
8 use FluentCart\Api\Resource\ShopResource;
9 use FluentCart\Api\Taxonomy;
10 use FluentCart\App\CPT\FluentProducts;
11 use FluentCart\App\Helpers\AdminHelper;
12 use FluentCart\App\Helpers\Helper;
13 use FluentCart\App\Http\Requests\ProductCreateRequest;
14 use FluentCart\App\Http\Requests\ProductRequest;
15 use FluentCart\App\Http\Requests\ProductUpdateRequest;
16 use FluentCart\App\Http\Requests\UpgradePathSettingRequest;
17 use FluentCart\App\Models\Meta;
18 use FluentCart\App\Models\Product;
19 use FluentCart\App\Models\ProductDetail;
20 use FluentCart\App\Models\ProductDownload;
21 use FluentCart\App\Models\ProductMeta;
22 use FluentCart\App\Models\ProductVariation;
23 use FluentCart\App\Models\ShippingClass;
24 use FluentCart\App\Models\TaxClass;
25 use FluentCart\App\Modules\ReportingModule\ProductReport;
26 use FluentCart\App\Services\Async\DummyProductService;
27 use FluentCart\App\Services\BulkProductInsertService;
28 use FluentCart\App\Services\BulkProductUpdateService;
29 use FluentCart\App\Services\Filter\ProductFilter;
30 use FluentCart\App\Services\PlanUpgradeService;
31 use FluentCart\Framework\Database\Orm\Builder;
32 use FluentCart\Framework\Http\Request\Request;
33 use FluentCart\Framework\Support\Arr;
34 use FluentCart\Framework\Support\Collection;
35 use FluentCart\Framework\Support\Str;
36 use WP_REST_Response;
37 use FluentCart\Api\Helper as ApiHelper;
38
39 class ProductController extends Controller
40 {
41 public function index(Request $request): WP_REST_Response
42 {
43 //$request->set('with', ['detail', 'variants:post_id,available,manage_stock,stock_status,variation_title,other_info']);
44 $products = ProductFilter::fromRequest($request)->paginate();
45
46 $products->setCollection(
47 $products->getCollection()->transform(function ($product) {
48 return $product->setAppends(['view_url', 'edit_url']);
49 })
50 );
51
52 $products = apply_filters('fluent_cart/products_list', $products);
53
54 return $this->sendSuccess([
55 'products' => $products
56 ]);
57 }
58
59 public function find(Request $request, Product $product): array
60 {
61 if ($request->get('with')) {
62 $product->load($request->get('with'));
63 }
64 $data = [
65 'product' => $product,
66 ];
67
68 if (in_array('product_menu', $request->get('with', []))) {
69 $data['product_menu'] = AdminHelper::getProductMenu($product);
70 }
71
72 return $data;
73 }
74
75 public function getRelatedProducts(Request $request, $productId): WP_REST_Response
76 {
77 $productId = absint($productId);
78
79 if (!$productId) {
80 return $this->sendError('Invalid product ID');
81 }
82
83 $relatedBy = [];
84
85 if (filter_var($request->get('related_by_categories'), FILTER_VALIDATE_BOOLEAN)) {
86 $relatedBy[] = 'product-categories';
87 }
88
89 if (filter_var($request->get('related_by_brands'), FILTER_VALIDATE_BOOLEAN)) {
90 $relatedBy[] = 'product-brands';
91 }
92
93 $orderBy = sanitize_text_field($request->get('order_by', 'title_asc'));
94 $postsPerPage = absint($request->get('posts_per_page', 6));
95
96 $products = ShopResource::getSimilarProducts($productId, true, [
97 'related_by' => $relatedBy,
98 'order_by' => $orderBy,
99 'posts_per_page' => $postsPerPage,
100 ]);
101
102 return $this->sendSuccess([
103 'products' => $products
104 ]);
105 }
106
107
108 /**
109 *
110 * @param ProductRequest $request
111 * @return WP_REST_Response
112 */
113 public function create(ProductCreateRequest $request): WP_REST_Response
114 {
115
116 $data = $request->getSafe($request->sanitize());
117
118
119 $postData = array_filter(Arr::only($data, [
120 'post_title',
121 'post_status',
122 //'detail',
123 ]));
124
125 $postData['post_name'] = sanitize_title($postData['post_title']);
126
127 $postData['post_type'] = FluentProducts::CPT_NAME;
128 $createdPostId = wp_insert_post($postData);
129
130 if (is_wp_error($createdPostId)) {
131 return $this->sendError([
132 'code' => 403,
133 'message' => $createdPostId->get_error_message()
134 ]);
135 }
136
137 $detail = Arr::get($data, 'detail');
138 $detail['post_id'] = $createdPostId;
139
140 $isDigital = Arr::get($detail, 'fulfillment_type') === 'digital';
141
142 $createdProductDetail = ProductDetail::query()->create($detail);
143 $variation = ProductVariation::query()->create([
144 'post_id' => $createdPostId,
145 'serial_index' => 1,
146 'variation_title' => $postData['post_title'],
147 //'stock_status' => $isDigital ? 'in-stock' : 'out-of-stock',
148 'stock_status' => 'in-stock',
149 'payment_type' => 'onetime',
150 'total_stock' => 1,
151 'available' => 1,
152 'fulfillment_type' => $detail['fulfillment_type'],
153 'other_info' => [
154 'description' => '',
155 'payment_type' => 'onetime',
156 'tax_class' => 'standard',
157 'tax_exempt' => 'no',
158 'times' => '',
159 'repeat_interval' => '',
160 'trial_days' => '',
161 'billing_summary' => '',
162 'manage_setup_fee' => 'no',
163 'signup_fee_name' => '',
164 'signup_fee' => '',
165 'setup_fee_per_item' => 'no',
166 'is_bundle_product' => Arr::get($detail, 'other_info.is_bundle_product', 'no'),
167 ]
168 ]);
169
170 if ($createdProductDetail) {
171 return $this->sendSuccess([
172 'data' => [
173 'ID' => $createdPostId,
174 'variant' => $variation,
175 'product_details' => Arr::get($createdProductDetail, 'data'),
176 ],
177 'message' => __('Product has been created successfully', 'fluent-cart')
178 ]);
179 }
180
181 return $this->sendError(['code' => 400, 'message' => __('Product creation failed!', 'fluent-cart')]);
182
183 }
184
185 /**
186 * Bulk insert products from import/manual entry.
187 *
188 * @param Request $request
189 * @return WP_REST_Response
190 */
191 public function bulkInsert(Request $request): WP_REST_Response
192 {
193 $products = $request->get('products', []);
194
195 if (empty($products) || !is_array($products)) {
196 return $this->sendError([
197 'message' => __('No products provided', 'fluent-cart'),
198 ]);
199 }
200
201 if (count($products) > 10) {
202 return $this->sendError([
203 'message' => __('Maximum 10 products per chunk allowed', 'fluent-cart'),
204 ]);
205 }
206
207 try {
208 $service = new BulkProductInsertService();
209 $result = $service->insertChunk($products);
210
211 if (empty($result['created']) && !empty($result['errors'])) {
212 return $this->sendError([
213 'message' => __('All products failed to insert', 'fluent-cart'),
214 'errors' => $result['errors'],
215 ]);
216 }
217
218 return $this->sendSuccess([
219 'message' => sprintf(
220 __('%d product(s) created successfully', 'fluent-cart'),
221 count($result['created'])
222 ),
223 'created' => $result['created'],
224 'errors' => $result['errors'],
225 ]);
226 } catch (\Throwable $e) {
227 return $this->sendError([
228 'message' => __('Bulk insert failed: ', 'fluent-cart') . $e->getMessage(),
229 ]);
230 }
231 }
232
233 /**
234 * Duplicate a product with selected options
235 *
236 * @param Request $request
237 * @param int $productId
238 * @return WP_REST_Response
239 */
240 public function duplicate(Request $request, $productId): WP_REST_Response
241 {
242 try {
243 $data = $request->getSafe([
244 'import_stock_management' => 'sanitize_text_field',
245 'import_license_settings' => 'sanitize_text_field',
246 'import_downloadable_files' => 'sanitize_text_field',
247 ]);
248
249 $importStockManagement = filter_var(
250 Arr::get($data, 'import_stock_management', false),
251 FILTER_VALIDATE_BOOLEAN
252 );
253 $importLicenseSettings = filter_var(
254 Arr::get($data, 'import_license_settings', false),
255 FILTER_VALIDATE_BOOLEAN
256 );
257 $importDownloadableFiles = filter_var(
258 Arr::get($data, 'import_downloadable_files', false),
259 FILTER_VALIDATE_BOOLEAN
260 );
261
262 try {
263 $newProductId = Product::duplicateProduct($productId, [
264 'import_stock_management' => $importStockManagement,
265 'import_license_settings' => $importLicenseSettings,
266 'import_downloadable_files' => $importDownloadableFiles,
267 ]);
268
269 return $this->sendSuccess([
270 'product_id' => $newProductId,
271 'message' => __('Product duplicated successfully. The new product has been saved as a draft.', 'fluent-cart')
272 ]);
273
274 } catch (\RuntimeException $e) {
275 if ((int)$e->getCode() === 404) {
276 return $this->sendError([
277 'message' => __('Product not found', 'fluent-cart')
278 ]);
279 }
280 return $this->sendError([
281 'message' => __('Failed to duplicate product: ', 'fluent-cart') . $e->getMessage()
282 ]);
283 } catch (\Exception $e) {
284 return $this->sendError([
285 'message' => __('Failed to duplicate product: ', 'fluent-cart') . $e->getMessage()
286 ]);
287 }
288
289 } catch (\Exception $e) {
290 return $this->sendError([
291 'message' => __('An error occurred while duplicating the product.', 'fluent-cart'),
292 'error' => $e->getMessage()
293 ]);
294 }
295 }
296
297 public function delete(Request $request, Product $product)
298 {
299
300 $isDeleted = ProductResource::delete($product->ID);
301
302 if (is_wp_error($isDeleted)) {
303 return $isDeleted;
304 }
305 return $this->response->sendSuccess($isDeleted);
306 }
307
308 public function update(ProductUpdateRequest $request, $postId)
309 {
310 $data = $request->getSafe($request->sanitize());
311
312 if (
313 Arr::get($data, 'detail.variation_type') === 'simple' &&
314 (empty(Arr::get($data, 'variants')) || empty(Arr::get($data, 'variants.0')))
315 ) {
316 return $this->sendError(
317 [
318 'message' => __('Variation info is not present', 'fluent-cart')
319 ]
320 );
321 }
322
323 // $hasError = ProductResource::validateDownloadableFiles($data);
324 // if (!empty($hasError)) {
325 // return $this->sendError($hasError);
326 // }
327
328 $isUpdated = ProductResource::update($data, $postId);
329
330
331 if (is_wp_error($isUpdated)) {
332 return $isUpdated;
333 }
334
335 do_action('fluent_cart/product_updated', [
336 'data' => $data,
337 'product' => $isUpdated['data']
338 ]);
339
340 return $this->response->sendSuccess($isUpdated);
341 }
342
343 public function updateLongDescEditorMode(Request $request, $postId)
344 {
345 // Validate input
346 $activeEditor = sanitize_text_field($request->get('active_editor'));
347
348 // Fetch product detail directly by post_id
349 $productDetail = ProductDetail::query()->where('post_id', $postId)->first();
350
351 if (!$productDetail) {
352 return $this->sendError([
353 'message' => __('Product not found', 'fluent-cart')
354 ]);
355 }
356
357 $otherInfo = $productDetail->other_info;
358 $otherInfo['active_editor'] = $activeEditor;
359
360 // Update product detail
361 $isUpdated = $productDetail->update([
362 'other_info' => $otherInfo
363 ]);
364
365 if (!$isUpdated) {
366 return $this->sendError([
367 'message' => __('Failed to update editor mode', 'fluent-cart')
368 ]);
369 }
370
371 return $this->sendSuccess([
372 'message' => __('Editor mode updated successfully', 'fluent-cart')
373 ]);
374 }
375
376
377 public function updateTaxClass(Request $request, $postId)
378 {
379 $taxClassId = sanitize_text_field(Arr::get($this->request->all(), 'tax_class', 0));
380
381 $taxClass = TaxClass::query()->findOrFail($taxClassId);
382
383 if (empty($taxClass)) {
384 return $this->sendError([
385 'message' => __('Tax Class not found', 'fluent-cart')
386 ]);
387 }
388
389 $productDetail = ProductDetail::query()->where('post_id', $postId)->first();
390
391 if (empty($productDetail)) {
392 return $this->sendError([
393 'message' => __('Product not found', 'fluent-cart')
394 ]);
395 }
396
397 // Get existing other_info and merge with new tax_class
398 $otherInfo = $productDetail->other_info;
399 $otherInfo['tax_class'] = $taxClass->id;
400
401 // update only tax_class inside the $productDetails->other_info
402 $productDetail->update([
403 'other_info' => $otherInfo
404 ]);
405
406 return $this->sendSuccess([
407 'message' => __('Tax profile updated successfully', 'fluent-cart')
408 ]);
409 }
410
411 public function removeTaxClass(Request $request, $postId)
412 {
413 $productDetail = ProductDetail::query()->where('post_id', $postId)->first();
414 if (empty($productDetail)) {
415 return $this->sendError([
416 'message' => __('Product not found', 'fluent-cart')
417 ]);
418 }
419 $otherInfo = $productDetail->other_info;
420 $otherInfo['tax_class'] = '';
421 $productDetail->update([
422 'other_info' => $otherInfo
423 ]);
424 return $this->sendSuccess([
425 'message' => __('Tax profile removed successfully', 'fluent-cart')
426 ]);
427 }
428
429 public function toggleTaxExempt(Request $request, $postId)
430 {
431 $productDetail = ProductDetail::query()->where('post_id', $postId)->first();
432 if (empty($productDetail)) {
433 return $this->sendError([
434 'message' => __('Product not found', 'fluent-cart')
435 ]);
436 }
437
438 $otherInfo = $productDetail->other_info ?: [];
439 $taxExempt = sanitize_text_field($request->get('tax_exempt', 'no'));
440 $existingTaxClass = Arr::get($otherInfo, 'tax_class');
441 // Product-level tax settings live on product detail, so convert the UI
442 // slug back to the stored tax-class ID before persisting the change.
443 $taxClassSlug = sanitize_text_field($request->get('tax_class', ''));
444 $otherInfo['tax_exempt'] = $taxExempt === 'yes' ? 'yes' : 'no';
445
446 if (!$taxClassSlug) {
447 $defaultClass = TaxClass::query()->where('slug', 'standard')->first();
448 $resolvedTaxClassId = $existingTaxClass ?: ($defaultClass ? $defaultClass->id : null);
449 if ($resolvedTaxClassId) {
450 $resolvedTaxClass = TaxClass::query()->find($resolvedTaxClassId);
451 $taxClassSlug = $resolvedTaxClass ? $resolvedTaxClass->slug : 'standard';
452 } else {
453 $taxClassSlug = 'standard';
454 }
455 } else {
456 $taxClass = TaxClass::query()->where('slug', $taxClassSlug)->first();
457 if (!$taxClass) {
458 return $this->sendError([
459 'message' => __('Invalid tax class', 'fluent-cart')
460 ], 422);
461 }
462 $resolvedTaxClassId = $taxClass->id;
463 }
464
465 $otherInfo['tax_class'] = $resolvedTaxClassId;
466
467 $productDetail->update([
468 'other_info' => $otherInfo
469 ]);
470
471 return $this->sendSuccess([
472 'message' => $taxExempt === 'yes'
473 ? __('Product is now tax exempt', 'fluent-cart')
474 : __('Tax will be charged on this product', 'fluent-cart'),
475 'tax_exempt' => $otherInfo['tax_exempt'],
476 'tax_class' => $otherInfo['tax_class'],
477 'tax_class_slug' => $taxClassSlug ?: 'standard'
478 ]);
479 }
480
481 public function updateShippingClass(Request $request, $postId)
482 {
483 $shippingClassId = sanitize_text_field(Arr::get($this->request->all(), 'shipping_class', 0));
484 $shippingClass = ShippingClass::query()->findOrFail($shippingClassId);
485
486 if (empty($shippingClass)) {
487 return $this->sendError([
488 'message' => __('Shipping Class not found', 'fluent-cart')
489 ]);
490 }
491
492 $productDetail = ProductDetail::query()->where('post_id', $postId)->first();
493 if (empty($productDetail)) {
494 return $this->sendError([
495 'message' => __('Product not found', 'fluent-cart')
496 ]);
497 }
498 $otherInfo = $productDetail->other_info;
499 $otherInfo['shipping_class'] = $shippingClass->id;
500 $productDetail->update([
501 'other_info' => $otherInfo
502 ]);
503 return $this->sendSuccess([
504 'message' => __('Shipping Class updated successfully', 'fluent-cart')
505 ]);
506 }
507
508 public function removeShippingClass(Request $request, $postId)
509 {
510 $productDetail = ProductDetail::query()->where('post_id', $postId)->first();
511 if (empty($productDetail)) {
512 return $this->sendError([
513 'message' => __('Product not found', 'fluent-cart')
514 ]);
515 }
516
517 $otherInfo = $productDetail->other_info;
518 $otherInfo['shipping_class'] = '';
519 $productDetail->update([
520 'other_info' => $otherInfo
521 ]);
522
523 return $this->sendSuccess([
524 'message' => __('Shipping Class removed successfully', 'fluent-cart')
525 ]);
526 }
527
528 /**
529 *
530 * @param Request $request
531 * @param $productId
532 * @return WP_REST_Response
533 */
534 public function get(Request $request, $productId)
535 {
536 $product = Product::with([
537 'detail',
538 'variants' => function ($query) {
539 $query->with(['media'])
540 ->orderBy('serial_index', 'ASC');
541 }
542 ])->with('downloadable_files')->find($productId);
543
544 if (empty($product)) {
545 return $this->entityNotFoundError(
546 __('Product not found', 'fluent-cart'),
547 __('Back to Product List', 'fluent-cart'),
548 '/products'
549 );
550 }
551
552 // $product = ProductResource::search(['id' => $productId], function (Builder $query) {
553 // return $query
554 // ->with([
555 // 'detail',
556 // 'variants' => function ($query) {
557 // $query->with(['media'])
558 // ->orderBy('serial_index', 'ASC');
559 // }
560 // ])
561 // ->with('downloadable_files');
562 // }, true)->first();
563
564
565 if (!empty($product)) {
566 $product->setAppends(['view_url', 'edit_url']);
567 $taxonomies = Taxonomy::getTaxonomies();
568 $taxonomies = Collection::make($taxonomies)
569 ->map(function ($taxonomy) use (&$product) {
570 $taxonomy_object = get_taxonomy($taxonomy);
571 $all_labels = (array)get_taxonomy_labels($taxonomy_object);
572 $filtered_labels = Arr::only($all_labels, ['singular_name', 'name']);
573 $product[$taxonomy] = Taxonomy::getTermIdsFromTerms($product->getTermByType($taxonomy)->get()->toArray());
574
575 return [
576 'name' => $taxonomy,
577 'label' => Str::headline($taxonomy),
578 'terms' => Taxonomy::getFormattedTerms($taxonomy),
579 'labels' => $filtered_labels
580 ];
581 });
582
583 if (in_array('product_menu', $request->get('with', []))) {
584 $productMenu = AdminHelper::getProductMenu($product);
585 }
586
587 $featuredImageId = get_post_thumbnail_id($product->ID);
588 $productData = $product->toArray();
589 $productData['featured_image_id'] = $featuredImageId;
590 //get featured image id
591 return $this->sendSuccess([
592 'product' => $productData,
593 'product_menu' => $productMenu ?? "",
594 'taxonomies' => $taxonomies,
595 ]);
596 } else {
597 return $this->sendError([
598 'message' => __('Something went wrong', 'fluent-cart'),
599 ]);
600 }
601 //return ProductResource::find($postId);
602 }
603
604
605 public function getUpgradeSettings($id, Request $request): WP_REST_Response
606 {
607 return $this->sendSuccess(
608 [
609 'data' => PlanUpgradeService::getUpgradeSettings($id)
610 ]
611 );
612
613 }
614
615 public function saveUpgradeSetting(UpgradePathSettingRequest $request, $id): WP_REST_Response
616 {
617 $data = ApiHelper::sanitizeTextAll(
618 $request->except('query_timestamp')
619 );
620
621 $isSaved = PlanUpgradeService::saveUpgradeSetting($data);
622
623 if ($isSaved) {
624 return $this->sendSuccess(
625 [
626 'message' => __('Settings saved successfully', 'fluent-cart')
627 ]
628 );
629 } else {
630 return $this->sendError(
631 [
632 'message' => __('Failed to save settings', 'fluent-cart')
633 ]
634 );
635 }
636
637 }
638
639 public function deleteUpgradePath($id): WP_REST_Response
640 {
641 $isDeleted = Meta::query()->where('id', $id)->delete();
642 if ($isDeleted) {
643 return $this->sendSuccess(
644 [
645 'message' => __('Path deleted successfully', 'fluent-cart')
646 ]
647 );
648 } else {
649 return $this->sendError(
650 [
651 'message' => __('Failed to delete path', 'fluent-cart')
652 ]
653 );
654 }
655 }
656
657 public function updateUpgradePath(UpgradePathSettingRequest $request, $id): WP_REST_Response
658 {
659 $data = ApiHelper::sanitizeTextAll(
660 $request->except('query_timestamp')
661 );
662
663 $isUpdated = PlanUpgradeService::updateUpgradeSetting($id, $data);
664
665 if ($isUpdated) {
666 return $this->sendSuccess(
667 [
668 'message' => __('Settings updated successfully', 'fluent-cart')
669 ]
670 );
671 } else {
672 return $this->sendError(
673 [
674 'message' => __('Failed to update settings', 'fluent-cart')
675 ]
676 );
677 }
678
679 }
680
681 public function getUpgradePaths($variationId, Request $request)
682 {
683 $params = $request->get('params');
684 $orderHash = Arr::get($params, 'order_hash');
685
686 if (!$variationId || !$orderHash) {
687 return [];
688 }
689
690 $upgradePaths = PlanUpgradeService::getUpgardePathsFromVariation($variationId, $orderHash);
691
692 return $this->sendSuccess([
693 'upgradePaths' => $upgradePaths
694 ]);
695 }
696
697 public function getPricingWidgets(Request $request, $productId)
698 {
699 $thisMonthKey = '-' . gmdate('d') . ' days';
700 $ranges = [
701 'all_time' => __('All time', 'fluent-cart'),
702 '-30 days' => __('Last 30 days', 'fluent-cart'),
703 $thisMonthKey => __('This month', 'fluent-cart'),
704 ];
705
706 $stats = ProductReport::getStatByProductIds([$productId], array_keys($ranges));
707
708 $html = '<ul class="fct-lists">';
709
710 foreach ($ranges as $rangeSlug => $range) {
711 $stat = $stats[$rangeSlug];
712 $prefix = '';
713 if ($stat->total_quantity) {
714 $prefix = ' <b title="Quantity">(' . $stat->total_quantity . ')</b> ';
715 }
716 $html .= '<li> <span>' . $range . $prefix . '</span>' . '<span>' . Helper::toDecimal($stat->total_amount, true) . '</span>';
717 }
718
719 $html .= '</ul>';
720
721 $widgets = [
722 [
723 'title' => __('Quick Sales Overview', 'fluent-cart'),
724 'body' => $html,
725 ],
726 ];
727
728 return [
729 'widgets' => $widgets,
730 ];
731 }
732
733 public function updateProductDetail(Request $request, $id)
734 {
735 $data = $request->getSafe([
736 'variation_type' => 'sanitize_key',
737 'variation_ids.*' => 'intval',
738 'action' => 'sanitize_key'
739 ]);
740
741 $isUpdated = ProductDetailResource::update(
742 $data,
743 $id,
744 ['action' => Arr::get($data, 'action', 'change_variation_type')]
745 );
746
747 if (is_wp_error($isUpdated)) {
748 return $isUpdated;
749 }
750 return $this->response->sendSuccess($isUpdated);
751 }
752
753 public function syncTaxonomyTerms(Request $request, $id)
754 {
755
756 if (empty(($request->get('terms')))) {
757 $data['taxonomy'] = sanitize_key($request->get('taxonomy'));
758 } else {
759 $data = $request->getSafe([
760 'taxonomy' => 'sanitize_key',
761 'terms.*' => 'intval',
762 ]);
763 }
764 $isUpdated = ProductResource::syncTaxonomyTerms($data, $id);
765
766 if (is_wp_error($isUpdated)) {
767 return $isUpdated;
768 }
769 return $this->response->sendSuccess($isUpdated);
770 }
771
772 public function deleteTaxonomyTerms(Request $request, $id)
773 {
774
775 $data = $request->getSafe([
776 'taxonomy' => 'sanitize_key',
777 'term' => 'intval',
778 ]);
779 $isUpdated = ProductResource::deleteTaxonomyTerms($data, $id);
780
781 if (is_wp_error($isUpdated)) {
782 return $isUpdated;
783 }
784 return $this->response->sendSuccess($isUpdated);
785 }
786
787 public function updateVariantOption(Request $request, $postId)
788 {
789
790
791 $data = $request->all();
792 // ProductValidator::validate($data, [
793 // 'variation_type' => 'required',
794 // 'product_id' => 'required',
795 // 'options.*.id' => 'required',
796 // 'options.*.variants' => 'required',
797 // ]);
798 $isSynced = ProductResource::syncVariantOption($postId, $data);
799
800 if (is_wp_error($isSynced)) {
801 return $isSynced;
802 }
803 return $this->response->sendSuccess($isSynced);
804 }
805
806
807 public function addProductTerms(Request $request)
808 {
809 $data = $request->get('term');
810 $name = Arr::get($data, 'name', '');
811 $taxonomy = Arr::get($data, 'taxonomy', '');
812 $parent = Arr::get($data, 'parent', '');
813 $name = sanitize_text_field($name);
814 $taxonomy = sanitize_text_field($taxonomy);
815 $parent = sanitize_text_field($parent);
816
817 $args = [];
818
819 if (!empty($parent)) {
820 $args['parent'] = $parent;
821 }
822
823
824 $termNames = explode(',', $name);
825 $ids = Taxonomy::addTaxonomyTerms($taxonomy, $termNames, $args);
826
827 if (count($ids)) {
828 $this->response->json([
829 'term_ids' => $ids,
830 'names' => $termNames
831 ]);
832 } else {
833 $this->response->json([
834 'message' => __('Unable To Create Term/s', 'fluent-cart'),
835 ], 423);
836 }
837
838 }
839
840 public function getProductTermsList(): array
841 {
842 $taxonomies = Taxonomy::getTaxonomies();
843
844 $taxonomies = Collection::make($taxonomies)
845 ->map(function ($taxonomy) {
846 return [
847 'name' => $taxonomy,
848 'label' => Str::headline($taxonomy),
849 'terms' => Taxonomy::getFormattedTerms($taxonomy),
850 ];
851 });
852 return [
853 "taxonomies" => $taxonomies,
854 ];
855 }
856
857 public function getProductTermListByParent(Request $request): WP_REST_Response
858 {
859 $parentTerms = $request->get('parents');
860 $termsData = [];
861 foreach ($request->get('listeners') as $listener) {
862 $termsData[$listener] = Taxonomy::getFormattedTerms($listener, false, $parentTerms);
863 }
864 return $this->sendSuccess([
865 'data' => $termsData
866 ]);
867
868 }
869
870 public function handleBulkActions(Request $request)
871 {
872
873 $isUpdated = ProductResource::manageBulkActions($request->all());
874
875 if (is_wp_error($isUpdated)) {
876 return $isUpdated;
877 }
878 return $this->response->sendSuccess($isUpdated);
879 }
880
881 public static function getMimeGroups()
882 {
883 return apply_filters('fluent_support/mime_groups', [
884 'images' => [
885 'title' => __('Photos', 'fluent-cart'),
886 'mimes' => [
887 'image/gif',
888 'image/ief',
889 'image/jpeg',
890 'image/webp',
891 'image/pjpeg',
892 'image/ktx',
893 'image/png',
894 ],
895 ],
896 'csv' => [
897 'title' => __('CSV', 'fluent-cart'),
898 'mimes' => [
899 'application/csv',
900 'application/txt',
901 'text/csv',
902 'text/plain',
903 'text/comma-separated-values',
904 'text/anytext',
905 ],
906 ],
907 'documents' => [
908 'title' => __('PDF/Docs', 'fluent-cart'),
909 'mimes' => [
910 'application/excel',
911 'application/vnd.ms-excel',
912 'application/vnd.msexcel',
913 'application/octet-stream',
914 'application/pdf',
915 'application/msword',
916 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
917 ],
918 ],
919 'zip' => [
920 'title' => __('Zip', 'fluent-cart'),
921 'mimes' => [
922 'application/zip',
923 ],
924 ],
925 'json' => [
926 'title' => __('JSON', 'fluent-cart'),
927 'mimes' => [
928 'application/json',
929 'application/jsonml+json',
930 ],
931 ],
932 ]);
933 }
934
935 public function searchVariantByName(Request $request): array
936 {
937 $data = $request->getSafe([
938 'name' => 'sanitize_text_field',
939 'ids.*' => 'intval',
940 'search' => 'sanitize_text_field',
941 ]);
942
943 $name = Arr::get($data, 'name', '');
944 if (empty($name)) {
945 $name = Arr::get($data, 'search', '');
946 }
947 $ids = Arr::get($data, 'ids', []);
948 $productVariations = [];
949 $query = [];
950 if (!empty($name) || count($ids) > 0) {
951 $query = [
952 "ID" =>
953 [
954 "column" => "ID",
955 "operator" => "in",
956 "value" => Arr::get($data, 'ids', [])
957 ]
958 ,
959 "post_title" =>
960 [
961 "column" => "post_title",
962 "operator" => "like",
963 "value" => '%' . Arr::get($data, 'name') . '%'
964 ],
965 "post_status" =>
966 [
967 "column" => "post_status",
968 "operator" => "=",
969 "value" => 'publish'
970 ]
971 ];
972 }
973
974 $products = Product::query()
975 ->with('variants')
976 ->when(count($query), function (Builder $q) use ($query) {
977 return $q->search($query, function (Builder $query) {
978 return $query;
979 }, true);
980 })
981 ->when(empty($name), function (Builder $q) {
982 return $q->limit(10);
983 })
984 ->limit(20)->get()
985 ->map(function ($product) {
986 return [
987 'value' => $product->ID,
988 'label' => $product->post_title,
989 'children' => $product->variants->map(function ($variation) use ($product) {
990 return [
991 'value' => $variation->id,
992 // 'label' => $product->post_title . ' - ' . $variation->variation_title,
993 'label' => $variation->variation_title,
994 ];
995 })
996 ->toArray()
997 ];
998 })->toArray();
999 return $products;
1000 }
1001
1002 public function searchProductVariantOptions(Request $request): array
1003 {
1004 $data = $request->getSafe([
1005 'include_ids.*' => 'intval',
1006 'search' => 'sanitize_text_field',
1007 'scopes.*' => 'sanitize_text_field',
1008 'subscription_status' => 'sanitize_text_field',
1009 ]);
1010
1011 $subscription_status = Arr::get($data, 'subscription_status');
1012 $search = Arr::get($data, 'search', '');
1013 $includeIds = Arr::get($data, 'include_ids', []);
1014
1015 $productsQuery = Product::query()
1016 ->whereIn('post_status', ['publish', 'private']);
1017
1018 $productsQuery->with(['detail', 'variants' => function ($query) use ($subscription_status) {
1019 if ($subscription_status === 'not_subscribable') {
1020 $query->where('payment_type', '!=', 'subscription');
1021 }
1022 }]);
1023
1024 $scopes = Arr::get($data, 'scopes', []);
1025 if ($scopes) {
1026 $productsQuery = $productsQuery->scopes($scopes);
1027 }
1028
1029 if ($search) {
1030 $productsQuery->where(function ($query) use ($search, $subscription_status) {
1031 $query->where('post_title', 'like', '%' . $search . '%')
1032 ->orWhereHas('variants', function ($query) use ($search, $subscription_status) {
1033 $query->where('variation_title', 'like', "%$search%");
1034 if ($subscription_status === 'not_subscribable') {
1035 $query->where('payment_type', '!=', 'subscription');
1036 }
1037 });
1038 });
1039 }
1040
1041 $productsQuery->limit(20);
1042
1043 $products = $productsQuery->get();
1044
1045 $pushedVariationIds = [];
1046 $formattedProducts = [];
1047
1048 foreach ($products as $product) {
1049 $detail = $product->detail;
1050 if ($detail && $detail->manage_stock && $detail->stock_availability !== Helper::IN_STOCK) {
1051 continue;
1052 }
1053
1054 $formatted = [
1055 'value' => 'product_' . $product->ID,
1056 'label' => $product->post_title,
1057 ];
1058
1059 $variants = $product->variants;
1060
1061 $children = [];
1062 foreach ($variants as $variant) {
1063 if ($variant->manage_stock && $variant->stock_status !== Helper::IN_STOCK) {
1064 continue;
1065 }
1066 $pushedVariationIds[] = $variant->id;
1067 $children[] = [
1068 'value' => $variant->id,
1069 'label' => $variant->variation_title,
1070 ];
1071 }
1072
1073 if (!$children) {
1074 continue;
1075 }
1076
1077 $formatted['children'] = $children;
1078 $formattedProducts[$product->ID] = $formatted;
1079 }
1080
1081 $leftVariationIds = array_diff($includeIds, $pushedVariationIds);
1082
1083 if ($leftVariationIds) {
1084 $leftVariants = ProductVariation::query()
1085 ->whereIn('id', $leftVariationIds)
1086 ->with(['product' => function ($query) {
1087 $query->whereIn('post_status', ['publish', 'private']);
1088 }, 'product.detail'])
1089 ->get();
1090
1091 foreach ($leftVariants as $variant) {
1092 if ($subscription_status == 'not_subscribable' && $variant->payment_type === 'subscription') {
1093 continue;
1094 }
1095 if ($variant->manage_stock && $variant->stock_status !== Helper::IN_STOCK) {
1096 continue;
1097 }
1098 $product = $variant->product;
1099 if (!$product) {
1100 continue;
1101 }
1102 $detail = $product->detail;
1103 if ($detail && $detail->manage_stock && $detail->stock_availability !== Helper::IN_STOCK) {
1104 continue;
1105 }
1106 if (isset($formattedProducts[$product->ID])) {
1107 $formattedProducts[$product->ID]['children'][] = [
1108 'value' => $variant->id,
1109 'label' => $variant->variation_title,
1110 ];
1111 } else {
1112 $formattedProducts[$product->ID] = [
1113 'value' => 'product_' . $product->ID,
1114 'label' => $product->post_title,
1115 'children' => [
1116 [
1117 'value' => $variant->id,
1118 'label' => $variant->variation_title,
1119 ]
1120 ]
1121 ];
1122 }
1123 }
1124 }
1125
1126 $products = array_values($formattedProducts);
1127
1128 // sort the products by label
1129 usort($products, function ($a, $b) {
1130 return strcmp($a['label'], $b['label']);
1131 });
1132
1133 return [
1134 'products' => $products
1135 ];
1136 }
1137
1138 public function findSubscriptionVariants(Request $request)
1139 {
1140 $data = $request->getSafe([
1141 'name' => 'sanitize_text_field',
1142 ]);
1143
1144 $search = Arr::get($data, 'name', '');
1145
1146
1147 if (!empty($search)) {
1148 $query['variation_title'] = [
1149 'column' => 'variation_title',
1150 'operator' => 'like',
1151 'value' => '%' . $search . '%',
1152 ];
1153 }
1154
1155 $variants = ProductVariation::query()
1156 ->when(!empty($search), function (Builder $query) use ($search) {
1157 $query->where('variation_title', 'like', '%' . $search . '%');
1158 })
1159 ->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(other_info, '$.payment_type')) = ?", ['subscription'])
1160 ->get()
1161 ->map(function ($variation) {
1162 return [
1163 'id' => $variation->id,
1164 'title' => $variation->variation_title,
1165 ];
1166 })
1167 ->toArray();
1168
1169 return $variants;
1170
1171
1172 }
1173
1174
1175 public function fetchVariationsByIds(Request $request): array
1176 {
1177 $ids = $request->getSafe(['productIds.*' => 'intval']);
1178 $ids = is_array($ids) ? $ids : [];
1179 if (empty($ids)) {
1180 return ['products' => []];
1181 }
1182 $products = ProductVariationResource::search(["id" => ["column" => "id", "operator" => "in", "value" => is_array($ids) ? $ids : [],]], function (Builder $query) {
1183 return $query;
1184 }, true)->pluck('variation_title', 'id')->map(function ($name, $id) {
1185 return [
1186 'value' => $id,
1187 'label' => $name,
1188 ];
1189 })->values()->toArray();
1190 return ['products' => $products];
1191 }
1192
1193 public function searchProductByName(Request $request): array
1194 {
1195 $searchValue = $request->getSafe('name', 'sanitize_text_field');
1196 $urlMode = $request->getSafe('url_mode', 'sanitize_text_field');
1197 $termId = $request->getSafe('termId', 'intval');
1198
1199 $defaultFilters =
1200 [
1201 "wildcard" => $searchValue,
1202 ];
1203
1204 $status = ["post_status" => ["column" => "post_status", "operator" => "in", "value" => ["publish"]]];
1205
1206 $params = [
1207 "select" => ['id AS ID', 'post_title'],
1208 "with" => ['wpTerms'],
1209 "selected_status" => true,
1210 "status" => $status,
1211 "default_filters" => $defaultFilters,
1212 ];
1213
1214 if (!empty($termId)) {
1215 $params["taxonomy_filters"] = [
1216 'product-categories' => Arr::wrap($termId)
1217 ];
1218 }
1219
1220 $products = ShopResource::get($params);
1221 $items = $products['products'];
1222
1223 return [
1224 'products' => $items
1225 ];
1226 }
1227
1228 public function getBundleInfo(Request $request, $productId): array
1229 {
1230 $variants = ProductVariation::query()
1231 ->where('post_id', $productId)
1232 ->select([
1233 'id',
1234 'variation_title',
1235 'other_info'
1236 ])
1237 ->get()
1238 ->toArray();
1239
1240 $variants = Helper::loadBundleChild($variants);
1241
1242 return $variants;
1243 }
1244
1245 public function saveBundleInfo(Request $request, $variationId): array
1246 {
1247 $variation = ProductVariation::query()->findOrFail($variationId);
1248
1249 $childIds = $request->get('bundle_child_ids');
1250
1251 if (!empty($childIds) && is_array($childIds)) {
1252 // Reject any child variations that belong to a bundle product
1253 $bundleChildVariations = ProductVariation::whereIn('id', $childIds)
1254 ->get(['id', 'post_id']);
1255
1256 foreach ($bundleChildVariations as $childVariation) {
1257 if ($childVariation->product && $childVariation->product->isBundleProduct()) {
1258 return wp_send_json([
1259 'message' => __('A bundle product cannot be added as a bundle child.', 'fluent-cart'),
1260 ], 422);
1261 }
1262 }
1263 }
1264
1265 $otherInfo = $variation->other_info ?? [];
1266 $otherInfo['bundle_child_ids'] = $childIds;
1267 $variation->other_info = $otherInfo;
1268
1269 return [$variation->update()];
1270 }
1271
1272 public function fetchProductsByIds(Request $request): array
1273 {
1274 $ids = $request->getSafe(['productIds.*' => 'intval']);
1275
1276 $ids = Arr::get($ids, 'productIds', []);
1277 $ids = is_array($ids) ? $ids : [];
1278
1279 if (empty($ids)) {
1280 return [
1281 'products' => []
1282 ];
1283 }
1284 $products = ProductResource::search([
1285 "id" => [
1286 "column" => "id",
1287 "operator" => "in",
1288 "value" => is_array($ids) ? $ids : [],
1289 ]
1290 ], function (Builder $query) {
1291 return $query
1292 ->with('detail');
1293 }, true);
1294
1295 return [
1296 'products' => $products
1297 ];
1298 }
1299
1300 public function getMaxExcerptWordCount(): WP_REST_Response
1301 {
1302 return $this->sendSuccess([
1303 'count' => (int)apply_filters('excerpt_length', 55)
1304 ]);
1305 }
1306
1307 public function createDummyProducts(Request $request)
1308 {
1309 return DummyProductService::create($request->get('category'), $request->get('index'));
1310 }
1311
1312 public function updateInventory(Request $request, $postId, $variantId)
1313 {
1314
1315 $variant = ProductVariation::query()->find($variantId);
1316
1317 if (!$variant) {
1318 return $this->response->sendError([
1319 'message' => __('Variant not found', 'fluent-cart')
1320 ]);
1321 }
1322
1323 $detail = ProductDetail::query()->where('post_id', $postId)->first();
1324
1325 // get variations by post_id
1326 $variations = ProductVariation::query()->where('post_id', $postId)->where('id', '!=', $variantId)->get();
1327 $updateData = [];
1328 foreach ($variations as $variation) {
1329 $updateData[] = [
1330 'id' => $variation->id,
1331 'manage_stock' => 1,
1332 'total_stock' => $variation->total_stock,
1333 'available' => $variation->available,
1334 'stock_status' => $variation->stock_status
1335 ];
1336 }
1337 $updateData[] = [
1338 'id' => $variantId,
1339 'total_stock' => sanitize_text_field($request->get('total_stock')),
1340 'available' => sanitize_text_field($request->get('available')),
1341 'manage_stock' => 1,
1342 'stock_status' => $request->get('available') > 0 ? 'in-stock' : 'out-of-stock'
1343 ];
1344 // update variations
1345 $isUpdated = ProductVariation::query()->batchUpdate($updateData);
1346
1347
1348 if ($detail) {
1349 $hasAvailableStock = ProductVariation::query()->where('post_id', $postId)->where('available', '>', 0)->exists();
1350 $detail->stock_availability = $hasAvailableStock ? 'in-stock' : 'out-of-stock';
1351 $detail->manage_stock = 1;
1352 $detail->save();
1353 }
1354
1355
1356 if (is_wp_error($isUpdated)) {
1357 return $this->response->sendError([
1358 'message' => __('Inventory update failed', 'fluent-cart')
1359 ]);
1360 }
1361
1362 return $this->response->sendSuccess([
1363 'message' => __('Inventory updated successfully', 'fluent-cart')
1364 ]);
1365 }
1366
1367 public function updateManageStock(Request $request, $postId)
1368 {
1369 $manageStock = sanitize_text_field($request->get('manage_stock'));
1370
1371 $detail = ProductDetail::query()->where('post_id', $postId)->first();
1372
1373 $updateData = [
1374 'manage_stock' => $manageStock,
1375 ];
1376 if ($manageStock == 0) {
1377 $updateData['stock_status'] = 'in-stock';
1378 }
1379
1380 $updatedVariations = ProductVariation::query()->where('post_id', $postId)->update($updateData);
1381
1382 $hasAvailableStock = ProductVariation::query()->where('post_id', $postId)->where('available', '>', 0)->exists();
1383 $detail->manage_stock = $manageStock;
1384 $detail->stock_availability = $hasAvailableStock || $manageStock == 0 ? 'in-stock' : 'out-of-stock';
1385 $updatedProductDetails = $detail->save();
1386
1387 if (is_wp_error($updatedProductDetails)) {
1388 return $this->response->sendError([
1389 'message' => __('Manage stock update failed', 'fluent-cart')
1390 ]);
1391 }
1392
1393 if (is_wp_error($updatedVariations)) {
1394 return $this->response->sendError([
1395 'message' => __('Manage stock update failed', 'fluent-cart')
1396 ]);
1397 }
1398
1399 return $this->response->sendSuccess([
1400 'message' => __('Manage stock updated successfully', 'fluent-cart')
1401 ]);
1402 }
1403
1404 /**
1405 * Suggest a unique SKU based on product title and optional variant title.
1406 *
1407 * @param Request $request
1408 * @return WP_REST_Response
1409 */
1410 public function suggestSku(Request $request)
1411 {
1412 $title = sanitize_text_field($request->get('title', ''));
1413 $variantTitle = sanitize_text_field($request->get('variant_title', ''));
1414 $excludeId = absint($request->get('exclude_id', 0));
1415
1416 if (empty($title)) {
1417 return $this->sendError([
1418 'message' => __('Product title is required to generate SKU.', 'fluent-cart')
1419 ]);
1420 }
1421
1422 $sku = $this->generateSkuFromTitle($title, $variantTitle);
1423
1424 if (empty($sku)) {
1425 return $this->sendError([
1426 'message' => __('Could not generate SKU from the given title.', 'fluent-cart')
1427 ]);
1428 }
1429
1430 $sku = $this->ensureUniqueSku($sku, $excludeId);
1431
1432 return $this->sendSuccess([
1433 'sku' => $sku,
1434 ]);
1435 }
1436
1437 /**
1438 * Generate a SKU string from a product title and optional variant title.
1439 *
1440 * @param string $title
1441 * @param string $variantTitle
1442 * @return string
1443 */
1444 private function generateSkuFromTitle($title, $variantTitle = '')
1445 {
1446 $stopWords = ['the', 'and', 'for', 'with', 'a', 'an', 'of', 'in', 'on', 'to', 'is', 'it', 'by', 'or', 'at', 'from'];
1447
1448 $fullTitle = trim($title);
1449 if (!empty($variantTitle) && strtolower(trim($variantTitle)) !== strtolower(trim($title))) {
1450 $fullTitle .= ' ' . trim($variantTitle);
1451 }
1452
1453 $cleaned = strtoupper($fullTitle);
1454 $cleaned = preg_replace('/[^A-Z0-9\s]/', '', $cleaned);
1455 $words = array_values(array_filter(explode(' ', $cleaned), function ($word) use ($stopWords) {
1456 return strlen($word) > 0 && !in_array(strtolower($word), $stopWords);
1457 }));
1458
1459 if (empty($words)) {
1460 return '';
1461 }
1462
1463 $parts = array_map(function ($word) {
1464 return substr($word, 0, 3);
1465 }, $words);
1466
1467 $base = implode('-', $parts);
1468
1469 // Keep base within 25 chars to leave room for uniqueness suffix
1470 if (strlen($base) > 25) {
1471 $base = substr($base, 0, 25);
1472 $base = rtrim($base, '-');
1473 }
1474
1475 return $base;
1476 }
1477
1478 /**
1479 * Ensure the SKU is unique in the database, appending a numeric suffix if needed.
1480 *
1481 * @param string $sku
1482 * @param int|null $excludeId
1483 * @return string
1484 */
1485 private function ensureUniqueSku($sku, $excludeId = null)
1486 {
1487 $original = $sku;
1488 $suffix = 0;
1489 $batchSize = 10;
1490
1491 while ($suffix < 100) {
1492 // Build a batch of candidate SKUs to check at once
1493 $candidates = [];
1494 for ($i = $suffix; $i < $suffix + $batchSize && $i < 100; $i++) {
1495 if ($i === 0) {
1496 $candidates[] = $original;
1497 } else {
1498 $maxBaseLen = 30 - strlen('-' . $i);
1499 $candidates[] = substr($original, 0, $maxBaseLen) . '-' . $i;
1500 }
1501 }
1502
1503 $query = ProductVariation::query()->whereIn('sku', $candidates);
1504 if ($excludeId) {
1505 $query->where('id', '!=', $excludeId);
1506 }
1507 $taken = $query->get()->pluck('sku')->toArray();
1508
1509 // Return the first candidate that isn't taken
1510 foreach ($candidates as $candidate) {
1511 if (!in_array($candidate, $taken)) {
1512 return $candidate;
1513 }
1514 }
1515
1516 $suffix += $batchSize;
1517 }
1518
1519 // All candidates exhausted — fallback to timestamp-based suffix
1520 $maxBaseLen = 30 - 1 - 10; // hyphen + 10-digit timestamp
1521 return substr($original, 0, $maxBaseLen) . '-' . substr(time(), -10);
1522 }
1523
1524 /**
1525 * Fetch products formatted for bulk editing.
1526 */
1527 public function bulkEditFetch(Request $request): WP_REST_Response
1528 {
1529 try {
1530 $service = new BulkProductUpdateService();
1531 $result = $service->fetchForBulkEdit($request);
1532
1533 return $this->sendSuccess($result);
1534 } catch (\Throwable $e) {
1535 return $this->sendError([
1536 'message' => __('Failed to fetch products: ', 'fluent-cart') . $e->getMessage(),
1537 ]);
1538 }
1539 }
1540
1541 /**
1542 * Bulk update products from the bulk edit spreadsheet.
1543 */
1544 public function bulkUpdate(Request $request): WP_REST_Response
1545 {
1546 $products = $request->get('products', []);
1547
1548 if (empty($products) || !is_array($products)) {
1549 return $this->sendError([
1550 'message' => __('No products provided', 'fluent-cart'),
1551 ]);
1552 }
1553
1554 if (count($products) > 10) {
1555 return $this->sendError([
1556 'message' => __('Maximum 10 products per chunk allowed', 'fluent-cart'),
1557 ]);
1558 }
1559
1560 try {
1561 $service = new BulkProductUpdateService();
1562 $result = $service->updateChunk($products);
1563
1564 if (empty($result['updated']) && !empty($result['errors'])) {
1565 return $this->sendError([
1566 'message' => __('All products failed to update', 'fluent-cart'),
1567 'errors' => $result['errors'],
1568 ]);
1569 }
1570
1571 return $this->sendSuccess([
1572 'message' => sprintf(
1573 __('%d product(s) updated successfully', 'fluent-cart'),
1574 count($result['updated'])
1575 ),
1576 'updated' => $result['updated'],
1577 'errors' => $result['errors'],
1578 ]);
1579 } catch (\Throwable $e) {
1580 return $this->sendError([
1581 'message' => __('Bulk update failed: ', 'fluent-cart') . $e->getMessage(),
1582 ]);
1583 }
1584 }
1585 }
1586