diff --git a/app/Http/Controllers/Api/V1/SimulationOfferDocumentController.php b/app/Http/Controllers/Api/V1/SimulationOfferDocumentController.php new file mode 100644 index 00000000..dc2176c0 --- /dev/null +++ b/app/Http/Controllers/Api/V1/SimulationOfferDocumentController.php @@ -0,0 +1,70 @@ +find($id, $request); + // Same private document infrastructure and technical file limit as investment attachments. + $request->validate(['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,image/png,image/jpeg', 'max:10240']]); + $file = $request->file('file'); + $hash = hash_file('sha256', $file->getRealPath()); + $existing = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->where('sha256', $hash)->whereNull('deleted_at')->first(); + if ($existing) { + return response()->json(['data' => $this->metadata($id, $existing)], 200)->header('Cache-Control', 'private, no-store'); + } + $documentId = (string) Str::uuid(); + $key = $file->storeAs('simulation-offers/'.$id, $documentId, 'local'); + abort_unless(is_string($key), 500); + try { + DB::table('documents')->insert(['id' => $documentId, 'source_system' => 'simulation-fiscal-offer', 'external_id' => $id, + 'file_name' => basename($file->getClientOriginalName()), 'media_type' => $file->getMimeType(), 'storage_key' => $key, + 'sha256' => $hash, 'size_bytes' => $file->getSize(), 'uploaded_by' => $request->user()->id, + 'status' => 'quarantined', 'data_classification' => 'confidential']); + } catch (\Throwable $e) { + Storage::disk('local')->delete($key); + throw $e; + } + ScanDocumentForMalware::dispatch($documentId)->afterCommit(); + return response()->json(['data' => $this->metadata($id, DB::table('documents')->where('id', $documentId)->first())], 201)->header('Cache-Control', 'private, no-store'); + } + + public function index(Request $request, string $id, SimulationFormDrafts $drafts) + { + abort_unless(Str::isUuid($id), 404); + $drafts->find($id, $request); + $documents = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->get(); + return response()->json(['data' => $documents->map(fn ($doc) => $this->metadata($id, $doc))])->header('Cache-Control', 'private, no-store'); + } + + public function download(Request $request, string $id, string $document, SimulationFormDrafts $drafts) + { + abort_unless(Str::isUuid($id) && Str::isUuid($document), 404); + $drafts->find($id, $request); + $doc = DB::table('documents')->where('id', $document)->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->first(); + abort_unless($doc, 404); + abort_unless($doc->status === 'active' && $doc->malware_scan_status === 'clean' && hash_equals(trim($doc->sha256), (string) $doc->malware_scanned_sha256), 409, 'Anexo conservado; descarga disponível após análise antimalware concluída.'); + $root = realpath(Storage::disk('local')->path('simulation-offers/'.$id)); + $path = realpath(Storage::disk('local')->path($doc->storage_key)); + abort_unless($root && $path && str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR)) && is_file($path), 404); + abort_unless(hash_equals(trim($doc->sha256), hash_file('sha256', $path)), 409, 'Conteúdo do anexo diverge do hash registado.'); + return response()->download($path, basename($doc->file_name), ['Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff']); + } + + private function metadata(string $id, object $doc): array + { + return ['id' => $doc->id, 'draftId' => $id, 'name' => $doc->file_name, 'sha256' => trim($doc->sha256), + 'scan' => $doc->malware_scan_status, 'url' => '/api/v1/simulation-drafts/'.$id.'/offer-documents/'.$doc->id]; + } +} diff --git a/app/Http/Middleware/EnsureSimulationRecoveryCookie.php b/app/Http/Middleware/EnsureSimulationRecoveryCookie.php index a5a0faa8..002491d5 100644 --- a/app/Http/Middleware/EnsureSimulationRecoveryCookie.php +++ b/app/Http/Middleware/EnsureSimulationRecoveryCookie.php @@ -53,7 +53,7 @@ final class EnsureSimulationRecoveryCookie } } - return $response->withCookie(cookie( + $response->headers->setCookie(cookie( self::NAME, $forget ? '' : $token, $minutes, @@ -64,6 +64,7 @@ final class EnsureSimulationRecoveryCookie false, 'lax', )); + return $response; } private function isExplicitDraftActivity(Request $request, Response $response): bool diff --git a/resources/js/components/simulation-currency-events.tsx b/resources/js/components/simulation-currency-events.tsx index 516f5e39..7042e942 100644 --- a/resources/js/components/simulation-currency-events.tsx +++ b/resources/js/components/simulation-currency-events.tsx @@ -1,10 +1,11 @@ +import {SimulationFxSelection} from './simulation-fx-selection'; import {useState} from 'react'; import {currencyEventBook,currencyEventCurrent,currencyEventReportRows,currencyEventSource,currencyEventTreatments,newCurrencyEvent,recordCurrencyEvents,saveCurrencyEventBook,switchCurrencyEvent,type CurrencyEventBook,type CurrencyEventRow} from '@/lib/iva-currency-events'; export function SimulationCurrencyEvents({draft,onChange}:{draft:Record;onChange:(field:string,value:string)=>void}){ const[error,setError]=useState('');let book:CurrencyEventBook;try{book=currencyEventBook(draft);}catch(e){return

