Files
compras/app/Services/WorkflowNotices.php
2026-11-02 08:08:05 +02:00

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