create(['balance_rub' => $balanceRub]); return Campaign::create([ 'tenant_id' => $tenant->id, 'ad_text' => 'Приходите к нам в канал', 'ad_link' => 'https://t.me/example_channel', 'audience_kind' => 'list', 'budget_cap_rub' => '1000.00', 'status' => Campaign::STATUS_MODERATING, ]); } it('списывает под боевой ролью с контекстом клиента — как это делает джоб запуска', function () { $campaign = tgDenezhnayaKampaniya('5000.00'); $svc = app(TelegramCampaignChargeService::class); podTgBoevoyRolyu(fn () => DB::transaction(function () use ($campaign, $svc) { DB::statement('SET LOCAL app.current_tenant_id = '.$campaign->tenant_id); $svc->charge($campaign, '1200.00'); })); // Читаем суперюзером — прибор не должен зависеть от той же защиты, что проверяем. expect((string) Tenant::whereKey($campaign->tenant_id)->first()->balance_rub)->toBe('3800.00'); expect(BalanceTransaction::where('related_id', $campaign->id) ->where('type', BalanceTransaction::TYPE_TG_AD_CHARGE)->count())->toBe(1); }); it('возвращает под боевой ролью с контекстом клиента — как это делает джоб модерации', function () { $campaign = tgDenezhnayaKampaniya('5000.00'); $svc = app(TelegramCampaignChargeService::class); $svc->charge($campaign, '1200.00'); // Модерация МТС отклонила кампанию: джоб возвращает клиенту всю смету. podTgBoevoyRolyu(fn () => DB::transaction(function () use ($campaign, $svc) { DB::statement('SET LOCAL app.current_tenant_id = '.$campaign->tenant_id); $svc->refund($campaign); })); expect((string) Tenant::whereKey($campaign->tenant_id)->first()->balance_rub)->toBe('5000.00'); expect(BalanceTransaction::where('related_id', $campaign->id) ->where('type', BalanceTransaction::TYPE_TG_AD_REFUND)->count())->toBe(1); }); it('без контекста клиента возврат под боевой ролью падает ГРОМКО, а не молчит', function () { $campaign = tgDenezhnayaKampaniya('5000.00'); $svc = app(TelegramCampaignChargeService::class); $svc->charge($campaign, '1200.00'); $balanceDo = (string) Tenant::whereKey($campaign->tenant_id)->first()->balance_rub; // Ровно та же работа, но джоб забыл поставить контекст. Сторож требует ошибку: // молчаливый выход означал бы «деньги не вернулись, и никто не узнал». // // 🪤 Держится это на ОДНОЙ букве: база без контекста молча отдаёт ноль строк // (`id = NULL` в политике `tenants_self_isolation` не совпадает ни с чем), и громким // это делает только `firstOrFail()` в сервисе. Заменить его на `first()` — и возврат // снова станет тихим. Поэтому проверяем именно исключение поиска тенанта. expect(fn () => podTgBoevoyRolyu(fn () => $svc->refund($campaign))) ->toThrow(ModelNotFoundException::class); // И деньги при этом не тронуты — ни возврата, ни половинчатого движения. expect((string) Tenant::whereKey($campaign->tenant_id)->first()->balance_rub)->toBe($balanceDo); });