24 lines
4.1 KiB
PHP
24 lines
4.1 KiB
PHP
<?php
|
|
namespace App\Modules\Inventory;
|
|
use Illuminate\Database\ConnectionInterface;use Illuminate\Support\Facades\Crypt;
|
|
|
|
/** Inactivity of known article/warehouse pairs; never invents placement or availability. */
|
|
final class StockInactiveReport
|
|
{
|
|
public function __construct(private ConnectionInterface $db){}
|
|
public function read(array $binding,string $fingerprint,array $f,?string $token):array
|
|
{
|
|
if($f['warehouse_id']!==null)abort_unless(in_array($f['warehouse_id'],$binding['warehouse_ids'],true),403);
|
|
return $this->db->transaction(function()use($binding,$fingerprint,$f,$token){abort_unless($this->db->transactionLevel()===1,503);$this->db->statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY');$this->db->statement("SET LOCAL statement_timeout='3s'");
|
|
$reader=new \Tempo\Inventory\InventoryReader($this->db,$binding['schema'],$binding['source_tenant_id'],$binding['warehouse_ids']);try{$revision=$reader->revision();}catch(\UnexpectedValueException){abort(503,'Fonte de stock incompatível.');}$observed=time();
|
|
if($token!==null){try{$c=json_decode(Crypt::decryptString($token),true,32,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Referência de inactividade inválida. Actualize a consulta.');}abort_unless(($c['fingerprint']??null)===$fingerprint&&($c['revision']??null)===$revision&&is_int($c['observed']??null)&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'A origem ou o contexto mudou. Actualize o relatório.');$observed=$c['observed'];}
|
|
$asOf=new \DateTimeImmutable($f['as_of'],new \DateTimeZone('UTC'));$until=$asOf->modify('+1 day')->format('Y-m-d').' 00:00:00+00';$cut=$asOf->modify('-'.($f['days']-1).' days')->format('Y-m-d').' 00:00:00+00';
|
|
$q=$this->db->table('public.stock_movements as m')->join('public.stock_items as i',fn($j)=>$j->on('i.id','=','m.stock_item_id')->on('i.tenant_id','=','m.tenant_id'))->join('public.warehouses as w',fn($j)=>$j->on('w.id','=','m.warehouse_id')->on('w.tenant_id','=','m.tenant_id'))->where('m.tenant_id',$binding['source_tenant_id'])->whereIn('m.warehouse_id',$binding['warehouse_ids'])->where('m.moved_at','<',$until);
|
|
if($f['warehouse_id']!==null)$q->where('m.warehouse_id',$f['warehouse_id']);if($f['search']!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$f['search']).'%';$q->where(fn($s)=>$s->whereRaw("i.code ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("i.name ILIKE ? ESCAPE '!'",[$needle]));}
|
|
$q->select(['m.stock_item_id','m.warehouse_id','i.code','i.name','i.unit','w.code as warehouse_code','w.name as warehouse_name'])->selectRaw('SUM(m.quantity_milli::numeric)::text physical_quantity_milli,MAX(m.moved_at) last_registered_at')->selectRaw("(?::date-(MAX(m.moved_at) AT TIME ZONE 'UTC')::date)::integer days_without_record",[$f['as_of']])->groupBy('m.stock_item_id','m.warehouse_id','i.code','i.name','i.unit','w.code','w.name')->havingRaw('MAX(m.moved_at) < ?',[$cut])->orderBy('m.stock_item_id')->orderBy('m.warehouse_id');
|
|
$total=(int)$this->db->query()->fromSub((clone$q)->reorder(),'stock_inactive_pairs')->count();$rows=$q->limit($f['per_page'])->offset(($f['page']-1)*$f['per_page'])->get()->map(fn($r)=>(array)$r)->all();
|
|
return ['status'=>'available','synthetic_only'=>true,'report'=>'known_pairs_inactivity','as_of'=>$f['as_of'],'minimum_days'=>$f['days'],'timezone'=>'UTC','rows'=>$rows,'pagination'=>['page'=>$f['page'],'per_page'=>$f['per_page'],'total'=>$total,'last_page'=>max(1,(int)ceil($total/$f['per_page']))],'observed_at'=>gmdate('c',$observed),'ledger_revision'=>$revision,'snapshot'=>Crypt::encryptString(json_encode(['fingerprint'=>$fingerprint,'revision'=>$revision,'observed'=>$observed],JSON_THROW_ON_ERROR)),'capabilities'=>['write'=>false,'valuation'=>false,'reserved'=>false,'blocked'=>false],'notice'=>'Último registo físico UTC em pares artigo/armazém com história conhecida até ao corte. Metadados actuais. Quantidade física não é disponível. Sem história não se atribui armazém nem se presume inactividade, saldo zero ou ausência de actividade real. Sem somar unidades; corte futuro não prevê movimentos por registar.'];
|
|
});
|
|
}
|
|
}
|