gruene-seele/tests/Feature/WhiteLabelResolverTest.php
Kevin Adametz cff749b133 White Label WL-02: verbindliche Tiegelfarbe, Etikettposition und gemeinsamer Resolver
- products.whitelabel_variant_id, product_images.position/size_cm mit Backfill aus Produktnamen
- WhiteLabelResolver: Farbe vom Produkt, Kundenlogo je Position, Fehler statt stillem Rückfall; Altlogik für Produkte ohne Variante
- Attribute: Variantenwert-Parsing und Farbname ohne Größe
- Produkt- und Kundenformular um Variante, Position und Größe ergänzt
- Lieferschein noch nicht umgestellt (WL-03, wartet auf K6/K7)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 12:33:44 +00:00

275 lines
13 KiB
PHP

<?php
use App\Models\Attribute;
use App\Models\AttributeType;
use App\Models\Country;
use App\Models\Product;
use App\Models\ProductAttribute;
use App\Models\ProductImage;
use App\Models\Shipping;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingOrderItem;
use App\Models\ShoppingUser;
use App\Models\UserWhitelabelProduct;
use App\Repositories\ProductRepository;
use App\Services\WhiteLabel\ResolvedItem;
use App\Services\WhiteLabel\WhiteLabelResolver;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function wlrUser(int $admin = 0): User
{
$user = User::query()->create(['email' => uniqid('wlr_', true).'@test.example', 'password' => bcrypt('password')]);
$user->forceFill(['admin' => $admin, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save();
return $user->fresh();
}
function wlrVariant(string $name, string $value): Attribute
{
$type = AttributeType::query()->firstOrCreate(['id' => 1], ['name' => 'Variante', 'slug' => 'variante']);
return Attribute::query()->create(['attribute_type_id' => $type->id, 'name' => $name, 'value' => $value, 'slug' => uniqid('v_'), 'active' => true, 'pos' => 0]);
}
function wlrProduct(string $name, ?Attribute $variant, array $possibleVariants = []): Product
{
$product = Product::query()->create(['name' => $name, 'title' => $name, 'active' => true, 'is_set' => false, 'whitelabel' => true, 'whitelabel_variant_id' => $variant?->id]);
foreach ($possibleVariants as $attribute) {
ProductAttribute::create(['product_id' => $product->id, 'type_id' => 1, 'attribute_id' => $attribute->id]);
}
return $product;
}
function wlrStandardLabel(Product $product, array $attributes, ?float $sizeCm = null, string $position = 'deckel', bool $withFile = true): ProductImage
{
$name = uniqid('std_').'.png';
if ($withFile) {
Storage::disk('public')->put('images/product/'.$product->id.'/'.$name, 'png');
}
return ProductImage::create(['product_id' => $product->id, 'type' => 'wllogo', 'filename' => $name, 'original_name' => $name, 'ext' => 'png', 'mine' => 'image/png', 'size' => 3, 'pos' => 0, 'active' => true, 'attributes' => $attributes, 'position' => $position, 'size_cm' => $sizeCm]);
}
function wlrAssignment(User $user, Product $product, array $attributes, string $position = 'deckel', bool $withFile = true): UserWhitelabelProduct
{
$assignment = UserWhitelabelProduct::firstOrCreate(['user_id' => $user->id, 'product_id' => $product->id], ['active' => true]);
$name = uniqid('logo_').'.png';
if ($withFile) {
Storage::disk('public')->put('images/user_product/'.$assignment->id.'/'.$name, 'png');
}
ProductImage::create(['user_wl_product_id' => $assignment->id, 'type' => 'uwllogo', 'filename' => $name, 'original_name' => $name, 'ext' => 'png', 'mine' => 'image/png', 'size' => 3, 'pos' => 0, 'active' => true, 'attributes' => $attributes, 'position' => $position]);
return $assignment->fresh();
}
function wlrOrder(User $partner, array $products): ShoppingOrder
{
$country = Country::query()->firstOrCreate(['code' => 'DE'], ['en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Germanija']);
$shoppingUser = ShoppingUser::query()->forceCreate(['billing_country_id' => $country->id, 'shipping_country_id' => $country->id, 'billing_firstname' => 'Test', 'billing_lastname' => 'Partner', 'billing_address' => 'Weg 1', 'billing_zipcode' => '11111', 'billing_city' => 'Stadt', 'same_as_billing' => true]);
$shipping = Shipping::query()->firstOrCreate(['name' => 'Standard']);
$shippingCountry = ShippingCountry::query()->firstOrCreate(['shipping_id' => $shipping->id, 'country_id' => $country->id]);
$order = ShoppingOrder::query()->forceCreate(['shopping_user_id' => $shoppingUser->id, 'auth_user_id' => $partner->id, 'country_id' => $shippingCountry->id, 'shipped' => 0, 'txaction' => 'paid', 'total' => 10, 'user_white_label' => false]);
foreach ($products as $product) {
ShoppingOrderItem::query()->forceCreate(['shopping_order_id' => $order->id, 'product_id' => $product->id, 'qty' => 1, 'price' => 10]);
}
return $order->fresh();
}
beforeEach(function () {
Storage::fake('public');
$this->schwarz40 = wlrVariant('Schwarz 40 ml', '40#3,5');
$this->gold40 = wlrVariant('Gold 40 ml', '40#3,5');
$this->schwarz15 = wlrVariant('Schwarz 15 ml', '15#2');
});
test('kundenlogo mit vorhandener datei ergibt kundenlogo und produktfarbe', function () {
$partner = wlrUser();
$black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40);
wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5);
wlrAssignment($partner, $black, [(string) $this->schwarz40->id]);
$result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first();
expect($result)->toBeInstanceOf(ResolvedItem::class)
->and($result->isComplete())->toBeTrue()
->and($result->color)->toBe('Schwarz')
->and($result->colorSource)->toBe(ResolvedItem::COLOR_SOURCE_VARIANT)
->and($result->hasCustomerLogo())->toBeTrue()
->and($result->labels['deckel']->sizeCm)->toBe(3.5)
->and($result->labels['deckel']->volumeMl)->toBe(40.0)
->and($result->colorLine())->toBe('Tiegel: Schwarz')
->and($result->labelSourceLine())->toBe('Etikett: Kundenlogo');
});
test('standardetikett auf schwarzem produkt behaelt die produktfarbe und meldet fehlende zuordnung nur als hinweis', function () {
$partner = wlrUser();
$black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40);
$other = wlrProduct('Creme 15 ml (Tiegel SCHWARZ)', $this->schwarz15);
wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5);
wlrAssignment($partner, $other, [(string) $this->schwarz15->id]);
$result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first();
expect($result->isComplete())->toBeTrue()
->and($result->color)->toBe('Schwarz')
->and($result->hasCustomerLogo())->toBeFalse()
->and($result->labelSourceLine())->toBe('Etikett: Standardetikett')
->and($result->warnings)->toHaveCount(1)
->and($result->warnings[0])->toContain('keine Kundenzuordnung');
});
test('kundenlogo ohne standardetikett ist zulaessig wenn die groesse aus der variante kommt', function () {
$partner = wlrUser();
$black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40);
wlrAssignment($partner, $black, []);
$result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first();
expect($result->isComplete())->toBeTrue()
->and($result->hasCustomerLogo())->toBeTrue()
->and($result->labels['deckel']->sizeCm)->toBe(3.5);
});
test('fehlende logodatei ist ein fehler ohne rueckfall auf das standardetikett', function () {
$partner = wlrUser();
$black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40);
wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5);
wlrAssignment($partner, $black, [(string) $this->schwarz40->id], 'deckel', false);
$result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first();
expect($result->isComplete())->toBeFalse()
->and($result->labels)->toBe([])
->and($result->errors[0])->toContain('Datei fehlt');
});
test('farbwiderspruch zwischen kundenbild und produkt ist ein fehler', function () {
$partner = wlrUser();
$gold = wlrProduct('Creme 40 ml (Tiegel GOLD)', $this->gold40);
wlrStandardLabel($gold, [(string) $this->gold40->id], 3.5);
wlrAssignment($partner, $gold, [(string) $this->schwarz40->id]);
$result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$gold]))->first();
expect($result->isComplete())->toBeFalse()
->and($result->errors)->toHaveCount(1)
->and($result->errors[0])->toContain('für Schwarz hinterlegt, Produktfarbe ist Gold');
});
test('mehrere kundenlogos auf derselben position sind ein konflikt ohne auswahl nach reihenfolge', function () {
$partner = wlrUser();
$black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40);
wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5);
wlrAssignment($partner, $black, [(string) $this->schwarz40->id]);
wlrAssignment($partner, $black, [(string) $this->schwarz40->id]);
$result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first();
expect($result->isComplete())->toBeFalse()
->and($result->errors[0])->toContain('mehrere Bilder für Position "deckel"');
});
test('positionen bleiben getrennt: kundenlogo am deckel ersetzt nicht das bodenetikett', function () {
$partner = wlrUser();
$black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40);
wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5, 'deckel');
$boden = wlrStandardLabel($black, [], 4.7, 'boden');
wlrAssignment($partner, $black, [(string) $this->schwarz40->id], 'deckel');
$result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first();
expect($result->isComplete())->toBeTrue()
->and($result->labels)->toHaveCount(2)
->and($result->labels['deckel']->isCustomer())->toBeTrue()
->and($result->labels['boden']->isCustomer())->toBeFalse()
->and($result->labels['boden']->image->id)->toBe($boden->id)
->and($result->labels['boden']->sizeCm)->toBe(4.7)
->and($result->maxLabelSize())->toBe(4.7);
});
test('altlogik ohne verbindliche variante: kundenlogo greift nur bei attributueberschneidung', function () {
$partner = wlrUser();
$legacy = wlrProduct('Tattoobalm 30 ml', null, [$this->schwarz40, $this->gold40]);
wlrStandardLabel($legacy, [(string) $this->schwarz40->id, (string) $this->gold40->id]);
$noOverlap = wlrAssignment($partner, $legacy, [(string) $this->schwarz15->id]);
$result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$legacy]))->first();
expect($result->isComplete())->toBeTrue()
->and($result->hasCustomerLogo())->toBeFalse()
->and($result->color)->toBe('Standard')
->and($result->colorSource)->toBe(ResolvedItem::COLOR_SOURCE_LEGACY)
->and($result->labels['deckel']->sizeCm)->toBe(3.5)
->and(collect($result->warnings)->first(fn ($w) => str_contains($w, 'greift nicht')))->not->toBeNull();
$noOverlap->whitelabel_images()->update(['attributes' => json_encode([(string) $this->schwarz40->id])]);
$result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$legacy]))->first();
expect($result->hasCustomerLogo())->toBeTrue()
->and($result->color)->toBe('Schwarz')
->and($result->volumeMl)->toBe(40.0);
});
test('resolver schreibt weder bestellung noch kennzeichen', function () {
$partner = wlrUser();
$black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40);
wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5);
wlrAssignment($partner, $black, [(string) $this->schwarz40->id]);
$order = wlrOrder($partner, [$black]);
(new WhiteLabelResolver)->resolveOrder($order);
expect($order->fresh()->user_white_label)->toBeFalse();
});
test('nicht-white-label-positionen werden nicht aufgeloest', function () {
$partner = wlrUser();
$plain = Product::query()->create(['name' => 'Seife', 'title' => 'Seife', 'active' => true, 'is_set' => false, 'whitelabel' => false]);
expect((new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$plain])))->toHaveCount(0);
});
test('produktformular speichert verbindliche variante sowie position und groesse je etikett', function () {
$black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', null);
$label = wlrStandardLabel($black, [(string) $this->schwarz40->id]);
(new ProductRepository($black))->update([
'id' => (string) $black->id,
'name' => $black->name,
'title' => $black->title,
'active' => '1',
'whitelabel' => '1',
'whitelabel_variant_id' => (string) $this->schwarz40->id,
'whitelabel_variants' => [(string) $this->schwarz40->id],
'image_wl_attributes' => [$label->id => [(string) $this->schwarz40->id]],
'image_wl_position' => [$label->id => 'boden'],
'image_wl_size' => [$label->id => '4,7'],
]);
$black->refresh();
$label->refresh();
expect($black->whitelabel_variant_id)->toBe($this->schwarz40->id)
->and($black->whitelabel_variant->colorName())->toBe('Schwarz')
->and($label->position)->toBe('boden')
->and($label->size_cm)->toBe(4.7);
$this->actingAs(wlrUser(9), 'user');
$this->get(route('admin_product_edit', $black->id))
->assertOk()
->assertSee('Tiegelfarbe / Größe (verbindlich)')
->assertSee('Etikettposition');
});
test('attribut-farbname trennt die groesse ab', function () {
expect($this->schwarz40->colorName())->toBe('Schwarz')
->and(wlrVariant('Braunglasflasche 30 ml', '30#5')->colorName())->toBe('Braunglasflasche')
->and(wlrVariant('Bodenetikett', '40#3,5')->colorName())->toBe('Bodenetikett')
->and($this->schwarz40->variantSize())->toBe(3.5)
->and($this->schwarz40->variantVolume())->toBe(40.0);
});