Sync SMS panel with SuperAdmin and mirror local wallet/logs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-05 22:24:35 +03:30
co-authored by Cursor
parent c6b1478b21
commit 62d83a5bfd
4 changed files with 485 additions and 68 deletions
+148 -68
View File
@@ -2,14 +2,18 @@
namespace App\Services\SmsBilling;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* SuperAdmin SMS wallet client (status / consume).
* SuperAdmin SMS wallet client (status / consume / panel sync).
*
* Preferred: SUPERADMIN_SMS_BASE_URL + SUPERADMIN_SMS_SHARED_SECRET (+ optional DOMAIN)
* Fallback: SUPERADMIN_REMOTE_TOKEN
* Preferred auth: shared HMAC + domain (one secret for all sold versions).
* Env: SUPERADMIN_SMS_BASE_URL + SUPERADMIN_SMS_SHARED_SECRET
* Optional: SUPERADMIN_SMS_DOMAIN (else host of APP_URL)
*
* Fallback: SUPERADMIN_REMOTE_TOKEN (per-tenant Bearer) if shared secret missing.
*/
class SuperAdminSmsClient
{
@@ -36,50 +40,24 @@ class SuperAdminSmsClient
*/
public function status(int $parts = 1, bool $isAlert = false): ?array
{
if (! $this->enabled()) {
$data = $this->requestJson('GET', '/api/remote/sms/status', 'status', [
'parts' => max(1, $parts),
'isAlert' => $isAlert ? '1' : '0',
], null, 4, 'status');
if ($data === null) {
return null;
}
try {
$path = '/api/remote/sms/status';
$response = Http::withHeaders($this->authHeaders('GET', $path))
->acceptJson()
->timeout(4)
->get($this->url('status'), [
'parts' => max(1, $parts),
'isAlert' => $isAlert ? '1' : '0',
]);
if (! $response->successful()) {
Log::warning('SuperAdmin SMS status failed', [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
}
$data = $response->json();
if (! is_array($data)) {
return null;
}
return [
'allowed' => (bool) ($data['allowed'] ?? false),
'blocked' => (bool) ($data['blocked'] ?? false),
'reason' => isset($data['reason']) ? (string) $data['reason'] : null,
'balanceRials' => (int) ($data['balanceRials'] ?? 0),
'pricePerPartRials' => (int) ($data['pricePerPartRials'] ?? 0),
'estimatedCostRials' => (int) ($data['estimatedCostRials'] ?? 0),
'balanceLevel' => (string) ($data['balanceLevel'] ?? 'unknown'),
];
} catch (\Throwable $e) {
Log::warning('SuperAdmin SMS status exception', [
'error' => $e->getMessage(),
]);
return null;
}
return [
'allowed' => (bool) ($data['allowed'] ?? false),
'blocked' => (bool) ($data['blocked'] ?? false),
'reason' => isset($data['reason']) ? (string) $data['reason'] : null,
'balanceRials' => (int) ($data['balanceRials'] ?? 0),
'pricePerPartRials' => (int) ($data['pricePerPartRials'] ?? 0),
'estimatedCostRials' => (int) ($data['estimatedCostRials'] ?? 0),
'balanceLevel' => (string) ($data['balanceLevel'] ?? 'unknown'),
];
}
/**
@@ -93,27 +71,136 @@ class SuperAdminSmsClient
?string $source = null,
bool $isAlert = false
): ?array {
if (! $this->enabled()) {
$data = $this->requestJson('POST', '/api/remote/sms/consume', 'consume', [], [
'receiver' => $receiver,
'body' => mb_substr($body, 0, 1000),
'parts' => max(1, $parts),
'provider' => $provider,
'source' => $source,
'isAlert' => $isAlert,
], 6, 'consume');
if ($data === null || !($data['ok'] ?? false)) {
return null;
}
return [
'ok' => true,
'balanceRials' => (int) ($data['balanceRials'] ?? 0),
'costRials' => (int) ($data['costRials'] ?? 0),
'pricePerPartRials' => (int) ($data['pricePerPartRials'] ?? 0),
];
}
public function overview(): ?array
{
return $this->requestJson('GET', '/api/remote/sms/overview', 'overview', [], null, 6, 'overview');
}
public function balance(): ?array
{
return $this->requestJson('GET', '/api/remote/sms/balance', 'balance', [], null, 4, 'balance');
}
public function chargeInfo(): ?array
{
return $this->requestJson('GET', '/api/remote/sms/charge-info', 'charge-info', [], null, 4, 'charge-info');
}
public function updateChargeInfo(array $payload): ?array
{
return $this->requestJson('PUT', '/api/remote/sms/charge-info', 'charge-info', [], $payload, 6, 'charge-info-update');
}
public function charge(int $amount, ?string $note = null): ?array
{
return $this->requestJson('POST', '/api/remote/sms/charge', 'charge', [], [
'amount' => $amount,
'note' => $note,
], 8, 'charge');
}
public function updatePrice(int $pricePerPart): ?array
{
return $this->requestJson('PUT', '/api/remote/sms/price', 'price', [], [
'price_per_part' => max(0, $pricePerPart),
], 6, 'price');
}
public function setBlocked(bool $isBlocked): ?array
{
return $this->requestJson('PUT', '/api/remote/sms/block', 'block', [], [
'is_blocked' => $isBlocked,
], 6, 'block');
}
public function logsTable(array $payload): ?array
{
return $this->requestJson('POST', '/api/remote/sms/logs/table', 'logs/table', [], $payload, 10, 'logs-table');
}
public function ledgerTable(array $payload): ?array
{
return $this->requestJson('POST', '/api/remote/sms/ledger/table', 'ledger/table', [], $payload, 10, 'ledger-table');
}
public function report(?string $fromDate = null, ?string $toDate = null): ?array
{
$query = array_filter([
'from_date' => $fromDate,
'to_date' => $toDate,
], static fn ($v) => $v !== null && $v !== '');
return $this->requestJson('GET', '/api/remote/sms/report', 'report', $query, null, 8, 'report');
}
public function chargeReport(?string $fromDate = null, ?string $toDate = null): ?array
{
$query = array_filter([
'from_date' => $fromDate,
'to_date' => $toDate,
], static fn ($v) => $v !== null && $v !== '');
return $this->requestJson('GET', '/api/remote/sms/report/charges', 'report/charges', $query, null, 8, 'charge-report');
}
/**
* @param array<string, mixed> $query
* @param array<string, mixed>|null $body
* @return array<string, mixed>|null
*/
private function requestJson(
string $method,
string $canonicalPath,
string $relativeUrl,
array $query = [],
?array $body = null,
int $timeout = 6,
string $label = 'request'
): ?array {
if (!$this->enabled()) {
return null;
}
try {
$path = '/api/remote/sms/consume';
$payload = [
'receiver' => $receiver,
'body' => mb_substr($body, 0, 1000),
'parts' => max(1, $parts),
'provider' => $provider,
'source' => $source,
'isAlert' => $isAlert,
];
$response = Http::withHeaders($this->authHeaders('POST', $path))
$pending = Http::withHeaders($this->authHeaders($method, $canonicalPath))
->acceptJson()
->timeout(6)
->post($this->url('consume'), $payload);
->timeout($timeout);
if (! $response->successful()) {
Log::error('SuperAdmin SMS consume failed', [
$url = $this->url($relativeUrl);
$method = strtoupper($method);
/** @var Response $response */
$response = match ($method) {
'GET' => $pending->get($url, $query),
'POST' => $pending->post($url, $body ?? []),
'PUT' => $pending->put($url, $body ?? []),
'PATCH' => $pending->patch($url, $body ?? []),
default => throw new \InvalidArgumentException("Unsupported method {$method}"),
};
if (!$response->successful()) {
Log::warning("SuperAdmin SMS {$label} failed", [
'status' => $response->status(),
'body' => $response->body(),
]);
@@ -122,17 +209,10 @@ class SuperAdminSmsClient
}
$data = $response->json();
if (! is_array($data) || ! ($data['ok'] ?? false)) {
return null;
}
return [
'ok' => true,
'balanceRials' => (int) ($data['balanceRials'] ?? 0),
'costRials' => (int) ($data['costRials'] ?? 0),
];
return is_array($data) ? $data : null;
} catch (\Throwable $e) {
Log::error('SuperAdmin SMS consume exception', [
Log::warning("SuperAdmin SMS {$label} exception", [
'error' => $e->getMessage(),
]);