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

28 lines
7.3 KiB
PHP

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