317 lines
10 KiB
PHP
317 lines
10 KiB
PHP
<?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',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ─── Retail Stock Transfer ──────────────────────────────────
|
|
|
|
describe('Retail Stock Transfer', function () {
|
|
test('user with permission can transfer stock from good to retail', function () {
|
|
$user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create([
|
|
'product_id' => $product->id,
|
|
'stock' => 10,
|
|
'retail_stock' => 5,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.retail-stock.transfer'), [
|
|
'product_variant_id' => $variant->id,
|
|
'quantity' => 3,
|
|
'notes' => 'Transfer test',
|
|
])
|
|
->assertOk();
|
|
|
|
$variant->refresh();
|
|
|
|
$this->assertEquals(7, $variant->stock);
|
|
$this->assertEquals(8, $variant->retail_stock);
|
|
|
|
$this->assertDatabaseHas('retail_stock_histories', [
|
|
'product_variant_id' => $variant->id,
|
|
'user_id' => $user->id,
|
|
'quantity' => 3,
|
|
'stock_before' => 10,
|
|
'retail_stock_before' => 5,
|
|
'stock_after' => 7,
|
|
'retail_stock_after' => 8,
|
|
'notes' => 'Transfer test',
|
|
]);
|
|
});
|
|
|
|
test('retail stock transfer does not require owner verification', function () {
|
|
$user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create([
|
|
'product_id' => $product->id,
|
|
'stock' => 10,
|
|
'retail_stock' => 5,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.retail-stock.transfer'), [
|
|
'product_variant_id' => $variant->id,
|
|
'quantity' => 3,
|
|
])
|
|
->assertOk();
|
|
|
|
$this->assertDatabaseCount('owner_verification_requests', 0);
|
|
|
|
$variant->refresh();
|
|
$this->assertEquals(7, $variant->stock);
|
|
$this->assertEquals(8, $variant->retail_stock);
|
|
});
|
|
|
|
test('retail stock transfer fails when quantity exceeds stock', function () {
|
|
$user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create([
|
|
'product_id' => $product->id,
|
|
'stock' => 5,
|
|
'retail_stock' => 0,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.retail-stock.transfer'), [
|
|
'product_variant_id' => $variant->id,
|
|
'quantity' => 10,
|
|
])
|
|
->assertUnprocessable()
|
|
->assertJsonValidationErrors('quantity');
|
|
});
|
|
|
|
test('retail stock transfer fails when quantity is zero', function () {
|
|
$user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create([
|
|
'product_id' => $product->id,
|
|
'stock' => 10,
|
|
'retail_stock' => 5,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.retail-stock.transfer'), [
|
|
'product_variant_id' => $variant->id,
|
|
'quantity' => 0,
|
|
])
|
|
->assertUnprocessable()
|
|
->assertJsonValidationErrors('quantity');
|
|
});
|
|
|
|
test('user without permission cannot transfer stock', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$product = Product::factory()->create();
|
|
$variant = ProductVariant::factory()->create([
|
|
'product_id' => $product->id,
|
|
'stock' => 10,
|
|
'retail_stock' => 5,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->postJson(route('admin.manage.retail-stock.transfer'), [
|
|
'product_variant_id' => $variant->id,
|
|
'quantity' => 3,
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
});
|