ready() ? (int) DB::table('warehouse_reservations')->where('tenant_id', auth()->user()?->tenant_id ?? app(ReplenishmentRun::class)->tenantId())->where('stock_item_id', $item)->where('warehouse_id', $warehouse)->sum('quantity_milli') : 0; } /** @return array */ public function lines(WarehouseRequest $request): array { $values = array_fill(0, count($request->snapshot['lines']), 0); if ($this->ready()) { foreach (DB::table('warehouse_reservations')->where('tenant_id', $request->tenant_id)->where('warehouse_request_id', $request->id)->selectRaw('line_index,SUM(quantity_milli) as quantity')->groupBy('line_index')->get() as $row) { $values[$row->line_index] = (int) $row->quantity; } } return $values; } public function usable(int $item, int $warehouse): int { $lots = app(InventoryLots::class)->context($item, $warehouse); if ($lots !== null && ! $lots['reconciled']) { throw ValidationException::withMessages(['availability' => 'Reconcilie os lotes antes de comprometer ou retirar stock.']); } return $lots === null ? app(StockWorkflow::class)->balance($item, $warehouse)['quantity'] : $lots['usable_milli']; } public function free(int $item, int $warehouse): int { return $this->usable($item, $warehouse) - $this->committed($item, $warehouse); } /** @param array $remaining */ public function reserve(User $actor, WarehouseRequest $request, WarehouseRequestEvent $authorization, array $remaining, bool $partial, string $key): void { if (! $this->ready()) { return; } $own = $this->lines($request); foreach ($request->snapshot['lines'] as $index => $line) { $need = $remaining[$index] - $own[$index]; if ($need <= 0) { continue; } $free = max(0, $this->free($line['item']['id'], $request->warehouse_id)); if (! $partial && $free < $need) { throw ValidationException::withMessages(['availability' => 'Stock livre insuficiente: parte do material está reservada para outros pedidos.']); } $quantity = min($need, $free); if ($quantity > 0) { $this->entry($actor, $request, $index, $authorization->id, $key, 'reserve', $quantity); } } } public function consume(User $actor, WarehouseRequest $request, int $index, int $quantity, string $key): void { if (! $this->ready()) { return; } $own = $this->lines($request); if ($quantity > $own[$index]) { throw ValidationException::withMessages(['lines' => 'A entrega excede a reserva desta linha. Actualize a reserva do pedido.']); } $authorization = WarehouseRequestEvent::query()->where('warehouse_request_id', $request->id)->where('action', 'authorize')->firstOrFail(); $this->entry($actor, $request, $index, $authorization->id, $key, 'consume', -$quantity); } public function release(User $actor, WarehouseRequest $request, string $key): void { if (! $this->ready()) { return; } $authorization = WarehouseRequestEvent::query()->where('warehouse_request_id', $request->id)->where('action', 'authorize')->first(); if ($authorization === null) { return; } foreach ($this->lines($request) as $index => $quantity) { if ($quantity > 0) { $this->entry($actor, $request, $index, $authorization->id, $key, 'release', -$quantity); } } } private function entry(User $actor, WarehouseRequest $request, int $index, int $authorization, string $key, string $kind, int $quantity): void { if (DB::transactionLevel() === 0) { throw new \LogicException('Reserva exige a transacção do processo.'); } DB::table('warehouse_reservations')->insert(['tenant_id' => $request->tenant_id, 'warehouse_request_id' => $request->id, 'line_index' => $index, 'stock_item_id' => $request->snapshot['lines'][$index]['item']['id'], 'warehouse_id' => $request->warehouse_id, 'authorization_event_id' => $authorization, 'actor_id' => $actor->id, 'command_key' => $key, 'kind' => $kind, 'quantity_milli' => $quantity, 'created_at' => now()]); } /** Refuse worsening a shortage of usable reserved material; quality holds remain independent. */ public function protect(int $item, int $warehouse, int $before): void { if (! $this->ready() || $this->committed($item, $warehouse) === 0) { return; } if ($this->free($item, $warehouse) < min(0, $before)) { throw ValidationException::withMessages(['quantity' => 'Esta operação consumiria material reservado. Liberte ou regularize a reserva autorizadamente.']); } } }