368 lines
14 KiB
PHP
368 lines
14 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\Customer;
|
||
|
|
use App\Models\ShayeganInboundCharge;
|
||
|
|
use App\Models\Transaction;
|
||
|
|
use App\Models\TransactionsAdmin;
|
||
|
|
use App\Models\User;
|
||
|
|
use App\Models\Withdraw;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Illuminate\Support\Facades\Log;
|
||
|
|
use Rooyesh\Wallet\interfaces\TransactionInterface;
|
||
|
|
use Rooyesh\Wallet\interfaces\WalletInterface;
|
||
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||
|
|
|
||
|
|
class ShayeganInboundChargeService
|
||
|
|
{
|
||
|
|
private ConfigService $config;
|
||
|
|
private ShayeganClient $client;
|
||
|
|
private WalletBroadcastService $walletBroadcast;
|
||
|
|
|
||
|
|
public function __construct(
|
||
|
|
ConfigService $config,
|
||
|
|
ShayeganClient $client,
|
||
|
|
WalletBroadcastService $walletBroadcast
|
||
|
|
) {
|
||
|
|
$this->config = $config;
|
||
|
|
$this->client = $client;
|
||
|
|
$this->walletBroadcast = $walletBroadcast;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* پیدا کردن مشتری با اولویت: شناسه مستقیم، کد تفصیلی، موبایل، کد ملی
|
||
|
|
*/
|
||
|
|
public function resolveCustomer(array $params): ?Customer
|
||
|
|
{
|
||
|
|
// 1. با شناسه مشتری در بورس مس
|
||
|
|
if (! empty($params['customer_id'])) {
|
||
|
|
$customer = Customer::find((int) $params['customer_id']);
|
||
|
|
if ($customer) {
|
||
|
|
return $customer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. با کد تفصیلی شایگان (moshtari_code)
|
||
|
|
$tafsil = trim((string) ($params['customer_tafsil'] ?? $params['moshtari_code'] ?? $params['job_number'] ?? ''));
|
||
|
|
if ($tafsil !== '') {
|
||
|
|
$customer = Customer::where('moshtari_code', $tafsil)->first();
|
||
|
|
if ($customer) {
|
||
|
|
return $customer;
|
||
|
|
}
|
||
|
|
|
||
|
|
// بررسی فرمول احتمالی کد کل + کد کاربر ۷ رقمی
|
||
|
|
$kol = trim($this->config->getString('shayegan_kol_customer', '121'));
|
||
|
|
if ($kol !== '' && str_starts_with($tafsil, $kol)) {
|
||
|
|
$subId = (int) substr($tafsil, strlen($kol));
|
||
|
|
if ($subId > 0) {
|
||
|
|
$customer = Customer::find($subId);
|
||
|
|
if ($customer) {
|
||
|
|
return $customer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. با شماره موبایل
|
||
|
|
$mobile = trim((string) ($params['mobile'] ?? ''));
|
||
|
|
if ($mobile !== '') {
|
||
|
|
$customer = Customer::findActiveByMobile($mobile);
|
||
|
|
if ($customer) {
|
||
|
|
return $customer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. با کد ملی
|
||
|
|
$nCode = trim((string) ($params['n_code'] ?? $params['national_code'] ?? $params['mellicode'] ?? ''));
|
||
|
|
if ($nCode !== '') {
|
||
|
|
$customer = Customer::where('n_code', $nCode)->first();
|
||
|
|
if ($customer) {
|
||
|
|
return $customer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* ثبت درخواست شارژ ورودی از شایگان در وضعیت pending
|
||
|
|
*/
|
||
|
|
public function registerCharge(array $data, string $source = 'webhook'): array
|
||
|
|
{
|
||
|
|
$customer = $this->resolveCustomer($data);
|
||
|
|
if (! $customer) {
|
||
|
|
return [
|
||
|
|
'ok' => false,
|
||
|
|
'message' => 'مشتری با مشخصات ارسالی (کد مشتری، کد تفصیلی، موبایل یا کد ملی) یافت نشد.',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$amount = (float) ($data['amount'] ?? $data['value'] ?? $data['RialBes'] ?? $data['AmountBes'] ?? 0);
|
||
|
|
if ($amount <= 0) {
|
||
|
|
return [
|
||
|
|
'ok' => false,
|
||
|
|
'message' => 'مبلغ شارژ باید مقداری مثبت و بزرگتر از صفر باشد.',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$shayeganNumber = trim((string) ($data['shayegan_number'] ?? $data['Number'] ?? $data['doc_number'] ?? ''));
|
||
|
|
$tafsil = trim((string) ($data['customer_tafsil'] ?? $data['moshtari_code'] ?? $customer->moshtari_code ?? ''));
|
||
|
|
$description = trim((string) ($data['description'] ?? $data['desc'] ?? $data['note'] ?? 'شارژ از طریق پنل حسابداری شایگان'));
|
||
|
|
|
||
|
|
// جلوگیری از ثبت تکراری اگر قبلاً سند با همین شماره برای این مشتری ثبت شده است
|
||
|
|
if ($shayeganNumber !== '') {
|
||
|
|
$existing = ShayeganInboundCharge::where('shayegan_number', $shayeganNumber)
|
||
|
|
->where('customer_id', $customer->id)
|
||
|
|
->first();
|
||
|
|
if ($existing) {
|
||
|
|
return [
|
||
|
|
'ok' => true,
|
||
|
|
'message' => 'این سند شایگان قبلاً ثبت شده است.',
|
||
|
|
'charge' => $existing->load('customer'),
|
||
|
|
'duplicate' => true,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$charge = ShayeganInboundCharge::create([
|
||
|
|
'customer_id' => $customer->id,
|
||
|
|
'customer_tafsil' => $tafsil ?: null,
|
||
|
|
'shayegan_number' => $shayeganNumber ?: null,
|
||
|
|
'amount' => $amount,
|
||
|
|
'description' => $description,
|
||
|
|
'source' => $source,
|
||
|
|
'status' => 'pending',
|
||
|
|
'raw_data' => $data,
|
||
|
|
]);
|
||
|
|
|
||
|
|
Log::info('shayegan.inbound_charge_registered', [
|
||
|
|
'charge_id' => $charge->id,
|
||
|
|
'customer_id' => $customer->id,
|
||
|
|
'amount' => $amount,
|
||
|
|
'shayegan_number' => $shayeganNumber,
|
||
|
|
'source' => $source,
|
||
|
|
]);
|
||
|
|
|
||
|
|
return [
|
||
|
|
'ok' => true,
|
||
|
|
'message' => "حساب شخص {$customer->name} از طریق پنل حسابداری مقدار " . number_format($amount) . " ریال شارژ شد.",
|
||
|
|
'charge' => $charge->load('customer'),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* تایید شارژ و افزایش کیف پول ریالی مشتری در بورس مس
|
||
|
|
*/
|
||
|
|
public function approveCharge(ShayeganInboundCharge $charge, ?User $adminUser = null): array
|
||
|
|
{
|
||
|
|
return DB::transaction(function () use ($charge, $adminUser) {
|
||
|
|
$charge = ShayeganInboundCharge::where('id', $charge->id)->lockForUpdate()->firstOrFail();
|
||
|
|
|
||
|
|
if ($charge->status === 'approved') {
|
||
|
|
return [
|
||
|
|
'ok' => true,
|
||
|
|
'message' => 'این شارژ قبلاً تایید شده است.',
|
||
|
|
'charge' => $charge->load(['customer', 'approver']),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($charge->status !== 'pending') {
|
||
|
|
throw new UnprocessableEntityHttpException('وضعیت این شارژ برای تایید معتبر نیست (' . $charge->status . ').');
|
||
|
|
}
|
||
|
|
|
||
|
|
$customer = $charge->customer;
|
||
|
|
if (! $customer) {
|
||
|
|
throw new UnprocessableEntityHttpException('مشتری مرتبط با این شارژ یافت نشد.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$adminId = $adminUser ? (int) $adminUser->id : 1;
|
||
|
|
$note = $charge->description ?: ('شارژ تایید شده از پنل حسابداری شایگان' . ($charge->shayegan_number ? " (سند {$charge->shayegan_number})" : ''));
|
||
|
|
|
||
|
|
// 1. ثبت Withdraw (واریز ریالی با sts = 4)
|
||
|
|
$withdraw = Withdraw::create([
|
||
|
|
'user_id' => $adminId,
|
||
|
|
'customer_id' => $customer->id,
|
||
|
|
'cash' => $charge->amount,
|
||
|
|
'note' => $note,
|
||
|
|
'sts' => 4,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 2. افزایش موجودی در کیف پول (Rooyesh Wallet)
|
||
|
|
$walletInterface = new WalletInterface((int) $customer->id, (int) $customer->type);
|
||
|
|
new TransactionInterface($walletInterface, [
|
||
|
|
'gold' => 0,
|
||
|
|
'cash' => $charge->amount,
|
||
|
|
'expire_at' => now(),
|
||
|
|
'note' => $note,
|
||
|
|
'withdraw_id' => $withdraw->id,
|
||
|
|
'active' => true,
|
||
|
|
], 3, true, true, 0); // type 3 = deposit, deposit = true
|
||
|
|
|
||
|
|
// 3. ثبت لاگ تراکنش ادمین
|
||
|
|
$lastTx = Transaction::where('withdraw_id', $withdraw->id)->latest('id')->first();
|
||
|
|
TransactionsAdmin::create([
|
||
|
|
'customer_id' => $customer->id,
|
||
|
|
'customer_to_id' => $customer->id,
|
||
|
|
'user_id' => $adminId,
|
||
|
|
'wallet' => null,
|
||
|
|
'transaction_id' => $lastTx?->id,
|
||
|
|
'cash' => $charge->amount,
|
||
|
|
'note' => $note,
|
||
|
|
'type' => 1,
|
||
|
|
'gold' => 0,
|
||
|
|
'balance_cash' => 0,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 4. بهروزرسانی وضعیت رکورد شارژ شایگان
|
||
|
|
$charge->status = 'approved';
|
||
|
|
$charge->approved_by = $adminId;
|
||
|
|
$charge->approved_at = now();
|
||
|
|
$charge->withdraw_id = $withdraw->id;
|
||
|
|
$charge->transaction_id = $lastTx?->id;
|
||
|
|
$charge->save();
|
||
|
|
|
||
|
|
// 5. برودکست ریلتایم موجودی کیف پول کاربر
|
||
|
|
try {
|
||
|
|
$this->walletBroadcast->broadcastForCustomer((int) $customer->id, (int) $customer->type);
|
||
|
|
} catch (\Throwable $e) {
|
||
|
|
Log::warning('shayegan.wallet_broadcast_failed', ['error' => $e->getMessage()]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. تریگر اتوماسیون
|
||
|
|
try {
|
||
|
|
\App\Services\Automation\AutomationEvents::fire('wallet.rial_charge', (int) $customer->id);
|
||
|
|
} catch (\Throwable $e) {
|
||
|
|
Log::warning('shayegan.automation_failed', ['error' => $e->getMessage()]);
|
||
|
|
}
|
||
|
|
|
||
|
|
Log::info('shayegan.inbound_charge_approved', [
|
||
|
|
'charge_id' => $charge->id,
|
||
|
|
'customer_id' => $customer->id,
|
||
|
|
'amount' => $charge->amount,
|
||
|
|
'withdraw_id' => $withdraw->id,
|
||
|
|
'admin_id' => $adminId,
|
||
|
|
]);
|
||
|
|
|
||
|
|
return [
|
||
|
|
'ok' => true,
|
||
|
|
'message' => "کیف پول شخص {$customer->name} به مبلغ " . number_format($charge->amount) . " ریال با موفقیت شارژ شد.",
|
||
|
|
'charge' => $charge->load(['customer', 'approver']),
|
||
|
|
];
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* رد درخواست شارژ
|
||
|
|
*/
|
||
|
|
public function rejectCharge(ShayeganInboundCharge $charge, ?User $adminUser = null, ?string $reason = null): array
|
||
|
|
{
|
||
|
|
if ($charge->status === 'approved') {
|
||
|
|
throw new UnprocessableEntityHttpException('شارژ تایید شده را نمیتوان رد کرد.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$adminId = $adminUser ? (int) $adminUser->id : 1;
|
||
|
|
$charge->status = 'rejected';
|
||
|
|
$charge->rejected_by = $adminId;
|
||
|
|
$charge->rejected_at = now();
|
||
|
|
$charge->reject_reason = $reason;
|
||
|
|
$charge->save();
|
||
|
|
|
||
|
|
Log::info('shayegan.inbound_charge_rejected', [
|
||
|
|
'charge_id' => $charge->id,
|
||
|
|
'customer_id' => $charge->customer_id,
|
||
|
|
'admin_id' => $adminId,
|
||
|
|
'reason' => $reason,
|
||
|
|
]);
|
||
|
|
|
||
|
|
return [
|
||
|
|
'ok' => true,
|
||
|
|
'message' => 'درخواست شارژ رد شد.',
|
||
|
|
'charge' => $charge->load(['customer', 'rejecter']),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* استعلام اسناد جدید از وبسرویس شایگان (Sync / Polling)
|
||
|
|
*/
|
||
|
|
public function pollFromShayegan(): array
|
||
|
|
{
|
||
|
|
if (! $this->client->isConfigured()) {
|
||
|
|
return [
|
||
|
|
'ok' => false,
|
||
|
|
'message' => 'تنظیمات اتصال وبسرویس شایگان در سیستم پیکربندی نشده است.',
|
||
|
|
'new_count' => 0,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// استعلام از وبسرویس شایگان (مانند Document/Get با فیلتر بستانکاری تفصیلی مشتریان)
|
||
|
|
$res = $this->client->get('Document/Get', [], 0, 50);
|
||
|
|
if (empty($res['ok']) || empty($res['body']['Result']) || ! is_array($res['body']['Result'])) {
|
||
|
|
return [
|
||
|
|
'ok' => true,
|
||
|
|
'message' => 'سند جدیدی در استعلام شایگان یافت نشد.',
|
||
|
|
'new_count' => 0,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$count = 0;
|
||
|
|
foreach ($res['body']['Result'] as $doc) {
|
||
|
|
$docNumber = (string) ($doc['Number'] ?? $doc['DocNumber'] ?? '');
|
||
|
|
if ($docNumber === '') {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$rows = $doc['Body'] ?? [];
|
||
|
|
if (! is_array($rows)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($rows as $row) {
|
||
|
|
$amountBes = (float) ($row['AmountBes'] ?? $row['RialBes'] ?? 0);
|
||
|
|
$accountNumber = trim((string) ($row['AccountNumber'] ?? ''));
|
||
|
|
if ($amountBes <= 0 || $accountNumber === '') {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// جستجوی مشتری با شماره تفصیلی
|
||
|
|
$customer = Customer::where('moshtari_code', $accountNumber)->first();
|
||
|
|
if (! $customer) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// آیا قبلا این شماره سند ثبت شده؟
|
||
|
|
$already = ShayeganInboundCharge::where('shayegan_number', $docNumber)
|
||
|
|
->where('customer_id', $customer->id)
|
||
|
|
->exists();
|
||
|
|
|
||
|
|
if (! $already) {
|
||
|
|
$this->registerCharge([
|
||
|
|
'customer_id' => $customer->id,
|
||
|
|
'customer_tafsil' => $accountNumber,
|
||
|
|
'shayegan_number' => $docNumber,
|
||
|
|
'amount' => $amountBes,
|
||
|
|
'description' => (string) ($row['DocRowDesc'] ?? $doc['DocDescription'] ?? 'استعلام خودکار از شایگان'),
|
||
|
|
], 'sync');
|
||
|
|
$count++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'ok' => true,
|
||
|
|
'message' => $count > 0 ? "تعداد {$count} شارژ جدید از شایگان واکشی شد." : 'سند شارژ جدیدی یافت نشد.',
|
||
|
|
'new_count' => $count,
|
||
|
|
];
|
||
|
|
} catch (\Throwable $e) {
|
||
|
|
Log::warning('shayegan.poll_failed', ['error' => $e->getMessage()]);
|
||
|
|
return [
|
||
|
|
'ok' => false,
|
||
|
|
'message' => 'خطا در ارتباط با وبسرویس شایگان: ' . $e->getMessage(),
|
||
|
|
'new_count' => 0,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|