Files
boursecu_server/app/Services/GoldCreditService.php
T
mohmdsdgandCursor 050bd26318 Add gold credit feature gated by SuperAdmin enable_gold_credit.
Port tables, APIs, and overdue cron from goldali, default the flag off, and keep customer routes behind auth.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 00:17:01 +03:30

852 lines
33 KiB
PHP

<?php
namespace App\Services;
use App\Models\Customer;
use App\Models\GoldCredit;
use App\Models\GoldCreditInstallment;
use App\Models\GoldCreditRequest;
use App\Models\Price;
use App\Models\User;
use App\Models\Withdraw;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Rooyesh\Wallet\interfaces\TransactionInterface;
use Rooyesh\Wallet\interfaces\WalletInterface;
use Rooyesh\Wallet\models\Wallet;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
class GoldCreditService
{
public function buildInstallmentPlan(
float $goldAmount,
int $months,
string $interestType = 'none',
float $interestValue = 0
): array {
if ($months < 1) {
throw new UnprocessableEntityHttpException('تعداد ماه باید حداقل ۱ باشد');
}
if ($goldAmount <= 0) {
throw new UnprocessableEntityHttpException('مقدار طلا باید بیشتر از صفر باشد');
}
$totalSoot = (int) round($goldAmount * 1000);
$baseSootPerMonth = intdiv($totalSoot, $months);
$remainder = $totalSoot - ($baseSootPerMonth * $months);
$totalInterestGold = $this->calculateTotalInterest($goldAmount, $months, $interestType, $interestValue);
$interestPerMonth = $months > 0 ? $totalInterestGold / $months : 0;
$installments = [];
for ($i = 1; $i <= $months; $i++) {
$soot = $baseSootPerMonth + ($i === $months ? $remainder : 0);
$principalGold = $soot / 1000;
$interestGold = round($interestPerMonth, 6);
$installments[] = [
'installment_number' => $i,
'amount_soot' => $soot,
'principal_gold' => $principalGold,
'interest_gold' => $interestGold,
'total_gold' => round($principalGold + $interestGold, 6),
];
}
return [
'installments' => $installments,
'total_interest_gold' => round($totalInterestGold, 6),
'total_payable_gold' => round($goldAmount + $totalInterestGold, 6),
];
}
public function createCredit(array $data, ?int $adminId = null, string $source = 'admin_direct'): GoldCredit
{
$customer = Customer::findOrFail($data['customer_id']);
$active = GoldCredit::where('customer_id', $customer->id)
->whereIn('status', ['pending', 'active'])
->exists();
if ($active) {
throw new UnprocessableEntityHttpException('این کاربر اعتبار فعال دارد');
}
$plan = $this->buildInstallmentPlan(
(float) $data['gold_amount'],
(int) $data['months'],
$data['interest_type'] ?? 'none',
(float) ($data['interest_value'] ?? 0)
);
return DB::transaction(function () use ($data, $adminId, $source, $customer, $plan) {
$cfg = app(ConfigService::class);
$credit = GoldCredit::create([
'customer_id' => $customer->id,
'gold_amount' => $data['gold_amount'],
'months' => $data['months'],
'interest_type' => $data['interest_type'] ?? 'none',
'interest_value' => $data['interest_value'] ?? 0,
'delivery_method' => $data['delivery_method'] ?? 'manual',
'status' => 'active',
'source' => $source,
'total_interest_gold' => $plan['total_interest_gold'],
'total_payable_gold' => $plan['total_payable_gold'],
'created_by' => $adminId,
'notes' => $data['notes'] ?? null,
'has_check' => (bool) ($data['has_check'] ?? false),
'has_contract' => (bool) ($data['has_contract'] ?? false),
'check_note' => $data['check_note'] ?? null,
'contract_note' => $data['contract_note'] ?? null,
'penalty_type' => $data['penalty_type'] ?? $cfg->get('gold_credit_penalty_type', 'rial'),
'penalty_amount' => $data['penalty_amount'] ?? $cfg->get('gold_credit_penalty_amount', '1000000'),
'grace_hours' => $data['grace_hours'] ?? $cfg->getInt('gold_credit_grace_hours', 24),
]);
$startDate = Carbon::now();
foreach ($plan['installments'] as $item) {
GoldCreditInstallment::create([
'gold_credit_id' => $credit->id,
'installment_number' => $item['installment_number'],
'due_date' => $startDate->copy()->addMonths($item['installment_number'])->toDateString(),
'amount_soot' => $item['amount_soot'],
'principal_gold' => $item['principal_gold'],
'interest_gold' => $item['interest_gold'],
'total_gold' => $item['total_gold'],
'status' => 'pending',
]);
}
if (($data['delivery_method'] ?? 'manual') === 'auto') {
$this->deliverCredit($credit, $adminId, $data['notes'] ?? 'اعتبار طلایی');
}
return $credit->load('installments', 'customer');
});
}
public function deliverCredit(GoldCredit $credit, ?int $adminId, string $note = 'اعتبار طلایی'): void
{
$customer = Customer::findOrFail($credit->customer_id);
$walletOwnerType = $customer->resolveWalletOwnerType();
$walletInterface = new WalletInterface($customer->id, $walletOwnerType);
$withdraw = Withdraw::create([
'user_id' => $adminId,
'customer_id' => $customer->id,
'gold' => $credit->gold_amount,
'note' => $note,
'sts' => 4,
]);
new TransactionInterface($walletInterface, [
'cash' => 0,
'gold' => $credit->gold_amount,
'expire_at' => now(),
'note' => $note,
'withdraw_id' => $withdraw->id,
'active' => true,
], 7, true, false, 0);
}
public function approveRequest(int $requestId, array $creditData, int $adminId): GoldCredit
{
$request = GoldCreditRequest::where('status', 'pending')->findOrFail($requestId);
$creditData['customer_id'] = $request->customer_id;
if (!isset($creditData['gold_amount'])) {
$creditData['gold_amount'] = $request->requested_gold;
}
if (!isset($creditData['months'])) {
$creditData['months'] = $request->requested_months
?? (int) app(ConfigService::class)->getInt('gold_credit_default_months', 12);
}
return DB::transaction(function () use ($request, $creditData, $adminId) {
$credit = $this->createCredit($creditData, $adminId, 'user_request');
$request->update([
'status' => 'approved',
'gold_credit_id' => $credit->id,
'admin_note' => $creditData['admin_note'] ?? $creditData['notes'] ?? null,
]);
return $credit;
});
}
public function rejectRequest(int $requestId, ?string $adminNote = null): GoldCreditRequest
{
$request = GoldCreditRequest::where('status', 'pending')->findOrFail($requestId);
$request->update([
'status' => 'rejected',
'admin_note' => $adminNote,
]);
return $request;
}
public function payInstallments(
GoldCredit $credit,
array $installmentIds,
string $payMethod,
?float $goldAmount = null,
?int $rialAmount = null,
?int $adminId = null
): array {
$installments = GoldCreditInstallment::where('gold_credit_id', $credit->id)
->whereIn('id', $installmentIds)
->orderBy('installment_number')
->get();
if ($installments->isEmpty()) {
throw new UnprocessableEntityHttpException('قسطی یافت نشد');
}
$this->assertSequentialPayable($credit, $installments);
$totalDueGold = (float) $installments->sum(fn ($i) => (float) $i->total_gold + (float) $i->penalty_gold);
$totalDueSoot = (int) $installments->sum('amount_soot');
$totalPenaltyRial = (int) $installments->sum('penalty_rial');
[$payGold, $payRial] = $this->resolvePaymentAmounts(
$payMethod,
$totalDueGold,
$goldAmount,
$rialAmount
);
$payRial += $totalPenaltyRial;
$customer = Customer::findOrFail($credit->customer_id);
$this->deductFromWallet($customer, $payGold, $payRial, $adminId);
$now = now();
foreach ($installments as $installment) {
$isPrepaid = $installment->due_date->isFuture();
$installment->update([
'paid_gold' => (float) $installment->total_gold + (float) $installment->penalty_gold,
'paid_rial' => (int) $installment->penalty_rial,
'status' => $isPrepaid ? 'prepaid' : 'paid',
'paid_at' => $now,
]);
}
$credit->increment('paid_gold', $totalDueGold);
$credit->increment('paid_rial', $payRial);
$remaining = GoldCreditInstallment::where('gold_credit_id', $credit->id)
->whereIn('status', ['pending', 'partial', 'overdue'])
->count();
if ($remaining === 0) {
$credit->update(['status' => 'completed']);
}
$this->refreshOverdueStatuses($credit);
$this->notifyAdminsPayment($customer, $credit, $installments, $totalDueSoot, $payGold, $payRial);
return [
'paid_installments' => $installments->pluck('id'),
'total_soot' => $totalDueSoot,
'paid_gold' => $payGold,
'paid_rial' => $payRial,
];
}
public function refreshOverdueStatuses(GoldCredit $credit): void
{
GoldCreditInstallment::where('gold_credit_id', $credit->id)
->where('status', 'pending')
->where('due_date', '<', now()->toDateString())
->update(['status' => 'overdue']);
}
public function updateCredit(int $creditId, array $data): GoldCredit
{
$credit = GoldCredit::with('installments')->findOrFail($creditId);
if (!in_array($credit->status, ['pending', 'active'], true)) {
throw new UnprocessableEntityHttpException('فقط اعتبارهای فعال قابل ویرایش هستند');
}
if (isset($data['notes'])) {
$credit->notes = $data['notes'];
}
foreach (['has_check', 'has_contract'] as $boolField) {
if (array_key_exists($boolField, $data)) {
$credit->{$boolField} = (bool) $data[$boolField];
}
}
foreach (['check_note', 'contract_note', 'penalty_type'] as $textField) {
if (array_key_exists($textField, $data)) {
$credit->{$textField} = $data[$textField];
}
}
if (array_key_exists('penalty_amount', $data)) {
$credit->penalty_amount = $data['penalty_amount'];
}
if (array_key_exists('grace_hours', $data)) {
$credit->grace_hours = $data['grace_hours'];
}
if (isset($data['status']) && in_array($data['status'], ['cancelled'], true)) {
$credit->status = $data['status'];
$credit->save();
return $credit;
}
$needsRecalc = isset($data['gold_amount']) || isset($data['months'])
|| isset($data['interest_type']) || isset($data['interest_value']);
if ($needsRecalc) {
$paidNumbers = $credit->installments
->whereIn('status', ['paid', 'prepaid'])
->pluck('installment_number')
->toArray();
if (count($paidNumbers) > 0) {
throw new UnprocessableEntityHttpException('پس از پرداخت قسط، امکان تغییر مبلغ/مدت وجود ندارد');
}
$goldAmount = (float) ($data['gold_amount'] ?? $credit->gold_amount);
$months = (int) ($data['months'] ?? $credit->months);
$interestType = $data['interest_type'] ?? $credit->interest_type;
$interestValue = (float) ($data['interest_value'] ?? $credit->interest_value);
$plan = $this->buildInstallmentPlan($goldAmount, $months, $interestType, $interestValue);
$credit->installments()->delete();
$startDate = Carbon::parse($credit->created_at);
foreach ($plan['installments'] as $item) {
GoldCreditInstallment::create([
'gold_credit_id' => $credit->id,
'installment_number' => $item['installment_number'],
'due_date' => $startDate->copy()->addMonths($item['installment_number'])->toDateString(),
'amount_soot' => $item['amount_soot'],
'principal_gold' => $item['principal_gold'],
'interest_gold' => $item['interest_gold'],
'total_gold' => $item['total_gold'],
'status' => 'pending',
]);
}
$credit->fill([
'gold_amount' => $goldAmount,
'months' => $months,
'interest_type' => $interestType,
'interest_value' => $interestValue,
'total_interest_gold' => $plan['total_interest_gold'],
'total_payable_gold' => $plan['total_payable_gold'],
]);
}
$credit->save();
return $credit->fresh(['installments', 'customer']);
}
public function getGramPriceRial(): int
{
$price = Price::query()->orderByDesc('id')->value('price');
return (int) ($price ?? 0);
}
/**
* هشدار SMS در سررسید + جریمه روزانه پس از مهلت (grace) + یادآوری ۱ و ۳ روز قبل.
* پرداخت خودکار نیست — فقط بدهی جریمه اضافه می‌شود.
*
* @return array{warnings:int,penalties:int,reminders:int}
*/
public function processOverdueInstallments(): array
{
$cfg = app(ConfigService::class);
if (! $cfg->isGoldCreditEnabled() || ! Schema::hasTable('gold_credit_installments')) {
return ['warnings' => 0, 'penalties' => 0, 'reminders' => 0];
}
$smsEnabled = $cfg->getBool('gold_credit_overdue_sms_enabled', true);
$warnings = 0;
$penalties = 0;
$reminders = $this->processUpcomingDueReminders();
$installments = GoldCreditInstallment::with(['goldCredit.customer'])
->whereIn('status', ['pending', 'overdue'])
->where('due_date', '<=', now()->toDateString())
->get();
foreach ($installments as $installment) {
$credit = $installment->goldCredit;
if (!$credit || $credit->status !== 'active') {
continue;
}
if ($installment->status === 'pending') {
$installment->status = 'overdue';
$installment->save();
}
if ($smsEnabled && !$installment->warning_sms_sent_at) {
$this->sendOverdueWarningSms($installment);
$installment->warning_sms_sent_at = now();
$installment->save();
$warnings++;
}
if ($this->applyDailyPenaltyIfDue($installment, $credit)) {
$penalties++;
}
}
return compact('warnings', 'penalties', 'reminders');
}
/** یادآوری ۳ روز و ۱ روز مانده به سررسید — برای مشتری و ادمین */
public function processUpcomingDueReminders(): int
{
$cfg = app(ConfigService::class);
if (!$cfg->getBool('gold_credit_upcoming_sms_enabled', true)) {
return 0;
}
$sent = 0;
$today = now()->startOfDay();
foreach ([3 => 'reminder_3d_sent_at', 1 => 'reminder_1d_sent_at'] as $days => $field) {
$targetDate = $today->copy()->addDays($days)->toDateString();
$installments = GoldCreditInstallment::with(['goldCredit.customer'])
->where('status', 'pending')
->whereNull($field)
->whereDate('due_date', $targetDate)
->get();
foreach ($installments as $installment) {
$credit = $installment->goldCredit;
if (!$credit || $credit->status !== 'active') {
continue;
}
$this->sendUpcomingReminderSms($installment, $days);
$installment->{$field} = now();
$installment->save();
$sent++;
}
}
return $sent;
}
public function notifyAdminsLoanRequest(GoldCreditRequest $request): void
{
$cfg = app(ConfigService::class);
if (!$cfg->getBool('gold_credit_admin_sms_enabled', true)) {
return;
}
$customer = Customer::find($request->customer_id);
$company = $cfg->get('name_fa', '');
$message = sprintf(
"درخواست اعتبار طلایی جدید\nمشتری: %s\nموبایل: %s\nمقدار: %s گرم\nمدت: %s ماه\n%s",
$customer->name ?? '-',
$customer->mobile ?? '-',
$request->requested_gold,
$request->requested_months ?? '-',
$company
);
$this->notifyAdminsSms($message, 'gold_credit_request');
}
private function notifyAdminsPayment(
Customer $customer,
GoldCredit $credit,
$installments,
int $totalSoot,
float $payGold,
int $payRial
): void {
$cfg = app(ConfigService::class);
if (!$cfg->getBool('gold_credit_admin_sms_enabled', true)) {
return;
}
$numbers = $installments->pluck('installment_number')->implode(',');
$company = $cfg->get('name_fa', '');
$message = sprintf(
"پرداخت قسط اعتبار طلایی\nمشتری: %s\nموبایل: %s\nاقساط: %s\nمبلغ: %s سوت\nطلا: %s گرم / ریال: %s\n%s",
$customer->name ?? '-',
$customer->mobile ?? '-',
$numbers,
number_format($totalSoot),
$payGold,
number_format($payRial),
$company
);
$this->notifyAdminsSms($message, 'gold_credit_payment');
}
private function notifyAdminsSms(string $message, string $source): void
{
User::where('send_sms', 1)->get()->each(function ($admin) use ($message, $source) {
if (!$admin->mobile) {
return;
}
try {
app(\App\Services\SmsBilling\SmsGatewayService::class)
->send($admin->mobile, $message, $source);
} catch (\Throwable $e) {
Log::warning('gold credit admin sms failed', [
'admin_id' => $admin->id,
'source' => $source,
'error' => $e->getMessage(),
]);
}
});
}
private function sendUpcomingReminderSms(GoldCreditInstallment $installment, int $daysLeft): void
{
$customer = $installment->goldCredit?->customer;
$cfg = app(ConfigService::class);
$company = $cfg->get('name_fa', '');
$soot = number_format((int) $installment->amount_soot);
$customerMsg = sprintf(
"%s عزیز، %d روز تا سررسید قسط شماره %d اعتبار طلایی باقی مانده است.\nمبلغ قسط: %s سوت\nلطفاً از بخش اعتبار طلایی پرداخت کنید.\n%s",
$customer->name ?? 'کاربر',
$daysLeft,
$installment->installment_number,
$soot,
$company
);
if ($customer?->mobile) {
try {
app(\App\Services\SmsBilling\SmsGatewayService::class)
->send($customer->mobile, $customerMsg, 'gold_credit_upcoming');
} catch (\Throwable $e) {
Log::warning('gold credit upcoming sms failed', [
'installment_id' => $installment->id,
'error' => $e->getMessage(),
]);
}
}
if ($cfg->getBool('gold_credit_admin_sms_enabled', true)) {
$adminMsg = sprintf(
"اخطار سررسید اعتبار طلایی\n%d روز مانده\nمشتری: %s\nموبایل: %s\nقسط %d — %s سوت\n%s",
$daysLeft,
$customer->name ?? '-',
$customer->mobile ?? '-',
$installment->installment_number,
$soot,
$company
);
$this->notifyAdminsSms($adminMsg, 'gold_credit_upcoming_admin');
}
}
public function resolvePenaltyConfig(GoldCredit $credit): array
{
$cfg = app(ConfigService::class);
$type = $credit->penalty_type ?: $cfg->get('gold_credit_penalty_type', 'rial');
$type = in_array($type, ['rial', 'gold'], true) ? $type : 'rial';
$amount = $credit->penalty_amount !== null
? (float) $credit->penalty_amount
: (float) $cfg->get('gold_credit_penalty_amount', '1000000');
$grace = $credit->grace_hours !== null
? (int) $credit->grace_hours
: $cfg->getInt('gold_credit_grace_hours', 24);
return [
'type' => $type,
'amount' => $amount,
'grace_hours' => max(0, $grace),
];
}
private function sendOverdueWarningSms(GoldCreditInstallment $installment): void
{
$customer = $installment->goldCredit?->customer;
if (!$customer?->mobile) {
return;
}
$cfg = app(ConfigService::class);
$grace = $this->resolvePenaltyConfig($installment->goldCredit)['grace_hours'];
$penalty = $this->resolvePenaltyConfig($installment->goldCredit);
$penaltyLabel = $penalty['type'] === 'gold'
? number_format($penalty['amount'], 4) . ' گرم طلا'
: number_format((int) $penalty['amount']) . ' ریال';
$template = $cfg->get(
'gold_credit_overdue_sms_text',
'قسط شماره {n} سررسید شده است. در صورت عدم پرداخت تا {hours} ساعت آینده، روزانه {penalty} به بدهی شما اضافه می‌شود.'
);
$message = str_replace(
['{n}', '{hours}', '{penalty}', '{name}'],
[
(string) $installment->installment_number,
(string) $grace,
$penaltyLabel,
$customer->name ?? '',
],
(string) $template
);
$message .= "\n" . $cfg->get('name_fa', '');
try {
app(\App\Services\SmsBilling\SmsGatewayService::class)
->send($customer->mobile, $message, 'gold_credit_overdue');
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::warning('gold credit overdue sms failed', [
'installment_id' => $installment->id,
'error' => $e->getMessage(),
]);
}
}
private function applyDailyPenaltyIfDue(GoldCreditInstallment $installment, GoldCredit $credit): bool
{
if (!$installment->warning_sms_sent_at) {
return false;
}
$config = $this->resolvePenaltyConfig($credit);
$graceEnds = Carbon::parse($installment->warning_sms_sent_at)->addHours($config['grace_hours']);
if (now()->lt($graceEnds)) {
return false;
}
if (!$installment->penalty_started_at) {
$installment->penalty_started_at = now();
}
$last = $installment->last_penalty_at
? Carbon::parse($installment->last_penalty_at)->startOfDay()
: null;
$today = now()->startOfDay();
if ($last && $last->equalTo($today)) {
return false;
}
if ($config['type'] === 'gold') {
$installment->penalty_gold = round((float) $installment->penalty_gold + $config['amount'], 6);
} else {
$installment->penalty_rial = (int) $installment->penalty_rial + (int) round($config['amount']);
}
$installment->penalty_days = (int) $installment->penalty_days + 1;
$installment->last_penalty_at = now();
$installment->status = 'overdue';
$installment->save();
return true;
}
private function calculateTotalInterest(
float $goldAmount,
int $months,
string $interestType,
float $interestValue
): float {
return match ($interestType) {
'percent' => round($goldAmount * ($interestValue / 100) * $months, 6),
'fixed_gram' => round($interestValue * $months, 6),
default => 0,
};
}
private function assertSequentialPayable(GoldCredit $credit, $installments): void
{
$allPending = GoldCreditInstallment::where('gold_credit_id', $credit->id)
->whereIn('status', ['pending', 'overdue', 'partial'])
->orderBy('installment_number')
->get();
$expectedIds = $allPending->take($installments->count())->pluck('id')->sort()->values();
$actualIds = $installments->pluck('id')->sort()->values();
if ($expectedIds->toArray() !== $actualIds->toArray()) {
throw new UnprocessableEntityHttpException('اقساط باید به ترتیب و بدون پرش پرداخت شوند');
}
foreach ($installments as $installment) {
if (!in_array($installment->status, ['pending', 'overdue', 'partial'], true)) {
throw new UnprocessableEntityHttpException('یکی از اقساط انتخاب‌شده قبلاً پرداخت شده است');
}
}
}
private function resolvePaymentAmounts(
string $payMethod,
float $totalDueGold,
?float $goldAmount,
?int $rialAmount
): array {
return match ($payMethod) {
'gold' => [$totalDueGold, 0],
'rial' => [0, $this->goldToRial($totalDueGold)],
'mixed' => [
(float) ($goldAmount ?? 0),
(int) ($rialAmount ?? 0),
],
default => throw new UnprocessableEntityHttpException('روش پرداخت نامعتبر است'),
};
}
public function defaultTiers(): array
{
return [
['min_gram' => 0.5, 'max_gram' => 1, 'credit_gram' => 3, 'monthly_soot' => 250, 'months' => 12],
['min_gram' => 1, 'max_gram' => 2, 'credit_gram' => 4, 'monthly_soot' => 333, 'months' => 12],
['min_gram' => 2, 'max_gram' => 3, 'credit_gram' => 5, 'monthly_soot' => 417, 'months' => 12],
];
}
public function getTiers(): array
{
$cfg = app(ConfigService::class);
$raw = $cfg->getString('gold_credit_tiers', '');
if ($raw === '') {
return $this->defaultTiers();
}
$decoded = json_decode($raw, true);
if (!is_array($decoded) || count($decoded) === 0) {
return $this->defaultTiers();
}
return array_values(array_map(function ($tier) {
return [
'min_gram' => (float) ($tier['min_gram'] ?? 0),
'max_gram' => (float) ($tier['max_gram'] ?? 0),
'credit_gram' => (float) ($tier['credit_gram'] ?? 0),
'monthly_soot' => (int) ($tier['monthly_soot'] ?? 0),
'months' => (int) ($tier['months'] ?? 12),
];
}, $decoded));
}
public function resolveTierForBalance(float $goldBalance): ?array
{
$tiers = $this->getTiers();
$count = count($tiers);
foreach ($tiers as $i => $tier) {
$min = (float) $tier['min_gram'];
$max = (float) $tier['max_gram'];
$isLast = $i === $count - 1;
if ($goldBalance >= $min && ($isLast ? $goldBalance <= $max : $goldBalance < $max)) {
return $tier;
}
}
return null;
}
public function getCustomerGoldBalance(int $customerId): float
{
$customer = Customer::findOrFail($customerId);
$walletOwnerType = $customer->resolveWalletOwnerType();
$wallet = Wallet::where('owner', $customerId)
->where('owner_type', $walletOwnerType)
->whereNull('fund_id')
->whereNull('product_id')
->first();
return round((float) ($wallet->gold ?? 0), 4);
}
public function getPublicConfig(): array
{
$cfg = app(ConfigService::class);
$defaultInfo = "اعتبار طلایی بر اساس موجودی طلای شما در قلک اعطا می‌شود.\n"
. "• کاربران با موجودی ۵۰۰ سوت تا ۱ گرم: ۳ گرم اعتبار — بازپرداخت ماهانه ۲۵۰ سوت\n"
. "• کاربران با موجودی ۱ تا ۲ گرم: ۴ گرم اعتبار — بازپرداخت ماهانه ۳۳۳ سوت\n"
. "• کاربران با موجودی ۲ تا ۳ گرم: ۵ گرم اعتبار — بازپرداخت ماهانه ۴۱۷ سوت\n"
. "بازپرداخت همه طرح‌ها ۱۲ ماهه است.";
$defaultSuccess = 'درخواست شما ثبت شد و در حال بررسی است. تا ۲۴ ساعت آینده با شما تماس گرفته می‌شود و توضیحات تکمیلی توسط کارشناس ارائه خواهد شد.';
return [
'min_balance_gram' => (float) $cfg->get('gold_credit_min_balance_gram', '0.5'),
'info_text' => $cfg->getString('gold_credit_info_text', $defaultInfo),
'request_success_text' => $cfg->getString('gold_credit_request_success_text', $defaultSuccess),
'default_months' => $cfg->getInt('gold_credit_default_months', 12),
'pay_methods' => [
'gold' => $cfg->getBool('gold_credit_pay_gold_enabled', true),
'rial' => $cfg->getBool('gold_credit_pay_rial_enabled', false),
'mixed' => $cfg->getBool('gold_credit_pay_mixed_enabled', false),
],
'tiers' => $this->getTiers(),
];
}
public function assertPayMethodAllowed(string $payMethod): void
{
$methods = $this->getPublicConfig()['pay_methods'];
if (empty($methods[$payMethod])) {
throw new UnprocessableEntityHttpException('این روش پرداخت فعال نیست');
}
}
private function goldToRial(float $gold): int
{
$gramPrice = $this->getGramPriceRial();
if ($gramPrice <= 0) {
throw new UnprocessableEntityHttpException('قیمت طلا در دسترس نیست');
}
return (int) ceil($gold * $gramPrice);
}
private function deductFromWallet(Customer $customer, float $payGold, int $payRial, ?int $adminId): void
{
$walletOwnerType = $customer->resolveWalletOwnerType();
$wallet = Wallet::where('owner', $customer->id)
->where('owner_type', $walletOwnerType)
->whereNull('fund_id')
->whereNull('product_id')
->first();
if ($payGold > 0) {
$available = $wallet ? (float) $wallet->gold : 0;
if ($payGold > $available) {
throw new UnprocessableEntityHttpException('موجودی طلای کیف پول کافی نیست');
}
}
if ($payRial > 0) {
$mainWallet = app(MainWalletProductService::class);
foreach (app(ConfigService::class)->mainWalletProductIds() as $productId) {
$mainWallet->consolidateOrphanedForOwner($customer->id, $walletOwnerType, (int) $productId);
}
$availableCash = $mainWallet->effectiveMainWalletCash($customer->id, $walletOwnerType);
if ($payRial > $availableCash) {
throw new UnprocessableEntityHttpException('موجودی ریالی کیف پول کافی نیست');
}
}
$walletInterface = new WalletInterface($customer->id, $walletOwnerType);
$note = 'بازپرداخت اعتبار طلایی';
if ($payGold > 0) {
$withdraw = Withdraw::create([
'user_id' => $adminId,
'customer_id' => $customer->id,
'gold' => -$payGold,
'note' => $note,
'sts' => 4,
]);
new TransactionInterface($walletInterface, [
'cash' => 0,
'gold' => -$payGold,
'expire_at' => now(),
'note' => $note,
'withdraw_id' => $withdraw->id,
'active' => true,
], 7, false, false, 0);
}
if ($payRial > 0) {
$withdraw = Withdraw::create([
'user_id' => $adminId,
'customer_id' => $customer->id,
'cash' => -$payRial,
'note' => $note,
'sts' => 4,
]);
new TransactionInterface($walletInterface, [
'cash' => -$payRial,
'gold' => 0,
'expire_at' => now(),
'note' => $note,
'withdraw_id' => $withdraw->id,
'active' => true,
], 4, false, false);
}
}
}