commit de armazens
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-04 09:40:26 +02:00
parent e753f7a0a4
commit 63d20b10fc
245 changed files with 10847 additions and 230 deletions
@@ -0,0 +1,43 @@
<?php
use App\Enums\PurchasingRole;
use App\Models\Budget;
use App\Models\User;
use App\Services\BudgetWorkflow;
use App\Services\OrderCancellationPolicyWorkflow;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
require_once __DIR__.'/../Support/ReceiptFixture.php';
function issuedOrderCancellationFixture(object $test): void
{
config(['order_cancellation.enabled' => true]);
$test->f = receiptFixture();
$base = ['tenant_id' => $test->f['tenant']->id, 'department_id' => $test->f['department']->id];
$test->buyer = User::factory()->create([...$base, 'purchasing_role' => PurchasingRole::Procurement]);
$test->finance = User::factory()->create([...$base, 'purchasing_role' => PurchasingRole::Finance]);
$test->admins = [];
foreach (range(1, 3) as $index) {
$test->admins[] = User::factory()->create([...$base, 'purchasing_role' => PurchasingRole::Admin]);
}
$test->actingAs($test->admins[0]);
$flow = app(OrderCancellationPolicyWorkflow::class);
$test->policy = $flow->propose($test->admins[0], ['previous_id' => 0, 'mode' => 'issued_order', 'enabled' => true,
'valid_from' => today()->toDateString(), 'valid_until' => today()->addWeek()->toDateString(), 'reason' => 'Política exclusivamente fictícia para o circuito de testes.']);
foreach ([$test->admins[1], $test->admins[2]] as $admin) {
$test->actingAs($admin);
$flow->review($admin, $test->policy, 'approved', 'Conferência independente da política fictícia.');
}
$test->actingAs($test->finance);
$test->budget = Budget::query()->create(['tenant_id' => $test->f['tenant']->id, 'cost_center_id' => $test->f['r']->cost_center_id, 'year' => now()->year,
'amount_minor' => 20000, 'includes_tax' => true, 'reason' => 'Dotação fictícia para ensaio da reversão.', 'created_by' => $test->finance->id,
'approved_by' => $test->admins[0]->id, 'approved_at' => now()]);
DB::transaction(function () use ($test) {
foreach (['reserve', 'award', 'commit'] as $kind) {
app(BudgetWorkflow::class)->record($test->finance, $test->f['r'], $kind, 10000, 10000);
}
});
$test->data = ['operation_key' => (string) Str::uuid(), 'reason' => 'Cancelamento integral fictício antes de qualquer execução.', 'evidence' => 'Referência documental fictícia do fundamento do cancelamento.'];
$test->actingAs($test->buyer);
}
@@ -0,0 +1,20 @@
<?php
use App\Models\SupplierEvaluation;
use App\Models\SupplierImprovement;
use App\Models\User;
use Illuminate\Support\Str;
function reportEvaluation(array $fixture, ?int $department = null, int $revision = 1, ?int $previous = null): SupplierEvaluation
{
return SupplierEvaluation::query()->create(['tenant_id' => $fixture['tenant']->id, 'supplier_id' => $fixture['order']->supplier_id, 'department_id' => $department ?? $fixture['r']->department_id,
'created_by' => $fixture['issuer']->id, 'operation_key' => (string) Str::uuid(), 'creation_hash' => str_repeat('b', 64), 'status' => 'approved', 'contributors' => [$fixture['issuer']->id],
'revision' => $revision, 'previous_id' => $previous, 'supplier_snapshot' => ['id' => $fixture['order']->supplier_id, 'name' => 'Fornecedor preservado QA', 'nuit' => '123456789'],
'payload' => ['title' => 'Avaliação QA', 'order_ids' => [], 'criteria' => [], 'period_start' => null, 'period_end' => null]]);
}
function reportMeasure(SupplierEvaluation $evaluation, User $responsible, array $extra = []): SupplierImprovement
{
return SupplierImprovement::query()->create([...['tenant_id' => $evaluation->tenant_id, 'supplier_evaluation_id' => $evaluation->id, 'created_by' => $responsible->id, 'responsible_id' => $responsible->id,
'operation_key' => (string) Str::uuid(), 'creation_hash' => str_repeat('c', 64), 'title' => 'Medida QA', 'description' => 'Objectivo fictício para o relatório.', 'due_on' => '2026-10-29', 'contributors' => [$responsible->id]], ...$extra]);
}
@@ -0,0 +1,76 @@
<?php
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
function orderCancellationRaces(array $environment, array $config, string $database): array
{
config(['database.connections.cancel_race_read' => [...$config, 'database' => $database, 'url' => null]]);
$reader = DB::connection('cancel_race_read');
$results = [];
foreach (['request_receipt', 'receipt_request', 'double_approval'] as $scenario) {
$worker = fn (array $args) => new Process([PHP_BINARY, 'tests/Support/order-cancellation-worker.php', ...$args], base_path(), $environment, timeout: 40);
$seed = $worker(['seed']);
$seed->mustRun();
$data = json_decode($seed->getOutput(), true, 64, JSON_THROW_ON_ERROR);
$data['barrier'] = storage_path('framework/testing/cancellation-race-'.bin2hex(random_bytes(8)));
if (! is_dir(dirname($data['barrier'])) && ! mkdir(dirname($data['barrier']), 0700, true)) {
throw new RuntimeException('Testing barrier directory unavailable.');
}
if ($scenario === 'double_approval') {
$worker(['run', json_encode($data, JSON_THROW_ON_ERROR), 'request'])->mustRun();
}
$first = $worker(['hold', json_encode($data, JSON_THROW_ON_ERROR), $scenario === 'receipt_request' ? 'receipt' : ($scenario === 'double_approval' ? 'approve' : 'request')]);
$second = $worker(['run', json_encode($data, JSON_THROW_ON_ERROR), $scenario === 'receipt_request' ? 'request' : ($scenario === 'double_approval' ? 'approve' : 'receipt')]);
try {
$first->start();
$deadline = microtime(true) + 15;
while ($first->isRunning() && ! str_contains($first->getOutput(), 'LOCKED') && microtime(true) < $deadline) {
usleep(20000);
}
if (! str_contains($first->getOutput(), 'LOCKED')) {
throw new RuntimeException('Company lock not acquired by first worker.');
}
$second->start();
$waited = false;
$deadline = microtime(true) + 15;
while ($second->isRunning() && microtime(true) < $deadline) {
$waited = $reader->selectOne("select count(*) as total from pg_stat_activity where datname = ? and application_name = 'cancel-race-run' and wait_event_type = 'Lock'", [$database])->total > 0;
if ($waited) {
break;
} usleep(20000);
}
if (! $waited) {
throw new RuntimeException('Company row lock wait not observed.');
}
file_put_contents($data['barrier'], 'release');
$first->wait();
$second->wait();
if (! $first->isSuccessful() || ! $second->isSuccessful() || ! str_contains($first->getOutput(), 'OK')
|| ! str_contains($second->getOutput(), $scenario === 'double_approval' ? 'OK' : 'BLOCKED')) {
throw new RuntimeException('Concurrent outcome not expected.');
}
$receipts = $reader->table('goods_receipts')->where('purchase_order_id', $data['order'])->count();
$cancellations = $reader->table('order_cancellations')->where('purchase_order_id', $data['order'])->get();
$reversals = $reader->table('budget_entries')->where('kind', 'order-cancel-'.$data['order'])->count();
if ($receipts !== ($scenario === 'receipt_request' ? 1 : 0) || $cancellations->count() !== ($scenario === 'receipt_request' ? 0 : 1)
|| $reversals !== ($scenario === 'double_approval' ? 1 : 0)
|| ($scenario === 'double_approval' && $cancellations->first()->status !== 'approved')) {
throw new RuntimeException('Concurrent effects not unique.');
}
$results[] = ['scenario' => $scenario, 'company_lock_wait_observed' => true, 'receipts' => $receipts, 'cancellations' => $cancellations->count(), 'reversal_entries' => $reversals, 'result' => 'passed'];
} finally {
if ($first->isRunning()) {
$first->stop();
} if ($second->isRunning()) {
$second->stop();
}
if (is_file($data['barrier'])) {
unlink($data['barrier']);
}
}
}
DB::purge('cancel_race_read');
return $results;
}
@@ -0,0 +1,73 @@
<?php
use App\Models\OrderCancellation;
use App\Models\PurchaseOrder;
use App\Models\User;
use App\Services\ConflictGuard;
use App\Services\OrderCancellationWorkflow;
use App\Services\ReceiptWorkflow;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
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('Disposable test database required.');
}
config(['order_cancellation.enabled' => true]);
require_once __DIR__.'/IssuedOrderCancellationFixture.php';
$mode = $argv[1];
if ($mode === 'seed') {
$fixture = new class extends stdClass
{
public function withoutVite(): void {}
public function actingAs(User $user): self
{
auth()->guard('web')->setUser($user);
return $this;
}
};
issuedOrderCancellationFixture($fixture);
echo json_encode(['order' => $fixture->f['order']->id, 'buyer' => $fixture->buyer->id, 'finance' => $fixture->finance->id, 'receiver' => $fixture->f['receiver']->id,
'warehouse' => $fixture->f['warehouse']->id, 'stock' => $fixture->f['stock']->id, 'line' => $fixture->f['line']->id, 'input' => $fixture->data], JSON_THROW_ON_ERROR);
exit;
}
$data = json_decode($argv[2], true, 32, JSON_THROW_ON_ERROR);
$action = $argv[3];
DB::select("select set_config('application_name', ?, false)", ['cancel-race-'.$mode]);
$actor = User::query()->findOrFail($data[$action === 'request' ? 'buyer' : ($action === 'receipt' ? 'receiver' : 'finance')]);
auth()->guard('web')->setUser($actor);
try {
DB::transaction(function () use ($data, $actor, $action, $mode) {
if ($mode === 'hold') {
app(ConflictGuard::class)->lockCompany($actor);
echo "LOCKED\n";
flush();
$deadline = microtime(true) + 20;
while (! is_file($data['barrier']) && microtime(true) < $deadline) {
usleep(20000);
}
if (! is_file($data['barrier'])) {
throw new RuntimeException('Barrier not released.');
}
}
$order = PurchaseOrder::query()->findOrFail($data['order']);
if ($action === 'request') {
app(OrderCancellationWorkflow::class)->request($actor, $order, $data['input']);
} elseif ($action === 'receipt') {
app(ReceiptWorkflow::class)->confirm($actor, $order, ['idempotency_key' => $data['input']['operation_key'], 'delivery_note' => 'RACE-GUIDE', 'received_on' => today()->toDateString(), 'warehouse_id' => $data['warehouse'],
'items' => [['purchase_order_item_id' => $data['line'], 'received_quantity' => '0.100', 'accepted_quantity' => '0.100', 'quality_status' => 'compliant', 'stock_item_id' => $data['stock'], 'notes' => null]]]);
} else {
$record = OrderCancellation::query()->where('purchase_order_id', $order->id)->where('status', 'pending')->first()
?? OrderCancellation::query()->where('purchase_order_id', $order->id)->where('status', 'approved')->firstOrFail();
app(OrderCancellationWorkflow::class)->decide($actor, $record, 'approved', 'Decisão financeira independente concorrente fictícia.');
}
});
echo "OK\n";
} catch (ValidationException $error) {
echo "BLOCKED\n";
}