import type { OfferPreparation, OfferRequest, } from './iva-offer-preparations.ts'; export const offerWorkflowSource = 'Procedimento operacional fornecido pela Tempo em25/10/2026:pedido escrito ao Ministério das Finanças/AT → pedido apresentado → decisão recebida e despacho anexo → preparação do formulário aplicável com despacho. Origem distinta do CIVA14(2)a/b e14(3) e do cotejo documental do ModeloI.1;sem nova certificação ou aprovação criada pelo portal.'; export type OfferAttachment = { id: string; draftId: string; name: string; sha256: string; scan: string; url: string; }; export type OfferWorkflow = { recipient: string; writtenRequest: string; requestAttachment?: OfferAttachment; decisionIssuer: string; decisionAttachment?: OfferAttachment; scopeConfirmed: '' | 'yes' | 'no'; scopeKey?: string; formName: string; formSource: string; formContent: string; formAttachment?: OfferAttachment; formDecisionId?: string; formDecisionHash?: string; formPreparedAt?: string; submission: 'outside'; submissionReference: string; records: Array<{ at: string; stage: string; request: OfferRequest; workflow: Omit; }>; }; export function emptyOfferWorkflow(): OfferWorkflow { return { recipient: 'Ministério das Finanças / Autoridade Tributária', writtenRequest: '', decisionIssuer: '', scopeConfirmed: '', formName: '', formSource: '', formContent: '', submission: 'outside', submissionReference: '', records: [], }; } export function offerScopeKey(row: OfferPreparation): string { return JSON.stringify({ branch: row.branch, description: row.description, beneficiary: row.beneficiary, operation: row.operation, period: row.period, currency: row.currency, amount: row.amount, purpose: row.purpose, evidence: row.evidence, national: row.nationalInstitution, social: row.publicSocialInstitution, suitable: row.goodsSuitable, use: row.publicInterestUse, disaster: row.disaster, mitigation: row.mitigation, humanitarian: row.humanitarianPurpose, request: row.request, issuer: row.workflow?.decisionIssuer, document: row.workflow?.decisionAttachment?.id, hash: row.workflow?.decisionAttachment?.sha256, }); } export function offerAttachmentValid( a: OfferAttachment | undefined, ): a is OfferAttachment { return ( !!a && /^[0-9a-f-]{36}$/i.test(a.id) && /^[0-9a-f-]{36}$/i.test(a.draftId) && !!a.name && /^[0-9a-f]{64}$/.test(a.sha256) && a.url === `/api/v1/simulation-drafts/${a.draftId}/offer-documents/${a.id}` ); } export function offerWrittenRequest( row: OfferPreparation, w: OfferWorkflow, ): string { return `Ao ${w.recipient}\nAssunto:Pedido de isenção — ${row.branch}\nEntidade/destinatário:${row.beneficiary}\nFinalidade:${row.purpose}\n${row.branch === 'P084' ? 'Justificação do interesse público/fins sociais:' + row.evidence : 'Calamidade:' + row.disaster + ';finalidade humanitária:' + row.humanitarianPurpose}\nBens:${row.description}\nOperação:${row.operation};período:${row.period};valor de referência:${row.amount} ${row.currency}\nDocumentos:${row.availableDocuments || row.documentRef}\n${w.writtenRequest}\nPreparação InvesteMoz;a apresentação ocorre fora do portal. Não constitui concessão.`; } export function offerWorkflowApplied(row: OfferPreparation): { applied: boolean; reason: string; } { const w = row.workflow, r = row.request; if (!w || !r || r.state !== 'granted') return { applied: false, reason: 'Sem despacho de deferimento registado;mantém-se hipótese condicionada.', }; if ( !r.decisionRef.trim() || !validDate(r.decisionOn) || !w.decisionIssuer.trim() || !offerAttachmentValid(w.decisionAttachment) ) return { applied: false, reason: 'Despacho recebido incompleto:número,data,emitente e anexo por registar.', }; if (w.scopeConfirmed !== 'yes' || w.scopeKey !== offerScopeKey(row)) return { applied: false, reason: 'Correspondência desta linha ao âmbito do despacho não confirmada ou dados alterados;sem extensão automática.', }; if ( (!r.operationScope?.trim() && r.operation && r.operation !== row.operation) || (!r.periodScope?.trim() && /^\d{4}-\d{2}$/.test(row.period) && (row.period < r.periodFrom.slice(0, 7) || row.period > r.periodTo.slice(0, 7))) ) return { applied: false, reason: 'A operação ou o período desta linha está fora do âmbito expresso do despacho;sem extensão automática.', }; const conditions = row.branch === 'P084' ? [ row.nationalInstitution, row.publicSocialInstitution, row.goodsSuitable, row.publicInterestUse, ] : [row.mitigation]; if ( conditions.some((v) => v !== 'yes') || !row.description || !row.beneficiary || !row.purpose || !row.operation || (row.branch === 'P085' && (row.operation !== 'acquisition' || !row.disaster || !row.humanitarianPurpose)) ) return { applied: false, reason: 'Condições próprias do tratamento por demonstrar;despacho não alarga o fundamento legal.', }; return { applied: true, reason: `Tratamento deferido apenas nesta linha:${r.goods};${r.operationScope || r.operation};${r.periodScope || r.periodFrom + ' a ' + r.periodTo};condições:${r.conditions}. Despacho${r.decisionRef},${r.decisionOn},${w.decisionIssuer}. Sem extensão às restantes linhas,períodos ou oportunidades;sem dedução automática.`, }; } function validDate(v: string) { const d = new Date(v + 'T00:00:00Z'); return ( /^\d{4}-\d{2}-\d{2}$/.test(v) && Number.isFinite(d.getTime()) && d.toISOString().slice(0, 10) === v ); } export function recordOfferWorkflow( row: OfferPreparation, stage: 'request' | 'presented' | 'decision' | 'form', ): OfferPreparation { const w = structuredClone(row.workflow ?? emptyOfferWorkflow()), r = structuredClone(row.request); if (!r) throw Error('Prepare primeiro o pedido.'); if ( !w.recipient.trim() || !row.beneficiary.trim() || !row.description.trim() || !row.purpose.trim() || !row.operation ) throw Error( 'Indique entidade,finalidade,bens,operação e destinatário do pedido;rascunho incompleto pode ser guardado.', ); if ( stage === 'presented' && (!r.reference.trim() || !validDate(r.presentedOn) || r.state !== 'presented') ) throw Error( 'Registe referência,data e estado apresentado;submissão fora do InvesteMoz.', ); if (stage === 'decision' || stage === 'form') { if ( !['granted', 'denied'].includes(r.state) || !r.decisionRef.trim() || !validDate(r.decisionOn) || !w.decisionIssuer.trim() || !offerAttachmentValid(w.decisionAttachment) ) throw Error( 'Decisão recebida exige número,data,emitente e despacho anexo.', ); const dates = validDate(r.periodFrom) && validDate(r.periodTo) && r.periodFrom <= r.periodTo; const months = /^\d{4}-(0[1-9]|1[0-2])$/.test(r.periodFrom) && /^\d{4}-(0[1-9]|1[0-2])$/.test(r.periodTo) && r.periodFrom <= r.periodTo; if ( !r.goods.trim() || (!r.operation && !r.operationScope?.trim()) || (!r.periodScope?.trim() && !dates && !months) || !r.conditions.trim() ) throw Error( 'Conserve o âmbito literal:bens,operação,período e condições da decisão.', ); if (stage === 'decision') w.scopeKey = w.scopeConfirmed === 'yes' ? offerScopeKey(row) : undefined; } if (stage === 'form') { if (r.state !== 'granted') throw Error( 'Não preparar formulário de isenção como deferido após indeferimento.', ); if (!offerWorkflowApplied({ ...row, workflow: w }).applied) throw Error( 'Confira explicitamente a correspondência desta linha ao despacho antes de preparar o formulário;alterações não renovam a confirmação automaticamente.', ); if ( !w.formName.trim() || !w.formSource.trim() || (!w.formContent.trim() && !offerAttachmentValid(w.formAttachment)) ) throw Error( 'Identifique formulário aplicável e referência da sua aplicabilidade;prepare conteúdo ou anexe o exemplar preenchido.Sem presumir que ModeloI.1 abrange todos os casos.', ); w.formDecisionId = w.decisionAttachment!.id; w.formDecisionHash = w.decisionAttachment!.sha256; w.formPreparedAt = new Date().toISOString(); } const { records, ...snapshot } = w; w.records = [ ...records, { at: new Date().toISOString(), stage, request: r, workflow: snapshot }, ]; return { ...row, workflow: w }; } export function offerWorkflowReport(row: OfferPreparation): string { const w = row.workflow; if (!w) return ''; const state = offerWorkflowApplied(row); return `${offerWorkflowSource} Pedido:${row.request?.reference || 'por indicar'};apresentado em:${row.request?.presentedOn || 'por indicar'};estado:${row.request?.state || 'preparation'};decisão:${row.request?.decisionRef || 'por indicar'},${row.request?.decisionOn || 'por indicar'};Pedido escrito:${offerWrittenRequest(row, w)};decisão emitente:${w.decisionIssuer || 'por indicar'};anexo:${w.decisionAttachment?.name || 'por anexar'},SHA256:${w.decisionAttachment?.sha256 || 'por indicar'};${state.reason};formulário:${w.formName || 'por identificar'},fonte/aplicabilidade:${w.formSource || 'por indicar'};conteúdo:${w.formContent || 'por preparar'};despacho ligado ao formulário:${w.formDecisionId || 'por preparar'}/${w.formDecisionHash || ''};submissão fora do InvesteMoz:${w.submissionReference || 'não registada'};histórico:${w.records.map((e) => e.stage + '@' + e.at + ':' + e.request.state + ':' + e.request.decisionRef).join(' | ')}`; }