128 lines
4.1 KiB
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();
|
|
}
|
|
}
|