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 $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']]); }); } }