176 lines
6.9 KiB
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']);
|
|
}
|
|
}
|