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

102 lines
5.9 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use App\Models\WarehouseRequest;
use App\Models\WarehouseRequestEvent;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
/** Pull boundary over committed facts; never posts another stock movement. */
class WarehouseExchange
{
public const SCHEMA = 'sgc.warehouse.fulfilment-feed.v1';
/** @return array<string,mixed> */
public function page(User $actor, ?string $cursor = null, int $limit = 50): array
{
abort_unless(auth('web')->id() === $actor->id && $actor->tenant_id !== null
&& $actor->hasFunctions(['admin', 'warehouse_request_read', 'warehouse_request_prepare', 'warehouse_request_review', 'warehouse_request_deliver']), 403);
$contexts = app(WorkContext::class);
$enforced = $contexts->enforced($actor);
abort_unless(! $enforced || $contexts->active(request()) === 'procurement.internal', 403);
abort_unless(Schema::hasTable('warehouse_request_events'), 503, 'Instalação das entregas pendente.');
abort_unless($limit >= 1 && $limit <= 100, 422);
$permissions = app(OrganizationAccess::class)->permissions($actor);
sort($permissions);
$permissionFingerprint = hash('sha256', json_encode($permissions, JSON_THROW_ON_ERROR));
$scope = [self::SCHEMA, $actor->tenant_id, $actor->id, $enforced ? 'procurement.internal' : 'legacy', $actor->department_id, $permissionFingerprint];
$after = 0;
if ($cursor !== null) {
try {
$decoded = json_decode(Crypt::decryptString($cursor), true, 8, JSON_THROW_ON_ERROR);
} catch (DecryptException|\JsonException $e) {
throw ValidationException::withMessages(['cursor' => 'Cursor inválido. Reinicie a leitura.']);
}
if (! is_array($decoded) || ($decoded['scope'] ?? null) !== $scope
|| ! is_int($decoded['after'] ?? null) || $decoded['after'] < 0) {
throw ValidationException::withMessages(['cursor' => 'O cursor pertence a outro âmbito. Reinicie a leitura.']);
}
$after = $decoded['after'];
}
return DB::transaction(function () use ($actor, $enforced, $scope, $after, $limit) {
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
}
$requests = app(WarehouseWorkdesk::class)->visible($actor)->where('tenant_id', $actor->tenant_id);
$requests->where(fn ($q) => $q->where('work_context_key', 'procurement.internal')
->when(! $enforced, fn ($q) => $q->orWhereNull('work_context_key')));
$events = WarehouseRequestEvent::query()->where('tenant_id', $actor->tenant_id)
->whereIn('warehouse_request_id', (clone $requests)->select('id'))
->whereIn('action', ['deliver', 'receive'])->where('id', '>', $after)->orderBy('id')->limit($limit + 1)->get();
$more = $events->count() > $limit;
$events = $events->take($limit);
$sources = $requests->whereIn('id', $events->pluck('warehouse_request_id'))->get()->keyBy('id');
$items = [];
foreach ($events as $event) {
$source = $sources->get($event->getAttribute('warehouse_request_id'));
abort_unless($source instanceof WarehouseRequest, 409, 'Origem da entrega indisponível.');
$items[] = $this->envelope($source, $event);
}
return ['schema' => self::SCHEMA, 'items' => $items, 'has_more' => $more,
'next_cursor' => Crypt::encryptString(json_encode(['scope' => $scope, 'after' => $events->max('id') ?? $after], JSON_THROW_ON_ERROR))];
});
}
/** @return array<string,mixed> */
private function envelope(WarehouseRequest $request, WarehouseRequestEvent $event): array
{
$key = 'sgc:'.$request->tenant_id.':warehouse-event:'.$event->id;
$snapshot = $request->snapshot;
$lines = [];
if ($event->getAttribute('action') === 'deliver') {
foreach ($event->data['lines'] as $line) {
$source = $snapshot['lines'][$line['index']];
$lines[] = ['request_line_index' => $line['index'], 'item' => $source['item'],
'quantity_milli' => $line['quantity_milli'], 'use' => $source['use'],
'destination' => $source['destination'], 'equipment' => $source['equipment'],
'stock_operation_id' => $line['stock_operation_id']];
}
}
$payload = ['schema' => 'sgc.warehouse.fulfilment-event.v1', 'idempotency_key' => $key,
'tenant_id' => $request->tenant_id, 'event_id' => $event->id,
'event_version' => $event->getAttribute('version'), 'action' => $event->getAttribute('action'),
'occurred_at' => CarbonImmutable::parse($event->getAttribute('created_at'))->utc()->toIso8601String(), 'actor' => $event->data['actor'],
'request' => ['id' => $request->id, 'reference' => 'RA-'.$request->id, 'document_type' => 'internal_warehouse_request',
'work_context_key' => $request->getAttribute('work_context_key'),
'requester' => $snapshot['requester'], 'warehouse' => $snapshot['warehouse'],
'cost_center' => $snapshot['cost_center'], 'destination' => $snapshot['destination']],
'lines' => $lines, 'delivery_event_key' => $event->getAttribute('action') === 'receive'
? 'sgc:'.$request->tenant_id.':warehouse-event:'.$event->data['delivery_event_id'] : null];
return [...$payload, 'payload_sha256' => hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))];
}
}