From c03e2b319bda11b8fa7d2655663e78c9f0fdcab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 28 Jun 2026 07:25:21 +0300 Subject: [PATCH] =?UTF-8?q?fix(=D0=B1=D0=B0=D0=BB=D0=B0=D0=BD=D1=81=D1=8B)?= =?UTF-8?q?:=20DaData=20X-Secret=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE=D0=BA=20+=20=D0=BA=D0=BB=D0=B0=D0=BC=D0=BF=20days?= =?UTF-8?q?=5Fleft=20=D0=BA=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DadataBalanceProvider: эндпоинт profile/balance требует X-Secret вместе с Token (был HTTP 401 на проде при первом сборе); добавлен заголовок при наличии secret. - BalanceHealth: отрицательный баланс больше не даёт «−1 дн.» (кламп max(0, days)). Co-Authored-By: Claude Opus 4.8 (1M context) --- app/app/Services/Dashboard/BalanceHealth.php | 3 ++- app/app/Services/External/DadataBalanceProvider.php | 9 ++++++++- app/tests/Unit/Dashboard/BalanceHealthTest.php | 2 ++ app/tests/Unit/External/DadataBalanceProviderTest.php | 5 ++++- docs/observer/STATUS.md | 6 +++--- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/app/Services/Dashboard/BalanceHealth.php b/app/app/Services/Dashboard/BalanceHealth.php index 3994eb8a..801ba6ce 100644 --- a/app/app/Services/Dashboard/BalanceHealth.php +++ b/app/app/Services/Dashboard/BalanceHealth.php @@ -24,8 +24,9 @@ class BalanceHealth float $redFloor, float $amberFloor, ): array { + // Отрицательный/нулевой баланс → денег уже нет: 0 дней (не отрицательное «−1 дн.»). $days = ($dailySpend !== null && $dailySpend > 0) - ? (int) floor($balance / $dailySpend) + ? max(0, (int) floor($balance / $dailySpend)) : null; $light = 'green'; diff --git a/app/app/Services/External/DadataBalanceProvider.php b/app/app/Services/External/DadataBalanceProvider.php index c5468d3e..5c63369a 100644 --- a/app/app/Services/External/DadataBalanceProvider.php +++ b/app/app/Services/External/DadataBalanceProvider.php @@ -25,8 +25,15 @@ class DadataBalanceProvider implements BalanceProvider if ($key === '') { return BalanceReading::fail('dadata', 'DaData api_key не задан'); } + // Эндпоинт profile/balance требует ОБА ключа: Authorization: Token + // И X-Secret: (иначе HTTP 401). secret — тот же, что для cleaner API. + $headers = ['Authorization' => 'Token '.$key, 'Accept' => 'application/json']; + $secret = (string) config('services.dadata.secret'); + if ($secret !== '') { + $headers['X-Secret'] = $secret; + } $resp = Http::timeout(10) - ->withHeaders(['Authorization' => 'Token '.$key, 'Accept' => 'application/json']) + ->withHeaders($headers) ->get((string) config('services.dadata.balance_url')); if (! $resp->ok()) { return BalanceReading::fail('dadata', 'HTTP '.$resp->status()); diff --git a/app/tests/Unit/Dashboard/BalanceHealthTest.php b/app/tests/Unit/Dashboard/BalanceHealthTest.php index b2f67e3a..feda48f5 100644 --- a/app/tests/Unit/Dashboard/BalanceHealthTest.php +++ b/app/tests/Unit/Dashboard/BalanceHealthTest.php @@ -20,4 +20,6 @@ it('красный при низком балансе ИЛИ малом числ expect(BalanceHealth::evaluate(600, 100, 0, 0)['light'])->toBe('amber'); // отрицательный баланс (YC в минусе) → red expect(BalanceHealth::evaluate(-540, null, 1000, 5000))->toMatchArray(['days_left' => null, 'light' => 'red']); + // отрицательный баланс + известный расход → days_left КЛАМПИТСЯ к 0 (не «−1 дн.») + expect(BalanceHealth::evaluate(-591.76, 600, 1000, 5000))->toMatchArray(['days_left' => 0, 'light' => 'red']); }); diff --git a/app/tests/Unit/External/DadataBalanceProviderTest.php b/app/tests/Unit/External/DadataBalanceProviderTest.php index 6179ba0f..88611227 100644 --- a/app/tests/Unit/External/DadataBalanceProviderTest.php +++ b/app/tests/Unit/External/DadataBalanceProviderTest.php @@ -8,13 +8,16 @@ use Tests\TestCase; uses(TestCase::class); // нужен booted-app: config()/app()/Http::fake() -it('читает баланс DaData по API', function () { +it('читает баланс DaData по API c X-Secret', function () { config()->set('services.dadata.api_key', 'test-token'); + config()->set('services.dadata.secret', 'test-secret'); Http::fake(['dadata.ru/*' => Http::response(['balance' => 4500.0], 200)]); $r = app(DadataBalanceProvider::class)->fetch(); expect($r->ok)->toBeTrue(); expect($r->balance)->toBe(4500.0); expect($r->serviceKey)->toBe('dadata'); + Http::assertSent(fn ($req) => $req->hasHeader('Authorization', 'Token test-token') + && $req->hasHeader('X-Secret', 'test-secret')); }); it('ошибка API → fail, не бросает', function () { diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index a8251531..b9e5f80e 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-06-28T04:12:57.855Z +Last updated: 2026-06-28T04:13:25.256Z | Контролёр | Состояние | Детали | |---|---|---| @@ -129,9 +129,9 @@ Episodes since last run: 542 / threshold: 10 | PID | Имя | CPU-время | Возраст | |---|---|---|---| -| 3440 | MsMpEng | 17.01ч | 0.0ч | +| 3440 | MsMpEng | 17.01ч | 220544.2ч | | 21928 | Code | 7.32ч | 0.0ч | -| 1212 | svchost | 4.38ч | 0.0ч | +| 1212 | svchost | 4.38ч | 10093734.7ч | ⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.