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

885 lines 34.7 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' => (int) Required. The item price in CENTS (129900 = $1,299.00).
134 * 'compare_price' => (int) Required. The compare price, in cents.
135 * 'manage_cost' => (string) Optional. Whether to manage costs.
136 * 'item_cost' => (int) Required if manage cost is yes. The item cost, in cents.
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 // Amounts arrive in CENTS; normalize float artifacts without scaling.
192 foreach ($priceColumns as $column) {
193 if (Arr::has($variant, $column)) {
194 $variant[$column] = Helper::roundCent(Arr::get($variant, $column));
195 }
196 }
197
198 unset($variant['rowId']);
199 unset($variant['media']);
200
201 // Recalculate stock_status and sync total_stock from available
202 if (isset($variant['manage_stock'])) {
203 if ($variant['manage_stock']) {
204 $avail = intval(Arr::get($variant, 'available', 0));
205 $variant['stock_status'] = $avail > 0 ? Helper::IN_STOCK : Helper::OUT_OF_STOCK;
206 $variant['total_stock'] = $avail;
207 } else {
208 $variant['stock_status'] = Helper::IN_STOCK;
209 }
210 }
211
212 $variantData = $variant;
213
214 // Remove empty sku and shipping_class to prevent unique constraint violation
215 if (array_key_exists('sku', $variantData) && empty($variantData['sku'])) {
216 unset($variantData['sku']);
217 }
218 if (array_key_exists('shipping_class', $variantData) && empty($variantData['shipping_class'])) {
219 unset($variantData['shipping_class']);
220 }
221
222 // Handle other_info
223 if (!empty($otherInfo)) {
224 if (Arr::get($otherInfo, 'payment_type') == 'subscription') {
225 if (Arr::get($otherInfo, 'manage_setup_fee') == 'yes') {
226 $signupFee = Helper::roundCent(Arr::get($otherInfo, 'signup_fee', 0));
227 Arr::set($otherInfo, 'signup_fee', $signupFee);
228 }
229 $variantData['payment_type'] = 'subscription';
230 } else {
231 $variantData['payment_type'] = 'onetime';
232 }
233 $variantData['other_info'] = $otherInfo;
234 }
235
236
237 // Only update if there's data to update
238 if (!empty($variantData)) {
239 ProductVariation::query()->where('id', Arr::get($variant, 'id'))->update($variantData);
240 }
241
242 } else {
243 $variantData = [];
244
245 foreach ($variants as $index => $variant) {
246 $otherInfo = Arr::get($variant, 'other_info', []);
247
248 $priceColumns = [
249 'item_price',
250 'compare_price',
251 'item_cost',
252 ];
253
254 // Amounts arrive in CENTS; normalize without scaling.
255 foreach ($priceColumns as $column) {
256 if (Arr::has($variant, $column)) {
257 $variant[$column] = Helper::roundCent(Arr::get($variant, $column));
258 }
259 }
260 unset($variant['rowId']);
261 $variant['serial_index'] = $index + 1;
262
263 // Recalculate stock_status from available and manage_stock
264 if (isset($variant['manage_stock'])) {
265 if ($variant['manage_stock']) {
266 $avail = intval(Arr::get($variant, 'available', 0));
267 $variant['stock_status'] = $avail > 0 ? Helper::IN_STOCK : Helper::OUT_OF_STOCK;
268 $variant['total_stock'] = $avail;
269 } else {
270 $variant['stock_status'] = Helper::IN_STOCK;
271 }
272 }
273
274 if (!empty($otherInfo)) {
275 if (Arr::get($otherInfo, 'payment_type') == 'subscription') {
276 if (Arr::get($otherInfo, 'manage_setup_fee') == 'yes') {
277 $signupFee = Helper::roundCent(Arr::get($otherInfo, 'signup_fee', 0));
278 Arr::set($otherInfo, 'signup_fee', $signupFee);
279 }
280 }
281 $variant['other_info'] = $otherInfo;
282 }
283 $variantData[] = apply_filters('fluent_cart/product/variant_save_data', $variant, $postId);
284 }
285
286 // Only batch update if there's data. Wrap the write in a transaction so
287 // a mid-batch failure (UNIQUE constraint, deadlock, etc.) rolls back the
288 // entire variant update instead of leaving the product in a partially-saved
289 // state. The variants_updated action fires only after a successful commit
290 // so listeners never observe a rolled-back state.
291 if (!empty($variantData)) {
292 $db = App::db();
293 $db->beginTransaction();
294 try {
295 ProductVariation::query()->batchUpdate($variantData);
296 $db->commit();
297 } catch (\Exception $e) {
298 $db->rollBack();
299 throw $e;
300 }
301 do_action('fluent_cart/product/variants_updated', [
302 'post_id' => $postId,
303 'variants' => $variantData,
304 ]);
305 }
306 }
307
308
309 }
310
311 $defaultVariationId = Arr::get($detail, 'default_variation_id');
312 $detail['default_variation_id'] = $defaultVariationId;
313
314 // Recalculate min_price / max_price from current variant prices
315 $variantPriceRange = ProductVariation::query()
316 ->where('post_id', $postId)
317 ->selectRaw('MIN(item_price) as min_price, MAX(item_price) as max_price')
318 ->first();
319
320 if ($variantPriceRange) {
321 $detail['min_price'] = $variantPriceRange->min_price ?: 0;
322 $detail['max_price'] = $variantPriceRange->max_price ?: 0;
323 }
324
325 ProductDetailResource::update($detail, Arr::get($detail, 'id'), ['action' => 'variant_modified']);
326
327 (new StockChanged([$postId]))->dispatch();
328
329 static::updateWpPost($postId, $product);
330 if (Arr::has($product, 'gallery')) {
331 update_post_meta($postId, FluentProducts::CPT_NAME . '-gallery-image', $gallery);
332
333 if (isset($gallery[0])) {
334 set_post_thumbnail($postId, Arr::get($gallery, '0.id'));
335 $thumbnailImageId = get_post_meta($postId, '_thumbnail_id', true);
336 $thumbnail = wp_prepare_attachment_for_js($thumbnailImageId);
337 $thumbUrl = Arr::get($thumbnail, 'url');
338 if (!empty($thumbUrl) && Arr::get($gallery, '0.id') !== $thumbnailImageId) {
339 update_post_meta($postId, '_thumbnail_id', $thumbnailImageId);
340 }
341 } else {
342 delete_post_thumbnail($postId);
343 }
344 }
345
346 $product = static::getQuery()->with('variants')->addAppends([
347 'viewUrl'
348 ])->find($postId);
349
350
351 return static::makeSuccessResponse(
352 $product,
353 __('Product has been updated', 'fluent-cart')
354 );
355 }
356
357 public static function partialUpdate(array $data, $postId)
358 {
359 $product = get_post($postId);
360
361 if (!$product || $product->post_type !== 'fluent-products') {
362 return new \WP_Error('not_found', __('Product not found', 'fluent-cart'));
363 }
364
365 $postData = ['ID' => (int) $postId];
366
367 $allowedFields = ['post_title', 'post_content', 'post_excerpt', 'post_status', 'post_date'];
368
369 foreach ($allowedFields as $field) {
370 if (\array_key_exists($field, $data)) {
371 $postData[$field] = $data[$field];
372 }
373 }
374
375 if (\count($postData) === 1) {
376 return new \WP_Error('no_fields', __('No valid fields provided for update', 'fluent-cart'));
377 }
378
379 $newStatus = $postData['post_status'] ?? null;
380 $syncOrmDates = false;
381
382 if ($newStatus === 'future') {
383 $postDate = DateTime::anyTimeToGmt($postData['post_date'])->format('Y-m-d H:i:s');
384 $postData['post_date'] = $postDate;
385 $postData['post_date_gmt'] = $postDate;
386 $syncOrmDates = true;
387 } elseif ($newStatus === 'publish' && $product->post_status === 'future') {
388 $now = DateTime::gmtNow()->format('Y-m-d H:i:s');
389 $postData['post_date'] = $now;
390 $postData['post_date_gmt'] = $now;
391 $syncOrmDates = true;
392 }
393
394 $updated = wp_update_post($postData, true);
395
396 if (is_wp_error($updated)) {
397 return $updated;
398 }
399
400 if ($syncOrmDates) {
401 Product::query()->where('ID', $postId)->update([
402 'post_status' => $newStatus,
403 'post_date' => $postData['post_date'],
404 'post_date_gmt' => $postData['post_date_gmt'],
405 ]);
406 }
407
408 $product = static::getQuery()->with('variants')->addAppends(['viewUrl'])->find($postId);
409
410 return static::makeSuccessResponse($product, __('Product has been updated', 'fluent-cart'));
411 }
412
413 public static function updateWpPost($postId, $params = [])
414 {
415
416 $postStatus = Arr::get($params, 'post_status');
417 $postTitle = Arr::get($params, 'post_title');
418 $postContent = Arr::get($params, 'post_content');
419 $postExcerpt = Arr::get($params, 'post_excerpt');
420 $commentStatus = Arr::get($params, 'comment_status');
421 $postName = Arr::get($params, 'post_name');
422
423 $data = [
424 'ID' => $postId,
425 'post_title' => $postTitle,
426 'post_status' => $postStatus,
427 'comment_status' => $commentStatus,
428 'post_name' => $postName,
429 ];
430
431 if (isset($postExcerpt)) {
432 $data['post_excerpt'] = $postExcerpt;
433 }
434
435 $activeEditor = Arr::get($params, 'detail.other_info.active_editor', 'wp-editor');
436 if (empty($activeEditor)) {
437 $activeEditor = 'wp-editor';
438 }
439 if (isset($postContent)) {
440 $data['post_content'] = $postContent;
441 }
442
443 // Only write post_date when the product is being scheduled (status
444 // "future") — the admin editor exposes the date picker in that case
445 // only. For ordinary edits we must NOT rewrite post_date/post_date_gmt,
446 // otherwise the creation date changes on every save and "sort by newest"
447 // breaks. WordPress updates post_modified on its own.
448 $postDate = null;
449 if ($postStatus === 'future') {
450 $scheduledDate = Arr::get($params, 'post_date');
451 if (empty($scheduledDate)) {
452 $scheduledDate = DateTime::gmtNow()->format('Y-m-d H:i:s');
453 }
454 $postDate = DateTime::anyTimeToGmt($scheduledDate)->format('Y-m-d H:i:s');
455 $data['post_date'] = $postDate;
456 $data['post_date_gmt'] = $postDate;
457 } elseif ($postStatus === 'publish' && get_post_field('post_status', $postId) === 'future') {
458 // Publishing a scheduled product early: stamp the creation date to
459 // now so it doesn't go live with a future date (which would sort as
460 // "newest"). Mirrors partialUpdate().
461 $postDate = DateTime::gmtNow()->format('Y-m-d H:i:s');
462 $data['post_date'] = $postDate;
463 $data['post_date_gmt'] = $postDate;
464 }
465
466 $updated = wp_update_post($data);
467
468 if ($updated) {
469 $ormData = ['post_status' => $postStatus];
470 if ($postDate !== null) {
471 $ormData['post_date'] = $postDate;
472 $ormData['post_date_gmt'] = $postDate;
473 }
474 Product::query()->where('ID', $postId)->update($ormData);
475 }
476
477 return $updated;
478 }
479
480 /**
481 * Delete a product and its associated data.
482 *
483 * @param int $postId The ID of the product to be deleted.
484 * @param array $params Additional parameters for the deletion process.
485 *
486 */
487 public static function delete($postId, $params = [])
488 {
489
490
491 $product = static::getQuery()
492 ->with('variants')
493 ->with('orderItems', function ($query) use ($postId) {
494 return $query->whereHas('order', function ($query) {
495 return $query->search(["status" => ["column" => "status", "operator" => "in", "value" => [Status::ORDER_ON_HOLD, Status::ORDER_PROCESSING]]]);
496 });
497 })
498 ->find($postId);
499
500
501 if (!empty($product)) {
502 if (count($product->orderItems) > 0) {
503 return static::makeErrorResponse([
504 ['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')]
505 ]);
506 }
507 $variantIds = $product->variants->pluck('id')->toArray();
508 foreach ($product->variants as $variant) {
509 $variant->media()->delete();
510 }
511 if ($variantIds) {
512 AttributeRelation::query()->whereIn('object_id', $variantIds)->delete();
513 }
514 $product->detail()->delete();
515 $product->variants()->delete();
516 $product->licensesMeta()->delete();
517 $product->downloadable_files()->delete();
518
519 $taxonomies = Taxonomy::getTaxonomies();
520 Collection::make($taxonomies)
521 ->each(function ($taxonomy) use (&$product) {
522 $ids = Taxonomy::getTermIdsFromTerms($product->getTermByType($taxonomy)->get()->toArray());
523 foreach ($ids as $id) {
524 Taxonomy::deleteTaxonomyTermFromProduct($product->ID, $taxonomy, $id);
525 }
526 });
527 $product->wp_terms()->delete();
528
529 $productTitle = $product->post_title;
530
531
532 $deletedProduct = $product->delete();
533
534 if ($deletedProduct) {
535
536 fluent_cart_success_log(
537 __('Product deleted', 'fluent-cart'),
538 sprintf(
539 /* translators: %s is the product title */
540 __('Product %s is deleted', 'fluent-cart'), $productTitle),
541 [
542 'module_name' => 'Product',
543 'module_id' => 0,
544 'module_type' => Product::class,
545 ]
546 );
547 return static::makeSuccessResponse(
548 '',
549 __('Selected product and associated data has been deleted', 'fluent-cart')
550 );
551 }
552 return static::makeErrorResponse([
553 ['code' => 400, 'message' => __('Product deletion failed!', 'fluent-cart')]
554 ]);
555 }
556
557 return static::makeErrorResponse([
558 ['code' => 404, 'message' => __('Product not found in database.', 'fluent-cart')]
559 ]);
560
561 }
562
563 /**
564 *
565 * @param $productId
566 * @param $data
567 * @return mixed
568 */
569 public static function syncVariantOption($productId, $data = [])
570 {
571 $preprocessed = apply_filters('fluent_cart/product/variant_option_payload', [
572 'product_id' => (int) $productId,
573 'data' => $data,
574 ]);
575 if (!is_array($preprocessed) || !isset($preprocessed['data'])) {
576 $preprocessed = ['product_id' => (int) $productId, 'data' => $data];
577 }
578 $data = $preprocessed['data'];
579
580 $result = apply_filters('fluent_cart/product/variant_option_sync', [
581 'product_id' => (int) $productId,
582 'data' => $data,
583 'handled' => false,
584 'response' => null,
585 ]);
586
587 if (!empty($result['handled'])) {
588 return $result['response'];
589 }
590
591 return static::makeErrorResponse([
592 ['code' => 400, 'message' => __('Illegal data provided.', 'fluent-cart')]
593 ]);
594 }
595
596 /**
597 * Manage products based on the provided action and product IDs.
598 *
599 * @param array $params Optional. Array containing the necessary parameters
600 * [
601 * 'action' => (string) Required. The action to be performed on the selected products.
602 * (e.g., Possible values: 'delete_products')
603 * 'product_ids' => (array) Required. Product IDs whose action will be performed.
604 * ]
605 *
606 */
607 public static function manageBulkActions($params = [])
608 {
609 $action = Arr::get($params, 'action', '');
610 $productIds = Arr::get($params, 'product_ids', []);
611
612 $productIds = array_map(function ($id) {
613 return (int)$id;
614 }, $productIds);
615
616 $productIds = array_filter($productIds);
617
618 if (!$productIds) {
619 return static::makeErrorResponse([
620 ['code' => 403, 'message' => __('Products selection is required', 'fluent-cart')]
621 ]);
622 }
623
624 $products = Product::whereIn('ID', $productIds)->get();
625
626 if ($action == 'delete_products') {
627
628 $failedProductIds = [];
629 $deletedProductIds = [];
630
631 foreach ($products as $product) {
632 $isDeleted = static::delete($product->ID);
633
634 if (is_wp_error($isDeleted)) {
635 $failedProductIds[] = $product->ID;
636 } else {
637 $deletedProductIds[] = $product->ID;
638 }
639 }
640
641 if (count($failedProductIds) > 0) {
642 $failedProductIds = implode(' , ', $failedProductIds);
643 /* translators: %s: The product ID(s) that could not be deleted. */
644 return count($deletedProductIds) > 0
645 ? static::makeSuccessResponse(
646 '',
647 sprintf(
648 /* translators: %s: The product ID(s) that could not be deleted. */
649 esc_html__(
650 '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.',
651 'fluent-cart'
652 ),
653 esc_html($failedProductIds)
654 )
655 )
656 : static::makeErrorResponse([
657 [
658 'code' => 400,
659 'message' => sprintf(
660 /* translators: %s: The product ID(s) that could not be deleted. */
661 esc_html__(
662 'The Product ID - %s cannot be deleted at the moment as there are pending orders associated with it.',
663 'fluent-cart'
664 ),
665 esc_html($failedProductIds)
666 ),
667 ],
668 ]);
669 }
670
671 if (count($deletedProductIds) > 0 && count($failedProductIds) < 1) {
672 return static::makeSuccessResponse('', __('Selected product and associated data have been deleted', 'fluent-cart'));
673 }
674 }
675
676 if ($action === 'duplicate_products') {
677 $importStockManagement = filter_var(
678 Arr::get($params, 'import_stock_management', false),
679 FILTER_VALIDATE_BOOLEAN
680 );
681 $importLicenseSettings = filter_var(
682 Arr::get($params, 'import_license_settings', false),
683 FILTER_VALIDATE_BOOLEAN
684 );
685 $importDownloadableFiles = filter_var(
686 Arr::get($params, 'import_downloadable_files', false),
687 FILTER_VALIDATE_BOOLEAN
688 );
689
690 $options = [
691 'import_stock_management' => $importStockManagement,
692 'import_license_settings' => $importLicenseSettings,
693 'import_downloadable_files' => $importDownloadableFiles,
694 ];
695
696 $failedProductIds = [];
697 $newProductIds = [];
698
699 foreach ($productIds as $productId) {
700 try {
701 $newProductIds[] = Product::duplicateProduct($productId, $options);
702 } catch (\Throwable $e) {
703 $failedProductIds[] = $productId;
704 }
705 }
706
707 if (count($failedProductIds) > 0) {
708 $failedProductIdsText = implode(' , ', $failedProductIds);
709 return count($newProductIds) > 0
710 ? static::makeSuccessResponse(
711 [
712 'new_product_ids' => $newProductIds
713 ],
714 sprintf(
715 esc_html__(
716 'Some products could not be duplicated (Product ID: %s). Remaining selected products have been duplicated as drafts.',
717 'fluent-cart'
718 ),
719 esc_html($failedProductIdsText)
720 )
721 )
722 : static::makeErrorResponse([
723 [
724 'code' => 400,
725 'message' => sprintf(
726 esc_html__(
727 'Selected products could not be duplicated (Product ID: %s).',
728 'fluent-cart'
729 ),
730 esc_html($failedProductIdsText)
731 ),
732 ],
733 ]);
734 }
735
736 return static::makeSuccessResponse(
737 [
738 'new_product_ids' => $newProductIds
739 ],
740 __('Selected products have been duplicated as drafts', 'fluent-cart')
741 );
742 }
743
744 return static::makeErrorResponse([
745 ['code' => 400, 'message' => __('Selected action is invalid', 'fluent-cart')]
746 ]);
747 }
748
749 public static function validateDownloadableFiles($data)
750 {
751 $downloadableFiles = Arr::except(Arr::get($data, 'downloadable_files', []), ['*']);
752 $variants = Arr::except(Arr::get($data, 'variants', []), ['*']);
753 $fulfilmentType = Arr::get($data, 'detail.fulfillment_type');
754 $errors = [];
755
756 if (!empty($downloadableFiles)) {
757 foreach ($variants as $index => $variant) {
758 $count = 0;
759 $id = Arr::get($variant, 'rowId', null);
760 foreach ($downloadableFiles as $idx => $downloadFile) {
761 $variantIds = Arr::get($downloadFile, 'product_variation_id', null);
762 if (empty($variantIds)) {
763 $errors['downloadable_files.' . $idx . '.product_variation_id'] = 'Please choose variant';
764 }
765 if ($fulfilmentType === 'digital' && is_array($variantIds) && in_array($id, $variantIds)) {
766 $count += 1;
767 }
768 if ($fulfilmentType === 'physical') {
769 if (Arr::get($variant, 'downloadable') == 'true' && is_array($variantIds) && in_array($id, $variantIds)) {
770 $count += 1;
771 }
772 if (Arr::get($variant, 'downloadable') == 'false') {
773 $count += 1;
774 }
775 }
776 }
777 if ($count == 0) {
778 $errors['variants.' . $index . '.downloadable'] = sprintf(
779 /* translators: %s is the variant title */
780 __('%s variant is downloadable without any downloadable file', 'fluent-cart'),
781 $variant['variation_title']
782
783 );
784
785 }
786 }
787 }
788 return $errors;
789 }
790
791 public static function getNextProduct($productId, $skipOutOfStock)
792 {
793
794 }
795
796 /**
797 * Count total no of products.
798 *
799 */
800 public static function countTotalProducts()
801 {
802 return static::getQuery()
803 ->whereHas('detail')
804 ->whereHas('variants')
805 ->count();
806 }
807
808 public static function syncTaxonomyTerms($data, $postId = '', $params = [])
809 {
810 $data ??= [];
811
812 if (count(Arr::get($data, 'terms', [])) == 0) {
813 $isSynced = Taxonomy::deleteAllTermRelationshipsFromProduct($postId, Arr::get($data, 'taxonomy'));
814 } else {
815 $isSynced = Taxonomy::syncTaxonomyTermsToProduct($postId, Arr::get($data, 'taxonomy'), Arr::get($data, 'terms', []));
816 }
817 if (!is_wp_error($isSynced)) {
818 return static::makeSuccessResponse(
819 $isSynced,
820 __("Product has been updated", 'fluent-cart')
821 );
822 }
823
824 return static::makeErrorResponse([
825 ['code' => 400, 'message' => __("Product update failed!", 'fluent-cart')]
826 ]);
827 }
828
829 public static function deleteTaxonomyTerms($data, $postId = '', $params = [])
830 {
831 $data ??= [];
832
833 $isDeleted = Taxonomy::deleteTaxonomyTermFromProduct(
834 $postId,
835 Arr::get($data, 'taxonomy'),
836 Arr::get($data, 'term', null)
837 );
838
839 if (!is_wp_error($isDeleted)) {
840 return static::makeSuccessResponse(
841 $isDeleted,
842 __("Product has been updated", 'fluent-cart')
843 );
844 }
845
846 return static::makeErrorResponse([
847 ['code' => 400, 'message' => __("Product update failed!", 'fluent-cart')]
848 ]);
849 }
850
851 public static function findByProductAndVariants(array $params = [])
852 {
853 $productId = Arr::get($params, 'product_id', null);
854 $variantIds = Arr::get($params, 'variant_ids', []);
855
856 if (empty($productId)) {
857 return null;
858 }
859
860 $product = static::getQuery()
861 ->where('ID', $productId)
862 ->with([
863 'postmeta',
864 'detail',
865 'variants' => function ($query) use ($variantIds) {
866 // Filter variants only if variant_ids provided
867 if (!empty($variantIds)) {
868 $query->whereIn('id', $variantIds);
869 }
870
871 $query->with('media')
872 ->orderBy('serial_index', 'ASC');
873 }
874 ])
875 ->first();
876
877 if (!$product) {
878 return null;
879 }
880
881 return $product;
882 }
883
884 }
885