'Super Admin', 'guard_name' => 'web']); Permission::firstOrCreate(['name' => 'projects.access', 'guard_name' => 'web']); Permission::firstOrCreate(['name' => 'materials-catalog.access', 'guard_name' => 'web']); Permission::firstOrCreate(['name' => 'inventory.access', 'guard_name' => 'web']); $this->admin = User::factory()->create([ 'status' => 'active', 'user_type' => 'admin', ]); $this->admin->assignRole($role); $this->estimatedMaterial = Material::create([ 'name' => 'Portland Cement Type 1', 'sku' => 'CEM-POR-001', 'unit' => 'bag', 'unit_cost' => 280.00, 'category' => 'cement', 'type' => 'raw', 'status' => 'active', ]); $this->missedMaterial = Material::create([ 'name' => 'High-Grade Waterproofing Sealant', 'sku' => 'SLN-WTR-999', 'unit' => 'gallon', 'unit_cost' => 1250.00, 'category' => 'chemicals', 'type' => 'raw', 'status' => 'active', ]); $this->project = Project::create([ 'name' => 'Metropolis Sky Tower', 'code' => 'PRJ-MTR-2026', 'status' => ProjectStatus::InProgress, 'project_type' => 'standard', 'is_unprofitable' => false, 'contract_value' => 15000000.00, 'current_wizard_step' => 8, ]); // Add initial estimated material $this->project->materialsEstimates()->create([ 'material_id' => $this->estimatedMaterial->id, 'estimated_qty' => 500, 'unit_cost' => 280.00, ]); $this->warehouse = Warehouse::create([ 'name' => 'Central Hub Warehouse', 'code' => 'WH-MAIN-001', 'project_id' => $this->project->id, 'is_active' => true, ]); } /** @test */ public function test_missed_material_can_be_requested_approved_ordered_and_received_into_project_inventory(): void { // 1. Create a Material Requisition for the missed material (which was not in original estimates) $mrResponse = $this->actingAs($this->admin)->post(route('requisitions.store'), [ 'project_ulid' => $this->project->ulid, 'requisition_type' => 'unestimated', 'notes' => 'Sudden water leak discovered on basement levels; requires urgent waterproofing sealant.', 'items' => [ [ 'material_ulid' => $this->missedMaterial->ulid, 'quantity' => 20, 'unit_cost' => 1250.00, 'is_unestimated' => true, ], ], ]); $mrResponse->assertRedirect(); $mr = MaterialRequisition::where('project_id', $this->project->id)->firstOrFail(); $this->assertEquals('draft', $mr->status); $this->assertCount(1, $mr->items); $this->assertEquals($this->missedMaterial->id, $mr->items->first()->material_id); // Verify that the missed material is now registered in project material estimates $this->assertTrue( $this->project->materialsEstimates()->where('material_id', $this->missedMaterial->id)->exists() ); // 2. Submit & Approve Material Requisition $mr->update(['status' => 'approved', 'approved_by' => $this->admin->id, 'approved_at' => now()]); // 3. Create Purchase Order for the approved MR $poResponse = $this->actingAs($this->admin)->post(route('purchase-orders.store'), [ 'project_ulid' => $this->project->ulid, 'target_warehouse_ulid' => $this->warehouse->ulid, 'supplier' => 'Prime Chemicals Industrial Supply', 'notes' => 'Urgent procurement for basement waterproofing.', 'requisition_ulids' => [$mr->ulid], 'items' => [ [ 'material_ulid' => $this->missedMaterial->ulid, 'quantity' => 20, 'unit_cost' => 1250.00, 'requisition_item_id' => $mr->items->first()->id, ], ], ]); $poResponse->assertRedirect(); $po = PurchaseOrder::where('supplier', 'Prime Chemicals Industrial Supply')->firstOrFail(); $this->assertEquals('draft', $po->status); $this->assertEquals(25000.00, (float) $po->total_cost); // 4. Approve Purchase Order $po->update(['status' => 'approved', 'approved_by' => $this->admin->id, 'approved_at' => now()]); // 5. Receive goods into warehouse stock (simulating PO payment / delivery) $warehouseService = new WarehouseService(); $warehouseService->receiveStock( warehouse: $this->warehouse, material: $this->missedMaterial, quantity: 20, unitCost: 1250.00, purchaseOrder: $po, materialRequisitionId: $mr->id, performedBy: $this->admin->id ); // 6. Dispatch material to project site $warehouseService->dispatchToProject( warehouse: $this->warehouse, project: $this->project, material: $this->missedMaterial, quantity: 20, performedBy: $this->admin->id, notes: 'Dispatched to Basement Level 2' ); // 7. Verify project materials page and project inventory on-hand reflect the missed material $materialsPageResponse = $this->actingAs($this->admin)->get(route('projects.materials', $this->project)); $materialsPageResponse->assertOk(); $projectMaterials = $materialsPageResponse->viewData('page')['props']['availableMaterials']; $this->assertTrue(collect($projectMaterials)->contains('name', 'High-Grade Waterproofing Sealant')); $this->assertEquals(20, collect($projectMaterials)->firstWhere('name', 'High-Grade Waterproofing Sealant')['on_hand_qty']); } }