122 lines
9.4 KiB
PHP
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');
|
|
}
|
|
}
|