commit melhorias1
This commit is contained in:
@@ -7,22 +7,23 @@ final class FiscalCoverage
|
||||
public static function describe(): array
|
||||
{
|
||||
return [
|
||||
'version'=>'SGF-FISCAL-COVERAGE-v2',
|
||||
'version'=>'SGF-FISCAL-COVERAGE-v4',
|
||||
'automatic_knowledge_import'=>false,
|
||||
'legal_validation'=>false,
|
||||
'notice'=>'Conhecimento em documentos ou conversas não passa automaticamente para a aplicação. Cada regra exige fonte, versão, âmbito, provas e revisão antes de utilização.',
|
||||
'taxes'=>[
|
||||
['id'=>'iva','label'=>'IVA','state'=>'partial','available'=>'Cálculo delimitado de serviço, MZN, uma linha e imposto acrescido; perfis versionados, revisão distinta e decisão temporal. Documentos QA e diário integrado; livro/mapa interno da origem seleccionada revistos distintamente. Núcleo comum com versões/vigência e cálculo interno QA de1–25linhas, preço acrescido/incluído, exactos e diferenças preservados.','pending'=>'Emissão documental por múltiplas linhas/tratamentos, livros gerais de apuramento/dedução, regularizações e modelos oficiais.','legal_use'=>false],
|
||||
['id'=>'irps','label'=>'IRPS','state'=>'pending','available'=>null,'pending'=>'Motor operacional de rendimentos/retenções, remunerações, apuramento e modelos ligados às operações do SGF.','legal_use'=>false],
|
||||
['id'=>'irps','label'=>'IRPS','state'=>'partial','available'=>'RH autónomo com cadastro, contratos/tempos/férias e processamento interno QA revisto; retenção artificial explicada e mapa conservado.','pending'=>'Cálculo legal de IRPS por regime/rendimento, retenções/apuramento e modelos oficiais; o ensaio interno não valida o imposto.','legal_use'=>false],
|
||||
['id'=>'irpc','label'=>'IRPC','state'=>'pending','available'=>null,'pending'=>'Ponte do resultado contabilístico para ajustamentos/base/colecta, retenções e modelos no SGF.','legal_use'=>false],
|
||||
['id'=>'ispc','label'=>'ISPC','state'=>'pending','available'=>null,'pending'=>'Enquadramento, cálculo, obrigações e modelos operacionais no SGF.','legal_use'=>false],
|
||||
['id'=>'ice','label'=>'ICE','state'=>'pending','available'=>null,'pending'=>'Classificação dos bens, parâmetros com vigência, liquidação e obrigações operacionais no SGF.','legal_use'=>false],
|
||||
['id'=>'inss','label'=>'INSS','state'=>'pending','available'=>null,'pending'=>'Cadastro/processamento de remunerações, contribuições e modelos no SGF.','legal_use'=>false],
|
||||
['id'=>'inss','label'=>'INSS','state'=>'partial','available'=>'Cadastro e processamento interno QA em RH, com desconto do trabalhador e encargo patronal artificiais separados e mapa revisto.','pending'=>'Regimes/bases/contribuições legais INSS, reconciliação e modelos oficiais; percentagens do exercício não são parâmetros legais activados.','legal_use'=>false],
|
||||
['id'=>'other','label'=>'Outros impostos e obrigações','state'=>'pending','available'=>null,'pending'=>'Identificação dos tributos aplicáveis, calendário legal, multas/juros e modelos; sem taxas ou prazos presumidos.','legal_use'=>false],
|
||||
],
|
||||
'management'=>[
|
||||
['id'=>'human_resources','label'=>'Recursos Humanos','state'=>'partial','available'=>'Módulo autónomo e perfis próprios: cadastro, contratos internos, plano/assiduidade, condições/pedidos/gozo de férias, conferência de período e remunerações internas QA; histórico e resumos legíveis.','pending'=>'Aplicabilidade laboral real, calendário/turnos gerais, benefícios, cálculo legal IRPS/INSS e efeitos salariais financeiros/contabilísticos.'],
|
||||
['id'=>'finance','label'=>'Gestão financeira','state'=>'partial','available'=>'Percursos QA de orçamento de Compras, obrigações/recebíveis, afectações/reversões e Caixa com fecho; conferência individual de extracto artificial contra duas linhas existentes.','pending'=>'Operação geral real, bancos e reconciliação bancária geral.'],
|
||||
['id'=>'accounting','label'=>'Contabilidade','state'=>'partial','available'=>'Diário, plano, razão, balancete e conferência/fecho QA com origens distintas.','pending'=>'Demonstrações financeiras legais, encerramento anual real e integração dos apuramentos fiscais.'],
|
||||
['id'=>'accounting','label'=>'Contabilidade','state'=>'partial','available'=>'Diário, plano, razão, balancete e conferência/fecho QA; demonstrações internas parciais revistas/conservadas, com contas não classificadas explícitas. Cadastro/custódia, plano mensal, quota e diário de activos QA integrados, com correcção e abate no recorte sintético.','pending'=>'Operação geral de activos e tratamento fiscal de depreciações, demonstrações financeiras legais, encerramento anual real e integração dos apuramentos fiscais.'],
|
||||
['id'=>'analysis','label'=>'Análise financeira','state'=>'partial','available'=>'Custos directos/industriais, orçamento versus realizado, diário/saldos e dois critérios/indicadores QA revistos com fontes explícitas e relatórios.','pending'=>'Rácios de liquidez/rentabilidade/endividamento e tendências conciliadas, com plano de contas e critérios aprovados.'],
|
||||
],
|
||||
'rule_lifecycle'=>['Fonte identificada e versão preservada','Aplicabilidade e vigência explícitas','Cálculo exacto com pressupostos/pendências','Provas e revisão por pessoa distinta','Activação específica e histórico sem reescrever operações anteriores'],
|
||||
|
||||
@@ -94,6 +94,15 @@ final class FiscalRules
|
||||
$gate=$this->evaluate($s,['base'=>'0','currency'=>$in['currency'],'date'=>$in['date']],$selected);if($gate['state']!=='determined_qa'){unset($gate['base']);return $gate+['policy'=>FiscalDocumentMath::POLICY,'price_mode'=>$in['price_mode'],'base_document'=>null,'lines'=>[]];}
|
||||
return FiscalDocumentMath::calculate($in['lines'],$in['price_mode'],$selected['definition']['parameters']['rate'])+['currency'=>$in['currency'],'rule'=>$gate['rule']];
|
||||
}
|
||||
/** Explicit read-only QA bridge; does not change the module required by rule administration. */
|
||||
public function invoiceDocument(int $actor,array $scope,int $event,array $input,?int $boundCalculation=null):array
|
||||
{
|
||||
abort_unless(config('multiline_invoices.enabled')===true&&config('fiscal_rules_qa.enabled')===true,404);CompositionRuntime::assert();$actual=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$actual===$scope&&ModuleRecords::active()==='commercial',403);if(!$this->permissions->has($actor,$scope['tenant_id'],$scope['company_id'],'fiscal_rules.view',$scope['unit_id'])){
|
||||
// Invoice readers cannot enumerate/administer the rule catalogue. Only revalidate
|
||||
// a calculation already captured for this exact draft/input by a fiscal preparer.
|
||||
$this->permissions->require($actor,$scope['tenant_id'],$scope['company_id'],'billing_drafts.view',$scope['unit_id']);$row=$boundCalculation?DB::table('mi_calculations')->where($scope)->where('id',$boundCalculation)->first():null;abort_unless($row,403,'Regra exige captura documental previamente autorizada.');$frozen=json_decode($row->snapshot,true,64,JSON_THROW_ON_ERROR);$expected=$frozen['common']['input'];unset($expected['rule_event_id']);abort_unless((int)$frozen['common']['rule']['id']===$event&&$this->hash($expected)===$this->hash($input),409,'Entrada/data da captura fiscal mudou.');
|
||||
}$input['rule_event_id']=$event;$in=$this->documentInput($input);$row=$this->events($scope)->where('id',$event)->first();abort_unless($row,403,'Regra fora do contexto.');$selected=$this->row($row);$this->fixture();return ['rule'=>$selected,'result'=>$this->evaluateDocument($scope,$in,$selected),'input'=>$in,'rule_stream'=>['count'=>$this->events($scope)->count(),'last_id'=>$this->events($scope)->max('id')]];
|
||||
}
|
||||
public function documentReport(int $actor,int $id):array
|
||||
{
|
||||
$detail=$this->detail($actor,$id,'calculation');$r=$detail['result'];abort_unless(isset($r['price_mode'],$r['lines']),422,'Este registo não é cálculo por linhas.');$fp=fopen('php://temp','w+');$rule=$r['rule']??[];$rows=[['MAPA INTERNO POR LINHAS QA — SEM VALIDADE FISCAL'],['Politica',$r['policy']],['Modo',$r['price_mode']],['Data',$detail['input']['date']],['Moeda',$detail['input']['currency']],['Regra escolhida',(string)($detail['input']['rule_event_id']??'INDETERMINADA')],['Versao regra',(string)($rule['version']??'INDETERMINADA')],['Fonte',$rule['source_reference']??'INDETERMINADA'],['SHA fonte',$rule['source_sha256']??'INDETERMINADO'],['Estado historico',$r['state']],['Estado actual',$detail['current_evaluation']['state']],['Linha','Referencia','Base exacta fraccao','Imposto exacto fraccao','Base documental','Imposto documental','Total','Diferenca base fraccao','Diferenca imposto fraccao']];foreach($r['lines']as$line){$ref="'".$line['reference'];$rows[]=[(string)$line['line'],$ref,$line['base_exact_fraction'],$line['tax_exact_fraction'],$line['base_document']??'INDETERMINADO',$line['tax_document'],$line['total_document']??'INDETERMINADO',$line['base_difference_fraction']??'INDETERMINADO',$line['tax_difference_fraction']];}$rows[]=['Totais documentais','','','',$r['base_document']??'INDETERMINADO',$r['tax_document']??'INDETERMINADO',$r['total']??'INDETERMINADO'];foreach($rows as$row)fputcsv($fp,$row,';','"','',"\r\n");rewind($fp);$bytes=stream_get_contents($fp);$s=$this->scope($actor);Audit::record('fiscal_rule.document_report',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'version'=>0,'review_hash'=>hash('sha256',$bytes)]);return ['bytes'=>$bytes,'sha256'=>hash('sha256',$bytes)];
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace App\Modules\Services\Http;
|
||||
use App\Modules\Services\{MultiLineInvoiceCalculation,MultiLineDrafts,ServiceWorkflow,ServiceInvoicing,DocumentPreparation,FiscalRules};
|
||||
use App\Shared\Context\ModuleRecords;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\{DB,Validator};
|
||||
final class MultiLineInvoiceController
|
||||
{
|
||||
private function response(\Closure $fn){try{$value=$fn();return $value instanceof \Symfony\Component\HttpFoundation\Response?$value:response()->json($value)->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException$e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw$e;return response()->json(['message'=>'Documento ocupado. Recupere a mesma operação, sem gerar outra chave.'],503)->header('Retry-After','5')->header('Cache-Control','no-store');}}
|
||||
private function exactQuery(Request $r,array $rules):array{abort_if(array_diff(array_keys($r->query()),array_keys($rules)),422);return Validator::make($r->query(),$rules)->validate();}
|
||||
public function catalogue(Request $r,MultiLineInvoiceCalculation $mi){return$this->response(function()use($r,$mi){$s=$mi->scope($r->user()->id,'unit.view');$d=$this->exactQuery($r,['kind'=>'required|in:clients,services,drafts,rules,series,temporals,calculations','draft_id'=>'sometimes|integer|min:1','search'=>'sometimes|nullable|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);$kind=$d['kind'];$q=null;
|
||||
if(in_array($kind,['clients','services','drafts'],true)){$area=$kind==='drafts'?'billing_drafts':$kind;app(ServiceWorkflow::class)->scope($area,$r->user()->id,'view');$q=ModuleRecords::query('a_'.$area)->where('tenant_id',$s['tenant_id'])->where('company_id',$s['company_id'])->where('unit_id',$kind==='drafts'?$s['unit_id']:null);if($kind==='drafts')$q->whereIn('id',DB::table('mi_drafts')->where($s)->select('draft_id'));if(!empty($d['search'])){$v='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->whereRaw("name ILIKE ? ESCAPE '!'",[$v]);}$columns=['id','name','version'];}
|
||||
elseif($kind==='rules'){$s=$mi->scope($r->user()->id,'fiscal_rules.view');$q=DB::table('fr_events as f')->where(['f.tenant_id'=>$s['tenant_id'],'f.company_id'=>$s['company_id'],'f.unit_id'=>$s['unit_id'],'f.state'=>'reviewed','f.definition->family'=>'qa','f.definition->source_status'=>'qa_checked'])->whereNotExists(function($n){$n->selectRaw('1')->from('fr_events as n')->whereColumn('n.tenant_id','f.tenant_id')->whereColumn('n.company_id','f.company_id')->whereColumn('n.unit_id','f.unit_id')->whereColumn('n.rule_key','f.rule_key')->whereColumn('n.rule_version','f.rule_version')->whereColumn('n.revision','>','f.revision');});$columns=['f.id','f.rule_key','f.rule_version','f.definition'];}
|
||||
elseif($kind==='series'||$kind==='temporals'){$mi->scope($r->user()->id,'service_invoices.view');$q=DB::table('si_proposals')->where($s)->where('kind',$kind==='series'?'series':'temporal');if($kind==='series')$q->where('identifier','like','TEST-MULTI-%');else{abort_unless(isset($d['draft_id']),422);app(ServiceWorkflow::class)->show('billing_drafts',(int)$d['draft_id'],$r->user()->id);$q->where('data->draft_id',(int)$d['draft_id'])->where('data->calculation_policy',MultiLineInvoiceCalculation::POLICY);}$columns=['id','identifier','version','kind','actor_id','data','created_at'];}
|
||||
else{abort_unless(isset($d['draft_id']),422);app(ServiceWorkflow::class)->show('billing_drafts',(int)$d['draft_id'],$r->user()->id);$q=DB::table('mi_calculations')->where($s)->where('draft_id',(int)$d['draft_id']);$columns=['id','actor_id','snapshot','created_at'];}
|
||||
if(!empty($d['search'])&&in_array($kind,['rules','series','temporals','calculations'],true)){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$column=match($kind){'rules'=>"f.definition->>'label'",'calculations'=>'id::text',default=>'identifier'};$q->whereRaw($column." ILIKE ? ESCAPE '!'",[$needle]);}
|
||||
$page=$q->orderByDesc($kind==='rules'?'f.id':'id')->paginate((int)($d['per_page']??25),$columns,'page',(int)($d['page']??1));$decisions=in_array($kind,['series','temporals'],true)?DB::table('si_decisions')->whereIn('proposal_id',$page->pluck('id')->all())->get(['proposal_id','actor_id','decision','reason'])->keyBy('proposal_id'):collect();$items=[];foreach($page as$row){$v=(array)$row;if($kind==='rules'){$definition=json_decode($v['definition'],true);unset($v['definition']);$v+=['name'=>$definition['label'],'valid_from'=>$definition['valid_from'],'valid_until'=>$definition['valid_until'],'rate'=>$definition['parameters']['rate']];}elseif(in_array($kind,['series','temporals'],true)){$v['data']=json_decode($v['data'],true);$v['decision']=$decisions->get($v['id']);}elseif($kind==='calculations'){$snapshot=json_decode($v['snapshot'],true,64);unset($v['snapshot']);$v+=['name'=>'Cálculo #'.$v['id'],'result'=>$snapshot['common']['result'],'rule_event_id'=>$snapshot['common']['rule']['id'],'draft_version'=>$snapshot['document']['draft_version'],'preparation_version'=>$snapshot['document']['preparation_version'],'date'=>$snapshot['expected_date']];}$items[]=$v;}$page->setCollection(collect($items));return ['kind'=>$kind,'records'=>$page->toArray()];});}
|
||||
public function editor(Request $r,int $id,MultiLineInvoiceCalculation $mi){abort_if($r->query()!==[],422);return$this->response(function()use($r,$id,$mi){$s=$mi->scope($r->user()->id);$binding=DB::table('mi_drafts')->where($s)->where('draft_id',$id)->first();abort_unless($binding,403);$w=app(ServiceWorkflow::class);$billing=$w->show('billing_drafts',$id,$r->user()->id);$source=$w->show('service_drafts',(int)$binding->source_id,$r->user()->id);$p=app(\App\Modules\Identity\Services\Permissions::class);$prep=$p->has($r->user()->id,$s['tenant_id'],$s['company_id'],'draft_documents.view',$s['unit_id'])?app(DocumentPreparation::class)->show('billing_drafts',$id,$r->user()->id):null;$parties=[];foreach(['issuer'=>$s['company_id'],'client'=>$billing['client_id']]as$kind=>$target){if($p->has($r->user()->id,$s['tenant_id'],$s['company_id'],$kind==='issuer'?'issuer_documents.view':'client_documents.view'))$parties[$kind]=app(DocumentPreparation::class)->show($kind,(int)$target,$r->user()->id);}$invoice=ModuleRecords::query('si_invoices')->where($s)->where('draft_id',$id)->first(['id','series_code','number']);if($invoice&&$p->has($r->user()->id,$s['tenant_id'],$s['company_id'],'service_invoices.view',$s['unit_id']))$invoice->totals=app(ServiceInvoicing::class)->show((int)$invoice->id,$r->user()->id)['snapshot']['calculation']['common_document_result']??null;return['billing'=>$billing,'source'=>$source,'preparation'=>$prep,'parties'=>$parties,'invoice'=>$invoice,'policy'=>MultiLineInvoiceCalculation::POLICY,'server_date'=>now(ServiceInvoicing::ZONE)->format('Y-m-d')];});}
|
||||
public function draftPreview(Request $r,MultiLineDrafts $d){abort_if(strlen(json_encode($r->all()))>65536,422);return$this->response(fn()=>$d->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));}
|
||||
public function draftConfirm(Request $r,MultiLineDrafts $d){abort_if(array_diff(array_keys($r->all()),['preview']),422);$r->validate(['preview'=>'required|string|max:262144']);return$this->response(fn()=>$d->confirm($r->user()->id,$r->input('preview')));}
|
||||
public function calculationPreview(Request $r,MultiLineInvoiceCalculation $c){abort_if(strlen(json_encode($r->all()))>16384,422);return$this->response(fn()=>$c->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));}
|
||||
public function calculationConfirm(Request $r,MultiLineInvoiceCalculation $c){abort_if(array_diff(array_keys($r->all()),['preview']),422);$r->validate(['preview'=>'required|string|max:262144']);return$this->response(fn()=>$c->confirm($r->user()->id,$r->input('preview')));}
|
||||
public function receipt(Request $r,string $key,MultiLineInvoiceCalculation $c){abort_if($r->query()!==[],422);return$this->response(fn()=>$c->receipt($r->user()->id,strtolower($key)));}
|
||||
private function command(Request $r,\Closure $work){return$this->response(fn()=>DB::transaction(function()use($r,$work){DB::statement("SET LOCAL lock_timeout='5s'");app(MultiLineInvoiceCalculation::class)->scope($r->user()->id);return$work();}));}
|
||||
private function administrative(Request $r,string $domain,string $right,?int $target,\Closure $work){return$this->command($r,function()use($r,$domain,$right,$target,$work){$s=app(MultiLineInvoiceCalculation::class)->scope($r->user()->id);app(\App\Modules\Identity\Services\Permissions::class)->require($r->user()->id,$s['tenant_id'],$s['company_id'],$right);$key=strtolower((string)$r->header('Idempotency-Key'));abort_unless(preg_match('/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/D',$key),422);$input=['domain'=>$domain,'target'=>$target,'data'=>$r->all()];$hash=hash('sha256',\Tempo\Finance\Payables\Payables::canonical($input));DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();$s=app(MultiLineInvoiceCalculation::class)->scope($r->user()->id);app(\App\Modules\Identity\Services\Permissions::class)->require($r->user()->id,$s['tenant_id'],$s['company_id'],$right);$q=$s+['actor_id'=>$r->user()->id,'command_key'=>$key];$old=DB::table('mi_commands')->where($q)->first();if($old){abort_unless(hash_equals($old->request_hash,$hash),409,'Chave usada noutra operação.');return json_decode($old->result,true)+['replayed'=>true];}$out=$work();$out['type']=$domain;DB::table('mi_commands')->insert($q+['request_hash'=>$hash,'input'=>\Tempo\Finance\Payables\Payables::canonical($input),'result'=>\Tempo\Finance\Payables\Payables::canonical($out),'created_at'=>now()->utc()]);return$out+['replayed'=>false];});}
|
||||
public function temporal(Request $r){abort_if(($r->input('kind')!=='temporal')||$r->input('data.calculation_policy')!==MultiLineInvoiceCalculation::POLICY,422);return$this->administrative($r,'temporal','service_fiscal.propose',null,fn()=>app(ServiceInvoicing::class)->propose($r->user()->id,$r->all()));}
|
||||
public function temporalReview(Request $r,int $id){return$this->administrative($r,'temporal_review','service_fiscal.approve',$id,function()use($r,$id){$s=app(MultiLineInvoiceCalculation::class)->scope($r->user()->id);abort_unless(DB::table('si_proposals')->where($s)->where('id',$id)->where('data->calculation_policy',MultiLineInvoiceCalculation::POLICY)->exists(),403);return app(ServiceInvoicing::class)->decide($id,$r->user()->id,$r->all());});}
|
||||
public function readiness(Request $r,int $id){abort_if(array_diff(array_keys($r->all()),['series_id','rule_id']),422);$d=$r->validate(['series_id'=>'required|integer|min:1','rule_id'=>'required|integer|min:1']);return$this->command($r,fn()=>app(ServiceInvoicing::class)->review($id,(int)$d['series_id'],(int)$d['rule_id'],$r->user()->id));}
|
||||
public function issue(Request $r,int $id){return$this->command($r,function()use($r,$id){$s=app(MultiLineInvoiceCalculation::class)->scope($r->user()->id);abort_unless(DB::table('mi_drafts')->where($s)->where('draft_id',$id)->exists()&&DB::table('si_proposals')->where($s)->where('id',$r->input('rule_id'))->where('data->calculation_policy',MultiLineInvoiceCalculation::POLICY)->exists(),403);return app(ServiceInvoicing::class)->issue($id,$r->user()->id,$r->all(),(string)$r->header('Idempotency-Key'));});}
|
||||
public function issueReceipt(Request $r,string $key,MultiLineInvoiceCalculation $c){abort_if($r->query()!==[],422);return$this->response(function()use($r,$key,$c){$s=$c->scope($r->user()->id,'service_invoices.view');abort_unless(preg_match('/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/D',$key),422);$row=DB::table('si_commands')->where($s+['actor_id'=>$r->user()->id,'command_key'=>$key])->first();abort_unless($row,404);$result=json_decode($row->result,true);$invoice=app(ServiceInvoicing::class)->show((int)$result['id'],$r->user()->id);abort_unless(($invoice['snapshot']['calculation_policy']??null)===MultiLineInvoiceCalculation::POLICY,404);return ['result'=>$result,'historical_receipt_only'=>true,'operation_executed'=>false];});}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ final class InvoicePdf
|
||||
public const VERSION='sgf-test-invoice-text-v2';
|
||||
public static function lines(array $s): array
|
||||
{
|
||||
if(($s['calculation_policy']??null)===MultiLineInvoiceCalculation::POLICY)return MultiLineInvoicePdf::lines($s);
|
||||
$lines=[['title','Factura de serviço - demonstração isolada'],['text',$s['series_code'].' / '.$s['number']],['text','Emissão: '.$s['issued_local'].' ('.$s['timezone'].')'],['section','Emitente']];
|
||||
foreach(['issuer'=>'Emitente','client'=>'Cliente'] as $key=>$title) {
|
||||
if($key==='client') $lines[]=['section',$title];
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace App\Modules\Services;
|
||||
use Illuminate\Support\Facades\{DB,Crypt,Validator};
|
||||
use Tempo\Finance\Payables\Payables;
|
||||
/** Atomic administrative composition of the existing draft/preparation writers. No invoice writer. */
|
||||
final class MultiLineDrafts
|
||||
{
|
||||
private function hash(array $a):string{return hash('sha256',Payables::canonical($a));}
|
||||
private function input(array $v):array{
|
||||
$rules=['draft_id'=>'present|nullable|integer|min:1','version'=>'required|integer|min:0','source_version'=>'required|integer|min:0','preparation_version'=>'required|integer|min:0','name'=>'required|string|min:3|max:255','client_id'=>'required|integer|min:1','currency'=>'required|in:MZN','price_mode'=>'required|in:undefined,added,included','service_date'=>'present|nullable|date_format:Y-m-d','period_start'=>'present|nullable|date_format:Y-m-d','period_end'=>'present|nullable|date_format:Y-m-d|after_or_equal:period_start','proposed_due_date'=>'present|nullable|date_format:Y-m-d','payment_terms'=>'present|nullable|string|max:1000','lines'=>'required|array|min:1|max:25','lines.*'=>'array:service_id,description,quantity,unit_price,unit','lines.*.service_id'=>'required|integer|min:1','lines.*.description'=>'nullable|string|max:2000','lines.*.quantity'=>'present|nullable|string|max:30','lines.*.unit_price'=>'present|nullable|string|max:30','lines.*.unit'=>'present|nullable|string|max:80','reason'=>'required|string|min:10|max:2000'];abort_if(array_diff(array_keys($v),array_filter(array_keys($rules),fn($k)=>!str_contains($k,'*'))),422);$d=Validator::make($v,$rules)->validate();foreach(['draft_id','version','source_version','preparation_version','client_id']as$k)if($d[$k]!==null)$d[$k]=(int)$d[$k];abort_unless(array_is_list($d['lines']),422);foreach($d['lines']as$i=>$row){$d['lines'][$i]['service_id']=(int)$row['service_id'];foreach(['quantity','unit_price']as$f)$d['lines'][$i][$f]=DraftValues::decimal($row[$f],'lines.'.$i.'.'.$f,$f==='quantity');}foreach([$d['name'],$d['payment_terms'],$d['reason'],...array_column($d['lines'],'description')]as$value)if(is_string($value))abort_if((bool)preg_match('/(?:password|palavra.passe|token|secret|cvv|cvc|pin)\\s*[:=]/iu',$value),422,'Não introduza credenciais ou segredos.');return$d;
|
||||
}
|
||||
private function scope(int $actor,array $d):array{
|
||||
$s=app(MultiLineInvoiceCalculation::class)->scope($actor,$d['draft_id']===null?'billing_drafts.create':'billing_drafts.edit');$m=DB::table('mi_draft_meta')->where('policy',MultiLineInvoiceCalculation::POLICY)->first();abort_unless($m&&$m->composition===\App\Shared\Installation\CompositionRuntime::physical()['composition']&&hash_equals($m->source_hash,(string)config('multiline_invoices.draft_schema_sha256')),503,'Composição de rascunhos não conferida.');return$s;
|
||||
}
|
||||
private function basis(int $actor,array $s,array $d):array{
|
||||
$w=app(ServiceWorkflow::class);$w->scope('clients',$actor,'view');$w->scope('services',$actor,'view');$client=\App\Shared\Context\ModuleRecords::query('a_clients')->where('tenant_id',$s['tenant_id'])->where('company_id',$s['company_id'])->where('id',$d['client_id'])->first();abort_unless($client,403);$ids=array_unique(array_column($d['lines'],'service_id'));$services=\App\Shared\Context\ModuleRecords::query('a_services')->where('tenant_id',$s['tenant_id'])->where('company_id',$s['company_id'])->whereIn('id',$ids)->orderBy('id')->get();abort_unless($services->count()===count($ids),403,'Serviço fora do módulo/contexto.');$before=null;$binding=null;
|
||||
if($d['draft_id']!==null){$binding=DB::table('mi_drafts')->where($s)->where('draft_id',$d['draft_id'])->first();abort_unless($binding,403,'Este editor altera apenas os novos rascunhos multilinha.');$bill=$w->show('billing_drafts',$d['draft_id'],$actor);$source=$w->show('service_drafts',(int)$binding->source_id,$actor);$prepared=app(DocumentPreparation::class)->show('billing_drafts',$d['draft_id'],$actor);abort_unless($bill['version']===$d['version']&&$source['version']===$d['source_version']&&$prepared['version']===$d['preparation_version'],409,'Origem/rascunho/preparação alterados.');abort_if(DB::table('si_invoices')->where('draft_id',$d['draft_id'])->orWhere('source_id',$binding->source_id)->exists(),409,'Documento emitido preservado; não editar a origem por este percurso.');$before=['source'=>$source,'billing'=>$bill,'preparation'=>$prepared];}else abort_unless($d['version']===0&&$d['source_version']===0&&$d['preparation_version']===0,409);
|
||||
return['client'=>(array)$client,'services'=>$services->map(fn($r)=>(array)$r)->all(),'binding'=>$binding?(array)$binding:null,'before'=>$before];
|
||||
}
|
||||
private function write(int $actor,array $s,array $d,array $b,string $key):array{
|
||||
$w=app(ServiceWorkflow::class);$lines=array_map(function($l){unset($l['unit']);return$l;},$d['lines']);$sourceInput=['name'=>$d['name'],'client_id'=>$d['client_id'],'currency'=>$d['currency'],'lines'=>$lines];if($b['binding'])$sourceInput['version']=$d['source_version'];$source=$w->save('service_drafts',$b['binding']['source_id']??null,$actor,$sourceInput,$key.'-source');$billInput=$b['binding']?['version'=>$d['version'],'reload_source'=>true,'name'=>$d['name']]:['source_id'=>$source['id'],'name'=>$d['name']];$bill=$w->save('billing_drafts',$d['draft_id'],$actor,$billInput,$key.'-billing');$currentPrep=app(DocumentPreparation::class)->show('billing_drafts',$bill['id'],$actor);$prepared=app(DocumentPreparation::class)->save('billing_drafts',$bill['id'],$actor,['version'=>$currentPrep['version'],'draft_version'=>$bill['version'],'currency'=>$d['currency'],'service_date'=>$d['service_date'],'period_start'=>$d['period_start'],'period_end'=>$d['period_end'],'proposed_due_date'=>$d['proposed_due_date'],'payment_terms'=>$d['payment_terms'],'price_mode'=>$d['price_mode'],'units'=>array_column($d['lines'],'unit'),'payment_instruction_ids'=>[]]);
|
||||
if(!$b['binding'])DB::table('mi_drafts')->insert($s+['draft_id'=>$bill['id'],'source_id'=>$source['id'],'actor_id'=>$actor,'created_at'=>now()->utc()]);return ['type'=>'draft','id'=>$bill['id'],'source_id'=>$source['id'],'version'=>$bill['version'],'source_version'=>$source['version'],'preparation_version'=>$prepared['version'],'new_documents'=>0,'new_receivables'=>0,'new_journals'=>0];
|
||||
}
|
||||
private function impact(array $d,array $b):array{$lines=$d['lines'];$map=collect($b['services'])->keyBy('id');foreach($lines as&$l)$l['service_name']=$map[$l['service_id']]['name'];unset($l);return['before'=>$b['before']?['source_version'=>$b['before']['source']['version'],'version'=>$b['before']['billing']['version'],'preparation_version'=>$b['before']['preparation']['version']]:null,'after'=>['client'=>$b['client']['name'],'name'=>$d['name'],'currency'=>$d['currency'],'price_mode'=>$d['price_mode'],'lines'=>$lines,'arithmetic_amount'=>DraftValues::base(['lines'=>$lines]),'period_start'=>$d['period_start'],'period_end'=>$d['period_end']],'new_documents'=>0,'new_receivables'=>0,'new_journals'=>0];}
|
||||
public function preview(int $actor,array $v,string $key):array{
|
||||
$d=$this->input($v);abort_unless(preg_match('/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/D',$key),422);$s=$this->scope($actor,$d);return DB::transaction(function()use($actor,$s,$d,$key){DB::statement("SET LOCAL lock_timeout='5s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();$b=$this->basis($actor,$s,$d);DB::beginTransaction();try{$this->write($actor,$s,$d,$b,$key);}finally{DB::rollBack();}$c=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'input'=>$d,'key'=>$key,'basis'=>$this->hash($b),'observed'=>time()];return ['preview'=>Crypt::encryptString(Payables::canonical($c)),'impact'=>$this->impact($d,$b)];});
|
||||
}
|
||||
public function confirm(int $actor,string $token):array{
|
||||
try{$c=json_decode(Crypt::decryptString($token),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$d=$this->input($c['input']??[]);$s=$this->scope($actor,$d);return DB::transaction(function()use($actor,$s,$c,$d){DB::statement("SET LOCAL lock_timeout='5s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();$this->scope($actor,$d);abort_unless($c['actor']===$actor&&$this->hash($c['scope'])===$this->hash($s)&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409);$q=$s+['actor_id'=>$actor,'command_key'=>$c['key']];$request=['domain'=>'draft','input'=>$d];$old=DB::table('mi_commands')->where($q)->first();if($old){abort_unless(hash_equals($old->request_hash,$this->hash($request)),409);return json_decode($old->result,true)+['replayed'=>true];}$b=$this->basis($actor,$s,$d);abort_unless(hash_equals($c['basis'],$this->hash($b)),409,'Origem ou versão alteradas; confira novamente.');$result=$this->write($actor,$s,$d,$b,$c['key']);DB::table('mi_commands')->insert($q+['request_hash'=>$this->hash($request),'input'=>Payables::canonical($request),'result'=>Payables::canonical($result),'created_at'=>now()->utc()]);\App\Modules\Identity\Services\Audit::record('service_invoice.multiline_draft_prepared',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$result['id'],'version'=>$result['version'],'reason'=>$d['reason']]);return$result+['replayed'=>false];});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace App\Modules\Services;
|
||||
use App\Modules\Identity\Services\{Audit,Permissions};
|
||||
use App\Shared\Context\{CompanyContext,ModuleRecords};
|
||||
use App\Shared\Installation\CompositionRuntime;
|
||||
use Illuminate\Support\Facades\{DB,Crypt,Validator};
|
||||
use Tempo\Finance\Payables\Payables;
|
||||
/** New typed QA capture. Existing one-line calculation and invoice writer retain their contracts. */
|
||||
final class MultiLineInvoiceCalculation
|
||||
{
|
||||
public const POLICY='SGF-MULTI-LINE-INVOICE-INTEGRATED-QA-v1';
|
||||
public function __construct(private CompanyContext $context,private Permissions $permissions){}
|
||||
private function hash(array $v):string{return hash('sha256',Payables::canonical($v));}
|
||||
private function exact(array $v,array $r):array{abort_if(array_diff(array_keys($v),array_keys($r)),422);return Validator::make($v,$r)->validate();}
|
||||
public function scope(int $actor,string $right='billing_drafts.view'):array{
|
||||
abort_unless(config('multiline_invoices.enabled')===true,404);CompositionRuntime::assert();ServiceInvoicing::guard();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']&&ModuleRecords::active()==='commercial',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],$right,str_starts_with($right,'service_fiscal.')&&$right!=='service_fiscal.calculate'?null:$s['unit_id']);if(str_starts_with($right,'service_fiscal.'))$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_fiscal.view');$m=DB::table('mi_meta')->where('policy',self::POLICY)->first();abort_unless($m&&$m->composition===CompositionRuntime::physical()['composition']&&hash_equals($m->source_hash,(string)config('multiline_invoices.schema_sha256')),503,'Captura multilinha não conferida.');return$s;
|
||||
}
|
||||
private function capture(int $actor,int $draft,int $event,?int $bound=null,?int $preparer=null):array{
|
||||
$s=$this->scope($actor);$binding=DB::table('mi_drafts')->where($s)->where('draft_id',$draft)->first();abort_unless($binding,403,'A captura nova exige rascunho criado neste percurso multilinha.');$view=app(DocumentPreview::class)->captureDraftForIssuance($draft,$actor);abort_unless((int)$binding->source_id===$view['source_id'],409);abort_unless(count($view['data']['lines'])>=1&&count($view['data']['lines'])<=25&&$view['data']['currency']==='MZN',422,'Seleccione1–25 linhas em MZN.');abort_unless(!empty($view['prepared']['period_start'])&&!empty($view['prepared']['period_end']),422,'Identifique a cobertura antes do cálculo documental.');$mode=match($view['prepared']['price_mode']??null){'added'=>'net_added','included'=>'gross_included',default=>abort(422,'Modalidade de preço explícita obrigatória.')};$rows=[];foreach($view['data']['lines']as$i=>$line){abort_unless(isset($view['prepared']['units'][$i])&&trim($view['prepared']['units'][$i])!==''&&$line['quantity']!==null&&$line['unit_price']!==null,422,'Quantidade/preço/unidade obrigatórios em cada linha.');$rows[]=['reference'=>'LINE-'.($i+1),'quantity'=>$line['quantity'],'unit_price'=>$line['unit_price']];}
|
||||
unset($view['consulted_at'],$view['revision'],$view['calculation'],$view['fiscal_history']);$date=now(ServiceInvoicing::ZONE)->format('Y-m-d');$common=app(FiscalRules::class)->invoiceDocument($actor,$s,$event,['currency'=>'MZN','date'=>$date,'price_mode'=>$mode,'lines'=>$rows],$bound);abort_unless($common['result']['state']==='determined_qa',409,implode(' ',$common['result']['pending']));$prepared=DB::table('users')->where('id',$preparer??$actor)->first(['id','name','email']);abort_unless($prepared,403);return ['prepared_identity'=>$this->hash((array)$prepared),'prepared_by'=>$preparer??$actor,'policy'=>self::POLICY,'document'=>$view,'common'=>$common,'expected_date'=>$date,'synthetic_only'=>true];
|
||||
}
|
||||
private function input(array $v):array{$d=$this->exact($v,['draft_id'=>'required|integer|min:1','rule_event_id'=>'required|integer|min:1','draft_version'=>'required|integer|min:1','preparation_version'=>'required|integer|min:1','reason'=>'required|string|min:10|max:2000']);foreach(['draft_id','rule_event_id','draft_version','preparation_version']as$k)$d[$k]=(int)$d[$k];return$d;}
|
||||
private function lock(array $s):void{DB::statement("SET LOCAL lock_timeout='5s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();}
|
||||
public function preview(int $actor,array $input,string $key):array{
|
||||
$d=$this->input($input);$this->exact(['key'=>$key],['key'=>'required|uuid']);$s=$this->scope($actor,'fiscal_rules.prepare');return DB::transaction(function()use($actor,$s,$d,$key){$this->lock($s);$snapshot=$this->capture($actor,$d['draft_id'],$d['rule_event_id']);$v=$snapshot['document'];abort_unless($v['draft_version']===$d['draft_version']&&$v['preparation_version']===$d['preparation_version'],409,'Versões desactualizadas.');$c=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'input'=>$d,'key'=>strtolower($key),'fingerprint'=>$this->hash($snapshot),'observed'=>time()];return ['preview'=>Crypt::encryptString(Payables::canonical($c)),'impact'=>['result'=>$snapshot['common']['result'],'new_documents'=>0,'new_receivables'=>0,'new_journals'=>0]];});
|
||||
}
|
||||
public function confirm(int $actor,string $preview):array{
|
||||
try{$c=json_decode(Crypt::decryptString($preview),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$d=$this->input($c['input']??[]);$s=$this->scope($actor,'fiscal_rules.prepare');return DB::transaction(function()use($actor,$s,$c,$d){$this->lock($s);$this->scope($actor,'fiscal_rules.prepare');abort_unless($c['actor']===$actor&&$this->hash($c['scope'])===$this->hash($s)&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Pessoa/contexto/prévia alterados.');$q=$s+['actor_id'=>$actor,'command_key'=>$c['key']];$old=DB::table('mi_commands')->where($q)->first();if($old){abort_unless(hash_equals($old->request_hash,$this->hash($d)),409);return json_decode($old->result,true)+['replayed'=>true];}$snapshot=$this->capture($actor,$d['draft_id'],$d['rule_event_id']);$fingerprint=$this->hash($snapshot);abort_unless(hash_equals($c['fingerprint'],$fingerprint),409,'Origem/regra/versão/data alteradas. Confira novamente.');$existing=DB::table('mi_calculations')->where($s+['draft_id'=>$d['draft_id'],'fingerprint'=>$fingerprint])->first();$id=$existing?->id??DB::table('mi_calculations')->insertGetId($s+['draft_id'=>$d['draft_id'],'actor_id'=>$actor,'fingerprint'=>$fingerprint,'snapshot'=>Payables::canonical($snapshot),'reason'=>$d['reason'],'created_at'=>now()->utc()]);$result=['id'=>$id,'draft_id'=>$d['draft_id'],'type'=>'calculation','rule_event_id'=>$d['rule_event_id'],'draft_version'=>$d['draft_version'],'preparation_version'=>$d['preparation_version'],'date'=>$snapshot['expected_date'],'result'=>$snapshot['common']['result'],'policy'=>self::POLICY];DB::table('mi_commands')->insert($q+['request_hash'=>$this->hash($d),'input'=>Payables::canonical($d),'result'=>Payables::canonical($result),'created_at'=>now()->utc()]);Audit::record('service_invoice.multiline_calculated',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'reason'=>$d['reason'],'review_hash'=>$fingerprint]);return$result+['replayed'=>false];});
|
||||
}
|
||||
public function current(int $actor,int $id,?int $draft=null):array{
|
||||
$s=$this->scope($actor);$r=DB::table('mi_calculations')->where($s)->where('id',$id)->first();abort_unless($r&&($draft===null||(int)$r->draft_id===$draft),403,'Cálculo fora do rascunho/contexto.');$this->permissions->require((int)$r->actor_id,$s['tenant_id'],$s['company_id'],'fiscal_rules.prepare',$s['unit_id']);abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$r->actor_id])->exists(),403);$snapshot=json_decode($r->snapshot,true,64,JSON_THROW_ON_ERROR);$now=$this->capture($actor,(int)$r->draft_id,(int)$snapshot['common']['rule']['id'],$id,(int)$r->actor_id);abort_unless(hash_equals($r->fingerprint,$this->hash($now)),409,'Cálculo anterior à origem/regra/data actual.');return ['id'=>$r->id,'fingerprint'=>$r->fingerprint,'snapshot'=>$snapshot];
|
||||
}
|
||||
public static function financialAmounts(array $snapshot):array{
|
||||
abort_unless(($snapshot['calculation_policy']??null)===self::POLICY&&($snapshot['renderer']??null)===MultiLineInvoicePdf::VERSION,409,'Tipo de fotografia multilinha incompatível.');$c=$snapshot['calculation'];$r=$c['common_document_result'];$lines=$snapshot['document']['data']['lines'];abort_unless(count($lines)>=1&&count($lines)<=25&&($snapshot['document']['data']['currency']??null)==='MZN',409);$in=[];foreach($lines as$i=>$line)$in[]=['reference'=>'LINE-'.($i+1),'quantity'=>$line['quantity'],'unit_price'=>$line['unit_price']];$mode=match($snapshot['document']['prepared']['price_mode']){'added'=>'net_added','included'=>'gross_included',default=>abort(409)};$expected=FiscalDocumentMath::calculate($in,$mode,$r['rate']);$frozen=$r;unset($frozen['currency'],$frozen['rule']);abort_unless(Payables::canonical($expected)===Payables::canonical($frozen)&&$expected['state']==='determined_qa'&&$c['base_document_proposed']===$r['base_document']&&$c['vat_document_proposed']===$r['tax_document']&&$c['total_document_proposed']===$r['total'],409,'Valores documentais por linha não conciliam com a fotografia.');return['base'=>$r['base_document'],'tax'=>$r['tax_document'],'total'=>$r['total']];
|
||||
}
|
||||
public function receipt(int $actor,string $key):array{$s=$this->scope($actor);$this->exact(['key'=>$key],['key'=>'required|uuid']);$r=DB::table('mi_commands')->where($s+['actor_id'=>$actor,'command_key'=>strtolower($key)])->first();abort_unless($r,404);return ['result'=>json_decode($r->result,true),'historical_receipt_only'=>true,'operation_executed'=>false];}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace App\Modules\Services;
|
||||
use Illuminate\Support\Facades\{DB,Validator};
|
||||
use Brick\Math\BigDecimal;
|
||||
/** Typed readiness and photograph only. ServiceInvoicing retains numbering, invoice and receipt writes. */
|
||||
final class MultiLineInvoiceIssuance
|
||||
{
|
||||
public function temporal(int $actor,array $input,array $scope,?array $previous=null):array{
|
||||
abort_if(array_diff(array_keys($input),['calculation_policy','common_calculation_id','common_rule_event_id','draft_id','draft_version','preparation_version','valid_from','valid_until','period_start','period_end','issue_window_start','issue_window_end','event','readiness_basis','evidence']),422);$d=Validator::make($input,['calculation_policy'=>'required|in:'.MultiLineInvoiceCalculation::POLICY,'common_calculation_id'=>'required|integer|min:1','common_rule_event_id'=>'required|integer|min:1','draft_id'=>'required|integer|min:1','draft_version'=>'required|integer|min:1','preparation_version'=>'required|integer|min:1','valid_from'=>'required|date_format:Y-m-d','valid_until'=>'nullable|date_format:Y-m-d|after_or_equal:valid_from','period_start'=>'required|date_format:Y-m-d','period_end'=>'required|date_format:Y-m-d|after_or_equal:period_start','issue_window_start'=>'required|date_format:Y-m-d','issue_window_end'=>'required|date_format:Y-m-d|after_or_equal:issue_window_start','event'=>'required|in:on_issue_within_deadline','readiness_basis'=>'required|string|min:10|max:4000','evidence'=>'required|string|min:10|max:4000'])->validate();foreach(['common_calculation_id','common_rule_event_id','draft_id','draft_version','preparation_version']as$k)$d[$k]=(int)$d[$k];$calc=app(MultiLineInvoiceCalculation::class)->current($actor,$d['common_calculation_id'],$d['draft_id']);$snapshot=$calc['snapshot'];$view=$snapshot['document'];abort_unless($d['draft_version']===$view['draft_version']&&$d['preparation_version']===$view['preparation_version']&&$d['period_start']===($view['prepared']['period_start']??null)&&$d['period_end']===($view['prepared']['period_end']??null)&&$d['common_rule_event_id']===$snapshot['common']['rule']['id'],409,'A regra temporal não corresponde à captura revista.');if($previous)abort_unless((int)$previous['draft_id']===$d['draft_id']&&($previous['calculation_policy']??null)===MultiLineInvoiceCalculation::POLICY,409,'A regra temporal não pode trocar de origem/política.');$d['calculation_fingerprint']=$calc['fingerprint'];return$d;
|
||||
}
|
||||
private function proposal(int $actor,int $id,array $s,string $date):array{
|
||||
abort_unless(app(MultiLineInvoiceCalculation::class)->scope($actor)===$s,403);$r=DB::table('si_proposals')->where($s)->where('id',$id)->first();abort_unless($r,403);$d=json_decode($r->data,true);$approved=DB::table('si_decisions')->where(['proposal_id'=>$r->id,'decision'=>'approved'])->exists()&&$d['valid_from']<=$date&&(!isset($d['valid_until'])||$d['valid_until']===''||$date<=$d['valid_until'])&&!DB::table('si_proposals as n')->where($s)->where(['kind'=>$r->kind,'identifier'=>$r->identifier])->where('version','>',$r->version)->whereRaw("data->>'valid_from'<=?",[$date])->whereExists(fn($q)=>$q->selectRaw('1')->from('si_decisions as d')->whereColumn('d.proposal_id','n.id')->where('d.decision','approved'))->exists();return ['row'=>$r,'data'=>$d,'approved'=>$approved];
|
||||
}
|
||||
public function review(int $draft,int $series,int $rule,int $actor,array $s):array{
|
||||
$date=now(ServiceInvoicing::ZONE)->format('Y-m-d');$p=$this->proposal($actor,$series,$s,$date);$t=$this->proposal($actor,$rule,$s,$date);$d=$t['data'];$blocks=[];abort_unless(($d['calculation_policy']??null)===MultiLineInvoiceCalculation::POLICY,422);$calc=app(MultiLineInvoiceCalculation::class)->current($actor,(int)$d['common_calculation_id'],$draft);$snap=$calc['snapshot'];$view=$snap['document'];$common=$snap['common'];
|
||||
if($p['row']->kind!=='series'||!$p['approved']||($p['data']['state']??null)!=='open'||!str_starts_with($p['data']['code']??'','TEST-MULTI-'))$blocks[]='Série de teste exacta não revista/vigente/aberta.';if($t['row']->kind!=='temporal'||!$t['approved']||(int)$d['draft_id']!==$draft)$blocks[]='Regra temporal exacta não revista/vigente.';
|
||||
if(($d['calculation_fingerprint']??null)!==$calc['fingerprint']||$d['draft_version']!==$view['draft_version']||$d['preparation_version']!==$view['preparation_version']||$d['period_start']!==($view['prepared']['period_start']??null)||$d['period_end']!==($view['prepared']['period_end']??null))$blocks[]='Origem/versão/período alterados; exige nova conferência.';
|
||||
if($date<$d['issue_window_start']||$date>$d['issue_window_end'])$blocks[]='Data actual fora da janela de emissão fundamentada.';
|
||||
foreach(['issuer'=>'Emitente','client'=>'Cliente']as$party=>$label)foreach(['name','nuit','address','locality','country']as$f){$v=$f==='name'?($view[$party]['name']??''):($view[$party]['data'][$f]??'');if(!is_string($v)||trim($v)==='')$blocks[]=$label.': '.$f.' por completar.';}
|
||||
if(DB::table('si_invoices')->where('draft_id',$draft)->orWhere('source_id',$view['source_id'])->exists())$blocks[]='Já existe documento desta origem.';
|
||||
$r=$common['result'];try{$minor=BigDecimal::of($r['total'])->multipliedBy(100)->toInt();if($minor<=0)$blocks[]='Documento financeiro exige total positivo.';if(BigDecimal::of($r['base_document'])->isLessThanOrEqualTo(0)||BigDecimal::of($r['tax_document'])->isLessThanOrEqualTo(0))$blocks[]='O plano contabilístico QA de três linhas exige base e imposto positivos.';}catch(\Brick\Math\Exception\MathException){$blocks[]='Montante fora da representação contabilística inteira.';}$def=$common['rule'];$rate=(string)BigDecimal::of($def['definition']['parameters']['rate'])->multipliedBy(100)->stripTrailingZeros();$result=['policy'=>MultiLineInvoiceCalculation::POLICY,'currency'=>'MZN','state'=>'synthetic_proposed','price_mode'=>$view['prepared']['price_mode'],'base_exact'=>$r['base_exact_fraction'],'base_document_proposed'=>$r['base_document'],'vat_exact'=>$r['tax_exact_fraction'],'vat_document_proposed'=>$r['tax_document'],'discarded_difference'=>$r['tax_difference_fraction'],'total_document_proposed'=>$r['total'],'common_document_result'=>$r,'common_rule'=>$def,'pending'=>[]];$profile=['kind'=>'synthetic_common_rule','rate_percent'=>$rate,'valid_from'=>$def['definition']['valid_from'],'valid_until'=>$def['definition']['valid_until'],'source_sha256'=>$def['definition']['source_sha256']];
|
||||
$entry=['id'=>$calc['id'],'snapshot'=>['input'=>['policy'=>MultiLineInvoiceCalculation::POLICY,'profile_identifier'=>$def['rule_key'],'profile_version'=>$def['rule_version'],'profile'=>$profile,'common_capture'=>$snap],'result'=>$result]];
|
||||
$payload=['calculation_policy'=>MultiLineInvoiceCalculation::POLICY,'draft_id'=>$draft,'series_id'=>$series,'series_version'=>(int)$p['row']->version,'series_code'=>$p['data']['code'],'series_definition'=>$p['data'],'rule_id'=>$rule,'rule_version'=>(int)$t['row']->version,'rule'=>$d,'expected_date'=>$date,'document'=>$view,'calculation_entry'=>$entry,'profile'=>$profile,'blocks'=>$blocks];
|
||||
if(!$blocks){$sample=app(ServiceInvoicing::class)->multiLineSnapshot($payload,0,now()->toIso8601String());if(!InvoicePdf::supports(MultiLineInvoicePdf::lines($sample)))$payload['blocks'][]='Texto não representável no PDF sem perda; confira os dados.';}
|
||||
return ['ready'=>$payload['blocks']===[],'notice'=>ServiceInvoicing::NOTICE,'payload'=>$payload,'review_hash'=>hash_hmac('sha256',json_encode($payload,JSON_THROW_ON_ERROR),(string)config('app.key'))];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace App\Modules\Services;
|
||||
final class MultiLineInvoicePdf
|
||||
{
|
||||
public const VERSION='sgf-test-invoice-multiline-text-v1';
|
||||
public static function lines(array $s):array{
|
||||
$c=$s['calculation'];$r=$c['common_document_result'];$p=$s['document']['prepared'];$rows=[['title','Factura de servicos - TESTE QA'],['text',$s['series_code'].' / '.$s['number']],['text','Emissao: '.$s['issued_local'].' ('.$s['timezone'].')'],['text',ServiceInvoicing::NOTICE]];
|
||||
foreach(['issuer'=>'Emitente','client'=>'Cliente']as$k=>$label){$party=$s['document'][$k];$rows[]=['section',$label];$rows[]=['text',$party['name']];foreach(['nuit'=>'NUIT','address'=>'Morada','locality'=>'Localidade','country'=>'Pais']as$f=>$n)$rows[]=['text',$n.': '.($party['data'][$f]??'Nao indicado')];}
|
||||
$rows[]=['section','Linhas do documento'];$rows[]=['text','Modalidade: '.($r['price_mode']==='gross_included'?'preco com imposto incluido':'preco sem imposto, acrescido')];
|
||||
foreach($s['document']['data']['lines']as$i=>$line){$v=$r['lines'][$i];$rows[]=['section','Linha '.($i+1).' - '.$line['service_name']];if(!empty($line['description']))$rows[]=['text',$line['description']];$rows[]=['text','Quantidade '.$line['quantity'].' | Unidade '.$p['units'][$i].' | Preco '.$line['unit_price']];$rows[]=['text','Base documental '.$v['base_document'].' | Imposto '.$v['tax_document'].' | Total '.$v['total_document']];$rows[]=['text','Base exacta '.$v['base_exact_fraction'].' | Imposto exacto '.$v['tax_exact_fraction']];$rows[]=['text','Diferenca base '.$v['base_difference_fraction'].' | Diferenca imposto '.$v['tax_difference_fraction']];}
|
||||
$rows[]=['section','Totais MZN - soma dos valores documentais por linha'];$rows[]=['text','Base '.$r['base_document'].' | Imposto '.$r['tax_document'].' | Total '.$r['total']];$rows[]=['text','Regra QA '.$s['profile_identifier'].' v'.$s['profile_version'].' | Taxa artificial '.$s['rate_percent'].'%'];$rows[]=['text','Politica '.$r['policy']];$rows[]=['text','Periodo '.$p['period_start'].' a '.$p['period_end']];$rows[]=['text','Condicoes: '.($p['payment_terms']??'Nao indicadas')];$rows[]=['text','Vencimento proposto: '.($p['proposed_due_date']??'Nao indicado')];$rows[]=['text','Origem: rascunho #'.$s['draft_id'].' v'.$s['document']['draft_version'].' | Regra temporal #'.$s['rule_id'].' v'.$s['rule_version']];$rows[]=['text','Emissao nao comprova recebimento ou aplicacao contabilistica. Sem comunicacao a AT.'];return$rows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace App\Modules\Services;
|
||||
use App\Shared\Context\{CompanyContext,ModuleRecords};
|
||||
use App\Shared\Installation\CompositionRuntime;
|
||||
use App\Modules\Identity\Services\Permissions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
/** Qualified new dependencies keep Commercial ownership and require existing target-module rights. */
|
||||
final class QualifiedInvoiceSource
|
||||
{
|
||||
public static function query(){
|
||||
$legacy=ModuleRecords::query('si_invoices');$module=ModuleRecords::active();if(config('multiline_invoices.enabled')!==true||!in_array($module,['finance','accounting'],true))return$legacy;CompositionRuntime::assert();$c=app(CompanyContext::class);$s=$c->identifiers()+['unit_id'=>$c->unitId()];$actor=$c->userId();if(!$s['unit_id']||!app(Permissions::class)->has($actor,$s['tenant_id'],$s['company_id'],$module==='finance'?'service_receivables.view':'general_ledger.view',$s['unit_id']))return$legacy;
|
||||
$owned=DB::table('a_billing_drafts');ModuleRecords::constrain($owned,'a_billing_drafts','billing_drafts','commercial');$owned->where($s);
|
||||
return DB::table('si_invoices')->where($s)->where(function($q)use($legacy,$s,$owned){$q->whereIn('id',$legacy->select('id'))->orWhere(function($q)use($s,$owned){$q->whereRaw("si_invoices.snapshot::jsonb->>'calculation_policy'=?",[MultiLineInvoiceCalculation::POLICY])->whereExists(function($m)use($s,$owned){$m->selectRaw('1')->from('mi_drafts as md')->whereColumn('md.draft_id','si_invoices.draft_id')->whereColumn('md.source_id','si_invoices.source_id')->where(['md.tenant_id'=>$s['tenant_id'],'md.company_id'=>$s['company_id'],'md.unit_id'=>$s['unit_id']])->whereIn('md.draft_id',$owned->select('id'));});});});
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,9 @@ final class ServiceInvoicing
|
||||
abort_unless($d['code']===$in['identifier'],422,'Identidade da série deve coincidir com o código sintético.');
|
||||
if($previous) { $old=$this->data($previous); abort_unless($old['code']===$d['code'] && (int)$old['first_number']===(int)$d['first_number'],422,'Não reiniciar ou transferir contador existente.'); }
|
||||
$d['first_number']=(int)$d['first_number'];
|
||||
} elseif(($in['data']['calculation_policy']??null)===MultiLineInvoiceCalculation::POLICY){
|
||||
$d=app(MultiLineInvoiceIssuance::class)->temporal($actor,$in['data'],$s,$previous?$this->data($previous):null);
|
||||
abort_if(DB::table('si_proposals')->where($s)->where('kind','temporal')->whereRaw("data->>'draft_id' = ?",[(string)$d['draft_id']])->where('identifier','<>',$in['identifier'])->exists(),409,'Use a identidade temporal existente.');
|
||||
} else {
|
||||
$d=$this->exact($in['data'],$common+['draft_id'=>'required|integer|min:1','draft_version'=>'required|integer|min:1','preparation_version'=>'required|integer|min:1','profile_proposal_id'=>'required|integer|min:1','period_start'=>'required|date_format:Y-m-d','period_end'=>'required|date_format:Y-m-d|after_or_equal:period_start','issue_window_start'=>'required|date_format:Y-m-d','issue_window_end'=>'required|date_format:Y-m-d|after_or_equal:issue_window_start','event'=>'required|in:on_issue_within_deadline','readiness_basis'=>'required|string|max:4000']);
|
||||
$view=$this->preview->captureDraftForIssuance((int)$d['draft_id'],$actor);
|
||||
@@ -90,6 +93,7 @@ final class ServiceInvoicing
|
||||
$d=$this->exact($input,['version'=>'required|integer|min:1','decision'=>'required|in:approved,refused','reason'=>'required|string|max:4000']);
|
||||
$p=$this->row($id,$s); abort_unless((int)$p->version===(int)$d['version'] && (int)$p->actor_id!==$actor,403,'Validador distinto e versão exacta obrigatórios.');
|
||||
abort_if($this->decision($id),409,'Decisão imutável; correcção exige nova versão.');
|
||||
$mi=$this->data($p);if(($mi['calculation_policy']??null)===MultiLineInvoiceCalculation::POLICY){$fingerprint=$mi['calculation_fingerprint'];unset($mi['calculation_fingerprint']);$fresh=app(MultiLineInvoiceIssuance::class)->temporal($actor,$mi,$s);abort_unless(hash_equals($fingerprint,$fresh['calculation_fingerprint']),409,'A captura mudou; confira nova proposta.');}
|
||||
DB::table('si_decisions')->insert(['proposal_id'=>$id,'actor_id'=>$actor,'decision'=>$d['decision'],'reason'=>$d['reason'],'created_at'=>now()]);
|
||||
Audit::record('service_invoice.decided',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'version'=>$p->version,'reason'=>$d['reason']]);
|
||||
return ['id'=>$id,'decision'=>$d['decision']];
|
||||
@@ -101,7 +105,7 @@ final class ServiceInvoicing
|
||||
if(!ServiceCalculation::isolated()) return ['real_blocked'=>true,'test_enabled'=>false,'notice'=>'Emissão real bloqueada: conformidade ainda não demonstrada.','proposals'=>[],'invoices'=>[]];
|
||||
TestingDatabaseGuard::assertSafe();
|
||||
if(!Schema::hasTable('si_invoices')) return ['real_blocked'=>true,'notice'=>'Migração documental ainda não aplicada.','proposals'=>[],'invoices'=>[]];
|
||||
return ['real_blocked'=>true,'test_enabled'=>true,'notice'=>self::NOTICE,'proposals'=>DB::table('si_proposals')->where($s)->orderByDesc('id')->get()->map(fn($p)=>(array)$p+['definition'=>$this->data($p),'decision'=>$this->decision($p->id)])->all(),'invoices'=>\App\Shared\Context\ModuleRecords::query('si_invoices')->where($s)->orderByDesc('id')->get(['id','series_code','number','draft_id','issued_at'])->all()];
|
||||
return ['real_blocked'=>true,'test_enabled'=>true,'notice'=>self::NOTICE,'proposals'=>DB::table('si_proposals')->where($s)->orderByDesc('id')->get()->map(fn($p)=>(array)$p+['definition'=>$this->data($p),'decision'=>$this->decision($p->id)])->all(),'invoices'=>QualifiedInvoiceSource::query()->where($s)->orderByDesc('id')->get(['id','series_code','number','draft_id','issued_at'])->all()];
|
||||
}
|
||||
public function review(int $draft,int $series,int $rule,int $actor): array
|
||||
{
|
||||
@@ -115,6 +119,7 @@ final class ServiceInvoicing
|
||||
}
|
||||
private function reviewInside(int $draft,int $series,int $rule,int $actor,array $s): array
|
||||
{
|
||||
$mi=$this->data($this->row($rule,$s));if(($mi['calculation_policy']??null)===MultiLineInvoiceCalculation::POLICY)return app(MultiLineInvoiceIssuance::class)->review($draft,$series,$rule,$actor,$s);
|
||||
$view=$this->preview->captureDraftForIssuance($draft,$actor); $date=now(self::ZONE)->format('Y-m-d'); $blocks=[];
|
||||
$p=$this->row($series,$s); $t=$this->row($rule,$s); $sd=$this->data($p); $td=$this->data($t);
|
||||
if($p->kind!=='series' || !$this->approved($p,$s,$date) || ($sd['state'] ?? null)!=='open') $blocks[]='Série sintética sem configuração aprovada/vigente e aberta.';
|
||||
@@ -147,11 +152,12 @@ final class ServiceInvoicing
|
||||
}
|
||||
private function snapshot(array $p,int $number,string $instant): array
|
||||
{
|
||||
return ['notice'=>self::NOTICE,'renderer'=>InvoicePdf::VERSION,'draft_id'=>$p['draft_id'],'series_code'=>$p['series_code'],'number'=>$number,'issued_at'=>$instant,'issued_local'=>\Carbon\CarbonImmutable::parse($instant)->timezone(self::ZONE)->format('Y-m-d H:i:s'),'timezone'=>self::ZONE,
|
||||
return ['notice'=>self::NOTICE,'renderer'=>isset($p['calculation_policy'])?MultiLineInvoicePdf::VERSION:InvoicePdf::VERSION,'draft_id'=>$p['draft_id'],'series_code'=>$p['series_code'],'number'=>$number,'issued_at'=>$instant,'issued_local'=>\Carbon\CarbonImmutable::parse($instant)->timezone(self::ZONE)->format('Y-m-d H:i:s'),'timezone'=>self::ZONE,
|
||||
'series_id'=>$p['series_id'],'series_version'=>$p['series_version'],'series_definition'=>$p['series_definition'],'rule_id'=>$p['rule_id'],'rule_version'=>$p['rule_version'],'rule'=>$p['rule'],'document'=>$p['document'],
|
||||
'calculation'=>array_replace($p['calculation_entry']['snapshot']['result'],['temporal_state'=>'issue_simulated','exigibility_date'=>$p['expected_date'],'assessment_period'=>substr($p['expected_date'],0,7),'pending'=>[]]),'prior_calculation_snapshot'=>$p['calculation_entry']['snapshot'],'calculation_id'=>$p['calculation_entry']['id'],'profile_identifier'=>$p['calculation_entry']['snapshot']['input']['profile_identifier'],'profile_version'=>$p['calculation_entry']['snapshot']['input']['profile_version'],'profile'=>$p['profile'],'rate_percent'=>$p['profile']['rate_percent'],
|
||||
'exigibility_date'=>$p['expected_date'],'assessment_period'=>substr($p['expected_date'],0,7),'simulation_only'=>true];
|
||||
}
|
||||
public function multiLineSnapshot(array $payload,int $number,string $instant):array{abort_unless(($payload['calculation_policy']??null)===MultiLineInvoiceCalculation::POLICY,422);return ['calculation_policy'=>MultiLineInvoiceCalculation::POLICY]+$this->snapshot($payload,$number,$instant);}
|
||||
public function issue(int $draft,int $actor,array $input,string $key): array
|
||||
{
|
||||
return $this->command($actor,'service_invoices.issue',function($s)use($draft,$actor,$input,$key) {
|
||||
@@ -170,7 +176,7 @@ final class ServiceInvoicing
|
||||
DB::table('si_counters')->where('id',$counter->id)->update(['last_number'=>$number]);
|
||||
$instant=now()->toIso8601String();
|
||||
abort_unless(\Carbon\CarbonImmutable::parse($instant)->timezone(self::ZONE)->format('Y-m-d')===$p['expected_date'],409,'Data mudou durante a confirmação; reveja antes de emitir.');
|
||||
$snapshot=$this->snapshot($p,$number,$instant);
|
||||
$snapshot=isset($p['calculation_policy'])?$this->multiLineSnapshot($p,$number,$instant):$this->snapshot($p,$number,$instant);
|
||||
$snapshot['document_lines']=InvoicePdf::lines($snapshot); // Freeze browser representation as well as PDF bytes.
|
||||
$bytes=$this->pdf->render($snapshot);
|
||||
$id=\App\Shared\Context\ModuleRecords::query('si_invoices')->insertGetId($s+['draft_id'=>$draft,'source_id'=>$p['document']['source_id'],'series_id'=>$p['series_id'],'rule_id'=>$p['rule_id'],'series_code'=>$sd['code'],'number'=>$number,'snapshot'=>json_encode($snapshot,JSON_THROW_ON_ERROR),'pdf_base64'=>base64_encode($bytes),'pdf_sha256'=>hash('sha256',$bytes),'actor_id'=>$actor,'issued_at'=>$instant]);
|
||||
@@ -185,13 +191,13 @@ final class ServiceInvoicing
|
||||
public function show(int $id,int $actor): array
|
||||
{
|
||||
self::guard(); $s=$this->scope($actor,'service_invoices.view');
|
||||
$r=\App\Shared\Context\ModuleRecords::query('si_invoices')->where($s)->where('id',$id)->first(); abort_unless($r,403,'Documento fora do âmbito.');
|
||||
$r=QualifiedInvoiceSource::query()->where($s)->where('id',$id)->first(); abort_unless($r,403,'Documento fora do âmbito.');
|
||||
$snapshot=json_decode($r->snapshot,true,32,JSON_THROW_ON_ERROR);
|
||||
return ['id'=>$r->id,'snapshot'=>$snapshot,'document_lines'=>$snapshot['document_lines'] ?? InvoicePdf::lines($snapshot),'pdf_sha256'=>$r->pdf_sha256,'corrections'=>DB::table('si_correction_requests')->where('invoice_id',$id)->get(['id','reason','actor_id','created_at'])->all()];
|
||||
}
|
||||
public function pdf(int $id,int $actor): string
|
||||
{
|
||||
$this->show($id,$actor); $r=\App\Shared\Context\ModuleRecords::query('si_invoices')->where('id',$id)->first(['pdf_base64','pdf_sha256']);
|
||||
$this->show($id,$actor); $r=QualifiedInvoiceSource::query()->where('id',$id)->first(['pdf_base64','pdf_sha256']);
|
||||
$bytes=base64_decode($r->pdf_base64,true); abort_unless($bytes!==false && hash_equals($r->pdf_sha256,hash('sha256',$bytes)),409,'Integridade do PDF não confirmada.'); return $bytes;
|
||||
}
|
||||
public function requestCorrection(int $id,int $actor,array $input): array
|
||||
|
||||
@@ -90,7 +90,7 @@ final class ServiceReceivables
|
||||
$right=$kind==='correction'?'correction_request':'prepare';
|
||||
if(isset($d['amount'])) $d['amount']=self::money($d['amount'],$kind!=='observation');
|
||||
return $this->command($actor,$right,$key,$d,function($s)use($actor,$d,$kind) {
|
||||
$inv=\App\Shared\Context\ModuleRecords::query('si_invoices')->where($s)->where('id',$d['invoice_id'])->first();
|
||||
$inv=QualifiedInvoiceSource::query()->where($s)->where('id',$d['invoice_id'])->first();
|
||||
abort_unless($inv,403,'Documento de origem não autorizado.');
|
||||
$snapshot=json_decode($inv->snapshot,true,32,JSON_THROW_ON_ERROR);
|
||||
abort_unless(($snapshot['simulation_only']??false)===true && ($snapshot['notice']??'')===self::NOTICE && ($snapshot['document']['data']['currency']??'')==='MZN',422,'Origem sintética compatível necessária.');
|
||||
@@ -252,7 +252,7 @@ final class ServiceReceivables
|
||||
return DB::transaction(function()use($s) {
|
||||
DB::table('companies')->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();
|
||||
$records=DB::table('sr_records')->where($s)->orderBy('id')->get();
|
||||
return ['blocked'=>false,'notice'=>self::NOTICE,'simulation'=>self::SIMULATION,'records'=>$records->map(function($r){$r->data=$this->data($r);return $r;})->all(),'obligations'=>$records->where('kind','obligation')->map(fn($r)=>$this->state($r->id))->values()->all(),'documents'=>\App\Shared\Context\ModuleRecords::query('si_invoices')->where($s)->get(['id','series_code','number'])->all()];
|
||||
return ['blocked'=>false,'notice'=>self::NOTICE,'simulation'=>self::SIMULATION,'records'=>$records->map(function($r){$r->data=$this->data($r);return $r;})->all(),'obligations'=>$records->where('kind','obligation')->map(fn($r)=>$this->state($r->id))->values()->all(),'documents'=>QualifiedInvoiceSource::query()->where($s)->get(['id','series_code','number'])->all()];
|
||||
});
|
||||
}
|
||||
/** Scoped single-position read reuses the authoritative state calculation without writer locks. */
|
||||
|
||||
Reference in New Issue
Block a user