Files
GSB-Construction/Modules/MaterialLogistics/app/Services/InventoryService.php

128 lines
4.1 KiB
PHP

<?php
namespace Modules\MaterialLogistics\Services;
use Modules\MaterialLogistics\Enums\DeploymentStatus;
use Modules\MaterialLogistics\Events\MaterialDelivered;
use Modules\MaterialLogistics\Events\MaterialRequested;
use Modules\MasterData\Models\Material;
use Modules\MaterialLogistics\Models\MaterialDeployment;
use Modules\MaterialLogistics\Models\ProjectInventory;
class InventoryService
{
/**
* Allocate stock to a project (add to on_hand).
*/
public function allocate(int $projectId, int $materialId, float $quantity, ?float $unitCost = null): ProjectInventory
{
$material = Material::findOrFail($materialId);
return ProjectInventory::updateOrCreate(
['project_id' => $projectId, 'material_id' => $materialId],
[
'on_hand_qty' => \DB::raw("on_hand_qty + {$quantity}"),
'unit_cost' => $unitCost ?? $material->unit_cost,
]
);
}
/**
* Request a deployment (creates deployment record).
*/
public function requestDeployment(
int $projectId,
int $materialId,
float $quantity,
?int $requestedBy = null,
?string $notes = null,
): MaterialDeployment {
$deployment = MaterialDeployment::create([
'project_id' => $projectId,
'material_id' => $materialId,
'requested_by' => $requestedBy,
'quantity' => $quantity,
'status' => DeploymentStatus::Requested,
'notes' => $notes,
]);
MaterialRequested::dispatch($deployment);
return $deployment;
}
/**
* Dispatch deployment (guard: check inventory).
*/
public function dispatch(MaterialDeployment $deployment): MaterialDeployment
{
$inventory = ProjectInventory::where('project_id', $deployment->project_id)
->where('material_id', $deployment->material_id)
->first();
if (!$inventory || $inventory->available_qty < $deployment->quantity) {
throw new \InvalidArgumentException(
"Insufficient stock. Available: " . ($inventory?->available_qty ?? 0) . ", Requested: {$deployment->quantity}"
);
}
// Allocate from inventory
$inventory->increment('allocated_qty', $deployment->quantity);
$deployment->transitionTo(DeploymentStatus::PendingDispatch);
$deployment->transitionTo(DeploymentStatus::InTransit);
return $deployment->fresh();
}
/**
* Mark as delivered.
*/
public function deliver(MaterialDeployment $deployment): MaterialDeployment
{
$deployment->transitionTo(DeploymentStatus::Delivered);
MaterialDelivered::dispatch($deployment);
return $deployment->fresh();
}
/**
* Mark as consumed (deduct from inventory).
*/
public function consume(MaterialDeployment $deployment): MaterialDeployment
{
$deployment->transitionTo(DeploymentStatus::Consumed);
$inventory = ProjectInventory::where('project_id', $deployment->project_id)
->where('material_id', $deployment->material_id)
->first();
if ($inventory) {
$inventory->decrement('on_hand_qty', $deployment->quantity);
$inventory->decrement('allocated_qty', $deployment->quantity);
$inventory->increment('consumed_qty', $deployment->quantity);
}
return $deployment->fresh();
}
/**
* Cancel a deployment.
*/
public function cancel(MaterialDeployment $deployment): MaterialDeployment
{
// Release allocated stock if it was dispatched
if (in_array($deployment->status, [DeploymentStatus::PendingDispatch, DeploymentStatus::InTransit])) {
$inventory = ProjectInventory::where('project_id', $deployment->project_id)
->where('material_id', $deployment->material_id)
->first();
if ($inventory) {
$inventory->decrement('allocated_qty', $deployment->quantity);
}
}
$deployment->transitionTo(DeploymentStatus::Cancelled);
return $deployment->fresh();
}
}