Empty SuperAdmin chargeInfo was wiping sms_charge_* on header refresh. Seed those keys only when empty, and move referral_bonus_amount to can_edit=1. Co-authored-by: Cursor <cursoragent@cursor.com>
390 lines
13 KiB
PHP
390 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Services\SmsBilling;
|
|
|
|
use App\Models\Setting;
|
|
use App\Models\SmsLedger;
|
|
use App\Models\SmsSendLog;
|
|
use App\Models\SmsWallet;
|
|
use App\Services\ConfigService;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
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';
|
|
|
|
/** کلیدهای تنظیمات اطلاعات واریز برای شارژ سامانه پیامکی (یکسان برای همه). */
|
|
public const DEPOSIT_SETTING_KEYS = [
|
|
'card_number' => 'sms_charge_card_number',
|
|
'sheba' => 'sms_charge_sheba',
|
|
'account_holder' => 'sms_charge_account_holder',
|
|
'note' => 'sms_charge_note',
|
|
];
|
|
|
|
/** مقادیر پیشفرض اطلاعات واریز. */
|
|
public const DEPOSIT_DEFAULTS = [
|
|
'card_number' => '6219861961033817',
|
|
'sheba' => 'IR080560611828005603604201',
|
|
'account_holder' => 'حسین منتظر',
|
|
'note' => 'لطفا مبلغ مورد نظر خود جهت شارژ سامانه پیامکی را به شماره کارت یا حساب زیر واریز نمایید و تصویر فیش پرداختی را برای پشتیبانی بفرستید.',
|
|
];
|
|
|
|
/** Billing/panel فعال است مگر sms_panel صریحاً false باشد. */
|
|
public static function isBillingEnabled(): bool
|
|
{
|
|
$value = app(ConfigService::class)->getString('sms_panel');
|
|
|
|
if ($value === null || trim((string) $value) === '') {
|
|
return true;
|
|
}
|
|
|
|
$normalized = strtolower(trim((string) $value));
|
|
|
|
return ! in_array($normalized, ['false', '0', 'no', 'off'], true);
|
|
}
|
|
|
|
/** billing فعال است و جداول migration هم وجود دارند. */
|
|
public static function isBillingOperational(): bool
|
|
{
|
|
if (! self::isBillingEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return Schema::hasTable('sms_wallets')
|
|
&& Schema::hasTable('sms_send_logs')
|
|
&& Schema::hasTable('sms_ledger');
|
|
} catch (\Throwable) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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::isBillingOperational()) {
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Keep local MySQL wallet mirrored with SuperAdmin during the parallel period.
|
|
*/
|
|
public function mirrorFromSuperAdmin(?int $balance = null, ?int $pricePerPart = null, ?bool $isBlocked = null): SmsWallet
|
|
{
|
|
return DB::transaction(function () use ($balance, $pricePerPart, $isBlocked) {
|
|
$wallet = SmsWallet::query()->lockForUpdate()->firstOrCreate([], [
|
|
'balance' => 0,
|
|
'price_per_part' => 0,
|
|
'is_blocked' => false,
|
|
'alert_days_sent' => 0,
|
|
]);
|
|
|
|
if ($balance !== null) {
|
|
$wallet->balance = $balance;
|
|
}
|
|
if ($pricePerPart !== null) {
|
|
$wallet->price_per_part = max(0, $pricePerPart);
|
|
}
|
|
if ($isBlocked !== null) {
|
|
$wallet->is_blocked = $isBlocked;
|
|
}
|
|
if ($wallet->balance > 0) {
|
|
$wallet->alert_days_sent = 0;
|
|
$wallet->last_alert_date = null;
|
|
}
|
|
$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;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* After SuperAdmin consume: set local balance to SA value and write local send log/ledger
|
|
* so the product SMS panel stays in sync during the parallel period.
|
|
*/
|
|
public function mirrorConsumeFromSuperAdmin(
|
|
string $receiver,
|
|
string $body,
|
|
int $parts,
|
|
string $provider,
|
|
?string $source,
|
|
bool $isAlert,
|
|
int $balanceAfter,
|
|
int $costRials,
|
|
int $pricePerPart
|
|
): void {
|
|
DB::transaction(function () use (
|
|
$receiver,
|
|
$body,
|
|
$parts,
|
|
$provider,
|
|
$source,
|
|
$isAlert,
|
|
$balanceAfter,
|
|
$costRials,
|
|
$pricePerPart
|
|
) {
|
|
$wallet = SmsWallet::query()->lockForUpdate()->firstOrCreate([], [
|
|
'balance' => 0,
|
|
'price_per_part' => 0,
|
|
'is_blocked' => false,
|
|
'alert_days_sent' => 0,
|
|
]);
|
|
|
|
$wallet->balance = $balanceAfter;
|
|
if ($pricePerPart > 0) {
|
|
$wallet->price_per_part = $pricePerPart;
|
|
}
|
|
if ($wallet->balance > 0) {
|
|
$wallet->alert_days_sent = 0;
|
|
$wallet->last_alert_date = null;
|
|
}
|
|
$wallet->save();
|
|
|
|
$log = SmsSendLog::create([
|
|
'receiver' => $receiver,
|
|
'body' => $body,
|
|
'parts' => $parts,
|
|
'price_per_part' => $pricePerPart,
|
|
'total_amount' => $costRials,
|
|
'provider' => $provider,
|
|
'source' => $source,
|
|
'is_alert' => $isAlert,
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
SmsLedger::create([
|
|
'type' => $isAlert ? self::TYPE_ALERT_DEBIT : self::TYPE_DEBIT,
|
|
'amount' => -$costRials,
|
|
'balance_after' => $balanceAfter,
|
|
'price_per_part' => $pricePerPart,
|
|
'parts' => $parts,
|
|
'sms_send_log_id' => $log->id,
|
|
'note' => $isAlert ? 'هشدار موجودی پیامک' : 'مصرف ارسال (superadmin)',
|
|
'created_at' => now(),
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @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()];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* اطلاعات واریز جهت شارژ سامانه پیامکی؛ اگر تنظیمی ذخیره نشده باشد، مقدار پیشفرض برمیگردد.
|
|
*
|
|
* @return array{card_number: string, sheba: string, account_holder: string, note: string}
|
|
*/
|
|
public static function getDepositInfo(): array
|
|
{
|
|
$values = [];
|
|
|
|
try {
|
|
$values = Setting::whereIn('key', array_values(self::DEPOSIT_SETTING_KEYS))
|
|
->pluck('value', 'key')
|
|
->toArray();
|
|
} catch (\Throwable) {
|
|
$values = [];
|
|
}
|
|
|
|
$info = [];
|
|
foreach (self::DEPOSIT_SETTING_KEYS as $field => $key) {
|
|
$stored = $values[$key] ?? null;
|
|
$info[$field] = ($stored === null || trim((string) $stored) === '')
|
|
? self::DEPOSIT_DEFAULTS[$field]
|
|
: (string) $stored;
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* ذخیره اطلاعات واریز در تنظیمات. فقط فیلدهای ارسالشده بهروزرسانی میشوند.
|
|
*
|
|
* @param array<string, string|null> $data
|
|
* @return array{card_number: string, sheba: string, account_holder: string, note: string}
|
|
*/
|
|
public static function setDepositInfo(array $data): array
|
|
{
|
|
foreach (self::DEPOSIT_SETTING_KEYS as $field => $key) {
|
|
if (! array_key_exists($field, $data)) {
|
|
continue;
|
|
}
|
|
|
|
$setting = Setting::firstOrNew(['key' => $key]);
|
|
$incoming = trim((string) ($data[$field] ?? ''));
|
|
if ($incoming === '') {
|
|
if ($setting->exists && trim((string) $setting->value) !== '') {
|
|
continue;
|
|
}
|
|
$incoming = self::DEPOSIT_DEFAULTS[$field];
|
|
}
|
|
$setting->value = $incoming;
|
|
|
|
if ($setting->title_fa === null || $setting->title_fa === '') {
|
|
$setting->title_fa = self::depositFieldTitle($field);
|
|
}
|
|
if (! $setting->exists) {
|
|
$setting->is_active = 1;
|
|
$setting->can_edit = 0;
|
|
}
|
|
|
|
$setting->save();
|
|
}
|
|
|
|
return self::getDepositInfo();
|
|
}
|
|
|
|
private static function depositFieldTitle(string $field): string
|
|
{
|
|
return match ($field) {
|
|
'card_number' => 'شماره کارت شارژ پیامک',
|
|
'sheba' => 'شماره شبا شارژ پیامک',
|
|
'account_holder' => 'نام صاحب حساب شارژ پیامک',
|
|
'note' => 'متن راهنمای شارژ پیامک',
|
|
default => $field,
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|