Files
compras/app/Services/PurchasePlanningWorkflow.php
nala 48347e2d3a
tests / ci (push) Canceled after 0s
commit5
2026-10-30 10:55:18 +02:00

120 lines
7.0 KiB
PHP

<?php
namespace App\Services;
use App\Enums\PurchasingRole;
use App\Models\Department;
use App\Models\PurchasePlanItem;
use App\Models\PurchasePlanMilestone;
use App\Models\Requisition;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class PurchasePlanningWorkflow
{
private function manager(User $actor): bool
{
return $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement]);
}
private function responsible(User $actor, int $id): void
{
if (! User::query()->where('tenant_id', $actor->tenant_id)->whereKey($id)->exists()) {
throw ValidationException::withMessages(['responsible_id' => 'O responsável deve pertencer à empresa.']);
}
}
private function version(int $expected, int $current): void
{
if ($expected !== $current) {
throw ValidationException::withMessages(['version' => 'O registo mudou. Actualize a página antes de voltar a guardar.']);
}
}
/** @param array<string,mixed> $data */
public function create(User $actor, array $data): PurchasePlanItem
{
return DB::transaction(function () use ($actor, $data) {
app(ConflictGuard::class)->lockCompany($actor);
$actor = app(CurrentActor::class)->lock($actor, [PurchasingRole::Admin, PurchasingRole::Procurement]);
Department::query()->findOrFail($data['department_id']);
$this->responsible($actor, $data['responsible_id']);
$requisition = null;
if ($data['requisition_id'] ?? null) {
$requisition = Requisition::query()->visibleTo($actor)->whereKey($data['requisition_id'])->firstOrFail();
if ($requisition->department_id !== $data['department_id'] || PurchasePlanItem::query()->where('requisition_id', $requisition->id)->exists()) {
throw ValidationException::withMessages(['requisition_id' => 'A requisição deve pertencer ao departamento e ainda não ter planeamento associado.']);
}
}
$item = PurchasePlanItem::query()->create(['tenant_id' => $actor->tenant_id, 'created_by' => $actor->id, 'department_id' => $data['department_id'], 'requisition_id' => $requisition?->id, 'responsible_id' => $data['responsible_id'], 'title' => $data['title'], 'nature' => $data['nature']]);
app(RequisitionWorkflow::class)->audit($actor, 'planning.created', $requisition, null, $item->toArray());
return $item;
});
}
public function assign(User $actor, PurchasePlanItem $item, int $responsibleId, int $version, string $reason): void
{
DB::transaction(function () use ($actor, $item, $responsibleId, $version, $reason) {
app(ConflictGuard::class)->lockCompany($actor);
$actor = app(CurrentActor::class)->lock($actor, [PurchasingRole::Admin, PurchasingRole::Procurement]);
$item = PurchasePlanItem::query()->lockForUpdate()->findOrFail($item->id);
$this->version($version, $item->version);
$this->responsible($actor, $responsibleId);
$before = $item->toArray();
$item->update(['responsible_id' => $responsibleId, 'version' => $item->version + 1]);
app(RequisitionWorkflow::class)->audit($actor, 'planning.assigned', null, $before, ['item' => $item->toArray(), 'reason' => $reason]);
});
}
public function link(User $actor, PurchasePlanItem $item, int $requisitionId, int $version, string $reason): void
{
DB::transaction(function () use ($actor, $item, $requisitionId, $version, $reason) {
app(ConflictGuard::class)->lockCompany($actor);
$actor = app(CurrentActor::class)->lock($actor, [PurchasingRole::Admin, PurchasingRole::Procurement]);
$item = PurchasePlanItem::query()->lockForUpdate()->findOrFail($item->id);
$this->version($version, $item->version);
$requisition = Requisition::query()->visibleTo($actor)->findOrFail($requisitionId);
if ($item->requisition_id !== null || $requisition->department_id !== $item->department_id || PurchasePlanItem::query()->where('requisition_id', $requisitionId)->exists()) {
throw ValidationException::withMessages(['requisition_id' => 'A ligação deve ser única, do mesmo departamento, e não pode substituir uma ligação anterior.']);
}
$before = $item->toArray();
$item->update(['requisition_id' => $requisitionId, 'version' => $item->version + 1]);
app(RequisitionWorkflow::class)->audit($actor, 'planning.linked', $requisition, $before, ['item' => $item->toArray(), 'reason' => $reason]);
});
}
public function milestone(User $actor, PurchasePlanItem $item, string $label, string $plannedOn): void
{
DB::transaction(function () use ($actor, $item, $label, $plannedOn) {
app(ConflictGuard::class)->lockCompany($actor);
$actor = app(CurrentActor::class)->lock($actor, [PurchasingRole::Admin, PurchasingRole::Procurement]);
$item = PurchasePlanItem::query()->lockForUpdate()->findOrFail($item->id);
if (PurchasePlanMilestone::query()->where('purchase_plan_item_id', $item->id)->where('label', trim($label))->exists()) {
throw ValidationException::withMessages(['label' => 'Já existe um marco com este nome neste item.']);
}
$milestone = PurchasePlanMilestone::query()->create(['tenant_id' => $actor->tenant_id, 'purchase_plan_item_id' => $item->id, 'created_by' => $actor->id, 'label' => trim($label), 'planned_on' => $plannedOn]);
app(RequisitionWorkflow::class)->audit($actor, 'planning.milestone_created', null, null, $milestone->toArray());
});
}
/** @param array{version:int,planned_on?:string,actual_on?:string|null,reason:string} $data */
public function update(User $actor, PurchasePlanMilestone $milestone, array $data): void
{
DB::transaction(function () use ($actor, $milestone, $data) {
app(ConflictGuard::class)->lockCompany($actor);
$actor = app(CurrentActor::class)->lock($actor);
$item = PurchasePlanItem::query()->visibleTo($actor)->lockForUpdate()->findOrFail($milestone->purchase_plan_item_id);
$milestone = PurchasePlanMilestone::query()->lockForUpdate()->findOrFail($milestone->id);
abort_unless($this->manager($actor) || $item->responsible_id === $actor->id, 403);
abort_if(! $this->manager($actor) && array_key_exists('planned_on', $data), 403);
$this->version($data['version'], $milestone->version);
$before = $milestone->toArray();
$changes = array_intersect_key($data, array_flip(['planned_on', 'actual_on']));
$milestone->update([...$changes, 'version' => $milestone->version + 1]);
app(RequisitionWorkflow::class)->audit($actor, 'planning.milestone_updated', null, $before, ['milestone' => $milestone->toArray(), 'reason' => $data['reason']]);
});
}
}