commit15
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Physical return to the same stock book
|
||||
|
||||
`DeliveryReturnWriter` writes one positive `workshop_return_in` movement inside the host transaction. It requires authorization, a frozen operation, explicit physical warehouse receipt and a matching original issue movement. Warehouse, article, operation and original movement are locked. Ledger revision and cumulative returned quantity are checked; replay returns the existing movement.
|
||||
|
||||
The host must verify the returned observation, source eligibility, unconsumed/unallocated quantity, independence, context, rights and audit. This primitive caps physical returns by the original issue; it does not know workshop allocations. It never cancels consumption, corrects financial documents or creates another stock balance.
|
||||
|
||||
The current adapter is synthetic SGF only. Native Purchasing return-command integration, operational installation, other drivers and capacity remain unvalidated.
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
require_once __DIR__.'/src/DeliveryReturnWriter.php';
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace Tempo\Inventory;
|
||||
|
||||
use Brick\Math\BigDecimal;
|
||||
use Closure;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
|
||||
/** A physical return into the same book; the host owns unconsumed allocation and receiving evidence. */
|
||||
final class DeliveryReturnWriter
|
||||
{
|
||||
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 return context');
|
||||
}
|
||||
private function host():void {if($this->db->transactionLevel()<1)throw new \LogicException('Return requires host transaction');($this->authorize)();}
|
||||
private function hash(object $o):string {return hash('sha256',json_encode((array)$o,JSON_THROW_ON_ERROR));}
|
||||
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&&$o->kind==='workshop_return'&&(int)$o->created_by===$this->actor&&BigDecimal::of((string)$o->quantity_milli)->isPositive(),409,'Operação de devolução incompatível.');return $o;
|
||||
}
|
||||
public function preview(int $id):string {$this->host();return $this->hash($this->operation($id));}
|
||||
public function write(int $id,string $expected):array
|
||||
{
|
||||
$this->host();$o=$this->operation($id);
|
||||
abort_unless($this->db->table($this->schema.'.warehouses')->where(['tenant_id'=>$this->tenant,'id'=>$o->warehouse_id])->lockForUpdate()->first(),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);$hash=$this->hash($o);abort_unless(hash_equals($expected,$hash),409,'Operação de devolução alterada.');
|
||||
$snapshot=json_decode($o->snapshot,true,512,JSON_THROW_ON_ERROR);$parentId=$snapshot['delivery_movement_id']??null;abort_unless(is_int($parentId)&&$parentId>0,409);
|
||||
abort_unless(($snapshot['received_in_warehouse']??false)===true,409,'Recepção física da devolução não declarada.');
|
||||
$parent=$this->db->table($this->schema.'.stock_movements')->where(['tenant_id'=>$this->tenant,'id'=>$parentId,'stock_item_id'=>$o->stock_item_id,'warehouse_id'=>$o->warehouse_id])->lockForUpdate()->first();
|
||||
abort_unless($parent&&$parent->movement_type==='issue'&&BigDecimal::of((string)$parent->quantity_milli)->isNegative()&&$this->hash($parent)===($snapshot['delivery_movement_hash']??null),409,'Entrega original incompatível ou alterada.');
|
||||
$old=$this->db->table($this->schema.'.stock_movements')->where(['tenant_id'=>$this->tenant,'stock_operation_id'=>$id,'warehouse_id'=>$o->warehouse_id])->first();
|
||||
if($old){$s=json_decode($old->snapshot,true,512,JSON_THROW_ON_ERROR);abort_unless(($s['inventory_return_hash']??null)===$hash&&(string)$old->quantity_milli===(string)$o->quantity_milli&&(int)$old->stock_item_id===(int)$o->stock_item_id,409,'Devolução existente incompatível.');return ['movement_id'=>(int)$old->id,'replayed'=>true];}
|
||||
$returned=(string)$this->db->table($this->schema.'.stock_movements')->where(['tenant_id'=>$this->tenant,'movement_type'=>'workshop_return_in','snapshot->delivery_movement_id'=>$parentId])->sum('quantity_milli');
|
||||
abort_unless(BigDecimal::of((string)$parent->quantity_milli)->negated()->minus($returned)->isGreaterThanOrEqualTo((string)$o->quantity_milli),409,'Devolução excede a quantidade entregue.');
|
||||
$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(max(id),0) AS revision')->first();
|
||||
abort_unless((string)$o->ledger_version===(string)$book->revision,409,'Livro alterado; confira novamente.');
|
||||
$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,'moved_by'=>$this->actor,'movement_type'=>'workshop_return_in','quantity_milli'=>$o->quantity_milli,'snapshot'=>json_encode($snapshot+['inventory_return_hash'=>$hash,'reason'=>$o->reason],JSON_THROW_ON_ERROR),'moved_at'=>now()]);
|
||||
return ['movement_id'=>(int)$movement,'replayed'=>false];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user