Files
compras/app/Services/WarehouseReservations.php
T
2026-11-02 20:27:11 +02:00

125 lines
5.4 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use App\Models\WarehouseRequest;
use App\Models\WarehouseRequestEvent;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
class WarehouseReservations
{
public function ready(): bool
{
return Schema::hasTable('warehouse_reservations');
}
public function committed(int $item, int $warehouse): int
{
return $this->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<int,int> */
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<int,int> $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.']);
}
}
}