200 lines
9.1 KiB
PHP
200 lines
9.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Employee;
|
|
use App\Models\StockVariant;
|
|
use App\Models\UniformItem;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class UniformWorkflowTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->actingAs(User::factory()->create(['is_admin' => true]));
|
|
}
|
|
|
|
private function employee(): Employee
|
|
{
|
|
return Employee::create(['employee_no' => 'E-100', 'name' => 'Test Employee', 'department' => 'Operations']);
|
|
}
|
|
|
|
private function variant(int $quantity = 5): StockVariant
|
|
{
|
|
$item = UniformItem::create(['code' => 'TS-01', 'name' => 'Test Shirt']);
|
|
|
|
return $item->variants()->create(['size' => 'M', 'quantity' => $quantity, 'new_quantity' => $quantity, 'unit_value' => 25, 'reorder_level' => 2]);
|
|
}
|
|
|
|
public function test_restock_increases_the_correct_size_balance(): void
|
|
{
|
|
$variant = $this->variant(3);
|
|
$this->post(route('inventory.restock'), ['uniform_item_id' => $variant->uniform_item_id, 'stocks' => [$variant->id => 4], 'values' => [$variant->id => 30], 'stock_category' => 'new'])->assertSessionHasNoErrors();
|
|
$this->assertSame(7, $variant->fresh()->quantity);
|
|
$this->assertSame(7, $variant->fresh()->new_quantity);
|
|
$this->assertDatabaseHas('stock_movements', ['stock_variant_id' => $variant->id, 'type' => 'restock', 'quantity_change' => 4, 'stock_category' => 'new', 'unit_value' => 30]);
|
|
}
|
|
|
|
public function test_bulk_receiving_updates_multiple_sizes_in_one_submission(): void
|
|
{
|
|
$medium = $this->variant(3);
|
|
$large = $medium->item->variants()->create(['size' => 'L', 'quantity' => 2, 'new_quantity' => 2, 'reorder_level' => 2]);
|
|
|
|
$this->post(route('inventory.restock'), [
|
|
'uniform_item_id' => $medium->uniform_item_id,
|
|
'stocks' => [$medium->id => 5, $large->id => 8],
|
|
'values' => [$medium->id => 32.50, $large->id => 35],
|
|
'stock_category' => 'new',
|
|
'notes' => 'Delivery GRN-100',
|
|
])->assertSessionHasNoErrors();
|
|
|
|
$this->assertSame(8, $medium->fresh()->quantity);
|
|
$this->assertSame(10, $large->fresh()->quantity);
|
|
$this->assertDatabaseHas('stock_movements', ['stock_variant_id' => $medium->id, 'quantity_change' => 5, 'notes' => 'Delivery GRN-100']);
|
|
$this->assertDatabaseHas('stock_movements', ['stock_variant_id' => $large->id, 'quantity_change' => 8, 'notes' => 'Delivery GRN-100']);
|
|
}
|
|
|
|
public function test_uniform_type_is_created_with_selected_sizes(): void
|
|
{
|
|
$this->post(route('inventory.items.store'), [
|
|
'code' => 'JKT-01',
|
|
'name' => 'Work Jacket',
|
|
'sizes' => ['S', 'M', 'XL'],
|
|
'reorder_level' => 3,
|
|
])->assertSessionHasNoErrors();
|
|
|
|
$item = UniformItem::where('code', 'JKT-01')->firstOrFail();
|
|
$this->assertEqualsCanonicalizing(['S', 'M', 'XL'], $item->variants()->pluck('size')->all());
|
|
}
|
|
|
|
public function test_issuing_a_uniform_reduces_stock(): void
|
|
{
|
|
$employee = $this->employee();
|
|
$variant = $this->variant(5);
|
|
$this->post(route('issues.store'), ['employee_id' => $employee->id, 'stock_variant_id' => $variant->id, 'quantity' => 2, 'issued_at' => '2026-07-13'])->assertSessionHasNoErrors();
|
|
$this->assertSame(3, $variant->fresh()->quantity);
|
|
$this->assertDatabaseHas('uniform_issues', ['employee_id' => $employee->id, 'stock_variant_id' => $variant->id, 'quantity' => 2, 'status' => 'issued']);
|
|
}
|
|
|
|
public function test_issue_lookups_search_active_employees_and_available_uniforms(): void
|
|
{
|
|
$employee = Employee::create(['employee_no' => 'WH-204', 'name' => 'Nimal Fernando', 'department' => 'Warehouse']);
|
|
Employee::create(['employee_no' => 'OLD-1', 'name' => 'Nimal Former', 'department' => 'Warehouse', 'status' => 'left']);
|
|
$variant = $this->variant(5);
|
|
|
|
$this->getJson(route('issues.lookups.employees', ['q' => 'WH-204']))
|
|
->assertOk()
|
|
->assertJsonCount(1)
|
|
->assertJsonFragment(['id' => $employee->id, 'title' => 'Nimal Fernando']);
|
|
|
|
$this->getJson(route('issues.lookups.variants', ['q' => 'Test Shirt']))
|
|
->assertOk()
|
|
->assertJsonCount(1)
|
|
->assertJsonFragment(['id' => $variant->id, 'title' => 'Test Shirt / Size M']);
|
|
}
|
|
|
|
public function test_issue_is_rejected_when_stock_is_insufficient(): void
|
|
{
|
|
$employee = $this->employee();
|
|
$variant = $this->variant(1);
|
|
$this->post(route('issues.store'), ['employee_id' => $employee->id, 'stock_variant_id' => $variant->id, 'quantity' => 2, 'issued_at' => '2026-07-13'])->assertSessionHasErrors('quantity');
|
|
$this->assertSame(1, $variant->fresh()->quantity);
|
|
}
|
|
|
|
public function test_exit_handover_returns_only_reusable_uniforms_to_stock(): void
|
|
{
|
|
$employee = $this->employee();
|
|
$variant = $this->variant(5);
|
|
$this->post(route('issues.store'), ['employee_id' => $employee->id, 'stock_variant_id' => $variant->id, 'quantity' => 2, 'issued_at' => '2026-07-13']);
|
|
$issue = $employee->issues()->first();
|
|
|
|
$this->post(route('handovers.store', $employee), ['handover_date' => '2026-07-14', 'items' => [['issue_id' => $issue->id, 'condition' => 'reusable']]])->assertSessionHasNoErrors();
|
|
|
|
$this->assertSame(5, $variant->fresh()->quantity);
|
|
$this->assertSame('left', $employee->fresh()->status);
|
|
$this->assertDatabaseHas('uniform_issues', ['id' => $issue->id, 'status' => 'returned', 'returned_quantity' => 2]);
|
|
}
|
|
|
|
public function test_damaged_handover_items_do_not_return_to_available_stock(): void
|
|
{
|
|
$employee = $this->employee();
|
|
$variant = $this->variant(5);
|
|
$this->post(route('issues.store'), ['employee_id' => $employee->id, 'stock_variant_id' => $variant->id, 'quantity' => 1, 'issued_at' => '2026-07-13']);
|
|
$issue = $employee->issues()->first();
|
|
|
|
$this->post(route('handovers.store', $employee), ['handover_date' => '2026-07-14', 'items' => [['issue_id' => $issue->id, 'condition' => 'damaged']]])->assertSessionHasNoErrors();
|
|
|
|
$this->assertSame(4, $variant->fresh()->quantity);
|
|
$this->assertSame('left', $employee->fresh()->status);
|
|
}
|
|
|
|
public function test_normal_returns_support_partial_reusable_and_damaged_items(): void
|
|
{
|
|
$employee = $this->employee();
|
|
$variant = $this->variant(5);
|
|
$this->post(route('issues.store'), ['employee_id' => $employee->id, 'stock_variant_id' => $variant->id, 'quantity' => 2, 'issued_at' => '2026-07-13']);
|
|
$issue = $employee->issues()->first();
|
|
|
|
$this->post(route('returns.store'), [
|
|
'employee_id' => $employee->id,
|
|
'uniform_issue_id' => $issue->id,
|
|
'quantity' => 1,
|
|
'condition' => 'reusable',
|
|
'returned_at' => '2026-07-14',
|
|
])->assertSessionHasNoErrors();
|
|
|
|
$this->assertSame(4, $variant->fresh()->quantity);
|
|
$this->assertSame(1, $variant->fresh()->returned_quantity);
|
|
$this->assertSame('issued', $issue->fresh()->status);
|
|
$this->assertSame(1, $issue->fresh()->returned_quantity);
|
|
|
|
$this->post(route('returns.store'), [
|
|
'employee_id' => $employee->id,
|
|
'uniform_issue_id' => $issue->id,
|
|
'quantity' => 1,
|
|
'condition' => 'damaged',
|
|
'returned_at' => '2026-07-14',
|
|
])->assertSessionHasNoErrors();
|
|
|
|
$this->assertSame(4, $variant->fresh()->quantity);
|
|
$this->assertSame('returned', $issue->fresh()->status);
|
|
$this->assertDatabaseHas('uniform_returns', ['uniform_issue_id' => $issue->id, 'condition' => 'reusable', 'quantity' => 1]);
|
|
$this->assertDatabaseHas('uniform_returns', ['uniform_issue_id' => $issue->id, 'condition' => 'damaged', 'quantity' => 1]);
|
|
}
|
|
|
|
public function test_stock_specification_details_can_be_saved(): void
|
|
{
|
|
$variant = $this->variant();
|
|
|
|
$this->put(route('stock-details.update', $variant->item), [
|
|
'material' => '65% polyester, 35% cotton',
|
|
'color' => 'Navy blue',
|
|
'description' => 'Regular fit work shirt',
|
|
'care_instructions' => 'Machine wash at 30 C',
|
|
])->assertSessionHasNoErrors();
|
|
|
|
$this->assertDatabaseHas('uniform_items', [
|
|
'id' => $variant->uniform_item_id,
|
|
'material' => '65% polyester, 35% cotton',
|
|
'color' => 'Navy blue',
|
|
]);
|
|
}
|
|
|
|
public function test_stock_details_can_be_searched_by_name_code_material_or_color(): void
|
|
{
|
|
UniformItem::create(['code' => 'BLZ-001', 'name' => 'Executive Blazer', 'material' => 'Wool blend', 'color' => 'Charcoal']);
|
|
UniformItem::create(['code' => 'APN-002', 'name' => 'Kitchen Apron', 'material' => 'Cotton', 'color' => 'White']);
|
|
|
|
$this->get(route('stock-details.index', ['search' => 'Charcoal']))
|
|
->assertOk()
|
|
->assertSee('Executive Blazer')
|
|
->assertDontSee('Kitchen Apron')
|
|
->assertSee('1 result(s)');
|
|
}
|
|
}
|