feat(iva): distinguish offer requests emergency drafts and conditional scenarios
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { newOfferPreparation,offerPreparationBook,offerPreparationSource,offerReusableData,recordOfferPreparations,reuseOfferData,saveOfferPreparationBook,type OfferPreparation,type OfferPreparationBook } from '@/lib/iva-offer-preparations';
|
||||
import { newOfferPreparation,offerPreparationBook,offerPreparationSource,offerReusableData,recordOfferPreparations,reuseOfferData,saveOfferPreparationBook,emptyOfferRequest,recordOfferRequest,offerRequestLabels,offerScenarios,type OfferRequest,type OfferPreparation,type OfferPreparationBook } from '@/lib/iva-offer-preparations';
|
||||
|
||||
export function SimulationOfferPreparations({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
|
||||
const [error,setError]=useState('');let book:OfferPreparationBook;let reusable:ReturnType<typeof offerReusableData>;
|
||||
@@ -10,7 +10,7 @@ export function SimulationOfferPreparations({draft,onChange}:{draft:Record<strin
|
||||
const question=(row:OfferPreparation,key:'nationalInstitution'|'publicSocialInstitution'|'goodsSuitable'|'publicInterestUse'|'mitigation',label:string)=><label className="block" key={key}>{label}<select className="block rounded border p-2" value={row[key]} onChange={e=>change(row.id,{[key]:e.target.value})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>;
|
||||
return <details className="space-y-3 rounded border p-3"><summary>Ofertas a instituições e para calamidades — preparar rascunhos</summary>
|
||||
<p>Prepare apenas as ofertas relevantes para a oportunidade {String(draft.context_opportunity||'entrada geral')}. Pode guardar dados incompletos e reutilizar referências existentes. A reutilização preenche apenas campos vazios; as suas edições mantêm-se.</p>
|
||||
<p>O procedimento de aplicação do benefício está em pesquisa. Este rascunho não concede isenção, não calcula IVA nem altera a dedução no campo11. Os restantes enquadramentos continuam disponíveis.</p>
|
||||
<p>O procedimento de aplicação do benefício está em pesquisa. Prepare o pedido e compare o cenário normal com uma hipótese condicionada à isenção. Apresentar o pedido não concede isenção. Estes cenários não alteram a dedução no campo11; os restantes enquadramentos continuam disponíveis.</p>
|
||||
{error&&<p role="alert">{error}</p>}
|
||||
<button type="button" className="underline" onClick={()=>run(()=>save({...book,rows:[...book.rows,newOfferPreparation(crypto.randomUUID(),'P084',String(draft.currency??''))]}))}>Preparar oferta a instituição</button>{' '}
|
||||
<button type="button" className="underline" onClick={()=>run(()=>save({...book,rows:[...book.rows,newOfferPreparation(crypto.randomUUID(),'P085',String(draft.currency??''))]}))}>Preparar oferta para calamidade</button>
|
||||
@@ -21,6 +21,24 @@ export function SimulationOfferPreparations({draft,onChange}:{draft:Record<strin
|
||||
{row.reusedFrom&&<p>Referência reutilizada: {row.reusedFrom.key}. A origem foi conservada; a aquisição/importação original não foi alterada.</p>}
|
||||
<div className="grid gap-3 md:grid-cols-2">{(['description','period','currency','amount','counterparty','documentRef','documentDate','beneficiary','purpose','evidence'] as const).map((key,index)=><label key={key}>{['Bens destinados à oferta','Período previsto / efectivo','Moeda','Valor de referência, sem cálculo fiscal','Fornecedor / importador','Documento / referência da previsão','Data do documento / prevista','Instituição / destinatário da oferta','Finalidade concreta da oferta','Fundamento factual / pressupostos'][index]}<input className="block rounded border p-2" type={key==='period'?'month':key==='documentDate'?'date':'text'} value={row[key]} onChange={e=>change(row.id,{[key]:e.target.value})}/></label>)}</div>
|
||||
{row.branch==='P084'?<fieldset><legend>Condições próprias da oferta à instituição</legend>{question(row,'nationalInstitution','A instituição beneficiária é nacional')}{question(row,'publicSocialInstitution','A instituição tem interesse público e relevantes fins sociais')}{question(row,'goodsSuitable','Os bens são inteiramente adequados à natureza da instituição')}{question(row,'publicInterestUse','A instituição utilizará os bens em actividades de evidente interesse público')}</fieldset>:<fieldset><legend>Finalidade própria da oferta para calamidade</legend><label>Calamidade e contexto afectados<input className="block rounded border p-2" value={row.disaster} onChange={e=>change(row.id,{disaster:e.target.value})}/></label><label>Natureza da calamidade<input className="block rounded border p-2" value={row.disasterKind} placeholder="Cheias, tempestades, secas, ciclones, sismos ou outra de idêntica natureza" onChange={e=>change(row.id,{disasterKind:e.target.value})}/></label>{question(row,'mitigation','Os bens destinam-se a ofertas para atenuar os efeitos da calamidade identificada')}</fieldset>}
|
||||
{row.branch==='P085'&&<label>Finalidade humanitária da resposta à emergência<textarea className="block w-full rounded border p-2" value={row.humanitarianPurpose??''} onChange={e=>change(row.id,{humanitarianPurpose:e.target.value})}/></label>}
|
||||
<label>Documentos disponíveis e respectivas referências<textarea className="block w-full rounded border p-2" value={row.availableDocuments??''} onChange={e=>change(row.id,{availableDocuments:e.target.value})}/></label>
|
||||
{!!row.documentRef&&<button type="button" className="underline" onClick={()=>{const reference=[row.documentRef,row.documentDate].filter(Boolean).join(', ');if(!(row.availableDocuments??'').includes(reference))change(row.id,{availableDocuments:[row.availableDocuments,reference].filter(Boolean).join('\n')});}}>Juntar referência do documento já indicado</button>}
|
||||
{row.branch==='P084'&&(()=>{const request=row.request??emptyOfferRequest();const edit=(patch:Partial<OfferRequest>)=>change(row.id,{request:{...request,...patch}});return <fieldset className="rounded border p-3"><legend>Pedido e âmbito da decisão</legend>
|
||||
<p>Registe o estado declarado e os elementos disponíveis. Este registo não apresenta um pedido à autoridade nem concede o benefício. Conserve o âmbito exacto de cada decisão, mesmo que cubra apenas parte dos bens ou períodos.</p>
|
||||
<label>Estado declarado do pedido<select className="block rounded border p-2" value={request.state} onChange={e=>edit({state:e.target.value as OfferRequest['state']})}>{Object.entries(offerRequestLabels).map(([value,label])=><option key={value} value={value}>{label}</option>)}</select></label>
|
||||
{(['reference','presentedOn','decisionRef','decisionOn','goods','periodFrom','periodTo','conditions'] as const).map((key,index)=><label className="block" key={key}>{['Referência do pedido','Data de apresentação, se disponível','Documento / referência da decisão, se disponível','Data da decisão, se disponível','Bens abrangidos pela decisão','Início do período abrangido','Fim do período abrangido','Condições, limites ou fundamento da decisão'][index]}<input className="block rounded border p-2" type={key==='presentedOn'||key==='decisionOn'?'date':'text'} placeholder={key==='periodFrom'||key==='periodTo'?'AAAA-MM ou AAAA-MM-DD, conforme a decisão':undefined} value={request[key]} onChange={e=>edit({[key]:e.target.value})}/></label>)}
|
||||
<label>Operação abrangida pela decisão<select className="block rounded border p-2" value={request.operation} onChange={e=>edit({operation:e.target.value as OfferRequest['operation']})}><option value="">Por indicar</option><option value="acquisition">Aquisição</option><option value="import">Importação</option></select></label>
|
||||
<button type="button" className="underline" onClick={()=>edit({goods:request.goods||row.description,operation:request.operation||row.operation,periodFrom:request.periodFrom||row.period,periodTo:request.periodTo||row.period})}>Reutilizar contexto nos campos de âmbito vazios</button>{' '}
|
||||
<button type="button" className="rounded border p-2" onClick={()=>run(()=>save({...book,rows:book.rows.map(item=>item.id===row.id?recordOfferRequest(item):item)}))}>Registar estado declarado e âmbito</button>
|
||||
{(row.requestRecords??[]).map((record,index)=><p key={index}>{offerRequestLabels[record.request.state]} — {record.recordedAt}; bens: {record.request.goods||'por indicar'}; operação: {record.request.operation||'por indicar'}; período: {record.request.periodFrom||'por indicar'} a {record.request.periodTo||'por indicar'}; condições: {record.request.conditions||'por indicar'}; decisão: {record.request.decisionRef||'por indicar'}. Este âmbito não se estende automaticamente à restante operação ou oportunidade.</p>)}
|
||||
</fieldset>;})()}
|
||||
<fieldset className="rounded border p-3"><legend>Cenário normal e cenário condicionado à isenção</legend>
|
||||
<label>IVA no cenário normal, indicado em documento ou pressuposto<input className="block rounded border p-2" inputMode="decimal" value={row.normalVat??''} onChange={e=>change(row.id,{normalVat:e.target.value})}/></label>
|
||||
<label>Documento ou pressuposto do IVA normal<input className="block rounded border p-2" value={row.normalVatSource??''} onChange={e=>change(row.id,{normalVatSource:e.target.value})}/></label>
|
||||
<label>Assumir, apenas para comparar, a hipótese condicionada de isenção<select className="block rounded border p-2" value={row.conditionalAssumption??''} onChange={e=>change(row.id,{conditionalAssumption:e.target.value as ''|'yes'|'no'})}><option value="">Por indicar</option><option value="yes">Sim, como hipótese condicionada</option><option value="no">Não</option></select></label>
|
||||
{(()=>{const scenarios=offerScenarios(row);return <><table><thead><tr><th>Cenário</th><th>IVA ({row.currency})</th></tr></thead><tbody><tr><td>Normal — valor indicado, sem taxa presumida</td><td>{scenarios.normalVat??'Indeterminado'}</td></tr><tr><td>Condicionado à isenção — hipótese, não concessão</td><td>{scenarios.conditionalVat??'Indeterminado'}</td></tr></tbody></table><p>{scenarios.conditionalReason}</p><p>O cenário condicionado não altera imposto, dedução, totais ou o estado do pedido. O procedimento só será aplicado quando a correspondência regulamentar estiver demonstrada.</p></>;})()}
|
||||
</fieldset>
|
||||
</details>)}
|
||||
{!!book.rows.length&&<><button type="button" className="rounded border p-2" onClick={()=>run(()=>save(recordOfferPreparations(book)))}>Guardar versão dos rascunhos de ofertas</button><p role="status">{book.saved?'Versão de preparação conservada; alterações seguintes não substituem essa versão.':'Preparação em curso; pode guardar mesmo com informação em falta.'} Benefício não aplicado. {book.history?.length??0} versões anteriores.</p></>}
|
||||
</details>;
|
||||
|
||||
@@ -3,7 +3,9 @@ import { importExemptionBook } from './iva-import-exemptions.ts';
|
||||
|
||||
type Choice = ''|'yes'|'no';
|
||||
export const offerPreparationSource='CIVA,Lei13/2016,14(2)(a/b) e14(3),PDF13/p.1426(271). Preparação de dados; procedimento regulamentar de aplicação ainda em pesquisa. Não concede isenção nem preenche dedução/campo11.';
|
||||
export type OfferPreparation = {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}};
|
||||
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};
|
||||
export const offerRequestLabels = {preparation:'Em preparação',presented:'Apresentado',granted:'Deferido',denied:'Indeferido'};
|
||||
export type OfferPreparation = {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:''};}
|
||||
@@ -13,7 +15,11 @@ function read(draft:Record<string,unknown>):Store {
|
||||
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.');
|
||||
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.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;
|
||||
}
|
||||
@@ -41,7 +47,29 @@ export function reuseOfferData(row:OfferPreparation,source:OfferReusable):OfferP
|
||||
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{
|
||||
if(row.branch!=='P084')throw new Error('Os estados deste pedido pertencem ao percurso P084; emergência conservada no percurso próprio.');
|
||||
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;};
|
||||
if(['granted','denied'].includes(request.state)&&(!request.goods.trim()||!['acquisition','import'].includes(request.operation)||!validPeriod(request.periodFrom)||!validPeriod(request.periodTo)||request.periodFrom.length!==request.periodTo.length||request.periodTo<request.periodFrom||!request.conditions.trim()))throw new Error('Conserve o âmbito da decisão: bens, operação, início/fim do período e condições. Use meses ou datas exactas com a mesma precisão. 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:false}{
|
||||
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};
|
||||
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.'};
|
||||
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);
|
||||
const scope=(r:OfferRequest)=>`bens ${r.goods||'por indicar'}; operação ${r.operation||'por indicar'}; período ${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'}; 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'}; sem cálculo de IVA,isenção automática ou dedução.`]);}
|
||||
try{return offerPreparationBook(draft).rows.map(row=>['IVA mensal',`Rascunho de oferta ${row.branch} — ${row.id}`,`${row.description||'Bens por indicar'}; 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.']];}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user