{String(e)}

;} const save=(next:CurrencyEventBook)=>{try{onChange('modelo_a_snapshot',saveCurrencyEventBook(draft,next));setError('');}catch(e){setError(String(e));}}; const change=(id:string,patch:Partial)=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)}); - return
Moeda e datas do facto gerador/exigibilidade

Facto gerador e exigibilidade são estados separados. Identifique a operação já preparada,sem repetir os seus cálculos. Conversão P104 é apenas cenário condicionado,enquanto a regra fiscal permanece por corresponder.

{currencyEventSource}

{error&&

{error}

} + return
Moeda e datas do facto gerador/exigibilidade

Facto gerador e exigibilidade são estados separados. Identifique a operação já preparada,sem repetir os seus cálculos. P104 permite a selecção operacional definida pela Tempo;os cenários antigos permanecem condicionados até selecção explícita,sem recálculo de históricos.

{currencyEventSource}

{error&&

{error}

} {book.rows.map(row=>
{row.description||'Operação por identificar'} — {row.period||'mês por indicar'} @@ -12,8 +13,9 @@ export function SimulationCurrencyEvents({draft,onChange}:{draft:Record)} {row.treatment==='foreign_scenario'?<> + change(row.id,patch)}/> {([['sourceCurrency','Moeda original'],['fxRate','Taxa de conversão indicada'],['fxDate','Data efectiva da taxa'],['fxSource','Fonte e exemplar da taxa'],['fxRuleRef','Regra/pressuposto da escolha da taxa e data']] as const).map(([field,label])=>)} -

O valor convertido é uma simulação condicionada. Não substitui a base fiscal,nem altera os valores/moedas dos livros anteriores.

+

{row.fxSelection?.applied?'Conversão operacional após selecção explícita da taxa.':'Cenário histórico condicionado;selecção operacional ainda não aplicada.'} Não activa base fiscal,IVA ou dedução,nem altera os livros anteriores.

:row.treatment&&<> {([['eventDate','Data própria do evento escolhido'],['eventRef','Referência e factos do evento7/8'],['reviewDate','Data até à qual os eventos foram conferidos']] as const).filter(([field])=>field!=='eventDate'||!['advance_payment','consignment_deadline'].includes(row.treatment)).map(([field,label])=>)} {['consignment_deadline','service_early_charge','contract_effects'].includes(row.treatment)&&} @@ -34,6 +36,6 @@ export function SimulationCurrencyEvents({draft,onChange}:{draft:Record}

Trocar percurso conserva dados ocultos e referências;reveja as confirmações aplicáveis.

)} {!!book.rows.length&&} - {book.result&&<>

{currencyEventCurrent(draft,book)?'Resultados correspondentes às entradas guardadas.':'Entradas alteradas;histórico conservado.'} {book.history?.length??0} versões anteriores.P104condicionado,semIVA/base/dedução activados.

{currencyEventReportRows(draft).map(([,label,value,origin],i)=>
{label}
{value}{origin}
)}
} + {book.result&&<>

{currencyEventCurrent(draft,book)?'Resultados correspondentes às entradas guardadas.':'Entradas alteradas;histórico conservado.'} {book.history?.length??0} versões anteriores.P104 conserva a natureza operacional ou condicionada indicada em cada resultado;semIVA/base/dedução activados.

{currencyEventReportRows(draft).map(([,label,value,origin],i)=>
{label}
{value}{label.startsWith('P104')?
Fonte,âmbito e versão{origin}
:{origin}}
)}
}
; } diff --git a/resources/js/components/simulation-fx-selection.tsx b/resources/js/components/simulation-fx-selection.tsx new file mode 100644 index 00000000..42dae608 --- /dev/null +++ b/resources/js/components/simulation-fx-selection.tsx @@ -0,0 +1,21 @@ +import {useState} from 'react'; +import type {CurrencyEventRow} from '@/lib/iva-currency-events'; +import {emptyFxSelection,selectFx,fxTempoSource,type FxSelection} from '@/lib/iva-fx-selection'; +const columnLabels:Record={buy:'Compra',sell:'Venda',mean:'Média de referência BM',exception:'Excepção fiscal identificada'}; +const directionLabels:Record={mzn_per_unit:'MZN por unidade da moeda original',units_per_mzn:'unidades da moeda original por MZN'}; +export function SimulationFxSelection({row,onChange}:{row:CurrencyEventRow;onChange:(patch:Partial)=>void}){ + const[error,setError]=useState(''),[preview,setPreview]=useState|null>(null);const s=row.fxSelection??{...emptyFxSelection(),sourceCurrency:row.sourceCurrency,direction:row.fxDirection}; + const edit=(patch:Partial)=>{setPreview(null);onChange({fxSelection:{...s,...patch}});}; + const text=(key:'submissionDate'|'bulletinDate'|'bulletinRef'|'source'|'sourceCurrency'|'bulletinPerspective'|'buy'|'sell'|'mean'|'exceptionRef'|'exceptionScope'|'exceptionDate'|'exceptionRate',label:string)=>; + return
P104 — selecção da taxa segundo orientação da Tempo

{fxTempoSource}

+ + {text('submissionDate','Data de submissão indicada')}{text('sourceCurrency','Moeda original já declarada')}{text('bulletinRef','Boletim,edição e série de referência')}{text('source','Fonte do boletim;URL do Banco de Moçambique para operações comerciais')}{text('bulletinDate','Data do boletim')} + + + + {s.mode==='exception'?<>{text('exceptionRef','Disposição fiscal que impõe outra data/taxa')}{text('exceptionScope','Âmbito da excepção')}{text('exceptionDate','Data imposta pela disposição')}{text('exceptionRate','Taxa da excepção,com fonte no boletim indicado')}:s.nature==='commercial'?text('mean','Coluna Média da série de referênciaBM;sem calcular média própria'):<>{text('bulletinPerspective','Perspectiva de compra/venda indicada no boletim')} + {text('buy','Taxa da coluna Compra do boletim')}{text('sell','Taxa da coluna Venda do boletim')}} + {error&&

{error}

} + {preview&&

Coluna:{columnLabels[preview.column]};taxa:{preview.rate};data:{preview.date};sentido:{directionLabels[s.direction]}. Confirmação substitui os campos cambiais desta linha:{row.fxRate||'taxa vazia'} → {preview.rate};{row.fxDate||'data vazia'} → {preview.date};{row.sourceCurrency||'moeda vazia'} → {s.sourceCurrency}. O resultado e os históricos permanecem até usar «Conferir moeda/eventos e guardar versão».

} +

Não usar taxa de apresentação nem a mais recente como substituto de boletim ausente. Compra/venda são interpretadas pela perspectiva indicada;relação desconhecida fica por esclarecer. Alterações não recalculam versões anteriores.

; +} diff --git a/resources/js/components/simulation-monthly-iva.tsx b/resources/js/components/simulation-monthly-iva.tsx index 304e53b5..29ac5f93 100644 --- a/resources/js/components/simulation-monthly-iva.tsx +++ b/resources/js/components/simulation-monthly-iva.tsx @@ -1,5 +1,6 @@ import { SimulationObjectiveImports } from './simulation-objective-imports'; import { SimulationMiners } from './simulation-miners'; +import { SimulationReturnees } from './simulation-returnees'; import { SimulationDigital } from './simulation-digital'; import { SimulationTravel } from './simulation-travel'; import { SimulationTravelEvents } from './simulation-travel-events'; @@ -117,6 +118,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: { + diff --git a/resources/js/components/simulation-offer-preparations.tsx b/resources/js/components/simulation-offer-preparations.tsx index 60ec755e..6b27834e 100644 --- a/resources/js/components/simulation-offer-preparations.tsx +++ b/resources/js/components/simulation-offer-preparations.tsx @@ -1,3 +1,4 @@ +import {SimulationOfferWorkflow} from './simulation-offer-workflow'; import { useState } from 'react'; import { newOfferPreparation,offerPreparationBook,offerPreparationSource,offerReusableData,recordOfferPreparations,reuseOfferData,saveOfferPreparationBook,emptyOfferRequest,recordOfferRequest,offerRequestLabels,offerScenarios,type OfferRequest,type OfferPreparation,type OfferPreparationBook } from '@/lib/iva-offer-preparations'; @@ -10,7 +11,7 @@ export function SimulationOfferPreparations({draft,onChange}:{draft:Record; return
Ofertas a instituições e para calamidades — preparar rascunhos

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.

-

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.

+

Procedimento operacional indicado pela Tempo:prepare o pedido escrito,registe a apresentação externa,a decisão com despacho e o formulário aplicável. Antes do deferimento,compare o cenário normal com uma hipótese condicionada. 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.

{error&&

{error}

} {' '} @@ -24,7 +25,7 @@ export function SimulationOfferPreparations({draft,onChange}:{draft:RecordFinalidade humanitária da resposta à emergência