41 lines
3.8 KiB
PHP
41 lines
3.8 KiB
PHP
<?php
|
|
namespace Tempo\Inventory;
|
|
|
|
use Brick\Math\BigDecimal;
|
|
use Closure;
|
|
use Illuminate\Database\ConnectionInterface;
|
|
|
|
/** One physical issue, within the host's authorised transaction. No independent balance. */
|
|
final class IssueMovementWriter
|
|
{
|
|
public function __construct(private ConnectionInterface $db,private string $schema,private int $tenant,private int $actor,private Closure $authorize)
|
|
{
|
|
if(!preg_match('/^[a-z][a-z0-9_]{0,62}$/D',$schema)||$tenant<1||$actor<1)throw new \InvalidArgumentException('Invalid issue context');
|
|
}
|
|
private function host(): void {if($this->db->transactionLevel()<1)throw new \LogicException('Issue requires the host transaction');($this->authorize)();}
|
|
private function operation(int $id,bool $lock=false): object
|
|
{
|
|
$q=$this->db->table($this->schema.'.stock_operations')->where(['tenant_id'=>$this->tenant,'id'=>$id]);if($lock)$q->lockForUpdate();
|
|
$o=$q->first();abort_unless($o,403);abort_unless($o->kind==='issue'&&(int)$o->created_by===$this->actor&&BigDecimal::of((string)$o->quantity_milli)->isPositive(),409,'Operação incompatível com saída física.');return $o;
|
|
}
|
|
public function preview(int $id): string {$this->host();return hash('sha256',json_encode((array)$this->operation($id),JSON_THROW_ON_ERROR));}
|
|
public function write(int $id,string $hash): array
|
|
{
|
|
$this->host();$o=$this->operation($id);
|
|
// Lock the actual warehouse, including a previously empty book. Hosts keep their company lock too.
|
|
$warehouse=$this->db->table($this->schema.'.warehouses')->where(['tenant_id'=>$this->tenant,'id'=>$o->warehouse_id])->lockForUpdate()->first();abort_unless($warehouse,403);
|
|
abort_unless($this->db->table($this->schema.'.stock_items')->where(['tenant_id'=>$this->tenant,'id'=>$o->stock_item_id])->lockForUpdate()->first(),403);
|
|
$o=$this->operation($id,true);abort_unless(hash_equals($hash,hash('sha256',json_encode((array)$o,JSON_THROW_ON_ERROR))),409,'A operação mudou. Faça nova conferência.');
|
|
$q=$this->db->table($this->schema.'.stock_movements')->where(['tenant_id'=>$this->tenant,'stock_operation_id'=>$id,'warehouse_id'=>$o->warehouse_id]);$old=$q->first();
|
|
$negative=(string)BigDecimal::of((string)$o->quantity_milli)->negated();
|
|
if($old){$s=json_decode($old->snapshot,true,512,JSON_THROW_ON_ERROR);abort_unless(($s['inventory_operation_hash']??null)===$hash&&(string)$old->quantity_milli===$negative&&(int)$old->stock_item_id===(int)$o->stock_item_id,409,'Saída existente incompatível; exige reconciliação.');return ['movement_id'=>(int)$old->id,'replayed'=>true];}
|
|
$book=$this->db->table($this->schema.'.stock_movements')->where(['tenant_id'=>$this->tenant,'stock_item_id'=>$o->stock_item_id,'warehouse_id'=>$o->warehouse_id])->selectRaw('coalesce(sum(quantity_milli),0) AS quantity,coalesce(max(id),0) AS revision')->first();
|
|
abort_unless((string)$o->ledger_version===(string)$book->revision,409,'O livro mudou. Faça nova conferência.');
|
|
abort_unless(BigDecimal::of((string)$book->quantity)->isGreaterThanOrEqualTo((string)$o->quantity_milli),409,'Stock físico insuficiente.');
|
|
$snapshot=json_decode($o->snapshot,true,512,JSON_THROW_ON_ERROR)+['reference'=>$o->reference,'reason'=>$o->reason,'inventory_operation_hash'=>$hash];
|
|
$movement=$this->db->table($this->schema.'.stock_movements')->insertGetId(['tenant_id'=>$this->tenant,'stock_operation_id'=>$id,'stock_item_id'=>$o->stock_item_id,'warehouse_id'=>$o->warehouse_id,
|
|
'equipment_id'=>$o->equipment_id??null,'spare_part_id'=>$o->spare_part_id??null,'moved_by'=>$this->actor,'movement_type'=>'issue','quantity_milli'=>$negative,'snapshot'=>json_encode($snapshot,JSON_THROW_ON_ERROR),'moved_at'=>now()]);
|
|
return ['movement_id'=>(int)$movement,'replayed'=>false];
|
|
}
|
|
}
|