125 lines
5.3 KiB
PHP
125 lines
5.3 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)->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.']);
|
|
}
|
|
}
|
|
}
|