add SMSpanel

This commit is contained in:
2026-07-04 11:55:10 +03:30
parent 1ce2e73ea9
commit 163097f42a
22 changed files with 6683 additions and 2214 deletions
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Exceptions;
use Exception;
class SmsBlockedException extends Exception
{
public function __construct(string $message = 'ارسال پیامک موقتاً غیرفعال است')
{
parent::__construct($message);
}
}
@@ -0,0 +1,237 @@
<?php
namespace App\Http\Controllers\Admin;
use App\DB\Te\TableEngine;
use App\Http\Controllers\Controller;
use App\Models\SmsLedger;
use App\Models\SmsSendLog;
use App\Services\SmsBilling\SmsWalletService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SmsPanelController extends Controller
{
public function overview()
{
$wallet = app(SmsWalletService::class)->getWallet();
$monthStart = Carbon::now()->startOfMonth();
$monthStats = SmsSendLog::query()
->where('created_at', '>=', $monthStart)
->selectRaw('COALESCE(SUM(parts), 0) as total_parts, COALESCE(SUM(total_amount), 0) as total_amount, COUNT(*) as total_messages')
->first();
$todayStats = SmsSendLog::query()
->whereDate('created_at', Carbon::today())
->selectRaw('COALESCE(SUM(parts), 0) as total_parts, COALESCE(SUM(total_amount), 0) as total_amount, COUNT(*) as total_messages')
->first();
return response()->json([
'wallet' => [
'balance' => (int) $wallet->balance,
'price_per_part' => (int) $wallet->price_per_part,
'is_blocked' => (bool) $wallet->is_blocked,
'alert_days_sent' => (int) $wallet->alert_days_sent,
'last_alert_date' => $wallet->last_alert_date?->toDateString(),
],
'stats' => [
'today' => [
'parts' => (int) ($todayStats->total_parts ?? 0),
'amount' => (int) ($todayStats->total_amount ?? 0),
'messages' => (int) ($todayStats->total_messages ?? 0),
],
'month' => [
'parts' => (int) ($monthStats->total_parts ?? 0),
'amount' => (int) ($monthStats->total_amount ?? 0),
'messages' => (int) ($monthStats->total_messages ?? 0),
],
],
]);
}
public function charge(Request $request)
{
$data = $request->validate([
'amount' => 'required|integer|min:1',
'note' => 'nullable|string|max:500',
]);
$wallet = app(SmsWalletService::class)->charge(
(int) $data['amount'],
auth()->id(),
$data['note'] ?? null
);
return response()->json([
'message' => 'کیف پول با موفقیت شارژ شد.',
'balance' => (int) $wallet->balance,
]);
}
public function updatePrice(Request $request)
{
$data = $request->validate([
'price_per_part' => 'required|integer|min:0',
]);
$wallet = app(SmsWalletService::class)->updatePricePerPart((int) $data['price_per_part']);
return response()->json([
'message' => 'قیمت هر پارت به‌روزرسانی شد.',
'price_per_part' => (int) $wallet->price_per_part,
]);
}
public function updateBlock(Request $request)
{
$data = $request->validate([
'is_blocked' => 'required|boolean',
]);
$wallet = app(SmsWalletService::class)->setBlocked((bool) $data['is_blocked']);
return response()->json([
'message' => $wallet->is_blocked ? 'ارسال پیامک مسدود شد.' : 'ارسال پیامک فعال شد.',
'is_blocked' => (bool) $wallet->is_blocked,
]);
}
public function logsTable(Request $request)
{
$records = SmsSendLog::query()->orderByDesc('id');
if ($request->filled('from_date')) {
$records->whereDate('created_at', '>=', $request->input('from_date'));
}
if ($request->filled('to_date')) {
$records->whereDate('created_at', '<=', $request->input('to_date'));
}
$table = TableEngine::create($records, $request->all())
->initSearch(['receiver', 'body', 'provider', 'source'])
->select([
'id',
'receiver',
'body',
'parts',
'price_per_part',
'total_amount',
'provider',
'source',
'is_alert',
'created_at',
]);
return response()->json($table->export());
}
public function ledgerTable(Request $request)
{
$records = SmsLedger::query()->orderByDesc('id');
if ($request->filled('from_date')) {
$records->whereDate('created_at', '>=', $request->input('from_date'));
}
if ($request->filled('to_date')) {
$records->whereDate('created_at', '<=', $request->input('to_date'));
}
$table = TableEngine::create($records, $request->all())
->initSearch(['type', 'note'])
->select([
'id',
'type',
'amount',
'balance_after',
'price_per_part',
'parts',
'note',
'created_by',
'created_at',
]);
return response()->json($table->export());
}
public function report(Request $request)
{
$from = $request->input('from_date', Carbon::now()->startOfMonth()->toDateString());
$to = $request->input('to_date', Carbon::now()->toDateString());
$summary = SmsSendLog::query()
->whereDate('created_at', '>=', $from)
->whereDate('created_at', '<=', $to)
->selectRaw('COALESCE(SUM(parts), 0) as total_parts, COALESCE(SUM(total_amount), 0) as total_amount, COUNT(*) as total_messages')
->first();
$byProvider = SmsSendLog::query()
->whereDate('created_at', '>=', $from)
->whereDate('created_at', '<=', $to)
->select('provider', DB::raw('COUNT(*) as messages'), DB::raw('SUM(parts) as parts'), DB::raw('SUM(total_amount) as amount'))
->groupBy('provider')
->get();
$wallet = app(SmsWalletService::class)->getWallet();
return response()->json([
'from_date' => $from,
'to_date' => $to,
'balance' => (int) $wallet->balance,
'summary' => [
'parts' => (int) ($summary->total_parts ?? 0),
'amount' => (int) ($summary->total_amount ?? 0),
'messages' => (int) ($summary->total_messages ?? 0),
],
'by_provider' => $byProvider,
]);
}
public function chargeReport(Request $request)
{
$request->validate([
'from_date' => 'nullable|date',
'to_date' => 'nullable|date',
]);
$baseQuery = SmsLedger::query()->where('type', SmsWalletService::TYPE_CHARGE);
if ($request->filled('from_date')) {
$baseQuery->whereDate('created_at', '>=', $request->input('from_date'));
}
if ($request->filled('to_date')) {
$baseQuery->whereDate('created_at', '<=', $request->input('to_date'));
}
$summary = (clone $baseQuery)
->selectRaw('COUNT(*) as total_charges, COALESCE(SUM(amount), 0) as total_amount')
->first();
$items = (clone $baseQuery)
->orderByDesc('created_at')
->select([
'id',
'amount',
'balance_after',
'note',
'created_by',
'created_at',
])
->get();
return response()->json([
'from_date' => $request->input('from_date'),
'to_date' => $request->input('to_date'),
'summary' => [
'charges' => (int) ($summary->total_charges ?? 0),
'amount' => (int) ($summary->total_amount ?? 0),
],
'items' => $items,
]);
}
}
@@ -8,6 +8,7 @@ use App\Models\Permission;
use App\Models\User;
use Illuminate\Http\Request;
use App\Models\Setting;
use App\Services\SmsBilling\SmsWalletService;
class UserController extends SuperController
{
@@ -33,6 +34,11 @@ class UserController extends SuperController
$query = Setting::query();
$settings = $query->get();
$user->settings= $settings;
$user->setAttribute('sms_billing_enabled', SmsWalletService::isBillingEnabled());
$user->setAttribute(
'sms_panel_access',
SmsWalletService::isBillingEnabled() && SmsWalletService::isSmsPanelSuperAdmin($user)
);
return response()->json($user);
}
+17 -34
View File
@@ -7,7 +7,9 @@ use App\Models\User;
use App\Models\Customer;
use App\Models\Setting;
use App\Models\Message;
use App\Exceptions\SmsBlockedException;
use App\Services\ConfigService;
use App\Services\SmsBilling\SmsGatewayService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
@@ -103,9 +105,6 @@ class Controller extends BaseController
public static function SendMessage($to, $msg ,$is_send = false,$force_method ='null')
{
$config = app(ConfigService::class);
// $config = config('message', false);
$credentials = [
"telegram" => [
"token" => self::Config('telegram_bot_token')
@@ -148,44 +147,28 @@ class Controller extends BaseController
//SMS
if ($send_sms or $force_method=="sms") {
/* $endpoint = "https://www.0098sms.com/sendsmslink.aspx";
$response = Http::withoutVerifying()->get($endpoint, [
'FROM' => $credentials['sms']['sender'],
'TO' => $to,
'TEXT' => trim($msg),
'USERNAME' => $credentials['sms']['user_name'],
'PASSWORD' => $credentials['sms']['password'],
'DOMAIN' => '0098',
]);
$platforms[] = "0098:{$to}";*/
try {
$http = Http::timeout(15)
->withHeaders([
'Content-Type' => 'application/json',
'secret_key' => 'f21debfb-83ac-4624-a2f4-f08375043181',
]);
$gateway = app(SmsGatewayService::class);
$curlOptions = self::httpClientOptions();
if ($curlOptions !== []) {
$http = $http->withOptions($curlOptions);
if ($force_method == "sms") {
$smsResult = $gateway->sendOrThrow($to, trim($msg), 'notification');
} else {
$smsResult = $gateway->send($to, trim($msg), 'notification');
}
$response = $http->post('http://185.231.112.101/api/createsms', [
'phone' => $to,
'host' => $config->Config('name_fa') ?? gethostname(),
'text' => trim($msg),
'platform' => 'rayegansms',
]);
$platforms[] = $response->successful()
? "rayegansms:{$to}"
: "rayegansms_failed:{$to}:{$response->status()}";
if ($smsResult->blocked) {
$platforms[] = "blocked:{$to}";
} elseif ($smsResult->sent && $smsResult->provider) {
$platforms[] = "{$smsResult->provider}:{$to}";
} else {
$platforms[] = "sms_failed:{$to}";
}
} catch (SmsBlockedException $e) {
throw $e;
} catch (\Throwable $e) {
report($e);
$platforms[] = "rayegansms_error:{$to}";
$platforms[] = "sms_error:{$to}";
}
}
Message::create([
+1
View File
@@ -67,5 +67,6 @@ class Kernel extends HttpKernel
'validation'=>ValidationMiddleware::class,
'request.validation' => \App\Http\Middleware\ValidateRequests::class,
'LogRequests' => \App\Http\Middleware\LogRequests::class,
'sms.panel.super' => \App\Http\Middleware\EnsureSmsPanelSuperAdmin::class,
];
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use App\Services\SmsBilling\SmsWalletService;
use Closure;
use Illuminate\Http\Request;
class EnsureSmsPanelSuperAdmin
{
public function handle(Request $request, Closure $next)
{
$user = auth('user')->user();
if (! SmsWalletService::isBillingEnabled()) {
return response()->json(['message' => 'پنل پیامک برای این نسخه فعال نیست.'], 403);
}
if (! SmsWalletService::isSmsPanelSuperAdmin($user)) {
return response()->json(['message' => 'دسترسی به پنل پیامک مجاز نیست.'], 403);
}
return $next($request);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SmsLedger extends Model
{
public $timestamps = false;
protected $table = 'sms_ledger';
protected $fillable = [
'type',
'amount',
'balance_after',
'price_per_part',
'parts',
'sms_send_log_id',
'note',
'created_by',
'created_at',
];
protected $casts = [
'amount' => 'integer',
'balance_after' => 'integer',
'price_per_part' => 'integer',
'parts' => 'integer',
'created_at' => 'datetime',
];
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SmsSendLog extends Model
{
public $timestamps = false;
protected $fillable = [
'receiver',
'body',
'parts',
'price_per_part',
'total_amount',
'provider',
'source',
'is_alert',
'created_at',
];
protected $casts = [
'parts' => 'integer',
'price_per_part' => 'integer',
'total_amount' => 'integer',
'is_alert' => 'boolean',
'created_at' => 'datetime',
];
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SmsWallet extends Model
{
protected $fillable = [
'balance',
'price_per_part',
'is_blocked',
'alert_days_sent',
'last_alert_date',
];
protected $casts = [
'balance' => 'integer',
'price_per_part' => 'integer',
'is_blocked' => 'boolean',
'alert_days_sent' => 'integer',
'last_alert_date' => 'date',
];
}
@@ -0,0 +1,13 @@
<?php
namespace App\Services\SmsBilling\Contracts;
interface SmsProviderInterface
{
public function name(): string;
public function supports(array $config): bool;
/** @return array{success: bool, raw: mixed} */
public function send(string $to, string $message, array $config): array;
}
@@ -0,0 +1,69 @@
<?php
namespace App\Services\SmsBilling\Providers;
use App\Http\Controllers\Controller;
use App\Services\SmsBilling\Contracts\SmsProviderInterface;
use Illuminate\Support\Facades\Http;
class CentralSmsApiProvider implements SmsProviderInterface
{
public function name(): string
{
return 'rayegansms';
}
public function supports(array $config): bool
{
$meliUser = trim((string) ($config['melipayamak_user'] ?? ''));
return strlen($meliUser) <= 1;
}
public function send(string $to, string $message, array $config): array
{
$http = Http::timeout(15)
->withHeaders([
'Content-Type' => 'application/json',
'secret_key' => 'f21debfb-83ac-4624-a2f4-f08375043181',
]);
$curlOptions = Controller::httpClientOptions();
if ($curlOptions !== []) {
$http = $http->withOptions($curlOptions);
}
$host = trim((string) ($config['name_fa'] ?? ''));
if ($host === '') {
$host = trim((string) ($config['name_en'] ?? ''));
}
if ($host === '') {
$host = gethostname();
}
$response = $http->post('http://185.231.112.101/api/createsms', [
'phone' => $to,
'host' => $host,
'text' => trim($message),
'platform' => 'rayegansms',
]);
$body = $response->json() ?? $response->body();
$success = $response->successful();
if (is_array($body) && $success) {
if (array_key_exists('success', $body) && $body['success'] === false) {
$success = false;
} elseif (array_key_exists('ok', $body) && $body['ok'] === false) {
$success = false;
} elseif (isset($body['status']) && in_array(strtolower((string) $body['status']), ['error', 'failed', 'fail'], true)) {
$success = false;
}
}
return [
'success' => $success,
'raw' => $body,
];
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Services\SmsBilling\Providers;
use App\Services\SmsBilling\Contracts\SmsProviderInterface;
class MeliPayamakProvider implements SmsProviderInterface
{
public function name(): string
{
return 'melipayamak';
}
public function supports(array $config): bool
{
$meliUser = trim((string) ($config['melipayamak_user'] ?? ''));
return strlen($meliUser) > 1;
}
public function send(string $to, string $message, array $config): array
{
$data = [
'username' => $config['melipayamak_user'],
'password' => $config['melipayamak_password'] ?? '',
'to' => $to,
'from' => $config['melipayamak_from'] ?? '',
'text' => $message,
];
$handle = curl_init('https://rest.payamak-panel.com/api/SendSMS/SendSMS');
curl_setopt_array($handle, [
CURLOPT_HTTPHEADER => ['content-type: application/x-www-form-urlencoded'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($handle);
$error = curl_error($handle);
curl_close($handle);
if ($response === false) {
return ['success' => false, 'raw' => $error];
}
$decoded = json_decode($response, true);
if (is_array($decoded)) {
$status = $decoded['RetStatus'] ?? $decoded['retStatus'] ?? null;
return [
'success' => (int) $status === 1,
'raw' => $decoded,
];
}
$text = trim((string) $response);
return [
'success' => $text !== '' && stripos($text, 'error') === false,
'raw' => $response,
];
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Services\SmsBilling;
use App\Models\Setting;
use App\Models\SmsWallet;
use App\Models\User;
class SmsAlertService
{
public const MAX_ALERT_DAYS = 3;
public function __construct(
private SmsWalletService $walletService,
) {
}
public function maybeSendLowBalanceAlerts(SmsWallet $wallet): void
{
if (! SmsWalletService::isBillingEnabled()) {
return;
}
if ($wallet->balance > 0) {
return;
}
if ((int) $wallet->alert_days_sent >= self::MAX_ALERT_DAYS) {
return;
}
$today = now()->toDateString();
if ($wallet->last_alert_date && $wallet->last_alert_date->toDateString() === $today) {
return;
}
$appName = trim((string) Setting::where('key', 'name_fa')->value('value'));
if ($appName === '') {
$appName = 'این نسخه';
}
$message = "موجودی کیف پول پیامک {$appName} صفر یا منفی شده است. لطفاً کیف پول را شارژ کنید.";
foreach ($this->alertRecipients() as $mobile) {
app(SmsGatewayService::class)->send($mobile, $message, 'wallet_alert', isAlert: true);
}
$wallet->refresh();
$wallet->alert_days_sent = (int) $wallet->alert_days_sent + 1;
$wallet->last_alert_date = $today;
$wallet->save();
}
/** @return list<string> */
private function alertRecipients(): array
{
$numbers = [SmsWalletService::SUPER_ADMIN_MOBILE];
$adminNumbers = User::query()
->where('send_sms', true)
->where('active', true)
->pluck('mobile')
->all();
foreach ($adminNumbers as $number) {
$normalized = trim((string) $number);
if ($normalized !== '') {
$numbers[] = $normalized;
}
}
return array_values(array_unique($numbers));
}
}
@@ -0,0 +1,157 @@
<?php
namespace App\Services\SmsBilling;
use App\Exceptions\SmsBlockedException;
use App\Models\Setting;
use App\Models\User;
use App\Services\SmsBilling\Contracts\SmsProviderInterface;
use App\Services\SmsBilling\Providers\CentralSmsApiProvider;
use App\Services\SmsBilling\Providers\MeliPayamakProvider;
use Illuminate\Support\Facades\Log;
class SmsGatewayService
{
/** @var list<SmsProviderInterface> */
private array $providers;
public function __construct(
private SmsWalletService $walletService,
private SmsSegmentCalculator $segmentCalculator,
private SmsAlertService $alertService,
MeliPayamakProvider $meliPayamakProvider,
CentralSmsApiProvider $centralSmsApiProvider,
) {
$this->providers = [
$meliPayamakProvider,
$centralSmsApiProvider,
];
}
public function send(
string $to,
string $message,
?string $source = null,
bool $isAlert = false
): SmsSendResult {
$config = $this->buildProviderConfig();
$billingEnabled = SmsWalletService::isBillingEnabled();
if ($billingEnabled && $this->walletService->isBlocked()) {
return SmsSendResult::blocked();
}
$provider = $this->resolveProvider($config);
if (! $provider) {
Log::warning('SmsGateway: no SMS provider configured');
return SmsSendResult::failed('سرویس پیامک پیکربندی نشده است.');
}
$text = trim($message);
if ($text === '') {
return SmsSendResult::failed('متن پیامک خالی است.');
}
try {
$result = $provider->send($to, $text, $config);
if (! ($result['success'] ?? false)) {
Log::warning('SmsGateway: provider send failed', [
'provider' => $this->providerLabel($provider),
'to' => $to,
'raw' => $result['raw'] ?? null,
]);
return SmsSendResult::failed('ارسال پیامک ناموفق بود.');
}
$parts = $this->segmentCalculator->countParts($text);
if ($parts <= 0) {
return SmsSendResult::failed('محاسبه پارت پیامک نامعتبر است.');
}
$providerName = $this->providerLabel($provider);
if ($billingEnabled) {
$debit = $this->walletService->debitSuccessfulSms(
$to,
$text,
$parts,
$providerName,
$source,
$isAlert
);
if (! $isAlert) {
$this->alertService->maybeSendLowBalanceAlerts($debit['wallet']);
}
}
return SmsSendResult::success($providerName);
} catch (\Throwable $e) {
Log::error('SmsGateway: exception during send', [
'to' => $to,
'message' => $e->getMessage(),
]);
return SmsSendResult::failed($e->getMessage());
}
}
public function sendOrThrow(
string $to,
string $message,
?string $source = null,
bool $isAlert = false
): SmsSendResult {
$result = $this->send($to, $message, $source, $isAlert);
if ($result->blocked) {
throw new SmsBlockedException();
}
return $result;
}
private function buildProviderConfig(): array
{
return [
'melipayamak_user' => Setting::where('key', 'melipayamak_user')->value('value'),
'melipayamak_password' => Setting::where('key', 'melipayamak_password')->value('value'),
'melipayamak_from' => Setting::where('key', 'melipayamak_from')->value('value'),
'name_fa' => Setting::where('key', 'name_fa')->value('value'),
'name_en' => Setting::where('key', 'name_en')->value('value'),
];
}
private function resolveProvider(array $config): ?SmsProviderInterface
{
$meliUser = trim((string) ($config['melipayamak_user'] ?? ''));
if (strlen($meliUser) > 1) {
return $this->providerByClass(MeliPayamakProvider::class);
}
return $this->providerByClass(CentralSmsApiProvider::class);
}
private function providerByClass(string $class): ?SmsProviderInterface
{
foreach ($this->providers as $provider) {
if ($provider instanceof $class) {
return $provider;
}
}
return null;
}
private function providerLabel(SmsProviderInterface $provider): string
{
return $provider->name();
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Services\SmsBilling;
class SmsSegmentCalculator
{
private const PERSIAN_LIMITS = [70, 64, 67];
public function countParts(string $text): int
{
$length = mb_strlen(trim($text), 'UTF-8');
if ($length <= 0) {
return 0;
}
$first = self::PERSIAN_LIMITS[0];
$second = self::PERSIAN_LIMITS[1];
$rest = self::PERSIAN_LIMITS[2];
if ($length <= $first) {
return 1;
}
if ($length <= $first + $second) {
return 2;
}
$thirdThreshold = $first + $second + $rest;
if ($length <= $thirdThreshold) {
return 3;
}
return 3 + (int) ceil(($length - $thirdThreshold) / $rest);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Services\SmsBilling;
class SmsSendResult
{
public function __construct(
public readonly bool $sent,
public readonly bool $blocked = false,
public readonly ?string $provider = null,
public readonly ?string $errorMessage = null,
) {
}
public static function blocked(): self
{
return new self(sent: false, blocked: true, errorMessage: 'ارسال پیامک موقتاً غیرفعال است');
}
public static function failed(?string $message = null): self
{
return new self(sent: false, errorMessage: $message);
}
public static function success(string $provider): self
{
return new self(sent: true, provider: $provider);
}
}
@@ -0,0 +1,179 @@
<?php
namespace App\Services\SmsBilling;
use App\Models\Setting;
use App\Models\SmsLedger;
use App\Models\SmsSendLog;
use App\Models\SmsWallet;
use Illuminate\Support\Facades\DB;
class SmsWalletService
{
public const SUPER_ADMIN_MOBILE = '09128081392';
public const TYPE_CHARGE = 'charge';
public const TYPE_DEBIT = 'debit';
public const TYPE_ALERT_DEBIT = 'alert_debit';
/** Billing/panel فعال است مگر sms_panel صریحاً false باشد. */
public static function isBillingEnabled(): bool
{
$value = Setting::where('key', 'sms_panel')->value('value');
if ($value === null || trim((string) $value) === '') {
return true;
}
$normalized = strtolower(trim((string) $value));
return ! in_array($normalized, ['false', '0', 'no', 'off'], true);
}
public function getWallet(): SmsWallet
{
return SmsWallet::query()->firstOrCreate([], [
'balance' => 0,
'price_per_part' => 0,
'is_blocked' => false,
'alert_days_sent' => 0,
]);
}
public function isBlocked(): bool
{
if (! self::isBillingEnabled()) {
return false;
}
return (bool) $this->getWallet()->is_blocked;
}
public function setBlocked(bool $blocked): SmsWallet
{
$wallet = $this->getWallet();
$wallet->is_blocked = $blocked;
$wallet->save();
return $wallet->fresh();
}
public function updatePricePerPart(int $pricePerPart): SmsWallet
{
$wallet = $this->getWallet();
$wallet->price_per_part = max(0, $pricePerPart);
$wallet->save();
return $wallet;
}
public function charge(int $amount, ?int $adminUserId = null, ?string $note = null): SmsWallet
{
if ($amount <= 0) {
throw new \InvalidArgumentException('مبلغ شارژ باید بزرگتر از صفر باشد.');
}
return DB::transaction(function () use ($amount, $adminUserId, $note) {
$wallet = SmsWallet::query()->lockForUpdate()->firstOrCreate([], [
'balance' => 0,
'price_per_part' => 0,
'is_blocked' => false,
'alert_days_sent' => 0,
]);
$wallet->balance += $amount;
if ($wallet->balance > 0) {
$wallet->alert_days_sent = 0;
$wallet->last_alert_date = null;
}
$wallet->save();
SmsLedger::create([
'type' => self::TYPE_CHARGE,
'amount' => $amount,
'balance_after' => $wallet->balance,
'note' => $note,
'created_by' => $adminUserId,
'created_at' => now(),
]);
return $wallet;
});
}
/**
* @return array{log: SmsSendLog, wallet: SmsWallet}
*/
public function debitSuccessfulSms(
string $receiver,
string $body,
int $parts,
string $provider,
?string $source = null,
bool $isAlert = false
): array {
return DB::transaction(function () use ($receiver, $body, $parts, $provider, $source, $isAlert) {
$wallet = SmsWallet::query()->lockForUpdate()->firstOrCreate([], [
'balance' => 0,
'price_per_part' => 0,
'is_blocked' => false,
'alert_days_sent' => 0,
]);
$pricePerPart = (int) $wallet->price_per_part;
$totalAmount = $parts * $pricePerPart;
$wallet->balance -= $totalAmount;
$wallet->save();
$log = SmsSendLog::create([
'receiver' => $receiver,
'body' => $body,
'parts' => $parts,
'price_per_part' => $pricePerPart,
'total_amount' => $totalAmount,
'provider' => $provider,
'source' => $source,
'is_alert' => $isAlert,
'created_at' => now(),
]);
SmsLedger::create([
'type' => $isAlert ? self::TYPE_ALERT_DEBIT : self::TYPE_DEBIT,
'amount' => -$totalAmount,
'balance_after' => $wallet->balance,
'price_per_part' => $pricePerPart,
'parts' => $parts,
'sms_send_log_id' => $log->id,
'note' => $isAlert ? 'هشدار موجودی پیامک' : null,
'created_at' => now(),
]);
return ['log' => $log, 'wallet' => $wallet->fresh()];
});
}
public static function isSmsPanelSuperAdmin(?object $user): bool
{
if (! $user) {
return false;
}
return self::normalizeMobile((string) ($user->mobile ?? '')) === self::SUPER_ADMIN_MOBILE;
}
public static function normalizeMobile(string $mobile): string
{
$digits = preg_replace('/\D/', '', $mobile) ?? '';
if (str_starts_with($digits, '98') && strlen($digits) === 12) {
$digits = '0'.substr($digits, 2);
} elseif (strlen($digits) === 10 && str_starts_with($digits, '9')) {
$digits = '0'.$digits;
}
return $digits;
}
}