import { inputText } from './input-text.ts'; import { assessFiscalClaim } from './fiscal-claim-events.ts'; import { closureBook, closureCurrent, closureFingerprint, } from './iva-closure.ts'; import { subtractFractions, fraction, fractionPresentation, type FiscalFraction, } from './fiscal-rational.ts'; export const refundSource = 'L3-D:CIVA21(5/6/7),Lei13/2016 PDF15–16,p1426(273–274);Lei22/2022 PDF3–4;Lei10/2025 PDF13/p2180(13),21(7a/8revogado/10/13/15). ModeloA/VAT Return9c0b99a8,p2/quadro11:26+27=25. Pedido,decisão e recebimento separados. Formalidades doRegulamento78/2017/30/2022/52/2025 não automatizadas;sem concessão automática.'; type Choice = '' | 'yes' | 'no'; export type RefundSource = { closureId: string; period: string; entityRef: string; origin: 'projected' | 'effective'; allocationKey: string; calculatedAt: string; credit: FiscalFraction; carry: FiscalFraction; requested: FiscalFraction; }; export type RefundReceipt = { id: string; reference: string; date: string; amount: string; }; export type RefundInput = { id: string; source: RefundSource | null; ground: '' | 'four_months' | 'high_credit' | 'cessation'; originPeriod: string; entitlementDate: string; cessationDate: string; groundRef: string; factsConfirmed: Choice; oldestFirst: Choice; stage: 'preparation' | 'submitted'; requestReference: string; requestDate: string; decision: 'pending' | 'approved' | 'partial' | 'rejected'; decisionReference: string; decisionDate: string; decisionIssuer: string; approvedAmount: string; receipts: RefundReceipt[]; }; export type RefundResult = { id: string; status: 'determined' | 'indeterminate'; reason: string; inputKey: string; eligibility: 'compatible' | 'not_demonstrated' | 'incompatible'; requested: FiscalFraction | null; reported: FiscalFraction | null; approved: FiscalFraction | null; received: FiscalFraction | null; awaitingReceipt: FiscalFraction | null; notGranted: FiscalFraction | null; source: string; }; export type RefundCalculation = { calculatedAt: string; key: string; inputs: RefundInput[]; rows: RefundResult[]; }; export type RefundBook = { version: 1; rows: RefundInput[]; result?: RefundCalculation; history?: RefundCalculation[]; }; const scope = (d: Record) => inputText(d.context_opportunity || 'general'), norm = (s: string) => s.trim().toLocaleLowerCase(); const date = (s: string) => /^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(s) && !Number.isNaN(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s; const period = (s: string) => /^20\d{2}-(0[1-9]|1[0-2])$/.test(s); const months = (a: string, b: string) => (Number(b.slice(0, 4)) - Number(a.slice(0, 4))) * 12 + Number(b.slice(5)) - Number(a.slice(5)); function store(d: Record): Record { const s = JSON.parse( inputText(d.modelo_a_snapshot || '{"version":1,"books":{}}'), ); if (s.version !== 1 || !s.books) throw Error('Livros não recuperáveis;originais preservados.'); return s; } function maps(d: Record): Record { const m = store(d).ivaRefunds; if (m === undefined) return {}; if (!m || typeof m !== 'object' || Array.isArray(m)) throw Error('Mapa de reembolsos não recuperável;sem o substituir.'); return m as Record; } function validate(b: RefundBook): void { if ( !b || b.version !== 1 || !Array.isArray(b.rows) || (b.history !== undefined && !Array.isArray(b.history)) || (b.result !== undefined && (!Array.isArray(b.result.rows) || !Array.isArray(b.result.inputs))) ) throw Error( 'Livro de reembolsos não recuperável;originais preservados.', ); for (const r of b.rows) if ( !r || ![ 'id', 'originPeriod', 'entitlementDate', 'cessationDate', 'groundRef', 'requestReference', 'requestDate', 'decisionReference', 'decisionDate', 'decisionIssuer', 'approvedAmount', ].every((k) => typeof r[k as keyof RefundInput] === 'string') || !['', 'four_months', 'high_credit', 'cessation'].includes( r.ground, ) || !['', 'yes', 'no'].includes(r.factsConfirmed) || !['', 'yes', 'no'].includes(r.oldestFirst) || !['preparation', 'submitted'].includes(r.stage) || !['pending', 'approved', 'partial', 'rejected'].includes( r.decision, ) || !Array.isArray(r.receipts) || r.receipts.some( (p) => !p || !['id', 'reference', 'date', 'amount'].every( (k) => typeof p[k as keyof RefundReceipt] === 'string', ), ) ) throw Error('EntradasD não recuperáveis;sem montantes presumidos.'); } export function refundBook(d: Record): RefundBook { const b = maps(d)[scope(d)] ?? { version: 1, rows: [] }; validate(b); return b; } export function saveRefundBook( d: Record, b: RefundBook, ): string { validate(b); const s = store(d); return JSON.stringify({ ...s, ivaRefunds: { ...maps(d), [scope(d)]: b } }); } export function refundSources(d: Record): RefundSource[] { const b = closureBook(d); if (!closureCurrent(d, b)) return []; return (b.result?.rows ?? []).flatMap((r) => { const p = r.fields['27'], c = r.fields['25'], carry = r.fields['26']; if ( r.status !== 'determined' || !p || !c || !carry || BigInt(p.numerator) <= 0n ) return []; const dependentUnreconciled = b.rows.some( (input) => input.credits.some( (credit) => credit.kind === 'previous' && credit.previousId === r.id, ) && !b.result!.rows.some( (result) => result.id === input.id && result.status === 'determined', ), ); if (dependentUnreconciled) return []; return [ { closureId: r.id, period: r.period, entityRef: r.entityRef, origin: r.origin, allocationKey: closureFingerprint([ r.id, r.entityRef, r.origin, r.period, c, carry, p, ]), calculatedAt: b.result!.calculatedAt, credit: c, carry, requested: p, }, ]; }); } export function newRefund( id: string, source: RefundSource | null, ): RefundInput { return { id, source: source ? structuredClone(source) : null, ground: '', originPeriod: '', entitlementDate: '', cessationDate: '', groundRef: '', factsConfirmed: '', oldestFirst: '', stage: 'preparation', requestReference: '', requestDate: '', decision: 'pending', decisionReference: '', decisionDate: '', decisionIssuer: '', approvedAmount: '', receipts: [], }; } // A new calculation timestamp alone does not change an identical allocation. function available(d: Record, r: RefundInput): boolean { return ( !!r.source && refundSources(d).some( (s) => s.allocationKey === r.source!.allocationKey && s.closureId === r.source!.closureId && s.entityRef === r.source!.entityRef && s.origin === r.source!.origin && s.period === r.source!.period && JSON.stringify([s.credit, s.carry, s.requested]) === JSON.stringify([ r.source!.credit, r.source!.carry, r.source!.requested, ]), ) ); } function identity(r: RefundInput): string { return JSON.stringify([ norm(r.source?.entityRef ?? ''), r.source?.origin, r.source?.period, r.source?.closureId, ]); } function uses(d: Record, b: RefundBook): RefundInput[] { return [ ...b.rows, ...Object.entries(maps(d)) .filter(([id]) => id !== scope(d)) .flatMap(([, v]) => { validate(v); return v.rows.filter((r) => r.source?.origin === 'effective'); }), ]; } function receiptIdentity(r: RefundInput, p: RefundReceipt): string { return JSON.stringify([ norm(r.source?.entityRef ?? ''), r.source?.origin, norm(p.reference), ]); } function past(d: Record, b: RefundBook): RefundInput[] { return Object.entries({ ...maps(d), [scope(d)]: b }).flatMap( ([context, v]) => [...(v.history ?? []), ...(v.result ? [v.result] : [])].flatMap( (c) => c.inputs.filter( (r) => context === scope(d) || r.source?.origin === 'effective', ), ), ); } function key(d: Record, b: RefundBook): string { const all = uses(d, b), old = past(d, b); return closureFingerprint([ b.rows, b.rows.map((r) => available(d, r)), all.map((r) => [ r.id, identity(r), r.receipts.map((p) => receiptIdentity(r, p)), ]), b.rows.map((r) => old .filter( (v) => v.id !== r.id && (v.stage === 'submitted' || v.decision !== 'pending' || v.receipts.length), ) .some( (v) => identity(v) === identity(r) || r.receipts.some((p) => v.receipts.some( (q) => receiptIdentity(v, q) === receiptIdentity(r, p), ), ), ), ), ]); } export function refundCurrent( d: Record, b: RefundBook, ): boolean { return ( !!b.result && b.result.key === key(d, b) && b.result.rows.length === b.rows.length && b.rows.every((r) => b.result!.rows.some( (v) => v.id === r.id && v.inputKey === closureFingerprint(r), ), ) ); } export function recordRefund( d: Record, b: RefundBook, now = new Date().toISOString(), ): RefundBook { validate(b); if (new Set(b.rows.map((r) => r.id)).size !== b.rows.length) throw Error('Identidade de pedido repetida.'); const all = uses(d, b), old = past(d, b); const rows = b.rows.map((row) => { const r: RefundResult = { id: row.id, status: 'indeterminate', reason: '', inputKey: closureFingerprint(row), eligibility: 'not_demonstrated', requested: null, reported: null, approved: null, received: null, awaitingReceipt: null, notGranted: null, source: refundSource, }; const stop = (reason: string) => ({ ...r, reason }); const s = row.source; if ( !s || !available(d, row) || d.currency !== 'MZN' || !/^2026-(0[1-9]|1[0-2])$/.test(s.period) ) return stop( 'Escolha afectação26/27 actual e recuperável do mesmo período2026/MZN;crédito já reportado não é pedido disponível.', ); if ( all.filter((v) => identity(v) === identity(row)).length > 1 || old.some( (v) => v.id !== row.id && identity(v) === identity(row) && (v.stage === 'submitted' || v.decision !== 'pending' || v.receipts.length), ) ) return stop( 'Parcela já ligada a outro pedido no âmbito recuperado;sem pedir ou consumir duas vezes o mesmo crédito.', ); r.requested = s.requested; r.reported = s.carry; let eligibilityReason = ''; if ( !row.ground || !period(row.originPeriod) || row.originPeriod > s.period || !date(row.entitlementDate) || row.entitlementDate.slice(0, 7) !== row.originPeriod || !row.groundRef.trim() || row.factsConfirmed !== 'yes' || row.oldestFirst !== 'yes' ) eligibilityReason = 'Fundamento/origem/nascimento do crédito,suporte ou consideração dos créditos mais antigos ainda não demonstrados;sem direito presumido.'; if ( !date(row.requestDate) || row.requestDate.slice(0, 7) < s.period || !row.requestReference.trim() ) return stop( 'Identifique referência e data do pedido ou preparação;separar da decisão.', ); if (!eligibilityReason) { const lastYear = Number(row.entitlementDate.slice(0, 4)) + 10, lastMonth = Number(row.entitlementDate.slice(5, 7)), lastDay = Math.min( Number(row.entitlementDate.slice(8)), new Date(Date.UTC(lastYear, lastMonth, 0)).getUTCDate(), ); const anniversary = `${lastYear}-${String(lastMonth).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; const above = (n: bigint) => BigInt(subtractFractions(s.credit, fraction(n)).numerator) > 0n; if ( row.requestDate < row.entitlementDate || row.requestDate > anniversary ) { r.eligibility = 'incompatible'; eligibilityReason = 'Datas declaradas incompatíveis com nascimento do direito/limite10anos de21(15),Lei10/2025;conservar documentos e identificar eventual excepção.'; } else if ( (row.ground === 'four_months' && (months(row.originPeriod, s.period) < 4 || !above(100000n))) || (row.ground === 'high_credit' && !above(500000n)) || (row.ground === 'cessation' && (!date(row.cessationDate) || row.cessationDate > row.requestDate)) ) { r.eligibility = 'incompatible'; eligibilityReason = 'Factos declarados não demonstram21(6),quatro meses/crédito superior100000;21(7a),superior500000;ou21(7b),cessação identificada.'; } else r.eligibility = 'compatible'; } const events = assessFiscalClaim(row, s.requested, (p) => { const id = receiptIdentity(row, p); return ( all.flatMap((v) => v.receipts.filter((q) => receiptIdentity(v, q) === id), ).length > 1 || old.some( (v) => v.id !== row.id && v.receipts.some((q) => receiptIdentity(v, q) === id), ) ); }); Object.assign(r, events); if (events.status === 'indeterminate') return r; if (row.decision === 'pending') { if (eligibilityReason) { r.status = 'indeterminate'; return stop( eligibilityReason + ' Pedido/estado conservados;formalidades regulamentares não automatizadas.', ); } r.reason = row.stage === 'preparation' ? 'Pedido em preparação;parcela separada do reporte,sem submissão,decisão ou recebimento.' : 'Pedido apresentado fora doInvesteMoz;decisão e recebimento ainda não identificados.'; return r; } r.reason = (eligibilityReason ? 'Decisão externa documentada registada independentemente da avaliação automática do fundamento: ' + eligibilityReason + ' ' : '') + 'Pedido,decisão e recebimento separados;parcela27 não é reutilizada em26. Parte não deferida/indeferimento não libertam crédito automaticamente;eventual retorno exige procedimento documentado e reconciliação,sem duplicar20. Recebimento é registo declarado,sem execução bancária ou alteração silenciosa de caixa. Formalidades e suspensões não automatizadas;150dias não são promessa de recebimento.'; return r; }); const result = { calculatedAt: now, key: key(d, b), inputs: structuredClone(b.rows), rows, }; return { ...b, ...(b.result ? { history: [...(b.history ?? []), structuredClone(b.result)] } : {}), result, }; } export function refundReportRows( d: Record, ): Array<[string, string, string, string]> { try { const b = refundBook(d); if (!b.rows.length) return []; const out: Array<[string, string, string, string]> = [ [ 'IVA mensal', 'L3-D — reembolso geral', refundCurrent(d, b) ? 'Resultado correspondente às entradas' : 'Histórico preservado;rever entradas/afectação', refundSource, ], ], fmt = (v: FiscalFraction | null | undefined) => v ? fractionPresentation(v).decimal : 'Indeterminado'; for (const r of b.result?.rows ?? []) { const input = b.result!.inputs.find((v) => v.id === r.id), s = input?.source; out.push([ 'IVA mensal', `Reembolso — ${s?.period ?? 'por identificar'}`, `Fundamento:${r.eligibility === 'compatible' ? 'Compatível com factos declarados' : r.eligibility === 'incompatible' ? 'Divergência nos factos — ver suporte' : 'Não demonstrado automaticamente'};Pedido27:${fmt(r.requested)};reporte26 separado:${fmt(r.reported)};reconhecido:${fmt(r.approved)};recebido:${fmt(r.received)};aguarda recebimento:${fmt(r.awaitingReceipt)};não deferido:${fmt(r.notGranted)}`, `${s?.entityRef ?? ''};${scope(d)};MZN;${s?.origin === 'effective' ? 'Dados efectivos declarados' : 'Pressuposto de simulação'};${input?.stage === 'submitted' ? 'Apresentado fora doInvesteMoz' : 'Em preparação'};pedido ${input?.requestReference ?? ''},${input?.requestDate ?? ''};origem${input?.originPeriod ?? ''},nascimento${input?.entitlementDate ?? ''};${input?.groundRef ?? ''};${r.reason}`, ]); out.push([ 'IVA mensal', `Decisão e recebimento — ${s?.period ?? ''}`, `Decisão:${input?.decision === 'approved' ? 'Deferido' : input?.decision === 'partial' ? 'Deferimento parcial' : input?.decision === 'rejected' ? 'Indeferido' : 'Pendente'};documento${input?.decisionReference ?? ''};${input?.decisionDate ?? ''};${input?.decisionIssuer ?? ''};recebimentos:${input?.receipts.map((p) => `${p.reference};${p.date};${p.amount}MZN`).join(' | ') || 'nenhum registado'}`, `Afectação do fecho${s?.closureId ?? ''},versão${s?.calculatedAt ?? ''};25:${fmt(s?.credit)}=26:${fmt(s?.carry)}+27:${fmt(s?.requested)};âmbito apenas desta parcela;versão guardada${b.result!.calculatedAt};${refundSource}`, ]); } return out; } catch { return [ [ 'IVA mensal', 'L3-D', 'Pedido não recuperável', 'Originais preservados;sem concessão/recebimento presumidos.', ], ]; } }