BF add example products, A test for it
This commit is contained in:
parent
a61620e607
commit
051a8bf95d
|
|
@ -6,7 +6,6 @@ use Blax\Shop\Models\Product;
|
||||||
use Blax\Shop\Models\ProductAction;
|
use Blax\Shop\Models\ProductAction;
|
||||||
use Blax\Shop\Models\ProductAttribute;
|
use Blax\Shop\Models\ProductAttribute;
|
||||||
use Blax\Shop\Models\ProductCategory;
|
use Blax\Shop\Models\ProductCategory;
|
||||||
use Blax\Shop\Models\ProductPrice;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Faker\Factory as Faker;
|
use Faker\Factory as Faker;
|
||||||
|
|
||||||
|
|
@ -122,26 +121,23 @@ class ShopAddExampleProducts extends Command
|
||||||
$productName = $this->generateProductName($type);
|
$productName = $this->generateProductName($type);
|
||||||
$slug = 'example-' . \Illuminate\Support\Str::slug($productName) . '-' . $this->faker->unique()->numberBetween(1000, 9999);
|
$slug = 'example-' . \Illuminate\Support\Str::slug($productName) . '-' . $this->faker->unique()->numberBetween(1000, 9999);
|
||||||
|
|
||||||
$regularPrice = $this->faker->randomFloat(2, 10, 500);
|
// Determine pricing and sale window for the product (prices managed via ProductPrice)
|
||||||
|
$baseUnitAmount = $this->faker->numberBetween(1000, 50000); // cents
|
||||||
$onSale = $this->faker->boolean(30); // 30% chance of being on sale
|
$onSale = $this->faker->boolean(30); // 30% chance of being on sale
|
||||||
|
$saleStart = $onSale ? now()->subDays($this->faker->numberBetween(1, 30)) : null;
|
||||||
|
$saleEnd = $onSale ? now()->addDays($this->faker->numberBetween(7, 60)) : null;
|
||||||
|
|
||||||
$product = Product::create([
|
$product = Product::create([
|
||||||
'name' => $productName,
|
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
'sku' => 'EX-' . strtoupper($this->faker->bothify('??-####')),
|
'sku' => 'EX-' . strtoupper($this->faker->bothify('??-####')),
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'status' => $this->faker->randomElement(['published', 'published', 'published', 'draft']), // mostly published
|
'status' => $this->faker->randomElement(['published', 'published', 'published', 'draft']),
|
||||||
'is_visible' => true,
|
'is_visible' => true,
|
||||||
'featured' => $this->faker->boolean(20), // 20% featured
|
'featured' => $this->faker->boolean(20),
|
||||||
'price' => $onSale ? $regularPrice * 0.8 : $regularPrice,
|
'sale_start' => $saleStart,
|
||||||
'sale_price' => $onSale ? $regularPrice * $this->faker->randomFloat(2, 0.6, 0.9) : null,
|
'sale_end' => $saleEnd,
|
||||||
'sale_start' => $onSale ? now()->subDays($this->faker->numberBetween(1, 30)) : null,
|
|
||||||
'sale_end' => $onSale ? now()->addDays($this->faker->numberBetween(7, 60)) : null,
|
|
||||||
'manage_stock' => $type !== 'external',
|
'manage_stock' => $type !== 'external',
|
||||||
'stock_quantity' => $type !== 'external' ? $this->faker->numberBetween(0, 100) : 0,
|
|
||||||
'low_stock_threshold' => $type !== 'external' ? 5 : null,
|
'low_stock_threshold' => $type !== 'external' ? 5 : null,
|
||||||
'in_stock' => true,
|
|
||||||
'stock_status' => 'instock',
|
|
||||||
'weight' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 0.1, 50),
|
'weight' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 0.1, 50),
|
||||||
'length' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
|
'length' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
|
||||||
'width' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
|
'width' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
|
||||||
|
|
@ -151,11 +147,11 @@ class ShopAddExampleProducts extends Command
|
||||||
'published_at' => now(),
|
'published_at' => now(),
|
||||||
'sort_order' => $this->faker->numberBetween(0, 100),
|
'sort_order' => $this->faker->numberBetween(0, 100),
|
||||||
'tax_class' => $this->faker->randomElement(['standard', 'reduced', 'zero']),
|
'tax_class' => $this->faker->randomElement(['standard', 'reduced', 'zero']),
|
||||||
'meta' => json_encode((object)[
|
'meta' => [
|
||||||
'description' => $this->faker->paragraph(3),
|
'description' => $this->faker->paragraph(3),
|
||||||
'short_description' => $this->faker->sentence(10),
|
'short_description' => $this->faker->sentence(10),
|
||||||
'example' => true,
|
'example' => true,
|
||||||
]),
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set localized name
|
// Set localized name
|
||||||
|
|
@ -165,12 +161,30 @@ class ShopAddExampleProducts extends Command
|
||||||
$randomCategories = $this->faker->randomElements($this->categories, $this->faker->numberBetween(1, 3));
|
$randomCategories = $this->faker->randomElements($this->categories, $this->faker->numberBetween(1, 3));
|
||||||
$product->categories()->attach(collect($randomCategories)->pluck('id'));
|
$product->categories()->attach(collect($randomCategories)->pluck('id'));
|
||||||
|
|
||||||
|
// Add localized fields
|
||||||
|
$product->setLocalized('name', $productName, null, true);
|
||||||
|
$product->setLocalized('short_description', $product->meta->short_description ?? '', null, true);
|
||||||
|
$product->setLocalized('description', $product->meta->description ?? '', null, true);
|
||||||
|
|
||||||
|
// Create default price entry (prices are morph-related)
|
||||||
|
$product->prices()->create([
|
||||||
|
'name' => 'Default',
|
||||||
|
'type' => 'one_time',
|
||||||
|
'currency' => 'EUR',
|
||||||
|
'unit_amount' => $baseUnitAmount,
|
||||||
|
'sale_unit_amount' => $onSale ? (int) round($baseUnitAmount * $this->faker->randomFloat(2, 0.6, 0.9)) : null,
|
||||||
|
'is_default' => true,
|
||||||
|
'active' => true,
|
||||||
|
'billing_scheme' => 'per_unit',
|
||||||
|
'meta' => ['example' => true],
|
||||||
|
]);
|
||||||
|
|
||||||
// Add attributes
|
// Add attributes
|
||||||
$this->addAttributes($product, $type);
|
$this->addAttributes($product, $type);
|
||||||
|
|
||||||
// Add additional prices (multi-currency or subscription)
|
// Add additional prices (multi-currency or subscription)
|
||||||
if ($type === 'simple' || $type === 'variable') {
|
if ($type === 'simple' || $type === 'variable') {
|
||||||
$this->addAdditionalPrices($product);
|
$this->addAdditionalPrices($product, $baseUnitAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add example actions
|
// Add example actions
|
||||||
|
|
@ -178,7 +192,7 @@ class ShopAddExampleProducts extends Command
|
||||||
|
|
||||||
// For variable products, add variations
|
// For variable products, add variations
|
||||||
if ($type === 'variable') {
|
if ($type === 'variable') {
|
||||||
$this->addVariations($product);
|
$this->addVariations($product, $baseUnitAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For grouped products, add child products
|
// For grouped products, add child products
|
||||||
|
|
@ -275,107 +289,118 @@ class ShopAddExampleProducts extends Command
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function addAdditionalPrices(Product $product): void
|
protected function addAdditionalPrices(Product $product, int $baseUnitAmount): void
|
||||||
{
|
{
|
||||||
// Add a subscription price option
|
// Add a subscription price option
|
||||||
if ($this->faker->boolean(30)) {
|
if ($this->faker->boolean(30)) {
|
||||||
ProductPrice::create([
|
$product->prices()->create([
|
||||||
'product_id' => $product->id,
|
|
||||||
'active' => true,
|
'active' => true,
|
||||||
'name' => 'Monthly Subscription',
|
'name' => 'Monthly Subscription',
|
||||||
'type' => 'recurring',
|
'type' => 'recurring',
|
||||||
'price' => (int)($product->price * 100 * 0.3), // 30% of regular price monthly
|
'unit_amount' => (int) round($baseUnitAmount * 0.3), // ~30% monthly
|
||||||
'billing_scheme' => 'per_unit',
|
'billing_scheme' => 'per_unit',
|
||||||
'interval' => 'month',
|
'interval' => 'month',
|
||||||
'interval_count' => 1,
|
'interval_count' => 1,
|
||||||
'trial_period_days' => $this->faker->randomElement([0, 7, 14, 30]),
|
'trial_period_days' => $this->faker->randomElement([0, 7, 14, 30]),
|
||||||
'currency' => 'usd',
|
'currency' => 'EUR',
|
||||||
'is_default' => false,
|
'is_default' => false,
|
||||||
'meta' => json_encode((object)['example' => true]),
|
'meta' => ['example' => true],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add EUR price
|
// Add USD price variant
|
||||||
if ($this->faker->boolean(40)) {
|
if ($this->faker->boolean(40)) {
|
||||||
ProductPrice::create([
|
$product->prices()->create([
|
||||||
'product_id' => $product->id,
|
|
||||||
'active' => true,
|
'active' => true,
|
||||||
'name' => 'EUR Price',
|
'name' => 'USD Price',
|
||||||
'type' => 'one_time',
|
'type' => 'one_time',
|
||||||
'price' => (int)($product->price * 100 * 0.92), // Convert to cents and EUR rate
|
'unit_amount' => (int) round($baseUnitAmount * 1.08), // approx conversion
|
||||||
'billing_scheme' => 'per_unit',
|
'billing_scheme' => 'per_unit',
|
||||||
'currency' => 'eur',
|
'currency' => 'USD',
|
||||||
'is_default' => false,
|
'is_default' => false,
|
||||||
'meta' => json_encode((object)['example' => true]),
|
'meta' => ['example' => true],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function addExampleActions(Product $product): void
|
protected function addExampleActions(Product $product): void
|
||||||
{
|
{
|
||||||
|
$namespace = config('shop.actions.namespace', 'App\\Jobs\\ProductAction');
|
||||||
$actions = [
|
$actions = [
|
||||||
[
|
[
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'SendThankYouEmail',
|
'class' => $namespace . '\\SendThankYouEmail',
|
||||||
'config' => ['template' => 'thank-you', 'delay' => 0],
|
'method' => null,
|
||||||
'description' => 'Send thank you email after purchase',
|
'parameters' => ['template' => 'thank-you', 'delay' => 0],
|
||||||
|
'defer' => true,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'UpdateCustomerStats',
|
'class' => $namespace . '\\UpdateCustomerStats',
|
||||||
'config' => ['increment' => 'total_purchases'],
|
'method' => null,
|
||||||
'description' => 'Update customer purchase statistics',
|
'parameters' => ['increment' => 'total_purchases'],
|
||||||
|
'defer' => true,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'event' => 'low_stock',
|
'events' => ['low_stock'],
|
||||||
'action_type' => 'NotifyAdmin',
|
'class' => $namespace . '\\NotifyAdmin',
|
||||||
'config' => ['threshold' => 5],
|
'method' => null,
|
||||||
'description' => 'Notify admin when stock is low',
|
'parameters' => ['threshold' => 5],
|
||||||
|
'defer' => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($actions as $index => $actionData) {
|
foreach ($actions as $index => $actionData) {
|
||||||
ProductAction::create([
|
$product->actions()->create([
|
||||||
'product_id' => $product->id,
|
'events' => $actionData['events'],
|
||||||
'event' => $actionData['event'],
|
'class' => $actionData['class'],
|
||||||
'action_type' => $actionData['action_type'],
|
'method' => $actionData['method'],
|
||||||
'config' => $actionData['config'],
|
'parameters' => $actionData['parameters'],
|
||||||
'active' => $this->faker->boolean(70), // 70% active
|
'defer' => $actionData['defer'],
|
||||||
|
'active' => $this->faker->boolean(70),
|
||||||
'sort_order' => $index,
|
'sort_order' => $index,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function addVariations(Product $product): void
|
protected function addVariations(Product $product, int $baseUnitAmount): void
|
||||||
{
|
{
|
||||||
$variations = ['Small', 'Medium', 'Large'];
|
$variations = ['Small', 'Medium', 'Large'];
|
||||||
|
|
||||||
foreach ($variations as $index => $variation) {
|
foreach ($variations as $index => $variation) {
|
||||||
$variationProduct = Product::create([
|
$variationProduct = Product::create([
|
||||||
'name' => $product->name . ' - ' . $variation,
|
|
||||||
'slug' => $product->slug . '-' . \Illuminate\Support\Str::slug($variation),
|
'slug' => $product->slug . '-' . \Illuminate\Support\Str::slug($variation),
|
||||||
'sku' => $product->sku . '-' . strtoupper(substr($variation, 0, 1)),
|
'sku' => $product->sku . '-' . strtoupper(substr($variation, 0, 1)),
|
||||||
'type' => 'simple',
|
'type' => 'simple',
|
||||||
'parent_id' => $product->id,
|
'parent_id' => $product->id,
|
||||||
'status' => 'published',
|
'status' => 'published',
|
||||||
'is_visible' => false, // Variations are not directly visible
|
'is_visible' => false,
|
||||||
'price' => $product->price + ($index * 5), // Slight price increase per size
|
|
||||||
'manage_stock' => true,
|
'manage_stock' => true,
|
||||||
'stock_quantity' => $this->faker->numberBetween(5, 50),
|
|
||||||
'in_stock' => true,
|
|
||||||
'stock_status' => 'instock',
|
|
||||||
'published_at' => now(),
|
'published_at' => now(),
|
||||||
'meta' => json_encode((object)['variation' => $variation, 'example' => true]),
|
'meta' => ['variation' => $variation, 'example' => true],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$variationProduct->setLocalized('name', $product->name . ' - ' . $variation, null, true);
|
$variationProduct->setLocalized('name', ($product->getLocalized('name') ?: 'Product') . ' - ' . $variation, null, true);
|
||||||
|
|
||||||
|
// Create a slightly adjusted default price for the variation
|
||||||
|
$variationAmount = $baseUnitAmount + ($index * 500); // +5.00 per size
|
||||||
|
$variationProduct->prices()->create([
|
||||||
|
'name' => 'Default',
|
||||||
|
'type' => 'one_time',
|
||||||
|
'currency' => 'EUR',
|
||||||
|
'unit_amount' => $variationAmount,
|
||||||
|
'is_default' => true,
|
||||||
|
'active' => true,
|
||||||
|
'billing_scheme' => 'per_unit',
|
||||||
|
'meta' => ['example' => true, 'variation' => $variation],
|
||||||
|
]);
|
||||||
|
|
||||||
ProductAttribute::create([
|
ProductAttribute::create([
|
||||||
'product_id' => $variationProduct->id,
|
'product_id' => $variationProduct->id,
|
||||||
'key' => 'Size',
|
'key' => 'Size',
|
||||||
'value' => $variation,
|
'value' => $variation,
|
||||||
'sort_order' => 0,
|
'sort_order' => 0,
|
||||||
'meta' => json_encode((object)[]),
|
'meta' => null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -386,23 +411,31 @@ class ShopAddExampleProducts extends Command
|
||||||
|
|
||||||
for ($i = 0; $i < $groupSize; $i++) {
|
for ($i = 0; $i < $groupSize; $i++) {
|
||||||
$childProduct = Product::create([
|
$childProduct = Product::create([
|
||||||
'name' => $product->name . ' Item ' . ($i + 1),
|
|
||||||
'slug' => $product->slug . '-item-' . ($i + 1),
|
'slug' => $product->slug . '-item-' . ($i + 1),
|
||||||
'sku' => $product->sku . '-' . ($i + 1),
|
'sku' => $product->sku . '-' . ($i + 1),
|
||||||
'type' => 'simple',
|
'type' => 'simple',
|
||||||
'parent_id' => $product->id,
|
'parent_id' => $product->id,
|
||||||
'status' => 'published',
|
'status' => 'published',
|
||||||
'is_visible' => false,
|
'is_visible' => false,
|
||||||
'price' => $this->faker->randomFloat(2, 10, 100),
|
|
||||||
'manage_stock' => true,
|
'manage_stock' => true,
|
||||||
'stock_quantity' => $this->faker->numberBetween(10, 50),
|
|
||||||
'in_stock' => true,
|
|
||||||
'stock_status' => 'instock',
|
|
||||||
'published_at' => now(),
|
'published_at' => now(),
|
||||||
'meta' => json_encode((object)['grouped_item' => true, 'example' => true]),
|
'meta' => ['grouped_item' => true, 'example' => true],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$childProduct->setLocalized('name', $this->faker->words(3, true), null, true);
|
$childProduct->setLocalized('name', $this->faker->words(3, true), null, true);
|
||||||
|
|
||||||
|
// Create a standalone default price for the child item
|
||||||
|
$childAmount = $this->faker->numberBetween(1000, 10000);
|
||||||
|
$childProduct->prices()->create([
|
||||||
|
'name' => 'Default',
|
||||||
|
'type' => 'one_time',
|
||||||
|
'currency' => 'EUR',
|
||||||
|
'unit_amount' => $childAmount,
|
||||||
|
'is_default' => true,
|
||||||
|
'active' => true,
|
||||||
|
'billing_scheme' => 'per_unit',
|
||||||
|
'meta' => ['example' => true, 'grouped_item' => true],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Console\Commands\ShopAddExampleProducts;
|
||||||
|
use Blax\Shop\Models\Cart;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class CommandProductExamplesTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_creates_example_products_and_related_data(): void
|
||||||
|
{
|
||||||
|
$this->artisan(ShopAddExampleProducts::class, ['--clean' => true, '--count' => 2])
|
||||||
|
->assertExitCode(0);
|
||||||
|
|
||||||
|
// Parent products (no parent_id) should be 4 types * 2 count = 8
|
||||||
|
$parents = Product::whereNull('parent_id')->get();
|
||||||
|
$this->assertCount(8, $parents, 'Expected 8 parent example products');
|
||||||
|
|
||||||
|
// Total products should include variations (3 per variable) and grouped children (>=2 each)
|
||||||
|
$this->assertGreaterThanOrEqual(18, Product::count(), 'Expected at least 18 total products including children');
|
||||||
|
|
||||||
|
// Categories are created (5 predefined) and attached to parents (1-3 each)
|
||||||
|
$this->assertGreaterThanOrEqual(5, \Blax\Shop\Models\ProductCategory::count(), 'Expected at least 5 example categories');
|
||||||
|
foreach ($parents as $product) {
|
||||||
|
$this->assertGreaterThanOrEqual(1, $product->categories()->count(), 'Parent product should have at least one category');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each parent product has 3 actions as per command
|
||||||
|
foreach ($parents as $product) {
|
||||||
|
$this->assertEquals(3, $product->actions()->count(), 'Parent product should have exactly 3 actions');
|
||||||
|
// Events field present and is array
|
||||||
|
$this->assertIsArray($product->actions()->first()->events);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each product (including variants/children) must have a default price
|
||||||
|
/** @var Product $p */
|
||||||
|
foreach (Product::all() as $p) {
|
||||||
|
$this->assertTrue($p->defaultPrice()->exists(), 'Each product should have a default price');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes exist for parents (>=2) and variations (Size)
|
||||||
|
foreach ($parents as $product) {
|
||||||
|
$this->assertGreaterThanOrEqual(2, $product->attributes()->count(), 'Parent should have attributes');
|
||||||
|
}
|
||||||
|
$variation = Product::whereNotNull('parent_id')->first();
|
||||||
|
$this->assertNotNull($variation, 'There should be at least one variation');
|
||||||
|
$this->assertTrue($variation->attributes()->where('key', 'Size')->exists());
|
||||||
|
|
||||||
|
// Localization for name is populated
|
||||||
|
$this->assertNotEmpty(Product::first()->getLocalized('name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_cleans_existing_examples_when_option_provided(): void
|
||||||
|
{
|
||||||
|
// Seed examples
|
||||||
|
$this->artisan(ShopAddExampleProducts::class, ['--clean' => true, '--count' => 1])->assertExitCode(0);
|
||||||
|
$this->assertGreaterThan(0, Product::where('slug', 'like', 'example-%')->count());
|
||||||
|
|
||||||
|
// Clean again (count=0 will create categories but no products)
|
||||||
|
$this->artisan(ShopAddExampleProducts::class, ['--clean' => true, '--count' => 0])->assertExitCode(0);
|
||||||
|
|
||||||
|
// All example products removed, categories recreated (5 default)
|
||||||
|
$this->assertEquals(0, Product::where('slug', 'like', 'example-%')->count());
|
||||||
|
$this->assertEquals(5, \Blax\Shop\Models\ProductCategory::where('slug', 'like', 'example-%')->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_honors_the_count_option_for_each_type(): void
|
||||||
|
{
|
||||||
|
$this->artisan(ShopAddExampleProducts::class, ['--clean' => true, '--count' => 3])
|
||||||
|
->assertExitCode(0);
|
||||||
|
|
||||||
|
// For each of the 4 types, expect 3 parent products
|
||||||
|
$parents = Product::whereNull('parent_id')->get();
|
||||||
|
$this->assertCount(12, $parents);
|
||||||
|
|
||||||
|
$byType = $parents->groupBy('type');
|
||||||
|
$this->assertEquals(3, $byType['simple']->count());
|
||||||
|
$this->assertEquals(3, $byType['variable']->count());
|
||||||
|
$this->assertEquals(3, $byType['grouped']->count());
|
||||||
|
$this->assertEquals(3, $byType['external']->count());
|
||||||
|
|
||||||
|
// Sanity: external products do not manage stock
|
||||||
|
$this->assertTrue($byType['external']->every(fn($p) => $p->manage_stock === false));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue