Files
Investidor-V2/resources/js/lib/simulation-form.ts
T

2050 lines
69 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { inheritSimulatorFields } from './context-inheritance.ts';
import { landStatusVisible } from './investment-land-preparation.ts';
import { csvCell } from './csv.js';
import { fiscalQaRows } from './fiscal-qa-preview.ts';
import { isApiSimulationResultSnapshotV1 } from './simulation-contract.ts';
export type SimulationDraft = Record<string, string | string[]>;
export const projectQuestionnaireVersion =
'investmoz-project-dossier-2026-10-05.1';
function isProjectGuidedJourney(draft: SimulationDraft): boolean {
return (
draft.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(draft.journey_type))
);
}
function isInvestorProjectJourney(draft: SimulationDraft): boolean {
return (
draft.purpose === 'simulate_only' && draft.journey_type === 'investor'
);
}
export function getFiscalPeriodRange(
draft: SimulationDraft,
): { start: string; end: string } | null {
const yearValue = String(draft.fiscal_period ?? '');
if (!/^\d{4}$/.test(yearValue)) return null;
const year = Number(yearValue);
if (draft.fiscal_periodicity === 'annual')
return { start: `${yearValue}-01-01`, end: `${yearValue}-12-31` };
if (draft.fiscal_periodicity === 'one_off') {
const date = String(draft.fiscal_reference_date ?? '');
const parsed = new Date(`${date}T00:00:00.000Z`);
return /^\d{4}-\d{2}-\d{2}$/.test(date) &&
Number.isFinite(parsed.getTime()) &&
parsed.toISOString().slice(0, 10) === date &&
date.startsWith(`${yearValue}-`)
? { start: date, end: date }
: null;
}
if (draft.fiscal_periodicity === 'monthly') {
const month = String(draft.fiscal_month ?? '');
if (!/^(0[1-9]|1[0-2])$/.test(month)) return null;
const endDay = new Date(Date.UTC(year, Number(month), 0)).getUTCDate();
return {
start: `${yearValue}-${month}-01`,
end: `${yearValue}-${month}-${String(endDay).padStart(2, '0')}`,
};
}
if (draft.fiscal_periodicity === 'quarterly') {
const quarter = String(draft.fiscal_quarter ?? '');
const startMonth = (
{ Q1: 1, Q2: 4, Q3: 7, Q4: 10 } as Record<string, number>
)[quarter];
if (!startMonth) return null;
const endMonth = startMonth + 2;
const endDay = new Date(Date.UTC(year, endMonth, 0)).getUTCDate();
return {
start: `${yearValue}-${String(startMonth).padStart(2, '0')}-01`,
end: `${yearValue}-${String(endMonth).padStart(2, '0')}-${String(endDay).padStart(2, '0')}`,
};
}
return null;
}
export const simulationSections = [
{ id: 'purpose', title: 'Finalidade' },
{ id: 'promoter', title: 'Perfil do promotor' },
{ id: 'identity', title: 'Identificação e contactos' },
{ id: 'project', title: 'Projecto ou negócio' },
{ id: 'location', title: 'Localização' },
{ id: 'financial', title: 'Estrutura financeira' },
{ id: 'impact', title: 'Emprego e impacto' },
{ id: 'tax', title: 'Operação fiscal' },
{ id: 'opportunity', title: 'Necessidade de negócio' },
{ id: 'documents', title: 'Documentos' },
{ id: 'review', title: 'Revisão' },
] as const;
/** Keep the anonymous arithmetic journey short; formal submissions retain the full questionnaire. */
export function getSimulationSections(draft: SimulationDraft) {
if (isProjectGuidedJourney(draft)) {
if (draft.journey_type === 'small_business') {
return [
{ id: 'purpose', title: 'Ideia e objectivos' },
{ id: 'market', title: 'Clientes e mercado' },
{ id: 'location', title: 'Localização e recursos' },
{ id: 'team', title: 'Equipa e execução' },
{ id: 'financial', title: 'Investimento e financiamento' },
{ id: 'review', title: 'Resultados, riscos e plano' },
];
}
if (draft.journey_type === 'investor') {
return [
...simulationSections.filter((section) =>
[
'purpose',
'project',
'location',
'financial',
'tax',
].includes(section.id),
),
{ id: 'scenarios', title: 'Cenários e retorno indicativo' },
{ id: 'procedures', title: 'Procedimentos e documentos' },
{ id: 'review', title: 'Revisão' },
];
}
}
if (draft.purpose === 'simulate_only') {
return simulationSections.filter((section) =>
['purpose', 'tax', 'review'].includes(section.id),
);
}
const journeys: Record<string, string[]> = {
personal: ['purpose', 'financial', 'tax', 'review'],
small_business: [
'purpose',
'promoter',
'project',
'financial',
'tax',
'impact',
'review',
],
company: simulationSections.map((section) => section.id),
investor: [
'purpose',
'promoter',
'identity',
'project',
'location',
'financial',
'opportunity',
'documents',
'review',
],
};
if (draft.journey_type && journeys[String(draft.journey_type)]) {
return simulationSections.filter((section) =>
journeys[String(draft.journey_type)].includes(section.id),
);
}
return simulationSections;
}
export const advancedSimulationFields = new Set([
'legal_form',
'nuit',
'registration_number',
'representative_name',
'representative_capacity',
'address',
'existing_project_id',
'expected_start_date',
'project_duration_years',
'latitude',
'longitude',
'area_required',
'area_unit',
'special_zone_id',
'infrastructure_needs',
'secured_amount',
'sought_amount',
'annual_revenue',
'annual_operating_costs',
'expected_profit',
'financing_term_months',
'funds_origin_countries',
'direct_jobs',
'indirect_jobs',
'national_jobs',
'foreign_jobs',
'skills_needed',
'local_content_percentage',
'exports_value',
'import_substitution_value',
'environmental_category',
'community_benefits',
'sales_taxable',
'sales_exempt',
'sales_zero_rate',
'exports',
'deductible_costs',
'non_deductible_costs',
'carried_losses',
'payroll',
'outbound_payments',
'imports',
'claimed_benefits',
'required_documents',
]);
export const simulationOptions: Record<string, string[]> = {
journey_type: ['personal', 'small_business', 'company', 'investor'],
detail_level: ['simple', 'advanced'],
purpose: [
'simulate_only',
'own_investment',
'seek_investor',
'seek_partner',
'present_opportunity',
'request_facilitation',
],
manifestation_type: ['investment', 'business'],
person_type: ['singular', 'collective', 'sole_trader', 'consortium'],
investor_origin: ['national', 'foreign', 'mixed'],
entity_stage: ['existing', 'being_formed'],
preferred_language: ['pt-MZ', 'en'],
preferred_channel: ['portal', 'email', 'phone'],
project_type: [
'new',
'expansion',
'acquisition',
'partnership',
'concession',
'supplier',
'buyer',
'other',
],
maturity_stage: [
'idea',
'concept',
'pre_feasibility',
'feasibility',
'financing',
'implementation',
'operation',
],
land_status: ['owned', 'duat', 'leased', 'requested', 'unknown'],
environmental_category: ['A', 'B', 'C', 'por_determinar'],
offer_or_demand: ['offer', 'demand', 'both'],
opportunity_type: [
'capital',
'partner',
'buyer',
'supplier',
'land',
'technology',
'licence',
'services',
'other',
],
confidentiality: [
'public',
'registered_users',
'restricted',
'confidential',
],
preferred_publication: ['yes', 'no'],
residency_status: ['resident', 'non_resident'],
fiscal_scenario_mode: [
'approved_parameters',
'custom_planning',
'no_calculation',
],
fiscal_period: Array.from(
{ length: new Date().getFullYear() - 1999 + 2 },
(_, index) => String(2000 + index),
),
fiscal_periodicity: [
'monthly',
'quarterly',
'annual',
'one_off',
'unknown',
],
preparedness_status: ['yes', 'no', 'partial', 'unknown', 'not_applicable'],
funding_status: [
'confirmed',
'in_process',
'not_started',
'unknown',
'not_applicable',
],
fiscal_month: Array.from({ length: 12 }, (_, index) =>
String(index + 1).padStart(2, '0'),
),
fiscal_quarter: ['Q1', 'Q2', 'Q3', 'Q4'],
irps_salary_view: [
'monthly_net_estimate',
'monthly_withholding',
'annual_assessment',
],
fiscal_component: [
'irpc',
'iva',
'ispc',
'irps',
'inss',
'withholding',
'customs_ice',
],
fiscal_income_category: [
'employment',
'rental',
'digital',
'capital_gain',
'business',
'other',
'unknown',
],
fiscal_residency_assessment: [
'self_reported_resident',
'self_reported_non_resident',
'unknown',
],
fiscal_operation: [
'domestic',
'non_resident',
'import',
'export',
'refund',
'other',
'unknown',
],
fiscal_documentation: [
'documented',
'partly_documented',
'not_documented',
'unknown',
],
fiscal_refund_state: [
'estimate_only',
'request_filed',
'recognized',
'received',
'unknown',
],
document_types: [
'identity_document',
'company_registration',
'nuit_certificate',
'company_profile',
'business_plan',
'feasibility_study',
'proof_of_funds',
'special_zone_certificate',
'licence',
'site_map',
],
area_unit: ['m2', 'ha', 'km2'],
};
/** Move the active index in a non-wrapping searchable choice list. */
export function moveSearchChoiceIndex(
activeIndex: number,
direction: -1 | 1,
optionCount: number,
): number {
if (!Number.isSafeInteger(optionCount) || optionCount <= 0) return -1;
if (!Number.isSafeInteger(activeIndex) || activeIndex < 0)
return direction > 0 ? 0 : optionCount - 1;
return Math.max(0, Math.min(optionCount - 1, activeIndex + direction));
}
export const simulationFieldGroups: Record<
string,
{
key: string;
label: string;
type?: 'number' | 'date' | 'email' | 'textarea';
required?: boolean | ((draft: SimulationDraft) => boolean);
options?: string;
multiple?: boolean;
showWhen?: (draft: SimulationDraft) => boolean;
}[]
> = {
purpose: [
{
key: 'journey_type',
label: 'O que pretende calcular ou planear? Escolha o percurso',
options: 'journey_type',
required: true,
},
{
key: 'detail_level',
label: 'Nível de detalhe',
options: 'detail_level',
required: true,
},
{
key: 'purpose',
label: 'O que pretende fazer?',
options: 'purpose',
required: true,
},
{
key: 'manifestation_type',
label: 'Tipo de manifestação',
options: 'manifestation_type',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'preferred_language',
label: 'Idioma preferido',
options: 'preferred_language',
required: true,
},
],
promoter: [
{
key: 'person_type',
label: 'Tipo de pessoa',
options: 'person_type',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'investor_origin',
label: 'Origem do investidor',
options: 'investor_origin',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'entity_stage',
label: 'Situação da entidade',
options: 'entity_stage',
required: true,
showWhen: (d) =>
d.purpose !== 'simulate_only' &&
['collective', 'consortium'].includes(String(d.person_type)),
},
{
key: 'legal_form',
label: 'Forma jurídica (conforme o registo)',
required: true,
showWhen: (d) =>
d.purpose !== 'simulate_only' && d.entity_stage === 'existing',
},
{
key: 'nuit',
label: 'NUIT',
showWhen: (d) => d.purpose !== 'simulate_only',
},
],
identity: [
{
key: 'legal_name',
label: 'Nome / denominação',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'commercial_name',
label: 'Nome comercial',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'registration_number',
label: 'Número de registo',
required: true,
showWhen: (d) =>
d.entity_stage === 'existing' && d.purpose !== 'simulate_only',
},
{
key: 'representative_name',
label: 'Nome do representante',
required: true,
showWhen: (d) =>
['collective', 'consortium'].includes(String(d.person_type)) &&
d.purpose !== 'simulate_only',
},
{
key: 'email',
label: 'Correio electrónico',
type: 'email',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'phone',
label: 'Telefone',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'address',
label: 'Endereço',
type: 'textarea',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'preferred_channel',
label: 'Canal de contacto',
options: 'preferred_channel',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
],
project: [
{
key: 'project_title',
label: 'Título do projecto',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'project_summary',
label: 'Resumo executivo',
type: 'textarea',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'project_problem',
label: 'Que problema ou necessidade pretende resolver?',
type: 'textarea',
showWhen: (d) =>
d.journey_type === 'small_business' &&
d.purpose === 'simulate_only',
},
{
key: 'target_customers',
label: 'Quem são os clientes ou beneficiários?',
type: 'textarea',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'value_proposition',
label: 'Que solução ou proposta de valor oferece?',
type: 'textarea',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'market_assumption',
label: 'Hipótese sobre mercado, concorrência ou procura',
type: 'textarea',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'market_source',
label: 'Origem e data da informação de mercado (se conhecida)',
showWhen: (d) =>
d.journey_type === 'small_business' &&
d.purpose === 'simulate_only',
},
{
key: 'project_type',
label: 'Tipo de projecto',
options: 'project_type',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'project_type_other',
label: 'Especifique o tipo de projecto',
required: true,
showWhen: (d) =>
d.purpose !== 'simulate_only' && d.project_type === 'other',
},
{
key: 'sector_id',
label: 'Sector de actividade',
options: 'sector',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'activity_id',
label: 'Actividade económica',
options: 'activity',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'products_services',
label: 'Produtos e serviços',
type: 'textarea',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'maturity_stage',
label: 'Fase de maturidade',
options: 'maturity_stage',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'expected_start_date',
label: 'Data prevista de início',
type: 'date',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'project_duration_years',
label: 'Duração prevista (anos)',
type: 'number',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'existing_project_id',
label: 'Referência do projecto existente',
showWhen: (d) => d.project_type === 'expansion',
},
],
market: [
{
key: 'customer_validation_status',
label: 'Já conversou com potenciais clientes sobre esta necessidade?',
options: 'preparedness_status',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'target_customers',
label: 'Quem são os clientes ou beneficiários? Onde estão e que necessidade têm?',
type: 'textarea',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'market_assumption',
label: 'O que sabe ou está a assumir sobre procura, preço e concorrência?',
type: 'textarea',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'market_source',
label: 'Qual é a origem e a data dessa informação, se as conhecer?',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'competitor_assumption',
label: 'Que alternativas ou concorrentes os clientes usam actualmente?',
type: 'textarea',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'sales_channel_assumption',
label: 'Como pretende chegar aos clientes e receber encomendas?',
type: 'textarea',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'marketing_approach',
label: 'Que acções de divulgação pretende experimentar?',
type: 'textarea',
showWhen: (d) =>
d.purpose === 'simulate_only' &&
['small_business', 'investor'].includes(String(d.journey_type)),
},
],
location: [
{
key: 'location_assessment_status',
label: 'Já avaliou a adequação desta localização?',
options: 'preparedness_status',
showWhen: (d) =>
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'province_id',
label: 'Província',
options: 'province',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'district_id',
label: 'Distrito',
options: 'district',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'admin_post_id',
label: 'Posto administrativo',
options: 'admin_post',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'locality_id',
label: 'Localidade',
options: 'locality',
showWhen: (d) =>
d.purpose !== 'simulate_only' ||
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'site_known',
label: 'Local conhecido?',
options: 'yes_no',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'location_rationale',
label: 'Porque considera esta localização e o que ainda precisa de confirmar?',
type: 'textarea',
showWhen: (d) =>
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'land_status',
label: 'Situação do terreno',
options: 'land_status',
showWhen: (d) =>
landStatusVisible(d) && (d.purpose !== 'simulate_only' || isInvestorProjectJourney(d)),
},
{
key: 'area_required',
label: 'Área necessária',
type: 'number',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'area_unit',
label: 'Unidade da área',
options: 'area_unit',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'latitude',
label: 'Latitude',
type: 'number',
showWhen: (d) => d.site_known === 'yes',
},
{
key: 'longitude',
label: 'Longitude',
type: 'number',
showWhen: (d) => d.site_known === 'yes',
},
{
key: 'special_zone_id',
label: 'Zona especial ZEE/ZFI',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'infrastructure_needs',
label: 'Necessidades de infra-estrutura',
options: 'infrastructure',
multiple: true,
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
],
financial: [
{
key: 'cost_estimate_status',
label: 'Já identificou os principais custos e respectiva periodicidade?',
options: 'preparedness_status',
showWhen: (d) =>
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'receivables_timing_status',
label: 'Já sabe quando as vendas serão recebidas?',
options: 'preparedness_status',
showWhen: (d) =>
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'funding_status',
label: 'Qual é o estado dos recursos de financiamento de que precisa?',
options: 'funding_status',
showWhen: (d) =>
['small_business', 'investor'].includes(String(d.journey_type)),
},
{
key: 'currency',
label: 'Moeda',
options: 'currency',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'total_investment',
label: 'Investimento total',
type: 'number',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'own_capital',
label: 'Capital próprio',
type: 'number',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'domestic_debt',
label: 'Financiamento interno',
type: 'number',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'foreign_debt',
label: 'Financiamento externo',
type: 'number',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'grants',
label: 'Subvenções',
type: 'number',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'secured_amount',
label: 'Montante mobilizado',
type: 'number',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'sought_amount',
label: 'Montante procurado',
type: 'number',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'annual_revenue',
label: 'Receitas anuais estimadas',
type: 'number',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'annual_operating_costs',
label: 'Custos operacionais anuais',
type: 'number',
showWhen: (d) =>
d.purpose !== 'simulate_only' || isInvestorProjectJourney(d),
},
{
key: 'expected_profit',
label: 'Lucro esperado',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'financing_term_months',
label: 'Prazo de financiamento (meses)',
type: 'number',
showWhen: (d) =>
Number(d.domestic_debt) + Number(d.foreign_debt) > 0,
},
{
key: 'funds_origin_countries',
label: 'Países de origem dos fundos',
options: 'country',
multiple: true,
showWhen: (d) => Number(d.foreign_debt) > 0,
},
],
impact: [
{
key: 'direct_jobs',
label: 'Empregos directos',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'indirect_jobs',
label: 'Empregos indirectos',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'national_jobs',
label: 'Empregos nacionais',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'foreign_jobs',
label: 'Empregos estrangeiros',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'exports_value',
label: 'Exportações estimadas',
type: 'number',
},
{
key: 'import_substitution_value',
label: 'Substituição de importações estimada',
type: 'number',
},
{
key: 'skills_needed',
label: 'Qualificações/competências necessárias',
type: 'textarea',
},
{
key: 'project_milestones',
label: 'Marcos, prazo previsto e estado (planeado ou realizado)',
type: 'textarea',
showWhen: (d) =>
d.journey_type === 'small_business' &&
d.purpose === 'simulate_only',
},
{
key: 'local_content_percentage',
label: 'Conteúdo local (%)',
type: 'number',
},
{
key: 'environmental_category',
label: 'Categoria ambiental',
options: 'environmental_category',
},
{
key: 'community_benefits',
label: 'Benefícios comunitários',
type: 'textarea',
},
],
tax: [
{
key: 'fiscal_currency',
label: 'Moeda da operação fiscal (sem conversão automática)',
options: 'currency',
showWhen: (d) => d.fiscal_component === 'iva' && d.fiscal_scenario_mode !== 'custom_planning',
},
{
key: 'fiscal_period',
label: 'Ano do período fiscal a simular',
options: 'fiscal_period',
required: true,
},
{
key: 'fiscal_periodicity',
label: 'Periodicidade/período de referência',
options: 'fiscal_periodicity',
required: true,
},
{
key: 'fiscal_month',
label: 'Mês do período fiscal',
options: 'fiscal_month',
required: true,
showWhen: (d) =>
d.fiscal_scenario_mode === 'approved_parameters' &&
d.fiscal_periodicity === 'monthly',
},
{
key: 'fiscal_quarter',
label: 'Trimestre do período fiscal',
options: 'fiscal_quarter',
required: true,
showWhen: (d) =>
d.fiscal_scenario_mode === 'approved_parameters' &&
d.fiscal_periodicity === 'quarterly',
},
{
key: 'fiscal_reference_date',
label: 'Data da operação fiscal',
type: 'date',
required: true,
showWhen: (d) =>
d.fiscal_scenario_mode === 'approved_parameters' &&
d.fiscal_periodicity === 'one_off',
},
{
key: 'fiscal_component',
label: 'Componente que pretende explorar',
options: 'fiscal_component',
required: true,
},
{
key: 'irps_salary_view',
label: 'Que resultado de remuneração pretende explorar?',
options: 'irps_salary_view',
required: true,
showWhen: (d) =>
d.fiscal_component === 'irps' &&
d.fiscal_income_category === 'employment',
},
{
key: 'fiscal_income_category',
label: 'Categoria de rendimento',
options: 'fiscal_income_category',
required: true,
showWhen: (d) =>
['irps', 'irpc', 'withholding'].includes(
String(d.fiscal_component),
),
},
{
key: 'fiscal_residency_assessment',
label: 'Residência fiscal indicada pelo utilizador',
options: 'fiscal_residency_assessment',
required: true,
showWhen: (d) =>
['irps', 'irpc', 'withholding', 'iva'].includes(
String(d.fiscal_component),
),
},
{
key: 'fiscal_residency_country',
label: 'País que indica como residência fiscal (opcional; não é verificação)',
options: 'country',
showWhen: (d) =>
['irps', 'irpc', 'withholding', 'iva'].includes(
String(d.fiscal_component),
),
},
{
key: 'fiscal_operation',
label: 'Tipo de operação',
options: 'fiscal_operation',
required: true,
showWhen: (d) =>
['iva', 'irpc', 'withholding', 'customs_ice'].includes(
String(d.fiscal_component),
),
},
{
key: 'fiscal_activity',
label: 'Actividade económica (descrição exploratória)',
required: true,
showWhen: (d) =>
['ispc', 'irpc'].includes(String(d.fiscal_component)),
},
{
key: 'fiscal_establishments',
label: 'Número de estabelecimentos envolvidos',
type: 'number',
required: true,
showWhen: (d) =>
['ispc', 'iva', 'irpc'].includes(String(d.fiscal_component)),
},
{
key: 'fiscal_documentation',
label: 'Estado da documentação de despesas',
options: 'fiscal_documentation',
required: true,
showWhen: (d) =>
['iva', 'irpc', 'irps'].includes(String(d.fiscal_component)),
},
{
key: 'fiscal_customs_code',
label: 'Código pautal (necessário para classificação aduaneira)',
required: true,
showWhen: (d) => d.fiscal_component === 'customs_ice',
},
{
key: 'fiscal_vat_refund_state',
label: 'Estado do crédito/pedido de reembolso de IVA',
options: 'fiscal_refund_state',
required: true,
showWhen: (d) =>
d.fiscal_component === 'iva' && d.fiscal_operation === 'refund',
},
{
key: 'fiscal_scenario_mode',
label: 'Modo de exploração fiscal',
options: 'fiscal_scenario_mode',
required: true,
},
{
key: 'base',
label: 'Base hipotética para planeamento',
type: 'number',
showWhen: (d) => d.fiscal_scenario_mode === 'custom_planning',
},
{
key: 'rate',
label: 'Taxa hipotética introduzida por si (%)',
type: 'number',
showWhen: (d) => d.fiscal_scenario_mode === 'custom_planning',
},
{
key: 'reduction',
label: 'Redução hipotética introduzida por si (%)',
type: 'number',
showWhen: (d) => d.fiscal_scenario_mode === 'custom_planning',
},
{
key: 'sales_taxable',
label: 'Vendas tributadas',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'sales_exempt',
label: 'Vendas isentas',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'sales_zero_rate',
label: 'Vendas à taxa zero',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'exports',
label: 'Exportações',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'deductible_costs',
label: 'Custos dedutíveis',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'non_deductible_costs',
label: 'Custos não dedutíveis',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'carried_losses',
label: 'Prejuízos transitados',
type: 'number',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'payroll',
label: 'Dados de remunerações (estrutura prevista)',
type: 'textarea',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'outbound_payments',
label: 'Pagamentos ao exterior (estrutura prevista)',
type: 'textarea',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'imports',
label: 'Importações (estrutura prevista)',
type: 'textarea',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'claimed_benefits',
label: 'Benefícios alegados para validação',
type: 'textarea',
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'reference_date',
label: 'Data de referência',
type: 'date',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
],
opportunity: [
{
key: 'offer_or_demand',
label: 'Oferta ou procura',
options: 'offer_or_demand',
showWhen: (d) =>
d.manifestation_type === 'business' ||
(d.journey_type === 'small_business' &&
d.purpose === 'simulate_only'),
},
{
key: 'opportunity_type',
label: 'Necessidade de negócio',
options: 'opportunity_type',
showWhen: (d) =>
d.manifestation_type === 'business' ||
(d.journey_type === 'small_business' &&
d.purpose === 'simulate_only'),
},
{
key: 'opportunity_type_other',
label: 'Especifique a necessidade',
required: (d) => d.purpose !== 'simulate_only',
showWhen: (d) =>
(d.manifestation_type === 'business' ||
(d.journey_type === 'small_business' &&
d.purpose === 'simulate_only')) &&
d.opportunity_type === 'other',
},
{
key: 'opportunity_description',
label: 'Descrição da necessidade',
type: 'textarea',
showWhen: (d) =>
d.manifestation_type === 'business' ||
(d.journey_type === 'small_business' &&
d.purpose === 'simulate_only'),
},
{
key: 'opportunity_value',
label: 'Valor da oportunidade',
type: 'number',
showWhen: (d) =>
d.manifestation_type === 'business' ||
(d.journey_type === 'small_business' &&
d.purpose === 'simulate_only'),
},
{
key: 'opportunity_currency',
label: 'Moeda da oportunidade',
options: 'currency',
showWhen: (d) =>
(d.manifestation_type === 'business' ||
(d.journey_type === 'small_business' &&
d.purpose === 'simulate_only')) &&
Boolean(d.opportunity_value),
},
{
key: 'opportunity_conditions',
label: 'Condições da oportunidade',
type: 'textarea',
showWhen: (d) => d.manifestation_type === 'business',
},
{
key: 'opportunity_deadline',
label: 'Prazo da oportunidade',
type: 'date',
showWhen: (d) => d.manifestation_type === 'business',
},
{
key: 'confidentiality',
label: 'Confidencialidade',
options: 'confidentiality',
showWhen: (d) => d.manifestation_type === 'business',
},
{
key: 'publish_requested',
label: 'Pretende pedir publicação?',
options: 'preferred_publication',
showWhen: (d) => d.manifestation_type === 'business',
},
],
documents: [
{
key: 'required_documents',
label: 'Documentos aplicáveis a preparar',
options: 'document_types',
multiple: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
],
procedures: [
{
key: 'required_documents',
label: 'Documentos de suporte que já possui ou pretende preparar',
options: 'document_types',
multiple: true,
showWhen: (d) =>
d.journey_type === 'investor' && d.purpose === 'simulate_only',
},
],
review: [
{
key: 'terms_accepted',
label: 'Aceito os termos de utilização',
options: 'yes_no',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'truth_declaration',
label: 'Declaro que as informações são verdadeiras',
options: 'yes_no',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'reuse_simulation',
label: 'Autorizo reutilizar os dados da simulação',
options: 'yes_no',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'contact_authorisation',
label: 'Autorizo contacto para acompanhamento',
options: 'yes_no',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'publication_authorisation',
label: 'Autorizo a publicação solicitada',
options: 'yes_no',
required: true,
showWhen: (d) => d.publish_requested === 'yes',
},
{
key: 'public_fields_confirmation',
label: 'Confirmo os campos propostos para publicação',
options: 'yes_no',
required: true,
showWhen: (d) => d.publish_requested === 'yes',
},
{
key: 'validation_acknowledged',
label: 'Confirmo que li os avisos de validação',
options: 'yes_no',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
{
key: 'final_declaration_name',
label: 'Nome para a declaração final',
required: true,
showWhen: (d) => d.purpose !== 'simulate_only',
},
],
};
simulationOptions.yes_no = ['yes', 'no'];
simulationOptions.infrastructure = [
'energia',
'água',
'estrada',
'porto',
'ferrovia',
'telecomunicações',
'armazenamento',
'customers',
'raw_materials',
'workers',
'facilities',
'costs',
'legal_requirements',
];
export function replaceSimulationValue(
draft: SimulationDraft,
key: string,
value: string | string[],
): SimulationDraft {
const next = { ...draft, [key]: value };
if (
key === 'journey_type' &&
['small_business', 'investor'].includes(String(value))
) {
next.purpose = 'simulate_only';
}
const dependencies: Record<string, string[]> = {
province_id: ['district_id', 'admin_post_id', 'locality_id'],
district_id: ['admin_post_id', 'locality_id'],
admin_post_id: ['locality_id'],
sector_id: ['activity_id'],
site_known: ['latitude', 'longitude'],
project_type: ['project_type_other'],
opportunity_type: ['opportunity_type_other'],
foreign_debt: ['funds_origin_countries'],
fiscal_scenario_mode: ['base', 'rate', 'reduction'],
fiscal_component: [
'fiscal_income_category',
'irps_salary_view',
'fiscal_residency_assessment',
'fiscal_residency_country',
'fiscal_operation',
'fiscal_activity',
'fiscal_establishments',
'fiscal_documentation',
'fiscal_customs_code',
'fiscal_vat_refund_state',
],
fiscal_operation: ['fiscal_vat_refund_state'],
fiscal_income_category: ['irps_salary_view'],
};
const clearChildren =
key === 'foreign_debt' &&
Number(value) > 0 &&
Number(draft[key] ?? 0) > 0
? []
: (dependencies[key] ?? []);
for (const child of clearChildren) {
delete next[child];
delete next[`${child}_label`];
}
if (key === 'manifestation_type' && value !== 'business') {
for (const field of simulationFieldGroups.opportunity) {
delete next[field.key];
delete next[`${field.key}_label`];
}
}
return next;
}
const privateDraftKeys = new Set([
'person_type',
'investor_origin',
'tax_residency_country',
'fiscal_residency_country',
'entity_stage',
'legal_form',
'nuit',
'investor_profile_id',
'profile_confirmation',
'legal_name',
'commercial_name',
'registration_number',
'representative_name',
'representative_capacity',
'email',
'phone',
'address',
'contact_country',
'contact_province',
'contact_district',
'preferred_channel',
]);
export function getAutosaveSnapshot(draft: SimulationDraft): SimulationDraft {
return Object.fromEntries(
Object.entries(draft).filter(([key]) => {
const field = key.replace(/_label$/, '');
return !privateDraftKeys.has(field);
}),
);
}
export function partitionSimulationDraft(
draft: SimulationDraft,
includePersonal = true,
): {
personal_snapshot: Record<string, unknown>;
economic_snapshot: Record<string, unknown>;
} {
const personal_snapshot: Record<string, unknown> = {};
const economic_snapshot: Record<string, unknown> = {};
const personal_labels: Record<string, string | string[]> = {};
const economic_labels: Record<string, string | string[]> = {};
for (const [key, value] of Object.entries(draft)) {
if (
!includePersonal &&
privateDraftKeys.has(key.replace(/_label$/, ''))
)
continue;
if (key.endsWith('_label')) {
const target = privateDraftKeys.has(key.slice(0, -6))
? personal_labels
: economic_labels;
target[key.slice(0, -6)] = value;
continue;
}
(privateDraftKeys.has(key) ? personal_snapshot : economic_snapshot)[
key
] = value;
}
if (Object.keys(personal_labels).length)
personal_snapshot.selection_labels = personal_labels;
if (Object.keys(economic_labels).length)
economic_snapshot.selection_labels = economic_labels;
return { personal_snapshot, economic_snapshot };
}
export function restoreSimulationDraft(
economic: Record<string, unknown>,
personal: Record<string, unknown>,
): SimulationDraft {
const draft = { ...economic, ...personal } as Record<string, unknown>;
if (draft.project_questionnaire_version === undefined) {
draft.project_questionnaire_version = 'legacy-unversioned';
}
const labels = {
...(economic.selection_labels as Record<string, unknown> | undefined),
...(personal.selection_labels as Record<string, unknown> | undefined),
};
delete draft.selection_labels;
delete draft.personal_snapshot;
delete draft.simulation_sources;
if (labels && typeof labels === 'object') {
for (const [key, value] of Object.entries(labels)) {
if (
typeof value === 'string' ||
(Array.isArray(value) &&
value.every((item) => typeof item === 'string'))
) {
draft[`${key}_label`] = Array.isArray(draft[key])
? value
: Array.isArray(value)
? value[0]
: value;
}
}
}
return inheritSimulatorFields(draft as SimulationDraft);
}
export function simulationDraftFingerprint(draft: SimulationDraft): string {
const sort = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(sort);
if (value && typeof value === 'object')
return Object.fromEntries(
Object.entries(value)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, entry]) => [key, sort(entry)]),
);
return value;
};
return JSON.stringify(sort(partitionSimulationDraft(draft)));
}
export function validateSimulationDraft(
draft: SimulationDraft,
sectionId: string,
): Record<string, string> {
// The separate arithmetic validator checks its three inputs. Fiscal
// eligibility facts are not inputs of this planning-only calculation.
if (sectionId === 'tax' && draft.fiscal_scenario_mode === 'custom_planning')
return {};
const errors: Record<string, string> = {};
for (const field of simulationFieldGroups[sectionId] ?? []) {
if (field.showWhen && !field.showWhen(draft)) continue;
if (
sectionId === 'tax' &&
draft.fiscal_scenario_mode === 'custom_planning' &&
['base', 'rate', 'reduction'].includes(field.key)
)
continue;
const value = draft[field.key];
const empty =
value === undefined ||
value === '' ||
(Array.isArray(value) && value.length === 0);
const required =
typeof field.required === 'function'
? field.required(draft)
: Boolean(field.required);
if (
required &&
empty &&
!(sectionId === 'tax' && isInvestorProjectJourney(draft))
) {
errors[field.key] = 'Este campo é obrigatório.';
}
if (field.type === 'number' && value !== undefined && value !== '') {
const number = Number(value);
if (!Number.isFinite(number) || number < 0)
errors[field.key] =
'Introduza um número igual ou superior a zero.';
}
if (
['rate', 'reduction', 'local_content_percentage'].includes(
field.key,
) &&
value !== undefined &&
value !== ''
) {
if (Number(value) > 100)
errors[field.key] = 'O valor não pode exceder 100.';
}
if (
field.key === 'email' &&
typeof value === 'string' &&
value &&
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
)
errors[field.key] =
'Introduza um endereço de correio electrónico válido.';
}
if (
sectionId === 'tax' &&
draft.fiscal_scenario_mode === 'approved_parameters' &&
draft.fiscal_periodicity === 'one_off' &&
!getFiscalPeriodRange(draft)
) {
errors.fiscal_reference_date =
'Introduza uma data válida da operação no ano fiscal seleccionado. Não foi usada uma data alternativa.';
}
if (
sectionId === 'financial' &&
[
'own_capital',
'domestic_debt',
'foreign_debt',
'grants',
'total_investment',
].every((key) => draft[key] !== undefined && draft[key] !== '')
) {
const total = [
'own_capital',
'domestic_debt',
'foreign_debt',
'grants',
].reduce((sum, key) => sum + Number(draft[key]), 0);
if (Math.abs(total - Number(draft.total_investment)) > 0.01)
errors.total_investment =
'A soma do capital, dívida e subvenções deve corresponder ao investimento total.';
}
return errors;
}
export function validateCustomFiscalPlanning(
draft: SimulationDraft,
): Record<string, string> {
if (draft.fiscal_scenario_mode !== 'custom_planning') return {};
const errors: Record<string, string> = {};
for (const key of ['base', 'rate', 'reduction']) {
const value = draft[key];
if (
value === undefined ||
value === '' ||
(typeof value === 'string' && value.trim() === '')
) {
errors[key] =
'Este pressuposto é necessário para executar este cenário.';
} else if (!Number.isFinite(Number(value)) || Number(value) < 0) {
errors[key] = 'Introduza um número igual ou superior a zero.';
} else if (['rate', 'reduction'].includes(key) && Number(value) > 100) {
errors[key] = 'O valor não pode exceder 100.';
}
}
return errors;
}
export type ArithmeticSnapshotRead = {
integrity: 'versioned' | 'legacy' | 'invalid';
value: string | null;
context: Record<string, unknown> | null;
explanation: string | null;
};
/** Reads an internally consistent arithmetic snapshot without inferring tax values. */
export function readArithmeticSnapshot(
snapshot: unknown,
): ArithmeticSnapshotRead {
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
return {
integrity: 'invalid',
value: null,
context: null,
explanation: null,
};
}
const result = snapshot as Record<string, unknown>;
const value = result.indicative_total;
if (['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(result.scenario_kind))) {
const presentation = result.presentation as Record<string, unknown> | undefined;
if (isApiSimulationResultSnapshotV1(result) && fiscalQaRows(result).length > 0 &&
result.result.value === value && presentation?.value === value &&
presentation?.rounding_applied === false && result.context.currency === 'MZN' &&
result.result.unit === 'MZN' &&
((result.result.status === 'calculated' && typeof value === 'string' && /^\d+(?:\.\d+)?$/.test(value)) ||
(result.result.status === 'indeterminate' && value === null && result.result.missing_inputs.length > 0))) {
return { integrity: 'versioned', value: value as string | null,
context: result.context as unknown as Record<string, unknown>, explanation: result.result.explanation };
}
return { integrity: 'invalid', value: null, context: null, explanation: null };
}
if (typeof value !== 'string' || !/^(?:\d+)(?:\.\d+)?$/.test(value)) {
return {
integrity: 'invalid',
value: null,
context: null,
explanation: null,
};
}
if (result.contract_version === undefined) {
return { integrity: 'legacy', value, context: null, explanation: null };
}
if (
result.contract_version !== 1 ||
!isApiSimulationResultSnapshotV1(result)
) {
return {
integrity: 'invalid',
value: null,
context: null,
explanation: null,
};
}
const context = result.context;
const output = result.result;
if (
!context ||
typeof context !== 'object' ||
Array.isArray(context) ||
!output ||
typeof output !== 'object' ||
Array.isArray(output)
) {
return {
integrity: 'invalid',
value: null,
context: null,
explanation: null,
};
}
const resultContext = context as Record<string, unknown>;
const resultOutput = output as Record<string, unknown>;
if (
resultOutput.status !== 'calculated' ||
resultOutput.value !== value ||
resultOutput.unit !== null ||
typeof resultOutput.explanation !== 'string' ||
!resultOutput.explanation.trim() ||
!Array.isArray(resultOutput.missing_inputs) ||
resultOutput.missing_inputs.length !== 0 ||
!Array.isArray(resultOutput.source_ids) ||
resultOutput.source_ids.length !== 0 ||
typeof resultContext.reference_date !== 'string' ||
!/^\d{4}-\d{2}-\d{2}$/.test(resultContext.reference_date) ||
typeof resultContext.scenario_id !== 'string' ||
!Number.isSafeInteger(resultContext.scenario_version) ||
Number(resultContext.scenario_version) < 1 ||
typeof resultContext.engine_version !== 'string' ||
resultContext.currency !== null ||
resultContext.currency_fraction_digits !== null ||
resultContext.ruleset_version !== null
) {
return {
integrity: 'invalid',
value: null,
context: null,
explanation: null,
};
}
return {
integrity: 'versioned',
value,
context: resultContext,
explanation: resultOutput.explanation,
};
}
function canonicalJson(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map(canonicalJson).join(',')}]`;
}
if (value && typeof value === 'object') {
const record = value as Record<string, unknown>;
return `{${Object.keys(record)
.sort()
.map(
(key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`,
)
.join(',')}}`;
}
return JSON.stringify(value) ?? 'null';
}
export type ArithmeticReportReference = {
id: string;
version_no: number;
report_sha256: string;
created_at: string | null;
};
/** Returns a saved report reference only when it matches the current immutable snapshot. */
export function readVersionedArithmeticReportReference(
simulation: Record<string, unknown> | null | undefined,
): ArithmeticReportReference | null {
if (
simulation?.status !== 'completed' ||
!simulation.result_snapshot ||
typeof simulation.result_snapshot !== 'object' ||
Array.isArray(simulation.result_snapshot)
) {
return null;
}
const snapshot = simulation.result_snapshot as Record<string, unknown>;
if (snapshot.contract_version !== 1) return null;
const context = snapshot.context;
if (!context || typeof context !== 'object' || Array.isArray(context))
return null;
const scenarioVersion = (context as Record<string, unknown>)
.scenario_version;
if (!Number.isSafeInteger(scenarioVersion) || Number(scenarioVersion) < 1)
return null;
const reports = Array.isArray(simulation.reports)
? (simulation.reports.filter(
(report) =>
report &&
typeof report === 'object' &&
!Array.isArray(report),
) as Record<string, unknown>[])
: [];
reports.sort((left, right) => {
const leftTime =
typeof left.created_at === 'string' ? left.created_at : '';
const rightTime =
typeof right.created_at === 'string' ? right.created_at : '';
return rightTime.localeCompare(leftTime);
});
const report = reports.find(
(candidate) =>
candidate.status === 'ready' &&
candidate.version_no === scenarioVersion &&
typeof candidate.id === 'string' &&
candidate.report_snapshot &&
typeof candidate.report_snapshot === 'object' &&
!Array.isArray(candidate.report_snapshot) &&
canonicalJson(candidate.report_snapshot) ===
canonicalJson(snapshot) &&
typeof candidate.report_sha256 === 'string' &&
/^[a-f0-9]{64}$/i.test(candidate.report_sha256),
);
if (!report) return null;
return {
id: report.id as string,
version_no: scenarioVersion as number,
report_sha256: report.report_sha256 as string,
created_at:
typeof report.created_at === 'string' ? report.created_at : null,
};
}
/** Local export of the generic arithmetic result; never includes personal/profile fields. */
export function exportCompletedArithmeticCsv(
simulation: Record<string, unknown>,
exportedAt = new Date().toISOString(),
): string {
if (
simulation.status !== 'completed' ||
!simulation.result_snapshot ||
typeof simulation.result_snapshot !== 'object'
) {
throw new Error(
'Só é possível exportar uma simulação concluída com relatório disponível.',
);
}
const result = simulation.result_snapshot as Record<string, unknown>;
if (['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(result.scenario_kind))) {
const rows = fiscalQaRows(result);
const report = readVersionedArithmeticReportReference(simulation);
if (!rows.length || !report) throw new Error('A pré-visualização fiscal não corresponde ao relatório preservado.');
return `\uFEFF${[...rows, ['ID do relatório', report.id], ['Hash do relatório', report.report_sha256], ['Data de exportação', exportedAt]]
.map((field) => field.map(csvCell).join(',')).join('\r\n')}`;
}
const decimal = (value: unknown, label: string) => {
if (typeof value !== 'string' || !/^(?:\d+)(?:\.\d+)?$/.test(value))
throw new Error(
`O campo ${label} não está disponível num formato seguro.`,
);
return value;
};
if (
result.contract_version !== undefined &&
result.contract_version !== 1
) {
throw new Error('A versão do contrato do resultado não é suportada.');
}
let versionedFields: Array<[string, unknown]> = [];
if (result.contract_version === 1) {
const context = result.context;
const output = result.result;
if (
!context ||
typeof context !== 'object' ||
!output ||
typeof output !== 'object'
) {
throw new Error(
'O snapshot versionado do resultado está incompleto.',
);
}
const resultContext = context as Record<string, unknown>;
const resultOutput = output as Record<string, unknown>;
const exactValue = decimal(resultOutput.value, 'resultado versionado');
if (
resultOutput.status !== 'calculated' ||
exactValue !== decimal(result.indicative_total, 'resultado') ||
result.legal_interpretation !== false ||
typeof resultContext.reference_date !== 'string' ||
!/^\d{4}-\d{2}-\d{2}$/.test(resultContext.reference_date) ||
typeof resultContext.scenario_id !== 'string' ||
!Number.isSafeInteger(resultContext.scenario_version) ||
Number(resultContext.scenario_version) < 1 ||
typeof resultContext.engine_version !== 'string' ||
resultContext.currency !== null ||
resultContext.ruleset_version !== null
) {
throw new Error(
'O envelope do resultado não reconcilia com o snapshot aritmético.',
);
}
versionedFields = [
['Versão do contrato', '1'],
['Data de referência', resultContext.reference_date],
['ID do cenário', resultContext.scenario_id],
['Versão do motor', resultContext.engine_version],
['Estado do resultado', resultOutput.status],
['Explicação do resultado', resultOutput.explanation],
];
}
const reports = Array.isArray(simulation.reports)
? (simulation.reports.filter(
(report) =>
report &&
typeof report === 'object' &&
!Array.isArray(report),
) as Record<string, unknown>[])
: [];
const readyReports = [...reports]
.filter((report) => report.status === 'ready')
.sort((left, right) => {
const leftTime =
typeof left.created_at === 'string' ? left.created_at : '';
const rightTime =
typeof right.created_at === 'string' ? right.created_at : '';
return rightTime.localeCompare(leftTime);
});
let latestReport = readyReports[0];
if (result.contract_version === 1) {
const reference = readVersionedArithmeticReportReference(simulation);
if (!reference) {
throw new Error(
'Relatorio imutavel nao corresponde a versao atual do resultado.',
);
}
latestReport = reference;
}
const version = Number.isSafeInteger(simulation.version_no)
? String(simulation.version_no)
: 'não identificada';
const fields: Array<[string, unknown]> = [
['Relatório', 'Simulação aritmética indicativa'],
...versionedFields,
['Data de exportação', exportedAt],
[
'Data do resultado',
typeof simulation.updated_at === 'string'
? simulation.updated_at
: 'não disponível',
],
['Período', 'cenário aritmético pontual; sem período fiscal atribuído'],
[
'ID da execução',
typeof simulation.id === 'string'
? simulation.id
: 'não disponível',
],
['Versão da execução', version],
[
'ID do relatório',
typeof latestReport?.id === 'string'
? latestReport.id
: 'não disponível',
],
[
'Versão do relatório/cenário',
Number.isSafeInteger(latestReport?.version_no)
? String(latestReport?.version_no)
: 'não identificada',
],
[
'Hash SHA-256 do relatório',
typeof latestReport?.report_sha256 === 'string' &&
/^[a-f0-9]{64}$/i.test(latestReport.report_sha256)
? latestReport.report_sha256
: 'não disponível',
],
[
'Fórmula',
typeof result.formula_version === 'string'
? result.formula_version
: 'não identificada',
],
['Versão de regras', 'não aplicável; motor aritmético genérico'],
[
'Expressão aplicada',
'base × taxa_percentual ÷ 100 × (1 − redução_percentual ÷ 100)',
],
['Base introduzida', decimal(result.base, 'base')],
['Taxa percentual introduzida', decimal(result.rate_percent, 'taxa')],
[
'Redução hipotética percentual',
decimal(result.reduction_percent, 'redução'),
],
[
'Resultado aritmético indicativo',
decimal(result.indicative_total, 'resultado'),
],
[
'Moeda',
typeof result.currency === 'string'
? result.currency
: 'não fornecida',
],
[
'Fontes legais',
'nenhuma associada; parâmetros fornecidos pelo utilizador',
],
['IDs de fontes', 'nenhum'],
[
'Arredondamento fiscal',
'não aplicado; sem regra de arredondamento fiscal',
],
[
'Interpretação legal',
result.legal_interpretation === false ? 'não' : 'não confirmada',
],
[
'Limitações',
'Aritmética indicativa; sem validação fiscal, fundamento legal, arredondamento fiscal ou aconselhamento.',
],
];
return `\uFEFF${fields.map((field) => field.map(csvCell).join(',')).join('\r\n')}`;
}