Files
compras/tests/Concurrency/supply-worker.php
T
2026-11-02 08:08:05 +02:00

167 lines
13 KiB
PHP

<?php
use App\Models\SupplyRecord;
use App\Models\User;
use App\Models\Warehouse;
use App\Services\InventoryCounts;
use App\Services\InventoryDispositions;
use App\Services\InventoryLots;
use App\Services\InventoryReversals;
use App\Services\ReceiptWorkflow;
use App\Services\StockWorkflow;
use App\Services\SupplyPlanning;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
require dirname(__DIR__, 2).'/vendor/autoload.php';
$app = require dirname(__DIR__, 2).'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
if (! app()->environment('testing') || DB::getDriverName() !== 'pgsql' || ! preg_match('/^compras_receipts_test_[a-f0-9]{12}$/D', DB::connection()->getDatabaseName())) {
throw new LogicException('Worker apenas em base PostgreSQL descartável.');
}
require_once dirname(__DIR__).'/Support/ReceiptFixture.php';
require_once dirname(__DIR__).'/Support/SupplyFixture.php';
if (($argv[1] ?? '') === 'prepare_reversal') {
$f = receiptFixture('100.000');
supplyGrant($f['receiver'], ['warehouse', 'inventory_reversal_prepare']);
supplyGrant($f['issuer'], ['inventory_reversal_review']);
auth()->guard()->setUser($f['receiver']);
app(ReceiptWorkflow::class)->confirm($f['receiver'], $f['order'], receiptPayload($f, '10.000'));
$issue = app(StockWorkflow::class)->record($f['receiver'], ['kind' => 'issue', 'reference' => 'ISSUE', 'idempotency_key' => (string) Str::uuid(), 'warehouse_id' => $f['warehouse']->id,
'stock_item_id' => $f['stock']->id, 'responsible_id' => $f['receiver']->id, 'quantity' => '3.000', 'reason' => 'Saída original para disputa de estorno.']);
$records = [];
foreach ([1, 2] as $number) {
auth()->guard()->setUser($f['receiver']);
$draft = app(InventoryReversals::class)->save($f['receiver'], supplyInput(['reference' => 'REV-'.$number, 'source_kind' => 'operation', 'source_operation_id' => $issue->id]));
auth()->guard()->setUser($f['issuer']);
$approved = app(InventoryReversals::class)->save($f['issuer'], supplyInput([], 'approve', $draft));
$records[] = ['record_key' => $approved->record_key, 'version' => $approved->version];
}
echo json_encode(['user' => $f['receiver']->id, 'tenant' => $f['tenant']->id, 'stock' => $f['stock']->id, 'warehouse' => $f['warehouse']->id, 'records' => $records], JSON_THROW_ON_ERROR);
exit;
}
if (($argv[1] ?? '') === 'prepare_return') {
$f = receiptFixture('100.000');
supplyGrant($f['receiver'], ['warehouse', 'inventory_disposition_prepare']);
supplyGrant($f['issuer'], ['inventory_disposition_review']);
auth()->guard()->setUser($f['receiver']);
$receipt = app(ReceiptWorkflow::class)->confirm($f['receiver'], $f['order'], receiptPayload($f, '10.000'));
app(ReceiptWorkflow::class)->confirm($f['receiver'], $f['order'], receiptPayload($f, '10.000'));
$records = [];
foreach ([1, 2] as $number) {
auth()->guard()->setUser($f['receiver']);
$draft = app(InventoryDispositions::class)->save($f['receiver'], supplyInput(['kind' => 'supplier_return', 'reference' => 'RET-'.$number,
'stock_item_id' => $f['stock']->id, 'warehouse_id' => $f['warehouse']->id, 'receipt_item_id' => $receipt->items->first()->id, 'quantity' => '7.000']));
auth()->guard()->setUser($f['issuer']);
$approved = app(InventoryDispositions::class)->save($f['issuer'], supplyInput([], 'approve', $draft));
$records[] = ['record_key' => $approved->record_key, 'version' => $approved->version];
}
echo json_encode(['user' => $f['receiver']->id, 'tenant' => $f['tenant']->id, 'stock' => $f['stock']->id, 'warehouse' => $f['warehouse']->id, 'records' => $records], JSON_THROW_ON_ERROR);
exit;
}
if (in_array($argv[1] ?? '', ['prepare_lots', 'prepare_count', 'prepare_policy'], true)) {
$f = receiptFixture('100.000');
supplyGrant($f['issuer'], ['inventory_lot_prepare', 'inventory_adjust_review']);
supplyGrant($f['receiver'], ['warehouse', 'inventory_lot_review', 'inventory_lot_release']);
auth()->guard()->setUser($f['receiver']);
app(ReceiptWorkflow::class)->confirm($f['receiver'], $f['order'], receiptPayload($f, '10.000'));
auth()->guard()->setUser($f['issuer']);
$draft = app(InventoryLots::class)->save($f['issuer'], supplyInput(['stock_item_id' => $f['stock']->id, 'warehouse_id' => $f['warehouse']->id,
'requires_expiry' => true, 'minimum_remaining_days' => 0, 'lots' => [['code' => 'RACE', 'expires_on' => today()->addDays(10)->toDateString()]]], 'prepare'));
auth()->guard()->setUser($f['receiver']);
app(InventoryLots::class)->save($f['receiver'], supplyInput([], 'activate', $draft));
$lot = app(InventoryLots::class)->context($f['stock']->id, $f['warehouse']->id)['lots'][0];
$metadata = SupplyRecord::query()->findOrFail($lot['metadata_id']);
$release = app(InventoryLots::class)->save($f['receiver'], supplyInput(['lot_id' => $lot['id'], 'warehouse_id' => $f['warehouse']->id, 'state_version' => 0], 'release', $metadata));
$destination = Warehouse::query()->create(['tenant_id' => $f['tenant']->id, 'created_by' => $f['receiver']->id, 'code' => 'DEST', 'name' => 'Destino da corrida']);
auth()->guard()->setUser($f['issuer']);
$destinationDraft = app(InventoryLots::class)->save($f['issuer'], supplyInput(['stock_item_id' => $f['stock']->id, 'warehouse_id' => $destination->id,
'requires_expiry' => true, 'minimum_remaining_days' => 0, 'lots' => []], 'prepare'));
auth()->guard()->setUser($f['receiver']);
app(InventoryLots::class)->save($f['receiver'], supplyInput([], 'activate', $destinationDraft));
$count = null;
$policy = null;
if ($argv[1] === 'prepare_policy') {
$active = app(InventoryLots::class)->setup($f['stock']->id, $f['warehouse']->id);
auth()->guard()->setUser($f['issuer']);
$policy = app(InventoryLots::class)->save($f['issuer'], supplyInput([...$active->data, 'minimum_remaining_days' => 2, 'lots' => []], 'prepare', $active));
}
if ($argv[1] === 'prepare_count') {
$count = app(StockWorkflow::class)->record($f['receiver'], ['kind' => 'count', 'reference' => 'CNT-'.Str::uuid(), 'idempotency_key' => (string) Str::uuid(),
'warehouse_id' => $f['warehouse']->id, 'stock_item_id' => $f['stock']->id, 'responsible_id' => $f['receiver']->id, 'quantity' => '9.000', 'reason' => 'Contagem para disputa de ajuste físico.',
'ledger_version' => app(StockWorkflow::class)->balance($f['stock']->id, $f['warehouse']->id)['version'],
'lot_version' => app(InventoryCounts::class)->source($f['stock']->id, $f['warehouse']->id)['hash'], 'lot_counts' => [['lot_id' => $lot['id'], 'quantity' => '9.000']]]);
}
echo json_encode(['user' => $f['receiver']->id, 'preparer' => $f['issuer']->id, 'tenant' => $f['tenant']->id, 'stock' => $f['stock']->id, 'warehouse' => $f['warehouse']->id,
'policy_record_key' => $policy?->record_key, 'policy_version' => $policy?->version,
'lot_id' => $lot['id'], 'reviewer' => $f['issuer']->id, 'count_id' => $count?->id, 'destination' => $destination->id, 'record_key' => $metadata->record_key, 'version' => $metadata->version, 'state_version' => $release->version], JSON_THROW_ON_ERROR);
exit;
}
if (($argv[1] ?? '') === 'prepare_suggestion') {
require_once dirname(__DIR__).'/Support/SuggestionFixture.php';
$f = suggestionFixture();
auth()->guard()->setUser($f['issuer']);
$draft = app(SupplyPlanning::class)->save($f['issuer'], 'suggestion', supplyInput(['policy_key' => $f['policy']->record_key]));
auth()->guard()->setUser($f['receiver']);
$approved = app(SupplyPlanning::class)->save($f['receiver'], 'suggestion', supplyInput([], 'approve', $draft));
echo json_encode(['user' => $f['issuer']->id, 'tenant' => $f['tenant']->id, 'record_key' => $approved->record_key, 'version' => $approved->version, 'cost_center_id' => $f['r']->cost_center_id], JSON_THROW_ON_ERROR);
exit;
}
if (($argv[1] ?? '') === 'prepare') {
$f = receiptFixture();
supplyGrant($f['receiver'], ['supply_reserve']);
echo json_encode(['user' => $f['receiver']->id, 'stock' => $f['stock']->id, 'warehouse' => $f['warehouse']->id, 'tenant' => $f['tenant']->id], JSON_THROW_ON_ERROR);
exit;
}
$input = json_decode($argv[2], true, 16, JSON_THROW_ON_ERROR);
$actor = User::query()->findOrFail(in_array($argv[1] ?? '', ['hold_lot_adjust', 'lot_adjust'], true) ? $input['reviewer'] : (($argv[1] ?? '') === 'hold_lot_identify' ? $input['preparer'] : $input['user']));
auth()->guard()->setUser($actor);
if (in_array($argv[1] ?? '', ['hold', 'hold_suggestion', 'hold_lot_issue', 'hold_lot_identify', 'hold_lot_transfer', 'hold_lot_adjust', 'hold_disposition', 'hold_stock_reversal'], true)) {
$held = false;
DB::listen(function (QueryExecuted $event) use (&$held) {
if (! $held && str_contains($event->sql, '"tenants"') && str_contains($event->sql, 'for update')) {
$held = true;
echo "locked\n";
fflush(STDOUT);
usleep(2000000);
}
});
}
try {
if (in_array($argv[1] ?? '', ['hold_stock_reversal', 'stock_reversal'], true)) {
$source = $input['records'][$input['selected']];
$record = app(InventoryReversals::class)->save($actor, ['operation_key' => $input['operation_key'], ...$source, 'action' => 'execute',
'reason' => 'Estornos concorrentes ligados à mesma origem.', 'data' => ['event_on' => today()->toDateString(), 'evidence' => 'Comprovativo da correcção física no ensaio de concorrência.']]);
} elseif (($argv[1] ?? '') === 'lot_activate') {
$record = app(InventoryLots::class)->save($actor, ['operation_key' => $input['operation_key'], 'record_key' => $input['policy_record_key'], 'version' => $input['policy_version'],
'action' => 'activate', 'reason' => 'Conferência de impacto concorrente com saída real.', 'data' => []]);
} elseif (in_array($argv[1] ?? '', ['hold_disposition', 'disposition'], true)) {
$source = $input['records'][$input['selected']];
$record = app(InventoryDispositions::class)->save($actor, ['operation_key' => $input['operation_key'], ...$source, 'action' => 'execute',
'reason' => 'Execuções concorrentes de devoluções à mesma origem.', 'data' => ['event_on' => today()->toDateString(), 'evidence' => 'Guia de devolução assinada no ensaio de concorrência.']]);
} elseif (in_array($argv[1] ?? '', ['hold_lot_adjust', 'lot_adjust'], true)) {
$record = app(StockWorkflow::class)->record($actor, ['kind' => 'adjustment', 'reference' => 'ADJ-'.Str::uuid(), 'idempotency_key' => $input['operation_key'],
'responsible_id' => $actor->id, 'warehouse_id' => $input['warehouse'], 'stock_item_id' => $input['stock'], 'quantity' => '0.000', 'count_id' => $input['count_id'], 'reason' => 'Duas conferências concorrentes da mesma contagem.']);
} elseif (in_array($argv[1] ?? '', ['hold_lot_issue', 'lot_issue', 'hold_lot_transfer', 'lot_transfer'], true)) {
$transfer = in_array($argv[1], ['hold_lot_transfer', 'lot_transfer'], true);
$record = app(StockWorkflow::class)->record($actor, ['kind' => $transfer ? 'transfer' : 'issue', ...($transfer ? ['destination_warehouse_id' => $input['destination']] : []), 'reference' => 'RACE-'.Str::uuid(), 'idempotency_key' => $input['operation_key'],
'responsible_id' => $actor->id, 'warehouse_id' => $input['warehouse'], 'stock_item_id' => $input['stock'], 'quantity' => '7.000', 'reason' => 'Saídas concorrentes do mesmo lote libertado.']);
} elseif (in_array($argv[1] ?? '', ['hold_lot_identify', 'lot_release'], true)) {
$record = app(InventoryLots::class)->save($actor, ['operation_key' => $input['operation_key'], 'record_key' => $input['record_key'], 'version' => $input['version'],
'action' => $argv[1] === 'hold_lot_identify' ? 'identify' : 'release', 'reason' => 'Conferência concorrente de identificação e libertação.',
'data' => $argv[1] === 'hold_lot_identify' ? ['code' => 'CORRECTED', 'expires_on' => today()->addDays(5)->toDateString()] : ['lot_id' => $input['lot_id'], 'warehouse_id' => $input['warehouse'], 'state_version' => $input['state_version']]]);
} elseif (in_array($argv[1] ?? '', ['hold_suggestion', 'convert_suggestion'], true)) {
$record = app(SupplyPlanning::class)->save($actor, 'suggestion', ['operation_key' => $input['operation_key'], 'record_key' => $input['record_key'], 'version' => $input['version'], 'action' => 'convert', 'reason' => 'Prova de conversão concorrente da sugestão.', 'data' => ['cost_center_id' => $input['cost_center_id']]]);
} else {
$record = app(SupplyPlanning::class)->save($actor, 'reservation', ['operation_key' => $input['operation_key'], 'version' => 0, 'action' => 'save', 'reason' => 'Prova de duas reservas concorrentes.',
'data' => ['stock_item_id' => $input['stock'], 'warehouse_id' => $input['warehouse'], 'quantity_milli' => 1000, 'needed_on' => today()->toDateString(), 'additional' => false, 'reference' => $input['reference']]]);
}
echo json_encode(['result' => 'saved', 'id' => $record->id], JSON_THROW_ON_ERROR);
} catch (ValidationException $e) {
echo json_encode(['result' => 'rejected', 'fields' => array_keys($e->errors())], JSON_THROW_ON_ERROR);
}