fix: recover simulator cashflow and verify QA continuity and report assumptions
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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], '');
|
||||
});
|
||||
Reference in New Issue
Block a user