120 lines
7.0 KiB
PHP
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']]);
|
|
});
|
|
}
|
|
}
|