b2in/tests/Feature/ProductCurationTest.php
2026-04-10 17:18:17 +02:00

423 lines
13 KiB
PHP

<?php
declare(strict_types=1);
use App\Enums\ProductStatus;
use App\Enums\ProductType;
use App\Models\Partner;
use App\Models\Product;
use App\Models\ProductActivity;
use App\Models\User;
use Livewire\Volt\Volt;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
beforeEach(function () {
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
Role::create(['name' => 'Admin']);
Role::create(['name' => 'Retailer']);
$permission = Permission::firstOrCreate(['name' => 'curate products']);
Role::findByName('Admin')->givePermissionTo($permission);
});
function makeAdminUser(): User
{
$admin = User::factory()->create();
$admin->assignRole('Admin');
return $admin;
}
// --- Access Tests ---
test('admin can access admin products page', function () {
$admin = makeAdminUser();
$this->actingAs($admin)
->get(route('admin.products.index'))
->assertSuccessful();
});
test('retailer cannot access admin products page', function () {
$retailer = User::factory()->create();
$retailer->assignRole('Retailer');
$this->actingAs($retailer)
->get(route('admin.products.index'))
->assertForbidden();
});
// --- Approval Tests ---
test('admin can approve a pending product', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Pending,
'is_curated' => false,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('approve', $product->id)
->assertHasNoErrors();
$product->refresh();
expect($product->status)->toBe(ProductStatus::Active);
expect($product->is_curated)->toBeTrue();
expect($product->curated_by)->toBe($admin->id);
expect($product->curated_at)->not->toBeNull();
expect($product->curation_notes)->toBeNull();
});
test('approve creates activity log entry', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Pending,
'is_curated' => false,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('approve', $product->id)
->assertHasNoErrors();
$activity = ProductActivity::where('product_id', $product->id)->first();
expect($activity)->not->toBeNull();
expect($activity->action)->toBe('approved');
expect($activity->user_id)->toBe($admin->id);
});
// --- Correction Tests ---
test('admin can send correction for a pending product', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Pending,
'is_curated' => false,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('openCorrection', $product->id)
->assertSet('correctingProductId', $product->id)
->set('curationNotes', 'Bitte Produktbilder in höherer Auflösung hochladen.')
->call('sendCorrection', $product->id)
->assertHasNoErrors();
$product->refresh();
expect($product->status)->toBe(ProductStatus::Correction);
expect($product->is_curated)->toBeFalse();
expect($product->curation_notes)->toBe('Bitte Produktbilder in höherer Auflösung hochladen.');
});
test('correction requires notes', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Pending,
'is_curated' => false,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('openCorrection', $product->id)
->set('curationNotes', '')
->call('sendCorrection', $product->id)
->assertHasErrors(['curationNotes' => 'required']);
});
test('correction creates activity log entry with notes', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Pending,
'is_curated' => false,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('openCorrection', $product->id)
->set('curationNotes', 'Bilder bitte verbessern.')
->call('sendCorrection', $product->id)
->assertHasNoErrors();
$activity = ProductActivity::where('product_id', $product->id)->first();
expect($activity)->not->toBeNull();
expect($activity->action)->toBe('correction');
expect($activity->note)->toBe('Bilder bitte verbessern.');
expect($activity->user_id)->toBe($admin->id);
});
// --- Rejection Tests ---
test('admin can reject a product with reason', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Pending,
'is_curated' => false,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('openRejection', $product->id)
->assertSet('rejectingProductId', $product->id)
->set('rejectionReason', 'Produkt entspricht nicht den Qualitätsstandards.')
->call('reject', $product->id)
->assertHasNoErrors();
$product->refresh();
expect($product->status)->toBe(ProductStatus::Archived);
expect($product->is_curated)->toBeFalse();
expect($product->curation_notes)->toBe('Produkt entspricht nicht den Qualitätsstandards.');
});
test('rejection requires a reason', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Pending,
'is_curated' => false,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('openRejection', $product->id)
->set('rejectionReason', '')
->call('reject', $product->id)
->assertHasErrors(['rejectionReason' => 'required']);
});
test('reject creates activity log entry with reason', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Pending,
'is_curated' => false,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('openRejection', $product->id)
->set('rejectionReason', 'Nicht geeignet für Plattform.')
->call('reject', $product->id)
->assertHasNoErrors();
$activity = ProductActivity::where('product_id', $product->id)->first();
expect($activity)->not->toBeNull();
expect($activity->action)->toBe('rejected');
expect($activity->note)->toBe('Nicht geeignet für Plattform.');
expect($activity->user_id)->toBe($admin->id);
});
// --- Archive / Sold Tests ---
test('admin can archive a product from list', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Active,
'is_curated' => true,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('archiveProduct', $product->id)
->assertHasNoErrors();
$product->refresh();
expect($product->status)->toBe(ProductStatus::Archived);
});
test('admin can mark product as sold from list', function () {
$admin = makeAdminUser();
$product = Product::factory()->create([
'status' => ProductStatus::Active,
'is_curated' => true,
]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('markAsSold', $product->id)
->assertHasNoErrors();
$product->refresh();
expect($product->status)->toBe(ProductStatus::Sold);
});
test('archive creates activity log entry', function () {
$admin = makeAdminUser();
$product = Product::factory()->create(['status' => ProductStatus::Active]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('archiveProduct', $product->id);
$activity = ProductActivity::where('product_id', $product->id)->first();
expect($activity)->not->toBeNull();
expect($activity->action)->toBe('archived');
});
test('mark as sold creates activity log entry', function () {
$admin = makeAdminUser();
$product = Product::factory()->create(['status' => ProductStatus::Active]);
$this->actingAs($admin);
Volt::test('admin.products.index')
->call('markAsSold', $product->id);
$activity = ProductActivity::where('product_id', $product->id)->first();
expect($activity)->not->toBeNull();
expect($activity->action)->toBe('sold');
});
// --- Filter Tests ---
test('admin product list shows all products', function () {
$admin = makeAdminUser();
$product = Product::factory()->create(['name' => 'Test Lampe Deluxe']);
$this->actingAs($admin);
Volt::test('admin.products.index')
->assertSeeText('Test Lampe Deluxe');
});
test('admin product list can filter by status', function () {
$admin = makeAdminUser();
Product::factory()->create(['status' => ProductStatus::Pending, 'name' => 'Pending Product']);
Product::factory()->create(['status' => ProductStatus::Active, 'name' => 'Active Product']);
$this->actingAs($admin);
Volt::test('admin.products.index')
->set('statusFilter', 'pending')
->assertSeeText('Pending Product')
->assertDontSeeText('Active Product');
});
test('admin product list can search by name', function () {
$admin = makeAdminUser();
Product::factory()->create(['name' => 'Designer Stuhl']);
Product::factory()->create(['name' => 'Vintage Lampe']);
$this->actingAs($admin);
Volt::test('admin.products.index')
->set('search', 'Stuhl')
->assertSeeText('Designer Stuhl')
->assertDontSeeText('Vintage Lampe');
});
test('admin product list can filter by partner', function () {
$admin = makeAdminUser();
$partner1 = Partner::factory()->create(['company_name' => 'Möbelhaus A']);
$partner2 = Partner::factory()->create(['company_name' => 'Möbelhaus B']);
Product::factory()->create(['partner_id' => $partner1->id, 'name' => 'Stuhl A']);
Product::factory()->create(['partner_id' => $partner2->id, 'name' => 'Stuhl B']);
$this->actingAs($admin);
Volt::test('admin.products.index')
->set('partnerFilter', $partner1->id)
->assertSeeText('Stuhl A')
->assertDontSeeText('Stuhl B');
});
test('retailer cannot approve products', function () {
$retailer = User::factory()->create();
$retailer->assignRole('Retailer');
$this->actingAs($retailer);
Volt::test('admin.products.index')
->assertForbidden();
});
// --- Product list archive/sold from retailer index ---
test('retailer can archive own product from product list', function () {
Role::findOrCreate('Retailer');
$partner = Partner::factory()->create();
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
$product = Product::factory()->create([
'partner_id' => $partner->id,
'status' => ProductStatus::Active,
]);
$this->actingAs($user);
Volt::test('products.index')
->call('archiveProduct', $product->id)
->assertHasNoErrors();
$product->refresh();
expect($product->status)->toBe(ProductStatus::Archived);
});
test('retailer can mark own product as sold from product list', function () {
Role::findOrCreate('Retailer');
$partner = Partner::factory()->create();
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
$product = Product::factory()->create([
'partner_id' => $partner->id,
'status' => ProductStatus::Active,
]);
$this->actingAs($user);
Volt::test('products.index')
->call('markAsSold', $product->id)
->assertHasNoErrors();
$product->refresh();
expect($product->status)->toBe(ProductStatus::Sold);
});
// --- Curation notes display in edit form ---
test('correction notes are displayed in standard product edit', function () {
Role::findOrCreate('Retailer');
$partner = Partner::factory()->create();
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
$product = Product::factory()->create([
'partner_id' => $partner->id,
'status' => ProductStatus::Correction,
'product_type' => ProductType::SmartOrder,
'curation_notes' => 'Bitte bessere Bilder hochladen.',
]);
$this->actingAs($user);
Volt::test('products.form-standard', ['product' => $product])
->assertSeeText('Korrektur erforderlich')
->assertSeeText('Bitte bessere Bilder hochladen.');
});
test('rejection notes are displayed in teaser product edit', function () {
Role::findOrCreate('Retailer');
$partner = Partner::factory()->create();
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
$product = Product::factory()->create([
'partner_id' => $partner->id,
'status' => ProductStatus::Archived,
'product_type' => ProductType::LocalStock,
'curation_notes' => 'Produkt nicht für die Plattform geeignet.',
]);
$this->actingAs($user);
Volt::test('products.form-teaser', ['product' => $product])
->assertSeeText('Produkt abgelehnt')
->assertSeeText('Produkt nicht für die Plattform geeignet.');
});