CluPilotCloud/app/Services/Stripe/StripeCheck.php

98 lines
3.5 KiB
PHP

<?php
namespace App\Services\Stripe;
use App\Services\Secrets\SecretVault;
use Illuminate\Support\Facades\Http;
use Throwable;
/**
* Read-only answers to "does this key actually work, and what is it wired to?"
*
* Separate from StripeClient, which exists to create and archive things. A key
* that is saved but wrong is worse than no key, because the failure surfaces
* later, somewhere else, and usually to a customer — so it can be checked at
* the moment it is entered.
*
* Reports the account and the LIVE/TEST mode, because the single most expensive
* mistake here is pasting a test key into a live installation, or the reverse,
* and both look identical in a form.
*/
final class StripeCheck
{
/** @return array<string, mixed> */
public function run(?string $candidate = null): array
{
$secret = $candidate ?: app(SecretVault::class)->get('stripe.secret');
if (blank($secret)) {
return ['ok' => false, 'reason' => 'missing'];
}
try {
$account = Http::withToken($secret)->acceptJson()->timeout(15)
->get($this->url('account'));
} catch (Throwable) {
return ['ok' => false, 'reason' => 'unreachable'];
}
if ($account->status() === 401) {
return ['ok' => false, 'reason' => 'rejected'];
}
if (! $account->successful()) {
return ['ok' => false, 'reason' => 'error', 'status' => $account->status()];
}
return [
'ok' => true,
'account' => $account->json('id'),
'business' => $account->json('settings.dashboard.display_name') ?: $account->json('business_profile.name'),
// Stripe does not label the key; the mode is inferred from the
// prefix, which is the only thing that is true of both key kinds.
'live' => str_starts_with($secret, 'sk_live_') || str_starts_with($secret, 'rk_live_'),
'restricted' => str_starts_with($secret, 'rk_'),
'webhooks' => $this->webhooks($secret),
];
}
/**
* The configured webhook endpoints and which events each one subscribes to.
*
* This is the part nobody can see from the outside: a key can be perfectly
* valid while the endpoint listens for the wrong five events, and nothing
* fails until a payment is not recorded.
*
* @return array<int, array<string, mixed>>|null null when the key may not read them
*/
private function webhooks(string $secret): ?array
{
try {
$response = Http::withToken($secret)->acceptJson()->timeout(15)
->get($this->url('webhook_endpoints'), ['limit' => 10]);
} catch (Throwable) {
return null;
}
if (! $response->successful()) {
// A restricted key legitimately cannot list these. Saying "unknown"
// beats reporting "none configured", which would send someone
// hunting for a problem that is not there.
return null;
}
return collect($response->json('data', []))
->map(fn (array $endpoint) => [
'url' => $endpoint['url'] ?? '—',
'status' => $endpoint['status'] ?? 'unknown',
'events' => $endpoint['enabled_events'] ?? [],
])
->all();
}
private function url(string $path): string
{
return rtrim((string) config('services.stripe.api_base', 'https://api.stripe.com/v1'), '/').'/'.$path;
}
}