diff --git a/packages/core/database/factories/BundleFactory.php b/packages/core/database/factories/BundleFactory.php new file mode 100644 index 0000000000..d859e5934f --- /dev/null +++ b/packages/core/database/factories/BundleFactory.php @@ -0,0 +1,30 @@ + TaxClass::factory()->hasTaxRateAmounts( + TaxRateAmount::factory() + ), + 'sku' => Str::random(12), + 'unit_quantity' => 1, + 'gtin' => $this->faker->unique()->isbn13, + 'mpn' => $this->faker->unique()->isbn13, + 'ean' => $this->faker->unique()->ean13, + 'shippable' => true, + ]; + } +} diff --git a/packages/core/database/migrations/2024_06_14_100000_create_bundles_table.php b/packages/core/database/migrations/2024_06_14_100000_create_bundles_table.php new file mode 100644 index 0000000000..ec62c22195 --- /dev/null +++ b/packages/core/database/migrations/2024_06_14_100000_create_bundles_table.php @@ -0,0 +1,34 @@ +prefix.'bundles', function (Blueprint $table) { + $table->id(); + $table->foreignId('tax_class_id')->constrained($this->prefix.'tax_classes'); + $table->string('tax_ref')->index()->nullable(); + $table->json('attribute_data'); + $table->integer('unit_quantity')->unsigned()->index()->default(1); + $table->string('sku')->nullable()->index(); + $table->string('gtin')->nullable()->index(); + $table->string('mpn')->nullable()->index(); + $table->string('ean')->nullable()->index(); + $table->boolean('shippable')->default(true)->index(); + $table->integer('stock')->default(0)->index(); + $table->integer('backorder')->default(0)->index(); + $table->string('purchasable')->default('always')->index(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists($this->prefix.'bundles'); + } +}; diff --git a/packages/core/database/migrations/2024_06_14_100002_create_bundleables_table.php b/packages/core/database/migrations/2024_06_14_100002_create_bundleables_table.php new file mode 100644 index 0000000000..1c65eb5201 --- /dev/null +++ b/packages/core/database/migrations/2024_06_14_100002_create_bundleables_table.php @@ -0,0 +1,23 @@ +prefix.'bundleables', function (Blueprint $table) { + $table->id(); + $table->foreignId('bundle_id')->constrained($this->prefix.'bundles'); + $table->morphs('bundleable'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists($this->prefix.'bundleables'); + } +}; diff --git a/packages/core/database/migrations/2024_06_15_103001_create_collection_bundle_table.php b/packages/core/database/migrations/2024_06_15_103001_create_collection_bundle_table.php new file mode 100644 index 0000000000..0e595bc4ed --- /dev/null +++ b/packages/core/database/migrations/2024_06_15_103001_create_collection_bundle_table.php @@ -0,0 +1,24 @@ +prefix.'collection_bundle', function (Blueprint $table) { + $table->id(); + $table->foreignId('collection_id')->constrained($this->prefix.'collections'); + $table->foreignId('bundle_id')->constrained($this->prefix.'bundles'); + $table->integer('position')->default(1)->index(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists($this->prefix.'collection_bundle'); + } +}; diff --git a/packages/core/src/Base/Traits/HasBundles.php b/packages/core/src/Base/Traits/HasBundles.php new file mode 100644 index 0000000000..e8f6df9076 --- /dev/null +++ b/packages/core/src/Base/Traits/HasBundles.php @@ -0,0 +1,33 @@ +morphToMany( + Bundle::class, + 'bundleable', + "{$prefix}bundleables", + )->withTimestamps(); + } + + + /** + * Add a bundle to the model. + */ + public function addToBundle($bundle) + { + $this->bundles()->attach($bundle); + } +} diff --git a/packages/core/src/DiscountTypes/AmountOff.php b/packages/core/src/DiscountTypes/AmountOff.php index 105bca64e9..34fc0fdd28 100644 --- a/packages/core/src/DiscountTypes/AmountOff.php +++ b/packages/core/src/DiscountTypes/AmountOff.php @@ -5,6 +5,7 @@ use Lunar\Base\ValueObjects\Cart\DiscountBreakdown; use Lunar\Base\ValueObjects\Cart\DiscountBreakdownLine; use Lunar\DataTypes\Price; +use Lunar\Models\Bundle; use Lunar\Models\Cart; use Lunar\Models\Collection; @@ -180,6 +181,12 @@ protected function getEligibleLines(Cart $cart): \Illuminate\Support\Collection if ($collectionIds->count()) { $lines = $lines->filter(function ($line) use ($collectionIds) { + if ($line->purchasable instanceof Bundle) { + return $line->purchasable->whereHas('collections', function ($query) use ($collectionIds) { + $query->whereIn((new Collection)->getTable().'.id', $collectionIds); + })->exists(); + } + return $line->purchasable->product()->whereHas('collections', function ($query) use ($collectionIds) { $query->whereIn((new Collection)->getTable().'.id', $collectionIds); })->exists(); diff --git a/packages/core/src/Models/Bundle.php b/packages/core/src/Models/Bundle.php new file mode 100644 index 0000000000..cd0ad79b8d --- /dev/null +++ b/packages/core/src/Models/Bundle.php @@ -0,0 +1,184 @@ + 'bool', + ]; + + /** + * Return a new factory instance for the model. + */ + protected static function newFactory(): BundleFactory + { + return BundleFactory::new(); + } + + public function bundleable(): MorphTo + { + return $this->morphTo(); + } + + /** + * Return the product collections relation. + */ + public function collections(): BelongsToMany + { + return $this->belongsToMany( + \Lunar\Models\Collection::class, + config('lunar.database.table_prefix').'collection_bundle' + )->withPivot(['position'])->withTimestamps(); + } + + + public function items(): MorphToMany + { + $prefix = config('lunar.database.table_prefix'); + + return $this->morphedByMany( + ProductVariant::class, + 'bundleable', + "{$prefix}bundleables" + ); + } + + /** + * Return the tax class relationship. + */ + public function taxClass(): BelongsTo + { + return $this->belongsTo(TaxClass::class); + } + + public function getPrices(): Collection + { + return $this->prices; + } + + /** + * Return the unit quantity for the variant. + */ + public function getUnitQuantity(): int + { + return $this->unit_quantity; + } + + /** + * Return the tax class. + */ + public function getTaxClass(): TaxClass + { + return Blink::once("tax_class_{$this->tax_class_id}", function () { + return $this->taxClass; + }); + } + + public function getTaxReference(): ?string + { + return $this->tax_ref; + } + + /** + * {@inheritDoc} + */ + public function getType(): string + { + return $this->shippable ? 'physical' : 'digital'; + } + + /** + * {@inheritDoc} + */ + public function isShippable(): bool + { + return $this->shippable; + } + + /** + * {@inheritDoc} + */ + public function getDescription(): string + { + return $this->product->translateAttribute('name'); + } + + /** + * {@inheritDoc} + */ + public function getOption(): void + { + return; + } + + /** + * {@inheritDoc} + */ + public function getIdentifier(): string + { + return $this->items->map(function ($item) { + return $item->getIdentifier(); + })->implode(','); + } + + public function getThumbnail(): ?Media + { + return $this->images?->first(function ($media) { + return (bool) $media->pivot?->primary; + }); + } +} diff --git a/packages/core/src/Models/ProductVariant.php b/packages/core/src/Models/ProductVariant.php index 74b4287e53..22ef0d709b 100644 --- a/packages/core/src/Models/ProductVariant.php +++ b/packages/core/src/Models/ProductVariant.php @@ -10,6 +10,7 @@ use Lunar\Base\Casts\AsAttributeData; use Lunar\Base\Purchasable; use Lunar\Base\Traits\HasAttributes; +use Lunar\Base\Traits\HasBundles; use Lunar\Base\Traits\HasDimensions; use Lunar\Base\Traits\HasMacros; use Lunar\Base\Traits\HasPrices; @@ -53,6 +54,7 @@ class ProductVariant extends BaseModel implements Purchasable { use HasAttributes; + use HasBundles; use HasDimensions; use HasFactory; use HasMacros;