ifnex/04_Laravel/app/Services/CustomerCreditService.php
Kazem Alghasi 3848f0b399 feat(finance): rate approval + multi-currency credit system (درخواست ۹)
- rate approval: API creates pending proposals, manager approves
- customer_credits + credit_settlements tables
- CustomerCreditService (grant/settle via wallet)
- rebuilt CustomerCreditResource
- CustomerFinancialController: real foreign-currency debt
- WP login redirect + custom login page wiring
2026-09-30 05:14:31 +03:30

177 lines
7.1 KiB
PHP

<?php
namespace App\Services;
use App\Enums\PaymentGateway;
use App\Enums\TransactionStatus;
use App\Enums\TransactionType;
use App\Models\CreditSettlement;
use App\Models\CustomerCredit;
use App\Models\User;
use App\Models\WalletTransaction;
use Illuminate\Support\Facades\DB;
use RuntimeException;
/**
* سرویس اعتبار چندارزی مشتری.
* کیف پول (IRR) مرجع است. اعتبار = بدهی به ارز خارجی. تسویه = پرداخت ریالی با نرخ روز.
*/
class CustomerCreditService
{
/**
* اعطای اعتبار (بدهی) چندارزی. فقط super_admin (Policy).
*/
public function grantCredit(
User $customer,
string $currencyCode,
float $amount,
?string $reason = null,
?string $reference = null,
?int $shipmentId = null,
int $grantedBy
): CustomerCredit {
if ($amount <= 0) {
throw new RuntimeException('مبلغ اعتبار باید مثبت باشد.');
}
return DB::transaction(function () use (
$customer, $currencyCode, $amount, $reason, $reference, $shipmentId, $grantedBy
) {
return CustomerCredit::create([
'user_id' => $customer->id,
'currency_code' => strtoupper(trim($currencyCode)),
'amount' => $amount,
'settled_amount' => 0,
'status' => 'open',
'reason' => $reason,
'reference' => $reference,
'shipment_id' => $shipmentId,
'granted_by' => $grantedBy,
'granted_at' => now(),
]);
});
}
/**
* ثبت تسویه (پرداخت ریالی) برای بخشی از یک اعتبار.
*
* @param bool $fromWallet اگه true، مبلغ ریالی از کیف پول کسر می‌شه.
*/
public function settle(
CustomerCredit $credit,
float $amountForeign,
float $exchangeRate,
string $rateDate,
bool $fromWallet,
int $settledBy,
?string $reason = null,
?string $reference = null
): CreditSettlement {
if ($amountForeign <= 0) {
throw new RuntimeException('مبلغ تسویه باید مثبت باشد.');
}
if ($exchangeRate <= 0) {
throw new RuntimeException('نرخ ارز باید مثبت باشد.');
}
if (! $credit->isOpen()) {
throw new RuntimeException('این اعتبار قابل تسویه نیست (settled یا بسته).');
}
if ($amountForeign > $credit->balance + 0.005) {
throw new RuntimeException('مبلغ تسویه از مانده بدهی بیشتر است. مانده: '
. number_format($credit->balance, 2) . ' ' . $credit->currency_code);
}
return DB::transaction(function () use (
$credit, $amountForeign, $exchangeRate, $rateDate, $fromWallet, $settledBy, $reason, $reference
) {
$credit = CustomerCredit::lockForUpdate()->findOrFail($credit->id);
$settlementAmountIrr = round($amountForeign * $exchangeRate, 2);
$walletTransactionId = null;
if ($fromWallet) {
$wallet = $credit->user->wallet;
if (! $wallet) {
throw new RuntimeException('کاربر کیف پول ندارد.');
}
if ($wallet->isFrozen()) {
throw new RuntimeException('کیف پول کاربر مسدود است.');
}
if (! $wallet->hasSufficientBalance($settlementAmountIrr)) {
throw new RuntimeException('موجودی کیف پول کافی نیست. نیاز: '
. number_format($settlementAmountIrr) . ' ریال');
}
$wt = WalletTransaction::create([
'wallet_id' => $wallet->id,
'amount' => -$settlementAmountIrr,
'balance_before' => $wallet->balance,
'balance_after' => $wallet->balance - $settlementAmountIrr,
'type' => TransactionType::CREDIT_SETTLEMENT,
'status' => TransactionStatus::COMPLETED,
'gateway' => PaymentGateway::WALLET,
'description' => "تسویه اعتبار {$credit->currency_code} {$amountForeign} (credit #{$credit->id})",
'transactionable_type' => CustomerCredit::class,
'transactionable_id' => $credit->id,
'created_by' => $settledBy,
]);
$wallet->update([
'balance' => $wallet->balance - $settlementAmountIrr,
'total_withdrawn' => $wallet->total_withdrawn + $settlementAmountIrr,
]);
$walletTransactionId = $wt->id;
}
$settlement = CreditSettlement::create([
'customer_credit_id' => $credit->id,
'settled_amount_foreign' => $amountForeign,
'currency_code' => $credit->currency_code,
'settlement_currency' => 'IRR',
'exchange_rate' => $exchangeRate,
'rate_date' => $rateDate,
'settlement_amount_irr' => $settlementAmountIrr,
'wallet_transaction_id' => $walletTransactionId,
'reason' => $reason,
'reference' => $reference,
'settled_by' => $settledBy,
'settled_at' => now(),
]);
$credit->settled_amount = (float) $credit->settled_amount + $amountForeign;
$credit->recomputeStatus();
return $settlement;
});
}
/**
* بدهی‌های باز مشتری به تفکیک ارز.
*
* @return array<int, array{currency_code: string, total_amount: float, total_settled: float, balance: float, count: int}>
*/
public function getCustomerDebtsByCurrency(User $customer): array
{
return CustomerCredit::where('user_id', $customer->id)
->whereIn('status', ['open', 'partially_settled'])
->select('currency_code')
->selectRaw('SUM(amount) as total_amount')
->selectRaw('SUM(settled_amount) as total_settled')
->selectRaw('SUM(amount - settled_amount) as balance')
->selectRaw('COUNT(*) as count')
->groupBy('currency_code')
->get()
->map(function ($row) {
return [
'currency_code' => $row->currency_code,
'total_amount' => (float) $row->total_amount,
'total_settled' => (float) $row->total_settled,
'balance' => (float) $row->balance,
'count' => (int) $row->count,
];
})
->toArray();
}
}