1032 lines
40 KiB
PHP
1032 lines
40 KiB
PHP
<?php
|
|
|
|
use App\Enums\CuttingStatus;
|
|
use App\Enums\OwnerVerificationAction;
|
|
use App\Enums\OwnerVerificationStatus;
|
|
use App\Enums\Permission as PermissionEnum;
|
|
use App\Enums\RawMaterialUnit;
|
|
use App\Models\Cutting;
|
|
use App\Models\CuttingMaterial;
|
|
use App\Models\OwnerVerificationRequest;
|
|
use App\Models\RawMaterial;
|
|
use App\Models\RawMaterialPrice;
|
|
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 createRawMaterialUserWithPermission(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 createRawMaterialWithPrices(): RawMaterial
|
|
{
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
|
|
$prices = RawMaterialPrice::factory()->count(2)->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
foreach ($prices as $price) {
|
|
$price->addMedia(UploadedFile::fake()->image("{$price->variant}.jpg", 100, 100))
|
|
->toMediaCollection('images');
|
|
}
|
|
|
|
return $rawMaterial;
|
|
}
|
|
|
|
function priceWithImage(string $variant = 'Default', int $price = 50000, float $stock = 10): array
|
|
{
|
|
$disk = config('filesystems.default') === 's3' ? 's3' : config('filesystems.default', 'public');
|
|
$key = 'raw-materials/'.Str::uuid().'.jpg';
|
|
$imageContent = UploadedFile::fake()->image("{$variant}.jpg", 100, 100)->get();
|
|
Storage::disk($disk)->put($key, $imageContent);
|
|
|
|
return [
|
|
'variant' => $variant,
|
|
'price' => $price,
|
|
'stock' => $stock,
|
|
's3_keys' => [$key],
|
|
];
|
|
}
|
|
|
|
function createRawMaterialVerifierUser(): 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;
|
|
}
|
|
|
|
// ─── Index ────────────────────────────────────────────────
|
|
|
|
describe('Raw Material Index', function () {
|
|
test('authenticated user with permission can view raw material index', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
createRawMaterialWithPrices();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('guest is redirected to login', function () {
|
|
$this->get(route('admin.master.raw_materials.index'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without permission is forbidden', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.index'))
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('index displays raw materials with prices', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
createRawMaterialWithPrices();
|
|
createRawMaterialWithPrices();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('index can search raw materials by name', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create(['name' => 'Kain Batik']);
|
|
RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.index', ['search' => 'Kain']))
|
|
->assertOk();
|
|
});
|
|
|
|
test('index can search raw materials by id', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$rawMaterial1 = RawMaterial::factory()->create(['name' => 'Kain Batik']);
|
|
RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial1->id]);
|
|
|
|
$rawMaterial2 = RawMaterial::factory()->create(['name' => 'Sutra Premium']);
|
|
RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial2->id]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.index', ['search_id' => $rawMaterial1->id]));
|
|
|
|
$response->assertOk();
|
|
$response->assertInertia(fn ($page) => $page
|
|
->has('rawMaterials.data', 1)
|
|
->where('rawMaterials.data.0.id', $rawMaterial1->id)
|
|
);
|
|
});
|
|
|
|
test('index can filter by active status', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
createRawMaterialWithPrices();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.index', ['is_active' => '1']))
|
|
->assertOk();
|
|
});
|
|
|
|
test('index can filter by stock status (out of stock)', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$inStock = RawMaterial::factory()->create();
|
|
RawMaterialPrice::factory()->create(['raw_material_id' => $inStock->id, 'stock' => 10]);
|
|
|
|
$outOfStock = RawMaterial::factory()->create();
|
|
RawMaterialPrice::factory()->create(['raw_material_id' => $outOfStock->id, 'stock' => 0]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.index', ['stock_status' => 'out_of_stock']));
|
|
|
|
$response->assertOk();
|
|
$response->assertInertia(fn ($page) => $page
|
|
->has('rawMaterials.data', 1)
|
|
->where('rawMaterials.data.0.id', $outOfStock->id)
|
|
);
|
|
});
|
|
|
|
test('index can filter by stock status (low stock)', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$yardNormal = RawMaterial::factory()->create(['unit' => RawMaterialUnit::YARD->value]);
|
|
RawMaterialPrice::factory()->create(['raw_material_id' => $yardNormal->id, 'stock' => 25]);
|
|
|
|
$yardLow = RawMaterial::factory()->create(['unit' => RawMaterialUnit::YARD->value]);
|
|
RawMaterialPrice::factory()->create(['raw_material_id' => $yardLow->id, 'stock' => 15]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.index', ['stock_status' => 'low_stock']));
|
|
|
|
$response->assertOk();
|
|
$response->assertInertia(fn ($page) => $page
|
|
->has('rawMaterials.data', 1)
|
|
->where('rawMaterials.data.0.id', $yardLow->id)
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── Create ───────────────────────────────────────────────
|
|
|
|
describe('Raw Material Create', function () {
|
|
test('authenticated user with permission can view create form', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.create'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('guest is redirected to login', function () {
|
|
$this->get(route('admin.master.raw_materials.create'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without create permission is forbidden', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.create'))
|
|
->assertForbidden();
|
|
});
|
|
});
|
|
|
|
// ─── Store ────────────────────────────────────────────────
|
|
|
|
describe('Raw Material Store', function () {
|
|
test('authenticated user with permission can create raw material', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [
|
|
priceWithImage('Merah', 75000, 50),
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.master.raw_materials.index'));
|
|
|
|
$this->assertDatabaseHas('raw_materials', [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'is_active' => true,
|
|
]);
|
|
});
|
|
|
|
test('guest cannot create a raw material', function () {
|
|
$this->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [priceWithImage()],
|
|
])->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without create permission is forbidden', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [priceWithImage()],
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('name is required', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => '',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [priceWithImage()],
|
|
])
|
|
->assertSessionHasErrors('name');
|
|
});
|
|
|
|
test('name must not exceed 200 characters', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => str_repeat('a', 201),
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [priceWithImage()],
|
|
])
|
|
->assertSessionHasErrors('name');
|
|
});
|
|
|
|
test('unit is required', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => '',
|
|
'prices' => [priceWithImage()],
|
|
])
|
|
->assertSessionHasErrors('unit');
|
|
});
|
|
|
|
test('unit must be a valid enum value', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => 'invalid_unit',
|
|
'prices' => [priceWithImage()],
|
|
])
|
|
->assertSessionHasErrors('unit');
|
|
});
|
|
|
|
test('prices is required', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [],
|
|
])
|
|
->assertSessionHasErrors('prices');
|
|
});
|
|
|
|
test('price variant is required', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [['variant' => '', 'price' => 50000, 'stock' => 10, 'images' => [UploadedFile::fake()->image('test.jpg')]]],
|
|
])
|
|
->assertSessionHasErrors('prices.0.variant');
|
|
});
|
|
|
|
test('price amount is required and must be greater than 0', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [priceWithImage('Merah', 0, 10)],
|
|
])
|
|
->assertSessionHasErrors('prices.0.price');
|
|
});
|
|
|
|
test('stock is required', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [['variant' => 'Merah', 'price' => 50000, 'stock' => '', 'images' => [UploadedFile::fake()->image('test.jpg')]]],
|
|
])
|
|
->assertSessionHasErrors('prices.0.stock');
|
|
});
|
|
|
|
test('stock must be non-negative', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [priceWithImage('Merah', 50000, -5)],
|
|
])
|
|
->assertSessionHasErrors('prices.0.stock');
|
|
});
|
|
|
|
test('price s3_keys is required', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Sutra',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [['variant' => 'Merah', 'price' => 50000, 'stock' => 10]],
|
|
])
|
|
->assertSessionHasErrors('prices.0.s3_keys');
|
|
});
|
|
|
|
test('creating raw material also creates prices', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Premium',
|
|
'unit' => RawMaterialUnit::YARD->value,
|
|
'prices' => [
|
|
priceWithImage('Merah', 75000, 50),
|
|
priceWithImage('Biru', 80000, 30),
|
|
],
|
|
]);
|
|
|
|
$rawMaterial = RawMaterial::where('name', 'Kain Premium')->first();
|
|
expect($rawMaterial)->not->toBeNull();
|
|
expect($rawMaterial->prices)->toHaveCount(2);
|
|
});
|
|
});
|
|
|
|
// ─── Edit ─────────────────────────────────────────────────
|
|
|
|
describe('Raw Material Edit', function () {
|
|
test('authenticated user with permission can view edit form', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.edit', $rawMaterial))
|
|
->assertOk();
|
|
});
|
|
|
|
test('guest is redirected to login', function () {
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$this->get(route('admin.master.raw_materials.edit', $rawMaterial))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without update permission is forbidden', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.edit', $rawMaterial))
|
|
->assertForbidden();
|
|
});
|
|
});
|
|
|
|
// ─── Update ───────────────────────────────────────────────
|
|
|
|
describe('Raw Material Update', function () {
|
|
test('authenticated user with permission can submit raw material update request', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$price = $rawMaterial->prices->first();
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
|
'name' => 'Nama Diubah',
|
|
'unit' => RawMaterialUnit::KILOGRAM->value,
|
|
'prices' => [
|
|
['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25],
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.master.raw_materials.index'));
|
|
|
|
$this->assertDatabaseHas('owner_verification_requests', [
|
|
'subject_id' => $rawMaterial->id,
|
|
'subject_type' => RawMaterial::class,
|
|
'action' => OwnerVerificationAction::UPDATE->value,
|
|
'status' => OwnerVerificationStatus::PENDING->value,
|
|
]);
|
|
|
|
expect($rawMaterial->fresh()->name)->not->toBe('Nama Diubah');
|
|
expect($price->fresh()->variant)->not->toBe('Putih');
|
|
});
|
|
|
|
test('approving update request applies raw material changes', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE);
|
|
$verifier = createRawMaterialVerifierUser();
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$price = $rawMaterial->prices->first();
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
|
'name' => 'Nama Diubah',
|
|
'unit' => RawMaterialUnit::KILOGRAM->value,
|
|
'prices' => [
|
|
['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25],
|
|
],
|
|
]);
|
|
|
|
approveLatestOwnerVerificationRequest($verifier);
|
|
|
|
expect($rawMaterial->fresh()->name)->toBe('Nama Diubah');
|
|
expect($price->fresh()->variant)->toBe('Putih');
|
|
});
|
|
|
|
test('guest cannot update a raw material', function () {
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$price = $rawMaterial->prices->first();
|
|
|
|
$this->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
|
'name' => 'Nama Baru',
|
|
'unit' => $rawMaterial->unit->value,
|
|
'prices' => [['id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'stock' => $price->stock]],
|
|
])->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without update permission is forbidden', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$price = $rawMaterial->prices->first();
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
|
'name' => 'Nama Baru',
|
|
'unit' => $rawMaterial->unit->value,
|
|
'prices' => [['id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'stock' => $price->stock]],
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('name is required on update', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$price = $rawMaterial->prices->first();
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
|
'name' => '',
|
|
'unit' => $rawMaterial->unit->value,
|
|
'prices' => [
|
|
['id' => $price->id, 'variant' => $price->variant, 'price' => $price->price, 'stock' => $price->stock],
|
|
],
|
|
])
|
|
->assertSessionHasErrors('name');
|
|
});
|
|
|
|
test('prices is required on update', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
|
'name' => 'Nama Bahan',
|
|
'unit' => $rawMaterial->unit->value,
|
|
'prices' => [],
|
|
])
|
|
->assertSessionHasErrors('prices');
|
|
});
|
|
|
|
test('unsubmitted prices are removed after approving update request', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE);
|
|
$verifier = createRawMaterialVerifierUser();
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$priceToKeep = $rawMaterial->prices->first();
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
|
'name' => 'Updated Material',
|
|
'unit' => $rawMaterial->unit->value,
|
|
'prices' => [
|
|
['id' => $priceToKeep->id, 'variant' => 'Kept', 'price' => 60000, 'stock' => 15],
|
|
],
|
|
]);
|
|
|
|
approveLatestOwnerVerificationRequest($verifier);
|
|
|
|
$rawMaterial->refresh();
|
|
expect($rawMaterial->prices)->toHaveCount(1);
|
|
expect($rawMaterial->prices->first()->variant)->toBe('Kept');
|
|
});
|
|
});
|
|
|
|
// ─── Toggle Status ────────────────────────────────────────
|
|
|
|
describe('Raw Material Toggle Status', function () {
|
|
test('authenticated user with permission can submit toggle status request', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [
|
|
'is_active' => false,
|
|
])
|
|
->assertRedirect();
|
|
|
|
$this->assertDatabaseHas('owner_verification_requests', [
|
|
'subject_id' => $rawMaterial->id,
|
|
'subject_type' => RawMaterial::class,
|
|
'action' => OwnerVerificationAction::TOGGLE_STATUS->value,
|
|
'status' => OwnerVerificationStatus::PENDING->value,
|
|
]);
|
|
|
|
expect($rawMaterial->fresh()->is_active)->toBeTrue();
|
|
});
|
|
|
|
test('raw material index exposes display is active for pending toggle status request', function () {
|
|
$user = createRawMaterialUserWithPermission(
|
|
PermissionEnum::RAW_MATERIALS_VIEW,
|
|
PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS,
|
|
);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create(['is_active' => true]);
|
|
|
|
$this->actingAs($user)
|
|
->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [
|
|
'is_active' => false,
|
|
])
|
|
->assertRedirect();
|
|
|
|
$response = $this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.index'));
|
|
|
|
$response->assertOk();
|
|
$response->assertInertia(fn ($page) => $page
|
|
->where('rawMaterials.data.0.id', $rawMaterial->id)
|
|
->where('rawMaterials.data.0.is_active', true)
|
|
->where('rawMaterials.data.0.display_is_active', false)
|
|
->where('rawMaterials.data.0.has_pending_request', true)
|
|
->where('rawMaterials.data.0.pending_request_action', OwnerVerificationAction::TOGGLE_STATUS->value)
|
|
);
|
|
});
|
|
|
|
test('approving toggle status request updates raw material status', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS);
|
|
$verifier = createRawMaterialVerifierUser();
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [
|
|
'is_active' => false,
|
|
]);
|
|
|
|
approveLatestOwnerVerificationRequest($verifier);
|
|
|
|
expect($rawMaterial->fresh()->is_active)->toBeFalse();
|
|
});
|
|
|
|
test('guest cannot toggle raw material status', function () {
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
|
|
$this->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [
|
|
'is_active' => false,
|
|
])->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without toggle permission is forbidden', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [
|
|
'is_active' => false,
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('is_active field is required', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [
|
|
'is_active' => null,
|
|
])
|
|
->assertSessionHasErrors('is_active');
|
|
});
|
|
});
|
|
|
|
// ─── Destroy ──────────────────────────────────────────────
|
|
|
|
describe('Raw Material Destroy', function () {
|
|
test('authenticated user with permission can delete raw material', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$priceIds = $rawMaterial->prices->pluck('id')->toArray();
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.master.raw_materials.destroy', $rawMaterial))
|
|
->assertRedirect(route('admin.master.raw_materials.index'));
|
|
|
|
$this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]);
|
|
|
|
foreach ($priceIds as $priceId) {
|
|
$this->assertSoftDeleted('raw_material_prices', ['id' => $priceId]);
|
|
}
|
|
});
|
|
|
|
test('guest cannot delete a raw material', function () {
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$this->delete(route('admin.master.raw_materials.destroy', $rawMaterial))
|
|
->assertRedirect(route('login'));
|
|
|
|
$this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]);
|
|
});
|
|
|
|
test('user without delete permission is forbidden', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.master.raw_materials.destroy', $rawMaterial))
|
|
->assertForbidden();
|
|
|
|
$this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]);
|
|
});
|
|
|
|
test('cannot delete raw material that is used in an in-progress cutting', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$price = $rawMaterial->prices->first();
|
|
|
|
$cutting = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS]);
|
|
CuttingMaterial::factory()->create([
|
|
'cutting_id' => $cutting->id,
|
|
'raw_material_price_id' => $price->id,
|
|
'user_id' => null,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.master.raw_materials.destroy', $rawMaterial))
|
|
->assertRedirect();
|
|
|
|
$this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]);
|
|
});
|
|
|
|
test('can delete raw material that is used in a completed cutting', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$price = $rawMaterial->prices->first();
|
|
|
|
$cutting = Cutting::factory()->create(['status' => CuttingStatus::COMPLETED]);
|
|
CuttingMaterial::factory()->create([
|
|
'cutting_id' => $cutting->id,
|
|
'raw_material_price_id' => $price->id,
|
|
'user_id' => null,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.master.raw_materials.destroy', $rawMaterial))
|
|
->assertRedirect(route('admin.master.raw_materials.index'));
|
|
|
|
$this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]);
|
|
});
|
|
});
|
|
|
|
// ─── Raw Material Model ───────────────────────────────────
|
|
|
|
describe('Raw Material Model', function () {
|
|
test('raw material uses soft deletes', function () {
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
|
|
$rawMaterial->delete();
|
|
|
|
expect($rawMaterial->trashed())->toBeTrue();
|
|
});
|
|
|
|
test('raw material can have prices', function () {
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
RawMaterialPrice::factory()->count(3)->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
expect($rawMaterial->fresh()->prices)->toHaveCount(3);
|
|
});
|
|
|
|
test('raw material has is_active cast to boolean', function () {
|
|
$rawMaterial = RawMaterial::factory()->create(['is_active' => true]);
|
|
|
|
expect($rawMaterial->is_active)->toBeTrue();
|
|
|
|
$rawMaterial->update(['is_active' => false]);
|
|
|
|
expect($rawMaterial->fresh()->is_active)->toBeFalse();
|
|
});
|
|
|
|
test('raw material has unit cast to enum', function () {
|
|
$rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::YARD->value]);
|
|
|
|
expect($rawMaterial->unit)->toBe(RawMaterialUnit::YARD);
|
|
});
|
|
|
|
test('raw material has unit label accessor', function () {
|
|
$rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::METER->value]);
|
|
|
|
expect($rawMaterial->unit_label)->toBe('Meter');
|
|
});
|
|
|
|
test('raw material has unit abbreviation accessor', function () {
|
|
$rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::KILOGRAM->value]);
|
|
|
|
expect($rawMaterial->unit_abbreviation)->toBe('kg');
|
|
});
|
|
|
|
test('raw material has total_stock_formatted accessor', function () {
|
|
$rawMaterial = RawMaterial::factory()->create(['unit' => RawMaterialUnit::METER->value]);
|
|
RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 10.5]);
|
|
RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 5.25]);
|
|
|
|
expect($rawMaterial->fresh(['prices'])->total_stock_formatted)->toContain('15,75');
|
|
expect($rawMaterial->fresh(['prices'])->total_stock_formatted)->toContain('m');
|
|
});
|
|
});
|
|
|
|
// ─── RawMaterialPrice Model ────────────────────────────────
|
|
|
|
describe('RawMaterialPrice Model', function () {
|
|
test('price has price cast to integer', function () {
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'price' => 75000]);
|
|
|
|
expect($price->price)->toBeInt();
|
|
expect($price->price)->toBe(75000);
|
|
});
|
|
|
|
test('price has formatted price accessor', function () {
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'price' => 1500000]);
|
|
|
|
expect($price->price_formatted)->toBe('Rp 1.500.000');
|
|
});
|
|
|
|
test('price has price_input accessor', function () {
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'price' => 200000]);
|
|
|
|
expect($price->price_input)->toBe('200000');
|
|
});
|
|
|
|
test('price belongs to raw material', function () {
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
expect($price->rawMaterial)->not->toBeNull();
|
|
expect($price->rawMaterial->id)->toBe($rawMaterial->id);
|
|
});
|
|
|
|
test('price uses soft deletes', function () {
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
|
|
|
$price->delete();
|
|
|
|
expect($price->trashed())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
// ─── Owner Direct Actions ──────────────────────────────────
|
|
|
|
describe('Owner Direct Actions', function () {
|
|
test('owner can create raw material directly without verification', function () {
|
|
$owner = createRawMaterialUserWithPermission(
|
|
PermissionEnum::RAW_MATERIALS_VIEW,
|
|
PermissionEnum::RAW_MATERIALS_CREATE,
|
|
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
|
);
|
|
|
|
$this->actingAs($owner)
|
|
->post(route('admin.master.raw_materials.store'), [
|
|
'name' => 'Kain Owner',
|
|
'unit' => RawMaterialUnit::METER->value,
|
|
'prices' => [
|
|
priceWithImage('Merah', 75000, 50),
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.master.raw_materials.index'));
|
|
|
|
$rawMaterial = RawMaterial::where('name', 'Kain Owner')->first();
|
|
expect($rawMaterial)->not->toBeNull();
|
|
expect($rawMaterial->is_active)->toBeTrue();
|
|
|
|
$this->assertDatabaseMissing('owner_verification_requests', [
|
|
'subject_id' => $rawMaterial->id,
|
|
'subject_type' => RawMaterial::class,
|
|
]);
|
|
});
|
|
|
|
test('owner can update raw material directly without verification', function () {
|
|
$owner = createRawMaterialUserWithPermission(
|
|
PermissionEnum::RAW_MATERIALS_VIEW,
|
|
PermissionEnum::RAW_MATERIALS_UPDATE,
|
|
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
|
);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$price = $rawMaterial->prices->first();
|
|
|
|
$this->actingAs($owner)
|
|
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
|
'name' => 'Kain Updated',
|
|
'unit' => RawMaterialUnit::KILOGRAM->value,
|
|
'prices' => [
|
|
['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25],
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.master.raw_materials.index'));
|
|
|
|
expect($rawMaterial->fresh()->name)->toBe('Kain Updated');
|
|
expect($price->fresh()->variant)->toBe('Putih');
|
|
});
|
|
|
|
test('owner can delete raw material directly without verification', function () {
|
|
$owner = createRawMaterialUserWithPermission(
|
|
PermissionEnum::RAW_MATERIALS_VIEW,
|
|
PermissionEnum::RAW_MATERIALS_DELETE,
|
|
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
|
);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$this->actingAs($owner)
|
|
->delete(route('admin.master.raw_materials.destroy', $rawMaterial))
|
|
->assertRedirect(route('admin.master.raw_materials.index'));
|
|
|
|
$this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]);
|
|
});
|
|
|
|
test('owner can toggle raw material status directly without verification', function () {
|
|
$owner = createRawMaterialUserWithPermission(
|
|
PermissionEnum::RAW_MATERIALS_VIEW,
|
|
PermissionEnum::RAW_MATERIALS_TOGGLE_STATUS,
|
|
PermissionEnum::OWNER_VERIFICATIONS_VERIFY,
|
|
);
|
|
|
|
$rawMaterial = RawMaterial::factory()->create();
|
|
|
|
$this->actingAs($owner)
|
|
->patch(route('admin.master.raw_materials.toggle_status', $rawMaterial), [
|
|
'is_active' => false,
|
|
]);
|
|
|
|
expect($rawMaterial->fresh()->is_active)->toBeFalse();
|
|
});
|
|
});
|
|
|
|
// ─── Reject Update Request ─────────────────────────────────
|
|
|
|
describe('Reject Update Request', function () {
|
|
test('rejecting update request rolls back to old values', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_UPDATE);
|
|
$verifier = createRawMaterialVerifierUser();
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
$price = $rawMaterial->prices->first();
|
|
$originalName = $rawMaterial->name;
|
|
$originalVariant = $price->variant;
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.master.raw_materials.update', $rawMaterial), [
|
|
'name' => 'Nama Diubah',
|
|
'unit' => RawMaterialUnit::KILOGRAM->value,
|
|
'prices' => [
|
|
['id' => $price->id, 'variant' => 'Putih', 'price' => 90000, 'stock' => 25],
|
|
],
|
|
]);
|
|
|
|
$verificationRequest = OwnerVerificationRequest::query()->pending()->latest()->firstOrFail();
|
|
|
|
$this->actingAs($verifier)
|
|
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
|
|
'reason' => 'Tidak sesuai standar',
|
|
])
|
|
->assertRedirect();
|
|
|
|
expect($rawMaterial->fresh()->name)->toBe($originalName);
|
|
expect($price->fresh()->variant)->toBe($originalVariant);
|
|
});
|
|
});
|
|
|
|
// ─── Show JSON ────────────────────────────────────────────
|
|
|
|
describe('Raw Material Show JSON', function () {
|
|
test('authenticated user with permission can get raw material details in JSON', function () {
|
|
$user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW);
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$response = $this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.show', $rawMaterial))
|
|
->assertOk();
|
|
|
|
$response->assertJsonStructure([
|
|
'id',
|
|
'name',
|
|
'unit',
|
|
'prices' => [
|
|
'*' => [
|
|
'id',
|
|
'variant',
|
|
'price',
|
|
'price_formatted',
|
|
'price_input',
|
|
'stock_formatted',
|
|
'stock_input',
|
|
'images',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$data = $response->json();
|
|
expect($data['id'])->toBe($rawMaterial->id);
|
|
expect($data['prices'])->toHaveCount(2);
|
|
});
|
|
|
|
test('guest cannot get raw material details in JSON', function () {
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$this->get(route('admin.master.raw_materials.show', $rawMaterial))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without view permission cannot get raw material details in JSON', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$rawMaterial = createRawMaterialWithPrices();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.master.raw_materials.show', $rawMaterial))
|
|
->assertForbidden();
|
|
});
|
|
});
|