Files
SGF/scripts/b1-concurrency-proof.php

41 lines
3.4 KiB
PHP

<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\Process\Process;
require __DIR__.'/../vendor/autoload.php';
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
$fixture = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'), true, 32, JSON_THROW_ON_ERROR);
$base = ['operation' => 'b1.confirm', 'actor' => $fixture['manager_id'], 'tenant' => $fixture['tenant'], 'company' => $fixture['company'], 'unit' => $fixture['unit'], 'version' => 1];
app(\App\Shared\Context\CompanyContext::class)->selectFor($base['actor'], $base['tenant'], $base['company'], $base['unit']);
$workflow = app(\App\Modules\Services\ServiceWorkflow::class);
$save = fn ($area, $data) => $workflow->save($area, null, $base['actor'], $data, (string) Str::uuid())['id'];
$client = $save('clients', ['name' => 'Cliente concorrência B1 sintético']);
$service = $save('services', ['name' => 'Serviço concorrência B1 sintético']);
$source = $save('service_drafts', ['client_id' => $client, 'lines' => [['service_id' => $service, 'quantity' => '1', 'unit_price' => '12.50']]]);
$reports = [];
foreach (['same_key', 'different_keys'] as $mode) {
$draft = $save('billing_drafts', ['source_id' => $source]);
$key = (string) Str::uuid();
$input = $base + ['id' => $draft, 'key' => $key];
$first = new Process([PHP_BINARY, 'scripts/a-concurrency-worker.php', '--env=testing'], base_path(), null, json_encode($input + ['hold' => true]));
$secondInput = $input; if ($mode === 'different_keys') { $secondInput['key'] = (string) Str::uuid(); }
$second = new Process([PHP_BINARY, 'scripts/a-concurrency-worker.php', '--env=testing'], base_path(), null, json_encode($secondInput));
$before = DB::table('a_events')->where('type', 'b1.billing_draft.confirmed_internal')->count();
$first->start();
if (! $first->waitUntil(fn ($type, $buffer) => str_contains($buffer, 'LOCK_ACQUIRED'))) { throw new RuntimeException('Concurrent lock not established.'); }
$second->start(); $first->wait(); $second->wait(); $results = [];
foreach ([$first, $second] as $process) {
if (! $process->isSuccessful()) { throw new RuntimeException('Synthetic worker failed.'); }
$lines = array_values(array_filter(explode("\n", trim($process->getOutput())), fn ($line) => str_starts_with($line, '{')));
$results[] = json_decode(end($lines), true, 32, JSON_THROW_ON_ERROR);
}
if ($results[0]['status'] !== 200 || $results[1]['status'] !== 200 || $results[0]['result']['id'] !== $results[1]['result']['id'] || ! $results[1]['result']['replayed'] || DB::table('a_events')->where('type', 'b1.billing_draft.confirmed_internal')->count() !== $before + 1 || DB::table('b_internal_confirmations')->where('billing_draft_id', $draft)->count() !== 1) { throw new RuntimeException('Confirmation deduplication failed.'); }
$reports[$mode] = ['parallel_processes' => true, 'company_lock_overlap' => true, 'same_confirmation' => true, 'new_confirmations' => 1, 'new_events' => 1, 'second_replayed' => true];
}
file_put_contents(base_path('docs/proofs/b1-concurrency.json'), json_encode(['database' => 'sgf_tempo_test', 'results' => $reports], JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo "B1 concurrent confirmations passed with identical and different keys: one confirmation/event per draft.\n";