fix: recover simulator cashflow and verify QA continuity and report assumptions

This commit is contained in:
2026-10-20 22:00:48 +02:00
parent a690c665fb
commit c22003a3da
20 changed files with 773 additions and 95 deletions
@@ -37,7 +37,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
'contact_province', 'contact_district', 'preferred_channel',
];
$economic = [
'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'cashflow_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
'competitor_assumption', 'sales_channel_assumption', 'marketing_approach', 'location_rationale', 'project_risk_responses_snapshot', 'project_questionnaire_version',
'customer_validation_status', 'cost_estimate_status', 'location_assessment_status', 'funding_status', 'receivables_timing_status',
'project_type_other', 'sector_id', 'activity_id', 'products_services', 'maturity_stage', 'expected_start_date',
@@ -71,6 +71,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
'economic_snapshot.detail_level' => ['nullable', 'in:simple,advanced'],
'economic_snapshot.business_plan_snapshot' => ['nullable', 'string', 'max:60000'],
'economic_snapshot.financial_tools_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.cashflow_snapshot' => ['nullable', 'string', 'max:20000'],
'economic_snapshot.project_team_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.project_execution_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.project_risk_responses_snapshot' => ['nullable', 'string', 'max:30000'],
@@ -16,32 +16,32 @@ final class SimulationTaxParameterResolver
public function validateParameters(array $parameters): void
{
if ($parameters === [] || count($parameters) > 100) {
throw new InvalidArgumentException('Provide between 1 and 100 versioned parameters.');
throw new InvalidArgumentException('Indique entre 1 e 100 parâmetros versionados.');
}
foreach ($parameters as $code => $parameter) {
if (! is_string($code) || ! preg_match('/^[A-Z][A-Z0-9._-]{1,63}$/', $code) || ! is_array($parameter)
|| array_diff(array_keys($parameter), ['value', 'unit', 'conditions']) !== []
|| array_diff(['value', 'unit', 'conditions'], array_keys($parameter)) !== []) {
throw new InvalidArgumentException('Each parameter requires only value, unit, and conditions.');
throw new InvalidArgumentException('Cada parâmetro deve conter apenas valor, unidade e condições.');
}
if ((! is_string($parameter['value']) && ! is_int($parameter['value'])) || (string) $parameter['value'] === ''
|| ! is_string($parameter['unit']) || ! preg_match('/^[a-z][a-z0-9._-]{0,31}$/', $parameter['unit'])
|| ! is_array($parameter['conditions'])) {
throw new InvalidArgumentException('Invalid parameter value, unit, or conditions.');
throw new InvalidArgumentException('Reveja o valor, a unidade e as condições do parâmetro.');
}
$isNumericValue = preg_match('/^-?\d+(?:\.\d+)?$/', (string) $parameter['value']) === 1;
if (in_array($parameter['unit'], ['percent', 'mzn', 'amount', 'days', 'count', 'mzn_per_unit', 'per_unit'], true) && ! $isNumericValue) {
throw new InvalidArgumentException('Numeric parameter units require a plain decimal value.');
throw new InvalidArgumentException('Esta unidade requer um valor numérico decimal.');
}
if ($isNumericValue) {
$value = BigRational::of((string) $parameter['value']);
if ($value->isNegative() || ($parameter['unit'] === 'percent' && $value->compareTo(100) > 0)) {
throw new InvalidArgumentException('Numeric parameters cannot be negative; percentages must be between 0 and 100.');
throw new InvalidArgumentException('Os valores não podem ser negativos; as percentagens devem estar entre 0 e 100.');
}
}
foreach ($parameter['conditions'] as $condition => $expected) {
if (! is_string($condition) || ! is_string($expected)) {
throw new InvalidArgumentException('Conditions must be a flat map of string assumptions.');
throw new InvalidArgumentException('Indique as condições numa lista simples de nomes e valores em texto.');
}
}
}
@@ -59,7 +59,7 @@ final class SimulationTaxParameterResolver
$start = $this->date($periodStart);
$end = $this->date($periodEnd);
if ($end < $start || $start->diff($end)->days > 365) {
throw new InvalidArgumentException('Invalid effective period.');
throw new InvalidArgumentException('Reveja o período de aplicação indicado.');
}
$rows = DB::table('simulation_tax_rule_versions as rule')
@@ -315,7 +315,7 @@ final class SimulationTaxParameterResolver
{
$parsed = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
if ($parsed === false || $parsed->format('Y-m-d') !== $date) {
throw new InvalidArgumentException('Effective date must use YYYY-MM-DD.');
throw new InvalidArgumentException('Indique uma data de aplicação válida no formato ano-mês-dia.');
}
return $parsed;
+14 -14
View File
@@ -95,16 +95,16 @@ final class SimulationTaxRuleExpression
private function topologicalOrder(array $definition): array
{
if (($definition['language'] ?? null) !== self::LANGUAGE || ! is_array($definition['nodes'] ?? null) || ! is_string($definition['result'] ?? null)) {
throw new InvalidArgumentException('Unsupported expression definition.');
throw new InvalidArgumentException('O formato da expressão não é suportado.');
}
$nodes = $definition['nodes'];
if ($nodes === [] || count($nodes) > 100) {
throw new InvalidArgumentException('Expression must contain between 1 and 100 nodes.');
throw new InvalidArgumentException('A expressão deve conter entre 1 e 100 elementos.');
}
foreach ($nodes as $id => $node) {
if (! is_string($id) || ! preg_match('/^[a-z][a-z0-9_]{0,47}$/', $id) || ! is_array($node) || ! is_string($node['op'] ?? null)) {
throw new InvalidArgumentException('Invalid expression node.');
throw new InvalidArgumentException('Reveja o elemento indicado na expressão.');
}
$op = $node['op'];
$allowedFields = match ($op) {
@@ -112,40 +112,40 @@ final class SimulationTaxRuleExpression
'literal' => ['op', 'value'],
'add', 'subtract', 'multiply', 'divide', 'min', 'max' => ['op', 'args'],
'round' => ['op', 'args', 'scale', 'mode'],
default => throw new InvalidArgumentException('Expression operation is not allowed.'),
default => throw new InvalidArgumentException('Esta operação não é suportada na expressão.'),
};
if (array_diff(array_keys($node), $allowedFields) !== [] || array_diff($allowedFields, array_keys($node)) !== []) {
throw new InvalidArgumentException('Expression node has missing or unsupported fields.');
throw new InvalidArgumentException('Há campos em falta ou não suportados num elemento da expressão.');
}
if (in_array($op, ['input', 'parameter'], true) && (! is_string($node['key']) || ! preg_match('/^[a-z][a-z0-9_.-]{0,79}$/', $node['key']))) {
throw new InvalidArgumentException('Invalid input or parameter key.');
throw new InvalidArgumentException('Reveja o identificador da entrada ou do parâmetro.');
}
if ($op === 'literal') {
$this->number($node['value']);
}
if (isset($node['args'])) {
if (! is_array($node['args'])) {
throw new InvalidArgumentException('Invalid expression arguments.');
throw new InvalidArgumentException('Reveja os argumentos da expressão.');
}
$count = count($node['args']);
$validCount = $op === 'round' ? $count === 1 : ($op === 'subtract' || $op === 'divide' ? $count === 2 : $count >= 2 && $count <= 20);
if (! $validCount || count(array_filter($node['args'], 'is_string')) !== $count) {
throw new InvalidArgumentException('Invalid expression arguments.');
throw new InvalidArgumentException('Reveja os argumentos da expressão.');
}
}
if ($op === 'round' && (! is_int($node['scale']) || $node['scale'] < 0 || $node['scale'] > 12 || ! isset(self::ROUNDING[$node['mode']]))) {
throw new InvalidArgumentException('Invalid explicit rounding configuration.');
throw new InvalidArgumentException('Reveja a configuração de arredondamento.');
}
}
if (! array_key_exists($definition['result'], $nodes)) {
throw new InvalidArgumentException('Expression result references an unknown node.');
throw new InvalidArgumentException('O resultado da expressão refere um elemento inexistente.');
}
$state = [];
$order = [];
$visit = function (string $id) use (&$visit, &$state, &$order, $nodes): void {
if (($state[$id] ?? 0) === 1) {
throw new InvalidArgumentException('Expression dependency graph contains a cycle.');
throw new InvalidArgumentException('A expressão contém uma dependência circular.');
}
if (($state[$id] ?? 0) === 2) {
return;
@@ -153,7 +153,7 @@ final class SimulationTaxRuleExpression
$state[$id] = 1;
foreach ($nodes[$id]['args'] ?? [] as $dependency) {
if (! is_string($dependency) || ! array_key_exists($dependency, $nodes)) {
throw new InvalidArgumentException('Expression references an unknown node.');
throw new InvalidArgumentException('A expressão refere um elemento inexistente.');
}
$visit($dependency);
}
@@ -191,13 +191,13 @@ final class SimulationTaxRuleExpression
if ($op === 'min' || $op === 'max') {
return array_reduce(array_slice($args, 1), static fn (BigRational $carry, BigRational $value): BigRational => ($op === 'min' ? $carry->compareTo($value) <= 0 : $carry->compareTo($value) >= 0) ? $carry : $value, $args[0]);
}
throw new InvalidArgumentException('Expression operation is not allowed.');
throw new InvalidArgumentException('Esta operação não é suportada na expressão.');
}
private function number(mixed $value): BigRational
{
if ((! is_string($value) && ! is_int($value)) || ! preg_match('/^-?\d+(?:\.\d+)?$/', (string) $value)) {
throw new InvalidArgumentException('Numbers must be plain decimal strings or integers.');
throw new InvalidArgumentException('Introduza valores numéricos decimais ou inteiros.');
}
return BigRational::of((string) $value);
+24 -1
View File
@@ -1,6 +1,7 @@
<?php
use App\Http\Middleware\AuditProfileDecision;
use App\Http\Middleware\EnsureHomologationAccess;
use App\Http\Middleware\EnsureSimulationRecoveryCookie;
use App\Http\Middleware\HandleAppearance;
use App\Http\Middleware\HandleInertiaRequests;
@@ -30,7 +31,7 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->encryptCookies(except: ['appearance', 'sidebar_state', EnsureSimulationRecoveryCookie::NAME]);
$middleware->web(append: [
\App\Http\Middleware\EnsureHomologationAccess::class,
EnsureHomologationAccess::class,
PortalProfileSession::class,
ProtectPrivilegedIdentity::class,
AuditProfileDecision::class,
@@ -68,6 +69,28 @@ return Application::configure(basePath: dirname(__DIR__))
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
$exceptions->respond(function (Response $response, Throwable $exception, Request $request): Response {
if (($request->is('api/*') || $request->expectsJson()) && $response->getStatusCode() >= 400) {
$body = json_decode((string) $response->getContent(), true);
$genericMessages = [
'Not Found' => 'O registo não foi encontrado ou não está acessível nesta conta.',
'Not found.' => 'O registo não foi encontrado ou não está acessível nesta conta.',
'Forbidden' => 'Esta conta não tem acesso a esta operação.',
'Forbidden.' => 'Esta conta não tem acesso a esta operação.',
'Unauthenticated.' => 'Entre na sua conta para continuar esta operação.',
'CSRF token mismatch.' => 'A sessão expirou. Entre novamente antes de repetir a operação.',
'Too Many Attempts.' => 'Foram feitas várias tentativas. Aguarde um pouco e tente novamente.',
'Server Error' => 'Não foi possível concluir a operação. Tente novamente.',
];
if (is_array($body) && is_string($body['message'] ?? null) && isset($genericMessages[$body['message']])) {
$body['message'] = $genericMessages[$body['message']];
$response->setContent(json_encode($body, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
} elseif (is_array($body) && is_string($body['message'] ?? null)
&& $response->getStatusCode() === 404 && str_starts_with($body['message'], 'The route ')
&& str_ends_with($body['message'], ' could not be found.')) {
$body['message'] = $genericMessages['Not Found'];
$response->setContent(json_encode($body, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
}
}
if ($request->is('api/*')) {
$response->headers->set(
'X-Correlation-ID',
@@ -0,0 +1,162 @@
{
"base": "a690c665fb4e",
"branch": "codex/simulator-integrated-review",
"scope": "QA only; no main merge; no remote execution; no migration; no real tax activation",
"source_files": [
{
"path": "app/Http/Requests/StoreSimulationFormDraftRequest.php",
"sha256": "b98bb46d7aab33b1e9bbc449b397dceea9aef9a07825a336f0dd5133828776b0"
},
{
"path": "app/Services/SimulationTaxParameterResolver.php",
"sha256": "62edefeb64274b7c0b516b2cebb66a96f94f498412bd0ce2f100d86bfe290e62"
},
{
"path": "app/Services/SimulationTaxRuleExpression.php",
"sha256": "a976c0eb43ebb706275ca17efc9354a8d84f2dbe698375211704b158df02f7e1"
},
{
"path": "bootstrap/app.php",
"sha256": "68e0a909a9b1c4f09cdb857707e93515672cdc7bcc87c131d6124acd8ca4ff7f"
},
{
"path": "docs/investmoz/unification/traceability-and-manual.md",
"sha256": "241bb07e89db3358c77b8984727ed1f196616a1d42560c38685b98ef0ae0c581"
},
{
"path": "docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv",
"sha256": "ab72dfe5dc85569f24e1b4b0767610936c33fbd1300f4e041037eb41cf0695f0"
},
{
"path": "docs/investmoz/v2/auditoria-portal/backlog.md",
"sha256": "1d37e153719b66ed90387107151753db31256d794220eb8570aa0a3bd6f44b66"
},
{
"path": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
"sha256": "b95bda4d791e666f0bc67203931c20d2dcd083fb76ea82c6760ff0f554bba815"
},
{
"path": "resources/js/components/simulation-cashflow-planner.tsx",
"sha256": "b11589c3b06129685b3105aa66992fda30b2ab699c502fdc8e1a4af320a82a03"
},
{
"path": "resources/js/components/simulation-financial-tools.tsx",
"sha256": "1275b72072d3f2b66822dbe6c1f5dd47e1a642d51460254a8871500035fa3b72"
},
{
"path": "resources/js/components/simulation-form.tsx",
"sha256": "4de2bb2fd1ccb9fb44bdec4e59820815f8a5eb0c6c326532c0e10bef087add45"
},
{
"path": "resources/js/lib/financial-calculators.ts",
"sha256": "f551ce4a818484459535d80b968ae77ee17d6baf4ae1e4f018a23e11ccb18fca"
},
{
"path": "resources/js/lib/monthly-cashflow.ts",
"sha256": "9206b7e07d5078f979d16a21c1ed22c03cce83407bf376e146cb9b7ef959762d"
},
{
"path": "resources/js/lib/project-dossier-report.ts",
"sha256": "95d491b481bccd3ac0d2169619528b40b49e4a115a03807305b69c06a95e2461"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256": "dfd2e5e28f2e2f0d1d185f111ecf9e43984c9b5952c1a39a6d483e13ad1a5758"
},
{
"path": "tests/Feature/SimulatorCashflowContinuityTest.php",
"sha256": "11a3423c8ef970b7f5522ae07bf1538055496d88d9559fd7e687024dade8848a"
},
{
"path": "tests/Feature/SimulatorReviewMessagesTest.php",
"sha256": "571d8df887ad78335d93caffc83bc21128cf4cb96ccd0038b14a5e4d30ef9797"
},
{
"path": "tests/Unit/SimulationTaxParameterResolverTest.php",
"sha256": "c0a14ce0f5e92a7a2159266e4fff6bfe54edbbcdfd324174ff7103745cc2fe9f"
},
{
"path": "tests/frontend/simulator-independent-review.test.mjs",
"sha256": "fcfaa6659d889a72eeb8be394e039d7a0b7b458a980e503a86d414a170c1828a"
}
],
"build_manifest_sha256": "2c9826427585cbf41123507611ad1f002090ceb888c2271bdb14005427181416",
"evidence": [
{
"path": "outputs/simulator-continuation-20261020/fiscal-corpus-inventory.json",
"sha256": "e51825792b5007f6bd285d066ffe683c9d241c0965a6fae74a843bbe3d8f4317"
},
{
"path": "outputs/simulator-continuation-20261020/local-rule-visibility.json",
"sha256": "99ef0aab6f30e2491692eeac123d9a9e1a4eeae08ee11bd647e832bf6db571db"
},
{
"path": "outputs/simulator-continuation-20261020/history-preservation-proof.json",
"sha256": "7e591c614f43deefc9e44cc1abe7d8a7decaaa0e8ea17c13e9893024b80bbe98"
},
{
"path": "outputs/simulator-continuation-20261020/interface-comparison.json",
"sha256": "44d54f49973d0fd20b28533a561fab03a32ccb43e0410ba54b0c017a62d96e63"
},
{
"path": "outputs/simulator-continuation-20261020/interface-report.docx",
"sha256": "de0dc3af6936020199dda963bc2aaa8d462fb560e419dcc5a86f369bf18730b7"
},
{
"path": "outputs/simulator-continuation-20261020/interface-financial.csv",
"sha256": "552817d6b9fa7e84f457f490e275f374cdd0254568e8370cfc21716147204150"
},
{
"path": "outputs/simulator-continuation-20261020/financial-screen.jpg",
"sha256": "73b6510861dfc4a29147331ac99f7a7db80987c1db03d1dc34efccfaffaa61e5"
},
{
"path": "outputs/simulator-continuation-20261020/financial-screen.txt",
"sha256": "de37ce59ee87650cdb9bf977e9dddc87ec56ecbb0efb52260c8c69f2898d6e57"
},
{
"path": "outputs/simulator-continuation-20261020/new-session-A.txt",
"sha256": "f1f2455a3d81bc9a140feae71bfd813ad3e3bd51f8167cbb8b7868540fb1b267"
},
{
"path": "outputs/simulator-continuation-20261020/new-session-B.txt",
"sha256": "ccc338bedaa8272db3a49bedb18d2872cb83cdc6b5912da73520132e14018900"
},
{
"path": "outputs/simulator-continuation-20261020/history-visible.txt",
"sha256": "39ac194341c81b1f2e81dc544f879da280ebf8f9e2d0bfad1b2ac95f2f0cc62f"
},
{
"path": "outputs/simulator-continuation-20261020/docx-render/interface-report.pdf",
"sha256": "00ae9586605bd29debffe96388bf874b0391170bbc7d60d6a94f1637975805b4"
}
],
"verified": {
"frontend_tests": 14,
"php_tests": 3,
"php_assertions": 18,
"typescript": "passed",
"build": "passed",
"ui": "Two synthetic dossiers recovered after logout/login; failed-save retry; calculation navigation; results 24 and 12 visible; CSV and DOCX downloaded through application",
"history": "Three original hashes unchanged; other-account case 403 and result policy 404",
"synthetic_parameters": "In-memory resolver only; 7.5 then 9.25; period transition and stale source version; no database rows",
"corpus": "9901 paths enumerated; 32 fiscal filename candidates; three existing repository originals hashed; prior analyses retained",
"credit_recipe": "1000 MZN; nominal annual 12%; 12 months; monthly 1%; half-up cents; no grace or fees; first 88.85; last 88.84; interest 66.19; total 1066.19",
"cashflow_delta": "Versioned editable snapshot in existing economic JSON; two-dossier/new-session API recovery and refusal tested; blank tax not zero; report summary/detail included"
},
"limitations": [
"Current homologation inventory awaits local SSH authentication; remote read script prepared but not executed",
"Native Edge print dialog opened; user PDF save pending; DOCX render not counted as native print",
"New cashflow browser and generated-output visual delta awaits completion of native print",
"Interactive attachment attempt blocked by extension file-URL permission before upload; accepted automated exporter limits retained",
"QA role bypasses RLS; new refusal proof is application policy; previous nonprivileged RLS evidence retained",
"UI DOCX evidence is dossier B without cashflow snapshot and before historical-result selection; its unchanged six-page output is retained",
"Tax DSL/governance/resolver exist but execution engine remains custom arithmetic; connecting legal tax formulas is not silently included"
],
"remaining_hours": [
2,
4
],
"remaining_scope": "Read-only remote collection; native PDF; new cashflow visual continuation; evidence closure; excludes waiting and tax-engine integration",
"secrets_and_private_fixtures": "Not included in source manifest or Git"
}
@@ -1594,3 +1594,32 @@ Provas novas: 34 testes dirigidos de formulário/relatório passaram; TypeScript
Exemplos para revisão por HTTP na aplicação QA local, sem `file:`: `http://127.0.0.1:18570/qa/relatorios/simulador/positivo.pdf`, `negativo.pdf`, `nulo.pdf`, `incompleto.pdf`, e os mesmos nomes com extensão `.docx`. Estes endereços são uma whitelist do router privado de QA, exclusivamente loopback; não são rotas públicas de produção. Ficheiros não sintéticos e configuração privada não são servidos por esta whitelist.
**Estimativa restante: 6–10 h**, já descontado o trabalho acima e as provas aceites; não são as 18–30 h iniciais. Distribuição: continuidade autenticada/histórico e mudança de parâmetros 2–3 h; controlos adicionais delimitados 1–2 h; impressão Edge e comparação ecrã/download 1–2 h; mensagens dinâmicas/falhas de gravação/exportação e fecho 2–3 h. Não inclui cadastrar regras fiscais ausentes nem decidir taxas/incentivos sem fundamento. Falta de dados fiscais não bloqueia os controlos aritméticos disponíveis. A revisão integral continua aberta nos pontos precisos desta tabela.
### Continuação delimitada do simulador — controlo operacional e fontes
Base `a690c665fb4e`, mesma branch QA. Clone exclusivo `simulator_review_delta_20261020_test`, loopback 55441, aplicação `http://127.0.0.1:18576`. A base anterior e as contas reais foram preservadas. Duas contas e dois dossiers SINTÉTICOS QA foram preparados apenas neste clone; sem funções de gestão e sem transporte para o destino. O sinalizador local `INVESTOR_PATH_ENABLED=true` habilitou a capacidade já existente neste clone; não representa configuração aplicada remotamente.
| Item existente | Resultado desta continuação | Saldo |
| --- | --- | --- |
| AUD-SIM-INT01 | Prova anterior de data inválida preservada; código não alterado neste ponto. | Concluído o delta anterior; sem repetição. |
| AUD-SIM-INT02 | Confirmado zero de regras de qualquer estado na base QA original, com visibilidade administrativa sem ocultação por RLS; biblioteca contém 30 documentos. Inventariados 9901 caminhos do acervo antes da selecção: 32 candidatos por nome fiscal e três originais versionados no repositório, estes com hash actual conferido. Existem análises de textos e correspondências preparadas em `primary-text-review-2025-2026.md`, `official-source-register.md`, `fiscal-source-references-2026-10-06.csv` e comando de arquivo existente. Não se identificou um conjunto serializado de parâmetros fiscais reais nos seeders/configuração pesquisados. | Não confundir ausência do cadastro executável QA com ausência de fontes/trabalho documental. Leitura actual da homologação aguarda autenticação SSH local; não se afirmou que o destino também está vazio. Os binários do acervo montado não foram todos relidos: enumeração de caminhos não comprova conteúdo/versão. |
| AUD-SIM-INT03 | A e B recuperaram títulos próprios; B conservou a edição após erro/repetição. Logout e novo login normais, depois recuperação de A e B, conferidos no navegador. Recusa de outra conta: 403 nos dois dossiers e 404 no resultado, pelo serviço/política utilizados pelo controlador. | Concluída a conferência delimitada. O papel PostgreSQL deste clone é superutilizador: esta prova de recusa é da aplicação, não nova certificação de RLS. Provas anteriores de RLS não privilegiado mantidas. |
| AUD-SIM-INT04 | Corrigida a abertura de histórico que carregava mas não mostrava o resultado no percurso de pequeno negócio. Resultado seleccionado agora visível e explicitamente separado das entradas do dossier. Navegador abriu 24 e 12; três snapshots mantiveram hashes/versões originais após a utilização. | Versões sintéticas exclusivamente em memória do resolvedor: 7,5% até Junho e 9,25% a partir de Julho, fonte e código QA que não representam legislação. Versão histórica conservada; transição dentro do período fica indeterminada; versão da fonte incompatível deixa de resolver. Nada foi registado/publicado na base ou no destino. É prova do resolvedor, não de execução fiscal integrada. |
| AUD-SIM-INT05 | Livro independente de três meses: resultado antes de impostos 225 MZN, fundo de maneio 130, usos 730, caixa inicial após usos 470 e caixa final 638. Distintos reconhecimento/pagamento de juros, capital, compras, despesas e cobranças. Tesouraria com mês de imposto desconhecido mantém total indeterminado; caixa conhecida 210, sem somar crédito/pedido/reconhecimento de IVA como dinheiro. | Controlos passaram. Pressupostos completos no teste `simulator-independent-review.test.mjs`; não são previsões ou taxas legais. |
| AUD-SIM-INT06 | CSV e Word efectivamente descarregados pelos botões normais do dossier B, não por gerador autónomo. Prestação 88,85, juros 66,19 e saldo 100 coincidem com o ecrã. DOCX usa vírgula decimal; CSV/ecrã usam ponto. Word de seis páginas integralmente conferido após renderização, com pressupostos e campos desconhecidos explícitos. | Impressão nativa Edge aberta, aguardando gravação de `SINTETICO-QA-Edge-continuacao.pdf` pelo utilizador; o diálogo não é acessível à automação. Não contar o PDF renderizado do DOCX como essa prova. |
| AUD-SIM-INT07 | Corrigido Abrir cálculo: pequeno negócio abre a etapa financeira existente, em vez de procurar uma etapa fiscal ausente. Falha única 503 de gravação preservou edição; botão Guardar agora repete efectivamente a escrita, com fila serializada e rechecagem da impressão digital para evitar gravação redundante. Recuperação posterior confirmou a edição. | Exportação normal Word/CSV comprovada. Provas automáticas aceites de limites de anexos preservadas e regressão do exportador passou. A tentativa interactiva com anexo sintético de 16 MiB foi impedida pela permissão de URLs de ficheiro da extensão, antes de o ficheiro entrar na aplicação; não foi registada como falha do portal nem como prova de recusa pela interface. |
| AUD-SIM-INT08 | Mensagem dinâmica Not found observada foi tratada no respondedor comum: mensagens genéricas de ausência/acesso/sessão/tentativas/servidor em português, preservando HTTP e correlação. Traduzidas as mensagens de validação de parâmetros/expressões que o controlador de governação apresenta nos campos, sem alterar DSL, códigos ou limites. | Conferência do 404 por pedido real ao kernel passou; mensagens das validações revistas no código/controlador. Inventário estático anterior e traduções intencionalmente seleccionáveis mantidos. Não se certificou cada conteúdo dinâmico do catálogo nem uma sessão administrativa de governação. |
**Pressupostos do crédito de controlo:** capital 1000 MZN; prazo 12 meses; periodicidade mensal (12 prestações); taxa nominal anual hipotética 12%, dividida por 12, resultando em 1% por mês; sem carência, comissão inicial ou periódica. Montantes com duas casas decimais, juros/prestação com metade para cima; saldo residual ajustado na última prestação. Primeira prestação 88,85; última 88,84; juros totais 66,19; total pago 1066,19 MZN. O CSV contém o cronograma completo; o Word apresenta prestação inicial, juros e receita de cálculo. O controlo anterior de 100 por mês usa capital 1200 MZN, 12 meses, periodicidade mensal, taxa nominal anual zero, sem carência/comissões, mesmo arredondamento, total 1200. Estes pressupostos foram acrescentados às saídas e à prova; nenhum produto de crédito foi confirmado.
**Reconciliação fiscal:** os três originais já versionados (BR 248/2025, republicação Lei 22/2022 e BR 89/2017), as análises e relações documentais são trabalho reutilizável. Não pedir novamente documentos já disponíveis nem reabrir a credibilidade das fontes oficiais aceites pela Tempo em 08/10. As referências antigas a reautenticação genérica ficam ultrapassadas por essa decisão; continuam a ser tarefas distintas identificar artigo, âmbito, período e parâmetro. A biblioteca documental não é a tabela de parâmetros. O repositório contém governação, DSL e resolvedor temporal; `RunSimulation` executa a aritmética personalizada e não consome essas fórmulas fiscais. Mesmo encontrar parâmetros no destino não comprovaria cálculo fiscal integrado. Essa ligação funcional não foi implementada silenciosamente neste recorte.
Artefactos em `outputs/simulator-continuation-20261020/`: `interface-comparison.json`, `interface-financial.csv`, `interface-report.docx`, seis páginas em `docx-render/`, provas de A/B e nova sessão, `history-preservation-proof.json`, `fiscal-corpus-inventory.json` e `local-rule-visibility.json`. Credenciais, dump e fixtures privadas ficam excluídos do Git. O script `read-fiscal-remote.py` preparado só faz leitura administrativa e `BEGIN READ ONLY`/SELECT/ROLLBACK; ainda não foi executado por faltar autenticar a ligação. Nenhum teste ou dado sintético foi enviado à homologação.
**Esforço restante do recorte: 1–3 h após os acessos/ficheiro necessários**, para recolha remota, conferência do PDF nativo e fecho das limitações de interface. O tempo de espera do utilizador não é esforço de programação. Não são as 6–10 h anteriores nem as 18–30 h iniciais. Não inclui implementar o motor fiscal integrado ou activar parâmetros reais. As pendências de acesso e impressão não bloqueiam a revisão dos resultados QA já utilizáveis.
**Achado adicional de continuidade, identificado antes da correcção:** o componente de tesouraria mantinha os seus pressupostos apenas no estado React; não recebia nem devolvia snapshot ao rascunho. Foi acrescentado `cashflow_snapshot` versionado ao JSON económico existente e ao validador do pedido, sem migration. O componente recupera os campos e o modo de imposto; guarda apenas após edição e não converte vazios em zero. Snapshots desconhecidos/inválidos não geram resultados alternativos e mostram aviso. O relatório consolidado e o CSV do dossier passam a incluir pressupostos, valores conhecidos/indeterminados e meses; o resumo destaca a tesouraria quando existir snapshot. Dossiers antigos sem esse campo mantêm a apresentação anterior — as seis páginas do DOCX de B já conferidas não tinham esse campo e não foram reemitidas desnecessariamente.
Provas adicionais: teste HTTP com dois dossiers, logout lógico/nova sessão, recuperação exacta e recusa por outra conta passou (8 asserções), em transacção revertida no clone. Quatro controlos frontend dirigidos passaram, incluindo recuperação de mês vazio, valor inválido para correcção, relatório sem zero inventado e pressupostos completos do crédito. As 10 verificações do exportador afectado passaram; teste temporal sintético passou (7 asserções); erro HTTP em português passou (3). TypeScript/build e sintaxe PHP passaram. A conferência visual do novo componente de tesouraria com sessão real QA permanece por executar depois de fechar a impressão do Edge; não foi substituída por uma prova unitária.
**Saldo revisto após esse achado: 2–4 h de verificação delimitada**, já descontado o desenvolvimento concluído: leitura remota 0,5–1 h; PDF nativo 0,5–1 h; continuidade visual/saída da tesouraria recém-corrigida 0,5–1 h; consolidação das provas e limitações 0,5–1 h. Exclui espera por autenticação/ficheiro e a integração funcional futura das regras fiscais. Esta estimativa substitui 1–3 h acima por uma divergência concreta; não retoma as 6–10 h nem as 18–30 h originais.
@@ -7382,11 +7382,11 @@ P1,AUD-SEARCH-T09,QA — revisão Tempo pendente,Gestão de procura/capacidade p
P1,AUD-SEARCH-T10,QA — revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,"Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
P1,AUD-SEARCH-T11,QA — revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,"Situação jurídica, consulta e cálculo continuam independentes.",,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
P1,AUD-GUIDE-CARD-20261020,QA review pending,New delta; not approved for main,Common educational card and existing source/context adapter,,Desktop/mobile/keyboard; public-state and source checks,,,,,,educational presentation,Tempo 20/10/2026; same manual,,,Tempo visual review; library integration verified on existing QA; previous dossier proof retained,,Integration approval for new delta,,,,1 test / 8 assertions; TypeScript/build; real public-source visual preview,,,,No import; no remote changes; no claim of new dossier proof,
P1,AUD-SIM-INT01,QA verified: inline date error and no substitute period,Invalid one-off fiscal date can throw instead of returning validation state,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,QA verified: inline date error and no substitute period,,,,No claim of full review completion,
P1,AUD-SIM-INT02,QA inventory complete: no fiscal rules; custom arithmetic only; unavailable parameters remain indeterminate,Review actual fiscal parameters sources periods and eligibility,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,QA inventory complete: no fiscal rules; custom arithmetic only; unavailable parameters remain indeterminate,,,,No claim of full review completion,
P1,AUD-SIM-INT03,Scope remount fixed; reload recovery verified; authenticated two-dossier and new-session frontend check pending,Review isolation recovery across projects and sessions,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Scope remount fixed; reload recovery verified; authenticated two-dossier and new-session frontend check pending,,,,No claim of full review completion,
P1,AUD-SIM-INT04,Stale warning verified after input edit; historical backend preserved; authenticated history and parameter-version delta pending,Review stale and historical results and correct dossier,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Stale warning verified after input edit; historical backend preserved; authenticated history and parameter-version delta pending,,,,No claim of full review completion,
P1,AUD-SIM-INT05,Independent cash rounding break-even and loan controls passed; remaining cash-flow and business-plan controls delimited,Independent financial controls unknown zero units and currencies,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Independent cash rounding break-even and loan controls passed; remaining cash-flow and business-plan controls delimited,,,,No claim of full review completion,
P1,AUD-SIM-INT06,Four real-exporter synthetic DOCX reports checked at 6/6/6/5 pages; native Edge print and UI comparison pending,Generated reports screen download and pagination,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Four real-exporter synthetic DOCX reports checked at 6/6/6/5 pages; native Edge print and UI comparison pending,,,,No claim of full review completion,
P1,AUD-SIM-INT07,Custom arithmetic context requirement removed; fiscal error/retry verified; save/export fault checks pending,Explicit validate calculate save export errors and retry,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Custom arithmetic context requirement removed; fiscal error/retry verified; save/export fault checks pending,,,,No claim of full review completion,
P1,AUD-SIM-INT08,4519 TSX literal inventory and auth/settings/common translations; dynamic backend/catalogue inventory remains open,Portuguese across portal and simulator outputs,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,4519 TSX literal inventory and auth/settings/common translations; dynamic backend/catalogue inventory remains open,,,,No claim of full review completion,
P1,AUD-SIM-INT01,Previous inline-date proof retained; unchanged delta not repeated,Invalid one-off fiscal date can throw instead of returning validation state,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Previous inline-date proof retained; unchanged delta not repeated,,,,No claim of full review completion,
P1,AUD-SIM-INT02,QA zero rules verified with full visibility; prepared archive and analyses reused; current homologation read awaits SSH authentication,Review actual fiscal parameters sources periods and eligibility,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,QA zero rules verified with full visibility; prepared archive and analyses reused; current homologation read awaits SSH authentication,,,,No claim of full review completion,
P1,AUD-SIM-INT03,Two dossiers and new login recovered in browser; cashflow persistence corrected and HTTP-tested; new visual cashflow delta pending,Review isolation recovery across projects and sessions,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Two dossiers and new login recovered in browser; cashflow persistence corrected and HTTP-tested; new visual cashflow delta pending,,,,No claim of full review completion,
P1,AUD-SIM-INT04,Historical results visible after fix; 12 and 24 recovered; hashes unchanged; synthetic temporal resolver versions verified in memory only,Review stale and historical results and correct dossier,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Historical results visible after fix; 12 and 24 recovered; hashes unchanged; synthetic temporal resolver versions verified in memory only,,,,No claim of full review completion,
P1,AUD-SIM-INT05,Independent business-plan and cashflow ledgers passed; complete credit recipe included in screen and exports,Independent financial controls unknown zero units and currencies,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Independent business-plan and cashflow ledgers passed; complete credit recipe included in screen and exports,,,,No claim of full review completion,
P1,AUD-SIM-INT06,Normal UI CSV and DOCX match shared values; all six DOCX pages checked; native Edge PDF awaits user save; new cashflow visual output pending,Generated reports screen download and pagination,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Normal UI CSV and DOCX match shared values; all six DOCX pages checked; native Edge PDF awaits user save; new cashflow visual output pending,,,,No claim of full review completion,
P1,AUD-SIM-INT07,Calculation navigation and failed-save retry fixed and browser-verified; normal downloads verified; interactive attachment blocked by extension permission,Explicit validate calculate save export errors and retry,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Calculation navigation and failed-save retry fixed and browser-verified; normal downloads verified; interactive attachment blocked by extension permission,,,,No claim of full review completion,
P1,AUD-SIM-INT08,Generic HTTP and fiscal-expression validation messages translated; status/correlation verified; selectable translations and source titles preserved,Portuguese across portal and simulator outputs,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Generic HTTP and fiscal-expression validation messages translated; status/correlation verified; selectable translations and source titles preserved,,,,No claim of full review completion,
Can't render this file because it is too large.
@@ -3004,3 +3004,5 @@ AUD-GUIDE-CARD-20261020: componente comum com imagem autorizada, título, resumo
AUD-SIM-INT01 through AUD-SIM-INT08 are recorded in the same matrix and manual. Initial diagnosis and targeted date/Portuguese corrections only; full review remains open. Initial effort 18-30 h, subject to discounting accepted evidence and completed work. No main integration, real-data modification or tax parameter activation.
Actualização QA de 20/10/2026: os mesmos oito itens foram reconciliados no manual e nas duas tabelas existentes. Data com mensagem junto ao campo; inventário dos parâmetros efectivos (zero regras fiscais QA); controlos matemáticos independentes; aviso de recálculo; quatro relatórios reais do exportador com 6/6/6/5 páginas; consulta fiscal com erro/nova tentativa e traduções comuns verificados. O formulário personalizado já não exige contexto fiscal que não envia ao cálculo. Restam continuidade autenticada entre dois dossiers/nova sessão e histórico, mudança de parâmetros, controlos delimitados de cash-flow/plano, impressão nativa Edge/comparação pela interface e mensagens dinâmicas/falhas dirigidas de gravação/exportação. Estimativa restante **6–10 h**, distinta das 18–30 h iniciais. Sem main, servidor remoto, importação ou activação fiscal. Exemplos PDF são renderizações do DOCX; não certificam impressão do Edge. Ver o saldo detalhado AUD-SIM-INT01–08 no mesmo manual.
Continuação delimitada: dois dossiers e nova entrada conferidos no navegador; histórico agora visível no percurso de pequeno negócio, hashes preservados; Abrir cálculo encaminha para a etapa financeira; repetição de gravação após 503 corrigida e conferida; controlos independentes de tesouraria/plano passaram. CSV e Word descarregados pela interface coincidem nos valores comuns e apresentam a receita completa do crédito. Seis páginas do DOCX conferidas; PDF nativo ainda aguarda gravação pelo utilizador. QA tem zero regras com visibilidade completa, mas existem três originais fiscais versionados e análises/referências reutilizáveis; não se concluiu ausência na homologação sem autenticação SSH. Versões sintéticas testadas apenas em memória, sem cadastro/activação. Encontrada e corrigida perda dos pressupostos da tesouraria: snapshot no JSON existente, recuperação por dossier e relatório, sem migration; testes HTTP/frontend passaram, falta conferência visual deste novo delta. Saldo **2–4 h** após acessos/ficheiro, detalhado no mesmo manual; integração do motor fiscal permanece separada e não foi implementada. Sem main ou intervenção remota.
@@ -10651,11 +10651,11 @@ AUD-SEARCH-T09,Consistência transversal da pesquisa e navegação — Educaçã
AUD-SEARCH-T10,Consistência transversal da pesquisa e navegação — Biblioteca,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/documentos,"portal/services; portal/document — Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,QA — delta delimitado; revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Dois resumos de contagem, filtros activos sem remoção individual, controlos sem opções e rodapé local repetido.",Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,portal/services; portal/document; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
AUD-SEARCH-T11,Consistência transversal da pesquisa e navegação — Legislação,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/legislacao,portal/services; portal/document — Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,QA — delta delimitado; revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Mesmo padrão da biblioteca; não deve expor um universo concorrente.,"Situação jurídica, consulta e cálculo continuam independentes.",portal/services; portal/document; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,"Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Situação jurídica, consulta e cálculo continuam independentes.",,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
AUD-GUIDE-CARD-20261020,"Related educational cards: image, source summary, clear actions",Tempo 20/10/2026; independent delta after 0e7d51f18fb4,educational presentation,/sectores/{slug}; /explorar/{group}; articles; /oportunidades and detail,EducationalArticleCard; EducationalGuides; LegacyContent; SectorInvestment,QA only,1 test / 8 assertions; PHP syntax; TypeScript/build; desktop2048/mobile390; keyboard,QA review pending,Normal QA sector and article verified against existing library resource 10 and local image 25; previous dossier proof retained,,Source-derived summary; public images; conditional opportunity action; context preserved,P1,,,,,,,,,,,,,,,,,,presentation,,,,20/10/2026,,,,,,,,,,,,content; navigation; visual presentation,,Ready for QA review
AUD-SIM-INT01,Invalid one-off fiscal date can throw instead of returning validation state,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Fixed in QA; invalid-date checks; 38 directed frontend tests passed,QA verified: inline date error and no substitute period,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT02,Review actual fiscal parameters sources periods and eligibility,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Verification pending; no parameter activation,QA inventory complete: no fiscal rules; custom arithmetic only; unavailable parameters remain indeterminate,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT03,Review isolation recovery across projects and sessions,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing controls and accepted proofs to reconcile; no leak established,Scope remount fixed; reload recovery verified; authenticated two-dossier and new-session frontend check pending,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT04,Review stale and historical results and correct dossier,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing versions/fingerprints; targeted verification pending,Stale warning verified after input edit; historical backend preserved; authenticated history and parameter-version delta pending,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT05,Independent financial controls unknown zero units and currencies,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Reconciliation pending; no invented tax assumptions,Independent cash rounding break-even and loan controls passed; remaining cash-flow and business-plan controls delimited,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT06,Generated reports screen download and pagination,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing exporters; examples for this review still pending,Four real-exporter synthetic DOCX reports checked at 6/6/6/5 pages; native Edge print and UI comparison pending,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT07,Explicit validate calculate save export errors and retry,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing idempotency; consumer-level verification pending,Custom arithmetic context requirement removed; fiscal error/retry verified; save/export fault checks pending,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT08,Portuguese across portal and simulator outputs,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,"Partial QA fix: rate labels, password confirmation/security and spinner; wider inventory pending",4519 TSX literal inventory and auth/settings/common translations; dynamic backend/catalogue inventory remains open,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT01,Invalid one-off fiscal date can throw instead of returning validation state,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Fixed in QA; invalid-date checks; 38 directed frontend tests passed,Previous inline-date proof retained; unchanged delta not repeated,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT02,Review actual fiscal parameters sources periods and eligibility,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Verification pending; no parameter activation,QA zero rules verified with full visibility; prepared archive and analyses reused; current homologation read awaits SSH authentication,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT03,Review isolation recovery across projects and sessions,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing controls and accepted proofs to reconcile; no leak established,Two dossiers and new login recovered in browser; cashflow persistence corrected and HTTP-tested; new visual cashflow delta pending,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT04,Review stale and historical results and correct dossier,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing versions/fingerprints; targeted verification pending,Historical results visible after fix; 12 and 24 recovered; hashes unchanged; synthetic temporal resolver versions verified in memory only,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT05,Independent financial controls unknown zero units and currencies,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Reconciliation pending; no invented tax assumptions,Independent business-plan and cashflow ledgers passed; complete credit recipe included in screen and exports,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT06,Generated reports screen download and pagination,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing exporters; examples for this review still pending,Normal UI CSV and DOCX match shared values; all six DOCX pages checked; native Edge PDF awaits user save; new cashflow visual output pending,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT07,Explicit validate calculate save export errors and retry,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing idempotency; consumer-level verification pending,Calculation navigation and failed-save retry fixed and browser-verified; normal downloads verified; interactive attachment blocked by extension permission,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT08,Portuguese across portal and simulator outputs,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,"Partial QA fix: rate labels, password confirmation/security and spinner; wider inventory pending",Generic HTTP and fiscal-expression validation messages translated; status/correlation verified; selectable translations and source titles preserved,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
Can't render this file because it is too large.
@@ -1,7 +1,11 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import {
exportMonthlyCashflowCsv,
projectMonthlyCashflow,
defaultCashflowDraftInputs,
restoreCashflowDraftSnapshot,
serializeCashflowDraftSnapshot,
type CashflowDraftSnapshot,
} from '@/lib/monthly-cashflow';
import {
isSimulationCurrency,
@@ -11,28 +15,38 @@ import {
export function SimulationCashflowPlanner({
currency,
onCurrencyChange,
snapshot,
onSnapshotChange,
}: {
currency: string;
onCurrencyChange: (currency: string) => void;
snapshot?: string;
onSnapshotChange?: (snapshot: string) => void;
}) {
const [taxMode, setTaxMode] = useState<'fixed' | 'schedule'>('fixed');
const [inputs, setInputs] = useState({
openingCash: '0',
monthlyRevenue: '200000',
monthlyOperatingCosts: '150000',
monthlyDebtService: '0',
monthlyTaxAssessed: '',
monthlyTaxSchedule: Array.from({ length: 12 }, () => ''),
vatCreditEstimated: '0',
vatRefundRequested: '0',
vatRefundRecognized: '0',
vatRefundExpectedReceipt: '0',
vatRefundExpectedMonth: '',
});
const restored = restoreCashflowDraftSnapshot(snapshot);
const [taxMode, setTaxMode] = useState<'fixed' | 'schedule'>(
restored?.taxMode ?? 'fixed',
);
const [inputs, setInputs] = useState(
restored?.inputs ?? defaultCashflowDraftInputs(),
);
useEffect(() => {
const saved = restoreCashflowDraftSnapshot(snapshot);
setTaxMode(saved?.taxMode ?? 'fixed');
setInputs(saved?.inputs ?? defaultCashflowDraftInputs());
}, [snapshot]);
function update(
next: typeof inputs,
mode: CashflowDraftSnapshot['taxMode'] = taxMode,
) {
setInputs(next);
setTaxMode(mode);
onSnapshotChange?.(serializeCashflowDraftSnapshot(mode, next));
}
const set = (
key: Exclude<keyof typeof inputs, 'monthlyTaxSchedule'>,
value: string,
) => setInputs((current) => ({ ...current, [key]: value }));
) => update({ ...inputs, [key]: value });
const cashflowInput = {
...inputs,
...(taxMode === 'fixed'
@@ -45,6 +59,10 @@ export function SimulationCashflowPlanner({
let projection;
let error = '';
try {
if (snapshot && !restored)
throw new Error(
'Não foi possível recuperar os pressupostos guardados de tesouraria. Reveja os campos antes de voltar a guardar; nenhum resultado foi calculado com dados alternativos.',
);
projection = projectMonthlyCashflow({ ...cashflowInput, currency });
} catch (cause) {
error =
@@ -97,9 +115,11 @@ export function SimulationCashflowPlanner({
role="note"
className="rounded bg-amber-50 p-3 text-sm text-amber-950"
>
Os valores preenchidos ao abrir são exemplos demonstrativos.
Confirme ou altere os pressupostos antes de interpretar os
resultados ou exportar o CSV.
{restored
? 'Pressupostos recuperados do rascunho deste investimento; reveja-os antes de interpretar os resultados ou exportar.'
: snapshot
? 'O rascunho guardado não pôde ser interpretado. Os campos mostram exemplos, que não substituem os dados guardados até os editar.'
: 'Os valores preenchidos ao abrir são exemplos demonstrativos. Confirme ou altere os pressupostos antes de interpretar os resultados ou exportar o CSV.'}
</p>
{projection?.status === 'incomplete' && (
<p
@@ -121,7 +141,7 @@ export function SimulationCashflowPlanner({
type="radio"
name="cashflow-tax-mode"
checked={taxMode === 'fixed'}
onChange={() => setTaxMode('fixed')}
onChange={() => update(inputs, 'fixed')}
/>
Valor mensal fixo
</label>
@@ -130,7 +150,7 @@ export function SimulationCashflowPlanner({
type="radio"
name="cashflow-tax-mode"
checked={taxMode === 'schedule'}
onChange={() => setTaxMode('schedule')}
onChange={() => update(inputs, 'schedule')}
/>
Calendário variável por mês
</label>
@@ -162,16 +182,16 @@ export function SimulationCashflowPlanner({
step="0.01"
value={value}
onChange={(event) =>
setInputs((current) => ({
...current,
update({
...inputs,
monthlyTaxSchedule:
current.monthlyTaxSchedule.map(
inputs.monthlyTaxSchedule.map(
(amount, month) =>
month === index
? event.target.value
: amount,
),
}))
})
}
className="mt-1 w-full rounded border p-2"
/>
@@ -352,6 +352,12 @@ export function SimulationFinancialTools({
/>
{schedule && isSimulationCurrency(currency) && (
<>
<p className="text-sm">
Montantes a duas casas decimais; juros e
prestação arredondados à unidade monetária
mínima, metade para cima. A última prestação
ajusta o saldo residual.
</p>
<p aria-live="polite">
Taxa por período: {schedule.periodicRatePercent}
%; desembolso líquido: {
+50 -27
View File
@@ -834,8 +834,10 @@ export function SimulationForm() {
}, [openedTool, step]);
function openDraftTool(tool: 'calculation' | 'fiscal') {
setToolError('');
const index = journeySections.findIndex(
(section) => section.id === 'tax',
const index = journeySections.findIndex((section) =>
tool === 'calculation'
? ['financial', 'tax'].includes(section.id)
: section.id === 'tax',
);
if (index < 0) {
setToolError(
@@ -927,6 +929,7 @@ export function SimulationForm() {
const [businessLinkError, setBusinessLinkError] = useState('');
const [businessLinkBusy, setBusinessLinkBusy] = useState(false);
const [continueLaterSaved, setContinueLaterSaved] = useState(false);
const [saveAttempt, setSaveAttempt] = useState(0);
const calculationInFlightRef = useRef(false);
const draftIdRef = useRef<string | null>(null);
const draftEtagRef = useRef<string | null>(null);
@@ -1322,6 +1325,7 @@ export function SimulationForm() {
setSaveStatus('A guardar o rascunho no servidor…');
saveQueueRef.current = saveQueueRef.current
.then(async () => {
if (fingerprint === lastSavedFingerprintRef.current) return;
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
@@ -1476,6 +1480,7 @@ export function SimulationForm() {
draftCreateKey,
draftIdKey,
draftKey,
saveAttempt,
]);
async function returnToInvestment() {
@@ -1700,6 +1705,7 @@ export function SimulationForm() {
async function saveAndContinueLater() {
if (!draft.purpose) return;
setContinueLaterSaved(false);
setSaveAttempt((attempt) => attempt + 1);
await new Promise((resolve) => window.setTimeout(resolve, 550));
await saveQueueRef.current;
const target = canSavePersonalData ? draft : getAutosaveSnapshot(draft);
@@ -3449,6 +3455,10 @@ export function SimulationForm() {
(draft.purpose === 'simulate_only' &&
current.id === 'tax')) && (
<SimulationCashflowPlanner
snapshot={String(draft.cashflow_snapshot ?? '')}
onSnapshotChange={(value) =>
change('cashflow_snapshot', value)
}
currency={String(draft.currency ?? '')}
onCurrencyChange={(value) => change('currency', value)}
/>
@@ -3692,36 +3702,49 @@ export function SimulationForm() {
documentos nesta simulação exploratória.
</p>
)}
{current.id === 'tax' && (
{(current.id === 'tax' || result) && (
<div className="space-y-3 border-t pt-4">
<h4 className="font-semibold">
Cenário personalizado de planeamento
{current.id === 'tax'
? 'Cenário personalizado de planeamento'
: 'Resultado histórico seleccionado'}
</h4>
{draft.fiscal_scenario_mode === 'custom_planning' && (
<p
role="note"
className="rounded bg-slate-50 p-3 text-sm"
>
Os valores abaixo são pressupostos que
introduziu para planeamento. O resultado é uma
operação percentual genérica, não uma taxa
legal, imposto devido nem confirmação de
elegibilidade.
{current.id !== 'tax' && result && (
<p>
Este resultado conserva os pressupostos e a
versão da execução seleccionada. Não substitui
as entradas deste dossier.
</p>
)}
{draft.fiscal_scenario_mode === 'custom_planning' && (
<button
type="button"
disabled={isCalculating}
aria-busy={isCalculating}
className="rounded-lg bg-emerald-800 px-4 py-3 font-semibold text-white focus-visible:outline-2 disabled:cursor-wait disabled:opacity-60"
onClick={() => void runSimulation()}
>
{isCalculating
? 'A calcular…'
: 'Validar e calcular no servidor'}
</button>
)}
{current.id === 'tax' &&
draft.fiscal_scenario_mode ===
'custom_planning' && (
<p
role="note"
className="rounded bg-slate-50 p-3 text-sm"
>
Os valores abaixo são pressupostos que
introduziu para planeamento. O resultado é
uma operação percentual genérica, não uma
taxa legal, imposto devido nem confirmação
de elegibilidade.
</p>
)}
{current.id === 'tax' &&
draft.fiscal_scenario_mode ===
'custom_planning' && (
<button
type="button"
disabled={isCalculating}
aria-busy={isCalculating}
className="rounded-lg bg-emerald-800 px-4 py-3 font-semibold text-white focus-visible:outline-2 disabled:cursor-wait disabled:opacity-60"
onClick={() => void runSimulation()}
>
{isCalculating
? 'A calcular…'
: 'Validar e calcular no servidor'}
</button>
)}
{serverError && (
<p role="alert" className="text-red-800">
{serverError}
@@ -474,6 +474,10 @@ export function exportFinancialToolsCsv(
],
['Prazo (meses)', input.loan.termMonths],
['Periodicidade (meses)', input.loan.periodMonths],
[
'Arredondamento',
'Montantes a duas casas decimais; juros e prestação arredondados à unidade monetária mínima, metade para cima; última prestação ajusta o saldo residual.',
],
['Carência (períodos)', input.loan.gracePeriods ?? 0],
['Comissão inicial', input.loan.upfrontFee ?? '0'],
['Comissão periódica', input.loan.recurringFee ?? '0'],
+68
View File
@@ -1,5 +1,73 @@
import { csvCell } from './csv.js';
export function defaultCashflowDraftInputs() {
return {
openingCash: '0',
monthlyRevenue: '200000',
monthlyOperatingCosts: '150000',
monthlyDebtService: '0',
monthlyTaxAssessed: '',
monthlyTaxSchedule: Array.from({ length: 12 }, () => ''),
vatCreditEstimated: '0',
vatRefundRequested: '0',
vatRefundRecognized: '0',
vatRefundExpectedReceipt: '0',
vatRefundExpectedMonth: '',
};
}
export type CashflowDraftSnapshot = {
version: 1;
taxMode: 'fixed' | 'schedule';
inputs: ReturnType<typeof defaultCashflowDraftInputs>;
};
/** Preserve editable strings, including unanswered and invalid amounts, without inventing zero. */
export function restoreCashflowDraftSnapshot(
snapshot: string | undefined,
): CashflowDraftSnapshot | null {
if (!snapshot || snapshot.length > 20000) return null;
try {
const parsed = JSON.parse(snapshot);
if (
!parsed ||
parsed.version !== 1 ||
!['fixed', 'schedule'].includes(parsed.taxMode) ||
!parsed.inputs ||
typeof parsed.inputs !== 'object' ||
Array.isArray(parsed.inputs)
)
return null;
const keys = Object.keys(defaultCashflowDraftInputs());
if (Object.keys(parsed.inputs).some((key) => !keys.includes(key)))
return null;
for (const key of keys) {
const value = parsed.inputs[key];
if (key === 'monthlyTaxSchedule') {
if (
!Array.isArray(value) ||
value.length !== 12 ||
value.some(
(item) => typeof item !== 'string' || item.length > 160,
)
)
return null;
} else if (typeof value !== 'string' || value.length > 160)
return null;
}
return { version: 1, taxMode: parsed.taxMode, inputs: parsed.inputs };
} catch {
return null;
}
}
export function serializeCashflowDraftSnapshot(
taxMode: CashflowDraftSnapshot['taxMode'],
inputs: CashflowDraftSnapshot['inputs'],
): string {
return JSON.stringify({ version: 1, taxMode, inputs });
}
export type MonthlyCashflowInput = {
openingCash: string;
monthlyRevenue: string;
+6 -1
View File
@@ -171,6 +171,7 @@ const rowSection: Record<string, DossierReportSectionId> = {
'Empréstimo educativo': 'finance',
'Ponto de equilíbrio': 'finance',
Orçamento: 'finance',
Tesouraria: 'finance',
Equipa: 'team',
'Necessidade de negócio': 'resources',
Financiamento: 'resources',
@@ -246,6 +247,8 @@ function makeSections(
'Saldo operacional estimado',
'Diferença por financiar',
'Prestação do primeiro período',
'Saldo final total',
'Caixa conhecida antes de imposto em falta',
]);
const budgetPeriod = rows.find(
(row) => row[1] === 'Periodicidade do orçamento',
@@ -256,7 +259,9 @@ function makeSections(
field:
field === 'Saldo operacional estimado' && budgetPeriod
? `${field} — ${budgetPeriod}`
: field,
: field === 'Saldo final total'
? 'Saldo final de tesouraria — 12 meses'
: field,
value: absent(
formatDossierReportValue(
field,
+68 -2
View File
@@ -1,5 +1,9 @@
import { displayValue } from './display-value.ts';
import { csvCellWithDelimiter } from './csv.js';
import {
restoreCashflowDraftSnapshot,
projectMonthlyCashflow,
} from './monthly-cashflow.ts';
import {
calculateFinancialTools,
restoreFinancialToolsSnapshot,
@@ -330,7 +334,7 @@ export function buildProjectDossierRows(
[
'Empréstimo educativo',
'Taxa anual pressuposta',
`${finance.loan.rate}% (${finance.loan.rateType})`,
`${finance.loan.rate}% (${finance.loan.rateType === 'nominal' ? 'nominal anual' : 'efectiva anual'})`,
'Declarada pelo utilizador; não é taxa de produto',
],
[
@@ -342,7 +346,7 @@ export function buildProjectDossierRows(
[
'Empréstimo educativo',
'Método e pressupostos substituídos',
`Saldo decrescente; capital ${finance.loan.principal}; taxa ${finance.loan.rate}% ${finance.loan.rateType}; ${finance.loan.months} meses; período ${finance.loan.periodMonths} meses; carência ${finance.loan.gracePeriods}; comissão inicial ${finance.loan.upfrontFee}; comissão periódica ${finance.loan.recurringFee}`,
`Saldo decrescente; capital ${finance.loan.principal} ${currency}; taxa ${finance.loan.rate}% ${finance.loan.rateType === 'nominal' ? 'nominal anual (dividida pelos períodos do ano)' : 'efectiva anual (convertida para o período)'}; prazo ${finance.loan.months} meses; periodicidade ${finance.loan.periodMonths} meses; carência ${finance.loan.gracePeriods} períodos; comissão inicial ${finance.loan.upfrontFee} ${currency}; comissão periódica ${finance.loan.recurringFee} ${currency}. Montantes a duas casas decimais; juros e prestação arredondados à unidade monetária mínima, metade para cima; última prestação ajusta o saldo residual.`,
'Cronograma do simulador; taxa declarada, sem correspondência a produto',
],
[
@@ -604,6 +608,68 @@ export function buildProjectDossierRows(
'Planeado/realizado declarado pelo utilizador',
]);
}
if (draft.cashflow_snapshot) {
const saved = restoreCashflowDraftSnapshot(
String(draft.cashflow_snapshot),
);
const origin =
'Estimativa de planeamento; pressupostos guardados da ferramenta que podem incluir exemplos iniciais. Não confirma impostos nem recebimentos.';
try {
if (!saved)
throw new Error(
'Pressupostos de tesouraria não recuperáveis; não foram substituídos por valores alternativos.',
);
const currency = String(draft.currency ?? '');
if (!currency)
throw new Error(
'Indique uma moeda para interpretar os montantes de tesouraria.',
);
const input = {
...saved.inputs,
currency,
...(saved.taxMode === 'fixed'
? { monthlyTaxSchedule: undefined }
: { monthlyTaxAssessed: '' }),
};
const projection = projectMonthlyCashflow(input);
const amount = (value: string | null) =>
value === null ? 'Indeterminado' : `${value} ${currency}`;
rows.push([
'Tesouraria',
'Pressupostos e período',
`12 meses; moeda ${currency}; saldo inicial ${saved.inputs.openingCash}; receita mensal ${saved.inputs.monthlyRevenue}; custos mensais ${saved.inputs.monthlyOperatingCosts}; dívida mensal ${saved.inputs.monthlyDebtService}; imposto ${saved.taxMode === 'fixed' ? 'mensal fixo: ' + (saved.inputs.monthlyTaxAssessed || 'não informado') : 'variável por mês (vazio não é zero)'}. Crédito estimado ${saved.inputs.vatCreditEstimated}; pedido ${saved.inputs.vatRefundRequested}; reconhecido ${saved.inputs.vatRefundRecognized}; recebimento previsto ${saved.inputs.vatRefundExpectedReceipt}; mês previsto ${saved.inputs.vatRefundExpectedMonth || 'não informado'}.`,
origin,
]);
rows.push([
'Tesouraria',
'Saldo final total',
amount(projection.endingCash),
origin,
]);
rows.push([
'Tesouraria',
'Caixa conhecida antes de imposto em falta',
amount(projection.endingCashBeforeUnassessedTax),
origin,
]);
for (const row of projection.rows)
rows.push([
'Tesouraria',
`Mês ${row.month}`,
`Imposto assumido: ${amount(row.taxAssessed)}; recebimento de IVA previsto: ${amount(row.vatRefundExpectedReceipt)}; fluxo líquido: ${amount(row.netFlow)}; saldo final total: ${amount(row.closingCash)}.`,
origin,
]);
} catch (cause) {
rows.push([
'Tesouraria',
'Estado da tesouraria',
cause instanceof Error
? cause.message
: 'Tesouraria indeterminada; reveja os pressupostos.',
origin,
]);
}
}
return rows;
}
@@ -0,0 +1,34 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\VirtualDesk;
use App\Support\IsolatedQaDatabase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Str;
use Tests\TestCase;
class SimulatorCashflowContinuityTest extends TestCase
{
use DatabaseTransactions;
public function test_cashflow_snapshot_recovers_in_each_dossier_after_a_new_session_without_turning_unknown_tax_into_zero(): void
{
$this->assertTrue(IsolatedQaDatabase::allowsSchemaChanges());
config(['investor-path.enabled' => true]);
$owner = User::factory()->create(['name' => 'SINTETICO QA continuidade']);
$cases = [];
foreach (['77.01', '99.02'] as $cash) {
$case = app(VirtualDesk::class)->createCase($owner, ['title' => 'SINTETICO QA tesouraria '.$cash, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true]);
$snapshot = json_encode(['version' => 1, 'taxMode' => 'schedule', 'inputs' => ['openingCash' => $cash, 'monthlyRevenue' => '200', 'monthlyOperatingCosts' => '150', 'monthlyDebtService' => '20', 'monthlyTaxAssessed' => '', 'monthlyTaxSchedule' => ['10', '', '20', ...array_fill(0, 9, '0')], 'vatCreditEstimated' => '0', 'vatRefundRequested' => '0', 'vatRefundRecognized' => '0', 'vatRefundExpectedReceipt' => '0', 'vatRefundExpectedMonth' => '']], JSON_THROW_ON_ERROR);
$this->actingAs($owner)->postJson('/api/v1/simulation-drafts', ['context' => $case->id, 'personal_snapshot' => [], 'economic_snapshot' => ['purpose' => 'simulate_only', 'journey_type' => 'small_business', 'cashflow_snapshot' => $snapshot]], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated();
$cases[] = [$case->id, $snapshot];
}
$this->flushSession();
foreach ($cases as [$id,$snapshot]) {
$this->actingAs(User::findOrFail($owner->id))->getJson('/api/v1/simulation-drafts/current?context='.$id)->assertOk()->assertJsonPath('data.economic_snapshot.cashflow_snapshot', $snapshot);
}
$this->actingAs(User::factory()->create())->getJson('/api/v1/simulation-drafts/current?context='.$cases[0][0])->assertForbidden();
}
}
@@ -0,0 +1,16 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
class SimulatorReviewMessagesTest extends TestCase
{
public function test_missing_api_route_has_a_portuguese_message_without_changing_the_status(): void
{
config(['app.debug' => false]);
$this->getJson('/api/v1/synthetic-qa-route-not-installed')->assertNotFound()
->assertJsonPath('message', 'O registo não foi encontrado ou não está acessível nesta conta.')
->assertHeader('X-Correlation-ID');
}
}
@@ -11,6 +11,28 @@ use PHPUnit\Framework\TestCase;
final class SimulationTaxParameterResolverTest extends TestCase
{
#[Test]
public function synthetic_qa_versions_keep_historical_values_and_expose_a_period_transition_without_activating_rules(): void
{
$before = $this->publicVersion('2026-01-01', '2026-06-30', '7.5', 1);
$after = $this->publicVersion('2026-07-01', null, '9.25', 2);
$before['rule_code'] = $after['rule_code'] = 'QA.SYNTHETIC.NOT.LEGISLATION';
$before['source']['title'] = $after['source']['title'] = 'SINTETICO QA — nao e legislacao';
$before['source']['diploma'] = $after['source']['diploma'] = 'QA-FIXTURE-ONLY';
$resolver = new SimulationTaxParameterResolver;
$historical = $resolver->resolveAvailableParameters([$before], 'IVA', '2026-06-01', '2026-06-30', ['operation' => 'domestic']);
$frozen = json_encode($historical, JSON_THROW_ON_ERROR);
$current = $resolver->resolveAvailableParameters([$before, $after], 'IVA', '2026-07-01', '2026-07-31', ['operation' => 'domestic']);
self::assertSame('7.5', $historical[0]['value']);
self::assertSame('9.25', $current[0]['value']);
self::assertSame(2, $current[0]['rule_version']);
self::assertSame('indeterminate_period_change', $resolver->resolveAvailableParameters([$before, $after], 'IVA', '2026-01-01', '2026-12-31', ['operation' => 'domestic'])[0]['status']);
self::assertSame($frozen, json_encode($historical, JSON_THROW_ON_ERROR));
self::assertSame($historical, $resolver->resolveAvailableParameters([$before, $after], 'IVA', '2026-06-01', '2026-06-30', ['operation' => 'domestic']));
$after['source']['version_no'] = 2;
self::assertSame([], $resolver->resolveAvailableParameters([$after], 'IVA', '2026-07-01', '2026-07-31', ['operation' => 'domestic']));
}
#[Test]
public function it_selects_the_published_version_effective_on_the_requested_date_and_returns_its_source(): void
{
@@ -0,0 +1,197 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { forecastBusinessPlan } from '../../resources/js/lib/business-financial-plan.ts';
import { projectMonthlyCashflow } from '../../resources/js/lib/monthly-cashflow.ts';
import {
defaultCashflowDraftInputs,
serializeCashflowDraftSnapshot,
restoreCashflowDraftSnapshot,
} from '../../resources/js/lib/monthly-cashflow.ts';
import { serializeFinancialToolsSnapshot } from '../../resources/js/lib/financial-calculators.ts';
import { buildProjectDossierRows } from '../../resources/js/lib/project-dossier.ts';
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
test('cashflow draft recovery preserves unanswered months and invalid editable values without using zero', () => {
const inputs = {
...defaultCashflowDraftInputs(),
openingCash: '77.01',
monthlyTaxSchedule: ['10', '', '20', ...Array(9).fill('0')],
};
const stored = serializeCashflowDraftSnapshot('schedule', inputs);
const restored = restoreCashflowDraftSnapshot(stored);
assert.equal(restored.taxMode, 'schedule');
assert.deepEqual(restored.inputs, inputs);
assert.equal(restored.inputs.monthlyTaxSchedule[1], '');
const rows = buildProjectDossierRows({
currency: 'MZN',
cashflow_snapshot: stored,
});
assert.equal(
rows.find(
(row) => row[0] === 'Tesouraria' && row[1] === 'Saldo final total',
)[2],
'Indeterminado',
);
assert.ok(
rows
.find((row) => row[0] === 'Tesouraria' && row[1] === 'Mês 2')[2]
.includes('Imposto assumido: Indeterminado'),
);
const report = createProjectDossierReport(
{
currency: 'MZN',
cashflow_snapshot: stored,
},
{
selectedSections: new Set(['summary', 'finance']),
version: 'SINTETICO QA',
},
);
assert.equal(
report.sections
.find((section) => section.id === 'summary')
.rows.find(
(row) => row.field === 'Saldo final de tesouraria — 12 meses',
).value,
'Indeterminado',
);
const invalid = { ...inputs, monthlyRevenue: 'corrigir' };
assert.equal(
restoreCashflowDraftSnapshot(
serializeCashflowDraftSnapshot('fixed', invalid),
).inputs.monthlyRevenue,
'corrigir',
);
assert.equal(restoreCashflowDraftSnapshot('{"version":2}'), null);
assert.equal(
restoreCashflowDraftSnapshot(
stored.replace('"version":1', '"version":2'),
),
null,
);
});
test('credit report preserves the complete reproducible recipe alongside the result', () => {
const rows = buildProjectDossierRows({
purpose: 'simulate_only',
journey_type: 'small_business',
currency: 'MZN',
financial_tools_snapshot: serializeFinancialToolsSnapshot({
budgetPeriod: 'Mensal',
loan: {
principal: '1000',
rate: '12',
months: '12',
rateType: 'nominal',
periodMonths: '1',
gracePeriods: '0',
upfrontFee: '0',
recurringFee: '0',
},
breakEven: { fixed: '100', price: '20', variable: '10' },
funding: { total: '1000', secured: '0' },
incomes: [{ label: 'QA', amount: '100' }],
expenses: [{ label: 'QA', amount: '50' }],
}),
});
const recipe = rows.find(
(row) => row[1] === 'Método e pressupostos substituídos',
)[2];
for (const text of [
'1000 MZN',
'12% nominal anual',
'12 meses',
'periodicidade 1 meses',
'carência 0',
'comissão inicial 0 MZN',
'comissão periódica 0 MZN',
'duas casas decimais',
'metade para cima',
'saldo residual',
])
assert.ok(recipe.includes(text), text);
assert.equal(
rows.find((row) => row[1] === 'Prestação do primeiro período')[2],
'88.85 MZN',
);
});
// Synthetic QA control ledger, independently calculated before invoking the engines.
// Sales 200; variable expense 80; operations 20; payroll 10;
// depreciation 10; interest expense 5 => profit before tax 75/month.
// Cash: receipts 180; inventory 70; operations 25; payroll 10;
// interest paid 7; principal paid 12 => cash increases 56/month.
// Initial funding 1200; asset 600 + inventory 100 + receivables 50 - payables 20 = 730;
// opening cash 470 => closing 638 after three months. No tax rate is assumed.
test('independent three-month ledger separates profit working capital and cash', () => {
const result = forecastBusinessPlan({
currency: 'MZN',
openingCash: '0',
products: [
{
label: 'SINTETICO QA',
monthlyUnits: '10',
unitPrice: '20',
unitVariableCost: '8',
},
],
monthlyCollections: '180',
monthlyVariableCostPayments: '70',
monthlyInventoryPurchases: '70',
monthlyOperatingExpenses: '20',
monthlyOperatingPayments: '25',
monthlyPayrollExpense: '10',
monthlyPayrollPayments: '10',
assetInvestment: '600',
inventory: '100',
receivables: '50',
payables: '20',
equity: '1000',
shareholderLoans: '0',
bankLoanProceeds: '200',
grants: '0',
assetDepreciation: '600',
depreciationLifeMonths: 60,
monthlyInterestExpense: '5',
monthlyInterestPayment: '7',
monthlyPrincipalRepayment: '12',
periods: 3,
});
assert.equal(result.workingCapital, '130.00');
assert.equal(result.initialUses, '730.00');
assert.equal(result.openingCashAfterInitialUses, '470.00');
assert.equal(result.totalProfitBeforeTax, '225.00');
assert.equal(result.endingCash, '638.00');
assert.equal(result.totalCashCollections, '540.00');
});
test('independent cash ledger excludes VAT stages and preserves the unknown tax month', () => {
const input = {
currency: 'MZN',
openingCash: '100',
monthlyRevenue: '200',
monthlyOperatingCosts: '150',
monthlyDebtService: '20',
monthlyTaxSchedule: ['10', '', '20'],
vatCreditEstimated: '1000',
vatRefundRequested: '900',
vatRefundRecognized: '800',
vatRefundExpectedReceipt: '50',
vatRefundExpectedMonth: '2',
periods: 3,
};
const result = projectMonthlyCashflow(input);
// Known cash: 100 + 20 + 80 + 10 = 210; month 2 tax is unknown.
assert.equal(result.rows[0].closingCash, '120.00');
assert.equal(result.rows[1].closingCash, null);
assert.equal(result.endingCash, null);
assert.equal(result.endingCashBeforeUnassessedTax, '210.00');
assert.equal(
projectMonthlyCashflow({
...input,
monthlyTaxSchedule: ['10', '0', '20'],
}).endingCash,
'210.00',
);
assert.equal(input.monthlyTaxSchedule[1], '');
});