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

115 lines
23 KiB
PHP

<?php
namespace App\Modules\Services;
use App\Shared\Context\{CompanyContext,ModuleRecords};
use App\Shared\Installation\CompositionRuntime;
use App\Modules\Identity\Services\{Permissions,Audit};
use Illuminate\Support\Facades\{DB,Crypt,Validator};
use Brick\Math\{BigDecimal,BigRational,RoundingMode};
use Tempo\Finance\Payables\Payables;
/** Versioned data-only rules. Only pinned artificial cases may execute in this QA contract. */
final class FiscalRules
{
public const CONTRACT='SGF-FISCAL-RULES-COMMON-QA-v1';
public function __construct(private CompanyContext $context,private Permissions $permissions){}
private function json(array $a):string{return Payables::canonical($a);}
private function hash(array $a):string{return hash('sha256',$this->json($a));}
public function scope(int $actor,string $action='view'):array
{
abort_unless(config('fiscal_rules_qa.enabled')===true,404);CompositionRuntime::assert();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2]&&ModuleRecords::active()==='accounting',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(array_unique(['view',$action])as$r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'fiscal_rules.'.$r,$s['unit_id']);return $s;
}
public function fixture():array
{
$file=base_path('resources/data/fiscal-rule-qa.json');$pin=config('fiscal_rules_qa.source_sha256');abort_unless(is_string($pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503,'Fonte artificial do contrato não conferida.');$f=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);abort_unless($f['contract']===self::CONTRACT&&$f['not_tax_law']===true,503);$f['sha256']=$pin;return $f;
}
private function events(array $s){return DB::table('fr_events')->where($s);}
private function row(object $r):array{$a=(array)$r;foreach(['definition','input','result','dependencies']as$k)if(isset($a[$k]))$a[$k]=json_decode($a[$k],true,64,JSON_THROW_ON_ERROR);return $a;}
private function currentVersions(array $s)
{
return DB::table('fr_events as f')->where(array_combine(array_map(fn($k)=>'f.'.$k,array_keys($s)),array_values($s)))->whereNotExists(function($q){$q->selectRaw('1')->from('fr_events as newer');foreach(['tenant_id','company_id','unit_id','rule_key','rule_version']as$k)$q->whereColumn('newer.'.$k,'f.'.$k);$q->whereColumn('newer.revision','>','f.revision');});
}
private function exact(array $data,array $rules):array{abort_if(array_diff(array_keys($data),array_keys($rules)),422,'Campo não suportado neste contrato.');return Validator::make($data,$rules)->validate();}
public function definition(array $d):array
{
$d=$this->exact($d,['label'=>'required|string|min:3|max:160','family'=>'required|in:qa,iva,irps,irpc,ispc,ice,inss,other','operation'=>'required|in:percentage_add,metadata_only','conditions'=>'required|string|min:10|max:2000','currency'=>'nullable|in:MZN','unit'=>'required|in:money,none','precision'=>'required|integer|in:2','rounding'=>'required|in:DOWN','priority'=>'required|integer|min:0|max:100','source_reference'=>'required|string|min:3|max:200','source_sha256'=>'nullable|string|regex:/^[a-f0-9]{64}$/D','source_status'=>'required|in:pending,qa_checked','valid_from'=>'nullable|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31','valid_until'=>'nullable|date_format:Y-m-d|after_or_equal:valid_from|before_or_equal:9998-12-31','parameters'=>'present|array','expression'=>'present|array']);
$this->exact($d['parameters'],['rate'=>'nullable|string|regex:/^0\.[0-9]{1,8}$/D']);
if($d['expression']){try{(new FiscalRuleExpression)->validate($d['expression']);}catch(\InvalidArgumentException){abort(422,'Expressão declarativa inválida; não é executado código.');}abort_if(array_diff(array_keys($d['expression']),['language','nodes','result']),422);}
if($d['source_status']==='qa_checked')abort_unless($this->verified($d),422,'Somente os casos artificiais exactos deste contrato podem ser conferidos para execução QA. Fontes legais não são activadas.');return $d;
}
private function verified(array $d):bool
{
$f=$this->fixture();if(($d['source_sha256']??null)!==$f['sha256']||($d['family']??null)!=='qa'||($d['source_status']??null)!=='qa_checked')return false;
foreach($f['cases']as$case){$expected=$case['definition'];$actual=$d;unset($actual['source_sha256'],$actual['label']);unset($expected['label']);if($this->hash($actual)===$this->hash($expected))return true;}return false;
}
private function basis(array $s,array $d):array
{
$q=$this->events($s);$last=isset($d['rule_key'])?(clone$q)->where('rule_key',$d['rule_key'])->orderByDesc('revision')->first():null;$target=isset($d['target_id'])?(clone$q)->where('id',$d['target_id'])->first():null;$selected=isset($d['data']['rule_event_id'])?(clone$q)->where('id',$d['data']['rule_event_id'])->first():null;
return ['source_sha256'=>$this->fixture()['sha256'],'count'=>(int)(clone$q)->count(),'last_id'=>(int)(clone$q)->max('id'),'last'=>$last?$this->row($last):null,'target'=>$target?$this->row($target):null,'selected'=>$selected?$this->row($selected):null];
}
private function right(string $action):string{return in_array($action,['review','withdraw'],true)?'review':'prepare';}
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'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->lockForUpdate()->first(),403);DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->orderBy('id')->lockForUpdate()->get();
}
public function preview(int $actor,array $input,string $key):array
{
abort_if(strlen(json_encode($input))>32768,422);$d=$this->exact($input,['action'=>'required|in:prepare,review,withdraw,calculate,calculate_document','rule_key'=>'nullable|required_unless:action,calculate,calculate_document|string|regex:/^[A-Z0-9_-]{3,80}$/D','revision'=>'required|integer|min:0','target_id'=>'nullable|required_if:action,review,withdraw|integer|min:1','data'=>'present|array','reason'=>'required|string|min:10|max:4000']);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);if($d['action']==='calculate_document')abort_unless(config('fiscal_rules_qa.document_enabled')===true,404);$d['revision']=(int)$d['revision'];if(isset($d['target_id']))$d['target_id']=(int)$d['target_id'];$s=$this->scope($actor,$this->right($d['action']));
return DB::transaction(function()use($actor,$s,$d,$key){$this->lock($actor,$s);$this->scope($actor,$this->right($d['action']));$b=$this->basis($s,$d);$level=DB::transactionLevel();DB::beginTransaction();try{$out=$this->execute($actor,$s,$d,$b);}finally{while(DB::transactionLevel()>$level)DB::rollBack();}$cut=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'input'=>$d,'key'=>$key,'observed'=>time(),'basis'=>$this->hash($b),'impact'=>$out['impact']];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['action']));return DB::transaction(function()use($actor,$s,$d,$c){$this->lock($actor,$s);$this->scope($actor,$this->right($d['action']));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('fr_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$c['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409,'A mesma chave não aceita outra entrada.');return json_decode($old->result,true)+['replayed'=>true,'historical_receipt_only'=>true];}$b=$this->basis($s,$d);abort_unless($this->hash($b)===$c['basis'],409,'Regras, fonte ou versão mudaram; exige nova conferência.');$out=$this->execute($actor,$s,$d,$b);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409);DB::table('fr_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$c['key'],'request_hash'=>$this->hash($d),'result'=>$this->json($out),'created_at'=>now()]);Audit::record('fiscal_rule.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'version'=>$out['revision'],'before_revision'=>$d['revision'],'reason'=>$d['reason'],'review_hash'=>$b['source_sha256']]);return $out+['replayed'=>false];});
}
private function execute(int $actor,array $s,array $d,array $basis):array
{
if(in_array($d['action'],['calculate','calculate_document'],true)){
abort_if(!empty($d['rule_key'])||isset($d['target_id'])||$d['revision']!==0,422);$in=$d['action']==='calculate_document'?$this->documentInput($d['data']):$this->exact($d['data'],['rule_event_id'=>'nullable|integer|min:1','base'=>'required|string|regex:/^\d{1,16}(\.\d{1,8})?$/D','currency'=>'required|string|regex:/^[A-Z]{3}$/D','date'=>'required|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31']);$selected=$basis['selected'];abort_if(isset($in['rule_event_id'])&&!$selected,403,'Regra fora do contexto.');$result=$d['action']==='calculate_document'?$this->evaluateDocument($s,$in,$selected):$this->evaluate($s,$in,$selected);$deps=['source_sha256'=>$basis['source_sha256'],'rules_count'=>$basis['count'],'rules_last_id'=>$basis['last_id'],'selected_id'=>$selected['id']??null,'selected_fingerprint'=>$selected?$this->hash($selected):null];$id=DB::table('fr_calculations')->insertGetId($s+['actor_id'=>$actor,'rule_event_id'=>$selected['id']??null,'input'=>$this->json($in),'result'=>$this->json($result),'dependencies'=>$this->json($deps),'reason'=>$d['reason'],'created_at'=>now()]);return ['id'=>$id,'type'=>'calculation','revision'=>0,'state'=>$result['state'],'impact'=>['action'=>'calculate','result'=>$result,'new_journals'=>0,'legal_activation'=>false]];
}
$last=$basis['last'];abort_unless(($last['revision']??0)===$d['revision'],409,'Revisão do catálogo alterada.');$prepared=$actor;$target=null;
if($d['action']==='prepare'){abort_if(isset($d['target_id']),422);$definition=$this->definition($d['data']);$version=(int)$this->events($s)->where('rule_key',$d['rule_key'])->max('rule_version')+1;$state='prepared';}
else{
$decision=$d['action']==='review'?$this->exact($d['data'],['decision'=>'required|in:reviewed,refused']):$this->exact($d['data'],[]);$target=$basis['target'];abort_unless($target&&$target['rule_key']===$d['rule_key'],403,'Escolha a versão exacta desta regra.');$current=$this->events($s)->where(['rule_key'=>$target['rule_key'],'rule_version'=>$target['rule_version']])->orderByDesc('revision')->value('id');abort_unless($current===$target['id'],409,'A versão escolhida já mudou.');abort_unless($target['prepared_by']!==$actor,403,'Preparação e revisão por pessoas distintas.');abort_unless($target['state']===($d['action']==='withdraw'?'reviewed':'prepared'),409,'Estado incompatível com a decisão.');$prepared=$target['prepared_by'];DB::table('users')->where('id',$prepared)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$prepared])->lockForUpdate()->first(),403);abort_unless(DB::table('permission_grants')->where($s+['user_id'=>$prepared,'permission'=>'fiscal_rules.prepare'])->lockForUpdate()->first(),403,'Autor da preparação sem autoridade actual.');$definition=$this->definition($target['definition']);$version=$target['rule_version'];$state=$d['action']==='withdraw'?'withdrawn':$decision['decision'];
}
$revision=$d['revision']+1;$id=DB::table('fr_events')->insertGetId($s+['rule_key'=>$d['rule_key'],'revision'=>$revision,'rule_version'=>$version,'previous_id'=>$last['id']??null,'target_id'=>$target['id']??null,'action'=>$d['action'],'state'=>$state,'prepared_by'=>$prepared,'actor_id'=>$actor,'definition'=>$this->json($definition),'reason'=>$d['reason'],'created_at'=>now()]);return ['id'=>$id,'type'=>'rule','revision'=>$revision,'rule_version'=>$version,'state'=>$state,'impact'=>['action'=>$d['action'],'before'=>['revision'=>$d['revision'],'state'=>$last['state']??null],'after'=>['revision'=>$revision,'rule_version'=>$version,'state'=>$state],'source_checked'=>$this->verified($definition),'definition'=>$definition,'new_journals'=>0,'legal_activation'=>false]];
}
private function evaluate(array $s,array $in,?array $selected):array
{
$pending=[];$def=$selected['definition']??null;
if(!$selected)$pending[]='Escolha explícita de regra em falta.';
else{$current=$this->events($s)->where(['rule_key'=>$selected['rule_key'],'rule_version'=>$selected['rule_version']])->orderByDesc('revision')->first();if(!$current||$current->id!==$selected['id']||$current->state!=='reviewed')$pending[]='Regra exacta não revista, revogada ou alterada.';if(!$this->verified($def))$pending[]='Fonte/aplicabilidade não conferida para execução neste ensaio.';if(($def['currency']??null)!==$in['currency'])$pending[]='Moeda incompatível.';if(empty($def['valid_from'])||$in['date']<$def['valid_from']||(!empty($def['valid_until'])&&$in['date']>$def['valid_until']))$pending[]='Data fora de vigência ou vigência desconhecida.';
$eligible=$this->currentVersions($s)->where('f.state','reviewed')->where('f.definition->family',$def['family'])->where('f.definition->operation',$def['operation'])->where('f.definition->conditions',$def['conditions'])->where('f.definition->currency',$in['currency'])->where('f.definition->source_status','qa_checked')->where('f.definition->valid_from','<=',$in['date'])->where(fn($q)=>$q->whereNull('f.definition->valid_until')->orWhere('f.definition->valid_until','>=',$in['date']));$max=(clone$eligible)->max(DB::raw("(f.definition->>'priority')::int"));if($max!==null){if((int)$max>(int)$def['priority'])$pending[]='Existe prioridade superior; escolha a regra exacta aplicável.';if((clone$eligible)->whereRaw("(f.definition->>'priority')::int=?",[$max])->count()>1)$pending[]='Conflito de regras com a mesma prioridade e âmbito.';}}
if($pending)return ['state'=>'pending','pending'=>$pending,'base'=>$in['base'],'tax_exact'=>null,'tax_document'=>null,'total'=>null,'synthetic_only'=>true,'legal_use'=>false];
$result=(new FiscalRuleExpression)->evaluate($def['expression'],['base'=>$in['base']],$def['parameters']);if($result['status']!=='determinate')return ['state'=>'pending','pending'=>$result['missing'],'tax_exact'=>null,'tax_document'=>null,'total'=>null,'synthetic_only'=>true,'legal_use'=>false];
$tax=BigRational::of($result['result'])->toBigDecimal();$document=$tax->toScale(2,RoundingMode::Down);$base=BigDecimal::of($in['base']);$representable=$base->stripTrailingZeros()->getScale()<=2;
return ['state'=>$representable?'determined_qa':'pending_representation','base'=>(string)$base->stripTrailingZeros(),'tax_exact'=>(string)$tax->stripTrailingZeros(),'tax_document'=>(string)$document,'difference'=>(string)$tax->minus($document)->stripTrailingZeros(),'total'=>$representable?(string)$base->plus($document)->toScale(2):null,'currency'=>$in['currency'],'rule'=>['id'=>$selected['id'],'key'=>$selected['rule_key'],'version'=>$selected['rule_version'],'revision'=>$selected['revision'],'source_reference'=>$def['source_reference'],'source_sha256'=>$def['source_sha256']],'steps'=>$result['steps'],'pending'=>$representable?[]:['Base não representável em duas casas; não truncada.'],'synthetic_only'=>true,'legal_use'=>false];
}
private function documentInput(array $d):array
{
$in=$this->exact($d,['rule_event_id'=>'nullable|integer|min:1','currency'=>'required|string|regex:/^[A-Z]{3}$/D','date'=>'required|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31','price_mode'=>'required|in:net_added,gross_included','lines'=>'required|array|min:1|max:25']);
foreach($in['lines']as$i=>$line){abort_unless(is_array($line),422);$in['lines'][$i]=$this->exact($line,['reference'=>'required|string|max:80','quantity'=>'required|string|regex:/^\d{1,16}(\.\d{1,8})?$/D','unit_price'=>'required|string|regex:/^\d{1,16}(\.\d{1,8})?$/D']);abort_unless(BigDecimal::of($line['quantity'])->isGreaterThan(0),422,'Quantidade positiva obrigatória.');}abort_unless(array_is_list($in['lines'])&&count(array_unique(array_column($in['lines'],'reference')))===count($in['lines']),422,'Linhas sequenciais e referências distintas obrigatórias.');return $in;
}
private function evaluateDocument(array $s,array $in,?array $selected):array
{
$gate=$this->evaluate($s,['base'=>'0','currency'=>$in['currency'],'date'=>$in['date']],$selected);if($gate['state']!=='determined_qa'){unset($gate['base']);return $gate+['policy'=>FiscalDocumentMath::POLICY,'price_mode'=>$in['price_mode'],'base_document'=>null,'lines'=>[]];}
return FiscalDocumentMath::calculate($in['lines'],$in['price_mode'],$selected['definition']['parameters']['rate'])+['currency'=>$in['currency'],'rule'=>$gate['rule']];
}
public function documentReport(int $actor,int $id):array
{
$detail=$this->detail($actor,$id,'calculation');$r=$detail['result'];abort_unless(isset($r['price_mode'],$r['lines']),422,'Este registo não é cálculo por linhas.');$fp=fopen('php://temp','w+');$rule=$r['rule']??[];$rows=[['MAPA INTERNO POR LINHAS QA — SEM VALIDADE FISCAL'],['Politica',$r['policy']],['Modo',$r['price_mode']],['Data',$detail['input']['date']],['Moeda',$detail['input']['currency']],['Regra escolhida',(string)($detail['input']['rule_event_id']??'INDETERMINADA')],['Versao regra',(string)($rule['version']??'INDETERMINADA')],['Fonte',$rule['source_reference']??'INDETERMINADA'],['SHA fonte',$rule['source_sha256']??'INDETERMINADO'],['Estado historico',$r['state']],['Estado actual',$detail['current_evaluation']['state']],['Linha','Referencia','Base exacta fraccao','Imposto exacto fraccao','Base documental','Imposto documental','Total','Diferenca base fraccao','Diferenca imposto fraccao']];foreach($r['lines']as$line){$ref="'".$line['reference'];$rows[]=[(string)$line['line'],$ref,$line['base_exact_fraction'],$line['tax_exact_fraction'],$line['base_document']??'INDETERMINADO',$line['tax_document'],$line['total_document']??'INDETERMINADO',$line['base_difference_fraction']??'INDETERMINADO',$line['tax_difference_fraction']];}$rows[]=['Totais documentais','','','',$r['base_document']??'INDETERMINADO',$r['tax_document']??'INDETERMINADO',$r['total']??'INDETERMINADO'];foreach($rows as$row)fputcsv($fp,$row,';','"','',"\r\n");rewind($fp);$bytes=stream_get_contents($fp);$s=$this->scope($actor);Audit::record('fiscal_rule.document_report',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'version'=>0,'review_hash'=>hash('sha256',$bytes)]);return ['bytes'=>$bytes,'sha256'=>hash('sha256',$bytes)];
}
public function listing(int $actor,array $input):array
{
$s=$this->scope($actor);$d=$this->exact($input,['view'=>'sometimes|in:rules,history,calculations','rule_key'=>'sometimes|string|regex:/^[A-Z0-9_-]{3,80}$/D','family'=>'sometimes|in:qa,iva,irps,irpc,ispc,ice,inss,other','state'=>'sometimes|in:prepared,reviewed,refused,withdrawn','search'=>'nullable|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);$view=$d['view']??'rules';abort_if($view==='history'&&!isset($d['rule_key']),422);abort_if($view==='calculations'&&array_intersect(array_keys($d),['rule_key','family','state','search']),422);
return DB::transaction(function()use($actor,$s,$d,$view){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$q=$view==='calculations'?DB::table('fr_calculations')->where($s):($view==='history'?$this->events($s):$this->currentVersions($s));$alias=$view==='rules'?'f.':'';if(isset($d['rule_key']))$q->where($alias.'rule_key',$d['rule_key']);foreach(['family'=>'definition->family','state'=>'state']as$filter=>$column)if(isset($d[$filter]))$q->where($alias.$column,$d[$filter]);if(!empty($d['search'])){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(fn($w)=>$w->whereRaw($alias."rule_key ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw($alias."definition->>'label' ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw($alias."definition->>'source_reference' ILIKE ? ESCAPE '!'",[$needle]));}$page=$q->orderByDesc($alias.'id')->paginate($d['per_page']??25,['*'],'page',$d['page']??1);$rows=$page->getCollection()->map(fn($r)=>$this->row($r));$ids=$rows->pluck('id')->all();$refs=DB::table('fr_commands')->where($s)->where('actor_id',$actor)->where('result->type',$view==='calculations'?'calculation':'rule')->whereIn(DB::raw("(result->>'id')::bigint"),$ids)->get();$page->setCollection($rows->map(function($r)use($refs){$r['command_reference']=null;foreach($refs as$ref)if(json_decode($ref->result,true)['id']===$r['id'])$r['command_reference']=$ref->command_key;return $r;}));return ['document_enabled'=>config('fiscal_rules_qa.document_enabled')===true,'stream_revision'=>isset($d['rule_key'])?(int)$this->events($s)->where('rule_key',$d['rule_key'])->max('revision'):null,'contract'=>self::CONTRACT,'view'=>$view,'records'=>$page->toArray(),'fixtures'=>$this->fixture(),'capabilities'=>['prepare'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_rules.prepare',$s['unit_id']),'review'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_rules.review',$s['unit_id'])],'synthetic_only'=>true,'legal_activation'=>false];});
}
public function detail(int $actor,int $id,string $type='rule'):array
{
$s=$this->scope($actor);$row=DB::table($type==='calculation'?'fr_calculations':'fr_events')->where($s)->where('id',$id)->first();abort_unless($row,404);$a=$this->row($row);if($type==='rule'){$a['current_revision']=(int)$this->events($s)->where('rule_key',$row->rule_key)->max('revision');$a['current_version_event_id']=(int)$this->events($s)->where(['rule_key'=>$row->rule_key,'rule_version'=>$row->rule_version])->orderByDesc('revision')->value('id');}else{$selected=isset($a['input']['rule_event_id'])?$this->events($s)->where('id',$a['input']['rule_event_id'])->first():null;$a['current_evaluation']=isset($a['input']['lines'])?$this->evaluateDocument($s,$a['input'],$selected?$this->row($selected):null):$this->evaluate($s,$a['input'],$selected?$this->row($selected):null);$a['historical_snapshot_unchanged']=true;}return $a;
}
public function receipt(int $actor,string $key):array
{
$s=$this->scope($actor);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$r=DB::table('fr_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$key])->first();abort_unless($r,404);return ['result'=>json_decode($r->result,true),'created_at'=>$r->created_at,'command_key'=>$key,'historical_receipt_only'=>true,'operation_executed'=>false];
}
}