commit melhorias sgf
This commit is contained in:
@@ -85,7 +85,7 @@ final class ServiceReceivables
|
||||
public function propose(int $actor,array $input,string $key): array
|
||||
{
|
||||
self::guard();
|
||||
$d=$this->validate($input,['kind'=>'required|in:obligation,receipt,observation,correction','invoice_id'=>'required|integer|min:1','obligation_id'=>'nullable|integer|min:1','receipt_id'=>'nullable|integer|min:1','currency'=>'required|in:MZN','amount'=>'nullable|string','source'=>'required|string|min:3|max:80','external_id'=>'required|string|min:3|max:100','date'=>'nullable|date_format:Y-m-d','evidence'=>'nullable|string|min:3|max:1000','due_date'=>'nullable|date_format:Y-m-d','reason'=>'nullable|string|min:3|max:1000','action'=>'nullable|in:reverse_allocation,reverse_reconcile,reverse_receipt,reduction,due','target_effect_id'=>'nullable|integer|min:1','document_ref'=>'nullable|string|max:100','line'=>'nullable|in:1','revision'=>'required|integer|min:0']);
|
||||
$d=$this->validate($input,['kind'=>'required|in:obligation,receipt,observation,correction','invoice_id'=>'required|integer|min:1','obligation_id'=>'nullable|integer|min:1','receipt_id'=>'nullable|integer|min:1','currency'=>'required|in:MZN','amount'=>'nullable|string','source'=>'required|string|min:3|max:80','external_id'=>'required|string|min:3|max:100','date'=>'nullable|date_format:Y-m-d','evidence'=>'nullable|string|min:3|max:1000','due_date'=>'nullable|date_format:Y-m-d','reason'=>'nullable|string|min:3|max:1000','action'=>'nullable|in:reverse_allocation,reverse_reconcile,reverse_receipt,reduction,reverse_reduction,due','target_effect_id'=>'nullable|integer|min:1','document_ref'=>'nullable|string|max:100','line'=>'nullable|in:1','revision'=>'required|integer|min:0']);
|
||||
$kind=$d['kind'];
|
||||
$right=$kind==='correction'?'correction_request':'prepare';
|
||||
if(isset($d['amount'])) $d['amount']=self::money($d['amount'],$kind!=='observation');
|
||||
@@ -121,10 +121,10 @@ final class ServiceReceivables
|
||||
abort_unless(isset($d['amount'],$d['document_ref'],$d['line']) && str_starts_with($d['document_ref'],'TEST-REDUCTION-'),422,'Documento redutor sintético e linha necessários.');
|
||||
$amount=$d['amount'];
|
||||
}
|
||||
if(in_array($d['action'],['reverse_allocation','reverse_reconcile'])) {
|
||||
if(in_array($d['action'],['reverse_allocation','reverse_reconcile','reverse_reduction'])) {
|
||||
$effect=DB::table('sr_effects')->where('obligation_id',$ob->id)->where('id',$d['target_effect_id']??0)->first();
|
||||
abort_unless($effect && $effect->kind===($d['action']==='reverse_allocation'?'allocate':'reconcile'),422,'Alvo de reversão incompatível.');
|
||||
$receipt=$this->row($effect->receipt_id,$s,'receipt'); $amount=$effect->amount;
|
||||
abort_unless($effect && $effect->kind===(match($d['action']){'reverse_allocation'=>'allocate','reverse_reduction'=>'reduction',default=>'reconcile'}),422,'Alvo de reversão incompatível.');
|
||||
if($d['action']==='reverse_reduction'){abort_unless(config('service_documents.enabled')===true&&DB::table('sd_outputs')->where($s)->where(['kind'=>'credit_note','effect_id'=>$effect->id])->exists(),403);DocumentPeriod::open($s);}else $receipt=$this->row($effect->receipt_id,$s,'receipt'); $amount=$effect->amount;
|
||||
}
|
||||
if($d['action']==='reverse_receipt') { abort_unless($receipt,422); $amount=$receipt->amount; }
|
||||
}
|
||||
@@ -153,7 +153,7 @@ final class ServiceReceivables
|
||||
if($r->kind==='correction') {
|
||||
abort_unless(($rd['revision']??-1)===$d['revision'],409,'Proposta ligada a revisão anterior; prepare nova proposta.');
|
||||
$kind=$rd['action']; $amount=$r->amount; $receipt=$r->receipt_id; $target=$rd['target_effect_id']??null;
|
||||
if($kind==='reduction' || $kind==='due') $receipt=null;
|
||||
if($kind==='reduction' || $kind==='due' || $kind==='reverse_reduction') $receipt=null;if($kind==='reverse_reduction'){DocumentPeriod::open($s);$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.credit_apply',$s['unit_id']);}
|
||||
}
|
||||
} elseif($r->kind==='observation') abort(422,'A conferência regista correspondência ou divergência; não é aprovação de recibo.');
|
||||
return $this->effect($id,$ob,$receipt,$kind,$amount,$target,$d['revision'],$actor,$d+['due_date'=>$rd['due_date']??null]);
|
||||
@@ -188,6 +188,36 @@ final class ServiceReceivables
|
||||
return $this->effect($id,$r->obligation_id,$id,'allocate',$d['amount'],null,$d['revision'],$actor,[]);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* One sealed QA invoice-receipt, using the original record/effect writers.
|
||||
* prepared_by is the actual prior declarant; actor_id on effects is the real executor.
|
||||
* No guard/context impersonation and no general delegation of prepare/allocate rights.
|
||||
*/
|
||||
public function completeDocument(int $versionId,int $invoiceId,int $actor,string $key):array
|
||||
{
|
||||
self::guard();abort_unless(config('service_documents.enabled')===true&&config('service_documents.invoice_receipt_enabled')===true&&DB::transactionLevel()>0,403);
|
||||
$s=$this->scope($actor,'confirm');foreach(['view','issue'] as $r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.'.$r,$s['unit_id']);
|
||||
$v=DB::table('sd_versions')->where($s)->where(['id'=>$versionId,'kind'=>'invoice_receipt','state'=>'approved'])->first();abort_unless($v&&$v->prepared_by!==$actor,403,'Intenção financeira distinta e revista necessária.');
|
||||
$latest=DB::table('sd_versions')->where($s)->where('document_key',$v->document_key)->max('version');abort_unless($latest===$v->version,409);
|
||||
$intent=json_decode($v->data,true,32,JSON_THROW_ON_ERROR);$preparer=(int)$v->prepared_by;
|
||||
abort_unless(DB::table('users')->where('id',$preparer)->lockForUpdate()->exists()&&DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$preparer])->lockForUpdate()->exists(),403);
|
||||
DB::table('permission_grants')->where($s)->where('user_id',$preparer)->orderBy('id')->lockForUpdate()->get();
|
||||
foreach(['service_documents.prepare','service_receivables.view','service_receivables.prepare','service_receivables.allocate'] as $right)$this->permissions->require($preparer,$s['tenant_id'],$s['company_id'],$right,$s['unit_id']);
|
||||
$inv=\App\Shared\Context\ModuleRecords::query('si_invoices')->where($s)->where(['id'=>$invoiceId,'draft_id'=>$intent['draft_id'],'series_id'=>$intent['series_id'],'rule_id'=>$intent['rule_id'],'actor_id'=>$actor])->first();abort_unless($inv,403,'A emissão deve pertencer à origem selada e ao executor actual.');
|
||||
$snap=json_decode($inv->snapshot,true,64,JSON_THROW_ON_ERROR);abort_unless(($snap['simulation_only']??null)===true&&($snap['notice']??null)===self::NOTICE&&($snap['calculation']['total_document_proposed']??null)==='1000.00'&&($snap['calculation']['currency']??null)==='MZN',422);
|
||||
abort_if(DB::table('sr_records')->where('invoice_id',$invoiceId)->exists()||DB::table('sd_outputs')->where('invoice_id',$invoiceId)->exists(),409,'Origem já utilizada.');
|
||||
$history=DB::table('a_history')->where($s)->where(['area'=>'billing_drafts','record_id'=>$inv->draft_id,'version'=>$snap['document']['draft_version']])->value('snapshot');abort_unless($history,409);$client=(int)json_decode($history,true)['client_id'];
|
||||
$source='SIM-FACTURA-RECIBO';$external='SD-'.$v->document_key;$reason='Conclusão atómica da intenção financeira QA revista: '.$v->document_key;
|
||||
$ob=$this->record($s,$preparer,'obligation',$inv,$client,null,null,'1000.00',$source,$external.'-OB',['due_date'=>null,'document_intent_id'=>$v->id]);$obId=$ob['id'];
|
||||
$this->effect($obId,$obId,null,'open','1000.00',null,0,$actor,['reason'=>$reason,'document_intent_id'=>$v->id]);
|
||||
$rc=$this->record($s,$preparer,'receipt',$inv,$client,$obId,null,'1000.00',$source,$external.'-RC',['date'=>$intent['payment_date'],'evidence'=>$intent['evidence'],'document_intent_id'=>$v->id]);
|
||||
$this->effect($rc['id'],$obId,null,'receipt_confirm','1000.00',null,1,$actor,['reason'=>$reason,'document_intent_id'=>$v->id]);
|
||||
$allocation=$this->effect($rc['id'],$obId,$rc['id'],'allocate','1000.00',null,2,$actor,['reason'=>$reason,'document_intent_id'=>$v->id,'aggregate_authority'=>'service_documents.issue']);
|
||||
$result=['id'=>$obId,'obligation_id'=>$obId,'receipt_id'=>$rc['id'],'allocation_id'=>$allocation['id'],'event'=>'invoice_receipt.simulation','balance'=>'0.00'];
|
||||
$command=DB::table('sr_commands')->insertGetId($s+['actor_id'=>$actor,'action'=>'document_issue','command_key'=>$key,'request_hash'=>hash('sha256',$this->json([$v->id,$invoiceId,$intent])),'result'=>$this->json($result),'created_at'=>now()]);
|
||||
$event=DB::table('sr_events')->insertGetId(['command_id'=>$command,'type'=>'invoice_receipt.simulation','schema_version'=>1,'payload'=>$this->json($s+['result'=>$result,'prepared_by'=>$preparer,'executed_by'=>$actor,'document_intent_id'=>$v->id]),'created_at'=>now()]);DB::table('sr_outbox')->insert(['event_id'=>$event,'state'=>'captured_simulation_only']);
|
||||
Audit::record('service_receivables.document_issue',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['command_id'=>$command,'prepared_by'=>$preparer,'document_intent_id'=>$v->id,'simulation_only'=>true]);return $result;
|
||||
}
|
||||
private function state(int $ob,?string $cut=null): array
|
||||
{
|
||||
$r=DB::table('sr_records')->find($ob); $rd=$this->data($r);
|
||||
@@ -204,7 +234,7 @@ final class ServiceReceivables
|
||||
$x=BigDecimal::of($e->amount)->multipliedBy($e->kind==='allocate'?1:-1); $allocated=$allocated->plus($x);
|
||||
$rc=&$receipts[$e->receipt_id]; $rc['allocated']=(string)BigDecimal::of($rc['allocated'])->plus($x)->toScale(2); $rc['available']=(string)BigDecimal::of($rc['available'])->minus($x)->toScale(2); unset($rc);
|
||||
}
|
||||
if($e->kind==='reduction') $reduced=$reduced->plus($e->amount);
|
||||
if($e->kind==='reduction') $reduced=$reduced->plus($e->amount);if($e->kind==='reverse_reduction') $reduced=$reduced->minus($e->amount);
|
||||
if($e->kind==='reconcile') $receipts[$e->receipt_id]['reconciled']=true;
|
||||
if($e->kind==='reverse_reconcile') $receipts[$e->receipt_id]['reconciled']=false;
|
||||
if($e->kind==='reverse_receipt') { $receipts[$e->receipt_id]['state']='reversed'; $receipts[$e->receipt_id]['available']='0.00'; }
|
||||
|
||||
Reference in New Issue
Block a user