feat: implement stock quality categorization in StokOpname; enhance validation rules and update frontend components for improved inventory tracking and reporting
This commit is contained in:
parent
0d77d6a4fd
commit
289fc4dc2c
197
tests/Feature/Admin/Manage/StockTest.php
Normal file
197
tests/Feature/Admin/Manage/StockTest.php
Normal file
@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\CuttingStatus;
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use App\Models\Cutting;
|
||||
use App\Models\CuttingMaterial;
|
||||
use App\Models\CuttingResult;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\RawMaterial;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
});
|
||||
|
||||
// ─── Helper ───────────────────────────────────────────────
|
||||
|
||||
function createStockUserWithPermission(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 createCompletedCuttingSetup(): array
|
||||
{
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->create([
|
||||
'product_id' => $product->id,
|
||||
'stock' => 10,
|
||||
'reject_stock' => 5,
|
||||
]);
|
||||
|
||||
$rawMaterial = RawMaterial::factory()->create();
|
||||
$rawMaterialPrice = RawMaterialPrice::factory()->create([
|
||||
'raw_material_id' => $rawMaterial->id,
|
||||
]);
|
||||
|
||||
$cutting = Cutting::factory()->create([
|
||||
'status' => CuttingStatus::COMPLETED,
|
||||
]);
|
||||
|
||||
CuttingMaterial::factory()->create([
|
||||
'cutting_id' => $cutting->id,
|
||||
'raw_material_price_id' => $rawMaterialPrice->id,
|
||||
]);
|
||||
|
||||
CuttingResult::factory()->create([
|
||||
'cutting_id' => $cutting->id,
|
||||
'product_variant_id' => $variant->id,
|
||||
'cutting_result' => 7,
|
||||
'sample' => 5, // Good stock result
|
||||
'original_outside_sample' => 2, // Reject stock result
|
||||
]);
|
||||
|
||||
return [
|
||||
'cutting' => $cutting,
|
||||
'variant' => $variant,
|
||||
'raw_material_price' => $rawMaterialPrice,
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────
|
||||
|
||||
describe('Stock Warehouse Management', function () {
|
||||
test('authenticated user with permission can view stocks index', function () {
|
||||
$user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.stocks.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('user with permission can submit verification for completed cutting', function () {
|
||||
$user = createStockUserWithPermission(
|
||||
PermissionEnum::STOCKS_VIEW,
|
||||
PermissionEnum::CUTTINGS_VERIFY
|
||||
);
|
||||
|
||||
$setup = createCompletedCuttingSetup();
|
||||
$cutting = $setup['cutting'];
|
||||
|
||||
$payload = [
|
||||
'verification_note' => 'Verification notes',
|
||||
'results' => [
|
||||
[
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'good' => 6, // changed from 5 to 6
|
||||
'reject' => 1, // changed from 2 to 1
|
||||
]
|
||||
],
|
||||
'result_prices' => [
|
||||
[
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'prices' => [
|
||||
[
|
||||
'type' => 'harga_modal',
|
||||
'price' => 50000,
|
||||
],
|
||||
[
|
||||
'type' => 'retail',
|
||||
'price' => 75000,
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.stocks.verify', $cutting), $payload)
|
||||
->assertRedirect(route('admin.manage.stocks.index'));
|
||||
|
||||
$cutting->refresh();
|
||||
$this->assertEquals(CuttingStatus::PENDING_VERIFICATION, $cutting->status);
|
||||
|
||||
// Verify cutting results are updated
|
||||
$this->assertDatabaseHas('cutting_results', [
|
||||
'cutting_id' => $cutting->id,
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'sample' => 6,
|
||||
'original_outside_sample' => 1,
|
||||
]);
|
||||
});
|
||||
|
||||
test('user with permission can approve verification and increase stock', function () {
|
||||
$user = createStockUserWithPermission(
|
||||
PermissionEnum::STOCKS_VIEW,
|
||||
PermissionEnum::OWNER_VERIFICATIONS_VERIFY
|
||||
);
|
||||
|
||||
$setup = createCompletedCuttingSetup();
|
||||
$cutting = $setup['cutting'];
|
||||
$cutting->status = CuttingStatus::PENDING_VERIFICATION;
|
||||
$cutting->save();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.owner_verifications.approve', $cutting), [
|
||||
'approval_note' => 'Approved by owner',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$cutting->refresh();
|
||||
$setup['variant']->refresh();
|
||||
|
||||
$this->assertEquals(CuttingStatus::VERIFIED, $cutting->status);
|
||||
|
||||
// Stock (good) should increment by 5 (10 + 5 = 15)
|
||||
$this->assertEquals(15, $setup['variant']->stock);
|
||||
|
||||
// Reject stock should increment by 2 (5 + 2 = 7)
|
||||
$this->assertEquals(7, $setup['variant']->reject_stock);
|
||||
});
|
||||
|
||||
test('user with permission can reject verification', function () {
|
||||
$user = createStockUserWithPermission(
|
||||
PermissionEnum::STOCKS_VIEW,
|
||||
PermissionEnum::OWNER_VERIFICATIONS_REJECT
|
||||
);
|
||||
|
||||
$setup = createCompletedCuttingSetup();
|
||||
$cutting = $setup['cutting'];
|
||||
$cutting->status = CuttingStatus::PENDING_VERIFICATION;
|
||||
$cutting->save();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.owner_verifications.reject', $cutting), [
|
||||
'reason' => 'Invalid results',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$cutting->refresh();
|
||||
|
||||
// Status should revert to COMPLETED
|
||||
$this->assertEquals(CuttingStatus::COMPLETED, $cutting->status);
|
||||
|
||||
$this->assertDatabaseHas('rejections', [
|
||||
'rejectable_id' => $cutting->id,
|
||||
'rejectable_type' => Cutting::class,
|
||||
'reason' => 'Invalid results',
|
||||
]);
|
||||
});
|
||||
});
|
||||
342
tests/Feature/Admin/Manage/StokOpnameTest.php
Normal file
342
tests/Feature/Admin/Manage/StokOpnameTest.php
Normal file
@ -0,0 +1,342 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use App\Enums\ProductStockQuality;
|
||||
use App\Enums\StokOpnameStatus;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\StokOpname;
|
||||
use App\Models\StokOpnameItem;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
});
|
||||
|
||||
// ─── Helper ───────────────────────────────────────────────
|
||||
|
||||
function createStokOpnameUserWithPermission(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 createCatalogSetup(): array
|
||||
{
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->create([
|
||||
'product_id' => $product->id,
|
||||
'stock' => 10,
|
||||
'retail_stock' => 5,
|
||||
'reject_stock' => 2,
|
||||
]);
|
||||
|
||||
return ['product' => $product, 'variant' => $variant];
|
||||
}
|
||||
|
||||
// ─── Index & Create ───────────────────────────────────────
|
||||
|
||||
describe('Stok Opname Index & Create', function () {
|
||||
test('authenticated user with permission can view stok opnames index', function () {
|
||||
$user = createStokOpnameUserWithPermission(PermissionEnum::STOK_OPNAMES_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.stok-opnames.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('user without permission is forbidden to view index', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.stok-opnames.index'))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('user with permission can view create page', function () {
|
||||
$user = createStokOpnameUserWithPermission(
|
||||
PermissionEnum::STOK_OPNAMES_VIEW,
|
||||
PermissionEnum::STOK_OPNAMES_CREATE
|
||||
);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.manage.stok-opnames.create'))
|
||||
->assertOk();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Store & Update ───────────────────────────────────────
|
||||
|
||||
describe('Stok Opname Store & Update', function () {
|
||||
test('user can store a new stok opname', function () {
|
||||
$user = createStokOpnameUserWithPermission(
|
||||
PermissionEnum::STOK_OPNAMES_VIEW,
|
||||
PermissionEnum::STOK_OPNAMES_CREATE
|
||||
);
|
||||
|
||||
$setup = createCatalogSetup();
|
||||
|
||||
$payload = [
|
||||
'opname_date' => now()->format('Y-m-d'),
|
||||
'notes' => 'Test Opname Notes',
|
||||
'items' => [
|
||||
[
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::GOOD->value,
|
||||
'physical_stock' => 8,
|
||||
'notes' => 'Kurang 2 bagus',
|
||||
],
|
||||
[
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::REJECT->value,
|
||||
'physical_stock' => 3,
|
||||
'notes' => 'Kelebihan 1 reject',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.stok-opnames.store'), $payload)
|
||||
->assertRedirect(route('admin.manage.stok-opnames.index'));
|
||||
|
||||
$this->assertDatabaseHas('stok_opnames', [
|
||||
'notes' => 'Test Opname Notes',
|
||||
'status' => StokOpnameStatus::DRAFT->value,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('stok_opname_items', [
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::GOOD->value,
|
||||
'physical_stock' => 8,
|
||||
'difference' => -2,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('stok_opname_items', [
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::REJECT->value,
|
||||
'physical_stock' => 3,
|
||||
'difference' => 1,
|
||||
]);
|
||||
});
|
||||
|
||||
test('user can auto-save a stok opname draft', function () {
|
||||
$user = createStokOpnameUserWithPermission(
|
||||
PermissionEnum::STOK_OPNAMES_VIEW,
|
||||
PermissionEnum::STOK_OPNAMES_CREATE
|
||||
);
|
||||
|
||||
$setup = createCatalogSetup();
|
||||
|
||||
$payload = [
|
||||
'opname_date' => now()->format('Y-m-d'),
|
||||
'notes' => 'Draft Notes',
|
||||
'items' => [
|
||||
[
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::RETAIL->value,
|
||||
'physical_stock' => 6,
|
||||
'notes' => 'Tambah eceran',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->postJson(route('admin.manage.stok-opnames.auto_save'), $payload)
|
||||
->assertOk();
|
||||
|
||||
$stokOpnameId = $response->json('stok_opname_id');
|
||||
|
||||
$this->assertDatabaseHas('stok_opnames', [
|
||||
'id' => $stokOpnameId,
|
||||
'status' => StokOpnameStatus::DRAFT->value,
|
||||
]);
|
||||
});
|
||||
|
||||
test('user can update a draft stok opname', function () {
|
||||
$user = createStokOpnameUserWithPermission(
|
||||
PermissionEnum::STOK_OPNAMES_VIEW,
|
||||
PermissionEnum::STOK_OPNAMES_UPDATE
|
||||
);
|
||||
|
||||
$setup = createCatalogSetup();
|
||||
|
||||
$stokOpname = StokOpname::create([
|
||||
'opname_date' => now(),
|
||||
'status' => StokOpnameStatus::DRAFT,
|
||||
'created_by_id' => $user->id,
|
||||
]);
|
||||
|
||||
$item = StokOpnameItem::create([
|
||||
'stok_opname_id' => $stokOpname->id,
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::GOOD,
|
||||
'system_stock' => 10,
|
||||
'physical_stock' => 8,
|
||||
'difference' => -2,
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'opname_date' => now()->format('Y-m-d'),
|
||||
'notes' => 'Updated Notes',
|
||||
'items' => [
|
||||
[
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::GOOD->value,
|
||||
'physical_stock' => 9,
|
||||
'notes' => 'Diubah jadi 9',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.manage.stok-opnames.update', $stokOpname), $payload)
|
||||
->assertRedirect(route('admin.manage.stok-opnames.index'));
|
||||
|
||||
$this->assertDatabaseHas('stok_opnames', [
|
||||
'id' => $stokOpname->id,
|
||||
'notes' => 'Updated Notes',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('stok_opname_items', [
|
||||
'stok_opname_id' => $stokOpname->id,
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::GOOD->value,
|
||||
'physical_stock' => 9,
|
||||
'difference' => -1,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Submit, Verify & Reject ──────────────────────────────
|
||||
|
||||
describe('Stok Opname Submission & Verification', function () {
|
||||
test('user can submit a draft stok opname for verification', function () {
|
||||
$user = createStokOpnameUserWithPermission(
|
||||
PermissionEnum::STOK_OPNAMES_VIEW,
|
||||
PermissionEnum::STOK_OPNAMES_SUBMIT
|
||||
);
|
||||
|
||||
$setup = createCatalogSetup();
|
||||
|
||||
$stokOpname = StokOpname::create([
|
||||
'opname_date' => now(),
|
||||
'status' => StokOpnameStatus::DRAFT,
|
||||
'created_by_id' => $user->id,
|
||||
]);
|
||||
|
||||
StokOpnameItem::create([
|
||||
'stok_opname_id' => $stokOpname->id,
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::GOOD,
|
||||
'system_stock' => 10,
|
||||
'physical_stock' => 8,
|
||||
'difference' => -2,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.stok-opnames.submit', $stokOpname))
|
||||
->assertRedirect(route('admin.manage.stok-opnames.index'));
|
||||
|
||||
$this->assertEquals(StokOpnameStatus::PENDING, $stokOpname->refresh()->status);
|
||||
});
|
||||
|
||||
test('user with verify permission can verify stok opname and reconcile multiple qualities', function () {
|
||||
$user = createStokOpnameUserWithPermission(
|
||||
PermissionEnum::STOK_OPNAMES_VIEW,
|
||||
PermissionEnum::STOK_OPNAMES_VERIFY
|
||||
);
|
||||
|
||||
$setup = createCatalogSetup();
|
||||
|
||||
$stokOpname = StokOpname::create([
|
||||
'opname_date' => now(),
|
||||
'status' => StokOpnameStatus::PENDING,
|
||||
'created_by_id' => $user->id,
|
||||
]);
|
||||
|
||||
// Create opname items for Good, Retail, and Reject qualities
|
||||
StokOpnameItem::create([
|
||||
'stok_opname_id' => $stokOpname->id,
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::GOOD,
|
||||
'system_stock' => 10,
|
||||
'physical_stock' => 8,
|
||||
'difference' => -2,
|
||||
]);
|
||||
|
||||
StokOpnameItem::create([
|
||||
'stok_opname_id' => $stokOpname->id,
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::RETAIL,
|
||||
'system_stock' => 5,
|
||||
'physical_stock' => 7,
|
||||
'difference' => 2,
|
||||
]);
|
||||
|
||||
StokOpnameItem::create([
|
||||
'stok_opname_id' => $stokOpname->id,
|
||||
'product_variant_id' => $setup['variant']->id,
|
||||
'stock_quality' => ProductStockQuality::REJECT,
|
||||
'system_stock' => 2,
|
||||
'physical_stock' => 0,
|
||||
'difference' => -2,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.stok-opnames.verify', $stokOpname), [
|
||||
'verification_notes' => 'Verified successfully',
|
||||
])
|
||||
->assertRedirect(route('admin.manage.stok-opnames.index'));
|
||||
|
||||
$stokOpname->refresh();
|
||||
$setup['variant']->refresh();
|
||||
|
||||
$this->assertEquals(StokOpnameStatus::VERIFIED, $stokOpname->status);
|
||||
$this->assertEquals('Verified successfully', $stokOpname->verification_notes);
|
||||
$this->assertEquals($user->id, $stokOpname->verified_by_id);
|
||||
|
||||
// Verify variant stocks are updated correctly matching their quality enums
|
||||
$this->assertEquals(8, $setup['variant']->stock);
|
||||
$this->assertEquals(7, $setup['variant']->retail_stock);
|
||||
$this->assertEquals(0, $setup['variant']->reject_stock);
|
||||
});
|
||||
|
||||
test('user with verify permission can reject stok opname', function () {
|
||||
$user = createStokOpnameUserWithPermission(
|
||||
PermissionEnum::STOK_OPNAMES_VIEW,
|
||||
PermissionEnum::STOK_OPNAMES_VERIFY
|
||||
);
|
||||
|
||||
$setup = createCatalogSetup();
|
||||
|
||||
$stokOpname = StokOpname::create([
|
||||
'opname_date' => now(),
|
||||
'status' => StokOpnameStatus::PENDING,
|
||||
'created_by_id' => $user->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.manage.stok-opnames.reject', $stokOpname), [
|
||||
'reason' => 'Data is invalid',
|
||||
])
|
||||
->assertRedirect(route('admin.manage.stok-opnames.index'));
|
||||
|
||||
$stokOpname->refresh();
|
||||
$this->assertEquals(StokOpnameStatus::REJECTED, $stokOpname->status);
|
||||
$this->assertEquals('Data is invalid', $stokOpname->verification_notes);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user