diff --git a/app/Http/Requests/StoreSimulationFormDraftRequest.php b/app/Http/Requests/StoreSimulationFormDraftRequest.php index d1a8d3b8..74cf0d90 100644 --- a/app/Http/Requests/StoreSimulationFormDraftRequest.php +++ b/app/Http/Requests/StoreSimulationFormDraftRequest.php @@ -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'], diff --git a/app/Services/SimulationTaxParameterResolver.php b/app/Services/SimulationTaxParameterResolver.php index 8d758d5e..a8881434 100644 --- a/app/Services/SimulationTaxParameterResolver.php +++ b/app/Services/SimulationTaxParameterResolver.php @@ -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; diff --git a/app/Services/SimulationTaxRuleExpression.php b/app/Services/SimulationTaxRuleExpression.php index 9821ac6b..ceb1fbec 100644 --- a/app/Services/SimulationTaxRuleExpression.php +++ b/app/Services/SimulationTaxRuleExpression.php @@ -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); diff --git a/bootstrap/app.php b/bootstrap/app.php index be108a69..f7f6d9a3 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,6 +1,7 @@ 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', diff --git a/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json b/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json new file mode 100644 index 00000000..54ebb083 --- /dev/null +++ b/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json @@ -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" +} diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index bb240277..efed4715 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -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. diff --git a/docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv b/docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv index c030f1eb..8571cb7e 100644 --- a/docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv +++ b/docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv @@ -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, diff --git a/docs/investmoz/v2/auditoria-portal/backlog.md b/docs/investmoz/v2/auditoria-portal/backlog.md index fee32b81..747d0beb 100644 --- a/docs/investmoz/v2/auditoria-portal/backlog.md +++ b/docs/investmoz/v2/auditoria-portal/backlog.md @@ -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. diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv index eb244868..f2e61250 100644 --- a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv +++ b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv @@ -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,,,,,,,,,,,,,, diff --git a/resources/js/components/simulation-cashflow-planner.tsx b/resources/js/components/simulation-cashflow-planner.tsx index e72c19f0..92995f4d 100644 --- a/resources/js/components/simulation-cashflow-planner.tsx +++ b/resources/js/components/simulation-cashflow-planner.tsx @@ -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, 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.'}

{projection?.status === 'incomplete' && (

setTaxMode('fixed')} + onChange={() => update(inputs, 'fixed')} /> Valor mensal fixo @@ -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 @@ -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" /> diff --git a/resources/js/components/simulation-financial-tools.tsx b/resources/js/components/simulation-financial-tools.tsx index f3351962..2b53c746 100644 --- a/resources/js/components/simulation-financial-tools.tsx +++ b/resources/js/components/simulation-financial-tools.tsx @@ -352,6 +352,12 @@ export function SimulationFinancialTools({ /> {schedule && isSimulationCurrency(currency) && ( <> +

+ 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. +

Taxa por período: {schedule.periodicRatePercent} %; desembolso líquido: { diff --git a/resources/js/components/simulation-form.tsx b/resources/js/components/simulation-form.tsx index 71fcc2bd..f3e51e69 100644 --- a/resources/js/components/simulation-form.tsx +++ b/resources/js/components/simulation-form.tsx @@ -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(null); const draftEtagRef = useRef(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')) && ( + 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.

)} - {current.id === 'tax' && ( + {(current.id === 'tax' || result) && (

- Cenário personalizado de planeamento + {current.id === 'tax' + ? 'Cenário personalizado de planeamento' + : 'Resultado histórico seleccionado'}

- {draft.fiscal_scenario_mode === 'custom_planning' && ( -

- 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 && ( +

+ Este resultado conserva os pressupostos e a + versão da execução seleccionada. Não substitui + as entradas deste dossier.

)} - {draft.fiscal_scenario_mode === 'custom_planning' && ( - - )} + {current.id === 'tax' && + draft.fiscal_scenario_mode === + 'custom_planning' && ( +

+ 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' && + draft.fiscal_scenario_mode === + 'custom_planning' && ( + + )} {serverError && (

{serverError} diff --git a/resources/js/lib/financial-calculators.ts b/resources/js/lib/financial-calculators.ts index 5a2d187d..b8834df5 100644 --- a/resources/js/lib/financial-calculators.ts +++ b/resources/js/lib/financial-calculators.ts @@ -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'], diff --git a/resources/js/lib/monthly-cashflow.ts b/resources/js/lib/monthly-cashflow.ts index 33f30fd2..fb614038 100644 --- a/resources/js/lib/monthly-cashflow.ts +++ b/resources/js/lib/monthly-cashflow.ts @@ -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; +}; + +/** 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; diff --git a/resources/js/lib/project-dossier-report.ts b/resources/js/lib/project-dossier-report.ts index 501085ba..7d732ff9 100644 --- a/resources/js/lib/project-dossier-report.ts +++ b/resources/js/lib/project-dossier-report.ts @@ -171,6 +171,7 @@ const rowSection: Record = { '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, diff --git a/resources/js/lib/project-dossier.ts b/resources/js/lib/project-dossier.ts index 2ba68b90..34e9b205 100644 --- a/resources/js/lib/project-dossier.ts +++ b/resources/js/lib/project-dossier.ts @@ -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; } diff --git a/tests/Feature/SimulatorCashflowContinuityTest.php b/tests/Feature/SimulatorCashflowContinuityTest.php new file mode 100644 index 00000000..12d18161 --- /dev/null +++ b/tests/Feature/SimulatorCashflowContinuityTest.php @@ -0,0 +1,34 @@ +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(); + } +} diff --git a/tests/Feature/SimulatorReviewMessagesTest.php b/tests/Feature/SimulatorReviewMessagesTest.php new file mode 100644 index 00000000..38d60f36 --- /dev/null +++ b/tests/Feature/SimulatorReviewMessagesTest.php @@ -0,0 +1,16 @@ + 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'); + } +} diff --git a/tests/Unit/SimulationTaxParameterResolverTest.php b/tests/Unit/SimulationTaxParameterResolverTest.php index d733a4f3..3428bb6e 100644 --- a/tests/Unit/SimulationTaxParameterResolverTest.php +++ b/tests/Unit/SimulationTaxParameterResolverTest.php @@ -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 { diff --git a/tests/frontend/simulator-independent-review.test.mjs b/tests/frontend/simulator-independent-review.test.mjs new file mode 100644 index 00000000..5c1df17c --- /dev/null +++ b/tests/frontend/simulator-independent-review.test.mjs @@ -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], ''); +});