import { inputText } from './input-text.ts'; import { acquisitionBook, acquisitionScope } from './iva-acquisitions.ts'; import { importExemptionBook, isImportService, } from './iva-import-exemptions.ts'; import { offerWorkflowApplied, offerWorkflowReport, type OfferWorkflow, } from './iva-offer-workflow.ts'; type Choice = '' | 'yes' | 'no'; export const offerPreparationSource = 'CIVA,Lei13/2016,14(2)(a/b) e14(3),PDF13/p.1426(271). Procedimento operacional definido pela Tempo25/10/2026,distinto das fontes legais;correspondência específica do formulário ainda por demonstrar. Não concede isenção nem preenche dedução/campo11.'; export type OfferRequest = { state: 'preparation' | 'presented' | 'granted' | 'denied'; reference: string; presentedOn: string; decisionRef: string; decisionOn: string; goods: string; operation: '' | 'acquisition' | 'import'; periodFrom: string; periodTo: string; conditions: string; operationScope?: string; periodScope?: string; }; export const offerRequestLabels = { preparation: 'Em preparação', presented: 'Apresentado', granted: 'Deferido', denied: 'Indeferido', }; export type OfferPreparation = { workflow?: OfferWorkflow; id: string; branch: 'P084' | 'P085'; operation: '' | 'acquisition' | 'import'; description: string; period: string; currency: string; amount: string; counterparty: string; documentRef: string; documentDate: string; beneficiary: string; purpose: string; evidence: string; nationalInstitution: Choice; publicSocialInstitution: Choice; goodsSuitable: Choice; publicInterestUse: Choice; disaster: string; disasterKind: string; mitigation: Choice; reusedFrom?: { key: string; copiedAt: string }; humanitarianPurpose?: string; availableDocuments?: string; request?: OfferRequest; requestRecords?: Array<{ recordedAt: string; request: OfferRequest }>; normalVat?: string; normalVatSource?: string; conditionalAssumption?: Choice; }; export type OfferPreparationBook = { rows: OfferPreparation[]; history?: Array<{ savedAt: string; rows: OfferPreparation[] }>; saved?: { savedAt: string; rows: OfferPreparation[] }; }; type Store = { version: 1; books: Record; offerPreparations?: Record; [key: string]: unknown; }; export function newOfferPreparation( id: string, branch: OfferPreparation['branch'], currency: string, ): OfferPreparation { return { id, branch, operation: branch === 'P085' ? 'acquisition' : '', description: '', period: '', currency, amount: '', counterparty: '', documentRef: '', documentDate: '', beneficiary: '', purpose: '', evidence: '', nationalInstitution: '', publicSocialInstitution: '', goodsSuitable: '', publicInterestUse: '', disaster: '', disasterKind: '', mitigation: '', }; } function read(draft: Record): Store { const value: Store = draft.modelo_a_snapshot ? JSON.parse(inputText(draft.modelo_a_snapshot)) : { version: 1, books: {} }; if ( value?.version !== 1 || !value.books || typeof value.books !== 'object' || Array.isArray(value.books) ) throw new Error( 'Dados fiscais não recuperáveis; originais preservados.', ); if ( value.offerPreparations !== undefined && (!value.offerPreparations || typeof value.offerPreparations !== 'object' || Array.isArray(value.offerPreparations)) ) throw new Error( 'Rascunhos de ofertas não recuperáveis; sem substituição por lista vazia.', ); for (const book of Object.values(value.offerPreparations ?? {})) { if ( !book || !Array.isArray(book.rows) || (book.history !== undefined && !Array.isArray(book.history)) ) throw new Error( 'Formato de rascunhos de ofertas não recuperável; originais preservados.', ); for (const row of book.rows) { if ( !row || !['P084', 'P085'].includes(row.branch) || Object.keys(newOfferPreparation('', 'P084', '')).some( (key) => typeof row[key as keyof OfferPreparation] !== 'string', ) || (row.branch === 'P085' && row.operation === 'import') ) throw new Error( 'Rascunho de oferta não recuperável ou operação fora do ramo; dados conservados.', ); if ( [ 'humanitarianPurpose', 'availableDocuments', 'normalVat', 'normalVatSource', 'conditionalAssumption', ].some( (key) => row[key as keyof OfferPreparation] !== undefined && typeof row[key as keyof OfferPreparation] !== 'string', ) || (row.requestRecords !== undefined && !Array.isArray(row.requestRecords)) ) throw new Error( 'Elementos do percurso de ofertas não recuperáveis; originais preservados.', ); if ( row.workflow !== undefined && (!row.workflow || !Array.isArray(row.workflow.records) || [ 'recipient', 'writtenRequest', 'decisionIssuer', 'scopeConfirmed', 'formName', 'formSource', 'formContent', 'submission', 'submissionReference', ].some( (k) => typeof row.workflow?.[k as keyof OfferWorkflow] !== 'string', )) ) throw new Error( 'Percurso pedido/decisão/formulário não recuperável;originais preservados.', ); if ( row.request !== undefined && (!row.request || !Object.hasOwn(offerRequestLabels, row.request.state) || Object.entries(emptyOfferRequest()).some( ([key]) => typeof row.request?.[key as keyof OfferRequest] !== 'string', )) ) throw new Error( 'Pedido de oferta não recuperável; decisão e originais conservados.', ); } } return value; } export function offerPreparationBook( draft: Record, ): OfferPreparationBook { return ( read(draft).offerPreparations?.[acquisitionScope(draft)] ?? { rows: [] } ); } export function saveOfferPreparationBook( draft: Record, book: OfferPreparationBook, ): string { const previous = read(draft); const next = { ...previous, offerPreparations: { ...previous.offerPreparations, [acquisitionScope(draft)]: book, }, }; const encoded = JSON.stringify(next); read({ ...draft, modelo_a_snapshot: encoded }); return encoded; } export function recordOfferPreparations( book: OfferPreparationBook, ): OfferPreparationBook { if ( !book.rows.length || new Set(book.rows.map((r) => r.id)).size !== book.rows.length ) throw new Error('Indique rascunhos com identificadores distintos.'); return { ...book, history: [ ...(book.history ?? []), ...(book.saved ? [structuredClone(book.saved)] : []), ], saved: { savedAt: new Date().toISOString(), rows: structuredClone(book.rows), }, }; } export type OfferReusable = { key: string; label: string; operation: 'acquisition' | 'import'; values: Partial; }; export function offerReusableData( draft: Record, ): OfferReusable[] { const purchases = acquisitionBook(draft).rows.map((row) => ({ key: 'acquisition:' + row.id, label: 'Aquisição: ' + (row.description || row.documentRef || row.id), operation: 'acquisition' as const, values: { description: row.description, period: row.period, currency: row.sourceCurrency, amount: row.net, documentRef: row.documentRef, documentDate: row.documentDate, counterparty: row.supplier, }, })); const imports = importExemptionBook(draft) .rows.filter((row) => !isImportService(row.treatment)) .map((row) => ({ key: 'import:' + row.id, label: 'Importação: ' + (row.description || row.documentRef || row.id), operation: 'import' as const, values: { description: row.description, period: row.period, currency: row.currency, amount: row.declaredValue, documentRef: row.documentRef, documentDate: row.documentDate, counterparty: row.importer, }, })); return [...purchases, ...imports]; } export function reuseOfferData( row: OfferPreparation, source: OfferReusable, ): OfferPreparation { if (row.branch === 'P085' && source.operation === 'import') throw new Error( 'P085 abrange aquisição; esta referência de importação conserva-se no seu percurso.', ); const next = { ...row }; if ( next.currency && source.values.currency && next.currency !== source.values.currency ) throw new Error( 'Moeda da referência divergente; valores e edições conservados, sem conversão.', ); // Only fill empty inputs. Deliberate edits and answers are never replaced. if (!next.operation) next.operation = source.operation; if (next.operation !== source.operation) throw new Error( 'A operação já escolhida diverge da origem; rascunho conservado.', ); for (const [key, value] of Object.entries(source.values)) if (!next[key as keyof OfferPreparation] && typeof value === 'string') Object.assign(next, { [key]: value }); return { ...next, reusedFrom: { key: source.key, copiedAt: new Date().toISOString() }, }; } export function emptyOfferRequest(): OfferRequest { return { state: 'preparation', reference: '', presentedOn: '', decisionRef: '', decisionOn: '', goods: '', operation: '', periodFrom: '', periodTo: '', conditions: '', }; } export function recordOfferRequest(row: OfferPreparation): OfferPreparation { const request = row.request ?? emptyOfferRequest(); const validPeriod = (value: string) => { if (/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) return true; if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false; const d = new Date(value + 'T00:00:00Z'); return ( Number.isFinite(d.getTime()) && d.toISOString().slice(0, 10) === value ); }; const temporalScope = !!request.periodScope?.trim() || (validPeriod(request.periodFrom) && validPeriod(request.periodTo) && request.periodFrom.length === request.periodTo.length && request.periodTo >= request.periodFrom); if ( ['granted', 'denied'].includes(request.state) && (!request.goods.trim() || (!['acquisition', 'import'].includes(request.operation) && !request.operationScope?.trim()) || !temporalScope || !request.conditions.trim()) ) throw new Error( 'Conserve o âmbito da decisão: bens, operação, período e condições. Pode usar datas, meses ou o âmbito temporal literal do documento, inclusive sem termo. Pode continuar a guardar o rascunho incompleto.', ); return { ...row, requestRecords: [ ...(row.requestRecords ?? []), { recordedAt: new Date().toISOString(), request: structuredClone(request), }, ], }; } export function offerScenarios(row: OfferPreparation): { normalVat: string | null; conditionalVat: string | null; conditionalReason: string; applied: boolean; appliedVat?: string; } { const normalVat = row.normalVatSource?.trim() && /^\d{1,15}(\.\d{1,6})?$/.test(row.normalVat ?? '') ? row.normalVat! : null; const result = { normalVat, conditionalVat: null as string | null, conditionalReason: 'Hipótese de isenção não assumida; benefício não aplicado.', applied: false as const, }; const workflow = offerWorkflowApplied(row); if (workflow.applied) return { ...result, applied: true, appliedVat: '0.00', conditionalReason: workflow.reason, }; if (row.conditionalAssumption !== 'yes') return result; const choices = row.branch === 'P084' ? [ row.nationalInstitution, row.publicSocialInstitution, row.goodsSuitable, row.publicInterestUse, ] : [row.mitigation]; if (choices.includes('no')) return { ...result, conditionalReason: 'Condição material contrariada; este cenário de isenção não está demonstrado. Benefício não aplicado.', }; if ( choices.some((value) => value !== 'yes') || !row.description.trim() || !row.beneficiary.trim() || !row.purpose.trim() || !row.operation || !/^2026-(0[1-9]|1[0-2])$/.test(row.period) || row.currency !== 'MZN' || (row.branch === 'P085' && (!row.disaster.trim() || !row.humanitarianPurpose?.trim())) ) return { ...result, conditionalReason: 'Pressupostos, contexto ou condições materiais por indicar; sem IVA zero presumido. Benefício não aplicado.', }; if (row.workflow) return { ...result, conditionalVat: '0.00', conditionalReason: 'Cenário condicionado à isenção antes de despacho favorável e âmbito demonstrado;procedimento operacional fornecido pela Tempo.Não é benefício concedido nem imposto aplicado;apresentar pedido não concede isenção.', }; return { ...result, conditionalVat: '0.00', conditionalReason: 'Cenário condicionado à isenção: hipótese expressa de cumprimento das condições e aplicação do procedimento, cuja correspondência permanece pendente. Não é benefício concedido nem imposto aplicado; apresentar pedido não concede isenção.', }; } export function offerPathReport(row: OfferPreparation): string { const request = row.request ?? emptyOfferRequest(), scenario = offerScenarios(row); if (row.workflow) return ( offerWorkflowReport(row) + `;Cenário normal — IVA ${scenario.normalVat ?? 'Indeterminado'};Cenário condicionado à isenção — IVA ${scenario.conditionalVat ?? 'Indeterminado'};IVA do âmbito deferido:${scenario.appliedVat ?? 'Não aplicado'};${scenario.conditionalReason}` ); const scope = (r: OfferRequest) => `bens ${r.goods || 'por indicar'}; operação ${r.operationScope || r.operation || 'por indicar'}; período ${r.periodScope || `${r.periodFrom || 'por indicar'} a ${r.periodTo || 'por indicar'}`}; condições ${r.conditions || 'por indicar'}; documento ${r.decisionRef || 'por indicar'},${r.decisionOn || 'data por indicar'}`; return `${row.branch === 'P084' ? `Pedido ${offerRequestLabels[request.state]} (estado declarado, sem concessão pelo simulador); referência ${request.reference || 'por indicar'}, apresentação ${request.presentedOn || 'por indicar'}; âmbito em edição ${scope(request)}; ${(row.requestRecords ?? []).map((record) => `estado registado ${offerRequestLabels[record.request.state]} em ${record.recordedAt}, ${scope(record.request)}`).join(' | ') || 'sem estado/decisão registado'}; não estender decisão aos restantes bens, operações, períodos ou oportunidades; apresentar pedido não concede isenção` : `Emergência ${row.disaster || 'por indicar'}; finalidade humanitária ${row.humanitarianPurpose || 'por indicar'}; procedimento de emergência/doação por corresponder, sem aplicação automática`}; documentos disponíveis ${row.availableDocuments || row.documentRef || 'por indicar'}; Cenário normal — IVA ${scenario.normalVat ?? 'Indeterminado'} ${row.currency}, documento/pressuposto ${row.normalVatSource || 'por indicar'}; Cenário condicionado à isenção — IVA ${scenario.conditionalVat ?? 'Indeterminado'} ${row.currency}; ${scenario.conditionalReason}`; } export function offerPreparationReportRows( draft: Record, ): Array<[string, string, string, string]> { try { return offerPreparationBook(draft).rows.map((row) => [ 'IVA mensal', `Rascunho de oferta ${row.branch} — ${row.id}`, `${row.description || 'Bens por indicar'}; ${offerWorkflowApplied(row).applied ? 'tratamento deferido no âmbito do despacho' : 'preparação, benefício não aplicado'}; ${row.amount || 'Valor não informado'} ${row.currency}; ${row.period || 'Período não informado'}`, `${offerPreparationSource} Oportunidade ${acquisitionScope(draft)}; operação ${row.operation || 'por indicar'}; contraparte ${row.counterparty || 'por indicar'}; documento ${row.documentRef || 'por indicar'},${row.documentDate || 'data por indicar'}; beneficiário ${row.beneficiary || 'por indicar'}; finalidade ${row.purpose || 'por indicar'}; ${row.branch === 'P084' ? `instituição nacional ${row.nationalInstitution || 'por indicar'}, interesse público/relevantes fins sociais ${row.publicSocialInstitution || 'por indicar'}, bens adequados ${row.goodsSuitable || 'por indicar'}, utilização de interesse público ${row.publicInterestUse || 'por indicar'}` : `calamidade ${row.disaster || 'por indicar'}, natureza ${row.disasterKind || 'por indicar'}, atenuação dos efeitos ${row.mitigation || 'por indicar'}`}; fundamento ${row.evidence || 'por indicar'}; origem reutilizada ${row.reusedFrom?.key || 'entrada directa'}; ${offerPathReport(row)}; sem isenção automática ou dedução.`, ]); } catch { return [ [ 'IVA mensal', 'Rascunhos de ofertas', 'Dados não recuperáveis', 'Originais preservados; sem substituição por zero.', ], ]; } }