102 lines
5.9 KiB
PHP
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))];
|
|
}
|
|
}
|