122 lines
6.9 KiB
PHP
122 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\Requisition;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class WorkflowNotices
|
|
{
|
|
/** @param array<string,mixed> $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<array<string,mixed>> */
|
|
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]);
|
|
}
|
|
});
|
|
}
|
|
}
|