91 lines
5.1 KiB
PHP
91 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Opportunity;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
/** Explicit internal review conditions in the existing workflow JSON and audit. */
|
|
class OpportunityReviewConditions
|
|
{
|
|
/** @param array<string,mixed> $data
|
|
* @param array<string,mixed> $snapshot
|
|
* @param array<string,mixed> $context */
|
|
public function review(Opportunity $record, User $actor, array $data, array $snapshot, array &$context): void
|
|
{
|
|
$gate = $data['gate'];
|
|
foreach ($data['resolve_condition_ids'] ?? [] as $id) {
|
|
$condition = $context['conditions'][$id] ?? null;
|
|
$this->require($condition !== null && $condition['gate'] === $gate && $condition['status'] === 'open', 'resolve_condition_ids', 'Selecione uma condição aberta desta revisão.');
|
|
$this->require(($data['gate_decision'] ?? '') === 'approved', 'gate_decision', 'A resolução exige verificação interna aprovada com evidência.');
|
|
$documents = [];
|
|
foreach ($data['evidence_document_ids'] ?? [] as $documentId) {
|
|
$documents[$documentId] = $snapshot['resources']['documents'][$documentId]['sha256'];
|
|
}
|
|
$this->require($documents !== [], 'evidence_document_ids', 'A resolução exige documento ativo e analisado desta oportunidade.');
|
|
$context['conditions'][$id]['status'] = 'resolved';
|
|
$context['conditions'][$id]['resolution'] = ['actor_id' => $actor->id, 'at' => now()->toIso8601String(), 'reason' => $data['reason'], 'document_hashes' => $documents, 'content_revision' => $context['content_revision'] ?? 1, 'scope' => 'internal_verification_not_external_decision'];
|
|
}
|
|
if (! empty($data['condition'])) {
|
|
$input = $data['condition'];
|
|
$subject = User::query()->whereKey($input['responsible_user_id'])->first();
|
|
$access = app(PortalProfileAccess::class);
|
|
$this->require($subject !== null && $access->canView($subject, $record)
|
|
&& (($access->scoped($subject, 'promoter', $record->organization_id, $record->opportunity_category) && $access->operational($subject, 'edit', $record->id))
|
|
|| ($access->scoped($subject, 'validator', $record->organization_id, $record->opportunity_category) && $access->operational($subject, 'validate', $record->id))),
|
|
'condition.responsible_user_id', 'Selecione um responsável autorizado neste âmbito; não atribua responsabilidade externa.');
|
|
$id = (string) Str::uuid();
|
|
$context['conditions'][$id] = ['id' => $id, 'gate' => $gate, 'description' => $input['description'], 'responsible_user_id' => $subject->id,
|
|
'responsible_name' => $subject->name, 'due_on' => $input['due_on'] ?? null, 'blocking_action' => $input['blocking_action'],
|
|
'status' => 'open', 'created_by' => $actor->id, 'created_at' => now()->toIso8601String(), 'scope' => 'internal_review_condition', 'resolution_history' => []];
|
|
}
|
|
}
|
|
|
|
/** @param array<string,mixed> $context
|
|
* @param array<string,mixed> $snapshot
|
|
* @param list<string> $reopened */
|
|
public function reconcile(array &$context, array $snapshot, array $reopened): void
|
|
{
|
|
foreach ($context['conditions'] ?? [] as $id => $condition) {
|
|
if ($condition['status'] !== 'resolved') {
|
|
continue;
|
|
}
|
|
$matches = ! in_array($condition['gate'], $reopened, true);
|
|
foreach ($condition['resolution']['document_hashes'] ?? [] as $documentId => $sha) {
|
|
$matches = $matches && ($snapshot['resources']['documents'][$documentId]['sha256'] ?? null) === $sha;
|
|
}
|
|
if (! $matches) {
|
|
$context['conditions'][$id]['resolution_history'][] = $condition['resolution'];
|
|
unset($context['conditions'][$id]['resolution']);
|
|
$context['conditions'][$id]['status'] = 'open';
|
|
$context['conditions'][$id]['reopened_at'] = now()->toIso8601String();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @param array<string,mixed> $context
|
|
* @param array<string,mixed> $snapshot */
|
|
public function authorize(array $context, array $snapshot, string $action): void
|
|
{
|
|
foreach ($context['conditions'] ?? [] as $condition) {
|
|
if ($action === 'approve' && $condition['blocking_action'] !== 'approve') {
|
|
continue;
|
|
}
|
|
$resolved = $condition['status'] === 'resolved';
|
|
foreach ($condition['resolution']['document_hashes'] ?? [] as $id => $sha) {
|
|
$resolved = $resolved && ($snapshot['resources']['documents'][$id]['sha256'] ?? null) === $sha;
|
|
}
|
|
$this->require($resolved, 'conditions.'.$condition['id'], 'Condição pendente: '.$condition['description'].'. Responsável: '.$condition['responsible_name'].'. Prazo: '.($condition['due_on'] ?? 'não definido').'.');
|
|
}
|
|
}
|
|
|
|
private function require(bool $ok, string $field, string $message): void
|
|
{
|
|
if (! $ok) {
|
|
throw ValidationException::withMessages([$field => $message]);
|
|
}
|
|
}
|
|
}
|