Files
Investidor-V2/app/Services/Ai/InvestorGuidanceRules.php
T
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

122 lines
9.4 KiB
PHP

<?php
namespace App\Services\Ai;
/** Internal preparation rules; never calculate tax, returns or eligibility. */
class InvestorGuidanceRules
{
public const VERSION = 'investor-guidance-2026-10-08-v3';
public const FIELDS = ['project_title', 'project_summary', 'activity', 'target_customers', 'market_evidence', 'location',
'location_evidence', 'resource_plan', 'resource_evidence', 'funding_evidence', 'expected_start_date', 'ready_date', 'schedule',
'demand_quantity', 'capacity_quantity', 'quantity_unit', 'quantity_period', 'resources_required', 'resources_available', 'resource_unit',
'funding_required', 'funding_committed', 'funding_currency', 'funding_ready_date', 'completion_date'];
/** @param array<string,mixed> $facts
* @param array<string,array<string,mixed>> $evidence
* @return list<array<string,mixed>> */
public function evaluate(array $facts, array $evidence = []): array
{
$phases = [
'idea' => ['Definir a actividade e o objectivo', ['project_title', 'project_summary', 'activity'], 'Confirme o que pretende produzir ou prestar.', 'plan'],
'market' => ['Documentar clientes e hipóteses de mercado', ['target_customers', 'market_evidence'], 'Reúna evidência de clientes; uma hipótese não demonstra procura.', 'opportunities'],
'location' => ['Comparar locais e requisitos da actividade', ['location', 'location_evidence'], 'Confirme terreno, acesso, infraestrutura e licenciamento para cada alternativa.', 'locations'],
'resources' => ['Preparar recursos e equipa', ['resource_plan', 'resource_evidence'], 'Distinga recursos declarados de disponibilidade comprovada.', 'partners'],
'finance' => ['Explorar financiamento e cenários', ['funding_evidence'], 'Reveja os pressupostos e os cenários no simulador determinístico.', 'simulator'],
'execution' => ['Reconciliar calendário e dependências', ['expected_start_date', 'ready_date', 'schedule'], 'Confirme datas, dependências e responsáveis antes de assumir marcos.', 'plan'],
'support' => ['Preparar o pedido e acompanhar a resposta', [], 'Prepare um rascunho no Balcão; reveja e submeta pessoalmente.', 'support'],
];
$result = [];
foreach ($phases as $code => [$objective, $fields, $explanation, $action]) {
$known = [];
$unknown = [];
$phaseEvidence = [];
$risks = [];
foreach ($fields as $field) {
$value = trim((string) ($facts[$field] ?? ''));
$record = $evidence[$field] ?? null;
$state = $record['state'] ?? ($value !== '' ? 'investor_declared_not_verified' : 'absent');
if (in_array($field, InvestorGuidanceEvidence::FIELDS, true)) {
$phaseEvidence[] = ['field' => $field, 'state' => $state, 'documents' => $record['documents'] ?? []];
if ($state !== 'internally_verified') {
$message = match ($state) {
'absent' => 'Ainda não existe informação ou documento para este pressuposto.',
'investor_declared_not_verified' => 'A declaração do investidor ainda não foi conferida.',
'attached_unchecked' => 'Existe um documento associado, sem conferência interna deste pressuposto.',
'expired' => 'A validade documental registada terminou; actualize a evidência.',
'superseded' => 'O ficheiro ou a versão mudou depois da conferência; é necessária nova análise.',
'contradiction' => 'Uma conferência interna identificou contradição no âmbito descrito.',
'issue_identified' => 'Uma conferência interna identificou um problema no âmbito descrito.',
default => 'A evidência não está disponível para utilização; consulte o motivo documental.',
};
$risks[] = ['code' => $field.'_'.$state, 'classification' => $state === 'contradiction' ? 'documented_contradiction' : 'insufficient_information', 'explanation' => $message, 'field' => $field, 'restriction' => false];
}
}
if ($value !== '') {
$known[] = ['field' => $field, 'value' => $value, 'status' => 'investor_declared_not_verified'];
} elseif (! $record) {
$unknown[] = $field;
}
}
$result[] = ['id' => $code, 'objective' => $objective, 'known' => $known, 'questions' => $unknown,
'recommendations' => [[
'code' => $code.'_preparation', 'kind' => 'automatic_recommendation', 'classification' => $unknown ? 'insufficient_information' : 'verification_required',
'explanation' => $explanation, 'action' => $action, 'assumptions' => $known,
'source' => ['identifier' => self::VERSION, 'reference' => 'app/Services/Ai/InvestorGuidanceRules.php#'.$code, 'date' => '2026-10-08', 'nature' => 'internal_preparation_rule'],
'limitations' => ['Uma conferência interna só sustenta o âmbito descrito pelo verificador; não aprova procura, infraestrutura, financiamento, registo ou licença institucional.', 'Sem previsão de sucesso, taxa fiscal ou elegibilidade inferida.'],
]], 'evidence' => $phaseEvidence, 'risks' => $risks];
}
if (($facts['expected_start_date'] ?? '') && ($facts['ready_date'] ?? '') && $facts['expected_start_date'] < $facts['ready_date']) {
$result[5]['risks'][] = ['code' => 'schedule_contradiction', 'classification' => 'declared_contradiction',
'explanation' => 'O início indicado antecede a disponibilidade declarada dos recursos.', 'basis' => ['expected_start_date' => $facts['expected_start_date'], 'ready_date' => $facts['ready_date']],
'restriction' => false];
}
$result[4]['risks'][] = ['code' => 'tax_eligibility_not_inferred', 'classification' => 'insufficient_information', 'explanation' => 'A evidência do financiamento não determina taxas fiscais ou elegibilidade. Consulte os pressupostos, fontes e versões no simulador.', 'restriction' => false];
foreach ([
[1, 'demand_quantity', 'capacity_quantity', ['quantity_unit', 'quantity_period'], 'A procura declarada excede a capacidade declarada para a mesma unidade e período.'],
[3, 'resources_required', 'resources_available', ['resource_unit'], 'Os recursos disponíveis declarados são inferiores aos necessários declarados.'],
[4, 'funding_required', 'funding_committed', ['funding_currency'], 'O financiamento comprometido declarado é inferior ao necessário declarado na mesma moeda.'],
] as [$phase, $required, $available, $scope, $explanation]) {
$values = array_intersect_key($facts, array_flip([$required, $available, ...$scope]));
if (! array_filter($values, fn ($value) => $value !== null && $value !== '')) {
continue;
}
$complete = true;
foreach ([$required, $available, ...$scope] as $field) {
if (! isset($facts[$field]) || trim((string) $facts[$field]) === '') {
$complete = false;
}
}
// Compare declared magnitudes only: this is no forecast, valuation or financial calculation.
$left = $facts[$required] ?? null;
$right = $facts[$available] ?? null;
$valid = is_scalar($left) && is_scalar($right)
&& preg_match('/^\d{1,12}(\.\d{1,2})?$/', (string) $left)
&& preg_match('/^\d{1,12}(\.\d{1,2})?$/', (string) $right);
if (! $complete || ! $valid) {
$result[$phase]['risks'][] = ['code' => $required.'_comparison_incomplete', 'classification' => 'insufficient_information',
'explanation' => 'Faltam valores comparáveis, unidade, período ou moeda para esta comparação.', 'basis' => $values, 'restriction' => false];
} elseif ($this->declaredMagnitude((string) $left) > $this->declaredMagnitude((string) $right)) {
$result[$phase]['risks'][] = ['code' => $required.'_declared_gap', 'classification' => 'declared_contradiction',
'explanation' => $explanation.' Estes valores são declarações, não disponibilidade comprovada.', 'basis' => $values, 'restriction' => false];
}
}
foreach ([['completion_date', 'expected_start_date', 'A conclusão declarada antecede o início.'],
['expected_start_date', 'funding_ready_date', 'O início declarado antecede a disponibilidade declarada do financiamento.']] as [$first, $second, $explanation]) {
if (! empty($facts[$first]) && ! empty($facts[$second]) && $facts[$first] < $facts[$second]) {
$result[5]['risks'][] = ['code' => $first.'_'.$second.'_contradiction', 'classification' => 'declared_contradiction',
'explanation' => $explanation, 'basis' => [$first => $facts[$first], $second => $facts[$second]], 'restriction' => false];
}
}
return $result;
}
private function declaredMagnitude(string $value): int
{
$parts = explode('.', $value);
return (int) $parts[0] * 100 + (int) str_pad($parts[1] ?? '', 2, '0');
}
}