Files
Investidor-V2/app/Services/CompanyComparison.php
T

209 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
/** Informative comparison, stored in the existing encrypted dossier and event history. */
final class CompanyComparison
{
public const EVENT = 'company_comparison_saved';
public function __construct(private readonly VirtualDesk $desk) {}
/** @return array<string,mixed> */
public function definition(): array
{
return config('company-comparison');
}
/** @param array<array-key,mixed> $questions
* @return list<string> */
private function questionKeys(array $questions): array
{
$keys = [];
foreach (array_keys($questions) as $key) {
if (! is_string($key)) {
throw new \LogicException('Comparison question identifiers must be strings.');
}
$keys[] = $key;
}
return $keys;
}
/** @param array<string,mixed> $input
* @return array<string,string> */
public function answers(array $input): array
{
return $this->normalize($input, $this->definition()['questions']);
}
/** @param array<string,mixed> $input
* @param array<array-key,mixed> $questions
* @return array<string,string> */
private function normalize(array $input, array $questions): array
{
$keys = $this->questionKeys($questions);
$rules = ['answers' => ['sometimes', 'array:'.implode(',', $keys)]];
foreach ($keys as $key) {
$rules['answers.'.$key] = ['nullable', 'string', Rule::in(array_keys($questions[$key]['options']))];
}
$values = Validator::make($input, $rules)->validate()['answers'] ?? [];
$answers = [];
foreach ($keys as $key) {
$answers[$key] = is_string($values[$key] ?? null) ? $values[$key] : 'unknown';
}
return $answers;
}
/** @param array<string,string> $answers
* @return list<string> */
public function active(array $answers): array
{
return in_array($answers['purpose'], ['representation', 'cooperation'], true) ? ['purpose', 'regulated'] : $this->questionKeys($this->definition()['questions']);
}
/** @param array<string,string> $answers
* @return array<string,mixed> */
public function evaluate(array $answers): array
{
$definition = $this->definition();
$active = $this->active($answers);
$pending = array_values(array_filter($active, fn ($key) => $key !== 'preferred' && $answers[$key] === 'unknown'));
$tasks = ['Definir contribuições, gestão, direitos e obrigações no acto a preparar.', 'Conferir os actos e o registo aplicáveis à alternativa escolhida.'];
if (in_array('capital', $active, true)) {
$tasks[] = $answers['capital'] === 'defined' ? 'Conferir os valores e o modo de realização das contribuições definidas.' : 'Definir o capital e as contribuições ainda em falta.';
if (in_array($answers['management'], ['participants', 'both'], true)) {
$tasks[] = 'Definir poderes de gestão e regras de deliberação dos participantes.';
}
if (in_array($answers['management'], ['appointed', 'both'], true)) {
$tasks[] = 'Definir a nomeação, o mandato e a prestação de contas da administração.';
}
if ($answers['responsibility'] !== 'unknown') {
$tasks[] = 'Conferir no registo e no contrato o regime de responsabilidade e as excepções aplicáveis.';
}
if ($answers['listing'] === 'yes') {
$tasks[] = 'Conferir os requisitos específicos da emissão e do mercado pretendido.';
}
}
if ($answers['regulated'] !== 'no') {
$tasks[] = 'Conferir as regras e habilitações específicas da actividade.';
}
$route = match ($answers['purpose']) {
'representation' => 'Compare a representação da sociedade existente; não é uma nova forma societária (artigo 69).',
'cooperation' => 'Prepare o acordo de cooperação; consórcio não é apresentado como uma forma societária autónoma.',
default => 'Compare alternativas para discutir a constituição; não existe uma forma universalmente preferível.',
};
$forms = [];
if (! in_array($answers['purpose'], ['representation', 'cooperation'], true)) {
foreach ($definition['forms'] as $code => $form) {
$constraints = [];
$notes = [];
if ($code === 'individual' && $answers['participants'] === 'several') {
$constraints[] = 'A titularidade individual não corresponde a vários titulares (artigo 3).';
}
if ($code === 'collective' && $answers['participants'] === 'one') {
$constraints[] = 'São necessários pelo menos dois sócios (artigo 264).';
}
if ($code === 'simplified' && $answers['listing'] === 'yes') {
$constraints[] = 'As acções desta forma não podem ser negociadas em Bolsa (artigo 444).';
}
if ($answers['participants'] === 'one' && in_array($code, ['quotas', 'anonymous', 'simplified'], true)) {
$notes[] = 'Comparar a modalidade unipessoal (artigo 257).';
}
if (($answers['participation'] === 'shares' && $code === 'quotas') || ($answers['participation'] === 'quotas' && in_array($code, ['anonymous', 'simplified'], true))) {
$notes[] = 'A estrutura de participação difere da preferência indicada.';
}
if ($answers['participation'] === 'work') {
$notes[] = $code === 'collective' ? 'Conferir a contribuição em trabalho (artigos 264–266).' : 'Conferir como realizar as contribuições pretendidas.';
}
$forms[] = ['code' => $code, ...$form, 'constraints' => $constraints, 'notes' => $notes,
'selected' => $answers['preferred'] === $code, 'source_href' => $definition['source']['url'].'#page='.$form['page']];
}
}
return ['engine_version' => $definition['version'], 'source' => $definition['source'], 'questions' => $definition['questions'], 'active' => $active,
'pending' => $pending, 'route' => $route, 'forms' => $forms, 'preparation' => $tasks,
'scope' => 'Comparação informativa da edição indicada; a actividade, o contrato e as regras especiais continuam a exigir conferência própria.'];
}
/** @param array<string,mixed> $input
* @return array<string,mixed> */
public function view(User $actor, string $id, array $input): array
{
$case = $this->desk->case($actor, $id);
$saved = $this->desk->decode($case->payload)['company_comparison'] ?? null;
$v = Validator::make($input, ['revision' => 'nullable|uuid', 'history_before' => 'nullable|integer|min:1'])->validate();
if (! empty($v['revision'])) {
if (array_key_exists('answers', $input)) {
throw ValidationException::withMessages(['answers' => 'Abra a preparação actual para comparar outras respostas; a revisão histórica conserva os dados guardados.']);
}
$row = DB::table('desk_events')->where('case_id', $id)->where('action', self::EVENT)->where('id', $v['revision'])->first();
abort_unless($row !== null, 404);
$saved = $this->desk->decode($row->payload)['snapshot'];
}
$answers = array_key_exists('answers', $input) ? $this->answers($input) : ($saved['answers'] ?? $this->answers([]));
$result = array_key_exists('answers', $input) || ! $saved ? $this->evaluate($answers) : $saved['result'];
$events = DB::table('desk_events')->where('case_id', $id)->where('action', self::EVENT)
->when(! empty($v['history_before']), fn ($q) => $q->where('event_order', '<', $v['history_before']))
->orderByDesc('event_order')->limit(26)->get(['id', 'payload', 'event_order']);
$history = $events->take(25)->map(function ($row) {
$snapshot = $this->desk->decode($row->payload)['snapshot'];
return ['id' => $row->id, 'revision' => $snapshot['revision'], 'at' => $snapshot['at'], 'engine_version' => $snapshot['result']['engine_version']];
})->all();
return ['case' => ['id' => $id, 'title' => $case->title, 'version' => (int) $case->version],
'canWrite' => ! $case->closed_at && $this->desk->owns($actor, $case), 'answers' => $answers, 'result' => $result,
'saved' => $saved, 'history' => $history, 'historyNext' => $events->count() > 25 ? $events[24]->event_order : null,
'historical' => ! empty($v['revision']), 'workingCopy' => array_key_exists('answers', $input)];
}
/** @param array<string,mixed> $input
* @return array<string,mixed> */
public function save(User $actor, string $id, array $input): array
{
$this->desk->case($actor, $id, true);
$v = Validator::make($input, ['version' => 'required|integer|min:1', 'operation_key' => 'required|uuid'])->validate();
$v['version'] = (int) $v['version'];
return DB::transaction(function () use ($actor, $id, $v, $input) {
$case = $this->desk->case($actor, $id, true, true);
foreach (DB::table('desk_events')->where('case_id', $id)->where('action', self::EVENT)->orderByDesc('event_order')->cursor() as $event) {
$data = $this->desk->decode($event->payload);
if ($data['operation_key'] === $v['operation_key']) {
$answers = $this->normalize($input, $data['snapshot']['result']['questions']);
$hash = hash('sha256', json_encode([$v['version'], $answers], JSON_THROW_ON_ERROR));
abort_unless(hash_equals($data['request_hash'], $hash), 409, 'Esta operação já guardou respostas diferentes. Reabra a comparação.');
return ['saved' => true, 'replayed' => true, 'revision' => $data['snapshot']['revision']];
}
}
$answers = $this->answers($input);
$hash = hash('sha256', json_encode([$v['version'], $answers], JSON_THROW_ON_ERROR));
abort_if($case->closed_at || (int) $case->version !== (int) $v['version'], 409, 'O dossier mudou ou foi fechado. Conserve as respostas e recupere a versão actual antes de guardar.');
$payload = $this->desk->decode($case->payload);
$result = $this->evaluate($answers);
$previous = $payload['company_comparison'] ?? null;
if ($previous !== null && $previous['answers'] === $answers && $previous['result'] === $result) {
return ['saved' => true, 'replayed' => false, 'unchanged' => true, 'revision' => $previous['revision']];
}
$snapshot = ['revision' => (int) ($payload['company_comparison']['revision'] ?? 0) + 1, 'answers' => $answers,
'result' => $result, 'at' => now()->toIso8601String(), 'actor_id' => $actor->id];
$payload['company_comparison'] = $snapshot;
$case->version++;
DB::table('desk_cases')->where('id', $id)->update(['payload' => $this->desk->encode($payload), 'version' => $case->version, 'updated_at' => now()]);
$this->desk->event($case, $actor, self::EVENT, ['operation_key' => $v['operation_key'], 'request_hash' => $hash, 'snapshot' => $snapshot]);
return ['saved' => true, 'replayed' => false, 'revision' => $snapshot['revision']];
});
}
}