$input */ public function refresh(User $actor, int $id, array $input): void { $data = Validator::make($input, ['operation_key' => 'required|uuid', 'process_version' => 'required|integer|min:1', 'step_id' => 'required|integer|min:1', 'reason' => 'required|string|min:10|max:4000'])->validate(); DB::transaction(function () use ($actor, $id, $data) { app(ConflictGuard::class)->lockCompany($actor); $actor = app(CurrentActor::class)->lock($actor); abort_unless($actor->hasFunctions(['admin', 'procurement']) && ! app(OrganizationAccess::class)->maintenanceOnly($actor), 403); abort_unless(Schema::hasTable('workflow_notices'), 503, 'Instalação dos avisos pendente.'); $request = Requisition::query()->visibleTo($actor)->lockForUpdate()->findOrFail($id); $hash = hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)); $old = AuditLog::query()->where('user_id', $actor->id)->where('requisition_id', $id)->where('action', 'workflow.routing_refreshed') ->where('after->operation_key', $data['operation_key'])->first(); if ($old !== null) { if (($old->after['input_hash'] ?? null) !== $hash) { throw ValidationException::withMessages(['operation_key' => 'Chave utilizada com outro conteúdo.']); } return; } $step = $request->steps->firstWhere('decision', 'pending'); if ($request->status !== 'pending' || $step === null || $step->id !== (int) $data['step_id'] || (int) $request->getAttribute('process_version') !== (int) $data['process_version']) { throw ValidationException::withMessages(['routing' => 'A etapa ou o pedido mudou. Actualize antes de encaminhar.']); } $before = DB::table('workflow_notices')->where('approval_step_id', $step->id)->pluck('recipient_id')->all(); $this->capture($request, $actor); $after = DB::table('workflow_notices')->where('approval_step_id', $step->id)->pluck('recipient_id')->all(); app(RequisitionWorkflow::class)->audit($actor, 'workflow.routing_refreshed', $request, ['recipients' => $before], ['recipients' => $after, 'step_id' => $step->id, 'reason' => $data['reason'], 'operation_key' => $data['operation_key'], 'input_hash' => $hash]); }); } public function outcome(Requisition $requisition, int $stepId): void { if (! Schema::hasTable('workflow_notices') || ! in_array($requisition->status, ['approved', 'rejected'], true)) { return; } if (DB::connection()->transactionLevel() === 0) { throw new \LogicException('Aviso de resultado exige a transacção da decisão.'); } $step = $requisition->steps()->whereKey($stepId)->firstOrFail(); if ($step->decision !== $requisition->status) { throw new \LogicException('O aviso deve referenciar a decisão final efectivamente registada.'); } $recipient = User::query()->where('tenant_id', $requisition->tenant_id)->findOrFail($requisition->requester_id); DB::table('workflow_notices')->insertOrIgnore(['tenant_id' => $requisition->tenant_id, 'requisition_id' => $requisition->id, 'approval_step_id' => $step->id, 'recipient_id' => $recipient->id, 'reference' => $requisition->number, 'role' => $step->role, 'created_at' => now()]); } public function capture(Requisition $requisition, User $actor): void { if (! Schema::hasTable('workflow_notices')) { return; // Existing installations keep the approved workflow until the additive migration. } if (DB::connection()->transactionLevel() === 0) { throw new \LogicException('Aviso exige a transacção do processo.'); } $requisition->unsetRelation('steps'); $routing = app(ApprovalRouting::class)->current($requisition, $actor); foreach ($routing['people'] as $person) { DB::table('workflow_notices')->insertOrIgnore(['tenant_id' => $requisition->tenant_id, 'requisition_id' => $requisition->id, 'approval_step_id' => $routing['step_id'], 'recipient_id' => $person['id'], 'reference' => $requisition->number, 'role' => $routing['role'], 'created_at' => now()]); } } /** @return list> */ public function visible(User $actor): array { if (! Schema::hasTable('workflow_notices') || app(OrganizationAccess::class)->maintenanceOnly($actor) || app(OrganizationAccess::class)->permissions($actor) === []) { return []; } $items = []; foreach (DB::table('workflow_notices')->where('tenant_id', $actor->tenant_id)->where('recipient_id', $actor->id)->orderByDesc('id')->get() as $notice) { $request = Requisition::query()->visibleTo($actor)->whereKey($notice->requisition_id)->first(); if ($request === null) { continue; } $step = $request->steps->firstWhere('id', $notice->approval_step_id); $outcome = $request->requester_id === $actor->id && $step !== null && in_array($step->decision, ['approved', 'rejected'], true); if (! $outcome && (! $request->canDecide($actor) || $request->steps->firstWhere('decision', 'pending')?->id !== $notice->approval_step_id)) { continue; } $items[] = ['id' => $notice->id, 'reference' => $notice->reference, 'role' => $notice->role, 'kind' => $outcome ? 'outcome' : 'task', 'outcome' => $outcome ? $step->decision : null, 'current_status' => $request->status, 'created_at' => $notice->created_at, 'read_at' => $notice->read_at, 'href' => '/requisicoes/'.$request->id]; } return $items; } public function read(User $actor, int $id): void { DB::transaction(function () use ($actor, $id) { app(ConflictGuard::class)->lockCompany($actor); $actor = app(CurrentActor::class)->lock($actor); $notice = collect($this->visible($actor))->firstWhere('id', $id); abort_unless($notice !== null, 404); if ($notice['read_at'] === null) { DB::table('workflow_notices')->where('tenant_id', $actor->tenant_id)->where('recipient_id', $actor->id)->where('id', $id)->update(['read_at' => now()]); app(RequisitionWorkflow::class)->audit($actor, 'workflow.notice_read', null, null, ['notice_id' => $id]); } }); } }