1027 lines
35 KiB
PHP
1027 lines
35 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Enums\PriceType;
|
|
use App\Enums\ProductStatus;
|
|
use App\Enums\ProductType;
|
|
use App\Models\Brand;
|
|
use App\Models\Category;
|
|
use App\Models\Hub;
|
|
use App\Models\Partner;
|
|
use App\Models\Product;
|
|
use App\Models\User;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Volt\Volt;
|
|
use Spatie\Permission\Models\Role;
|
|
|
|
beforeEach(function () {
|
|
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
|
Role::create(['name' => 'Retailer']);
|
|
Role::create(['name' => 'Manufacturer']);
|
|
Role::create(['name' => 'Customer']);
|
|
});
|
|
|
|
function makePartnerWithHub(): array
|
|
{
|
|
$hub = Hub::factory()->create();
|
|
$partner = Partner::factory()->setupCompleted()->create(['hub_id' => $hub->id]);
|
|
$user = User::factory()->create(['partner_id' => $partner->id]);
|
|
|
|
return [$user, $partner, $hub];
|
|
}
|
|
|
|
// --- Access Tests ---
|
|
|
|
test('retailer can access standard product creation page', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
|
|
$this->actingAs($user)
|
|
->get(route('products.create.standard'))
|
|
->assertSuccessful();
|
|
});
|
|
|
|
test('manufacturer can access standard product creation page', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
|
|
$this->actingAs($user)
|
|
->get(route('products.create.standard'))
|
|
->assertSuccessful();
|
|
});
|
|
|
|
test('customer cannot access standard product creation page', function () {
|
|
$customer = User::factory()->create();
|
|
$customer->assignRole('Customer');
|
|
|
|
$this->actingAs($customer)
|
|
->get(route('products.create.standard'))
|
|
->assertForbidden();
|
|
});
|
|
|
|
// --- Happy Path Tests ---
|
|
|
|
test('manufacturer can create standard product with fixed price', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner, $hub] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Sofa ALBA 3-Sitzer')
|
|
->set('descriptionShort', 'Modernes Sofa mit Massivholzgestell.')
|
|
->set('descriptionLong', 'Das Sofa ALBA verbindet zeitloses Design mit regionaler Handwerkskunst.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'SOFA-ALBA-3S')
|
|
->set('sellingPrice', 1250.00)
|
|
->set('mainImages', [UploadedFile::fake()->image('sofa.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Sofa ALBA 3-Sitzer')->first();
|
|
|
|
expect($product)->not->toBeNull();
|
|
expect($product->product_type)->toBe(ProductType::SmartOrder);
|
|
expect($product->price_type)->toBe(PriceType::Fixed);
|
|
expect($product->status)->toBe(ProductStatus::Pending);
|
|
expect($product->partner_id)->toBe($partner->id);
|
|
expect($product->hub_id)->toBe($hub->id);
|
|
expect($product->is_curated)->toBeFalse();
|
|
expect($product->description_long)->toBe('Das Sofa ALBA verbindet zeitloses Design mit regionaler Handwerkskunst.');
|
|
|
|
// Master-Variante prüfen
|
|
$variant = $product->variants()->where('is_master_variant', true)->first();
|
|
expect($variant)->not->toBeNull();
|
|
expect($variant->sku)->toBe('SOFA-ALBA-3S');
|
|
expect($variant->selling_price)->toBe(125000);
|
|
expect($variant->is_active)->toBeTrue();
|
|
|
|
// Bild prüfen
|
|
expect($product->media)->toHaveCount(1);
|
|
});
|
|
|
|
test('retailer can create standard product with from_price', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = makePartnerWithHub();
|
|
$user->assignRole('Retailer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Tisch Eiche massiv')
|
|
->set('descriptionShort', 'Massivholztisch auf Maß.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'from_price')
|
|
->set('priceDisplayText', 'Ab 1.800 €')
|
|
->set('status', 'draft')
|
|
->set('sku', 'TISCH-EICHE-01')
|
|
->set('sellingPrice', 1800.00)
|
|
->set('mainImages', [UploadedFile::fake()->image('table.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Tisch Eiche massiv')->first();
|
|
|
|
expect($product)->not->toBeNull();
|
|
expect($product->product_type)->toBe(ProductType::SmartOrder);
|
|
expect($product->price_type)->toBe(PriceType::FromPrice);
|
|
expect($product->price_display_text)->toBe('Ab 1.800 €');
|
|
expect($product->status)->toBe(ProductStatus::Draft);
|
|
});
|
|
|
|
test('standard product saves physical dimensions and logistics', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Sideboard mit Logistik')
|
|
->set('descriptionShort', 'Ein Sideboard mit Maßen.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'SB-LOG-001')
|
|
->set('sellingPrice', 990.00)
|
|
->set('widthCm', 180)
|
|
->set('heightCm', 85)
|
|
->set('depthCm', 45)
|
|
->set('weightG', 45000)
|
|
->set('assemblyStatus', 'partially')
|
|
->set('packageCount', 2)
|
|
->set('packageWeightG', 52000)
|
|
->set('packageWidthCm', 190)
|
|
->set('packageHeightCm', 50)
|
|
->set('packageDepthCm', 50)
|
|
->set('mainImages', [UploadedFile::fake()->image('sideboard.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Sideboard mit Logistik')->first();
|
|
|
|
expect($product->width_cm)->toBe(180);
|
|
expect($product->height_cm)->toBe(85);
|
|
expect($product->depth_cm)->toBe(45);
|
|
expect($product->assembly_status)->toBe('partially');
|
|
|
|
$variant = $product->variants()->where('is_master_variant', true)->first();
|
|
expect($variant->variant_weight_g)->toBe(45000);
|
|
|
|
$logistics = $variant->logistics;
|
|
expect($logistics)->not->toBeNull();
|
|
expect($logistics->package_count)->toBe(2);
|
|
expect($logistics->package_weight_g)->toBe(52000);
|
|
expect($logistics->package_width_cm)->toBe(190);
|
|
});
|
|
|
|
test('standard product saves commercial variant data', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Produkt Kommerziell')
|
|
->set('descriptionShort', 'Kommerzielles Produkt.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'KOMM-001')
|
|
->set('hanMpn', 'MPN-XYZ')
|
|
->set('eanGtin', '4012345678901')
|
|
->set('sellingPrice', 1250.00)
|
|
->set('purchasePrice', 680.00)
|
|
->set('msrp', 1499.00)
|
|
->set('availabilityStatus', 'on_order')
|
|
->set('deliveryTimeText', '4-6 Wochen')
|
|
->set('mainImages', [UploadedFile::fake()->image('product.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Produkt Kommerziell')->first();
|
|
$variant = $product->variants()->where('is_master_variant', true)->first();
|
|
|
|
expect($variant->sku)->toBe('KOMM-001');
|
|
expect($variant->han_mpn)->toBe('MPN-XYZ');
|
|
expect($variant->ean_gtin)->toBe('4012345678901');
|
|
expect($variant->selling_price)->toBe(125000);
|
|
expect($variant->purchase_price)->toBe(68000);
|
|
expect($variant->msrp)->toBe(149900);
|
|
expect($variant->availability_status)->toBe('on_order');
|
|
expect($variant->delivery_time_text)->toBe('4-6 Wochen');
|
|
});
|
|
|
|
test('standard product saves SEO metadata', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'SEO Produkt')
|
|
->set('descriptionShort', 'Ein Produkt mit SEO-Daten.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'SEO-001')
|
|
->set('sellingPrice', 500.00)
|
|
->set('metaTitle', 'SEO Titel für Produkt')
|
|
->set('metaDescription', 'Eine SEO-Beschreibung für das Produkt.')
|
|
->set('mainImages', [UploadedFile::fake()->image('seo.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'SEO Produkt')->first();
|
|
expect($product->meta_title)->toBe('SEO Titel für Produkt');
|
|
expect($product->meta_description)->toBe('Eine SEO-Beschreibung für das Produkt.');
|
|
});
|
|
|
|
// --- Validation Tests ---
|
|
|
|
test('validation errors switch to the tab with the first error', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('activeTab', 'kommerziell')
|
|
->set('name', '')
|
|
->call('save')
|
|
->assertHasErrors(['name' => 'required'])
|
|
->assertSet('activeTab', 'basis');
|
|
});
|
|
|
|
test('standard product requires name', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', '')
|
|
->call('save')
|
|
->assertHasErrors(['name' => 'required']);
|
|
});
|
|
|
|
test('standard product requires short description', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Test')
|
|
->set('descriptionShort', '')
|
|
->call('save')
|
|
->assertHasErrors(['descriptionShort' => 'required']);
|
|
});
|
|
|
|
test('standard product requires category', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Test')
|
|
->set('descriptionShort', 'Beschreibung.')
|
|
->set('categoryId', null)
|
|
->call('save')
|
|
->assertHasErrors(['categoryId' => 'required']);
|
|
});
|
|
|
|
test('standard product can be created without sku and selling price', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Minimal-Produkt')
|
|
->set('descriptionShort', 'Produkt ohne SKU und Preis.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', '')
|
|
->set('sellingPrice', null)
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Minimal-Produkt')->first();
|
|
expect($product)->not->toBeNull();
|
|
|
|
$variant = $product->variants()->where('is_master_variant', true)->first();
|
|
expect($variant->sku)->toBeNull();
|
|
expect($variant->selling_price)->toBeNull();
|
|
});
|
|
|
|
test('standard product requires unique sku', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
// Erstelle existierende Variante mit gleicher SKU
|
|
$existingProduct = Product::factory()->create(['partner_id' => $user->partner_id]);
|
|
$existingProduct->variants()->create([
|
|
'sku' => 'DOPPEL-SKU',
|
|
'is_master_variant' => true,
|
|
'selling_price' => 10000,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Doppelte SKU')
|
|
->set('descriptionShort', 'Beschreibung.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'DOPPEL-SKU')
|
|
->set('sellingPrice', 500.00)
|
|
->set('mainImages', [UploadedFile::fake()->image('test.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasErrors(['sku' => 'unique']);
|
|
});
|
|
|
|
test('standard product allows all three price types', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
// Fixed Price
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Fixed-Preis Produkt')
|
|
->set('descriptionShort', 'Ein Produkt mit Festpreis.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'FIX-001')
|
|
->set('sellingPrice', 500.00)
|
|
->set('mainImages', [UploadedFile::fake()->image('fix.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Fixed-Preis Produkt')->first();
|
|
expect($product->price_type)->toBe(PriceType::Fixed);
|
|
|
|
// On Request
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Anfrage-Produkt')
|
|
->set('descriptionShort', 'Ein Produkt auf Anfrage.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'on_request')
|
|
->set('status', 'active')
|
|
->set('sku', 'REQ-001')
|
|
->set('sellingPrice', 500.00)
|
|
->set('mainImages', [UploadedFile::fake()->image('req.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product2 = Product::where('name', 'Anfrage-Produkt')->first();
|
|
expect($product2->price_type)->toBe(PriceType::OnRequest);
|
|
});
|
|
|
|
test('standard product requires price display text when from_price', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Ab-Preis ohne Text')
|
|
->set('descriptionShort', 'Beschreibung.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'from_price')
|
|
->set('priceDisplayText', '')
|
|
->set('sku', 'ABP-001')
|
|
->set('sellingPrice', 500.00)
|
|
->call('save')
|
|
->assertHasErrors(['priceDisplayText']);
|
|
});
|
|
|
|
test('standard product description short max 180 characters', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Test')
|
|
->set('descriptionShort', str_repeat('x', 181))
|
|
->call('save')
|
|
->assertHasErrors(['descriptionShort' => 'max']);
|
|
});
|
|
|
|
// --- CSV Extension Tests ---
|
|
|
|
test('standard product saves material fields', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Material-Produkt')
|
|
->set('descriptionShort', 'Ein Produkt mit Materialdaten.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'MAT-001')
|
|
->set('sellingPrice', 500.00)
|
|
->set('countryOfOrigin', 'DE')
|
|
->set('mainMaterial', 'Massivholz Buche')
|
|
->set('surfaceMaterial', 'Furnier Eiche geölt')
|
|
->set('coverMaterial', 'Stoff (Polyester)')
|
|
->set('colorFinish', 'Eiche natur / Anthrazit')
|
|
->set('certificates', ['FSC', 'OEKO-TEX'])
|
|
->set('assemblyTimeMin', 45)
|
|
->set('loadCapacityKg', 120)
|
|
->set('mainImages', [UploadedFile::fake()->image('mat.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Material-Produkt')->first();
|
|
|
|
expect($product->country_of_origin)->toBe('DE');
|
|
expect($product->main_material)->toBe('Massivholz Buche');
|
|
expect($product->surface_material)->toBe('Furnier Eiche geölt');
|
|
expect($product->cover_material)->toBe('Stoff (Polyester)');
|
|
expect($product->color_finish)->toBe('Eiche natur / Anthrazit');
|
|
expect($product->certificates)->toBe(['FSC', 'OEKO-TEX']);
|
|
expect($product->assembly_time_min)->toBe(45);
|
|
expect($product->load_capacity_kg)->toBe(120);
|
|
});
|
|
|
|
test('standard product saves logistics extension fields', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Logistik-Erweitert')
|
|
->set('descriptionShort', 'Produkt mit erweiterten Logistikdaten.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'LOG-EXT-001')
|
|
->set('sellingPrice', 990.00)
|
|
->set('packageCount', 3)
|
|
->set('packageWeightG', 80000)
|
|
->set('packageWidthCm', 200)
|
|
->set('packageHeightCm', 60)
|
|
->set('packageDepthCm', 100)
|
|
->set('packagingType', 'karton_kantenschutz')
|
|
->set('packagingRecyclablePercent', 85)
|
|
->set('isPalletizable', true)
|
|
->set('hsCode', '94016100')
|
|
->set('deliveryType', 'spedition')
|
|
->set('mainImages', [UploadedFile::fake()->image('log.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Logistik-Erweitert')->first();
|
|
$variant = $product->variants()->where('is_master_variant', true)->first();
|
|
$logistics = $variant->logistics;
|
|
|
|
expect($product->delivery_type)->toBe('spedition');
|
|
expect($logistics)->not->toBeNull();
|
|
expect($logistics->packaging_type)->toBe('karton_kantenschutz');
|
|
expect($logistics->packaging_recyclable_percent)->toBe(85);
|
|
expect($logistics->is_palletizable)->toBeTrue();
|
|
expect($logistics->hs_code)->toBe('94016100');
|
|
});
|
|
|
|
test('standard product saves service and warranty fields', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Service-Produkt')
|
|
->set('descriptionShort', 'Produkt mit Service.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'SVC-001')
|
|
->set('sellingPrice', 1200.00)
|
|
->set('assemblyService', true)
|
|
->set('serviceRadiusKm', 50)
|
|
->set('warrantyMonths', 24)
|
|
->set('mainImages', [UploadedFile::fake()->image('svc.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Service-Produkt')->first();
|
|
|
|
expect($product->assembly_service)->toBeTrue();
|
|
expect($product->service_radius_km)->toBe(50);
|
|
expect($product->warranty_months)->toBe(24);
|
|
});
|
|
|
|
test('standard product saves sustainability fields', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Nachhaltiges Produkt')
|
|
->set('descriptionShort', 'Nachhaltiges Möbelstück.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'GREEN-001')
|
|
->set('sellingPrice', 800.00)
|
|
->set('co2FootprintKg', 35.50)
|
|
->set('recyclingPercentage', 40)
|
|
->set('isRegionalProduction', true)
|
|
->set('mainImages', [UploadedFile::fake()->image('green.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Nachhaltiges Produkt')->first();
|
|
|
|
expect((float) $product->co2_footprint_kg)->toBe(35.50);
|
|
expect($product->recycling_percentage)->toBe(40);
|
|
expect($product->is_regional_production)->toBeTrue();
|
|
});
|
|
|
|
test('standard product saves EUDR wood origins', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'EUDR-Produkt')
|
|
->set('descriptionShort', 'Produkt mit Holzherkunft.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'EUDR-001')
|
|
->set('sellingPrice', 1500.00)
|
|
->set('woodOrigins', [
|
|
[
|
|
'wood_species' => 'Quercus robur',
|
|
'origin_country' => 'PL',
|
|
'origin_region' => 'Masowien',
|
|
'harvest_year' => 2024,
|
|
'forest_operator' => 'ForestPol Sp. z o.o.',
|
|
'sustainability_certificate' => 'FSC',
|
|
'eudr_reference_id' => 'EUDR-DD-2025-PL-03421',
|
|
],
|
|
[
|
|
'wood_species' => 'Fagus sylvatica',
|
|
'origin_country' => 'DE',
|
|
'origin_region' => '',
|
|
'harvest_year' => 2023,
|
|
'forest_operator' => '',
|
|
'sustainability_certificate' => 'PEFC',
|
|
'eudr_reference_id' => '',
|
|
],
|
|
])
|
|
->set('mainImages', [UploadedFile::fake()->image('eudr.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'EUDR-Produkt')->first();
|
|
|
|
expect($product->woodOrigins)->toHaveCount(2);
|
|
|
|
$first = $product->woodOrigins->first();
|
|
expect($first->wood_species)->toBe('Quercus robur');
|
|
expect($first->origin_country)->toBe('PL');
|
|
expect($first->origin_region)->toBe('Masowien');
|
|
expect($first->harvest_year)->toBe(2024);
|
|
expect($first->sustainability_certificate)->toBe('FSC');
|
|
|
|
$second = $product->woodOrigins->last();
|
|
expect($second->wood_species)->toBe('Fagus sylvatica');
|
|
expect($second->origin_country)->toBe('DE');
|
|
});
|
|
|
|
test('standard product saves visibility and scoring fields', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Scoring-Produkt')
|
|
->set('descriptionShort', 'Produkt mit Scoring.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'SCORE-001')
|
|
->set('sellingPrice', 600.00)
|
|
->set('visibleIsAvailable', true)
|
|
->set('visibleFrom', '2026-03-01')
|
|
->set('visibleUntil', '2027-03-01')
|
|
->set('storageVolumeLiters', 280)
|
|
->set('assemblyEffortScore', 3)
|
|
->set('designScore', 5)
|
|
->set('mainImages', [UploadedFile::fake()->image('score.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Scoring-Produkt')->first();
|
|
|
|
expect($product->visible_from->format('Y-m-d'))->toBe('2026-03-01');
|
|
expect($product->visible_until->format('Y-m-d'))->toBe('2027-03-01');
|
|
expect($product->storage_volume_liters)->toBe(280);
|
|
expect($product->assembly_effort_score)->toBe(3);
|
|
expect($product->design_score)->toBe(5);
|
|
});
|
|
|
|
test('standard product saves currency on variant', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'CHF-Produkt')
|
|
->set('descriptionShort', 'Produkt in Schweizer Franken.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('sku', 'CHF-001')
|
|
->set('sellingPrice', 1100.00)
|
|
->set('currency', 'CHF')
|
|
->set('mainImages', [UploadedFile::fake()->image('chf.jpg', 800, 600)])
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'CHF-Produkt')->first();
|
|
$variant = $product->variants()->where('is_master_variant', true)->first();
|
|
|
|
expect($variant->currency)->toBe('CHF');
|
|
});
|
|
|
|
test('standard product country of origin must be 2 characters', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Test')
|
|
->set('descriptionShort', 'Beschreibung.')
|
|
->set('categoryId', $category->id)
|
|
->set('sku', 'TEST-CO')
|
|
->set('sellingPrice', 100.00)
|
|
->set('countryOfOrigin', 'DEU')
|
|
->call('save')
|
|
->assertHasErrors(['countryOfOrigin' => 'size']);
|
|
});
|
|
|
|
test('standard product visible_until must be after visible_from', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Test')
|
|
->set('descriptionShort', 'Beschreibung.')
|
|
->set('categoryId', $category->id)
|
|
->set('sku', 'TEST-VIS')
|
|
->set('sellingPrice', 100.00)
|
|
->set('visibleFrom', '2027-01-01')
|
|
->set('visibleUntil', '2026-01-01')
|
|
->call('save')
|
|
->assertHasErrors(['visibleUntil']);
|
|
});
|
|
|
|
test('standard product assembly_effort_score must be between 1 and 5', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Test')
|
|
->set('descriptionShort', 'Beschreibung.')
|
|
->set('categoryId', $category->id)
|
|
->set('sku', 'TEST-AES')
|
|
->set('sellingPrice', 100.00)
|
|
->set('assemblyEffortScore', 6)
|
|
->call('save')
|
|
->assertHasErrors(['assemblyEffortScore' => 'max']);
|
|
});
|
|
|
|
test('standard product recycling_percentage must be between 0 and 100', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Test')
|
|
->set('descriptionShort', 'Beschreibung.')
|
|
->set('categoryId', $category->id)
|
|
->set('sku', 'TEST-REC')
|
|
->set('sellingPrice', 100.00)
|
|
->set('recyclingPercentage', 101)
|
|
->call('save')
|
|
->assertHasErrors(['recyclingPercentage' => 'max']);
|
|
});
|
|
|
|
test('wood origin add and remove works', function () {
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->call('addWoodOrigin')
|
|
->assertSet('woodOrigins', fn ($value) => count($value) === 1)
|
|
->call('addWoodOrigin')
|
|
->assertSet('woodOrigins', fn ($value) => count($value) === 2)
|
|
->call('removeWoodOrigin', 0)
|
|
->assertSet('woodOrigins', fn ($value) => count($value) === 1);
|
|
});
|
|
|
|
// --- Product Number Tests ---
|
|
|
|
test('partner product number is auto-generated on mount', function () {
|
|
[$user, $partner] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
|
|
$this->actingAs($user);
|
|
|
|
$expected = sprintf('P%03d-%04d', $partner->id, 1);
|
|
|
|
Volt::test('products.form-standard')
|
|
->assertSet('partnerProductNumber', $expected);
|
|
});
|
|
|
|
test('partner product number can be overwritten', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Custom-Nr Produkt')
|
|
->set('descriptionShort', 'Produkt mit eigener Nummer.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('partnerProductNumber', 'MEINE-NR-42')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Custom-Nr Produkt')->first();
|
|
expect($product->partner_product_number)->toBe('MEINE-NR-42');
|
|
});
|
|
|
|
test('b2in article number is auto-generated on save', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'B2in-Nr Produkt')
|
|
->set('descriptionShort', 'Produkt mit B2in-Nummer.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'B2in-Nr Produkt')->first();
|
|
expect($product->b2in_article_number)->toStartWith('B2IN-');
|
|
expect(strlen($product->b2in_article_number))->toBe(11);
|
|
});
|
|
|
|
test('b2in article number increments sequentially', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
// Erstes Produkt
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Sequenz-Produkt 1')
|
|
->set('descriptionShort', 'Erstes Sequenz-Produkt.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
// Zweites Produkt
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Sequenz-Produkt 2')
|
|
->set('descriptionShort', 'Zweites Sequenz-Produkt.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$first = Product::where('name', 'Sequenz-Produkt 1')->first();
|
|
$second = Product::where('name', 'Sequenz-Produkt 2')->first();
|
|
|
|
expect($first->b2in_article_number)->toBe('B2IN-000001');
|
|
expect($second->b2in_article_number)->toBe('B2IN-000002');
|
|
});
|
|
|
|
// --- Brand Autocomplete Tests ---
|
|
|
|
test('selecting existing system brand links product to it', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
$systemBrand = Brand::factory()->create(['partner_id' => null, 'name' => 'Hülsta']);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Produkt mit System-Marke')
|
|
->set('descriptionShort', 'Bestehende Marke gewählt.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('brandName', 'Hülsta')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Produkt mit System-Marke')->first();
|
|
expect($product->brand_id)->toBe($systemBrand->id);
|
|
expect(Brand::count())->toBe(1);
|
|
});
|
|
|
|
test('typing new brand name creates partner-specific brand', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Produkt mit neuer Marke')
|
|
->set('descriptionShort', 'Eigene Marke eingegeben.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('brandName', 'Meine Tischlerei')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Produkt mit neuer Marke')->first();
|
|
$newBrand = Brand::where('name', 'Meine Tischlerei')->first();
|
|
|
|
expect($newBrand)->not->toBeNull();
|
|
expect($newBrand->partner_id)->toBe($partner->id);
|
|
expect($newBrand->is_active)->toBeTrue();
|
|
expect($product->brand_id)->toBe($newBrand->id);
|
|
});
|
|
|
|
test('reusing custom brand name does not create duplicate', function () {
|
|
Storage::fake('public');
|
|
[$user, $partner] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
$existingBrand = Brand::factory()->create(['partner_id' => $partner->id, 'name' => 'Werkstatt Meier']);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Zweites Produkt gleiche Marke')
|
|
->set('descriptionShort', 'Vorhandene Partner-Marke.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('brandName', 'Werkstatt Meier')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Zweites Produkt gleiche Marke')->first();
|
|
expect($product->brand_id)->toBe($existingBrand->id);
|
|
expect(Brand::where('name', 'Werkstatt Meier')->count())->toBe(1);
|
|
});
|
|
|
|
test('product can be created without brand', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Produkt ohne Marke')
|
|
->set('descriptionShort', 'Keine Marke angegeben.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->set('brandName', '')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Produkt ohne Marke')->first();
|
|
expect($product->brand_id)->toBeNull();
|
|
});
|
|
|
|
// --- Curation Workflow Tests ---
|
|
|
|
test('submitting product as active sets status to pending', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Freigabe-Produkt')
|
|
->set('descriptionShort', 'Produkt zur Freigabe.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'active')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Freigabe-Produkt')->first();
|
|
|
|
expect($product->status)->toBe(ProductStatus::Pending);
|
|
expect($product->is_curated)->toBeFalse();
|
|
expect($product->curated_at)->toBeNull();
|
|
expect($product->curated_by)->toBeNull();
|
|
});
|
|
|
|
test('saving product as draft keeps status as draft', function () {
|
|
Storage::fake('public');
|
|
[$user] = makePartnerWithHub();
|
|
$user->assignRole('Manufacturer');
|
|
$category = Category::factory()->create();
|
|
|
|
$this->actingAs($user);
|
|
|
|
Volt::test('products.form-standard')
|
|
->set('name', 'Entwurf-Produkt')
|
|
->set('descriptionShort', 'Noch nicht fertig.')
|
|
->set('categoryId', $category->id)
|
|
->set('priceType', 'fixed')
|
|
->set('status', 'draft')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$product = Product::where('name', 'Entwurf-Produkt')->first();
|
|
|
|
expect($product->status)->toBe(ProductStatus::Draft);
|
|
expect($product->is_curated)->toBeFalse();
|
|
});
|