PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.1
1.6.6 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 All 49 releases
fluent-cart / api / Resource / ProductResource.php

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

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