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('an invalid fiscal operation date has a field error and no substitute period', () => { for (const date of ['', '2026-99-99', '2026-02-30', '2025-06-15']) { const draft = { fiscal_period: '2026', fiscal_periodicity: 'one_off', fiscal_scenario_mode: 'approved_parameters', fiscal_reference_date: date, }; assert.equal(getFiscalPeriodRange(draft), null); assert.match( validateSimulationDraft(draft, 'tax').fiscal_reference_date, /data válida/, ); } assert.equal( validateSimulationDraft( { fiscal_period: '2026', fiscal_periodicity: 'one_off', fiscal_scenario_mode: 'approved_parameters', fiscal_reference_date: '2026-06-15', }, 'tax', ).fiscal_reference_date, undefined, ); }); void test('fiscal period range requires and respects exact annual, monthly, quarterly, and one-off dates', () => { for (const invalid of [ '2026-99-99', '2026-02-30', '2026-00-01', '2026-01-00', '', ]) { assert.equal( getFiscalPeriodRange({ fiscal_period: '2026', fiscal_periodicity: 'one_off', fiscal_reference_date: invalid, }), null, ); } 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', () => { assert.deepEqual( validateSimulationDraft( { fiscal_scenario_mode: 'custom_planning' }, 'tax', ), {}, ); 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""\)"/, ); });