144 lines
22 KiB
PHP
144 lines
22 KiB
PHP
<?php
|
|
namespace App\Modules\Services;
|
|
use App\Modules\Identity\Services\{Permissions,Audit};use App\Shared\Context\{CompanyContext,ModuleRecords};use App\Shared\Installation\CompositionRuntime;use Illuminate\Support\Facades\{DB,Crypt,Validator};
|
|
|
|
/** Reviewed, versioned synthetic documents. Original invoice and receivable writers own effects. */
|
|
final class ServiceDocuments
|
|
{
|
|
public function __construct(private CompanyContext $context,private Permissions $permissions){}
|
|
private function json(array $v):string{return \Tempo\Finance\Payables\Payables::canonical($v);}
|
|
private function hash(mixed $v):string{return hash('sha256',$this->json($v));}
|
|
private function scope(int $actor,string $right='view'):array
|
|
{
|
|
abort_unless(config('service_documents.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']);
|
|
foreach(['view',$right] as $r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.'.$r,$s['unit_id']);return $s;
|
|
}
|
|
private function inputs():array
|
|
{
|
|
$p=CompositionRuntime::physical();$file='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-'.$p['composition'].'/single-document-inputs.json';$pin=config('service_documents.inputs_sha256');
|
|
abort_unless(is_string($pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503);return json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);
|
|
}
|
|
private function current(array $s,string $key):?object{return DB::table('sd_versions')->where($s)->where('document_key',$key)->orderByDesc('version')->first();}
|
|
private function invoice(array $s,int $id):object
|
|
{
|
|
$allowed=$this->inputs();$i=ModuleRecords::query('si_invoices')->where($s)->where('id',$id)->first();abort_unless($i&&in_array($i->draft_id,array_column($allowed,'draft_id'),true),403,'Documento fora das novas origens QA autorizadas.');return $i;
|
|
}
|
|
private function right(array $d):string{return match($d['action']){'review','cancel_review'=>'review','issue'=>'issue',default=>'prepare'};}
|
|
private function basis(int $actor,array $s,array $d):array
|
|
{
|
|
$old=$this->current($s,$d['document_key']);abort_unless(($old?->version??0)===$d['version'],409,'Versão alterada.');
|
|
if($old&&$d['action']!=='prepare'){$data=json_decode($old->data,true);$kind=$old->kind;}else{$data=$d['data'];$kind=$d['kind'];}
|
|
if(str_starts_with($d['action'],'cancel_')){
|
|
abort_unless($old&&$old->state==='issued'&&$old->kind===$d['kind']&&$d['data']===[],409);$out=DB::table('sd_outputs')->where($s)->where('document_key',$old->document_key)->firstOrFail();$this->invoice($s,$out->invoice_id);$effect=DB::table('sr_effects')->find($out->effect_id);$last=DB::table('sd_cancellations')->where($s)->where('output_id',$out->id)->orderByDesc('version')->first();$period=DocumentPeriod::open($s);return ['old'=>(array)$old,'data'=>$data,'kind'=>$kind,'origin'=>['output'=>(array)$out,'effect'=>(array)$effect,'last'=>$last?(array)$last:null,'revision'=>DB::table('sr_effects')->where('obligation_id',$effect->obligation_id)->count(),'position'=>app(ServiceReceivables::class)->position($effect->obligation_id,$actor),'period'=>$period]];
|
|
}
|
|
abort_unless(!$old||$kind===$d['kind'],409);$origin=[];
|
|
if($kind==='invoice_receipt'){
|
|
abort_unless(config('service_documents.invoice_receipt_enabled')===true,403,'Facturação-recibo pendente dos acessos de consulta documental apresentados.');
|
|
$allowed=$this->inputs()['invoice_receipt'];abort_unless(($data['draft_id']??null)===$allowed['draft_id']&&($data['series_id']??null)===$allowed['series_id']&&($data['rule_id']??null)===$allowed['rule_id'],403);
|
|
// Readiness uses the proposed document authority; final issue retains the old SI right.
|
|
$review=app(ServiceInvoicing::class)->reviewDocumentIntent($data['draft_id'],$data['series_id'],$data['rule_id'],$actor);
|
|
abort_unless($review['ready'],422,'Origem documental ainda não pronta.');$origin=['invoice_review_hash'=>$review['review_hash'],'invoice_review'=>$review['payload']];
|
|
}else{
|
|
$invoice=$this->invoice($s,(int)($data['invoice_id']??0));$origin=['invoice_id'=>$invoice->id,'invoice_sha256'=>hash('sha256',$invoice->snapshot)];
|
|
$ob=DB::table('sr_records')->where($s)->where(['invoice_id'=>$invoice->id,'kind'=>'obligation'])->first();abort_unless($ob,409,'Reconhecimento explícito da obrigação necessário.');
|
|
$origin['obligation_id']=$ob->id;$origin['revision']=DB::table('sr_effects')->where('obligation_id',$ob->id)->count();
|
|
if($kind==='receipt'){
|
|
$effect=DB::table('sr_effects')->where(['obligation_id'=>$ob->id,'id'=>$data['allocation_id']??0,'kind'=>'allocate'])->first();abort_unless($effect,403,'Escolha a afectação efectiva da nova origem.');
|
|
abort_if(DB::table('sr_effects')->where('target_effect_id',$effect->id)->exists(),409,'Afectação revertida; o recibo não pode ser emitido.');$origin['allocation']=(array)$effect;
|
|
abort_if(DB::table('sd_outputs')->where($s)->where(['kind'=>'receipt','effect_id'=>$effect->id])->where('document_key','<>',$d['document_key'])->exists(),409,'A afectação já tem recibo. Consulte o documento original.');
|
|
}
|
|
}
|
|
return ['old'=>$old?(array)$old:null,'data'=>$data,'kind'=>$kind,'origin'=>$origin];
|
|
}
|
|
private function lock(int $actor,array $s):void
|
|
{
|
|
DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where('tenant_id',$s['tenant_id'])->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();
|
|
DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();
|
|
abort_unless(DB::table('company_memberships')->where(['user_id'=>$actor,'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->lockForUpdate()->first(),403);
|
|
DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->where(fn($q)=>$q->whereNull('unit_id')->orWhere('unit_id',$s['unit_id']))->orderBy('id')->lockForUpdate()->get();
|
|
}
|
|
public function preview(int $actor,array $input,string $key):array
|
|
{
|
|
$rules=['action'=>'required|in:prepare,review,issue,cancel_prepare,cancel_review','kind'=>'required|in:receipt,credit_note,invoice_receipt','document_key'=>'required|uuid','version'=>'required|integer|min:0','reason'=>'required|string|min:10|max:4000','data'=>'present|array'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();abort_unless(preg_match('/^[a-f0-9-]{36}$/D',$key),422);$s=$this->scope($actor,$this->right($d));
|
|
return DB::transaction(function()use($actor,$s,$d,$key){$this->lock($actor,$s);$this->scope($actor,$this->right($d));$b=$this->basis($actor,$s,$d);$level=DB::transactionLevel();DB::beginTransaction();try{$out=$this->execute($actor,$s,$d,$b,$key);}finally{while(DB::transactionLevel()>$level)DB::rollBack();}
|
|
$cut=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'input'=>$d,'key'=>$key,'basis'=>$this->hash($b),'impact'=>$out['impact'],'observed'=>time()];return ['preview'=>Crypt::encryptString($this->json($cut)),'impact'=>$out['impact']];});
|
|
}
|
|
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=$c['input'];$s=$this->scope($actor,$this->right($d));
|
|
return DB::transaction(function()use($actor,$s,$c,$d){$this->lock($actor,$s);$this->scope($actor,$this->right($d));abort_unless($c['actor']===$actor&&$c['scope']==$s&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Contexto ou prévia expirados.');
|
|
$old=DB::table('sd_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$c['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409);return json_decode($old->result,true)+['replayed'=>true];}
|
|
$b=$this->basis($actor,$s,$d);abort_unless($this->hash($b)===$c['basis'],409,'A origem ou dependências mudaram. Confira novamente.');$out=$this->execute($actor,$s,$d,$b,$c['key']);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409,'Impacto alterado.');
|
|
DB::table('sd_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$c['key'],'request_hash'=>$this->hash($d),'result'=>$this->json($out),'created_at'=>now()]);Audit::record('service_document.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'reason'=>$d['reason'],'before'=>$b['old'],'after'=>$out]);return $out+['replayed'=>false];});
|
|
}
|
|
private function execute(int $actor,array $s,array $d,array $b,string $key):array
|
|
{
|
|
$old=$b['old'];$kind=$b['kind'];$data=$b['data'];$action=$d['action'];$prepared=$old['prepared_by']??$actor;$effect=null;$output=null;
|
|
if(str_starts_with($action,'cancel_'))return $this->cancel($actor,$s,$d,$b,$key);
|
|
if($action==='prepare'){
|
|
abort_if($old&&$kind==='credit_note',409,'A preparação financeira é imutável; conserve-a e prepare outro processo após a recusa.');
|
|
abort_unless(!$old||in_array($old['state'],['draft','refused'],true),409);abort_unless(!$old||$prepared===$actor,403);
|
|
$rules=match($kind){'receipt'=>['invoice_id'=>'required|integer','allocation_id'=>'required|integer'], 'credit_note'=>['invoice_id'=>'required|integer','amount'=>'required|string','explanation'=>'required|string|min:10|max:1000'], 'invoice_receipt'=>['draft_id'=>'required|integer','series_id'=>'required|integer','rule_id'=>'required|integer','payment_date'=>'required|date_format:Y-m-d|before_or_equal:today','evidence'=>'required|string|min:10|max:1000']};
|
|
abort_if(array_diff(array_keys($d['data']),array_keys($rules)),422);$data=Validator::make($d['data'],$rules)->validate();
|
|
if($kind==='credit_note'){
|
|
$data['amount']=ServiceReceivables::money($data['amount']);abort_unless($data['amount']==='100.00',422,'Este recorte QA limita o crédito a100MZN.');
|
|
$record=app(ServiceReceivables::class)->propose($actor,['kind'=>'correction','invoice_id'=>$data['invoice_id'],'obligation_id'=>$b['origin']['obligation_id'],'currency'=>'MZN','amount'=>$data['amount'],'source'=>'SIM-DOCUMENTOS','external_id'=>'SD-'.$d['document_key'],'revision'=>$b['origin']['revision'],'action'=>'reduction','document_ref'=>'TEST-REDUCTION-'.$d['document_key'],'line'=>'1','reason'=>$data['explanation']],$key);
|
|
$data['correction_id']=$record['id'];
|
|
}
|
|
if($kind==='invoice_receipt')foreach(['prepare','allocate'] as $r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_receivables.'.$r,$s['unit_id']);
|
|
$state='draft';
|
|
}elseif($action==='review'){
|
|
abort_unless($old&&$old['state']==='draft'&&$prepared!==$actor,403);$review=Validator::make($d['data'],['decision'=>'required|in:approved,refused'])->validate();abort_if(array_diff(array_keys($d['data']),['decision']),422);$state=$review['decision']==='approved'?'approved':'refused';
|
|
}else{
|
|
abort_unless($old&&$old['state']==='approved'&&$d['data']===[],409);$state='issued';
|
|
if($kind==='credit_note'){
|
|
$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.credit_apply',$s['unit_id']);
|
|
$decided=app(ServiceReceivables::class)->decide($data['correction_id'],$actor,['version'=>1,'revision'=>$b['origin']['revision'],'decision'=>'approved','reason'=>$d['reason']],$key);$effect=$decided['id'];$invoice=$data['invoice_id'];$amount=$data['amount'];
|
|
}elseif($kind==='receipt'){$effect=$data['allocation_id'];$invoice=$data['invoice_id'];$amount=$b['origin']['allocation']['amount'];}
|
|
else{
|
|
$review=app(ServiceInvoicing::class)->review($data['draft_id'],$data['series_id'],$data['rule_id'],$actor);$issued=app(ServiceInvoicing::class)->issue($data['draft_id'],$actor,['series_id'=>$data['series_id'],'rule_id'=>$data['rule_id'],'review_hash'=>$review['review_hash']],$key);$invoice=$issued['id'];$amount='1000.00';
|
|
$aggregate=app(ServiceReceivables::class)->completeDocument($old['id'],$invoice,$actor,$key);$effect=$aggregate['allocation_id'];
|
|
}
|
|
}
|
|
$id=DB::table('sd_versions')->insertGetId($s+['document_key'=>$d['document_key'],'version'=>$d['version']+1,'previous_id'=>$old['id']??null,'kind'=>$kind,'action'=>$action,'state'=>$state,'prepared_by'=>$prepared,'actor_id'=>$actor,'data'=>$this->json($data),'dependencies'=>$this->json($b['origin']),'reason'=>$d['reason'],'created_at'=>now()]);
|
|
if($state==='issued'){
|
|
$number=(int)DB::table('sd_outputs')->where($s)->where('kind',$kind)->max('number')+1;$inv=$this->invoice($s,$invoice);$origin=json_decode($inv->snapshot,true);$label=match($kind){'receipt'=>'Recibo','credit_note'=>'Nota de credito',default=>'Factura-recibo'};
|
|
$lines=[['title',$label.' de TESTE'],['text','TEST-SD-'.strtoupper($kind).'/'.$number],['text',ServiceInvoicing::NOTICE],['text','Origem: '.$inv->series_code.'/'.$inv->number],['text','Cliente: '.($origin['document']['client']['name']??'Cliente QA')],['text','Montante: '.$amount.' MZN'],['text','Autor da preparacao: '.$prepared],['text','Autor da emissao: '.$actor],['text','Fundamento: '.$d['reason']],['text','Sem dinheiro real. Sem efeito automatico em stock.']];
|
|
if($kind==='credit_note')$lines[]=['text','Reducao nominal de teste; sem reparticao fiscal certificada.'];if($kind==='invoice_receipt')$lines[]=['text','Base862,07 + imposto documental137,93 =1000,00 MZN.'];
|
|
$snapshot=['document_lines'=>$lines,'kind'=>$kind,'invoice_id'=>$invoice,'effect_id'=>$effect,'amount'=>$amount,'simulation_only'=>true,'notice'=>ServiceInvoicing::NOTICE,'source_sha256'=>hash('sha256',$inv->snapshot),'prepared_by'=>$prepared,'issued_by'=>$actor,'issued_at'=>now()->toIso8601String()];$pdf=app(InvoicePdf::class)->render($snapshot);
|
|
$output=DB::table('sd_outputs')->insertGetId($s+['document_key'=>$d['document_key'],'version_id'=>$id,'kind'=>$kind,'number'=>$number,'invoice_id'=>$invoice,'effect_id'=>$effect,'snapshot'=>$this->json($snapshot),'pdf_base64'=>base64_encode($pdf),'pdf_sha256'=>hash('sha256',$pdf),'actor_id'=>$actor,'created_at'=>now()]);
|
|
}
|
|
$impact=['kind'=>$kind,'action'=>$action,'state'=>$state,'amount'=>$kind==='credit_note'?($data['amount']??null):($kind==='receipt'?($b['origin']['allocation']['amount']??null):'1000.00'),'physical_change'=>false,'financial_reduction'=>$kind==='credit_note'&&$action==='issue','atomic_invoice_receipt'=>$kind==='invoice_receipt'&&$action==='issue'];
|
|
return ['id'=>$id,'document_key'=>$d['document_key'],'version'=>$d['version']+1,'state'=>$state,'output_id'=>$output,'effect_id'=>$effect,'impact'=>$impact];
|
|
}
|
|
public function listing(int $actor,array $input):array
|
|
{
|
|
$s=$this->scope($actor);$d=Validator::make($input,['kind'=>'sometimes|in:receipt,credit_note,invoice_receipt','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','document_key'=>'sometimes|uuid'])->validate();abort_if(array_diff(array_keys($input),array_keys($d)),422);
|
|
$q=DB::table('sd_versions as v')->where(['v.tenant_id'=>$s['tenant_id'],'v.company_id'=>$s['company_id'],'v.unit_id'=>$s['unit_id']]);if(isset($d['document_key']))$q->where('v.document_key',$d['document_key']);else $q->whereNotExists(fn($x)=>$x->selectRaw('1')->from('sd_versions as n')->whereColumn('n.document_key','v.document_key')->whereColumn('n.tenant_id','v.tenant_id')->whereColumn('n.company_id','v.company_id')->whereColumn('n.unit_id','v.unit_id')->whereColumn('n.version','>','v.version'));
|
|
if(isset($d['kind']))$q->where('v.kind',$d['kind']);$p=$q->orderByDesc('v.id')->paginate((int)($d['per_page']??25),['v.*'],'page',(int)($d['page']??1));$outputs=DB::table('sd_outputs')->where($s)->whereIn('document_key',$p->pluck('document_key')->all())->get(['id','document_key','number','pdf_sha256'])->keyBy('document_key');foreach($p->items() as $row){$row->data=json_decode($row->data,true);$row->output=$outputs->get($row->document_key);}if(DB::getSchemaBuilder()->hasTable('sd_cancellations')){$events=DB::table('sd_cancellations')->where($s)->whereIn('output_id',$outputs->pluck('id')->all())->orderBy('version')->get()->groupBy('output_id');foreach($p->items() as $row)if($row->output)$row->output->cancellations=($events->get($row->output->id)??collect())->all();}$caps=[];foreach(['prepare','review','issue','credit_apply'] as $right)$caps[$right]=$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_documents.'.$right,$s['unit_id']);if(($d['kind']??null)==='invoice_receipt')foreach(['prepare','allocate'] as $right)$caps['prepare']=$caps['prepare']&&$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_receivables.'.$right,$s['unit_id']);if(($d['kind']??null)==='credit_note')$caps['prepare']=$caps['prepare']&&$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_receivables.correction_request',$s['unit_id']);$caps['cancel_prepare']=$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_documents.prepare',$s['unit_id'])&&$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_receivables.correction_request',$s['unit_id']);$caps['cancel_review']=$caps['review']&&$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_receivables.correction_approve',$s['unit_id']);return ['documents'=>$p,'notice'=>ServiceInvoicing::NOTICE,'inputs'=>$this->inputs(),'capabilities'=>$caps,'actor_id'=>$actor];
|
|
}
|
|
public function pdf(int $actor,int $id):object{$s=$this->scope($actor);$row=DB::table('sd_outputs')->where($s)->where('id',$id)->first();abort_unless($row,403);$this->invoice($s,$row->invoice_id);abort_unless(hash_equals($row->pdf_sha256,hash('sha256',base64_decode($row->pdf_base64,true))),409,'Integridade documental incompatível.');return $row;}
|
|
private function cancel(int $actor,array $s,array $d,array $b,string $key):array
|
|
{
|
|
$o=$b['origin'];$last=$o['last'];abort_if(($last['state']??null)==='cancelled',409,'Documento já anulado tecnicamente.');$target=$o['effect'];abort_if(DB::table('sr_effects')->where('target_effect_id',$target['id'])->exists(),409,'O efeito já foi revertido; exige reconciliação própria.');$sr=app(ServiceReceivables::class);
|
|
if($d['action']==='cancel_prepare'){
|
|
$record=$sr->propose($actor,['kind'=>'correction','invoice_id'=>$o['output']['invoice_id'],'obligation_id'=>$target['obligation_id'],'currency'=>'MZN','source'=>'SIM-SD-ANULACAO','external_id'=>'SD-CANCEL-'.$key,'revision'=>$o['revision'],'action'=>$b['kind']==='credit_note'?'reverse_reduction':'reverse_allocation','target_effect_id'=>$target['id'],'reason'=>$d['reason']],$key);$correction=$record['id'];$effect=null;$state='prepared';
|
|
}else{
|
|
abort_unless($last&&$last['state']==='prepared'&&$last['actor_id']!==$actor,403,'Revisão distinta da preparação da anulação necessária.');$correction=$last['correction_id'];$r=DB::table('sr_records')->find($correction);abort_unless((json_decode($r->data,true)['revision']??null)===$o['revision'],409,'O recebível mudou. O preparador deve renovar a conferência da anulação.');
|
|
if($b['kind']==='credit_note')$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.credit_apply',$s['unit_id']);$result=$sr->decide($correction,$actor,['version'=>1,'revision'=>$o['revision'],'decision'=>'approved','reason'=>$d['reason']],$key);$effect=$result['id'];$state='cancelled';
|
|
}
|
|
$id=DB::table('sd_cancellations')->insertGetId($s+['cancellation_key'=>$d['document_key'],'version'=>($last['version']??0)+1,'output_id'=>$o['output']['id'],'state'=>$state,'actor_id'=>$actor,'correction_id'=>$correction,'effect_id'=>$effect,'snapshot'=>$this->json(['target_effect_id'=>$target['id'],'output_sha256'=>$o['output']['pdf_sha256'],'revision'=>$o['revision'],'period'=>$o['period'],'invoice_preserved'=>true,'money_refunded'=>false]),'reason'=>$d['reason'],'created_at'=>now()]);
|
|
return ['id'=>$id,'document_key'=>$d['document_key'],'version'=>$d['version'],'cancellation_version'=>($last['version']??0)+1,'state'=>$state,'effect_id'=>$effect,'impact'=>['kind'=>$b['kind'],'action'=>$d['action'],'state'=>$state,'amount'=>$target['amount'],'balance_increase_on_confirmation'=>$target['amount'],'balance_before'=>$o['position']['balance'],'balance_after_confirmation'=>(string)\Brick\Math\BigDecimal::of($o['position']['balance'])->plus($target['amount'])->toScale(2),'invoice_preserved'=>true,'physical_change'=>false,'money_refunded'=>false,'financial_reduction'=>false,'atomic_invoice_receipt'=>false]];
|
|
}
|
|
public function options(int $actor,array $input):array
|
|
{
|
|
$s=$this->scope($actor);$d=Validator::make($input,['view'=>'required|in:invoices,allocations','invoice_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100'])->validate();abort_if(array_diff(array_keys($input),array_keys($d)),422);
|
|
$invoices=ModuleRecords::query('si_invoices')->where($s)->whereIn('draft_id',array_column($this->inputs(),'draft_id'));
|
|
if($d['view']==='invoices')$q=$invoices->select('id','series_code','number','draft_id');else{
|
|
$invoice=$this->invoice($s,$d['invoice_id']??0);$q=DB::table('sr_effects as e')->join('sr_records as r','r.id','=','e.record_id')->where(['r.tenant_id'=>$s['tenant_id'],'r.company_id'=>$s['company_id'],'r.unit_id'=>$s['unit_id'],'r.invoice_id'=>$invoice->id,'e.kind'=>'allocate'])->whereNotExists(fn($v)=>$v->selectRaw('1')->from('sr_effects as rev')->whereColumn('rev.target_effect_id','e.id'))->select('e.id','e.amount','r.invoice_id','e.created_at');
|
|
}return ['records'=>$q->orderBy('id')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1))];
|
|
}
|
|
public function sourceInvoices(int $actor){$s=$this->scope($actor);return ModuleRecords::query('si_invoices')->where($s)->whereIn('draft_id',array_column($this->inputs(),'draft_id'));}
|
|
}
|