diff --git a/resources/js/components/simulation-offer-preparations.tsx b/resources/js/components/simulation-offer-preparations.tsx index f768ccac..60ec755e 100644 --- a/resources/js/components/simulation-offer-preparations.tsx +++ b/resources/js/components/simulation-offer-preparations.tsx @@ -29,9 +29,11 @@ export function SimulationOfferPreparations({draft,onChange}:{draft:RecordEstado declarado do pedido {(['reference','presentedOn','decisionRef','decisionOn','goods','periodFrom','periodTo','conditions'] as const).map((key,index)=>)} + + {' '} - {(row.requestRecords??[]).map((record,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.

)} + {(row.requestRecords??[]).map((record,index)=>

{offerRequestLabels[record.request.state]} — {record.recordedAt}; bens: {record.request.goods||'por indicar'}; operação: {record.request.operationScope||record.request.operation||'por indicar'}; período: {record.request.periodScope||`${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.

)} ;})()}
Cenário normal e cenário condicionado à isenção diff --git a/resources/js/lib/iva-offer-preparations.ts b/resources/js/lib/iva-offer-preparations.ts index 82ae93d7..835a472f 100644 --- a/resources/js/lib/iva-offer-preparations.ts +++ b/resources/js/lib/iva-offer-preparations.ts @@ -3,7 +3,7 @@ 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 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 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 = {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[]}}; @@ -52,7 +52,8 @@ 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); + 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:false}{ @@ -66,7 +67,7 @@ export function offerScenarios(row:OfferPreparation):{normalVat:string|null;cond } 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'}`; + 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]>{ diff --git a/tests/frontend/iva-offer-paths.test.mjs b/tests/frontend/iva-offer-paths.test.mjs index a20e18aa..0141a96c 100644 --- a/tests/frontend/iva-offer-paths.test.mjs +++ b/tests/frontend/iva-offer-paths.test.mjs @@ -19,6 +19,7 @@ test('P084 recorded decisions preserve goods/operation/period/conditions and pre row={...row,description:'Bens B posteriores',operation:'acquisition',period:'2027-01',request:{...row.request,state:'denied',decisionRef:'SYNTHETIC QA decisão B',goods:'Bens B',periodFrom:'2027-01',periodTo:'2027-01',operation:'acquisition',conditions:'SYNTHETIC QA apenas pedido B'}};row=recordOfferRequest(row);assert.deepEqual(row.requestRecords[0],accepted);assert.equal(row.requestRecords.length,2);assert.equal(offerScenarios(row).applied,false);assert.equal(offerScenarios(row).conditionalVat,null);assert.match(offerPathReport(row),/não estender decisão/); for(const key of ['goods','operation','periodFrom','periodTo','conditions'])assert.throws(()=>recordOfferRequest({...row,request:{...row.request,[key]:''}}),/âmbito da decisão/); const exact=recordOfferRequest({...row,request:{...row.request,periodFrom:'2027-01-10',periodTo:'2027-01-15'}});assert.equal(exact.requestRecords.at(-1).request.periodFrom,'2027-01-10');assert.equal(exact.requestRecords.at(-1).request.periodTo,'2027-01-15');assert.throws(()=>recordOfferRequest({...row,request:{...row.request,periodFrom:'2027-02-30',periodTo:'2027-03-01'}}),/âmbito da decisão/); + const literal=recordOfferRequest({...row,request:{...row.request,operation:'',operationScope:'SYNTHETIC QA aquisição e importação dos bens A',periodFrom:'',periodTo:'',periodScope:'SYNTHETIC QA desde a decisão até revogação, sem termo'}});assert.match(offerPathReport(literal),/até revogação, sem termo/);assert.equal(literal.requestRecords.at(-1).request.operationScope,'SYNTHETIC QA aquisição e importação dos bens A'); assert.equal(row.requestRecords[0].request.operation,'import');assert.equal(row.operation,'acquisition'); }); test('P085 humanitarian purpose/emergency/documents are distinct;missing inputs stay indeterminate and no P084 state is imposed',()=>{