- 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
177 lines
7.1 KiB
PHP
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();
|
|
}
|
|
} |