import { inputText } from './input-text.ts'; import { acquisitionScope } from './iva-acquisitions.ts'; import { decimalFraction, fractionPresentation, type FiscalFraction, } from './fiscal-rational.ts'; export const ispcEligibilitySource = 'L4-A:Lei9/2025,BR248/1.ºsuplemento,republicaçãoISPC,arts2–5,9-A/13;PDF10–11/p2180(10–11);vigência2026art6/PDF9. Enquadramento preliminar de actividade habitual;opção separada;sem liquidação trimestral,retenção,submissão ou alteração de IVA/IRPC/IRPS.'; export const ispcActivities = [ ['forestry', 'Silvícola'], ['fishery', 'Pesqueira'], ['livestock', 'Pecuária'], ['agriculture', 'Agrícola'], ['poultry', 'Avícola'], ['beekeeping', 'Apícola'], ['industry', 'Industrial'], ['commerce', 'Comercial'], ['craft', 'Artesanato'], ['services12', 'Serviços do artigo8(1)b'], ['liberal15', 'Profissão liberal do artigo8(1)c'], ['other', 'Outra actividade,fora dos âmbitos identificados'], ] as const; export type IspcActivity = (typeof ispcActivities)[number][0]; type Choice = '' | 'yes' | 'no'; export type IspcEligibilityInput = { entityRef: string; personType: '' | 'singular' | 'collective'; origin: 'projected' | 'effective'; assessmentDate: string; currency: string; mode: 'regular' | 'occasional'; activities: IspcActivity[]; activityDescription: string; territory: Choice; establishments: string; basis: '' | 'previous_year' | 'start_forecast'; referenceYear: string; annualReference: string; annualReferenceRef: string; forecastConfirmed: Choice; forecastConfirmationRef: string; organizedRequired: Choice; sharesInIspc: Choice; sharesAnonymous: Choice; otherActivities: Choice; unlistedServices: Choice; liberalDaysSameEntity: string; liberalClientRef: string; generalRegimeChosen: Choice; generalThreeYearsCompleted: Choice; generalRegimeRef: string; option: 'preparation' | 'opted' | 'not_opted'; optionReference: string; optionDate: string; }; export type IspcEligibilityResult = { calculatedAt: string; key: string; scope: string; input: IspcEligibilityInput; status: 'compatible' | 'incompatible' | 'indeterminate'; reason: string; missing: string[]; annualReference: FiscalFraction | null; optionStatus: 'preparation' | 'recorded' | 'not_opted' | 'indeterminate'; optionReason: string; source: string; }; export type IspcEligibilityBook = { version: 1; input: IspcEligibilityInput; result?: IspcEligibilityResult; history?: IspcEligibilityResult[]; }; const scope = (d: Record) => acquisitionScope(d); export function ispcReuseValues( d: Record, ): Partial { const p: Partial = {}, person = inputText(d.person_type ?? ''); if (['singular', 'sole_trader'].includes(person)) p.personType = 'singular'; else if (person === 'collective') p.personType = 'collective'; if (typeof d.fiscal_activity === 'string' && d.fiscal_activity.trim()) p.activityDescription = d.fiscal_activity; const establishments = inputText(d.fiscal_establishments ?? ''); if (/^[1-9]\d{0,4}$/.test(establishments)) p.establishments = establishments; const entity = inputText(d.fiscal_entity_ref || d.organization_name || ''); if (entity.trim()) p.entityRef = entity; return p; } export function newIspcEligibilityInput( d: Record, ): IspcEligibilityInput { return { entityRef: '', personType: '', origin: 'projected', assessmentDate: '2026-01-01', currency: d.currency === 'MZN' ? 'MZN' : '', mode: 'regular', activities: [], activityDescription: '', territory: '', establishments: '', basis: '', referenceYear: '', annualReference: '', annualReferenceRef: '', forecastConfirmed: '', forecastConfirmationRef: '', organizedRequired: '', sharesInIspc: '', sharesAnonymous: '', otherActivities: '', unlistedServices: '', liberalDaysSameEntity: '', liberalClientRef: '', generalRegimeChosen: '', generalThreeYearsCompleted: '', generalRegimeRef: '', option: 'preparation', optionReference: '', optionDate: '', ...ispcReuseValues(d), }; } function store(d: Record): Record { const s = JSON.parse( inputText(d.modelo_a_snapshot || '{"version":1,"books":{}}'), ); if ( s.version !== 1 || !s.books || typeof s.books !== 'object' || Array.isArray(s.books) ) throw Error('Livros não recuperáveis;originais preservados.'); return s; } function map(d: Record): Record { const value = store(d).ispcEligibility; if (value === undefined) return {}; if (!value || typeof value !== 'object' || Array.isArray(value)) throw Error('PreparaçãoISPC não recuperável;originais preservados.'); return value as Record; } function validate(b: IspcEligibilityBook): void { const sample = newIspcEligibilityInput({}); if ( !b || b.version !== 1 || !b.input || typeof b.input !== 'object' || Object.keys(sample).some((k) => k === 'activities' ? !Array.isArray(b.input.activities) || new Set(b.input.activities).size !== b.input.activities.length || b.input.activities.some( (a) => !ispcActivities.some(([kind]) => kind === a), ) : typeof b.input[k as keyof IspcEligibilityInput] !== 'string', ) || !['', 'singular', 'collective'].includes(b.input.personType) || !['projected', 'effective'].includes(b.input.origin) || !['regular', 'occasional'].includes(b.input.mode) || !['', 'previous_year', 'start_forecast'].includes(b.input.basis) || !['preparation', 'opted', 'not_opted'].includes(b.input.option) || [ 'territory', 'forecastConfirmed', 'organizedRequired', 'sharesInIspc', 'sharesAnonymous', 'otherActivities', 'unlistedServices', 'generalRegimeChosen', 'generalThreeYearsCompleted', ].some( (k) => !['', 'yes', 'no'].includes( String(b.input[k as keyof IspcEligibilityInput]), ), ) || (b.history !== undefined && !Array.isArray(b.history)) || (b.result !== undefined && (!b.result.input || !Array.isArray(b.result.missing) || !['compatible', 'incompatible', 'indeterminate'].includes( b.result.status, ))) ) throw Error( 'EntradasISPC não recuperáveis;sem substituir por valores vazios.', ); } export function ispcEligibilityBook( d: Record, ): IspcEligibilityBook { const b = map(d)[scope(d)] ?? { version: 1, input: newIspcEligibilityInput(d), }; validate(b); return b; } export function saveIspcEligibilityBook( d: Record, b: IspcEligibilityBook, ): string { validate(b); return JSON.stringify({ ...store(d), ispcEligibility: { ...map(d), [scope(d)]: b }, }); } const validDate = (v: string) => /^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(v) && !Number.isNaN(Date.parse(v)) && new Date(v).toISOString().slice(0, 10) === v; function option( i: IspcEligibilityInput, ): Pick { if (i.option === 'not_opted') return { optionStatus: 'not_opted', optionReason: 'Sem opçãoISPC declarada;não aplicar a exclusão de outros impostos doart5.', }; if (i.option === 'preparation') return { optionStatus: 'preparation', optionReason: 'Opção em preparação;compatibilidade de acesso não é adesão nem alteração automática de regime.', }; if ( !i.optionReference.trim() || !validDate(i.optionDate) || !validDate(i.assessmentDate) || i.optionDate > i.assessmentDate ) return { optionStatus: 'indeterminate', optionReason: 'Identifique referência e data da opção,até à data de avaliação;não inferir adesão pela selecçãoISPC.', }; return { optionStatus: 'recorded', optionReason: 'Opção registada segundo documento ou pressuposto declarado;sem submissãoAT ou reescrita dos livros deIVA/IRPC/IRPS.', }; } function assess( i: IspcEligibilityInput, ): Pick< IspcEligibilityResult, 'status' | 'reason' | 'missing' | 'annualReference' > { let annual: FiscalFraction | null = null; try { annual = decimalFraction(i.annualReference); } catch { /* Missing or invalid is never zero. */ } const stop = ( status: IspcEligibilityResult['status'], reason: string, missing: string[] = [], ) => ({ status, reason, missing, annualReference: annual }); if ( !validDate(i.assessmentDate) || !i.assessmentDate.startsWith('2026-') || i.currency !== 'MZN' ) return stop( 'indeterminate', 'Este incremento cobre apenas avaliação2026emMZN;identifique data e moeda sem conversão silenciosa.', ['data2026', 'moedaMZN'], ); if (i.mode === 'occasional') return stop( 'indeterminate', 'Operação ocasional3(3–4):conservar dados;use o percurso específico de operações ocasionais próprias abaixo,com incidênciaIRPS e famílias3(4).', ['enquadramento específico da operação ocasional'], ); if (i.territory === 'no') return stop( 'incompatible', 'O âmbito territorial declarado não corresponde ao exercício em território nacional de2(1);nacionalidade do investidor não substitui este facto.', ); if (i.activities.includes('other') || i.otherActivities === 'yes') return stop( 'incompatible', 'Actividade fora dos âmbitos2/3declarada;exclusão3(2)c,não aplicarISPC por nome do sector.', ); if (i.organizedRequired === 'yes') return stop( 'incompatible', 'Obrigação de contabilidade organizada declarada:4(1)b não permite este enquadramento. Manter contabilidade voluntária distinto de obrigação.', ); if (i.sharesInIspc === 'yes' || i.sharesAnonymous === 'yes') return stop( 'incompatible', 'Participações abrangidas por3(2)a/b declaradas;enquadramentoISPC incompatível.', ); if (/^[1-9]\d{0,4}$/.test(i.establishments) && Number(i.establishments) > 1) return stop( 'incompatible', 'Actividade distribuída por vários estabelecimentos:exclusão3(2)d;sem presumir um estabelecimento por oportunidade.', ); const service = i.activities.some((a) => ['services12', 'liberal15'].includes(a), ), liberal = i.activities.includes('liberal15'); if (service && i.unlistedServices === 'yes') return stop( 'incompatible', 'Serviços fora dos previstos declarados:exclusão3(2)f. Categoria e natureza próprias devem ser identificadas.', ); if ( liberal && /^\d{1,3}$/.test(i.liberalDaysSameEntity) && Number(i.liberalDaysSameEntity) <= 366 && Number(i.liberalDaysSameEntity) > 183 ) return stop( 'incompatible', 'Serviços8(1)càmesma entidade por mais183dias durante o ano:exclusão3(2)e. Não estender esta condição automaticamente aos serviços8(1)b.', ); if ( i.generalRegimeChosen === 'yes' && i.generalThreeYearsCompleted === 'no' ) return stop( 'incompatible', 'Permanência mínima de três anos nos impostos gerais5(3)ainda não cumprida,segundo factos declarados.', ); if ( annual && ((i.basis === 'previous_year' && i.referenceYear === '2025') || (i.basis === 'start_forecast' && i.referenceYear === '2026')) && BigInt(annual.numerator) > 4000000n * BigInt(annual.denominator) ) return stop( 'incompatible', 'Volume anual de referência superior4.000.000MZN:fora do acesso4(1)a/4(2). Excesso do próprio ano e20%de8(1)d têm tratamento separado;não liquidado aqui.', ); const missing: string[] = []; if (!i.personType) missing.push('pessoa singular ou colectiva'); if (i.territory !== 'yes') missing.push('exercício em território nacional'); if (!i.activities.length || !i.activityDescription.trim()) missing.push('actividade/natureza identificadas'); if (!/^[1-9]\d{0,4}$/.test(i.establishments)) missing.push('número de estabelecimentos'); for (const [field, label] of [ ['organizedRequired', 'obrigação de contabilidade organizada'], ['sharesInIspc', 'participações noutras sociedadesISPC'], ['sharesAnonymous', 'participaçõesSA/sócios não identificáveis'], ['otherActivities', 'actividades fora do âmbito'], ['generalRegimeChosen', 'opção pelos impostos gerais'], ] as const) if (i[field] === '') missing.push(label); if (!i.basis) missing.push('origem do volume anual de referência'); if (!annual) missing.push('montante anual de referência'); const expected = i.basis === 'previous_year' ? '2025' : i.basis === 'start_forecast' ? '2026' : ''; if (expected && i.referenceYear !== expected) missing.push(`ano${expected}da referência;não reinterpretar outro ano`); if ( i.basis === 'start_forecast' && (i.forecastConfirmed !== 'yes' || (i.origin === 'effective' && !i.forecastConfirmationRef.trim())) ) missing.push( 'previsão da declaração de início confirmadaAT4(2),ou pressuposto expresso na projecção', ); if (service && i.unlistedServices === '') missing.push('correspondência dos serviços aos âmbitos daLei'); if ( liberal && (!/^\d{1,3}$/.test(i.liberalDaysSameEntity) || Number(i.liberalDaysSameEntity) > 366 || !i.liberalClientRef.trim()) ) missing.push( 'maior duração anual por mesma entidade e suporte declarado', ); if ( i.generalRegimeChosen === 'yes' && (i.generalThreeYearsCompleted !== 'yes' || !i.generalRegimeRef.trim()) ) missing.push( 'cumprimento dos três anos5(3)e referência da opção/registo', ); if (missing.length) return stop( 'indeterminate', 'Condições específicas por esclarecer;desconhecido não significa não aplicável nemzero.', missing, ); return stop( 'compatible', 'Condições de acesso da actividade habitual compatíveis com os dados de referência declarados;opção separada. Volume do ano em curso, trimestre, excesso e mínimo têm apuramento trimestral próprio; retenções têm registo separado.' + (liberal ? ' Serviçosliberais:limite5anos9-A,contagemtransitória13a partir da vigência2026;sem presumir permanência fora da cobertura.' : ''), ); } const key = (d: Record, b: IspcEligibilityBook) => JSON.stringify(['ispc-eligibility/1', scope(d), b.input]); export function ispcEligibilityCurrent( d: Record, b: IspcEligibilityBook, ): boolean { return !!b.result && b.result.key === key(d, b); } export function recordIspcEligibility( d: Record, b: IspcEligibilityBook, now = new Date().toISOString(), ): IspcEligibilityBook { validate(b); const result: IspcEligibilityResult = { calculatedAt: now, key: key(d, b), scope: scope(d), input: structuredClone(b.input), ...assess(b.input), ...option(b.input), source: ispcEligibilitySource, }; return { ...b, ...(b.result ? { history: [...(b.history ?? []), structuredClone(b.result)] } : {}), result, }; } export function ispcReuseDiff( d: Record, b: IspcEligibilityBook, ): Array<{ field: keyof IspcEligibilityInput; before: string; after: string }> { return Object.entries(ispcReuseValues(d)) .filter( ([field, value]) => b.input[field as keyof IspcEligibilityInput] !== value, ) .map(([field, value]) => ({ field: field as keyof IspcEligibilityInput, before: String(b.input[field as keyof IspcEligibilityInput]), after: String(value), })); } export function ispcEligibilityReportRows( d: Record, ): Array<[string, string, string, string]> { try { const b = ispcEligibilityBook(d), r = b.result; if (!r) return []; const i = r.input; return [ [ 'ISPC', 'L4-A — enquadramento2026', `${r.status === 'compatible' ? 'Compatível com os dados de referência' : r.status === 'incompatible' ? 'Incompatível' : 'Indeterminado'};${r.reason};falta:${r.missing.join('; ') || 'nenhum dado deste recorte'}`, `${r.scope};${ispcEligibilityCurrent(d, b) ? 'Entradas correspondentes' : 'Histórico preservado;rever entradas'};${r.calculatedAt};${r.source}`, ], [ 'ISPC', 'Origem e âmbito', `${i.entityRef || 'Entidade por identificar'};${i.personType};${i.origin === 'projected' ? 'Pressupostos de simulação' : 'Dados efectivos declarados'};${i.activities.map((a) => ispcActivities.find(([kind]) => kind === a)?.[1] ?? a).join('; ')};${i.activityDescription};estabelecimentos${i.establishments};territórionacional${i.territory}`, `Referênciaanual:${i.basis}/${i.referenceYear};${r.annualReference ? fractionPresentation(r.annualReference).decimal : 'Indeterminado'}MZN;${i.annualReferenceRef};previsãoAT${i.forecastConfirmed}/${i.forecastConfirmationRef};liberal${i.liberalDaysSameEntity}dias/${i.liberalClientRef};factos:${JSON.stringify({ organizedRequired: i.organizedRequired, sharesInIspc: i.sharesInIspc, sharesAnonymous: i.sharesAnonymous, otherActivities: i.otherActivities, unlistedServices: i.unlistedServices, generalRegimeChosen: i.generalRegimeChosen, generalThreeYearsCompleted: i.generalThreeYearsCompleted, generalRegimeRef: i.generalRegimeRef })}`, ], [ 'ISPC', 'Opção e próximo passo', `${r.optionReason};referência${i.optionReference};data${i.optionDate}`, `Prossiga no apuramento trimestral; esta avaliação não adopta regime nem calcula imposto. ${r.source}`, ], ]; } catch (error) { return [ [ 'ISPC', 'Enquadramento', 'Dados não recuperáveis;originais preservados', String(error), ], ]; } }