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)'); } }