Files
SGF/app/Modules/Services/Http/MultiLineInvoiceController.php
2026-11-11 15:04:01 +02:00

32 lines
12 KiB
PHP

<?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];});}
}