Files
Investidor-V2/tests/frontend/simulation-form.test.mjs
T
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

865 lines
27 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
getSimulationSections,
simulationFieldGroups,
getAutosaveSnapshot,
advancedSimulationFields,
partitionSimulationDraft,
replaceSimulationValue,
restoreSimulationDraft,
simulationOptions,
validateCustomFiscalPlanning,
simulationDraftFingerprint,
validateSimulationDraft,
exportCompletedArithmeticCsv,
readArithmeticSnapshot,
readVersionedArithmeticReportReference,
moveSearchChoiceIndex,
getFiscalPeriodRange,
} from '../../resources/js/lib/simulation-form.ts';
void test('fiscal period range requires and respects exact annual, monthly, quarterly, and one-off dates', () => {
assert.deepEqual(
getFiscalPeriodRange({
fiscal_period: '2024',
fiscal_periodicity: 'annual',
}),
{ start: '2024-01-01', end: '2024-12-31' },
);
assert.deepEqual(
getFiscalPeriodRange({
fiscal_period: '2024',
fiscal_periodicity: 'monthly',
fiscal_month: '02',
}),
{ start: '2024-02-01', end: '2024-02-29' },
);
assert.deepEqual(
getFiscalPeriodRange({
fiscal_period: '2026',
fiscal_periodicity: 'monthly',
}),
null,
);
assert.deepEqual(
getFiscalPeriodRange({
fiscal_period: '2026',
fiscal_periodicity: 'quarterly',
fiscal_quarter: 'Q4',
}),
{ start: '2026-10-01', end: '2026-12-31' },
);
assert.deepEqual(
getFiscalPeriodRange({
fiscal_period: '2026',
fiscal_periodicity: 'one_off',
fiscal_reference_date: '2026-06-15',
}),
{ start: '2026-06-15', end: '2026-06-15' },
);
assert.deepEqual(
getFiscalPeriodRange({
fiscal_period: '2026',
fiscal_periodicity: 'one_off',
fiscal_reference_date: '2025-06-15',
}),
null,
);
});
void test('searchable choice keyboard navigation starts at the first result and stays in bounds', () => {
assert.equal(moveSearchChoiceIndex(-1, 1, 3), 0);
assert.equal(moveSearchChoiceIndex(-1, -1, 3), 2);
assert.equal(moveSearchChoiceIndex(0, -1, 3), 0);
assert.equal(moveSearchChoiceIndex(2, 1, 3), 2);
assert.equal(moveSearchChoiceIndex(0, 1, 0), -1);
assert.equal(moveSearchChoiceIndex(0, -1, -1), -1);
});
void test('anonymous arithmetic journey skips formal investor and project sections', () => {
assert.deepEqual(
getSimulationSections({ purpose: 'simulate_only' }).map(
(section) => section.id,
),
['purpose', 'tax', 'review'],
);
assert.equal(
getSimulationSections({ purpose: 'own_investment' }).length,
11,
);
});
void test('small business exploratory draft includes idea and finance without mandatory formal project data', () => {
const draft = {
journey_type: 'small_business',
purpose: 'simulate_only',
detail_level: 'simple',
};
assert.deepEqual(
getSimulationSections(draft).map((section) => section.id),
['purpose', 'market', 'location', 'team', 'financial', 'review'],
);
assert.deepEqual(validateSimulationDraft(draft, 'market'), {});
const ideaFields = [
...simulationFieldGroups.project,
...simulationFieldGroups.market,
]
.filter((field) => field.showWhen?.(draft))
.filter(
(field, index, all) =>
all.findIndex((candidate) => candidate.key === field.key) ===
index,
);
for (const key of [
'project_title',
'project_summary',
'project_problem',
'target_customers',
'value_proposition',
'market_assumption',
'market_source',
'project_type',
'sector_id',
'activity_id',
'products_services',
'maturity_stage',
'expected_start_date',
'project_duration_years',
])
assert.ok(
ideaFields.some((field) => field.key === key),
`missing ${key}`,
);
assert.equal(
ideaFields.some((field) =>
typeof field.required === 'function'
? field.required(draft)
: Boolean(field.required),
),
false,
);
assert.equal(
getSimulationSections(draft).some((section) =>
['identity', 'documents', 'tax'].includes(section.id),
),
false,
);
assert.deepEqual(validateSimulationDraft(draft, 'location'), {});
assert.deepEqual(validateSimulationDraft(draft, 'opportunity'), {});
});
void test('project investment evaluation opens a direct project, location, finance, tax and analysis journey', () => {
const selected = replaceSimulationValue({}, 'journey_type', 'investor');
assert.equal(selected.purpose, 'simulate_only');
const sections = getSimulationSections(selected).map(
(section) => section.id,
);
assert.deepEqual(sections, [
'purpose',
'project',
'location',
'financial',
'tax',
'scenarios',
'procedures',
'review',
]);
for (const sectionId of [
'project',
'location',
'financial',
'tax',
'procedures',
]) {
assert.deepEqual(validateSimulationDraft(selected, sectionId), {});
}
const fieldsVisible = (group) =>
simulationFieldGroups[group].filter(
(field) => field.showWhen?.(selected) ?? true,
);
assert.ok(
fieldsVisible('project').some((field) => field.key === 'activity_id'),
);
assert.ok(
fieldsVisible('location').some(
(field) => field.key === 'infrastructure_needs',
),
);
assert.ok(
fieldsVisible('financial').some(
(field) => field.key === 'total_investment',
),
);
assert.equal(simulationFieldGroups.procedures[0].showWhen(selected), true);
assert.ok(simulationOptions.infrastructure.includes('raw_materials'));
assert.ok(simulationOptions.infrastructure.includes('legal_requirements'));
});
void test('public journeys offer distinct section paths for personal, small business, company, and investor', () => {
assert.deepEqual(
getSimulationSections({
journey_type: 'personal',
purpose: 'own_investment',
}).map((section) => section.id),
['purpose', 'financial', 'tax', 'review'],
);
assert.deepEqual(
getSimulationSections({
journey_type: 'small_business',
purpose: 'own_investment',
}).map((section) => section.id),
[
'purpose',
'promoter',
'project',
'financial',
'impact',
'tax',
'review',
],
);
assert.equal(
getSimulationSections({
journey_type: 'company',
purpose: 'own_investment',
}).length,
11,
);
assert.deepEqual(
getSimulationSections({
journey_type: 'investor',
purpose: 'seek_investor',
}).map((section) => section.id),
[
'purpose',
'promoter',
'identity',
'project',
'location',
'financial',
'opportunity',
'documents',
'review',
],
);
assert.equal(advancedSimulationFields.has('deductible_costs'), true);
});
void test('switching to anonymous mode keeps non-sensitive journey and detail choices', () => {
assert.deepEqual(
replaceSimulationValue(
{
purpose: 'own_investment',
journey_type: 'personal',
detail_level: 'advanced',
legal_name: 'Nome',
email: 'a@b.co',
},
'purpose',
'simulate_only',
),
{
purpose: 'simulate_only',
journey_type: 'personal',
detail_level: 'advanced',
legal_name: 'Nome',
email: 'a@b.co',
},
);
});
void test('changing territorial or sector parents clears every dependent value', () => {
assert.deepEqual(
replaceSimulationValue(
{
province_id: 'p1',
district_id: 'd1',
admin_post_id: 'a1',
locality_id: 'l1',
},
'province_id',
'p2',
),
{ province_id: 'p2' },
);
assert.deepEqual(
replaceSimulationValue(
{ sector_id: 's1', activity_id: 'a1', activity_id_label: 'A' },
'sector_id',
's2',
),
{ sector_id: 's2' },
);
});
void test('changing the purpose preserves private and project answers for later correction', () => {
const next = replaceSimulationValue(
{
purpose: 'own_investment',
legal_name: 'Empresa',
email: 'a@b.co',
project_title: 'Projecto',
total_investment: '10',
total_investment_label: '10',
},
'purpose',
'simulate_only',
);
assert.deepEqual(next, {
purpose: 'simulate_only',
legal_name: 'Empresa',
email: 'a@b.co',
project_title: 'Projecto',
total_investment: '10',
total_investment_label: '10',
});
});
void test('approved-parameter fiscal mode does not ask users to enter a rate', () => {
const base = {
purpose: 'simulate_only',
fiscal_period: '2026',
fiscal_periodicity: 'monthly',
fiscal_month: '04',
fiscal_component: 'iva',
fiscal_residency_assessment: 'unknown',
fiscal_operation: 'domestic',
fiscal_establishments: '1',
fiscal_documentation: 'unknown',
fiscal_scenario_mode: 'approved_parameters',
};
assert.deepEqual(validateSimulationDraft(base, 'tax'), {});
});
void test('custom planning mode requires user assumptions and validates percentage limits', () => {
const base = {
purpose: 'simulate_only',
fiscal_period: '2026',
fiscal_periodicity: 'monthly',
fiscal_component: 'iva',
fiscal_residency_assessment: 'unknown',
fiscal_operation: 'domestic',
fiscal_establishments: '1',
fiscal_documentation: 'unknown',
fiscal_scenario_mode: 'custom_planning',
};
assert.deepEqual(validateSimulationDraft(base, 'tax'), {});
assert.deepEqual(validateCustomFiscalPlanning(base), {
base: 'Este pressuposto é necessário para executar este cenário.',
rate: 'Este pressuposto é necessário para executar este cenário.',
reduction: 'Este pressuposto é necessário para executar este cenário.',
});
assert.deepEqual(
validateSimulationDraft(
{ ...base, fiscal_documentation: 'unknown' },
'tax',
),
{},
);
const overLimit = {
...base,
fiscal_documentation: 'unknown',
base: '0',
rate: '101',
reduction: '0',
};
assert.deepEqual(validateSimulationDraft(overLimit, 'tax'), {});
assert.equal(
validateCustomFiscalPlanning(overLimit).rate,
'O valor não pode exceder 100.',
);
});
void test('fiscal intake asks only component-specific questions and leaves residence unresolved when unknown', () => {
assert.equal(
simulationOptions.fiscal_period.includes(
String(new Date().getFullYear() + 1),
),
true,
);
const ispcErrors = validateSimulationDraft(
{
purpose: 'simulate_only',
fiscal_period: '2026',
fiscal_periodicity: 'quarterly',
fiscal_component: 'ispc',
fiscal_scenario_mode: 'approved_parameters',
},
'tax',
);
assert.equal(ispcErrors.fiscal_activity, 'Este campo é obrigatório.');
assert.equal(ispcErrors.fiscal_establishments, 'Este campo é obrigatório.');
assert.equal(ispcErrors.fiscal_residency_assessment, undefined);
const irpsErrors = validateSimulationDraft(
{
purpose: 'simulate_only',
fiscal_period: '2026',
fiscal_periodicity: 'annual',
fiscal_component: 'irps',
fiscal_scenario_mode: 'approved_parameters',
},
'tax',
);
assert.equal(
irpsErrors.fiscal_residency_assessment,
'Este campo é obrigatório.',
);
assert.equal(
irpsErrors.fiscal_income_category,
'Este campo é obrigatório.',
);
assert.deepEqual(simulationOptions.irps_salary_view, [
'monthly_net_estimate',
'monthly_withholding',
'annual_assessment',
]);
const salaryViewErrors = validateSimulationDraft(
{
purpose: 'simulate_only',
fiscal_period: '2026',
fiscal_periodicity: 'monthly',
fiscal_component: 'irps',
fiscal_income_category: 'employment',
fiscal_residency_assessment: 'unknown',
fiscal_documentation: 'unknown',
fiscal_scenario_mode: 'approved_parameters',
},
'tax',
);
assert.equal(
salaryViewErrors.irps_salary_view,
'Este campo é obrigatório.',
);
const customsErrors = validateSimulationDraft(
{
purpose: 'simulate_only',
fiscal_period: '2026',
fiscal_periodicity: 'one_off',
fiscal_component: 'customs_ice',
fiscal_operation: 'import',
fiscal_scenario_mode: 'approved_parameters',
},
'tax',
);
assert.equal(
customsErrors.fiscal_customs_code,
'Este campo é obrigatório.',
);
});
void test('financial reconciliation reports a mismatch without inventing a fiscal value', () => {
const errors = validateSimulationDraft(
{
currency: 'USD',
total_investment: '100',
own_capital: '20',
domestic_debt: '20',
foreign_debt: '20',
grants: '20',
},
'financial',
);
assert.match(errors.total_investment, /corresponder ao investimento total/);
assert.equal(
validateSimulationDraft({}, 'financial').currency,
'Este campo é obrigatório.',
);
});
void test('autosave omits identity and contact fields from browser storage', () => {
assert.deepEqual(
getAutosaveSnapshot({
total_investment: '50',
legal_name: 'Nome',
email: 'person@example.com',
nuit: '123',
project_title: 'Projecto',
legal_name_label: 'Nome',
}),
{ total_investment: '50', project_title: 'Projecto' },
);
});
void test('server draft snapshots split private identity from economic values', () => {
assert.deepEqual(
partitionSimulationDraft({
journey_type: 'personal',
detail_level: 'simple',
purpose: 'simulate_only',
legal_name: 'Nome',
base: '10',
financial_tools_snapshot: '{"version":1}',
rate: '2',
province_id_label: 'Tete',
person_type: 'collective',
person_type_label: 'Pessoa colectiva',
}),
{
personal_snapshot: {
legal_name: 'Nome',
person_type: 'collective',
selection_labels: { person_type: 'Pessoa colectiva' },
},
economic_snapshot: {
journey_type: 'personal',
detail_level: 'simple',
purpose: 'simulate_only',
base: '10',
financial_tools_snapshot: '{"version":1}',
rate: '2',
selection_labels: { province_id: 'Tete' },
},
},
);
assert.deepEqual(
partitionSimulationDraft({
purpose: 'own_investment',
business_plan_snapshot: '{"version":1}',
financial_tools_snapshot: '{"version":1}',
}).economic_snapshot,
{
purpose: 'own_investment',
business_plan_snapshot: '{"version":1}',
financial_tools_snapshot: '{"version":1}',
},
);
assert.deepEqual(
restoreSimulationDraft(
{
activity_id: 'uuid',
infrastructure_needs: ['energia', 'água'],
selection_labels: {
activity_id: 'Energia',
infrastructure_needs: ['Energia', 'Água'],
},
},
{},
),
{
activity_id: 'uuid',
project_questionnaire_version: 'legacy-unversioned',
activity_id_label: 'Energia',
infrastructure_needs: ['energia', 'água'],
infrastructure_needs_label: ['Energia', 'Água'],
},
);
assert.equal(
simulationDraftFingerprint({ base: '10', purpose: 'simulate_only' }),
simulationDraftFingerprint({ purpose: 'simulate_only', base: '10' }),
);
});
void test('anonymous server snapshots omit personal values and labels before transmission', () => {
assert.deepEqual(
partitionSimulationDraft(
{
legal_name: 'Nome',
legal_name_label: 'Nome',
email: 'person@example.com',
fiscal_residency_country: 'mzn-country',
fiscal_residency_country_label: 'Moçambique',
purpose: 'simulate_only',
base: '10',
financial_tools_snapshot: '{"version":1}',
},
false,
),
{
personal_snapshot: {},
economic_snapshot: {
purpose: 'simulate_only',
base: '10',
financial_tools_snapshot: '{"version":1}',
},
},
);
});
void test('server-owned converted simulation snapshots are restored separately from editable form fields', () => {
const restored = restoreSimulationDraft(
{
purpose: 'own_investment',
simulation_sources: [{ simulation_id: 'abc' }],
base: '100',
},
{},
);
assert.equal(restored.base, '100');
assert.equal('simulation_sources' in restored, false);
assert.equal(
'simulation_sources' in
partitionSimulationDraft(restored).economic_snapshot,
false,
);
});
void test('completed arithmetic CSV preserves calculation details without exporting personal data', () => {
const simulation = {
id: '12345678-aaaa-bbbb-cccc-123456789abc',
status: 'completed',
version_no: 4,
updated_at: '2026-10-02T10:00:00Z',
legal_name: 'Pessoa Privada',
email: 'private@example.com',
reports: [
{
id: 'report-1',
version_no: 3,
status: 'ready',
created_at: '2026-10-02T10:00:00Z',
report_sha256: 'a'.repeat(64),
},
],
result_snapshot: {
base: '100000.00',
rate_percent: '10',
reduction_percent: '5',
indicative_total: '9500.000000',
currency: null,
legal_interpretation: false,
formula_version: 'portal-tax-arithmetic-v1',
contract_version: 1,
context: {
reference_date: '2026-10-02',
currency: null,
currency_fraction_digits: null,
scenario_id: '87654321-aaaa-bbbb-cccc-123456789abc',
scenario_version: 3,
engine_version: 'portal-tax-arithmetic-v1+simulation-v1',
ruleset_version: null,
},
result: {
status: 'calculated',
value: '9500.000000',
unit: null,
explanation: 'Resultado aritmético; sem conclusão fiscal.',
missing_inputs: [],
source_ids: [],
},
},
};
simulation.reports[0].report_snapshot = structuredClone(
simulation.result_snapshot,
);
const csv = exportCompletedArithmeticCsv(
simulation,
'2026-10-02T11:00:00Z',
);
assert.match(csv, /^\uFEFF/);
assert.match(csv, /Versão da execução,4/);
assert.match(csv, /Versão do contrato,1/);
assert.match(csv, /Data de referência,2026-10-02/);
assert.match(csv, /ID do cenário,87654321-aaaa-bbbb-cccc-123456789abc/);
assert.match(
csv,
/Versão do motor,portal-tax-arithmetic-v1\+simulation-v1/,
);
assert.match(csv, /Estado do resultado,calculated/);
assert.match(csv, /ID do relatório,report-1/);
assert.match(csv, /Versão do relatório\/cenário,3/);
assert.match(csv, /Hash SHA-256 do relatório,aaaaaaaa/);
assert.match(csv, /Período,cenário aritmético pontual/);
assert.match(csv, /IDs de fontes,nenhum/);
assert.match(csv, /portal-tax-arithmetic-v1/);
assert.match(csv, /Base introduzida,100000\.00/);
assert.match(csv, /Resultado aritmético indicativo,9500\.000000/);
assert.match(csv, /Moeda,não fornecida/);
assert.match(csv, /Fontes legais,nenhuma associada/);
assert.doesNotMatch(csv, /Pessoa Privada|private@example\.com/);
assert.throws(
() => exportCompletedArithmeticCsv({ status: 'running' }),
/concluída/,
);
assert.throws(
() =>
exportCompletedArithmeticCsv({
status: 'completed',
result_snapshot: { base: 1 },
}),
/base/,
);
});
void test('versioned arithmetic report reference requires the exact current snapshot and a valid hash', () => {
const snapshot = {
contract_version: 1,
context: { scenario_version: 4 },
indicative_total: '12.50',
};
const simulation = {
status: 'completed',
result_snapshot: snapshot,
reports: [
{
id: 'matching-report',
status: 'ready',
version_no: 4,
report_snapshot: structuredClone(snapshot),
report_sha256: 'b'.repeat(64),
created_at: '2026-10-03T10:00:00Z',
},
],
};
assert.deepEqual(readVersionedArithmeticReportReference(simulation), {
id: 'matching-report',
version_no: 4,
report_sha256: 'b'.repeat(64),
created_at: '2026-10-03T10:00:00Z',
});
assert.equal(
readVersionedArithmeticReportReference({
...simulation,
reports: [
{
...simulation.reports[0],
report_snapshot: { ...snapshot, indicative_total: '99' },
},
],
}),
null,
);
assert.equal(
readVersionedArithmeticReportReference({
...simulation,
reports: [{ ...simulation.reports[0], report_sha256: 'invalid' }],
}),
null,
);
});
void test('versioned arithmetic CSV refuses a result envelope that differs from the legacy total', () => {
assert.throws(
() =>
exportCompletedArithmeticCsv({
status: 'completed',
result_snapshot: {
base: '10',
rate_percent: '2',
reduction_percent: '0',
indicative_total: '0.2',
legal_interpretation: false,
contract_version: 1,
context: {
reference_date: '2026-10-02',
currency: null,
scenario_id: '87654321-aaaa-bbbb-cccc-123456789abc',
engine_version:
'portal-tax-arithmetic-v1+simulation-v1',
ruleset_version: null,
},
result: {
status: 'calculated',
value: '0.3',
explanation: 'Aritmética apenas.',
},
},
}),
/não reconcilia/,
);
});
void test('versioned arithmetic CSV refuses a report from another scenario revision', () => {
const snapshot = {
indicative_total: '10.00',
legal_interpretation: false,
contract_version: 1,
context: {
reference_date: '2026-10-02',
currency: null,
currency_fraction_digits: null,
scenario_id: '87654321-aaaa-bbbb-cccc-123456789abc',
scenario_version: 2,
engine_version: 'portal-tax-arithmetic-v1+simulation-v2',
ruleset_version: null,
},
result: {
status: 'calculated',
value: '10.00',
unit: null,
explanation: 'Aritmética apenas.',
missing_inputs: [],
source_ids: [],
},
};
assert.throws(
() =>
exportCompletedArithmeticCsv({
status: 'completed',
result_snapshot: snapshot,
reports: [
{
id: 'stale-report',
status: 'ready',
version_no: 1,
report_sha256: 'a'.repeat(64),
report_snapshot: structuredClone(snapshot),
},
],
}),
/nao corresponde a versao atual do resultado/,
);
});
void test('arithmetic result reader exposes only reconciled versioned or legacy values', () => {
const snapshot = {
indicative_total: '0.20',
legal_interpretation: false,
contract_version: 1,
formula_version: 'portal-tax-arithmetic-v1',
context: {
reference_date: '2026-10-02',
currency: null,
currency_fraction_digits: null,
scenario_id: '87654321-aaaa-bbbb-cccc-123456789abc',
scenario_version: 3,
engine_version: 'portal-tax-arithmetic-v1+simulation-v1',
ruleset_version: null,
},
result: {
status: 'calculated',
value: '0.20',
unit: null,
explanation: 'Aritmética apenas.',
missing_inputs: [],
source_ids: [],
},
};
assert.equal(readArithmeticSnapshot(snapshot).integrity, 'versioned');
assert.equal(readArithmeticSnapshot(snapshot).value, '0.20');
assert.equal(
readArithmeticSnapshot({ indicative_total: '0.20' }).integrity,
'legacy',
);
assert.equal(
readArithmeticSnapshot({
...snapshot,
result: { ...snapshot.result, value: '0.2' },
}).integrity,
'invalid',
);
assert.equal(
readArithmeticSnapshot({ ...snapshot, contract_version: 99 }).integrity,
'invalid',
);
});
void test('arithmetic result CSV neutralizes formula-like report metadata', () => {
const csv = exportCompletedArithmeticCsv({
id: '=HYPERLINK("https://example.invalid")',
status: 'completed',
version_no: 1,
result_snapshot: {
base: '10',
rate_percent: '2',
reduction_percent: '0',
indicative_total: '0.2',
formula_version: 'portal-tax-arithmetic-v1',
},
});
assert.match(
csv,
/ID da execução,"'=HYPERLINK\(""https:\/\/example\.invalid""\)"/,
);
});