seed(RolePermissionSeeder::class); Storage::fake('public'); }); // ─── Helper ─────────────────────────────────────────────── function createProductUserWithPermission(PermissionEnum ...$permissions): User { $user = User::factory()->create(); $user->givePermissionTo( array_merge( [PermissionEnum::DASHBOARD_VIEW->value], array_map(fn (PermissionEnum $p) => $p->value, $permissions) ) ); $user->forgetCachedPermissions(); return $user; } function createProductWithVariants(?Category $category = null): Product { $category ??= Category::factory()->create(); $product = Product::factory()->create(); $product->categories()->attach($category->id); $variants = ProductVariant::factory()->count(2)->create(['product_id' => $product->id]); foreach ($variants as $variant) { $variant->addMedia(UploadedFile::fake()->image("{$variant->name}.jpg", 100, 100)) ->toMediaCollection('images'); } return $product; } function createProductVerifierUser(): User { $user = User::factory()->create(); $user->givePermissionTo([ PermissionEnum::DASHBOARD_VIEW->value, PermissionEnum::OWNER_VERIFICATIONS_VIEW->value, PermissionEnum::OWNER_VERIFICATIONS_VERIFY->value, PermissionEnum::OWNER_VERIFICATIONS_REJECT->value, ]); $user->forgetCachedPermissions(); return $user; } function variantWithImage(string $name = 'All Size', int $stock = 10): array { $disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public'); $key = 'products/'.Str::uuid().'.jpg'; $imageContent = UploadedFile::fake()->image("{$name}.jpg", 100, 100)->get(); Storage::disk($disk)->put($key, $imageContent); return [ 'name' => $name, 'stock' => $stock, 'retail_stock' => 0, 's3_keys' => [$key], 'prices' => defaultPrices(), ]; } function defaultPrices(): array { return [ 'distributor' => 100000, 'agent' => 110000, 'sub_agent' => 120000, 'grosir' => 130000, 'retail' => 150000, 'tiktok' => 160000, 'shopee' => 165000, 'harga_modal' => 80000, ]; } function variantUpdateData(int $id, string $name = 'Updated Variant', int $stock = 20): array { return [ 'id' => $id, 'name' => $name, 'stock' => $stock, 'retail_stock' => 0, 'prices' => defaultPrices(), ]; } // ─── Index ──────────────────────────────────────────────── describe('Product Index', function () { test('authenticated user with permission can view product index', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); createProductWithVariants(); $this->actingAs($user) ->get(route('admin.master.products.index')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.master.products.index')) ->assertRedirect(route('login')); }); test('user without permission is forbidden', function () { $user = User::factory()->create(); $this->actingAs($user) ->get(route('admin.master.products.index')) ->assertForbidden(); }); test('index displays products with variants and categories', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); createProductWithVariants(); createProductWithVariants(); $this->actingAs($user) ->get(route('admin.master.products.index')) ->assertOk(); }); test('index can search products by name', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $category = Category::factory()->create(); $product = Product::factory()->create(['name' => 'Batik Modern']); $product->categories()->attach($category->id); ProductVariant::factory()->create(['product_id' => $product->id]); $this->actingAs($user) ->get(route('admin.master.products.index', ['search' => 'Batik'])) ->assertOk(); }); test('index can search products by id', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $category = Category::factory()->create(); $product1 = Product::factory()->create(['name' => 'Batik Modern']); $product1->categories()->attach($category->id); ProductVariant::factory()->create(['product_id' => $product1->id]); $product2 = Product::factory()->create(['name' => 'Baju Sutra']); $product2->categories()->attach($category->id); ProductVariant::factory()->create(['product_id' => $product2->id]); $response = $this->actingAs($user) ->get(route('admin.master.products.index', ['search_id' => $product1->id])); $response->assertOk(); $response->assertInertia(fn ($page) => $page ->has('products.data', 1) ->where('products.data.0.id', $product1->id) ); }); test('index can filter by active status', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); createProductWithVariants(); $this->actingAs($user) ->get(route('admin.master.products.index', ['is_active' => '1'])) ->assertOk(); }); test('index can filter by category', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $category = Category::factory()->create(); createProductWithVariants($category); $this->actingAs($user) ->get(route('admin.master.products.index', ['category_id' => $category->id])) ->assertOk(); }); }); // ─── Create ─────────────────────────────────────────────── describe('Product Create', function () { test('authenticated user with permission can view create form', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $this->actingAs($user) ->get(route('admin.master.products.create')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.master.products.create')) ->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $this->actingAs($user) ->get(route('admin.master.products.create')) ->assertForbidden(); }); }); // ─── Store ──────────────────────────────────────────────── describe('Product Store', function () { test('authenticated user with permission can submit product creation request', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Baru', 'description' => 'Deskripsi produk', 'category_ids' => [$category->id], 'variants' => [ variantWithImage('All Size', 10), ], ]) ->assertRedirect(route('admin.master.products.index')); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_type' => Product::class, 'action' => OwnerVerificationAction::CREATE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); $this->assertDatabaseHas('products', [ 'name' => 'Produk Baru', 'is_active' => false, ]); }); test('guest cannot create a product', function () { $category = Category::factory()->create(); $this->post(route('admin.master.products.store'), [ 'name' => 'Produk Baru', 'category_ids' => [$category->id], 'variants' => [variantWithImage()], ])->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Baru', 'category_ids' => [$category->id], 'variants' => [variantWithImage()], ]) ->assertForbidden(); }); test('name is required', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => '', 'category_ids' => [$category->id], 'variants' => [variantWithImage()], ]) ->assertSessionHasErrors('name'); }); test('name must not exceed 200 characters', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => str_repeat('a', 201), 'category_ids' => [$category->id], 'variants' => [variantWithImage()], ]) ->assertSessionHasErrors('name'); }); test('category_ids is required', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Baru', 'category_ids' => [], 'variants' => [variantWithImage()], ]) ->assertSessionHasErrors('category_ids'); }); test('category must exist', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Baru', 'category_ids' => [99999], 'variants' => [variantWithImage()], ]) ->assertSessionHasErrors('category_ids.0'); }); test('variants is required', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Baru', 'category_ids' => [$category->id], 'variants' => [], ]) ->assertSessionHasErrors('variants'); }); test('variant name is required', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Baru', 'category_ids' => [$category->id], 'variants' => [variantWithImage('', 10)], ]) ->assertSessionHasErrors('variants.0.name'); }); test('variant stock is required', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Baru', 'category_ids' => [$category->id], 'variants' => [['name' => 'All Size', 'stock' => '', 'images' => [UploadedFile::fake()->image('test.jpg')]]], ]) ->assertSessionHasErrors('variants.0.stock'); }); test('variant stock must be non-negative integer', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Baru', 'category_ids' => [$category->id], 'variants' => [variantWithImage('All Size', -5)], ]) ->assertSessionHasErrors('variants.0.stock'); }); test('approving create request activates product with variants and categories', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $verifier = createProductVerifierUser(); $cat1 = Category::factory()->create(); $cat2 = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Lengkap', 'description' => 'Deskripsi lengkap', 'category_ids' => [$cat1->id, $cat2->id], 'variants' => [ variantWithImage('S', 5), variantWithImage('M', 10), variantWithImage('L', 15), ], ]); $product = Product::where('name', 'Produk Lengkap')->first(); expect($product)->not->toBeNull(); expect($product->is_active)->toBeFalse(); expect($product->categories)->toHaveCount(2); expect($product->variants)->toHaveCount(3); approveLatestOwnerVerificationRequest($verifier); $product->refresh(); expect($product->is_active)->toBeTrue(); }); test('approving create request auto-generates product slug', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $verifier = createProductVerifierUser(); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Batik Modern Elegan', 'category_ids' => [$category->id], 'variants' => [variantWithImage()], ]); $this->assertDatabaseHas('products', [ 'name' => 'Batik Modern Elegan', 'slug' => 'batik-modern-elegan', 'is_active' => false, ]); approveLatestOwnerVerificationRequest($verifier); $this->assertDatabaseHas('products', [ 'name' => 'Batik Modern Elegan', 'slug' => 'batik-modern-elegan', 'is_active' => true, ]); }); test('rejecting create request deletes product', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE); $verifier = createProductVerifierUser(); $category = Category::factory()->create(); $this->actingAs($user) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Ditolak', 'category_ids' => [$category->id], 'variants' => [variantWithImage()], ]); $product = Product::where('name', 'Produk Ditolak')->first(); expect($product)->not->toBeNull(); $verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail(); $this->actingAs($verifier) ->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [ 'reason' => 'Tidak sesuai standar', ]) ->assertRedirect(); $this->assertSoftDeleted('products', ['id' => $product->id]); }); }); // ─── Edit ───────────────────────────────────────────────── describe('Product Edit', function () { test('authenticated user with permission can view edit form', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE); $product = createProductWithVariants(); $this->actingAs($user) ->get(route('admin.master.products.edit', $product)) ->assertOk(); }); test('guest is redirected to login', function () { $product = createProductWithVariants(); $this->get(route('admin.master.products.edit', $product)) ->assertRedirect(route('login')); }); test('user without update permission is forbidden', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $product = createProductWithVariants(); $this->actingAs($user) ->get(route('admin.master.products.edit', $product)) ->assertForbidden(); }); test('edit is blocked when product has pending request', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE); $product = createProductWithVariants(); $category = $product->categories->first(); $variant = $product->variants->first(); $this->actingAs($user) ->put(route('admin.master.products.update', $product), [ 'name' => 'Nama Diubah', 'category_ids' => [$category->id], 'variants' => [ variantUpdateData($variant->id, $variant->name, $variant->stock), ], ]); $this->actingAs($user) ->get(route('admin.master.products.edit', $product)) ->assertRedirect(route('admin.master.products.index')); }); }); // ─── Update ─────────────────────────────────────────────── describe('Product Update', function () { test('authenticated user with permission can submit product update request', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE); $product = createProductWithVariants(); $category = $product->categories->first(); $variant = $product->variants->first(); $this->actingAs($user) ->put(route('admin.master.products.update', $product), [ 'name' => 'Nama Diubah', 'description' => 'Deskripsi baru', 'category_ids' => [$category->id], 'variants' => [ variantUpdateData($variant->id, 'New Variant', 20), ], ]) ->assertRedirect(route('admin.master.products.index')); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_id' => $product->id, 'subject_type' => Product::class, 'action' => OwnerVerificationAction::UPDATE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); expect($product->fresh()->name)->not->toBe('Nama Diubah'); }); test('approving update request applies product changes', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE); $verifier = createProductVerifierUser(); $product = createProductWithVariants(); $category = $product->categories->first(); $variant = $product->variants->first(); $this->actingAs($user) ->put(route('admin.master.products.update', $product), [ 'name' => 'Nama Diubah', 'description' => 'Deskripsi baru', 'category_ids' => [$category->id], 'variants' => [ variantUpdateData($variant->id, 'New Variant', 20), ], ]); approveLatestOwnerVerificationRequest($verifier); expect($product->fresh()->name)->toBe('Nama Diubah'); expect($variant->fresh()->name)->toBe('New Variant'); }); test('guest cannot update a product', function () { $product = createProductWithVariants(); $variant = $product->variants->first(); $this->put(route('admin.master.products.update', $product), [ 'name' => 'Nama Baru', 'category_ids' => [$product->categories->first()->id], 'variants' => [variantUpdateData($variant->id, $variant->name, $variant->stock)], ])->assertRedirect(route('login')); }); test('user without update permission is forbidden', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $product = createProductWithVariants(); $variant = $product->variants->first(); $this->actingAs($user) ->put(route('admin.master.products.update', $product), [ 'name' => 'Nama Baru', 'category_ids' => [$product->categories->first()->id], 'variants' => [variantUpdateData($variant->id, $variant->name, $variant->stock)], ]) ->assertForbidden(); }); test('name is required on update', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE); $product = createProductWithVariants(); $category = $product->categories->first(); $variant = $product->variants->first(); $this->actingAs($user) ->put(route('admin.master.products.update', $product), [ 'name' => '', 'category_ids' => [$category->id], 'variants' => [ variantUpdateData($variant->id, $variant->name, $variant->stock), ], ]) ->assertSessionHasErrors('name'); }); test('category_ids is required on update', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE); $product = createProductWithVariants(); $variant = $product->variants->first(); $this->actingAs($user) ->put(route('admin.master.products.update', $product), [ 'name' => 'Nama Produk', 'category_ids' => [], 'variants' => [ variantUpdateData($variant->id, $variant->name, $variant->stock), ], ]) ->assertSessionHasErrors('category_ids'); }); test('approving update request removes unsubmitted variants', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE); $verifier = createProductVerifierUser(); $product = createProductWithVariants(); $category = $product->categories->first(); $variantToKeep = $product->variants->first(); $this->actingAs($user) ->put(route('admin.master.products.update', $product), [ 'name' => 'Updated Product', 'category_ids' => [$category->id], 'variants' => [ variantUpdateData($variantToKeep->id, 'Kept Variant', 5), ], ]); approveLatestOwnerVerificationRequest($verifier); $product->refresh(); expect($product->variants)->toHaveCount(1); expect($product->variants->first()->name)->toBe('Kept Variant'); }); }); // ─── Toggle Status ──────────────────────────────────────── describe('Product Toggle Status', function () { test('authenticated user with permission can submit toggle status request', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_TOGGLE_STATUS); $product = createProductWithVariants(); $this->actingAs($user) ->patch(route('admin.master.products.toggle_status', $product), [ 'is_active' => false, ]) ->assertRedirect(); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_id' => $product->id, 'subject_type' => Product::class, 'action' => OwnerVerificationAction::TOGGLE_STATUS->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); expect($product->fresh()->is_active)->toBeTrue(); }); test('product index exposes display is active for pending toggle status request', function () { $user = createProductUserWithPermission( PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_TOGGLE_STATUS, ); $product = createProductWithVariants(); $product->update(['is_active' => true]); $this->actingAs($user) ->patch(route('admin.master.products.toggle_status', $product), [ 'is_active' => false, ]) ->assertRedirect(); $response = $this->actingAs($user) ->get(route('admin.master.products.index')); $response->assertOk(); $response->assertInertia(fn ($page) => $page ->where('products.data.0.id', $product->id) ->where('products.data.0.is_active', true) ->where('products.data.0.display_is_active', false) ->where('products.data.0.has_pending_request', true) ->where('products.data.0.pending_request_action', OwnerVerificationAction::TOGGLE_STATUS->value) ); }); test('approving toggle status request updates product status', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_TOGGLE_STATUS); $verifier = createProductVerifierUser(); $product = createProductWithVariants(); $this->actingAs($user) ->patch(route('admin.master.products.toggle_status', $product), [ 'is_active' => false, ]); approveLatestOwnerVerificationRequest($verifier); expect($product->fresh()->is_active)->toBeFalse(); }); test('guest cannot toggle product status', function () { $product = createProductWithVariants(); $this->patch(route('admin.master.products.toggle_status', $product), [ 'is_active' => false, ])->assertRedirect(route('login')); }); test('user without toggle permission is forbidden', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $product = createProductWithVariants(); $this->actingAs($user) ->patch(route('admin.master.products.toggle_status', $product), [ 'is_active' => false, ]) ->assertForbidden(); }); test('is_active field is required', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_TOGGLE_STATUS); $product = createProductWithVariants(); $this->actingAs($user) ->patch(route('admin.master.products.toggle_status', $product), [ 'is_active' => null, ]) ->assertSessionHasErrors('is_active'); }); }); // ─── Destroy ────────────────────────────────────────────── describe('Product Destroy', function () { test('authenticated user with permission can submit delete request', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_DELETE); $product = createProductWithVariants(); $this->actingAs($user) ->delete(route('admin.master.products.destroy', $product)) ->assertRedirect(route('admin.master.products.index')); $this->assertDatabaseHas('owner_verification_requests', [ 'subject_id' => $product->id, 'subject_type' => Product::class, 'action' => OwnerVerificationAction::DELETE->value, 'status' => OwnerVerificationStatus::PENDING->value, ]); $this->assertNotSoftDeleted('products', ['id' => $product->id]); }); test('approving delete request soft deletes product and variants', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_DELETE); $verifier = createProductVerifierUser(); $product = createProductWithVariants(); $variantIds = $product->variants->pluck('id')->toArray(); $this->actingAs($user) ->delete(route('admin.master.products.destroy', $product)); approveLatestOwnerVerificationRequest($verifier); $this->assertSoftDeleted('products', ['id' => $product->id]); foreach ($variantIds as $variantId) { $this->assertSoftDeleted('product_variants', ['id' => $variantId]); } }); test('approving delete request detaches categories', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_DELETE); $verifier = createProductVerifierUser(); $product = createProductWithVariants(); $this->actingAs($user) ->delete(route('admin.master.products.destroy', $product)); approveLatestOwnerVerificationRequest($verifier); $this->assertDatabaseMissing('product_categories', ['product_id' => $product->id]); }); test('guest cannot delete a product', function () { $product = createProductWithVariants(); $this->delete(route('admin.master.products.destroy', $product)) ->assertRedirect(route('login')); $this->assertNotSoftDeleted('products', ['id' => $product->id]); }); test('user without delete permission is forbidden', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $product = createProductWithVariants(); $this->actingAs($user) ->delete(route('admin.master.products.destroy', $product)) ->assertForbidden(); $this->assertNotSoftDeleted('products', ['id' => $product->id]); }); }); // ─── Product Model ──────────────────────────────────────── describe('Product Model', function () { test('product uses soft deletes', function () { $product = Product::factory()->create(); $product->delete(); expect($product->trashed())->toBeTrue(); }); test('product can have categories', function () { $product = Product::factory()->create(); $category = Category::factory()->create(); $product->categories()->attach($category->id); expect($product->fresh()->categories)->toHaveCount(1); }); test('product can have variants', function () { $product = Product::factory()->create(); ProductVariant::factory()->count(3)->create(['product_id' => $product->id]); expect($product->fresh()->variants)->toHaveCount(3); }); test('product has is_active cast to boolean', function () { $product = Product::factory()->create(['is_active' => true]); expect($product->is_active)->toBeTrue(); $product->update(['is_active' => false]); expect($product->fresh()->is_active)->toBeFalse(); }); test('product has total_stock_formatted accessor', function () { $product = Product::factory()->create(); ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 10]); ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 20]); expect($product->fresh(['variants'])->total_stock_formatted)->toBe('30'); }); test('product has slug auto-generated', function () { $product = Product::create(['name' => 'Test Product Name']); expect($product->slug)->toBe('test-product-name'); }); }); // ─── ProductVariant Model ────────────────────────────────── describe('ProductVariant Model', function () { test('variant uses soft deletes', function () { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create(['product_id' => $product->id]); $variant->delete(); expect($variant->trashed())->toBeTrue(); }); test('variant has stock cast to integer', function () { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 50]); expect($variant->stock)->toBeInt(); expect($variant->stock)->toBe(50); }); test('variant has formatted stock accessor', function () { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 1500]); expect($variant->stock_formatted)->toBe('1.500'); }); test('variant belongs to product', function () { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create(['product_id' => $product->id]); expect($variant->product)->not->toBeNull(); expect($variant->product->id)->toBe($product->id); }); test('variant can have prices', function () { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create(['product_id' => $product->id]); ProductPrice::factory()->count(3)->create(['variant_id' => $variant->id]); expect($variant->fresh()->prices)->toHaveCount(3); }); test('variant has minStock constant', function () { expect(ProductVariant::minStock())->toBe(5); }); }); // ─── ProductPrice Model ──────────────────────────────────── describe('ProductPrice Model', function () { test('price has price cast to integer', function () { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create(['product_id' => $product->id]); $price = ProductPrice::factory()->create(['variant_id' => $variant->id, 'price' => 150000]); expect($price->price)->toBeInt(); expect($price->price)->toBe(150000); }); test('price has formatted price accessor', function () { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create(['product_id' => $product->id]); $price = ProductPrice::factory()->create(['variant_id' => $variant->id, 'price' => 1500000]); expect($price->price_formatted)->toBe('Rp 1.500.000'); }); test('price has price_input accessor', function () { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create(['product_id' => $product->id]); $price = ProductPrice::factory()->create(['variant_id' => $variant->id, 'price' => 200000]); expect($price->price_input)->toBe('200000'); }); test('price belongs to variant', function () { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create(['product_id' => $product->id]); $price = ProductPrice::factory()->create(['variant_id' => $variant->id]); expect($price->variant)->not->toBeNull(); expect($price->variant->id)->toBe($variant->id); }); }); // ─── Owner Direct Actions ────────────────────────────────── describe('Owner Direct Actions', function () { test('owner can create product directly without verification', function () { $owner = createProductUserWithPermission( PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_CREATE, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $category = Category::factory()->create(); $this->actingAs($owner) ->post(route('admin.master.products.store'), [ 'name' => 'Produk Owner', 'description' => 'Deskripsi', 'category_ids' => [$category->id], 'variants' => [ variantWithImage('All Size', 10), ], ]) ->assertRedirect(route('admin.master.products.index')); $product = Product::where('name', 'Produk Owner')->first(); expect($product)->not->toBeNull(); expect($product->is_active)->toBeTrue(); $this->assertDatabaseMissing('owner_verification_requests', [ 'subject_id' => $product->id, 'subject_type' => Product::class, ]); }); test('owner can update product directly without verification', function () { $owner = createProductUserWithPermission( PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $product = createProductWithVariants(); $category = $product->categories->first(); $variant = $product->variants->first(); $this->actingAs($owner) ->put(route('admin.master.products.update', $product), [ 'name' => 'Produk Updated', 'description' => 'Deskripsi baru', 'category_ids' => [$category->id], 'variants' => [ variantUpdateData($variant->id, 'Updated Variant', 25), ], ]) ->assertRedirect(route('admin.master.products.index')); expect($product->fresh()->name)->toBe('Produk Updated'); expect($variant->fresh()->name)->toBe('Updated Variant'); }); test('owner can delete product directly without verification', function () { $owner = createProductUserWithPermission( PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_DELETE, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $product = createProductWithVariants(); $this->actingAs($owner) ->delete(route('admin.master.products.destroy', $product)) ->assertRedirect(route('admin.master.products.index')); $this->assertSoftDeleted('products', ['id' => $product->id]); }); test('owner can toggle product status directly without verification', function () { $owner = createProductUserWithPermission( PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_TOGGLE_STATUS, PermissionEnum::OWNER_VERIFICATIONS_VERIFY, ); $product = createProductWithVariants(); $this->actingAs($owner) ->patch(route('admin.master.products.toggle_status', $product), [ 'is_active' => false, ]); expect($product->fresh()->is_active)->toBeFalse(); }); }); // ─── Reject Update Request ───────────────────────────────── describe('Reject Update Request', function () { test('rejecting update request rolls back to old values', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW, PermissionEnum::PRODUCTS_UPDATE); $verifier = createProductVerifierUser(); $product = createProductWithVariants(); $category = $product->categories->first(); $variant = $product->variants->first(); $originalName = $product->name; $originalVariantName = $variant->name; $this->actingAs($user) ->put(route('admin.master.products.update', $product), [ 'name' => 'Nama Diubah', 'description' => 'Deskripsi baru', 'category_ids' => [$category->id], 'variants' => [ variantUpdateData($variant->id, 'New Variant', 20), ], ]); $verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail(); $this->actingAs($verifier) ->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [ 'reason' => 'Tidak sesuai standar', ]) ->assertRedirect(); expect($product->fresh()->name)->toBe($originalName); expect($variant->fresh()->name)->toBe($originalVariantName); }); }); // ─── Show JSON ──────────────────────────────────────────── describe('Product Show JSON', function () { test('authenticated user with permission can get product details in JSON', function () { $user = createProductUserWithPermission(PermissionEnum::PRODUCTS_VIEW); $product = createProductWithVariants(); $response = $this->actingAs($user) ->get(route('admin.master.products.show', $product)) ->assertOk(); $response->assertJsonStructure([ 'id', 'name', 'description', 'categories' => [ '*' => [ 'id', 'name', ], ], 'variants' => [ '*' => [ 'id', 'product_id', 'name', 'stock', 'reject_stock', 'retail_stock', 'prices' => [ '*' => [ 'id', 'variant_id', 'type', 'price', ], ], 'images', ], ], ]); $data = $response->json(); expect($data['id'])->toBe($product->id); expect($data['variants'])->toHaveCount(2); }); test('guest cannot get product details in JSON', function () { $product = createProductWithVariants(); $this->get(route('admin.master.products.show', $product)) ->assertRedirect(route('login')); }); test('user without view permission cannot get product details in JSON', function () { $user = User::factory()->create(); $product = createProductWithVariants(); $this->actingAs($user) ->get(route('admin.master.products.show', $product)) ->assertForbidden(); }); });