81 lines
14 KiB
TypeScript
81 lines
14 KiB
TypeScript
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<string,unknown>;offerPreparations?:Record<string,OfferPreparationBook>;[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<string,unknown>):Store {
|
|
const value:Store=draft.modelo_a_snapshot?JSON.parse(String(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<string,unknown>):OfferPreparationBook{return read(draft).offerPreparations?.[acquisitionScope(draft)]??{rows:[]};}
|
|
export function saveOfferPreparationBook(draft:Record<string,unknown>,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<OfferPreparation>};
|
|
export function offerReusableData(draft:Record<string,unknown>):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<string,unknown>):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.']];}
|
|
}
|