PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 1.2.0 All 47 releases
fluent-cart / app / Models / Product.php

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

809 lines 26.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\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 getMediaUrl($size = 'thumbnail')
181 {
182 return get_the_post_thumbnail_url($this->ID, $size);
183 }
184
185
186 /*
187 * Transforming old getters with accessor
188 * Todo check
189 */
190 public function getCategoriesAttribute($value)
191 {
192 return get_the_terms($this->ID, 'product-categories');
193 }
194
195
196 public function getThumbnailAttribute()
197 {
198 if (empty($this->detail) || empty($this->detail->featured_media)) {
199 return Vite::getAssetUrl('images/placeholder.svg');
200 }
201 return Arr::get($this->detail->featured_media, 'url');
202 }
203
204
205 public function getViewUrlAttribute()
206 {
207 return get_permalink($this->ID);
208 }
209
210
211 public function getEditUrlAttribute()
212 {
213 return admin_url('post.php?post=' . $this->ID . '&action=edit');
214 }
215
216
217 public function wpTerms()
218 {
219 return $this->hasManyThrough(
220 TermTaxonomy::class,
221 TermRelationship::class,
222 'object_id', // Product ID In TermRelationShip Table
223 'term_taxonomy_id',
224 'ID',
225 'term_taxonomy_id',
226 );
227 }
228
229 public function getTermByType($type)
230 {
231 return $this
232 ->hasMany(TermRelationship::class, 'object_id')
233 ->whereHas('taxonomy', function ($query) use ($type) {
234 return $query->where('taxonomy', $type);
235 })
236 ->join('term_taxonomy', 'term_taxonomy.term_taxonomy_id', '=', 'term_relationships.term_taxonomy_id')
237 ->join('terms', 'terms.term_id', '=', 'term_taxonomy.term_id')
238 ->addSelect('terms.*', 'term_relationships.*');
239 }
240
241
242 /*
243 // Get Category Relationship
244 */
245 public function categories()
246 {
247 return $this->getTermByType('product-categories');
248 }
249
250
251 /*
252 * Todo: Discuss on below relation
253 */
254 public function thumbUrl(): HasOne
255 {
256 return $this
257 ->hasOne(PostMeta::class, 'post_id')
258 ->where('postmeta.meta_key', '_thumbnail_id')
259 ->leftJoin('postmeta as image_table', function ($join) {
260 $join->on('postmeta.meta_value', '=', 'image_table.post_id')
261 ->where('image_table.meta_key', '=', '_wp_attached_file');
262 })
263 ->addSelect('postmeta.*', 'image_table.meta_value as image');
264 }
265
266 public function licensesMeta(): HasOne
267 {
268 return $this->hasOne(ProductMeta::class, 'object_id', 'ID')
269 ->where('meta_key', 'license_settings');
270 }
271
272 public function scopeCartable(Builder $query): Builder
273 {
274 return $query->whereDoesntHave('licensesMeta')
275 ->withWhereHas('variants', function ($query) {
276 $query->where('payment_type', '!=', 'subscription')
277 ->with('media');
278 });
279 }
280
281 public function getProductMeta($metaKey, $objectType = null, $default = null)
282 {
283 $query = ProductMeta::query()
284 ->where('object_id', $this->ID)
285 ->where('meta_key', $metaKey);
286
287 if (!is_null($objectType)) {
288 $query->where('object_type', $objectType);
289 }
290
291 $meta = $query->first();
292
293 if ($meta) {
294 return $meta->meta_value;
295 }
296
297 return $default;
298 }
299
300 public function updateProductMeta($metaKey, $metaValue, $objectType = null)
301 {
302 $query = ProductMeta::query()
303 ->where('object_id', $this->ID)
304 ->where('meta_key', $metaKey);
305
306
307 if (!is_null($objectType)) {
308 $query->where('object_type', $objectType);
309 }
310
311 $exist = $query->first();
312
313 if ($exist) {
314 $exist->meta_value = $metaValue;
315 $exist->save();
316 return $exist;
317 }
318
319
320 $meta = new ProductMeta();
321 $meta->object_id = $this->ID;
322 $meta->meta_key = $metaKey;
323 $meta->meta_value = $metaValue;
324 $meta->object_type = $objectType;
325 $meta->save();
326
327 return $meta;
328 }
329
330 public function scopeApplyCustomSortBy($query, $sortKey, $sortType = 'DESC')
331 {
332 //id|date|title|price
333 $validKeys = [
334 'id' => 'ID',
335 'date' => 'post_date',
336 'title' => 'post_title',
337 'price' => 'item_price',
338 ];
339 $sortBy = Arr::get($validKeys, $sortKey, 'ID');
340 $sortType = in_array($sortType, ['ASC', 'DESC']) ? $sortType : 'DESC';
341
342 if ($sortBy === 'item_price') {
343 return $query->leftJoin('fct_product_details as pd', 'posts.ID', '=', 'pd.post_id')
344 ->orderBy("pd.min_price", $sortType);
345 }
346 return $query->orderBy($sortBy, $sortType);
347 }
348
349 public function scopeByVariantTypes($query, $type = null)
350 {
351 $validTypes = ['physical', 'digital', 'subscription', 'onetime', 'simple', 'variations'];
352 if (!$type || !in_array($type, $validTypes)) {
353 return $query;
354 }
355 if ($type === 'physical' || $type === 'digital') {
356 return $query->whereHas('variants', function ($query) use ($type) {
357 $query->where('fulfillment_type', $type);
358 });
359 }
360 if ($type === 'subscription' || $type === 'onetime') {
361 return $query->whereHas('variants', function ($query) use ($type) {
362 $query->where('payment_type', $type);
363 });
364 }
365
366 if ($type === 'simple') {
367 //search from details
368 return $query->whereHas('detail', function ($query) {
369 $query->where('variation_type', Helper::PRODUCT_TYPE_SIMPLE);
370 });
371 }
372 if ($type === 'variations') {
373 return $query->whereHas('detail', function ($query) {
374 $query->whereIn('variation_type', [
375 Helper::PRODUCT_TYPE_SIMPLE_VARIATION,
376 Helper::PRODUCT_TYPE_ADVANCE_VARIATION
377 ]);
378 });
379 }
380
381 return $query;
382 }
383
384 public function scopeFilterByTaxonomy($query, $taxonomies)
385 {
386
387 //example $taxonomies
388 // $taxonomies = [
389 // 'product-categories' => [1, 2, 3],
390 // 'product-brands' => [4, 5, 6]
391 // ];
392 $taxonomies = array_filter($taxonomies, function ($taxonomy) {
393 return !empty($taxonomy) && is_array($taxonomy);
394 });
395
396 if (empty($taxonomies)) {
397 return $query;
398 }
399
400 foreach ($taxonomies as $taxonomy => $terms) {
401 $query->whereHas('wpTerms', function ($query) use ($terms) {
402 return $query->search(["term_id" => ["column" => "term_id", "operator" => "in", "value" => $terms]]);
403 });
404 }
405
406 return $query;
407 }
408
409 public function soldIndividually()
410 {
411 if (
412 $this->detail &&
413 $this->detail->other_info &&
414 Arr::get($this->detail->other_info, 'sold_individually') === 'yes'
415 ) {
416 return true;
417 }
418
419 return false;
420 }
421
422 public function isStock(): bool
423 {
424 $detail = $this->detail;
425 if (!$detail) {
426 return true;
427 }
428
429 $isBundle = $this->isBundleProduct();
430
431 if (!$detail->manage_stock) {
432 if ($isBundle) {
433 $variation = $detail->default_variation_id
434 ? $this->variants->firstWhere('id', $detail->default_variation_id)
435 : $this->variants->first();
436
437 $childIds = $variation ? Arr::get($variation->other_info, 'bundle_child_ids', []) : [];
438 if (!empty($childIds)) {
439 $children = ProductVariation::query()
440 ->whereIn('id', $childIds)
441 ->get(['manage_stock', 'available', 'stock_status']);
442
443 foreach ($children as $child) {
444 if ((int)$child->manage_stock === 1) {
445 if ((int)$child->available <= 0 || $child->stock_status !== Helper::IN_STOCK) {
446 return false;
447 }
448 }
449 }
450 }
451 }
452 return true;
453 }
454
455 $parentInStock = ($detail->stock_availability === Helper::IN_STOCK);
456 if (!$isBundle) {
457 return $parentInStock;
458 }
459 if (!$parentInStock) {
460 return false;
461 }
462
463 $variation = $detail->default_variation_id
464 ? $this->variants->firstWhere('id', $detail->default_variation_id)
465 : $this->variants->first();
466
467 if (!$variation) {
468 return $parentInStock;
469 }
470
471 $childIds = Arr::get($variation->other_info, 'bundle_child_ids', []);
472 if (empty($childIds)) {
473 return $parentInStock;
474 }
475
476 $children = ProductVariation::query()
477 ->whereIn('id', $childIds)
478 ->get(['manage_stock', 'available', 'stock_status']);
479
480 foreach ($children as $child) {
481 if ((int)$child->manage_stock === 1) {
482 if ((int)$child->available <= 0 || $child->stock_status !== Helper::IN_STOCK) {
483 return false;
484 }
485 }
486 }
487
488 return true;
489 }
490
491
492 public function images(): array
493 {
494 $images = [];
495 $thumbnailImage = $this->thumbnail ?? Vite::getAssetUrl('images/placeholder.svg');
496
497 $galleryImages = get_post_meta($this->ID, 'fluent-products-gallery-image', true);
498
499
500 if (!empty($galleryImages)) {
501 foreach ($galleryImages as $image) {
502 $images[] = [
503 'type' => 'gallery_image',
504 'url' => Arr::get($image, 'url', ''),
505 'alt' => Arr::get($image, 'title', ''),
506 'product_title' => $this->post_title,
507 'attachment_id' => Arr::get($image, 'id', ''),
508 ];
509 }
510 } else {
511 $images[] = [
512 'type' => 'thumbnail',
513 'url' => $thumbnailImage,
514 'alt' => $this->post_title,
515 'product_title' => $this->post_title,
516 'attachment_id' => null,
517 ];
518 }
519
520 foreach ($this->variants as $variant) {
521 if (!empty($variant['media']['meta_value'])) {
522 foreach ($variant['media']['meta_value'] as $image) {
523 $images[] = [
524 'type' => 'variation_image',
525 'url' => Arr::get($image, 'url', ''),
526 'alt' => Arr::get($image, 'title', ''),
527 'variation_title' => Arr::get($variant, 'variation_title', ''),
528 'variation_id' => Arr::get($variant, 'id', ''),
529 'attachment_id' => Arr::get($image, 'id', ''),
530 ];
531 }
532
533 }
534 }
535 return $images;
536 }
537
538 public function isBundleProduct(): bool
539 {
540 return $this->detail && $this->detail->other_info && Arr::get($this->detail->other_info, 'is_bundle_product') === 'yes';
541 }
542
543
544
545 public function scopeBundle($query)
546 {
547 return $query->whereHas('detail', function ($q) {
548 $q->whereNotNull('other_info')
549 ->whereRaw("JSON_EXTRACT(other_info, '$.is_bundle_product') = 'yes'");
550 });
551 }
552
553
554 public function scopeNonBundle($query)
555 {
556 return $query->whereHas('detail', function ($q) {
557 $q->where(function ($subQuery) {
558 $subQuery->whereNull('other_info')
559 ->orWhereRaw("JSON_EXTRACT(other_info, '$.is_bundle_product') != 'yes'")
560 ->orWhereRaw("JSON_EXTRACT(other_info, '$.is_bundle_product') IS NULL");
561 });
562 });
563 }
564
565 public static function duplicateProduct($productId, array $options = []): int
566 {
567 $originalProduct = static::with([
568 'detail',
569 'variants' => function ($query) {
570 $query->with(['media'])->orderBy('serial_index', 'ASC');
571 },
572 'downloadable_files'
573 ])->find($productId);
574
575 if (!$originalProduct) {
576 throw new \RuntimeException(\__('Product not found', 'fluent-cart'), 404);
577 }
578
579 return $originalProduct->performDuplicate($options);
580 }
581
582 protected function performDuplicate(array $options = []): int
583 {
584 $importStockManagement = (bool)Arr::get($options, 'import_stock_management', false);
585 $importLicenseSettings = (bool)Arr::get($options, 'import_license_settings', false);
586 $importDownloadableFiles = (bool)Arr::get($options, 'import_downloadable_files', false);
587
588 $productId = (int)$this->ID;
589
590 global $wpdb;
591 $wpdb->query('START TRANSACTION');
592
593 try {
594 $newPostData = [
595 'post_title' => $this->post_title . ' (' . \__('Copy', 'fluent-cart') . ')',
596 'post_name' => \sanitize_title($this->post_title . '-copy-' . time()),
597 'post_content' => $this->post_content,
598 'post_excerpt' => $this->post_excerpt,
599 'post_status' => 'draft',
600 'post_type' => FluentProducts::CPT_NAME,
601 'post_author' => \get_current_user_id(),
602 ];
603
604 $newProductId = \wp_insert_post($newPostData);
605
606 if (\is_wp_error($newProductId)) {
607 throw new \RuntimeException($newProductId->get_error_message());
608 }
609
610 if ($this->detail) {
611 $detailData = $this->detail->toArray();
612
613 unset($detailData['id'], $detailData['created_at'], $detailData['updated_at']);
614 // Remove appended/computed attributes and relations that are not actual DB columns
615 unset($detailData['featured_media'], $detailData['formatted_min_price'], $detailData['formatted_max_price'], $detailData['gallery_image']);
616 $detailData['post_id'] = $newProductId;
617
618 // Ensure JSON columns have valid JSON values (MySQL rejects empty strings)
619 if (empty($detailData['default_media'])) {
620 $detailData['default_media'] = [];
621 }
622 if (empty($detailData['other_info'])) {
623 $detailData['other_info'] = [];
624 }
625
626 if (!$importStockManagement) {
627 $detailData['manage_stock'] = 0;
628 $detailData['stock_status'] = 'in-stock';
629
630 if (isset($detailData['other_info'])) {
631 $otherInfo = $detailData['other_info'];
632 $detailData['other_info'] = $otherInfo;
633 }
634 }
635
636 if ($importLicenseSettings) {
637 $licenseSettings = ProductMeta::query()
638 ->where('object_id', $productId)
639 ->where('object_type', null)
640 ->where('meta_key', 'license_settings')
641 ->first();
642
643 if ($licenseSettings) {
644 ProductMeta::query()->create([
645 'object_id' => $newProductId,
646 'object_type' => null,
647 'meta_key' => 'license_settings',
648 'meta_value' => $licenseSettings->meta_value
649 ]);
650 }
651 }
652
653 if (!$importDownloadableFiles) {
654 $detailData['manage_downloadable'] = 0;
655 }
656
657 ProductDetail::query()->create($detailData);
658 }
659
660 $variationIdMap = [];
661 if ($this->variants) {
662 foreach ($this->variants as $originalVariant) {
663 $variantData = $originalVariant->toArray();
664
665 unset($variantData['id'], $variantData['created_at'], $variantData['updated_at']);
666 // Remove appended/computed attributes and relations that are not actual DB columns
667 unset($variantData['thumbnail'], $variantData['media']);
668 $variantData['post_id'] = $newProductId;
669 unset($variantData['sku']); // Remove SKU to avoid unique constraint violations
670
671 if (!$importStockManagement) {
672 $variantData['manage_stock'] = 0;
673 $variantData['stock_status'] = 'in-stock';
674 $variantData['total_stock'] = 0;
675 $variantData['available'] = 0;
676 $variantData['on_hold'] = 0;
677 $variantData['committed'] = 0;
678 }
679
680 $newVariant = ProductVariation::query()->create($variantData);
681 $variationIdMap[$originalVariant->id] = $newVariant->id;
682
683 // Copy the variant's thumbnail meta to the new variant.
684 // $originalVariant->media is hasOne on fct_product_meta
685 // where meta_key = 'product_thumbnail'; the old code
686 // foreach'd over it (treating a single Model as a
687 // collection — iterating its attribute scalars) and
688 // mis-routed the data through wp_set_object_terms (the
689 // WordPress taxonomy API, unrelated to fct_product_meta).
690 // Mirror the Pro AdvancedVariationService pattern: a
691 // fresh fct_product_meta row owned by the new variant id
692 // with the same meta_value payload.
693 if ($originalVariant->media && $newVariant) {
694 $media = $originalVariant->media;
695 ProductMeta::query()->create([
696 'object_id' => $newVariant->id,
697 'object_type' => 'product_variant_info',
698 'meta_key' => 'product_thumbnail',
699 'meta_value' => $media->meta_value,
700 ]);
701 }
702 }
703 }
704
705 // Mirror advanced-variation attribute relations onto the new
706 // variants. Without this, fct_atts_relations stays empty for
707 // the duplicated product so variant.attr_map is empty
708 // downstream — AdvancedVariationTable can't group by an
709 // attribute, the per-variant breadcrumb collapses to "—", and
710 // (with the Pro variation_title snapshot) future re-saves
711 // would compose blank titles. Re-key on the variationIdMap
712 // built above so each (old_variant_id, group_id, term_id)
713 // tuple becomes a (new_variant_id, group_id, term_id) tuple.
714 if (!empty($variationIdMap)) {
715 $originalVariantIds = array_keys($variationIdMap);
716 $relations = AttributeRelation::query()
717 ->whereIn('object_id', $originalVariantIds)
718 ->get();
719 if ($relations->isNotEmpty()) {
720 $rows = [];
721 foreach ($relations as $rel) {
722 $newVariantId = Arr::get($variationIdMap, $rel->object_id);
723 if (!$newVariantId) {
724 continue;
725 }
726 $rows[] = [
727 'group_id' => (int) $rel->group_id,
728 'term_id' => (int) $rel->term_id,
729 'object_id' => (int) $newVariantId,
730 ];
731 }
732 if (!empty($rows)) {
733 AttributeRelation::query()->insert($rows);
734 }
735 }
736 }
737
738 if ($importDownloadableFiles && $this->downloadable_files) {
739 foreach ($this->downloadable_files as $file) {
740 $fileData = $file->toArray();
741
742 unset($fileData['id'], $fileData['created_at'], $fileData['updated_at']);
743 $fileData['post_id'] = $newProductId;
744 $fileData['download_identifier'] = Str::uuid();
745
746 if (!empty($fileData['product_variation_id'])) {
747 $productVariationIds = [];
748 foreach ($fileData['product_variation_id'] as $variationId) {
749 $productVariationIds[] = Arr::get($variationIdMap, $variationId);
750 }
751 $fileData['product_variation_id'] = $productVariationIds;
752 }
753
754 ProductDownload::query()->create($fileData);
755 }
756 }
757
758 $featuredImageId = \get_post_thumbnail_id($productId);
759 if ($featuredImageId) {
760 \set_post_thumbnail($newProductId, $featuredImageId);
761 }
762
763 $taxonomies = \get_object_taxonomies(FluentProducts::CPT_NAME);
764 foreach ($taxonomies as $taxonomy) {
765 $terms = \wp_get_object_terms($productId, $taxonomy, ['fields' => 'ids']);
766 if (!empty($terms) && !\is_wp_error($terms)) {
767 \wp_set_object_terms($newProductId, $terms, $taxonomy);
768 }
769 }
770
771 $postMeta = \get_post_meta($productId);
772 if ($postMeta) {
773 foreach ($postMeta as $key => $values) {
774 $skipKeys = ['_edit_lock', '_edit_last'];
775 if (in_array($key, $skipKeys)) {
776 continue;
777 }
778
779 foreach ($values as $value) {
780 \add_post_meta($newProductId, $key, \maybe_unserialize($value));
781 }
782 }
783 }
784
785 $wpdb->query('COMMIT');
786
787 \do_action('fluent_cart/product_duplicated', [
788 'original_product_id' => $productId,
789 'new_product_id' => $newProductId,
790 'options' => [
791 'import_stock_management' => $importStockManagement,
792 'import_license_settings' => $importLicenseSettings,
793 'import_downloadable_files' => $importDownloadableFiles,
794 ]
795 ]);
796
797 return (int)$newProductId;
798 } catch (\Throwable $e) {
799 $wpdb->query('ROLLBACK');
800 throw $e;
801 }
802 }
803
804 public function integrations(): \FluentCart\Framework\Database\Orm\Relations\HasMany
805 {
806 return $this->hasMany(ProductMeta::class, 'object_id')->where('object_type', 'product_integration');
807 }
808 }
809