2049 lines
69 KiB
TypeScript
2049 lines
69 KiB
TypeScript
import { inheritSimulatorFields } from './context-inheritance.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) =>
|
||
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')}`;
|
||
}
|