PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.0
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 / app / Models / Product.php

Product.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.0, at app/Models/Product.php

827 lines 27.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Models;
4
5 use FluentCart\App\CPT\FluentProducts;
6 use FluentCart\App\Helpers\Helper;
7 use FluentCart\App\Helpers\Status;
8 use FluentCart\App\Models\Concerns\CanSearch;
9 use FluentCart\App\Models\WpModels\PostMeta;
10 use FluentCart\App\Models\WpModels\Term;
11 use FluentCart\App\Models\WpModels\TermRelationship;
12 use FluentCart\App\Models\WpModels\TermTaxonomy;
13 use FluentCart\App\Vite;
14 use FluentCart\Framework\Database\Orm\Builder;
15 use FluentCart\Framework\Database\Orm\Relations\HasOne;
16 use FluentCart\Framework\Support\Arr;
17 use FluentCart\Framework\Support\Str;
18
19 /**
20 * Product Model - DB Model for Products
21 *
22 * Database Model
23 *
24 * This model is intended to be use for relationships and DB query
25 * For insert update we will use WordPress's native functions
26 *
27 * @package FluentCart\App\Models
28 *
29 * @version 1.0.0
30 */
31 class Product extends Model
32 {
33 use CanSearch;
34
35 protected $table = 'posts';
36
37 protected $primaryKey = 'ID';
38
39 protected $hidden = [
40 'post_content_filtered',
41 'post_password',
42 'post_author',
43 'to_ping',
44 'pinged',
45 'post_parent',
46 'menu_order',
47 'post_mime_type',
48 'comment_count',
49 ];
50
51 protected $fillable = [
52 'post_content',
53 'post_title',
54 'post_excerpt',
55 'post_author',
56 'post_date',
57 'post_date_gmt',
58 'post_content_filtered',
59 'post_status',
60 'post_type',
61 'comment_status',
62 'ping_status',
63 'post_password',
64 'post_name',
65 'to_ping',
66 'pinged',
67 'post_modified',
68 'post_modified_gmt',
69 'post_parent',
70 'menu_order',
71 'post_mime_type',
72 'guid',
73 ];
74 const UPDATED_AT = null;
75 const CREATED_AT = null;
76
77 protected $appends = [
78 'thumbnail',
79 ];
80
81 protected $searchable = [
82 'post_title',
83 'post_status'
84 ];
85
86 public static function boot()
87 {
88 parent::boot();
89
90 static::creating(function ($model) {
91 $model->post_type = FluentProducts::CPT_NAME;
92 });
93
94 static::addGlobalScope('post_type', function (Builder $builder) {
95 $builder->where('post_type', '=', FluentProducts::CPT_NAME)->whereNot('post_status', 'auto-draft');
96 });
97 }
98
99 public function scopePublished($query)
100 {
101 return $query->where('post_status', 'publish');
102 }
103
104 public function scopeStatusOf($query, $status)
105 {
106 return $query->where('post_status', $status);
107 }
108
109
110 public function scopeAdminAll($query)
111 {
112 return $query->whereIn('post_status', Status::productAdminAllStatuses());
113 }
114
115
116 /**
117 * One2One: Product Details belongs to one Product
118 * @return HasOne
119 */
120 public function detail(): HasOne
121 {
122 return $this->hasOne(ProductDetail::class, 'post_id', 'ID');
123 }
124
125 public function variants(): \FluentCart\Framework\Database\Orm\Relations\HasMany
126 {
127 return $this->hasMany(ProductVariation::class, 'post_id', 'ID');
128 }
129
130 public function getHasSubscriptionAttribute()
131 {
132 // Ensure the variants relationship is loaded
133 $variants = $this->variants;
134
135 foreach ($variants as $variation) {
136 if (isset($variation->other_info['payment_type']) &&
137 $variation->other_info['payment_type'] === 'subscription') {
138 return true;
139 }
140 }
141
142 return false;
143 }
144
145 public function downloadable_files(): \FluentCart\Framework\Database\Orm\Relations\HasMany
146 {
147 return $this->hasMany(ProductDownload::class, 'post_id', 'ID');
148 }
149
150 /**
151 * One2One: Product belongs to one Post meta which is : Gallery Image
152 * @return HasOne
153 */
154 public function postmeta(): HasOne
155 {
156 return $this->hasOne(PostMeta::class, 'post_id', 'ID')
157 ->where('postmeta.meta_key', 'fluent-products-gallery-image');
158 }
159
160 public function wp_terms(): \FluentCart\Framework\Database\Orm\Relations\HasMany
161 {
162 return $this->hasMany(
163 TermRelationship::class,
164 'object_id',
165 'ID',
166 );
167 }
168
169 public function orderItems(): \FluentCart\Framework\Database\Orm\Relations\HasMany
170 {
171 return $this->hasMany(OrderItem::class, 'post_id', 'ID');
172 }
173
174 public function getCategories()
175 {
176 return get_the_terms($this->ID, 'product-categories');
177 }
178
179
180 public function getTags()
181 {
182 return get_the_terms($this->ID, 'product-tags');
183 }
184
185
186 public function getMediaUrl($size = 'thumbnail')
187 {
188 return get_the_post_thumbnail_url($this->ID, $size);
189 }
190
191
192 /*
193 * Transforming old getters with accessor
194 * Todo check
195 */
196 public function getTagsAttribute($value)
197 {
198 return get_the_terms($this->ID, 'product-tags');
199 }
200
201
202 public function getCategoriesAttribute($value)
203 {
204 return get_the_terms($this->ID, 'product-categories');
205 }
206
207
208 public function getThumbnailAttribute()
209 {
210 if (empty($this->detail) || empty($this->detail->featured_media)) {
211 return Vite::getAssetUrl('images/placeholder.svg');
212 }
213 return Arr::get($this->detail->featured_media, 'url');
214 }
215
216
217 public function getViewUrlAttribute()
218 {
219 return get_permalink($this->ID);
220 }
221
222
223 public function getEditUrlAttribute()
224 {
225 return admin_url('post.php?post=' . $this->ID . '&action=edit');
226 }
227
228
229 public function wpTerms()
230 {
231 return $this->hasManyThrough(
232 TermTaxonomy::class,
233 TermRelationship::class,
234 'object_id', // Product ID In TermRelationShip Table
235 'term_taxonomy_id',
236 'ID',
237 'term_taxonomy_id',
238 );
239 }
240
241 public function getTermByType($type)
242 {
243 return $this
244 ->hasMany(TermRelationship::class, 'object_id')
245 ->whereHas('taxonomy', function ($query) use ($type) {
246 return $query->where('taxonomy', $type);
247 })
248 ->join('term_taxonomy', 'term_taxonomy.term_taxonomy_id', '=', 'term_relationships.term_taxonomy_id')
249 ->join('terms', 'terms.term_id', '=', 'term_taxonomy.term_id')
250 ->addSelect('terms.*', 'term_relationships.*');
251 }
252
253
254 /*
255 // Get Category Relationship
256 */
257 public function categories()
258 {
259 return $this->getTermByType('product-categories');
260 }
261
262
263 public function tags()
264 {
265 return $this->getTermByType('product-tags');
266 }
267
268
269 /*
270 * Todo: Discuss on below relation
271 */
272 public function thumbUrl(): HasOne
273 {
274 return $this
275 ->hasOne(PostMeta::class, 'post_id')
276 ->where('postmeta.meta_key', '_thumbnail_id')
277 ->leftJoin('postmeta as image_table', function ($join) {
278 $join->on('postmeta.meta_value', '=', 'image_table.post_id')
279 ->where('image_table.meta_key', '=', '_wp_attached_file');
280 })
281 ->addSelect('postmeta.*', 'image_table.meta_value as image');
282 }
283
284 public function licensesMeta(): HasOne
285 {
286 return $this->hasOne(ProductMeta::class, 'object_id', 'ID')
287 ->where('meta_key', 'license_settings');
288 }
289
290 public function scopeCartable(Builder $query): Builder
291 {
292 return $query->whereDoesntHave('licensesMeta')
293 ->withWhereHas('variants', function ($query) {
294 $query->where('payment_type', '!=', 'subscription')
295 ->with('media');
296 });
297 }
298
299 public function getProductMeta($metaKey, $objectType = null, $default = null)
300 {
301 $query = ProductMeta::query()
302 ->where('object_id', $this->ID)
303 ->where('meta_key', $metaKey);
304
305 if (!is_null($objectType)) {
306 $query->where('object_type', $objectType);
307 }
308
309 $meta = $query->first();
310
311 if ($meta) {
312 return $meta->meta_value;
313 }
314
315 return $default;
316 }
317
318 public function updateProductMeta($metaKey, $metaValue, $objectType = null)
319 {
320 $query = ProductMeta::query()
321 ->where('object_id', $this->ID)
322 ->where('meta_key', $metaKey);
323
324
325 if (!is_null($objectType)) {
326 $query->where('object_type', $objectType);
327 }
328
329 $exist = $query->first();
330
331 if ($exist) {
332 $exist->meta_value = $metaValue;
333 $exist->save();
334 return $exist;
335 }
336
337
338 $meta = new ProductMeta();
339 $meta->object_id = $this->ID;
340 $meta->meta_key = $metaKey;
341 $meta->meta_value = $metaValue;
342 $meta->object_type = $objectType;
343 $meta->save();
344
345 return $meta;
346 }
347
348 public function scopeApplyCustomSortBy($query, $sortKey, $sortType = 'DESC')
349 {
350 //id|date|title|price
351 $validKeys = [
352 'id' => 'ID',
353 'date' => 'post_date',
354 'title' => 'post_title',
355 'price' => 'item_price',
356 ];
357 $sortBy = Arr::get($validKeys, $sortKey, 'ID');
358 $sortType = in_array($sortType, ['ASC', 'DESC']) ? $sortType : 'DESC';
359
360 if ($sortBy === 'item_price') {
361 return $query->leftJoin('fct_product_details as pd', 'posts.ID', '=', 'pd.post_id')
362 ->orderBy("pd.min_price", $sortType);
363 }
364 return $query->orderBy($sortBy, $sortType);
365 }
366
367 public function scopeByVariantTypes($query, $type = null)
368 {
369 $validTypes = ['physical', 'digital', 'subscription', 'onetime', 'simple', 'variations'];
370 if (!$type || !in_array($type, $validTypes)) {
371 return $query;
372 }
373 if ($type === 'physical' || $type === 'digital') {
374 return $query->whereHas('variants', function ($query) use ($type) {
375 $query->where('fulfillment_type', $type);
376 });
377 }
378 if ($type === 'subscription' || $type === 'onetime') {
379 return $query->whereHas('variants', function ($query) use ($type) {
380 $query->where('payment_type', $type);
381 });
382 }
383
384 if ($type === 'simple') {
385 //search from details
386 return $query->whereHas('detail', function ($query) {
387 $query->where('variation_type', Helper::PRODUCT_TYPE_SIMPLE);
388 });
389 }
390 if ($type === 'variations') {
391 return $query->whereHas('detail', function ($query) {
392 $query->whereIn('variation_type', [
393 Helper::PRODUCT_TYPE_SIMPLE_VARIATION,
394 Helper::PRODUCT_TYPE_ADVANCE_VARIATION
395 ]);
396 });
397 }
398
399 return $query;
400 }
401
402 public function scopeFilterByTaxonomy($query, $taxonomies)
403 {
404
405 //example $taxonomies
406 // $taxonomies = [
407 // 'product-categories' => [1, 2, 3],
408 // 'product-brands' => [4, 5, 6]
409 // ];
410 $taxonomies = array_filter($taxonomies, function ($taxonomy) {
411 return !empty($taxonomy) && is_array($taxonomy);
412 });
413
414 if (empty($taxonomies)) {
415 return $query;
416 }
417
418 foreach ($taxonomies as $taxonomy => $terms) {
419 $query->whereHas('wpTerms', function ($query) use ($terms) {
420 return $query->search(["term_id" => ["column" => "term_id", "operator" => "in", "value" => $terms]]);
421 });
422 }
423
424 return $query;
425 }
426
427 public function soldIndividually()
428 {
429 if (
430 $this->detail &&
431 $this->detail->other_info &&
432 Arr::get($this->detail->other_info, 'sold_individually') === 'yes'
433 ) {
434 return true;
435 }
436
437 return false;
438 }
439
440 public function isStock(): bool
441 {
442 $detail = $this->detail;
443 if (!$detail) {
444 return true;
445 }
446
447 $isBundle = $this->isBundleProduct();
448
449 if (!$detail->manage_stock) {
450 if ($isBundle) {
451 $variation = $detail->default_variation_id
452 ? $this->variants->firstWhere('id', $detail->default_variation_id)
453 : $this->variants->first();
454
455 $childIds = $variation ? Arr::get($variation->other_info, 'bundle_child_ids', []) : [];
456 if (!empty($childIds)) {
457 $children = ProductVariation::query()
458 ->whereIn('id', $childIds)
459 ->get(['manage_stock', 'available', 'stock_status']);
460
461 foreach ($children as $child) {
462 if ((int)$child->manage_stock === 1) {
463 if ((int)$child->available <= 0 || $child->stock_status !== Helper::IN_STOCK) {
464 return false;
465 }
466 }
467 }
468 }
469 }
470 return true;
471 }
472
473 $parentInStock = ($detail->stock_availability === Helper::IN_STOCK);
474 if (!$isBundle) {
475 return $parentInStock;
476 }
477 if (!$parentInStock) {
478 return false;
479 }
480
481 $variation = $detail->default_variation_id
482 ? $this->variants->firstWhere('id', $detail->default_variation_id)
483 : $this->variants->first();
484
485 if (!$variation) {
486 return $parentInStock;
487 }
488
489 $childIds = Arr::get($variation->other_info, 'bundle_child_ids', []);
490 if (empty($childIds)) {
491 return $parentInStock;
492 }
493
494 $children = ProductVariation::query()
495 ->whereIn('id', $childIds)
496 ->get(['manage_stock', 'available', 'stock_status']);
497
498 foreach ($children as $child) {
499 if ((int)$child->manage_stock === 1) {
500 if ((int)$child->available <= 0 || $child->stock_status !== Helper::IN_STOCK) {
501 return false;
502 }
503 }
504 }
505
506 return true;
507 }
508
509
510 public function images(): array
511 {
512 $images = [];
513 $thumbnailImage = $this->thumbnail ?? Vite::getAssetUrl('images/placeholder.svg');
514
515 $galleryImages = get_post_meta($this->ID, 'fluent-products-gallery-image', true);
516
517
518 if (!empty($galleryImages)) {
519 foreach ($galleryImages as $image) {
520 $images[] = [
521 'type' => 'gallery_image',
522 'url' => Arr::get($image, 'url', ''),
523 'alt' => Arr::get($image, 'title', ''),
524 'product_title' => $this->post_title,
525 'attachment_id' => Arr::get($image, 'id', ''),
526 ];
527 }
528 } else {
529 $images[] = [
530 'type' => 'thumbnail',
531 'url' => $thumbnailImage,
532 'alt' => $this->post_title,
533 'product_title' => $this->post_title,
534 'attachment_id' => null,
535 ];
536 }
537
538 foreach ($this->variants as $variant) {
539 if (!empty($variant['media']['meta_value'])) {
540 foreach ($variant['media']['meta_value'] as $image) {
541 $images[] = [
542 'type' => 'variation_image',
543 'url' => Arr::get($image, 'url', ''),
544 'alt' => Arr::get($image, 'title', ''),
545 'variation_title' => Arr::get($variant, 'variation_title', ''),
546 'variation_id' => Arr::get($variant, 'id', ''),
547 'attachment_id' => Arr::get($image, 'id', ''),
548 ];
549 }
550
551 }
552 }
553 return $images;
554 }
555
556 public function isBundleProduct(): bool
557 {
558 return $this->detail && $this->detail->other_info && Arr::get($this->detail->other_info, 'is_bundle_product') === 'yes';
559 }
560
561
562
563 public function scopeBundle($query)
564 {
565 return $query->whereHas('detail', function ($q) {
566 $q->whereNotNull('other_info')
567 ->whereRaw("JSON_EXTRACT(other_info, '$.is_bundle_product') = 'yes'");
568 });
569 }
570
571
572 public function scopeNonBundle($query)
573 {
574 return $query->whereHas('detail', function ($q) {
575 $q->where(function ($subQuery) {
576 $subQuery->whereNull('other_info')
577 ->orWhereRaw("JSON_EXTRACT(other_info, '$.is_bundle_product') != 'yes'")
578 ->orWhereRaw("JSON_EXTRACT(other_info, '$.is_bundle_product') IS NULL");
579 });
580 });
581 }
582
583 public static function duplicateProduct($productId, array $options = []): int
584 {
585 $originalProduct = static::with([
586 'detail',
587 'variants' => function ($query) {
588 $query->with(['media'])->orderBy('serial_index', 'ASC');
589 },
590 'downloadable_files'
591 ])->find($productId);
592
593 if (!$originalProduct) {
594 throw new \RuntimeException(\__('Product not found', 'fluent-cart'), 404);
595 }
596
597 return $originalProduct->performDuplicate($options);
598 }
599
600 protected function performDuplicate(array $options = []): int
601 {
602 $importStockManagement = (bool)Arr::get($options, 'import_stock_management', false);
603 $importLicenseSettings = (bool)Arr::get($options, 'import_license_settings', false);
604 $importDownloadableFiles = (bool)Arr::get($options, 'import_downloadable_files', false);
605
606 $productId = (int)$this->ID;
607
608 global $wpdb;
609 $wpdb->query('START TRANSACTION');
610
611 try {
612 $newPostData = [
613 'post_title' => $this->post_title . ' (' . \__('Copy', 'fluent-cart') . ')',
614 'post_name' => \sanitize_title($this->post_title . '-copy-' . time()),
615 'post_content' => $this->post_content,
616 'post_excerpt' => $this->post_excerpt,
617 'post_status' => 'draft',
618 'post_type' => FluentProducts::CPT_NAME,
619 'post_author' => \get_current_user_id(),
620 ];
621
622 $newProductId = \wp_insert_post($newPostData);
623
624 if (\is_wp_error($newProductId)) {
625 throw new \RuntimeException($newProductId->get_error_message());
626 }
627
628 if ($this->detail) {
629 $detailData = $this->detail->toArray();
630
631 unset($detailData['id'], $detailData['created_at'], $detailData['updated_at']);
632 // Remove appended/computed attributes and relations that are not actual DB columns
633 unset($detailData['featured_media'], $detailData['formatted_min_price'], $detailData['formatted_max_price'], $detailData['gallery_image']);
634 $detailData['post_id'] = $newProductId;
635
636 // Ensure JSON columns have valid JSON values (MySQL rejects empty strings)
637 if (empty($detailData['default_media'])) {
638 $detailData['default_media'] = [];
639 }
640 if (empty($detailData['other_info'])) {
641 $detailData['other_info'] = [];
642 }
643
644 if (!$importStockManagement) {
645 $detailData['manage_stock'] = 0;
646 $detailData['stock_status'] = 'in-stock';
647
648 if (isset($detailData['other_info'])) {
649 $otherInfo = $detailData['other_info'];
650 $detailData['other_info'] = $otherInfo;
651 }
652 }
653
654 if ($importLicenseSettings) {
655 $licenseSettings = ProductMeta::query()
656 ->where('object_id', $productId)
657 ->where('object_type', null)
658 ->where('meta_key', 'license_settings')
659 ->first();
660
661 if ($licenseSettings) {
662 ProductMeta::query()->create([
663 'object_id' => $newProductId,
664 'object_type' => null,
665 'meta_key' => 'license_settings',
666 'meta_value' => $licenseSettings->meta_value
667 ]);
668 }
669 }
670
671 if (!$importDownloadableFiles) {
672 $detailData['manage_downloadable'] = 0;
673 }
674
675 ProductDetail::query()->create($detailData);
676 }
677
678 $variationIdMap = [];
679 if ($this->variants) {
680 foreach ($this->variants as $originalVariant) {
681 $variantData = $originalVariant->toArray();
682
683 unset($variantData['id'], $variantData['created_at'], $variantData['updated_at']);
684 // Remove appended/computed attributes and relations that are not actual DB columns
685 unset($variantData['thumbnail'], $variantData['media']);
686 $variantData['post_id'] = $newProductId;
687 unset($variantData['sku']); // Remove SKU to avoid unique constraint violations
688
689 if (!$importStockManagement) {
690 $variantData['manage_stock'] = 0;
691 $variantData['stock_status'] = 'in-stock';
692 $variantData['total_stock'] = 0;
693 $variantData['available'] = 0;
694 $variantData['on_hold'] = 0;
695 $variantData['committed'] = 0;
696 }
697
698 $newVariant = ProductVariation::query()->create($variantData);
699 $variationIdMap[$originalVariant->id] = $newVariant->id;
700
701 // Copy the variant's thumbnail meta to the new variant.
702 // $originalVariant->media is hasOne on fct_product_meta
703 // where meta_key = 'product_thumbnail'; the old code
704 // foreach'd over it (treating a single Model as a
705 // collection — iterating its attribute scalars) and
706 // mis-routed the data through wp_set_object_terms (the
707 // WordPress taxonomy API, unrelated to fct_product_meta).
708 // Mirror the Pro AdvancedVariationService pattern: a
709 // fresh fct_product_meta row owned by the new variant id
710 // with the same meta_value payload.
711 if ($originalVariant->media && $newVariant) {
712 $media = $originalVariant->media;
713 ProductMeta::query()->create([
714 'object_id' => $newVariant->id,
715 'object_type' => 'product_variant_info',
716 'meta_key' => 'product_thumbnail',
717 'meta_value' => $media->meta_value,
718 ]);
719 }
720 }
721 }
722
723 // Mirror advanced-variation attribute relations onto the new
724 // variants. Without this, fct_atts_relations stays empty for
725 // the duplicated product so variant.attr_map is empty
726 // downstream — AdvancedVariationTable can't group by an
727 // attribute, the per-variant breadcrumb collapses to "—", and
728 // (with the Pro variation_title snapshot) future re-saves
729 // would compose blank titles. Re-key on the variationIdMap
730 // built above so each (old_variant_id, group_id, term_id)
731 // tuple becomes a (new_variant_id, group_id, term_id) tuple.
732 if (!empty($variationIdMap)) {
733 $originalVariantIds = array_keys($variationIdMap);
734 $relations = AttributeRelation::query()
735 ->whereIn('object_id', $originalVariantIds)
736 ->get();
737 if ($relations->isNotEmpty()) {
738 $rows = [];
739 foreach ($relations as $rel) {
740 $newVariantId = Arr::get($variationIdMap, $rel->object_id);
741 if (!$newVariantId) {
742 continue;
743 }
744 $rows[] = [
745 'group_id' => (int) $rel->group_id,
746 'term_id' => (int) $rel->term_id,
747 'object_id' => (int) $newVariantId,
748 ];
749 }
750 if (!empty($rows)) {
751 AttributeRelation::query()->insert($rows);
752 }
753 }
754 }
755
756 if ($importDownloadableFiles && $this->downloadable_files) {
757 foreach ($this->downloadable_files as $file) {
758 $fileData = $file->toArray();
759
760 unset($fileData['id'], $fileData['created_at'], $fileData['updated_at']);
761 $fileData['post_id'] = $newProductId;
762 $fileData['download_identifier'] = Str::uuid();
763
764 if (!empty($fileData['product_variation_id'])) {
765 $productVariationIds = [];
766 foreach ($fileData['product_variation_id'] as $variationId) {
767 $productVariationIds[] = Arr::get($variationIdMap, $variationId);
768 }
769 $fileData['product_variation_id'] = $productVariationIds;
770 }
771
772 ProductDownload::query()->create($fileData);
773 }
774 }
775
776 $featuredImageId = \get_post_thumbnail_id($productId);
777 if ($featuredImageId) {
778 \set_post_thumbnail($newProductId, $featuredImageId);
779 }
780
781 $taxonomies = \get_object_taxonomies(FluentProducts::CPT_NAME);
782 foreach ($taxonomies as $taxonomy) {
783 $terms = \wp_get_object_terms($productId, $taxonomy, ['fields' => 'ids']);
784 if (!empty($terms) && !\is_wp_error($terms)) {
785 \wp_set_object_terms($newProductId, $terms, $taxonomy);
786 }
787 }
788
789 $postMeta = \get_post_meta($productId);
790 if ($postMeta) {
791 foreach ($postMeta as $key => $values) {
792 $skipKeys = ['_edit_lock', '_edit_last'];
793 if (in_array($key, $skipKeys)) {
794 continue;
795 }
796
797 foreach ($values as $value) {
798 \add_post_meta($newProductId, $key, \maybe_unserialize($value));
799 }
800 }
801 }
802
803 $wpdb->query('COMMIT');
804
805 \do_action('fluent_cart/product_duplicated', [
806 'original_product_id' => $productId,
807 'new_product_id' => $newProductId,
808 'options' => [
809 'import_stock_management' => $importStockManagement,
810 'import_license_settings' => $importLicenseSettings,
811 'import_downloadable_files' => $importDownloadableFiles,
812 ]
813 ]);
814
815 return (int)$newProductId;
816 } catch (\Throwable $e) {
817 $wpdb->query('ROLLBACK');
818 throw $e;
819 }
820 }
821
822 public function integrations(): \FluentCart\Framework\Database\Orm\Relations\HasMany
823 {
824 return $this->hasMany(ProductMeta::class, 'object_id')->where('object_type', 'product_integration');
825 }
826 }
827