load(); \support\App::loadAllConfig(['route']); date_default_timezone_set('Asia/Shanghai'); use app\miniapi\auth\Principal; use app\miniapi\exception\ApiException; use app\miniapi\model\MiniAudit; use app\miniapi\model\MiniCommand; use app\miniapi\model\MiniGrant; use app\miniapi\model\MiniUser; use app\miniapi\service\Access; use app\miniapi\service\EnterpriseCommands; use app\miniapi\service\WaterPresenter; use Illuminate\Container\Container; use Illuminate\Database\ConnectionResolver; use Illuminate\Database\Connectors\ConnectionFactory; use plugin\saiadmin\app\model\water\WaterAccount; use plugin\saiadmin\app\model\water\WaterDelivery; use plugin\saiadmin\app\model\water\WaterEnterprise; use plugin\saiadmin\app\model\water\WaterLedger; use plugin\saiadmin\app\model\water\WaterSku; use plugin\saiadmin\app\service\WaterBusiness; use support\Model; $checks = 0; function check(bool $ok, string $message): void { global $checks; $checks++; if (!$ok) { throw new RuntimeException($message); } } $db = (new MiniUser())->getConnection(); $originalResolver = Model::getConnectionResolver(); $peer = (new ConnectionFactory(new Container()))->make($db->getConfig(), 'mini_concurrency_peer'); $peerResolver = new ConnectionResolver(['mini_concurrency_peer' => $peer]); $peerResolver->setDefaultConnection('mini_concurrency_peer'); $enterprise = $sku = $user = 0; $armed = false; $tag = 'MINI-RACE-' . bin2hex(random_bytes(8)); $now = date('Y-m-d H:i:s'); try { check($peer->getPdo() !== $db->getPdo(), 'test requires independent connections'); // Commit only isolated fixtures so the second connection can see them. $db->transaction(function () use (&$enterprise, &$sku, &$user, $tag, $now) { $enterprise = WaterEnterprise::query()->insertGetId([ 'name' => $tag, 'contact' => 'Concurrency test', 'phone' => '13812345678', 'address' => $tag . '-original', 'created_at' => $now, 'updated_at' => $now, ]); $sku = WaterSku::query()->insertGetId([ 'name' => $tag, 'liters' => 18.9, 'available' => 10, 'created_at' => $now, 'updated_at' => $now, ]); WaterAccount::query()->insert([ 'enterprise_id' => $enterprise, 'sku_id' => $sku, 'entitlement' => 10, 'updated_at' => $now, ]); $user = MiniUser::query()->insertGetId([ 'appid' => $tag, 'openid' => $tag, 'created_at' => $now, 'updated_at' => $now, ]); MiniGrant::query()->insert([ 'user_id' => $user, 'role' => 'enterprise_admin', 'resource_id' => $enterprise, 'created_by' => 0, 'created_at' => $now, 'updated_at' => $now, ]); }); $revision = WaterPresenter::revision(WaterEnterprise::query()->findOrFail($enterprise)); $interleaved = false; $armed = true; $db->beforeExecuting(function (string $sql) use (&$armed, &$interleaved, $db, $peerResolver, $originalResolver, $enterprise, $sku) { if (!$armed || !str_contains($sql, 'sa_water_enterprise') || !str_contains(strtolower($sql), 'for update')) { return; } $armed = false; check($db->transactionLevel() > 0, 'address transaction already started'); check(!WaterDelivery::query()->where('enterprise_id', $enterprise)->exists(), 'address snapshot predates booking'); try { Model::setConnectionResolver($peerResolver); (new WaterBusiness())->createDelivery(['enterprise_id' => $enterprise, 'sku_id' => $sku, 'quantity' => 2], 0); } finally { Model::setConnectionResolver($originalResolver); } check(!WaterDelivery::query()->where('enterprise_id', $enterprise)->exists(), 'plain read still misses committed booking under REPEATABLE READ'); $interleaved = true; }); try { (new EnterpriseCommands(new Access(new Principal((string) $user))))->address($enterprise, [ 'request_key' => $tag . '-address', 'revision' => $revision, 'address' => $tag . '-moved', 'contact' => 'Concurrency test', 'phone' => '13812345678', ]); throw new RuntimeException('Address change must reject the concurrent booking'); } catch (ApiException $error) { check($error->getCode() === 40902, 'current read rejects active task'); } check($interleaved, 'booking committed before enterprise lock'); check(WaterEnterprise::query()->findOrFail($enterprise)->address === $tag . '-original', 'address preserved'); check(WaterDelivery::query()->where('enterprise_id', $enterprise)->count() === 1, 'concurrent booking remains committed'); check(!MiniCommand::query()->where('user_id', $user)->exists(), 'rejected command not persisted'); } finally { $armed = false; Model::setConnectionResolver($originalResolver); while ($db->transactionLevel() > 0) { $db->rollBack(); } $peer->disconnect(); // Exact fixture IDs only; existing enterprises, inventory and users are never modified. $db->transaction(function () use ($enterprise, $sku, $user) { if ($enterprise) { WaterLedger::query()->where('enterprise_id', $enterprise)->delete(); WaterDelivery::query()->where('enterprise_id', $enterprise)->delete(); WaterAccount::query()->where('enterprise_id', $enterprise)->delete(); WaterEnterprise::query()->where('id', $enterprise)->delete(); } if ($sku) { WaterSku::query()->where('id', $sku)->delete(); } if ($user) { MiniAudit::query()->where('actor_type', 'mini')->where('actor_id', $user)->delete(); MiniCommand::query()->where('user_id', $user)->delete(); MiniGrant::query()->where('user_id', $user)->delete(); MiniUser::query()->where('id', $user)->delete(); } }); } check(!WaterEnterprise::query()->where('id', $enterprise)->exists(), 'enterprise fixture cleaned'); check(!WaterSku::query()->where('id', $sku)->exists(), 'inventory fixture cleaned'); check(!MiniUser::query()->where('id', $user)->exists(), 'identity fixture cleaned'); echo "PASS: $checks two-connection address checks; isolated fixtures cleaned.\n";