1100 lines
40 KiB
PHP
1100 lines
40 KiB
PHP
<?php
|
|
|
|
use App\Enums\OwnerVerificationAction;
|
|
use App\Enums\OwnerVerificationStatus;
|
|
use App\Enums\Permission as PermissionEnum;
|
|
use App\Models\Category;
|
|
use App\Models\OwnerVerificationRequest;
|
|
use App\Models\Product;
|
|
use App\Models\ProductPrice;
|
|
use App\Models\ProductVariant;
|
|
use App\Models\User;
|
|
use Database\Seeders\RolePermissionSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->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 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()->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);
|
|
});
|
|
});
|