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 / api / Resource / ProductResource.php

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

799 lines 30.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\Api\Resource;
4
5 use FluentCart\Api\Taxonomy;
6 use FluentCart\App\CPT\FluentProducts;
7 use FluentCart\App\Events\StockChanged;
8 use FluentCart\App\Helpers\Helper;
9 use FluentCart\App\Helpers\ProductAdminHelper;
10 use FluentCart\App\Helpers\Status;
11 use FluentCart\App\Models\Product;
12 use FluentCart\App\Models\ProductDetail;
13 use FluentCart\App\Models\ProductMeta;
14 use FluentCart\App\Models\ProductVariation;
15 use FluentCart\App\Services\DateTime\DateTime;
16 use FluentCart\Framework\Database\Orm\Builder;
17 use FluentCart\Framework\Support\Arr;
18 use FluentCart\Framework\Support\Collection;
19
20 class ProductResource extends BaseResourceApi
21 {
22
23 public static function getQuery(): Builder
24 {
25 return Product::query();
26 }
27
28 public static function get(array $params = []): array
29 {
30 return [];
31 }
32
33 public static function getProducttitle($productId)
34 {
35 $product = static::getQuery()->find($productId);
36 if ($product) {
37 return $product->post_title;
38 }
39 return '';
40 }
41
42
43 /**
44 * Find product by its ID.
45 *
46 * @param int|string $id The ID of the post.
47 * @param array $data Additional data for finding product (optional).
48 *
49 */
50 public static function find($id, $data = []): ?array
51 {
52 return ShopResource::find($id, $data = []);
53 }
54
55 /**
56 * Create a new product with the given data.
57 *
58 * @param array $data Array containing the necessary parameters.
59 *
60 * $data = [
61 * 'post_title' => (string) Required. The title of the product.
62 * 'post_status' => (string) Optional. The status of the product default:draft.
63 * 'post_content' => (string) Optional. The content of the product.
64 * 'post_date' => (date) Optional. The date of the product.
65 * 'detail' => (array) Required. Details of the product.
66 * 'fulfillment_type' => (string) Required. The fulfillment type default:physical.
67 * 'variation_type' => (string) Required. The variation type default:simple.
68 * 'manage_stock' => (int) Required. The manage stock default:1.
69 * ];
70 */
71 public static function create($data, $params = [])
72 {
73 $postData = array_filter(Arr::only($data, [
74 'post_title',
75 'post_excerpt',
76 'post_content',
77 'post_status',
78 ]));
79 $postData['post_type'] = FluentProducts::CPT_NAME;
80
81 $createdPostId = wp_insert_post($postData);
82 if (!$createdPostId) {
83 return static::makeErrorResponse([
84 [
85 'code' => 403,
86 'message' => esc_html($createdPostId->get_error_message()),
87 ]
88 ]);
89
90 }
91
92 $detail = Arr::get($data, 'detail');
93 $detail['post_id'] = $createdPostId;
94 $createdProductDetail = ProductDetailResource::create($detail);
95
96 if ($createdProductDetail) {
97 return static::makeSuccessResponse(
98 [
99 'ID' => $createdPostId,
100 'product_details' => Arr::get($createdProductDetail, 'data'),
101 ],
102 __('Product has been created successfully', 'fluent-cart')
103 );
104 }
105
106 return static::makeErrorResponse([
107 ['code' => 400, 'message' => __('Product creation failed!', 'fluent-cart')]
108 ]);
109 }
110
111 /**
112 * Update a product with the given data.
113 *
114 * @param array $product Array containing the necessary parameters.
115 *
116 * $product = [
117 * 'ID' => (int) Required. The product ID.
118 * 'post_title' => (string) Required. The title of the product.
119 * 'post_status' => (string) Optional. The status of the product.
120 * 'post_content' => (string) Optional. The content of the product.
121 * 'post_date' => (string) Optional. The date of the product.
122 * 'detail' => (array) Required. Details of the product.
123 * 'id' => (int) Required. The detail ID.
124 * 'post_id' => (int) Required. The product ID.
125 * 'fulfillment_type' => (string) Required. The fulfillment type.
126 * 'variation_type' => (string) Required. The variation type.
127 * 'default_variation_id' => (int) Required. The default variation ID.
128 * 'variants' => (array) Required. Variants of the product.
129 * 'id' => (int) Required. The variant ID.
130 * 'post_id' => (int) Required. The product ID.
131 * 'variant_title' => (string) Required. The variant title.
132 * 'item_price' => (float) Required. The item price.
133 * 'compare_price' => (float) Required. The compare price.
134 * 'manage_cost' => (string) Optional. Whether to manage costs.
135 * 'item_cost' => (float) Required if manage cost is yes. The item cost.
136 * 'manage_stock' => (string) Required. Whether to manage stock.
137 * 'stock_status' => (string) Required. The stock status.
138 * 'stock' => (int) Required. The stock quantity.
139 * 'media' => (array) Optional. Info of media files for each variant.
140 * 'id' => (string) Required if upload any media. The media ID.
141 * 'url' => (string) Required if upload any media. The media URL.
142 * 'title' => (string) Required if upload any media. The media title.
143 * 'other_info' => (array) Optional. Other information for the variant.
144 * 'payment_type' => (string) Required. The payment type.
145 * 'times' => (string) Required. The number of times.
146 * 'repeat_interval' => (string) Required. The repeat interval unit.
147 * 'signup_fee' => (string) Required. The signup fee.
148 * 'downloadable_files' => (array) Required if downloadable is true.
149 * 'download_limit' => (string) Required. The download limit.
150 * 'download_expiry' => (string) Required. The download expiry.
151 * 'downloadable' => (bool) Optional. Whether the product is downloadable.
152 * 'files' => (array) Optional. Info of downloadable files for each variant.
153 * 'title' => (string) Required if files. The file title.
154 * 'type' => (string) Required if files. The file type.
155 * 'file_name' => (string) Required if files. The file name.
156 * 'file_path' => (string) Required if files. The file path.
157 * 'file_url' => (string) Required if files. The file URL.
158 * 'serial' => (string) Required if files. The file serial
159 * 'product_terms' => (array) Optional. Terms of the product.
160 * 'product-categories' => (array) Required if categories. Product categories.
161 * [0] => (int) Optional. The category ID.
162 * 'product-brands' => (array) Required if brands. Product brands.
163 * [0] => (int) Optional. The tag ID.
164 * ];
165 */
166 public static function update($product, $postId = '', $params = [])
167 {
168
169 $product ??= [];
170 $variants = Arr::get($product, 'variants', []);
171 $detail = Arr::get($product, 'detail');
172 $gallery = Arr::get($product, 'gallery', []);
173 $variants = Arr::except($variants, ['*']);
174
175
176 if (count($variants) > 0) {
177
178 $variationType = Arr::get($detail, 'variation_type', 'simple');
179 if ($variationType === 'simple') {
180 $variant = $variants[0];
181 $otherInfo = Arr::get($variant, 'other_info', []);
182
183 $priceColumns = [
184 'item_price',
185 'compare_price',
186 'item_cost',
187 ];
188
189 foreach ($priceColumns as $column) {
190 if (Arr::has($variant, $column)) {
191 $variant[$column] = Arr::get($variant, $column) * 100;
192 }
193 }
194
195 unset($variant['rowId']);
196 unset($variant['media']);
197
198 // Recalculate stock_status and sync total_stock from available
199 if (isset($variant['manage_stock'])) {
200 if ($variant['manage_stock']) {
201 $avail = intval(Arr::get($variant, 'available', 0));
202 $variant['stock_status'] = $avail > 0 ? Helper::IN_STOCK : Helper::OUT_OF_STOCK;
203 $variant['total_stock'] = $avail;
204 } else {
205 $variant['stock_status'] = Helper::IN_STOCK;
206 }
207 }
208
209 $variantData = $variant;
210
211 // Remove empty sku and shipping_class to prevent unique constraint violation
212 if (array_key_exists('sku', $variantData) && empty($variantData['sku'])) {
213 unset($variantData['sku']);
214 }
215 if (array_key_exists('shipping_class', $variantData) && empty($variantData['shipping_class'])) {
216 unset($variantData['shipping_class']);
217 }
218
219 // Handle other_info
220 if (!empty($otherInfo)) {
221 if (Arr::get($otherInfo, 'payment_type') == 'subscription') {
222 if (Arr::get($otherInfo, 'manage_setup_fee') == 'yes') {
223 $signupFee = Helper::toCent(floatval(Arr::get($otherInfo, 'signup_fee', 0)));
224 Arr::set($otherInfo, 'signup_fee', $signupFee);
225 }
226 $variantData['payment_type'] = 'subscription';
227 } else {
228 $variantData['payment_type'] = 'onetime';
229 }
230 $variantData['other_info'] = $otherInfo;
231 }
232
233
234 // Only update if there's data to update
235 if (!empty($variantData)) {
236 ProductVariation::query()->where('id', Arr::get($variant, 'id'))->update($variantData);
237 }
238
239 } else {
240 $variantData = [];
241
242 foreach ($variants as $index => $variant) {
243 $otherInfo = Arr::get($variant, 'other_info', []);
244
245 $priceColumns = [
246 'item_price',
247 'compare_price',
248 'item_cost',
249 ];
250
251 foreach ($priceColumns as $column) {
252 if (Arr::has($variant, $column)) {
253 $variant[$column] = Arr::get($variant, $column) * 100;
254 }
255 }
256 unset($variant['rowId']);
257 $variant['serial_index'] = $index + 1;
258
259 // Recalculate stock_status from available and manage_stock
260 if (isset($variant['manage_stock'])) {
261 if ($variant['manage_stock']) {
262 $avail = intval(Arr::get($variant, 'available', 0));
263 $variant['stock_status'] = $avail > 0 ? Helper::IN_STOCK : Helper::OUT_OF_STOCK;
264 $variant['total_stock'] = $avail;
265 } else {
266 $variant['stock_status'] = Helper::IN_STOCK;
267 }
268 }
269
270 if (!empty($otherInfo)) {
271 if (Arr::get($otherInfo, 'payment_type') == 'subscription') {
272 if (Arr::get($otherInfo, 'manage_setup_fee') == 'yes') {
273 $signupFee = Helper::toCent(floatval(Arr::get($otherInfo, 'signup_fee', 0)));
274 Arr::set($otherInfo, 'signup_fee', $signupFee);
275 }
276 }
277 $variant['other_info'] = $otherInfo;
278 }
279 $variantData[] = $variant;
280
281 }
282
283 // Only batch update if there's data
284 if (!empty($variantData)) {
285 ProductVariation::query()->batchUpdate($variantData);
286 }
287 }
288
289
290 // $variationDetails = $detail;
291 // $variants = ProductAdminHelper::syncProduct($variationDetails, $variants);
292 }
293
294 $defaultVariationId = Arr::get($detail, 'default_variation_id');
295 $detail['default_variation_id'] = $defaultVariationId;
296
297 // Recalculate min_price / max_price from current variant prices
298 $variantPriceRange = ProductVariation::query()
299 ->where('post_id', $postId)
300 ->selectRaw('MIN(item_price) as min_price, MAX(item_price) as max_price')
301 ->first();
302
303 if ($variantPriceRange) {
304 $detail['min_price'] = $variantPriceRange->min_price ?: 0;
305 $detail['max_price'] = $variantPriceRange->max_price ?: 0;
306 }
307
308 ProductDetailResource::update($detail, Arr::get($detail, 'id'), ['action' => 'variant_modified']);
309
310 (new StockChanged([$postId]))->dispatch();
311
312 static::updateWpPost($postId, $product);
313 if (Arr::has($product, 'gallery')) {
314 update_post_meta($postId, FluentProducts::CPT_NAME . '-gallery-image', $gallery);
315
316 if (isset($gallery[0])) {
317 set_post_thumbnail($postId, Arr::get($gallery, '0.id'));
318 $thumbnailImageId = get_post_meta($postId, '_thumbnail_id', true);
319 $thumbnail = wp_prepare_attachment_for_js($thumbnailImageId);
320 $thumbUrl = Arr::get($thumbnail, 'url');
321 if (!empty($thumbUrl) && Arr::get($gallery, '0.id') !== $thumbnailImageId) {
322 update_post_meta($postId, '_thumbnail_id', $thumbnailImageId);
323 }
324 } else {
325 delete_post_thumbnail($postId);
326 }
327 }
328
329 $product = static::getQuery()->with('variants')->addAppends([
330 'viewUrl'
331 ])->find($postId);
332
333
334 return static::makeSuccessResponse(
335 $product,
336 __('Product has been updated', 'fluent-cart')
337 );
338 }
339
340 public static function updateWpPost($postId, $params = [])
341 {
342
343 $postStatus = Arr::get($params, 'post_status');
344 $postTitle = Arr::get($params, 'post_title');
345 $postContent = Arr::get($params, 'post_content');
346 $postExcerpt = Arr::get($params, 'post_excerpt');
347 $commentStatus = Arr::get($params, 'comment_status');
348 $postName = Arr::get($params, 'post_name');
349 $postDate = Arr::get($params, 'post_date');
350 if (empty($postDate) || $postStatus !== 'future') {
351 $postDate = DateTime::gmtNow()->format('Y-m-d H:i:s');
352 }
353
354 if ($postStatus === 'future') {
355 $postDate = DateTime::anyTimeToGmt($postDate)->format('Y-m-d H:i:s');
356 }
357
358 $data = [
359 'ID' => $postId,
360 'post_title' => $postTitle,
361 'post_status' => $postStatus,
362 'comment_status' => $commentStatus,
363 'post_name' => $postName,
364 ];
365
366 if (isset($postExcerpt)) {
367 $data['post_excerpt'] = $postExcerpt;
368 }
369
370 $activeEditor = Arr::get($params, 'detail.other_info.active_editor', 'wp-editor');
371 if (empty($activeEditor)) {
372 $activeEditor = 'wp-editor';
373 }
374 if (isset($postContent)) {
375 $data['post_content'] = $postContent;
376 }
377 if (!empty($postDate)) {
378 $data['post_date'] = $postDate;
379 $data['post_date_gmt'] = $postDate;
380 $data['post_modified'] = $postDate;
381 $data['post_modified_gmt'] = $postDate;
382 }
383
384 $updated = wp_update_post($data);
385
386 if ($updated) {
387 Product::query()->where('ID', $postId)->update([
388 'post_status' => $postStatus,
389 'post_date' => $postDate,
390 'post_date_gmt' => $postDate,
391 'post_modified' => $postDate,
392 'post_modified_gmt' => $postDate,
393 ]);
394 }
395
396 return $updated;
397 }
398
399 /**
400 * Delete a product and its associated data.
401 *
402 * @param int $postId The ID of the product to be deleted.
403 * @param array $params Additional parameters for the deletion process.
404 *
405 */
406 public static function delete($postId, $params = [])
407 {
408
409
410 $product = static::getQuery()
411 ->with('variants')
412 ->with('orderItems', function ($query) use ($postId) {
413 return $query->whereHas('order', function ($query) {
414 return $query->search(["status" => ["column" => "status", "operator" => "in", "value" => [Status::ORDER_ON_HOLD, Status::ORDER_PROCESSING]]]);
415 });
416 })
417 ->find($postId);
418
419
420 if (!empty($product)) {
421 if (count($product->orderItems) > 0) {
422 return static::makeErrorResponse([
423 ['code' => 400, 'message' => __('This product cannot be deleted at the moment. There are pending orders associated with it. Deleting the product will disrupt the order processing and might cause inconvenience to our customers.', 'fluent-cart')]
424 ]);
425 }
426 foreach ($product->variants as $variant) {
427 $variant->media()->delete();
428 }
429 $product->detail()->delete();
430 $product->variants()->delete();
431 $product->licensesMeta()->delete();
432 $product->downloadable_files()->delete();
433
434 $taxonomies = Taxonomy::getTaxonomies();
435 Collection::make($taxonomies)
436 ->each(function ($taxonomy) use (&$product) {
437 $ids = Taxonomy::getTermIdsFromTerms($product->getTermByType($taxonomy)->get()->toArray());
438 foreach ($ids as $id) {
439 Taxonomy::deleteTaxonomyTermFromProduct($product->ID, $taxonomy, $id);
440 }
441 });
442 $product->wp_terms()->delete();
443
444 $productTitle = $product->post_title;
445
446
447 $deletedProduct = $product->delete();
448
449 if ($deletedProduct) {
450
451 fluent_cart_success_log(
452 __('Product deleted', 'fluent-cart'),
453 sprintf(
454 /* translators: %s is the product title */
455 __('Product %s is deleted', 'fluent-cart'), $productTitle),
456 [
457 'module_name' => 'Product',
458 'module_id' => 0,
459 'module_type' => Product::class,
460 ]
461 );
462 return static::makeSuccessResponse(
463 '',
464 __('Selected product and associated data has been deleted', 'fluent-cart')
465 );
466 }
467 return static::makeErrorResponse([
468 ['code' => 400, 'message' => __('Product deletion failed!', 'fluent-cart')]
469 ]);
470 }
471
472 return static::makeErrorResponse([
473 ['code' => 404, 'message' => __('Product not found in database.', 'fluent-cart')]
474 ]);
475
476 }
477
478 /**
479 *
480 * @param $productId
481 * @param $data
482 * @return mixed
483 */
484 public static function syncVariantOption($productId, $data = [])
485 {
486 $srcPricing = ProductDetail::where('post_id', $productId)->first();
487 $settings = Arr::get($data, 'options');
488 $variationType = Arr::get($data, 'variation_type');
489
490 if (!empty($variationType) && $variationType === Helper::PRODUCT_TYPE_ADVANCE_VARIATION) {
491
492 $variants = ProductAdminHelper::syncProduct($srcPricing, $settings);
493
494 $srcPricing->fill([
495 'other_info' => $settings,
496 'variation_type' => Helper::PRODUCT_TYPE_ADVANCE_VARIATION,
497 ])->save();
498
499 return static::makeSuccessResponse(
500 $variants,
501 __('Variation combination updated!', 'fluent-cart')
502 );
503 }
504
505 return static::makeErrorResponse([
506 ['code' => 400, 'message' => __('Illegal data provided.', 'fluent-cart')]
507 ]);
508 }
509
510 /**
511 * Manage products based on the provided action and product IDs.
512 *
513 * @param array $params Optional. Array containing the necessary parameters
514 * [
515 * 'action' => (string) Required. The action to be performed on the selected products.
516 * (e.g., Possible values: 'delete_products')
517 * 'product_ids' => (array) Required. Product IDs whose action will be performed.
518 * ]
519 *
520 */
521 public static function manageBulkActions($params = [])
522 {
523 $action = Arr::get($params, 'action', '');
524 $productIds = Arr::get($params, 'product_ids', []);
525
526 $productIds = array_map(function ($id) {
527 return (int)$id;
528 }, $productIds);
529
530 $productIds = array_filter($productIds);
531
532 if (!$productIds) {
533 return static::makeErrorResponse([
534 ['code' => 403, 'message' => __('Products selection is required', 'fluent-cart')]
535 ]);
536 }
537
538 $products = Product::whereIn('ID', $productIds)->get();
539
540 if ($action == 'delete_products') {
541
542 $failedProductIds = [];
543 $deletedProductIds = [];
544
545 foreach ($products as $product) {
546 $isDeleted = static::delete($product->ID);
547
548 if (is_wp_error($isDeleted)) {
549 $failedProductIds[] = $product->ID;
550 } else {
551 $deletedProductIds[] = $product->ID;
552 }
553 }
554
555 if (count($failedProductIds) > 0) {
556 $failedProductIds = implode(' , ', $failedProductIds);
557 /* translators: %s: The product ID(s) that could not be deleted. */
558 return count($deletedProductIds) > 0
559 ? static::makeSuccessResponse(
560 '',
561 sprintf(
562 /* translators: %s: The product ID(s) that could not be deleted. */
563 esc_html__(
564 'The Product ID - %s cannot be deleted at the moment as there are pending orders associated with it. And remaining product and its associated data have been deleted.',
565 'fluent-cart'
566 ),
567 esc_html($failedProductIds)
568 )
569 )
570 : static::makeErrorResponse([
571 [
572 'code' => 400,
573 'message' => sprintf(
574 /* translators: %s: The product ID(s) that could not be deleted. */
575 esc_html__(
576 'The Product ID - %s cannot be deleted at the moment as there are pending orders associated with it.',
577 'fluent-cart'
578 ),
579 esc_html($failedProductIds)
580 ),
581 ],
582 ]);
583 }
584
585 if (count($deletedProductIds) > 0 && count($failedProductIds) < 1) {
586 return static::makeSuccessResponse('', __('Selected product and associated data have been deleted', 'fluent-cart'));
587 }
588 }
589
590 if ($action === 'duplicate_products') {
591 $importStockManagement = filter_var(
592 Arr::get($params, 'import_stock_management', false),
593 FILTER_VALIDATE_BOOLEAN
594 );
595 $importLicenseSettings = filter_var(
596 Arr::get($params, 'import_license_settings', false),
597 FILTER_VALIDATE_BOOLEAN
598 );
599 $importDownloadableFiles = filter_var(
600 Arr::get($params, 'import_downloadable_files', false),
601 FILTER_VALIDATE_BOOLEAN
602 );
603
604 $options = [
605 'import_stock_management' => $importStockManagement,
606 'import_license_settings' => $importLicenseSettings,
607 'import_downloadable_files' => $importDownloadableFiles,
608 ];
609
610 $failedProductIds = [];
611 $newProductIds = [];
612
613 foreach ($productIds as $productId) {
614 try {
615 $newProductIds[] = Product::duplicateProduct($productId, $options);
616 } catch (\Throwable $e) {
617 $failedProductIds[] = $productId;
618 }
619 }
620
621 if (count($failedProductIds) > 0) {
622 $failedProductIdsText = implode(' , ', $failedProductIds);
623 return count($newProductIds) > 0
624 ? static::makeSuccessResponse(
625 [
626 'new_product_ids' => $newProductIds
627 ],
628 sprintf(
629 esc_html__(
630 'Some products could not be duplicated (Product ID: %s). Remaining selected products have been duplicated as drafts.',
631 'fluent-cart'
632 ),
633 esc_html($failedProductIdsText)
634 )
635 )
636 : static::makeErrorResponse([
637 [
638 'code' => 400,
639 'message' => sprintf(
640 esc_html__(
641 'Selected products could not be duplicated (Product ID: %s).',
642 'fluent-cart'
643 ),
644 esc_html($failedProductIdsText)
645 ),
646 ],
647 ]);
648 }
649
650 return static::makeSuccessResponse(
651 [
652 'new_product_ids' => $newProductIds
653 ],
654 __('Selected products have been duplicated as drafts', 'fluent-cart')
655 );
656 }
657
658 return static::makeErrorResponse([
659 ['code' => 400, 'message' => __('Selected action is invalid', 'fluent-cart')]
660 ]);
661 }
662
663 public static function validateDownloadableFiles($data)
664 {
665 $downloadableFiles = Arr::except(Arr::get($data, 'downloadable_files', []), ['*']);
666 $variants = Arr::except(Arr::get($data, 'variants', []), ['*']);
667 $fulfilmentType = Arr::get($data, 'detail.fulfillment_type');
668 $errors = [];
669
670 if (!empty($downloadableFiles)) {
671 foreach ($variants as $index => $variant) {
672 $count = 0;
673 $id = Arr::get($variant, 'rowId', null);
674 foreach ($downloadableFiles as $idx => $downloadFile) {
675 $variantIds = Arr::get($downloadFile, 'product_variation_id', null);
676 if (empty($variantIds)) {
677 $errors['downloadable_files.' . $idx . '.product_variation_id'] = 'Please choose variant';
678 }
679 if ($fulfilmentType === 'digital' && is_array($variantIds) && in_array($id, $variantIds)) {
680 $count += 1;
681 }
682 if ($fulfilmentType === 'physical') {
683 if (Arr::get($variant, 'downloadable') == 'true' && is_array($variantIds) && in_array($id, $variantIds)) {
684 $count += 1;
685 }
686 if (Arr::get($variant, 'downloadable') == 'false') {
687 $count += 1;
688 }
689 }
690 }
691 if ($count == 0) {
692 $errors['variants.' . $index . '.downloadable'] = sprintf(
693 /* translators: %s is the variant title */
694 __('%s variant is downloadable without any downloadable file', 'fluent-cart'),
695 $variant['variation_title']
696
697 );
698
699 }
700 }
701 }
702 return $errors;
703 }
704
705 public static function getNextProduct($productId, $skipOutOfStock)
706 {
707
708 }
709
710 /**
711 * Count total no of products.
712 *
713 */
714 public static function countTotalProducts()
715 {
716 return static::getQuery()
717 ->whereHas('detail')
718 ->whereHas('variants')
719 ->count();
720 }
721
722 public static function syncTaxonomyTerms($data, $postId = '', $params = [])
723 {
724 $data ??= [];
725
726 if (count(Arr::get($data, 'terms', [])) == 0) {
727 $isSynced = Taxonomy::deleteAllTermRelationshipsFromProduct($postId, Arr::get($data, 'taxonomy'));
728 } else {
729 $isSynced = Taxonomy::syncTaxonomyTermsToProduct($postId, Arr::get($data, 'taxonomy'), Arr::get($data, 'terms', []));
730 }
731 if (!is_wp_error($isSynced)) {
732 return static::makeSuccessResponse(
733 $isSynced,
734 __("Product has been updated", 'fluent-cart')
735 );
736 }
737
738 return static::makeErrorResponse([
739 ['code' => 400, 'message' => __("Product update failed!", 'fluent-cart')]
740 ]);
741 }
742
743 public static function deleteTaxonomyTerms($data, $postId = '', $params = [])
744 {
745 $data ??= [];
746
747 $isDeleted = Taxonomy::deleteTaxonomyTermFromProduct(
748 $postId,
749 Arr::get($data, 'taxonomy'),
750 Arr::get($data, 'term', null)
751 );
752
753 if (!is_wp_error($isDeleted)) {
754 return static::makeSuccessResponse(
755 $isDeleted,
756 __("Product has been updated", 'fluent-cart')
757 );
758 }
759
760 return static::makeErrorResponse([
761 ['code' => 400, 'message' => __("Product update failed!", 'fluent-cart')]
762 ]);
763 }
764
765 public static function findByProductAndVariants(array $params = [])
766 {
767 $productId = Arr::get($params, 'product_id', null);
768 $variantIds = Arr::get($params, 'variant_ids', []);
769
770 if (empty($productId)) {
771 return null;
772 }
773
774 $product = static::getQuery()
775 ->where('ID', $productId)
776 ->with([
777 'postmeta',
778 'detail',
779 'variants' => function ($query) use ($variantIds) {
780 // Filter variants only if variant_ids provided
781 if (!empty($variantIds)) {
782 $query->whereIn('id', $variantIds);
783 }
784
785 $query->with('media')
786 ->orderBy('serial_index', 'ASC');
787 }
788 ])
789 ->first();
790
791 if (!$product) {
792 return null;
793 }
794
795 return $product;
796 }
797
798 }
799