88e816c576
Ежедневный контроль баланса DaData/Поставщик/Yandex Cloud плиткой дашборда. - Таблица external_service_balances (pgsql_supplier, BYPASSRLS, last-value upsert) - BalanceHealth: чистая логика светофора (red <floor или <3д; amber <floor или <7д) - BalanceProvider+DTO; провайдеры DaData(API)/YC(OAuth→IAM→billing)/Supplier(Playwright) - RefreshExternalBalancesJob: изоляция провайдеров (try/catch), расписание 06:30 МСК - AdminDashboardController::balances() + плитка в summary + topup_url (кнопка «Пополнить») - Тесты: BalanceHealth, 3 провайдера, джоба, endpoint (102 теста зелёные) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
3.1 KiB
PHP
67 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\External\RefreshExternalBalancesJob;
|
|
use App\Services\External\BalanceProvider;
|
|
use App\Services\External\BalanceReading;
|
|
use App\Services\External\DadataBalanceProvider;
|
|
use App\Services\External\SupplierBalanceProvider;
|
|
use App\Services\External\YandexCloudBalanceProvider;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\Concerns\SharesSupplierPdo;
|
|
|
|
uses(DatabaseTransactions::class, SharesSupplierPdo::class); // запись идёт через pgsql_supplier
|
|
|
|
/** Стаб-провайдер с заранее заданным результатом. */
|
|
function fakeProvider(string $key, BalanceReading $reading): BalanceProvider
|
|
{
|
|
return new class($key, $reading) implements BalanceProvider
|
|
{
|
|
public function __construct(private string $key, private BalanceReading $reading) {}
|
|
|
|
public function serviceKey(): string
|
|
{
|
|
return $this->key;
|
|
}
|
|
|
|
public function fetch(): BalanceReading
|
|
{
|
|
return $this->reading;
|
|
}
|
|
};
|
|
}
|
|
|
|
it('пишет балансы трёх сервисов + считает светофор', function () {
|
|
config()->set('services.yandex_cloud.red_floor_rub', 1000);
|
|
config()->set('services.yandex_cloud.amber_floor_rub', 5000);
|
|
|
|
app()->instance(DadataBalanceProvider::class, fakeProvider('dadata', BalanceReading::ok('dadata', 4500, 'RUB', 100)));
|
|
app()->instance(SupplierBalanceProvider::class, fakeProvider('supplier', BalanceReading::ok('supplier', 50000, 'RUB', null)));
|
|
app()->instance(YandexCloudBalanceProvider::class, fakeProvider('yandex_cloud', BalanceReading::ok('yandex_cloud', -540.48, 'RUB', 600)));
|
|
|
|
(new RefreshExternalBalancesJob)->handle();
|
|
|
|
$rows = DB::connection('pgsql_supplier')->table('external_service_balances')->get()->keyBy('service_key');
|
|
expect($rows)->toHaveCount(3);
|
|
expect((float) $rows['yandex_cloud']->balance_amount)->toBe(-540.48);
|
|
expect($rows['yandex_cloud']->light)->toBe('red'); // минус < red_floor
|
|
expect((bool) $rows['yandex_cloud']->ok)->toBeTrue();
|
|
expect($rows['dadata']->ok)->toBeTruthy();
|
|
});
|
|
|
|
it('упавший провайдер не роняет джобу и сохраняет ошибку, остальные пишутся', function () {
|
|
app()->instance(DadataBalanceProvider::class, fakeProvider('dadata', BalanceReading::fail('dadata', 'HTTP 403')));
|
|
app()->instance(SupplierBalanceProvider::class, fakeProvider('supplier', BalanceReading::ok('supplier', 50000, 'RUB', null)));
|
|
app()->instance(YandexCloudBalanceProvider::class, fakeProvider('yandex_cloud', BalanceReading::ok('yandex_cloud', 42000, 'RUB', 600)));
|
|
|
|
(new RefreshExternalBalancesJob)->handle();
|
|
|
|
$rows = DB::connection('pgsql_supplier')->table('external_service_balances')->get()->keyBy('service_key');
|
|
expect((bool) $rows['dadata']->ok)->toBeFalse();
|
|
expect($rows['dadata']->error)->toContain('403');
|
|
expect((bool) $rows['supplier']->ok)->toBeTrue();
|
|
expect((bool) $rows['yandex_cloud']->ok)->toBeTrue();
|
|
});
|