/** Shared vocabulary for simulator values. Decimal amounts stay strings at API boundaries. */ export type DecimalString = string; export type ResultStatus = | 'calculated' | 'estimated' | 'indeterminate' | 'not_applicable'; export const fiscalComponentCodes = [ 'IRPS', 'INSS', 'IVA', 'IRPC', 'ISPC', 'RETENCOES', ] as const; export type FiscalComponentCode = (typeof fiscalComponentCodes)[number]; export type SimulationContext = { contractVersion: 1; referenceDate: string; currency: string | null; currencyFractionDigits: number | null; scenarioId: string; engineVersion: string; rulesetVersion: string | null; }; export type SimulationResult = { status: ResultStatus; value: TValue | null; unit: string | null; explanation: string; missingInputs: string[]; sourceIds: string[]; }; /** Exact JSON envelope returned by the v1 API (wire keys stay snake_case). */ export type ApiSimulationResultV1 = { status: ResultStatus; value: DecimalString | null; unit: string | null; explanation: string; missing_inputs: string[]; source_ids: string[]; }; export type ApiSimulationResultContextV1 = { reference_date: string; currency: string | null; currency_fraction_digits: number | null; scenario_id: string; scenario_version: number; engine_version: string; ruleset_version: string | null; }; export type ApiSimulationResultSnapshotV1 = { contract_version: 1; context: ApiSimulationResultContextV1; result: ApiSimulationResultV1; indicative_total: DecimalString | null; legal_interpretation: false; formula_version: string; }; const apiDecimalPattern = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/; const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** Runtime guard for untrusted API JSON; zero remains a valid decimal value. */ export function isApiSimulationResultSnapshotV1( value: unknown, ): value is ApiSimulationResultSnapshotV1 { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; const snapshot = value as Record; const context = snapshot.context; const result = snapshot.result; if ( snapshot.contract_version !== 1 || snapshot.legal_interpretation !== false || typeof snapshot.formula_version !== 'string' || !snapshot.formula_version.trim() || !( snapshot.indicative_total === null || (typeof snapshot.indicative_total === 'string' && apiDecimalPattern.test(snapshot.indicative_total)) ) || !context || typeof context !== 'object' || Array.isArray(context) || !result || typeof result !== 'object' || Array.isArray(result) ) return false; const meta = context as Record; const output = result as Record; const currencyValid = meta.currency === null || (typeof meta.currency === 'string' && /^[A-Z]{3}$/.test(meta.currency)); const currencyDigitsValid = meta.currency_fraction_digits === null || (Number.isInteger(meta.currency_fraction_digits) && Number(meta.currency_fraction_digits) >= 0 && Number(meta.currency_fraction_digits) <= 6); if ( !isIsoDate(meta.reference_date) || !uuidPattern.test(String(meta.scenario_id)) || !Number.isSafeInteger(meta.scenario_version) || Number(meta.scenario_version) < 1 || typeof meta.engine_version !== 'string' || !meta.engine_version.trim() || !( meta.ruleset_version === null || typeof meta.ruleset_version === 'string' ) || !currencyValid || !currencyDigitsValid || (meta.currency === null) !== (meta.currency_fraction_digits === null) || ![ 'calculated', 'estimated', 'indeterminate', 'not_applicable', ].includes(String(output.status)) || !( output.value === null || (typeof output.value === 'string' && apiDecimalPattern.test(output.value)) ) || output.value !== snapshot.indicative_total || !(output.unit === null || typeof output.unit === 'string') || typeof output.explanation !== 'string' || !output.explanation.trim() || !Array.isArray(output.missing_inputs) || !output.missing_inputs.every( (item) => typeof item === 'string' && item.length > 0, ) || !Array.isArray(output.source_ids) || !output.source_ids.every( (item) => typeof item === 'string' && item.length > 0, ) ) return false; if ( (output.status === 'calculated' || output.status === 'estimated') && output.value === null ) return false; if ( output.status === 'indeterminate' && (output.value !== null || output.missing_inputs.length === 0) ) return false; if (output.status === 'not_applicable' && output.value !== null) return false; return true; } export const apiSimulationExecutionStatuses = [ 'queued', 'running', 'completed', 'failed', 'cancelled', ] as const; export type ApiSimulationExecutionStatus = (typeof apiSimulationExecutionStatuses)[number]; /** Minimum execution fields consumed by the public form and result reader. */ export type ApiSimulationExecutionV1 = { id: string; status: ApiSimulationExecutionStatus; version_no: number; result_snapshot?: Record | null; [key: string]: unknown; }; export function isApiSimulationExecutionV1( value: unknown, ): value is ApiSimulationExecutionV1 { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; const execution = value as Record; const hasSnapshot = Object.prototype.hasOwnProperty.call( execution, 'result_snapshot', ); const snapshot = execution.result_snapshot; const snapshotValid = snapshot === undefined || snapshot === null ? execution.status !== 'completed' : !!snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot) && (isApiSimulationResultSnapshotV1(snapshot) || (typeof (snapshot as Record) .indicative_total === 'string' && /^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test( (snapshot as Record).indicative_total, ) && !Object.prototype.hasOwnProperty.call( snapshot, 'contract_version', ))); return ( typeof execution.id === 'string' && uuidPattern.test(execution.id) && apiSimulationExecutionStatuses.includes( execution.status as ApiSimulationExecutionStatus, ) && Number.isSafeInteger(execution.version_no) && Number(execution.version_no) > 0 && snapshotValid && (execution.status !== 'completed' || hasSnapshot) ); } export type ApiSimulationCreateResponseV1 = { data: ApiSimulationExecutionV1; replayed: boolean; access_token?: string; }; export function isApiSimulationCreateResponseV1( value: unknown, ): value is ApiSimulationCreateResponseV1 { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; const response = value as Record; return ( isApiSimulationExecutionV1(response.data) && typeof response.replayed === 'boolean' && (response.access_token === undefined || (typeof response.access_token === 'string' && response.access_token.length >= 32 && response.access_token.length <= 256)) ); } export function isApiSimulationReadResponseV1( value: unknown, ): value is { data: ApiSimulationExecutionV1 } { return ( !!value && typeof value === 'object' && !Array.isArray(value) && isApiSimulationExecutionV1((value as Record).data) ); } const isPlainObject = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); const isIsoDateTime = (value: unknown): value is string => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test( value, ) && Number.isFinite(Date.parse(value)); const isIsoDate = (value: unknown): value is string => typeof value === 'string' && (() => { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); if (!match) return false; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); if (year < 1 || month < 1 || month > 12 || day < 1) return false; const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); const daysInMonth = [ 31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, ]; return day <= daysInMonth[month - 1]; })(); export function isApiErrorV1(value: unknown): value is { message?: string; errors?: Record; [key: string]: unknown; } { if (!isPlainObject(value)) return false; if (value.message !== undefined && typeof value.message !== 'string') return false; if (value.errors !== undefined) { if (!isPlainObject(value.errors)) return false; if ( !Object.values(value.errors).every( (messages) => Array.isArray(messages) && messages.every((message) => typeof message === 'string'), ) ) return false; } return typeof value.message === 'string' || value.errors !== undefined; } export function isApiQuestionnaireReadResponseV1(value: unknown): value is { data: { id: string; code: string; version_no: number; locale: 'pt-MZ'; schema_snapshot: Record; status: 'published'; valid_from: string | null; valid_to: string | null; }; } { if (!isPlainObject(value) || !isPlainObject(value.data)) return false; const data = value.data; return ( typeof data.id === 'string' && uuidPattern.test(data.id) && typeof data.code === 'string' && /^[a-z0-9][a-z0-9-]{0,79}$/.test(data.code) && Number.isSafeInteger(data.version_no) && Number(data.version_no) > 0 && data.locale === 'pt-MZ' && isPlainObject(data.schema_snapshot) && data.status === 'published' && (data.valid_from === null || isIsoDate(data.valid_from)) && (data.valid_to === null || isIsoDate(data.valid_to)) ); } export function isApiSimulationReportReadResponseV1(value: unknown): value is { data: { id: string; simulation_id: string; version_no: number; status: 'queued' | 'ready' | 'failed'; report_snapshot: Record | null; report_sha256: string | null; }; } { if (!isPlainObject(value) || !isPlainObject(value.data)) return false; const data = value.data; const shapeValid = typeof data.id === 'string' && uuidPattern.test(data.id) && typeof data.simulation_id === 'string' && uuidPattern.test(data.simulation_id) && Number.isSafeInteger(data.version_no) && Number(data.version_no) > 0 && (data.status === 'queued' || data.status === 'ready' || data.status === 'failed') && (data.report_snapshot === null || isPlainObject(data.report_snapshot)) && (data.report_sha256 === null || (typeof data.report_sha256 === 'string' && /^[a-f0-9]{64}$/i.test(data.report_sha256))); if (!shapeValid) return false; if (data.status === 'ready') { return ( isPlainObject(data.report_snapshot) && typeof data.report_sha256 === 'string' && /^[a-f0-9]{64}$/i.test(data.report_sha256) ); } return data.report_snapshot === null && data.report_sha256 === null; } export type ApiSimulationJobV1 = { id: string; simulation_id: string; job_type: 'execute' | 'report'; status: 'pending' | 'running' | 'succeeded' | 'failed'; attempts: number; available_at: string; started_at: string | null; finished_at: string | null; error_code: string | null; }; export function isApiSimulationJobsReadResponseV1(value: unknown): value is { data: ApiSimulationJobV1[]; } { if (!isPlainObject(value) || !Array.isArray(value.data)) return false; return value.data.every((job: unknown) => { if (!isPlainObject(job)) return false; return ( typeof job.id === 'string' && uuidPattern.test(job.id) && typeof job.simulation_id === 'string' && uuidPattern.test(job.simulation_id) && (job.job_type === 'execute' || job.job_type === 'report') && (job.status === 'pending' || job.status === 'running' || job.status === 'succeeded' || job.status === 'failed') && Number.isSafeInteger(job.attempts) && Number(job.attempts) >= 0 && Number(job.attempts) <= 3 && isIsoDateTime(job.available_at) && (job.started_at === null || isIsoDateTime(job.started_at)) && (job.finished_at === null || isIsoDateTime(job.finished_at)) && (job.error_code === null || typeof job.error_code === 'string') ); }); } export type ApiSimulationHistoryEntryV1 = { id: string; scope: 'personal' | 'organization'; organization_name: string | null; status: ApiSimulationExecutionStatus; version_no: number; created_at: string | null; updated_at: string | null; has_ready_report: boolean; }; export type ApiSimulationHistoryResponseV1 = { data: ApiSimulationHistoryEntryV1[]; meta: { current_page: number; last_page: number; per_page: number; total: number; }; }; export function isApiSimulationHistoryResponseV1( value: unknown, ): value is ApiSimulationHistoryResponseV1 { if ( !isPlainObject(value) || !Array.isArray(value.data) || !isPlainObject(value.meta) ) return false; const meta = value.meta; if ( ![meta.current_page, meta.last_page, meta.per_page, meta.total].every( (item) => Number.isSafeInteger(item) && Number(item) >= 0, ) || Number(meta.current_page) < 1 || Number(meta.last_page) < 1 || Number(meta.per_page) < 1 ) return false; return value.data.every((entry: unknown) => { if (!isPlainObject(entry)) return false; return ( typeof entry.id === 'string' && uuidPattern.test(entry.id) && (entry.scope === 'personal' || entry.scope === 'organization') && (entry.organization_name === null || typeof entry.organization_name === 'string') && apiSimulationExecutionStatuses.includes( entry.status as ApiSimulationExecutionStatus, ) && Number.isSafeInteger(entry.version_no) && Number(entry.version_no) > 0 && (entry.created_at === null || isIsoDateTime(entry.created_at)) && (entry.updated_at === null || isIsoDateTime(entry.updated_at)) && typeof entry.has_ready_report === 'boolean' ); }); } export type ApiSimulationDraftV1 = { id: string; version_no: number; snapshot_sha256: string; personal_snapshot: Record | []; economic_snapshot: Record; simulation_sources: ApiSimulationSourceV1[]; updated_at: string | null; expires_at?: string | null; }; export type ApiSimulationDraftSummaryV1 = { id: string; title: string; updated_at: string | null; expires_at: string | null; version_no: number; }; export type ApiSimulationSourceV1 = { simulation_id: string; source_version_no: number; result_snapshot: Record; report_sha256: string; consented_at: string; }; function isApiSimulationSourceV1( value: unknown, ): value is ApiSimulationSourceV1 { if (!isPlainObject(value)) return false; return ( typeof value.simulation_id === 'string' && uuidPattern.test(value.simulation_id) && Number.isSafeInteger(value.source_version_no) && Number(value.source_version_no) > 0 && isPlainObject(value.result_snapshot) && typeof value.report_sha256 === 'string' && /^[a-f0-9]{64}$/i.test(value.report_sha256) && isIsoDateTime(value.consented_at) ); } function isApiSimulationDraftV1(value: unknown): value is ApiSimulationDraftV1 { if (!isPlainObject(value)) return false; return ( typeof value.id === 'string' && uuidPattern.test(value.id) && Number.isSafeInteger(value.version_no) && Number(value.version_no) > 0 && typeof value.snapshot_sha256 === 'string' && /^[a-f0-9]{64}$/i.test(value.snapshot_sha256) && (isPlainObject(value.personal_snapshot) || (Array.isArray(value.personal_snapshot) && value.personal_snapshot.length === 0)) && isPlainObject(value.economic_snapshot) && Array.isArray(value.simulation_sources) && value.simulation_sources.every(isApiSimulationSourceV1) && (value.updated_at === null || isIsoDateTime(value.updated_at)) && (value.expires_at === undefined || value.expires_at === null || isIsoDateTime(value.expires_at)) ); } function isApiSimulationDraftSummaryV1( value: unknown, ): value is ApiSimulationDraftSummaryV1 { return ( isPlainObject(value) && typeof value.id === 'string' && uuidPattern.test(value.id) && typeof value.title === 'string' && (value.updated_at === null || isIsoDateTime(value.updated_at)) && (value.expires_at === null || isIsoDateTime(value.expires_at)) && Number.isSafeInteger(value.version_no) && Number(value.version_no) > 0 ); } export function isApiSimulationDraftListResponseV1( value: unknown, ): value is { data: ApiSimulationDraftSummaryV1[] } { return ( isPlainObject(value) && Array.isArray(value.data) && value.data.every(isApiSimulationDraftSummaryV1) ); } export function isApiSimulationDraftClaimPreviewV1( value: unknown, ): value is { data: ApiSimulationDraftSummaryV1 | null } { return ( isPlainObject(value) && (value.data === null || isApiSimulationDraftSummaryV1(value.data)) ); } export function isApiSimulationDraftReadResponseV1( value: unknown, ): value is { data: ApiSimulationDraftV1 | null } { if (!isPlainObject(value)) return false; return value.data === null || isApiSimulationDraftV1(value.data); } export function isApiSimulationDraftWriteResponseV1(value: unknown): value is { data: { id: string; version_no: number; replayed?: boolean }; expires_at?: string | null; } { if (!isPlainObject(value) || !isPlainObject(value.data)) return false; const data = value.data; return ( typeof data.id === 'string' && uuidPattern.test(data.id) && Number.isSafeInteger(data.version_no) && Number(data.version_no) > 0 && (value.expires_at === undefined || value.expires_at === null || isIsoDateTime(value.expires_at)) && (data.replayed === undefined || typeof data.replayed === 'boolean') ); } export function isApiSimulationDraftAttachResponseV1( value: unknown, ): value is { data: ApiSimulationDraftV1; replayed: boolean } { return ( isPlainObject(value) && isApiSimulationDraftV1(value.data) && typeof value.replayed === 'boolean' ); } export function indeterminateFiscalComponent( component: FiscalComponentCode, referenceDate: string, ): SimulationResult & { component: FiscalComponentCode; referenceDate: string; } { return { component, referenceDate, status: 'indeterminate', value: null, unit: null, explanation: `A componente ${component} não tem regra oficial aprovada e vigente associada neste simulador.`, missingInputs: [ 'approved_rule', 'effective_parameters', 'applicable_classification', ], sourceIds: [], }; } export type SimulationSnapshot = { personal_snapshot: Record; economic_snapshot: Record; context: SimulationContext; }; const intl = Intl as unknown as { supportedValuesOf?: (key: 'currency') => string[]; }; export function simulationCurrencyFractionDigits(value: string): number | null { try { const digits = new Intl.NumberFormat('en', { style: 'currency', currency: value, }).resolvedOptions().maximumFractionDigits; return digits !== undefined && Number.isInteger(digits) ? digits : null; } catch { return null; } } export const simulationCurrencyOptions = [ ...new Set([ 'MZN', 'USD', 'EUR', 'ZAR', ...(intl.supportedValuesOf?.('currency') ?? []), ]), ] .filter((code) => simulationCurrencyFractionDigits(code) === 2) .sort(); export const isSimulationCurrency = (value: string): boolean => simulationCurrencyOptions.includes(value); /** Rejects contradictory states so missing/unknown values cannot silently become zero. */ export function validateSimulationResult( result: SimulationResult, ): string[] { const errors: string[] = []; if (!result.explanation.trim()) errors.push('explanation'); if (result.status === 'calculated' && result.value === null) errors.push('calculated_value'); if ( result.status === 'indeterminate' && (result.value !== null || result.missingInputs.length === 0) ) errors.push('indeterminate_inputs'); if (result.status === 'not_applicable' && result.value !== null) errors.push('not_applicable_value'); if (result.status === 'estimated' && result.value === null) errors.push('estimated_value'); return errors; }