118 lines
7.1 KiB
PHP
118 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\SupplierEvaluation;
|
|
use App\Models\SupplierResponse;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class SupplierResponseWorkflow
|
|
{
|
|
public function canAct(User $actor, SupplierEvaluation $evaluation, SupplierResponse $record, string $action): bool
|
|
{
|
|
if ($evaluation->status !== 'approved' || $record->tenant_id !== $actor->tenant_id
|
|
|| $record->supplier_evaluation_id !== $evaluation->id
|
|
|| ! app(SupplierImprovementWorkflow::class)->canParticipate($actor, $evaluation)) {
|
|
return false;
|
|
}
|
|
|
|
return match ($action) {
|
|
'edit' => in_array($record->status, ['draft', 'returned'], true),
|
|
'review' => $record->status === 'review' && ! in_array($actor->id, $record->contributors, true),
|
|
default => false,
|
|
};
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function snapshot(SupplierResponse $record): array
|
|
{
|
|
return $record->only(['id', 'supplier_evaluation_id', 'created_by', 'version', 'status', 'payload', 'contributors', 'decision', 'reviewed_by', 'review_reason', 'reviewed_at', 'created_at', 'updated_at']);
|
|
}
|
|
|
|
/** @param array<string,mixed> $payload
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function normalise(array $payload): array
|
|
{
|
|
return ['title' => $payload['title'], 'received_on' => $payload['received_on'] ?? null, 'source_reference' => $payload['source_reference'] ?? '', 'statement' => $payload['statement'] ?? ''];
|
|
}
|
|
|
|
/** @param array<string,mixed> $data */
|
|
public function execute(User $actor, int $evaluationId, ?int $id, string $command, array $data): SupplierResponse
|
|
{
|
|
return DB::transaction(function () use ($actor, $evaluationId, $id, $command, $data) {
|
|
app(ConflictGuard::class)->lockCompany($actor);
|
|
$actor = app(CurrentActor::class)->lock($actor, SupplierEvaluationWorkflow::ROLES);
|
|
$evaluation = SupplierEvaluation::query()->visibleTo($actor)->lockForUpdate()->findOrFail($evaluationId);
|
|
abort_unless($evaluation->status === 'approved', 409, 'O contraditório refere-se à revisão aprovada.');
|
|
abort_unless(app(SupplierImprovementWorkflow::class)->canParticipate($actor, $evaluation), 403, 'Existe conflito no âmbito da avaliação.');
|
|
if ($command === 'create') {
|
|
$payload = $this->normalise($data['payload']);
|
|
$hash = hash('sha256', json_encode([$evaluationId, $payload], JSON_THROW_ON_ERROR));
|
|
$existing = SupplierResponse::query()->where('operation_key', $data['operation_key'])->first();
|
|
if ($existing) {
|
|
abort_unless($existing->created_by === $actor->id && $existing->creation_hash === $hash, 409, 'Pedido já utilizado com outro conteúdo.');
|
|
|
|
return $existing;
|
|
}
|
|
$record = SupplierResponse::query()->create(['tenant_id' => $actor->tenant_id, 'supplier_evaluation_id' => $evaluationId,
|
|
'created_by' => $actor->id, 'operation_key' => $data['operation_key'], 'creation_hash' => $hash,
|
|
'payload' => $payload, 'contributors' => [$actor->id]]);
|
|
$before = null;
|
|
} else {
|
|
$record = SupplierResponse::query()->where('supplier_evaluation_id', $evaluationId)->lockForUpdate()->findOrFail($id);
|
|
if ($record->version !== (int) $data['version']) {
|
|
throw ValidationException::withMessages(['version' => 'O registo foi alterado. Consulte a versão actual antes de comparar; os seus dados não foram sobrepostos.']);
|
|
}
|
|
abort_if($record->status === 'decided', 409, 'A conclusão já está registada e preservada.');
|
|
$before = $this->snapshot($record);
|
|
if ($command === 'save') {
|
|
abort_unless(in_array($record->status, ['draft', 'returned'], true), 409);
|
|
abort_unless($this->canAct($actor, $evaluation, $record, 'edit'), 403);
|
|
$payload = $this->normalise($data['payload']);
|
|
if ($payload === $record->payload) {
|
|
return $record;
|
|
}
|
|
$record->fill(['payload' => $payload, 'contributors' => array_values(array_unique([...$record->contributors, $actor->id]))]);
|
|
} elseif ($command === 'submit') {
|
|
abort_unless(in_array($record->status, ['draft', 'returned'], true), 409);
|
|
abort_unless($this->canAct($actor, $evaluation, $record, 'edit'), 403);
|
|
$errors = [];
|
|
foreach (['received_on', 'source_reference', 'statement'] as $key) {
|
|
if (trim($record->payload[$key] ?? '') === '') {
|
|
$errors['payload.'.$key] = 'Complete a data, a origem documental e os argumentos recebidos antes de submeter.';
|
|
}
|
|
}
|
|
if (($record->payload['received_on'] ?? '') > today()->toDateString()) {
|
|
$errors['payload.received_on'] = 'A data de recepção não pode ser futura.';
|
|
}
|
|
if ($errors) {
|
|
throw ValidationException::withMessages($errors);
|
|
}
|
|
$record->fill(['status' => 'review', 'contributors' => array_values(array_unique([...$record->contributors, $actor->id]))]);
|
|
} else {
|
|
abort_unless(in_array($command, ['return', 'decide'], true) && $record->status === 'review', 409);
|
|
abort_unless($this->canAct($actor, $evaluation, $record, 'review'), 403, 'A análise deve ser concluída por outra pessoa que não preparou o registo.');
|
|
if (mb_strlen(trim($data['reason'] ?? '')) < 10) {
|
|
throw ValidationException::withMessages(['reason' => 'Fundamente a análise ou a devolução.']);
|
|
}
|
|
if ($command === 'decide') {
|
|
if (in_array($data['outcome'], ['accepted', 'partially_accepted'], true) && mb_strlen(trim($data['follow_up'] ?? '')) < 10) {
|
|
throw ValidationException::withMessages(['follow_up' => 'Descreva como será dado seguimento ao acolhimento dos argumentos.']);
|
|
}
|
|
$record->decision = ['outcome' => $data['outcome'], 'reason' => $data['reason'], 'follow_up' => $data['follow_up'] ?? '', 'response_reference' => $data['response_reference'] ?? ''];
|
|
}
|
|
$record->fill(['status' => $command === 'decide' ? 'decided' : 'returned', 'reviewed_by' => $actor->id, 'reviewed_at' => now(), 'review_reason' => $data['reason']]);
|
|
}
|
|
$record->version++;
|
|
$record->save();
|
|
}
|
|
app(RequisitionWorkflow::class)->audit($actor, 'supplier_response.'.$command, null, $before, $this->snapshot($record));
|
|
|
|
return $record;
|
|
});
|
|
}
|
|
}
|