Files
GSB-Construction/Modules/MaterialLogistics/tests/Feature/MissedMaterialsProcurementFlowTest.php

176 lines
6.9 KiB
PHP

<?php
namespace Modules\MaterialLogistics\Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Modules\MasterData\Models\Material;
use Modules\MaterialLogistics\Enums\PurchaseOrderStatus;
use Modules\MaterialLogistics\Models\MaterialRequisition;
use Modules\MaterialLogistics\Models\PurchaseOrder;
use Modules\MaterialLogistics\Models\Warehouse;
use Modules\MaterialLogistics\Services\WarehouseService;
use Modules\ProjectManagement\Enums\ProjectStatus;
use Modules\ProjectManagement\Models\Project;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;
class MissedMaterialsProcurementFlowTest extends TestCase
{
use RefreshDatabase;
protected User $admin;
protected Project $project;
protected Material $estimatedMaterial;
protected Material $missedMaterial;
protected Warehouse $warehouse;
protected function setUp(): void
{
parent::setUp();
$role = Role::firstOrCreate(['name' => '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,
'notes' => 'Sudden water leak discovered on basement levels; requires urgent waterproofing sealant.',
'items' => [
[
'material_ulid' => $this->missedMaterial->ulid,
'quantity' => 20,
'unit_cost' => 1250.00,
],
],
]);
$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'), [
'vendor_name' => 'Prime Chemicals Industrial Supply',
'vendor_contact' => '09175556789',
'target_warehouse_id' => $this->warehouse->id,
'payment_terms' => 'cash',
'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('vendor_name', 'Prime Chemicals Industrial Supply')->firstOrFail();
$this->assertEquals(PurchaseOrderStatus::Draft, $po->status);
$this->assertEquals(25000.00, (float) $po->total_cost);
// 4. Approve Purchase Order
$po->update(['status' => PurchaseOrderStatus::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']);
}
}