Files
SGF/app/Modules/Services/BillingRegister.php
T
2026-11-11 09:00:17 +02:00

42 lines
9.5 KiB
PHP

<?php
namespace App\Modules\Services;
use App\Modules\Identity\Services\Permissions;use App\Shared\Context\{CompanyContext,ModuleRecords};use App\Shared\Installation\CompositionRuntime;use Illuminate\Support\Facades\{DB,Validator};
/** Read-only register. SQL filters/pages precede bounded financial enrichment. */
final class BillingRegister
{
public function __construct(private CompanyContext $context,private Permissions $permissions){}
public function listing(int $actor,array $input):array
{
abort_unless(config('service_documents.enabled')===true,404);CompositionRuntime::assert();$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']);
$rules=['kind'=>'sometimes|in:invoice,invoice_receipt,receipt,credit_note,quote,draft','search'=>'sometimes|nullable|string|max:100','from'=>'sometimes|date_format:Y-m-d','to'=>'sometimes|date_format:Y-m-d','currency'=>'sometimes|regex:/^[A-Z]{3}$/','series'=>'sometimes|string|max:100','author'=>'sometimes|integer|min:1','state'=>'sometimes|in:issued,cancelled,draft,confirmed_internal,superseded,approved,rejected,refused,customer_accepted,customer_declined,converted','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();abort_if(isset($d['from'],$d['to'])&&$d['from']>$d['to'],422,'Intervalo de emissão inválido.');
$has=fn($right)=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],$right,$s['unit_id']);$rights=['invoice'=>$has('service_invoices.view'),'additional'=>$has('service_documents.view'),'quote'=>$has('commercial_quotes.view'),'draft'=>$has('billing_drafts.view'),'finance'=>$has('service_receivables.view')];abort_unless($rights['invoice']||$rights['additional']||$rights['quote']||$rights['draft'],403);
return DB::transaction(function()use($actor,$s,$d,$rights){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$families=[];
$invoiceQuery=fn()=>ModuleRecords::query('si_invoices')->where($s);
if($rights['invoice']){$q=$invoiceQuery();if($rights['additional'])$q->whereNotExists(fn($v)=>$v->selectRaw('1')->from('sd_outputs as fr')->whereColumn('fr.invoice_id','si_invoices.id')->where(['fr.tenant_id'=>$s['tenant_id'],'fr.company_id'=>$s['company_id'],'fr.unit_id'=>$s['unit_id'],'fr.kind'=>'invoice_receipt'])->whereIn('fr.invoice_id',app(ServiceDocuments::class)->sourceInvoices($actor)->select('id')));
$families[]=$q->selectRaw("'invoice'::text kind,id source_id,series_code||'/'||number::text reference,snapshot->'document'->'client'->>'name' client,snapshot->'document'->'client'->'data'->>'nuit' nuit,(issued_at AT TIME ZONE 'Africa/Maputo')::date document_on,snapshot->'calculation'->>'total_document_proposed' amount,NULL::text base,'MZN'::text currency,'issued'::text document_state,actor_id,id invoice_id,NULL::text source_key,1::integer version,issued_at sort_at");}
if($rights['additional']){
$q=DB::table('sd_outputs as o')->join('si_invoices as i','i.id','=','o.invoice_id')->where(['o.tenant_id'=>$s['tenant_id'],'o.company_id'=>$s['company_id'],'o.unit_id'=>$s['unit_id']])->whereIn('i.id',app(ServiceDocuments::class)->sourceInvoices($actor)->select('id'));
$families[]=$q->selectRaw("o.kind,o.id source_id,'TEST-SD-'||upper(o.kind)||'/'||o.number::text reference,i.snapshot->'document'->'client'->>'name' client,i.snapshot->'document'->'client'->'data'->>'nuit' nuit,(o.created_at AT TIME ZONE 'Africa/Maputo')::date document_on,o.snapshot->>'amount' amount,NULL::text base,'MZN'::text currency,CASE WHEN EXISTS(SELECT 1 FROM sd_cancellations c WHERE c.output_id=o.id AND c.state='cancelled') THEN 'cancelled' ELSE 'issued' END document_state,o.actor_id,o.invoice_id,o.document_key::text source_key,3::integer version,o.created_at sort_at");
}
if($rights['draft']){
$draftState="CASE WHEN EXISTS(SELECT 1 FROM b_internal_confirmations b WHERE b.billing_draft_id=a_billing_drafts.id AND b.tenant_id=a_billing_drafts.tenant_id AND b.company_id=a_billing_drafts.company_id AND b.unit_id=a_billing_drafts.unit_id) THEN 'confirmed_internal' ELSE 'draft' END";
if(DB::getSchemaBuilder()->hasTable('wc_applications'))$draftState="CASE WHEN EXISTS(SELECT 1 FROM wc_applications w WHERE w.old_billing_id=a_billing_drafts.id AND w.kind='replace') THEN 'superseded' WHEN EXISTS(SELECT 1 FROM wc_applications w WHERE w.old_billing_id=a_billing_drafts.id) THEN 'cancelled' ELSE ".$draftState.' END';
$families[]=ModuleRecords::query('a_billing_drafts')->where($s)->selectRaw("'draft'::text kind,id source_id,'Interno #'||id::text reference,data->>'client_name' client,NULL::text nuit,NULL::date document_on,NULL::text amount,NULL::text base,data->>'currency' currency,".$draftState." document_state,(SELECT h.actor_id FROM a_history h WHERE h.area='billing_drafts' AND h.record_id=a_billing_drafts.id AND h.version=a_billing_drafts.version AND h.tenant_id=a_billing_drafts.tenant_id AND h.company_id=a_billing_drafts.company_id AND h.unit_id=a_billing_drafts.unit_id) actor_id,NULL::bigint invoice_id,NULL::text source_key,version,updated_at sort_at");
}
if($rights['quote']){
$q=DB::table('cq_versions as q')->where(['q.tenant_id'=>$s['tenant_id'],'q.company_id'=>$s['company_id'],'q.unit_id'=>$s['unit_id']])->whereIn('q.client_id',ModuleRecords::query('a_clients')->select('id'))->whereNotExists(fn($v)=>$v->selectRaw('1')->from('cq_versions as n')->whereColumn('n.quote_key','q.quote_key')->whereColumn('n.tenant_id','q.tenant_id')->whereColumn('n.company_id','q.company_id')->whereColumn('n.unit_id','q.unit_id')->whereColumn('n.version','>','q.version'));
$families[]=$q->selectRaw("'quote'::text kind,q.id source_id,'ORC-QA-'||upper(substr(q.quote_key::text,1,8)) reference,q.dependencies->'client'->>'name' client,NULL::text nuit,NULL::date document_on,NULL::text amount,q.data->>'base' base,q.data->>'currency' currency,q.state document_state,q.actor_id,NULL::bigint invoice_id,q.quote_key::text source_key,q.version,q.created_at sort_at");
}
$union=array_shift($families);foreach($families as $q)$union->unionAll($q);$q=DB::query()->fromSub($union,'document_register');if(isset($d['kind']))$q->where('kind',$d['kind']);if(isset($d['state']))$q->where('document_state',$d['state']);if(isset($d['from']))$q->where('document_on','>=',$d['from']);if(isset($d['to']))$q->where('document_on','<=',$d['to']);
if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(fn($v)=>$v->whereRaw("reference ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("client ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("nuit ILIKE ? ESCAPE '!'",[$needle]));}
if(isset($d['currency']))$q->where('currency',$d['currency']);if(isset($d['author']))$q->where('actor_id',$d['author']);if(isset($d['series']))$q->whereRaw("CASE WHEN kind IN('invoice','invoice_receipt','receipt','credit_note') THEN split_part(reference,'/',1) ELSE NULL END = ?",[$d['series']]);
$p=$q->orderByDesc('sort_at')->orderBy('kind')->orderByDesc('source_id')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1));$ids=$p->pluck('invoice_id')->filter()->unique()->values()->all();$positions=collect();$draftIds=collect($p->items())->where('kind','draft')->pluck('source_id')->all();$bases=[];
if($draftIds!==[])foreach(ModuleRecords::query('a_billing_drafts')->where($s)->whereIn('id',$draftIds)->get(['id','data']) as $draft)$bases[$draft->id]=DraftValues::base(json_decode($draft->data,true,32,JSON_THROW_ON_ERROR));
if($rights['finance']&&$ids!==[]){$positions=DB::table('sr_records as register_ob')->leftJoin('sr_effects as e','e.obligation_id','=','register_ob.id')->where(['register_ob.tenant_id'=>$s['tenant_id'],'register_ob.company_id'=>$s['company_id'],'register_ob.unit_id'=>$s['unit_id'],'register_ob.kind'=>'obligation'])->whereIn('register_ob.invoice_id',$ids)->groupBy('register_ob.id','register_ob.invoice_id','register_ob.amount')->selectRaw("register_ob.invoice_id,CASE WHEN count(*) FILTER(WHERE e.kind='open')>0 THEN (register_ob.amount-coalesce(sum(CASE WHEN e.kind IN('allocate','reduction') THEN e.amount WHEN e.kind IN('reverse_allocation','reverse_reduction') THEN -e.amount ELSE 0 END),0))::text ELSE NULL END balance")->get()->keyBy('invoice_id');}
foreach($p->items() as $row){if($row->kind==='draft')$row->base=$bases[$row->source_id]??null;$row->series=in_array($row->kind,['invoice','invoice_receipt','receipt','credit_note'],true)?explode('/',$row->reference,2)[0]:null;$row->origin_invoice_balance=$rights['finance']?$positions->get($row->invoice_id)?->balance:null;$row->balance_available=$rights['finance']&&$row->origin_invoice_balance!==null;$row->pdf_url=match($row->kind){'invoice'=>'/api/service-invoices/'.$row->source_id.'/pdf','invoice_receipt','receipt','credit_note'=>'/api/service-documents/'.$row->source_id.'/pdf',default=>null};$row->print_url=$row->kind==='quote'?'/commercial-quotes/'.$row->source_key.'/print?version='.$row->version:null;}
return ['documents'=>$p,'financial_access'=>$rights['finance'],'notice'=>ServiceInvoicing::NOTICE,'has_mixed_document_types'=>true,'totals_not_summed'=>true];
});
}
}