Files

62 lines
3.3 KiB
PHP
Raw Permalink Normal View History

2026-08-29 15:53:53 +03:00
<?php
namespace App\Serp\Operations;
use App\Models\SerpOperationState;
use App\Serp\Data\ProviderHealth;
use App\Serp\Exceptions\UnexpectedProviderResponseException;
use App\Serp\Providers\DataForSeo\DataForSeoClient;
use App\Serp\Providers\DataForSeo\DataForSeoErrorClassifier;
use Illuminate\Support\Facades\Cache;
final class DataForSeoHealthService
{
public function __construct(private DataForSeoClient $client, private DataForSeoErrorClassifier $errors, private KillSwitch $kill, private OperationAuditLogger $audit) {}
public function validateConfig(): array
{
$configured = (string) config('serp.dataforseo.login') !== '' && (string) config('serp.dataforseo.password') !== '';
$base = rtrim((string) config('serp.dataforseo.base_url'), '/');
$parts = parse_url($base);
if (! is_array($parts) || ($parts['scheme'] ?? null) !== 'https' || strtolower($parts['host'] ?? '') !== 'api.dataforseo.com') {
throw new \InvalidArgumentException('Invalid provider endpoint.');
}if ($configured) {
$this->client->validateConfiguration();
}
foreach (['daily_budget_usd', 'monthly_budget_usd', 'low_balance_usd', 'critical_balance_usd'] as $key) {
if (Money::micros((string) config('serp.'.$key)) <= 0) {
throw new \InvalidArgumentException('Invalid '.$key);
}
}
return ['provider' => 'dataforseo', 'host' => 'api.dataforseo.com', 'tls' => true, 'credentials_configured' => $configured, 'operation_mode' => app(ActivationManager::class)->state()->value];
}
public function remote(bool $fresh = false): ProviderHealth
{
return Cache::remember('serp:dataforseo:health', $fresh ? 0 : (int) config('serp.health_ttl_seconds'), function () {
$data = $this->client->userData();
$status = $data['status_code'] ?? null;
if (! is_int($status)) {
throw new UnexpectedProviderResponseException('Health status missing.');
}if ($status !== 20000) {
throw $this->errors->api($status);
}$task = $data['tasks'][0] ?? null;
if (! is_array($task) || ($task['status_code'] ?? null) !== 20000) {
throw new UnexpectedProviderResponseException('Health task malformed.');
}$balance = $task['result'][0]['money']['balance'] ?? null;
if (! is_int($balance) && ! is_float($balance) && ! is_string($balance)) {
throw new UnexpectedProviderResponseException('Balance missing.');
}$health = new ProviderHealth('dataforseo', Money::micros($balance), 20000, now('UTC'));
SerpOperationState::query()->updateOrCreate(['id' => 1], ['last_health_at' => $health->checkedAt, 'last_balance_micros' => $health->balanceMicros, 'last_provider_status_code' => 20000, 'last_provider_success_at' => $health->checkedAt, 'last_error_category' => null]);
if ($health->balanceMicros < Money::micros((string) config('serp.critical_balance_usd'))) {
$this->kill->activate('critical_low_balance');
} elseif ($health->balanceMicros < Money::micros((string) config('serp.low_balance_usd'))) {
$this->audit->record('low_balance', ['cost_micros' => $health->balanceMicros]);
}
return $health;
});
}
}