PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.4
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.5.4, at api/Resource/ProductResource.php

883 lines 34.5 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 partialUpdate(array $data, $postId)
356 {
357 $product = get_post($postId);
358
359 if (!$product || $product->post_type !== 'fluent-products') {
360 return new \WP_Error('not_found', __('Product not found', 'fluent-cart'));
361 }
362
363 $postData = ['ID' => (int) $postId];
364
365 $allowedFields = ['post_title', 'post_content', 'post_excerpt', 'post_status', 'post_date'];
366
367 foreach ($allowedFields as $field) {
368 if (\array_key_exists($field, $data)) {
369 $postData[$field] = $data[$field];
370 }
371 }
372
373 if (\count($postData) === 1) {
374 return new \WP_Error('no_fields', __('No valid fields provided for update', 'fluent-cart'));
375 }
376
377 $newStatus = $postData['post_status'] ?? null;
378 $syncOrmDates = false;
379
380 if ($newStatus === 'future') {
381 $postDate = DateTime::anyTimeToGmt($postData['post_date'])->format('Y-m-d H:i:s');
382 $postData['post_date'] = $postDate;
383 $postData['post_date_gmt'] = $postDate;
384 $syncOrmDates = true;
385 } elseif ($newStatus === 'publish' && $product->post_status === 'future') {
386 $now = DateTime::gmtNow()->format('Y-m-d H:i:s');
387 $postData['post_date'] = $now;
388 $postData['post_date_gmt'] = $now;
389 $syncOrmDates = true;
390 }
391
392 $updated = wp_update_post($postData, true);
393
394 if (is_wp_error($updated)) {
395 return $updated;
396 }
397
398 if ($syncOrmDates) {
399 Product::query()->where('ID', $postId)->update([
400 'post_status' => $newStatus,
401 'post_date' => $postData['post_date'],
402 'post_date_gmt' => $postData['post_date_gmt'],
403 ]);
404 }
405
406 $product = static::getQuery()->with('variants')->addAppends(['viewUrl'])->find($postId);
407
408 return static::makeSuccessResponse($product, __('Product has been updated', 'fluent-cart'));
409 }
410
411 public static function updateWpPost($postId, $params = [])
412 {
413
414 $postStatus = Arr::get($params, 'post_status');
415 $postTitle = Arr::get($params, 'post_title');
416 $postContent = Arr::get($params, 'post_content');
417 $postExcerpt = Arr::get($params, 'post_excerpt');
418 $commentStatus = Arr::get($params, 'comment_status');
419 $postName = Arr::get($params, 'post_name');
420
421 $data = [
422 'ID' => $postId,
423 'post_title' => $postTitle,
424 'post_status' => $postStatus,
425 'comment_status' => $commentStatus,
426 'post_name' => $postName,
427 ];
428
429 if (isset($postExcerpt)) {
430 $data['post_excerpt'] = $postExcerpt;
431 }
432
433 $activeEditor = Arr::get($params, 'detail.other_info.active_editor', 'wp-editor');
434 if (empty($activeEditor)) {
435 $activeEditor = 'wp-editor';
436 }
437 if (isset($postContent)) {
438 $data['post_content'] = $postContent;
439 }
440
441 // Only write post_date when the product is being scheduled (status
442 // "future") — the admin editor exposes the date picker in that case
443 // only. For ordinary edits we must NOT rewrite post_date/post_date_gmt,
444 // otherwise the creation date changes on every save and "sort by newest"
445 // breaks. WordPress updates post_modified on its own.
446 $postDate = null;
447 if ($postStatus === 'future') {
448 $scheduledDate = Arr::get($params, 'post_date');
449 if (empty($scheduledDate)) {
450 $scheduledDate = DateTime::gmtNow()->format('Y-m-d H:i:s');
451 }
452 $postDate = DateTime::anyTimeToGmt($scheduledDate)->format('Y-m-d H:i:s');
453 $data['post_date'] = $postDate;
454 $data['post_date_gmt'] = $postDate;
455 } elseif ($postStatus === 'publish' && get_post_field('post_status', $postId) === 'future') {
456 // Publishing a scheduled product early: stamp the creation date to
457 // now so it doesn't go live with a future date (which would sort as
458 // "newest"). Mirrors partialUpdate().
459 $postDate = DateTime::gmtNow()->format('Y-m-d H:i:s');
460 $data['post_date'] = $postDate;
461 $data['post_date_gmt'] = $postDate;
462 }
463
464 $updated = wp_update_post($data);
465
466 if ($updated) {
467 $ormData = ['post_status' => $postStatus];
468 if ($postDate !== null) {
469 $ormData['post_date'] = $postDate;
470 $ormData['post_date_gmt'] = $postDate;
471 }
472 Product::query()->where('ID', $postId)->update($ormData);
473 }
474
475 return $updated;
476 }
477
478 /**
479 * Delete a product and its associated data.
480 *
481 * @param int $postId The ID of the product to be deleted.
482 * @param array $params Additional parameters for the deletion process.
483 *
484 */
485 public static function delete($postId, $params = [])
486 {
487
488
489 $product = static::getQuery()
490 ->with('variants')
491 ->with('orderItems', function ($query) use ($postId) {
492 return $query->whereHas('order', function ($query) {
493 return $query->search(["status" => ["column" => "status", "operator" => "in", "value" => [Status::ORDER_ON_HOLD, Status::ORDER_PROCESSING]]]);
494 });
495 })
496 ->find($postId);
497
498
499 if (!empty($product)) {
500 if (count($product->orderItems) > 0) {
501 return static::makeErrorResponse([
502 ['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')]
503 ]);
504 }
505 $variantIds = $product->variants->pluck('id')->toArray();
506 foreach ($product->variants as $variant) {
507 $variant->media()->delete();
508 }
509 if ($variantIds) {
510 AttributeRelation::query()->whereIn('object_id', $variantIds)->delete();
511 }
512 $product->detail()->delete();
513 $product->variants()->delete();
514 $product->licensesMeta()->delete();
515 $product->downloadable_files()->delete();
516
517 $taxonomies = Taxonomy::getTaxonomies();
518 Collection::make($taxonomies)
519 ->each(function ($taxonomy) use (&$product) {
520 $ids = Taxonomy::getTermIdsFromTerms($product->getTermByType($taxonomy)->get()->toArray());
521 foreach ($ids as $id) {
522 Taxonomy::deleteTaxonomyTermFromProduct($product->ID, $taxonomy, $id);
523 }
524 });
525 $product->wp_terms()->delete();
526
527 $productTitle = $product->post_title;
528
529
530 $deletedProduct = $product->delete();
531
532 if ($deletedProduct) {
533
534 fluent_cart_success_log(
535 __('Product deleted', 'fluent-cart'),
536 sprintf(
537 /* translators: %s is the product title */
538 __('Product %s is deleted', 'fluent-cart'), $productTitle),
539 [
540 'module_name' => 'Product',
541 'module_id' => 0,
542 'module_type' => Product::class,
543 ]
544 );
545 return static::makeSuccessResponse(
546 '',
547 __('Selected product and associated data has been deleted', 'fluent-cart')
548 );
549 }
550 return static::makeErrorResponse([
551 ['code' => 400, 'message' => __('Product deletion failed!', 'fluent-cart')]
552 ]);
553 }
554
555 return static::makeErrorResponse([
556 ['code' => 404, 'message' => __('Product not found in database.', 'fluent-cart')]
557 ]);
558
559 }
560
561 /**
562 *
563 * @param $productId
564 * @param $data
565 * @return mixed
566 */
567 public static function syncVariantOption($productId, $data = [])
568 {
569 $preprocessed = apply_filters('fluent_cart/product/variant_option_payload', [
570 'product_id' => (int) $productId,
571 'data' => $data,
572 ]);
573 if (!is_array($preprocessed) || !isset($preprocessed['data'])) {
574 $preprocessed = ['product_id' => (int) $productId, 'data' => $data];
575 }
576 $data = $preprocessed['data'];
577
578 $result = apply_filters('fluent_cart/product/variant_option_sync', [
579 'product_id' => (int) $productId,
580 'data' => $data,
581 'handled' => false,
582 'response' => null,
583 ]);
584
585 if (!empty($result['handled'])) {
586 return $result['response'];
587 }
588
589 return static::makeErrorResponse([
590 ['code' => 400, 'message' => __('Illegal data provided.', 'fluent-cart')]
591 ]);
592 }
593
594 /**
595 * Manage products based on the provided action and product IDs.
596 *
597 * @param array $params Optional. Array containing the necessary parameters
598 * [
599 * 'action' => (string) Required. The action to be performed on the selected products.
600 * (e.g., Possible values: 'delete_products')
601 * 'product_ids' => (array) Required. Product IDs whose action will be performed.
602 * ]
603 *
604 */
605 public static function manageBulkActions($params = [])
606 {
607 $action = Arr::get($params, 'action', '');
608 $productIds = Arr::get($params, 'product_ids', []);
609
610 $productIds = array_map(function ($id) {
611 return (int)$id;
612 }, $productIds);
613
614 $productIds = array_filter($productIds);
615
616 if (!$productIds) {
617 return static::makeErrorResponse([
618 ['code' => 403, 'message' => __('Products selection is required', 'fluent-cart')]
619 ]);
620 }
621
622 $products = Product::whereIn('ID', $productIds)->get();
623
624 if ($action == 'delete_products') {
625
626 $failedProductIds = [];
627 $deletedProductIds = [];
628
629 foreach ($products as $product) {
630 $isDeleted = static::delete($product->ID);
631
632 if (is_wp_error($isDeleted)) {
633 $failedProductIds[] = $product->ID;
634 } else {
635 $deletedProductIds[] = $product->ID;
636 }
637 }
638
639 if (count($failedProductIds) > 0) {
640 $failedProductIds = implode(' , ', $failedProductIds);
641 /* translators: %s: The product ID(s) that could not be deleted. */
642 return count($deletedProductIds) > 0
643 ? static::makeSuccessResponse(
644 '',
645 sprintf(
646 /* translators: %s: The product ID(s) that could not be deleted. */
647 esc_html__(
648 '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.',
649 'fluent-cart'
650 ),
651 esc_html($failedProductIds)
652 )
653 )
654 : static::makeErrorResponse([
655 [
656 'code' => 400,
657 'message' => sprintf(
658 /* translators: %s: The product ID(s) that could not be deleted. */
659 esc_html__(
660 'The Product ID - %s cannot be deleted at the moment as there are pending orders associated with it.',
661 'fluent-cart'
662 ),
663 esc_html($failedProductIds)
664 ),
665 ],
666 ]);
667 }
668
669 if (count($deletedProductIds) > 0 && count($failedProductIds) < 1) {
670 return static::makeSuccessResponse('', __('Selected product and associated data have been deleted', 'fluent-cart'));
671 }
672 }
673
674 if ($action === 'duplicate_products') {
675 $importStockManagement = filter_var(
676 Arr::get($params, 'import_stock_management', false),
677 FILTER_VALIDATE_BOOLEAN
678 );
679 $importLicenseSettings = filter_var(
680 Arr::get($params, 'import_license_settings', false),
681 FILTER_VALIDATE_BOOLEAN
682 );
683 $importDownloadableFiles = filter_var(
684 Arr::get($params, 'import_downloadable_files', false),
685 FILTER_VALIDATE_BOOLEAN
686 );
687
688 $options = [
689 'import_stock_management' => $importStockManagement,
690 'import_license_settings' => $importLicenseSettings,
691 'import_downloadable_files' => $importDownloadableFiles,
692 ];
693
694 $failedProductIds = [];
695 $newProductIds = [];
696
697 foreach ($productIds as $productId) {
698 try {
699 $newProductIds[] = Product::duplicateProduct($productId, $options);
700 } catch (\Throwable $e) {
701 $failedProductIds[] = $productId;
702 }
703 }
704
705 if (count($failedProductIds) > 0) {
706 $failedProductIdsText = implode(' , ', $failedProductIds);
707 return count($newProductIds) > 0
708 ? static::makeSuccessResponse(
709 [
710 'new_product_ids' => $newProductIds
711 ],
712 sprintf(
713 esc_html__(
714 'Some products could not be duplicated (Product ID: %s). Remaining selected products have been duplicated as drafts.',
715 'fluent-cart'
716 ),
717 esc_html($failedProductIdsText)
718 )
719 )
720 : static::makeErrorResponse([
721 [
722 'code' => 400,
723 'message' => sprintf(
724 esc_html__(
725 'Selected products could not be duplicated (Product ID: %s).',
726 'fluent-cart'
727 ),
728 esc_html($failedProductIdsText)
729 ),
730 ],
731 ]);
732 }
733
734 return static::makeSuccessResponse(
735 [
736 'new_product_ids' => $newProductIds
737 ],
738 __('Selected products have been duplicated as drafts', 'fluent-cart')
739 );
740 }
741
742 return static::makeErrorResponse([
743 ['code' => 400, 'message' => __('Selected action is invalid', 'fluent-cart')]
744 ]);
745 }
746
747 public static function validateDownloadableFiles($data)
748 {
749 $downloadableFiles = Arr::except(Arr::get($data, 'downloadable_files', []), ['*']);
750 $variants = Arr::except(Arr::get($data, 'variants', []), ['*']);
751 $fulfilmentType = Arr::get($data, 'detail.fulfillment_type');
752 $errors = [];
753
754 if (!empty($downloadableFiles)) {
755 foreach ($variants as $index => $variant) {
756 $count = 0;
757 $id = Arr::get($variant, 'rowId', null);
758 foreach ($downloadableFiles as $idx => $downloadFile) {
759 $variantIds = Arr::get($downloadFile, 'product_variation_id', null);
760 if (empty($variantIds)) {
761 $errors['downloadable_files.' . $idx . '.product_variation_id'] = 'Please choose variant';
762 }
763 if ($fulfilmentType === 'digital' && is_array($variantIds) && in_array($id, $variantIds)) {
764 $count += 1;
765 }
766 if ($fulfilmentType === 'physical') {
767 if (Arr::get($variant, 'downloadable') == 'true' && is_array($variantIds) && in_array($id, $variantIds)) {
768 $count += 1;
769 }
770 if (Arr::get($variant, 'downloadable') == 'false') {
771 $count += 1;
772 }
773 }
774 }
775 if ($count == 0) {
776 $errors['variants.' . $index . '.downloadable'] = sprintf(
777 /* translators: %s is the variant title */
778 __('%s variant is downloadable without any downloadable file', 'fluent-cart'),
779 $variant['variation_title']
780
781 );
782
783 }
784 }
785 }
786 return $errors;
787 }
788
789 public static function getNextProduct($productId, $skipOutOfStock)
790 {
791
792 }
793
794 /**
795 * Count total no of products.
796 *
797 */
798 public static function countTotalProducts()
799 {
800 return static::getQuery()
801 ->whereHas('detail')
802 ->whereHas('variants')
803 ->count();
804 }
805
806 public static function syncTaxonomyTerms($data, $postId = '', $params = [])
807 {
808 $data ??= [];
809
810 if (count(Arr::get($data, 'terms', [])) == 0) {
811 $isSynced = Taxonomy::deleteAllTermRelationshipsFromProduct($postId, Arr::get($data, 'taxonomy'));
812 } else {
813 $isSynced = Taxonomy::syncTaxonomyTermsToProduct($postId, Arr::get($data, 'taxonomy'), Arr::get($data, 'terms', []));
814 }
815 if (!is_wp_error($isSynced)) {
816 return static::makeSuccessResponse(
817 $isSynced,
818 __("Product has been updated", 'fluent-cart')
819 );
820 }
821
822 return static::makeErrorResponse([
823 ['code' => 400, 'message' => __("Product update failed!", 'fluent-cart')]
824 ]);
825 }
826
827 public static function deleteTaxonomyTerms($data, $postId = '', $params = [])
828 {
829 $data ??= [];
830
831 $isDeleted = Taxonomy::deleteTaxonomyTermFromProduct(
832 $postId,
833 Arr::get($data, 'taxonomy'),
834 Arr::get($data, 'term', null)
835 );
836
837 if (!is_wp_error($isDeleted)) {
838 return static::makeSuccessResponse(
839 $isDeleted,
840 __("Product has been updated", 'fluent-cart')
841 );
842 }
843
844 return static::makeErrorResponse([
845 ['code' => 400, 'message' => __("Product update failed!", 'fluent-cart')]
846 ]);
847 }
848
849 public static function findByProductAndVariants(array $params = [])
850 {
851 $productId = Arr::get($params, 'product_id', null);
852 $variantIds = Arr::get($params, 'variant_ids', []);
853
854 if (empty($productId)) {
855 return null;
856 }
857
858 $product = static::getQuery()
859 ->where('ID', $productId)
860 ->with([
861 'postmeta',
862 'detail',
863 'variants' => function ($query) use ($variantIds) {
864 // Filter variants only if variant_ids provided
865 if (!empty($variantIds)) {
866 $query->whereIn('id', $variantIds);
867 }
868
869 $query->with('media')
870 ->orderBy('serial_index', 'ASC');
871 }
872 ])
873 ->first();
874
875 if (!$product) {
876 return null;
877 }
878
879 return $product;
880 }
881
882 }
883