Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 1m50s

This commit is contained in:
masoodafar-web
2025-12-27 07:37:30 +03:30
13 changed files with 566 additions and 576 deletions
@@ -0,0 +1,15 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoans;
/// <summary>
/// Command یکپارچه برای استعلام وضعیت وام از سرویس دایا و پردازش خودکار وام‌های تأیید شده
/// این Command هم استعلام می‌کند و هم در صورت تأیید، کیف پول را شارژ می‌کند
/// </summary>
public record CheckAndProcessDayaLoansCommand : IRequest<CheckAndProcessDayaLoansResponseDto>
{
/// <summary>
/// لیست کدهای ملی برای استعلام
/// </summary>
public required List<string> NationalCodes { get; init; }
}
@@ -0,0 +1,301 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Application.DayaLoanCQ.Services;
using CMSMicroservice.Application.Common.Interfaces;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoans;
/// <summary>
/// Handler یکپارچه برای استعلام و پردازش وام دایا
/// 1. استعلام از API دایا
/// 2. ذخیره/به‌روزرسانی DayaLoanContract
/// 3. شارژ کیف پول برای وام‌های تأیید شده
/// 4. ثبت Order پکیج طلایی
/// 5. ارسال SMS
/// </summary>
public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndProcessDayaLoansCommand, CheckAndProcessDayaLoansResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IDayaLoanApiService _dayaApiService;
private readonly IKavenegarService _smsService;
private readonly ILogger<CheckAndProcessDayaLoansCommandHandler> _logger;
public CheckAndProcessDayaLoansCommandHandler(
IApplicationDbContext context,
IDayaLoanApiService dayaApiService,
IKavenegarService smsService,
ILogger<CheckAndProcessDayaLoansCommandHandler> logger)
{
_context = context;
_dayaApiService = dayaApiService;
_smsService = smsService;
_logger = logger;
}
public async Task<CheckAndProcessDayaLoansResponseDto> Handle(CheckAndProcessDayaLoansCommand request, CancellationToken cancellationToken)
{
var results = new List<DayaLoanProcessResult>();
var processedCount = 0;
try
{
// 1. استعلام از سرویس دایا
_logger.LogInformation("Checking Daya loan status for {Count} national codes", request.NationalCodes.Count);
var dayaResults = await _dayaApiService.CheckLoanStatusAsync(request.NationalCodes, cancellationToken);
foreach (var dayaResult in dayaResults)
{
var result = new DayaLoanProcessResult
{
NationalCode = dayaResult.NationalCode,
Status = dayaResult.Status,
ContractNumber = dayaResult.ContractNumber,
WasProcessed = false,
Message = "استعلام موفق"
};
try
{
// 2. پیدا کردن کاربر
var user = await _context.Users
.Include(u => u.UserWallets)
.FirstOrDefaultAsync(u => u.NationalCode == dayaResult.NationalCode, cancellationToken);
if (user == null)
{
result.Message = "کاربر یافت نشد";
results.Add(result);
continue;
}
result.UserId = user.Id;
// 3. ذخیره/به‌روزرسانی DayaLoanContract
var contract = await _context.DayaLoanContracts
.FirstOrDefaultAsync(d => d.NationalCode == dayaResult.NationalCode, cancellationToken);
if (contract == null)
{
contract = new DayaLoanContract
{
UserId = user.Id,
NationalCode = dayaResult.NationalCode,
Status = dayaResult.Status,
ContractNumber = dayaResult.ContractNumber,
LastCheckDate = DateTime.Now,
IsProcessed = false
};
await _context.DayaLoanContracts.AddAsync(contract, cancellationToken);
}
else
{
contract.Status = dayaResult.Status;
contract.ContractNumber = dayaResult.ContractNumber;
contract.LastCheckDate = DateTime.Now;
}
// 4. آیا باید پردازش مالی انجام شود؟
var shouldProcess = !contract.IsProcessed &&
!user.HasReceivedDayaCredit &&
!string.IsNullOrEmpty(dayaResult.ContractNumber) &&
(dayaResult.Status == DayaLoanStatus.PendingReceive ||
dayaResult.Status == DayaLoanStatus.Received);
if (shouldProcess)
{
// 5. پردازش مالی
var processResult = await ProcessDayaLoanAsync(user, dayaResult.ContractNumber!, cancellationToken);
contract.IsProcessed = true;
result.WasProcessed = true;
result.NewWalletBalance = processResult.NewBalance;
result.Message = "وام پردازش و کیف پول شارژ شد";
processedCount++;
_logger.LogInformation(
"Daya loan processed for User {UserId}, Contract: {ContractNumber}, NewBalance: {Balance}",
user.Id, dayaResult.ContractNumber, processResult.NewBalance);
}
else if (contract.IsProcessed || user.HasReceivedDayaCredit)
{
result.Message = "قبلاً پردازش شده";
}
else if (string.IsNullOrEmpty(dayaResult.ContractNumber))
{
result.Message = "هنوز قرارداد ندارد";
}
await _context.SaveChangesAsync(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing Daya loan for NationalCode {NationalCode}", dayaResult.NationalCode);
result.Message = $"خطا: {ex.Message}";
}
results.Add(result);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calling Daya API service");
// در صورت خطا در API
foreach (var nationalCode in request.NationalCodes)
{
if (!results.Any(r => r.NationalCode == nationalCode))
{
results.Add(new DayaLoanProcessResult
{
NationalCode = nationalCode,
Status = DayaLoanStatus.PendingReceive,
WasProcessed = false,
Message = $"خطا در استعلام: {ex.Message}"
});
}
}
}
return new CheckAndProcessDayaLoansResponseDto
{
Results = results,
TotalChecked = request.NationalCodes.Count,
WithContractCount = results.Count(r => !string.IsNullOrEmpty(r.ContractNumber)),
ProcessedCount = processedCount
};
}
/// <summary>
/// پردازش مالی وام دایا
/// </summary>
private async Task<(long NewBalance, long TransactionId)> ProcessDayaLoanAsync(
User user,
string contractNumber,
CancellationToken cancellationToken)
{
// 1. ایجاد تراکنش
var transaction = new Transaction
{
Amount = SystemConstants.DayaLoanAmount,
Description = $"دریافت اعتبار دایا - قرارداد {contractNumber}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.Now,
RefId = contractNumber,
Type = TransactionType.DepositExternal1
};
await _context.Transactions.AddAsync(transaction, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// 2. یافتن یا ایجاد کیف پول
var wallet = user.UserWallets.FirstOrDefault();
if (wallet == null)
{
wallet = new UserWallet
{
UserId = user.Id,
Balance = 0,
NetworkBalance = 0,
DiscountBalance = 0
};
await _context.UserWallets.AddAsync(wallet, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
// 3. شارژ کیف پول عادی
wallet.Balance += SystemConstants.DayaLoanAmount;
var mainLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = SystemConstants.DayaLoanAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(mainLog, cancellationToken);
// 4. شارژ کیف پول تخفیف (دو برابر)
var discountAmount = SystemConstants.DayaLoanAmount * 2;
wallet.DiscountBalance += discountAmount;
var discountLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = 0,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = discountAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken);
// 5. به‌روزرسانی کاربر
user.HasReceivedDayaCredit = true;
user.DayaCreditReceivedAt = DateTime.Now;
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
// 6. ثبت Order پکیج پایه
var goldenPackage = await _context.Packages
.FirstOrDefaultAsync(p => p.Id == 4, cancellationToken);
if (goldenPackage != null)
{
// 6. ثبت UserPackagePurchase برای پکیج پایه
var goldenPackageId =goldenPackage.Id;
var packagePurchase = new UserPackagePurchase
{
UserId = user.Id,
PackageId = goldenPackageId,
PurchaseMethod = PackagePurchaseMethod.DayaLoan,
PurchasedAt = DateTime.Now,
Amount = SystemConstants.DayaLoanAmount,
TransactionId = transaction.Id
};
await _context.UserPackagePurchases.AddAsync(packagePurchase, cancellationToken);
}
// 7. Domain Event
user.AddDomainEvent(new DayaLoanApprovedEvent(user, transaction, contractNumber));
await _context.SaveChangesAsync(cancellationToken);
// 8. ارسال SMS
await SendDayaLoanSmsAsync(user);
return (wallet.Balance, transaction.Id);
}
/// <summary>
/// ارسال SMS اطلاع‌رسانی
/// </summary>
private async Task SendDayaLoanSmsAsync(User user)
{
try
{
var userName = SmsTemplates.GetUserName(user.FirstName+" "+user.LastName);
var message = SmsTemplates.DayaLoanReceived(userName);
await _smsService.SendAsync(user.Mobile, message);
// ارسال SMS به ادمین برای اطلاع
var adminMessage = $"وام دایا دریافت شد\nکاربر: {user.FirstName} {user.LastName}\nموبایل: {user.Mobile}\nکدملی: {user.NationalCode}";
await _smsService.SendAsync("09199877503", adminMessage);
_logger.LogInformation("Daya loan SMS sent to User {UserId}", user.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send Daya loan SMS to User {UserId}", user.Id);
}
}
}
@@ -0,0 +1,70 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoans;
/// <summary>
/// پاسخ Command استعلام و پردازش وام دایا
/// </summary>
public class CheckAndProcessDayaLoansResponseDto
{
/// <summary>
/// نتایج استعلام و پردازش
/// </summary>
public required List<DayaLoanProcessResult> Results { get; set; }
/// <summary>
/// تعداد کل استعلام شده
/// </summary>
public int TotalChecked { get; set; }
/// <summary>
/// تعداد موفق (دارای قرارداد)
/// </summary>
public int WithContractCount { get; set; }
/// <summary>
/// تعداد پردازش شده (شارژ کیف پول)
/// </summary>
public int ProcessedCount { get; set; }
}
/// <summary>
/// نتیجه پردازش هر کاربر
/// </summary>
public class DayaLoanProcessResult
{
/// <summary>
/// کد ملی
/// </summary>
public required string NationalCode { get; set; }
/// <summary>
/// شناسه کاربر
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// وضعیت وام در دایا
/// </summary>
public DayaLoanStatus Status { get; set; }
/// <summary>
/// شماره قرارداد
/// </summary>
public string? ContractNumber { get; set; }
/// <summary>
/// آیا پردازش مالی انجام شد؟
/// </summary>
public bool WasProcessed { get; set; }
/// <summary>
/// موجودی کیف پول بعد از شارژ
/// </summary>
public long? NewWalletBalance { get; set; }
/// <summary>
/// پیام
/// </summary>
public required string Message { get; set; }
}
@@ -1,14 +0,0 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
/// <summary>
/// Command برای استعلام وضعیت وام از سرویس دایا
/// </summary>
public record CheckDayaLoanStatusCommand : IRequest<CheckDayaLoanStatusResponseDto>
{
/// <summary>
/// لیست کدهای ملی برای استعلام
/// </summary>
public List<string> NationalCodes { get; init; }
}
@@ -1,235 +0,0 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Application.DayaLoanCQ.Services;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanStatusCommand, CheckDayaLoanStatusResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IDayaLoanApiService _dayaApiService;
private readonly ILogger<CheckDayaLoanStatusCommandHandler> _logger;
public CheckDayaLoanStatusCommandHandler(
IApplicationDbContext context,
IDayaLoanApiService dayaApiService,
ILogger<CheckDayaLoanStatusCommandHandler> logger)
{
_context = context;
_dayaApiService = dayaApiService;
_logger = logger;
}
public async Task<CheckDayaLoanStatusResponseDto> Handle(CheckDayaLoanStatusCommand request, CancellationToken cancellationToken)
{
var results = new List<DayaLoanStatusItem>();
try
{
// فراخوانی سرویس دایا (Mock یا Real)
var dayaResults = await _dayaApiService.CheckLoanStatusAsync(request.NationalCodes, cancellationToken);
foreach (var dayaResult in dayaResults)
{
try
{
results.Add(new DayaLoanStatusItem
{
NationalCode = dayaResult.NationalCode,
Status = dayaResult.Status,
ContractNumber = dayaResult.ContractNumber,
Message = "استعلام موفق"
});
// ذخیره یا به‌روزرسانی در دیتابیس
var existingContract = await _context.DayaLoanContracts
.FirstOrDefaultAsync(d => d.NationalCode == dayaResult.NationalCode, cancellationToken);
if (existingContract != null)
{
var previousStatus = existingContract.Status;
existingContract.LastCheckDate = DateTime.Now;
existingContract.Status = dayaResult.Status;
existingContract.ContractNumber = dayaResult.ContractNumber;
// بررسی تغییر وضعیت به PendingReceive یا Received
// فقط اگر قبلاً پردازش نشده باشد
if (!existingContract.IsProcessed &&
!string.IsNullOrEmpty(dayaResult.ContractNumber) &&
(dayaResult.Status == DayaLoanStatus.PendingReceive || dayaResult.Status == DayaLoanStatus.Received))
{
await ProcessDayaLoanReceivedAsync(existingContract.UserId, dayaResult.ContractNumber, cancellationToken);
existingContract.IsProcessed = true;
_logger.LogInformation(
"Daya loan processed for User {UserId}, ContractNumber: {ContractNumber}, Amount: {Amount}",
existingContract.UserId, dayaResult.ContractNumber, SystemConstants.DayaLoanAmount);
}
}
else
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.NationalCode == dayaResult.NationalCode, cancellationToken);
if (user != null)
{
var isProcessed = false;
// اگر وضعیت PendingReceive یا Received بود و قرارداد دارد، فوری پردازش کن
if (!string.IsNullOrEmpty(dayaResult.ContractNumber) &&
(dayaResult.Status == DayaLoanStatus.PendingReceive || dayaResult.Status == DayaLoanStatus.Received))
{
await ProcessDayaLoanReceivedAsync(user.Id, dayaResult.ContractNumber, cancellationToken);
isProcessed = true;
_logger.LogInformation(
"Daya loan processed for new contract - User {UserId}, ContractNumber: {ContractNumber}, Amount: {Amount}",
user.Id, dayaResult.ContractNumber, SystemConstants.DayaLoanAmount);
}
var newContract = new DayaLoanContract
{
UserId = user.Id,
NationalCode = dayaResult.NationalCode,
Status = dayaResult.Status,
ContractNumber = dayaResult.ContractNumber,
LastCheckDate = DateTime.Now,
IsProcessed = isProcessed
};
await _context.DayaLoanContracts.AddAsync(newContract, cancellationToken);
}
}
await _context.SaveChangesAsync(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing Daya result for {NationalCode}", dayaResult.NationalCode);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calling Daya API service");
// در صورت خطا، نتایج خالی برمی‌گردانیم
foreach (var nationalCode in request.NationalCodes)
{
results.Add(new DayaLoanStatusItem
{
NationalCode = nationalCode,
Status = DayaLoanStatus.PendingReceive,
ContractNumber = null,
Message = $"خطا در استعلام: {ex.Message}"
});
}
}
return new CheckDayaLoanStatusResponseDto
{
Results = results,
TotalChecked = request.NationalCodes.Count,
SuccessCount = results.Count(r => r.ContractNumber != null)
};
}
/// <summary>
/// پردازش دریافت وام دایا برای کاربر
/// 1. شارژ کیف پول (Balance + DiscountBalance)
/// 2. ثبت تراکنش
/// 3. ثبت لاگ کیف پول
/// 4. به‌روزرسانی وضعیت کاربر
/// </summary>
private async Task ProcessDayaLoanReceivedAsync(long userId, string contractNumber, CancellationToken cancellationToken)
{
// پیدا کردن کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
if (user == null)
{
_logger.LogWarning("User {UserId} not found for Daya loan processing", userId);
return;
}
// بررسی اینکه قبلاً دریافت نکرده باشد
if (user.HasReceivedDayaCredit)
{
_logger.LogWarning("User {UserId} has already received Daya credit", userId);
return;
}
// پیدا کردن یا ایجاد کیف پول کاربر
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == userId, cancellationToken);
if (wallet == null)
{
wallet = new UserWallet
{
UserId = userId,
Balance = 0,
NetworkBalance = 0,
DiscountBalance = 0
};
await _context.UserWallets.AddAsync(wallet, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
// 1. شارژ کیف پول - هم موجودی اصلی و هم موجودی تخفیف
var previousBalance = wallet.Balance;
var previousDiscountBalance = wallet.DiscountBalance;
wallet.Balance += SystemConstants.DayaLoanAmount;
wallet.DiscountBalance += SystemConstants.DayaLoanAmount;
// 2. ثبت تراکنش - RefId = شماره قرارداد، Status = 0 (Success)، Type = 2 (DepositExternal1)
var transaction = new Transaction
{
Amount = SystemConstants.DayaLoanAmount,
Description = $"شارژ کیف پول از وام دایا - قرارداد {contractNumber}",
PaymentStatus = PaymentStatus.Success, // 0
PaymentDate = DateTime.Now,
RefId = contractNumber,
Type = TransactionType.DepositExternal1 // 2
};
await _context.Transactions.AddAsync(transaction, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// 3. ثبت لاگ کیف پول با شناسه تراکنش
var walletLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = SystemConstants.DayaLoanAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = SystemConstants.DayaLoanAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken);
// 4. به‌روزرسانی وضعیت کاربر
user.HasReceivedDayaCredit = true;
user.DayaCreditReceivedAt = DateTime.Now;
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan; // 1
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Daya loan fully processed for User {UserId}: " +
"Wallet Balance {PreviousBalance} -> {NewBalance}, " +
"DiscountBalance {PreviousDiscountBalance} -> {NewDiscountBalance}, " +
"Transaction Id: {TransactionId}",
userId, previousBalance, wallet.Balance,
previousDiscountBalance, wallet.DiscountBalance,
transaction.Id);
}
}
@@ -1,18 +0,0 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
public class CheckDayaLoanStatusResponseDto
{
public List<DayaLoanStatusItem> Results { get; set; }
public int TotalChecked { get; set; }
public int SuccessCount { get; set; }
}
public class DayaLoanStatusItem
{
public string NationalCode { get; set; }
public DayaLoanStatus Status { get; set; }
public string? ContractNumber { get; set; }
public string Message { get; set; }
}
@@ -1,32 +0,0 @@
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
/// <summary>
/// Command برای پردازش تایید وام دایا و شارژ کیف پول
/// </summary>
public record ProcessDayaLoanApprovalCommand : IRequest<ProcessDayaLoanApprovalResponseDto>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; init; }
/// <summary>
/// شماره قرارداد دایا
/// </summary>
public string ContractNumber { get; init; }
/// <summary>
/// مبلغ کیف پول عادی
/// </summary>
public long WalletAmount { get; init; } = SystemConstants.DayaLoanAmount;
/// <summary>
/// مبلغ کیف پول تخفیف (دو برابر)
/// </summary>
public long DiscountWalletAmount { get; init; } = SystemConstants.DayaLoanAmount * 2;
}
@@ -1,192 +0,0 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Application.Common.Interfaces;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDayaLoanApprovalCommand, ProcessDayaLoanApprovalResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IKavenegarService _smsService;
private readonly ILogger<ProcessDayaLoanApprovalCommandHandler> _logger;
public ProcessDayaLoanApprovalCommandHandler(
IApplicationDbContext context,
IKavenegarService smsService,
ILogger<ProcessDayaLoanApprovalCommandHandler> logger)
{
_context = context;
_smsService = smsService;
_logger = logger;
}
public async Task<ProcessDayaLoanApprovalResponseDto> Handle(ProcessDayaLoanApprovalCommand request, CancellationToken cancellationToken)
{
// پیدا کردن کاربر
var user = await _context.Users
.Include(u => u.UserWallets)
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
if (user == null)
{
throw new NotFoundException(nameof(User), request.UserId);
}
// چک کردن که قبلاً دریافت نکرده باشد
if (user.HasReceivedDayaCredit)
{
throw new InvalidOperationException($"کاربر {request.UserId} قبلاً اعتبار دایا را دریافت کرده است");
}
// ایجاد تراکنش با RefId = شماره قرارداد دایا
var transaction = new Transaction
{
Amount = request.WalletAmount , // 168 میلیون
Description = $"دریافت اعتبار دایا - قرارداد {request.ContractNumber}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.Now,
RefId = request.ContractNumber, // شماره قرارداد دایا
Type = TransactionType.DepositExternal1
};
await _context.Transactions.AddAsync(transaction, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// یافتن یا ایجاد کیف پول کاربر
var wallet = user.UserWallets.FirstOrDefault();
if (wallet == null)
{
wallet = new UserWallet
{
UserId = request.UserId,
Balance = 0,
NetworkBalance = 0,
DiscountBalance = 0
};
await _context.UserWallets.AddAsync(wallet, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
// شارژ کیف پول عادی (56 میلیون)
var balanceBeforeMain = wallet.Balance;
wallet.Balance += request.WalletAmount;
// لاگ کیف پول عادی
var mainLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = request.WalletAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(mainLog, cancellationToken);
// شارژ کیف پول تخفیف (56 میلیون)
var balanceBeforeDiscount = wallet.DiscountBalance;
wallet.DiscountBalance += request.DiscountWalletAmount;
// لاگ کیف پول تخفیف
var discountLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = 0,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = request.DiscountWalletAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken);
// به‌روزرسانی وضعیت کاربر
user.HasReceivedDayaCredit = true;
user.DayaCreditReceivedAt = DateTime.Now;
// تنظیم نحوه خرید پکیج به DayaLoan
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
// ثبت سفارش پکیج (فعلاً پکیج پایه)
var goldenPackage = await _context.Packages
.FirstOrDefaultAsync(p => p.Id==4, cancellationToken);
if (goldenPackage != null)
{
// پیدا کردن آدرس پیش‌فرض کاربر
var defaultAddress = await _context.UserAddresses
.Where(a => a.UserId == request.UserId)
.OrderByDescending(a => a.Created)
.FirstOrDefaultAsync(cancellationToken);
if (defaultAddress != null)
{
var packageOrder = new UserOrder
{
UserId = request.UserId,
PackageId = goldenPackage.Id,
Amount = request.WalletAmount, // 56 میلیون
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.Now,
DeliveryStatus = DeliveryStatus.None,
UserAddressId = defaultAddress.Id,
TransactionId = transaction.Id,
PaymentMethod = PaymentMethod.IPG
};
await _context.UserOrders.AddAsync(packageOrder, cancellationToken);
}
}
// ثبت Event
user.AddDomainEvent(new DayaLoanApprovedEvent(user, transaction, request.ContractNumber));
await _context.SaveChangesAsync(cancellationToken);
// ارسال SMS به کاربر
await SendDayaLoanSmsAsync(user);
return new ProcessDayaLoanApprovalResponseDto
{
UserId = user.Id,
TransactionId = transaction.Id,
ContractNumber = request.ContractNumber,
MainWalletBalance = wallet.Balance,
LockedWalletBalance = wallet.NetworkBalance,
DiscountWalletBalance = wallet.DiscountBalance,
Message = "اعتبار دایا با موفقیت دریافت شد"
};
}
/// <summary>
/// ارسال SMS اطلاع‌رسانی دریافت اعتبار دایا
/// </summary>
private async Task SendDayaLoanSmsAsync(User user)
{
try
{
var userName = SmsTemplates.GetUserName(user.FirstName);
var message = SmsTemplates.DayaLoanReceived(userName);
await _smsService.SendAsync(user.Mobile, message);
_logger.LogInformation(
"Daya loan SMS sent to User {UserId}, Mobile: {Mobile}",
user.Id, user.Mobile);
}
catch (Exception ex)
{
// خطای SMS نباید فرایند اصلی رو متوقف کنه
_logger.LogError(ex,
"Failed to send Daya loan SMS to User {UserId}, Mobile: {Mobile}",
user.Id, user.Mobile);
}
}
}
@@ -1,21 +0,0 @@
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
public class ProcessDayaLoanApprovalCommandValidator : AbstractValidator<ProcessDayaLoanApprovalCommand>
{
public ProcessDayaLoanApprovalCommandValidator()
{
RuleFor(v => v.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
RuleFor(v => v.ContractNumber)
.NotEmpty()
.WithMessage("شماره قرارداد الزامی است")
.MaximumLength(100)
.WithMessage("شماره قرارداد نباید بیش از 100 کاراکتر باشد");
RuleFor(v => v.WalletAmount)
.GreaterThan(0)
.WithMessage("مبلغ کیف پول باید بزرگتر از صفر باشد");
}
}
@@ -1,12 +0,0 @@
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
public class ProcessDayaLoanApprovalResponseDto
{
public long UserId { get; set; }
public long TransactionId { get; set; }
public string ContractNumber { get; set; }
public long MainWalletBalance { get; set; }
public long LockedWalletBalance { get; set; }
public long DiscountWalletBalance { get; set; }
public string Message { get; set; }
}
@@ -9,7 +9,7 @@ public static class SmsTemplates
/// پیام دریافت اعتبار دایا
/// </summary>
public static string DayaLoanReceived(string userName) =>
$"{userName} گرامی، اعتبار شما واریز گردید. شما می‌توانید با مراجعه به سامانه کارابازار فرایند فعال‌سازی خود را ادامه دهید. با تشکر از حسن انتخاب شما";
$"{userName} عزیز، تامین اعتبار شما انجام شد. شما می‌توانید با مراجعه به سامانه کارابازار فرایند فعال‌سازی خود را ادامه دهید. با تشکر از حسن انتخاب شما";
/// <summary>
/// پیام فعال‌سازی باشگاه مشتریان
@@ -64,8 +64,8 @@ public static class SmsTemplates
/// <summary>
/// دریافت نام کاربر یا مقدار پیش‌فرض
/// </summary>
public static string GetUserName(string? firstName) =>
!string.IsNullOrWhiteSpace(firstName) ? firstName : "کاربر";
public static string GetUserName(string? fullName) =>
!string.IsNullOrWhiteSpace(fullName) ? fullName : "کاربر";
#endregion
}
@@ -0,0 +1,162 @@
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Protobuf.Protos.Configuration;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Services;
/// <summary>
/// سرویس تنظیمات سیستم - خواندن از SystemConstants
/// </summary>
public class ConfigurationService : ConfigurationContract.ConfigurationContractBase
{
private readonly ILogger<ConfigurationService> _logger;
public ConfigurationService(ILogger<ConfigurationService> logger)
{
_logger = logger;
}
/// <summary>
/// دریافت تنظیمات با کلید خاص
/// </summary>
public override Task<GetConfigurationByKeyResponse> GetConfigurationByKey(GetConfigurationByKeyRequest request, ServerCallContext context)
{
var response = new GetConfigurationByKeyResponse
{
Key = request.Key,
Scope = request.Scope,
IsActive = true,
Created = Timestamp.FromDateTime(DateTime.UtcNow),
LastModified = Timestamp.FromDateTime(DateTime.UtcNow)
};
// خواندن مقدار از SystemConstants بر اساس کلید
response.Value = GetConfigurationValue(request.Key);
response.Description = GetConfigurationDescription(request.Key);
_logger.LogDebug("Configuration requested: Key={Key}, Value={Value}", request.Key, response.Value);
return Task.FromResult(response);
}
/// <summary>
/// دریافت تمام تنظیمات
/// </summary>
public override Task<GetAllConfigurationsResponse> GetAllConfigurations(GetAllConfigurationsRequest request, ServerCallContext context)
{
var response = new GetAllConfigurationsResponse();
// Club Settings
response.Models.Add(CreateConfigModel("Club.ActivationFee", SystemConstants.ClubActivationFee.ToString(), "هزینه فعال‌سازی عضویت باشگاه", 2));
response.Models.Add(CreateConfigModel("Club.MembershipGiftValue", SystemConstants.ClubMembershipGiftValue.ToString(), "مبلغ هدیه حق عضویت باشگاه", 2));
// Commission Settings
response.Models.Add(CreateConfigModel("Commission.MinWithdrawalAmount", SystemConstants.CommissionMinWithdrawalAmount.ToString(), "حداقل مبلغ برداشت", 3));
response.Models.Add(CreateConfigModel("Commission.MaxWeeklyBalancesPerLeg", SystemConstants.CommissionMaxWeeklyBalancesPerLeg.ToString(), "سقف تعادل هفتگی برای هر دست", 3));
response.Models.Add(CreateConfigModel("Commission.MaxNetworkLevel", SystemConstants.CommissionMaxNetworkLevel.ToString(), "حداکثر عمق شبکه برای محاسبه کمیسیون", 3));
response.Models.Add(CreateConfigModel("Commission.CashWithdrawalEnabled", SystemConstants.CommissionCashWithdrawalEnabled.ToString(), "امکان برداشت نقدی", 3));
response.Models.Add(CreateConfigModel("Commission.CalculationStrategy", SystemConstants.CommissionCalculationStrategy, "روش محاسبه کمیسیون", 3));
// Network Settings
response.Models.Add(CreateConfigModel("Network.AllowOrphanNodes", SystemConstants.NetworkAllowOrphanNodes.ToString(), "اجازه حذف والدین با فرزند", 1));
response.Models.Add(CreateConfigModel("Network.MaxChildrenPerLeg", SystemConstants.NetworkMaxChildrenPerLeg.ToString(), "حداکثر تعداد فرزند مستقیم در هر پا", 1));
// Package Settings
response.Models.Add(CreateConfigModel("Package.BasePackageAmount", SystemConstants.BasePackageAmount.ToString(), "مبلغ پکیج پایه", 0));
response.Models.Add(CreateConfigModel("Package.DayaLoanAmount", SystemConstants.DayaLoanAmount.ToString(), "مبلغ وام دایا", 0));
// System Settings
response.Models.Add(CreateConfigModel("System.MaintenanceMode", SystemConstants.SystemMaintenanceMode.ToString(), "حالت تعمیر و نگهداری", 0));
response.Models.Add(CreateConfigModel("System.EnableAuditLog", SystemConstants.SystemEnableAuditLog.ToString(), "فعال‌سازی لاگ تغییرات", 0));
// Shop Settings
response.Models.Add(CreateConfigModel("Shop.VAT", SystemConstants.ShopVAT.ToString(), "مالیات بر ارزش افزوده", 0));
return Task.FromResult(response);
}
/// <summary>
/// سایر عملیات‌ها که فعلاً پیاده‌سازی نشده‌اند (چون از constant استفاده می‌کنیم)
/// </summary>
public override Task<Empty> CreateOrUpdateConfiguration(CreateOrUpdateConfigurationRequest request, ServerCallContext context)
{
_logger.LogWarning("CreateOrUpdateConfiguration called but SystemConstants are read-only");
throw new RpcException(new Status(StatusCode.Unimplemented, "تنظیمات سیستم فقط خواندنی هستند"));
}
public override Task<Empty> DeactivateConfiguration(DeactivateConfigurationRequest request, ServerCallContext context)
{
_logger.LogWarning("DeactivateConfiguration called but SystemConstants are read-only");
throw new RpcException(new Status(StatusCode.Unimplemented, "تنظیمات سیستم فقط خواندنی هستند"));
}
public override Task<GetConfigurationHistoryResponse> GetConfigurationHistory(GetConfigurationHistoryRequest request, ServerCallContext context)
{
// چون constant هستند، تاریخچه‌ای وجود نداره
return Task.FromResult(new GetConfigurationHistoryResponse());
}
#region Private Helpers
private static string GetConfigurationValue(string key)
{
return key switch
{
"Club.ActivationFee" => SystemConstants.ClubActivationFee.ToString(),
"Club.MembershipGiftValue" => SystemConstants.ClubMembershipGiftValue.ToString(),
"Commission.MinWithdrawalAmount" => SystemConstants.CommissionMinWithdrawalAmount.ToString(),
"Commission.MaxWeeklyBalancesPerLeg" => SystemConstants.CommissionMaxWeeklyBalancesPerLeg.ToString(),
"Commission.MaxNetworkLevel" => SystemConstants.CommissionMaxNetworkLevel.ToString(),
"Commission.CashWithdrawalEnabled" => SystemConstants.CommissionCashWithdrawalEnabled.ToString(),
"Commission.CalculationStrategy" => SystemConstants.CommissionCalculationStrategy,
"Network.AllowOrphanNodes" => SystemConstants.NetworkAllowOrphanNodes.ToString(),
"Network.MaxChildrenPerLeg" => SystemConstants.NetworkMaxChildrenPerLeg.ToString(),
"Package.BasePackageAmount" => SystemConstants.BasePackageAmount.ToString(),
"Package.DayaLoanAmount" => SystemConstants.DayaLoanAmount.ToString(),
"System.MaintenanceMode" => SystemConstants.SystemMaintenanceMode.ToString(),
"System.EnableAuditLog" => SystemConstants.SystemEnableAuditLog.ToString(),
"Shop.VAT" => SystemConstants.ShopVAT.ToString(),
_ => string.Empty
};
}
private static string GetConfigurationDescription(string key)
{
return key switch
{
"Club.ActivationFee" => "هزینه فعال‌سازی عضویت باشگاه (ریال)",
"Club.MembershipGiftValue" => "مبلغ هدیه حق عضویت باشگاه (ریال)",
"Commission.MinWithdrawalAmount" => "حداقل مبلغ برداشت (ریال)",
"Commission.MaxWeeklyBalancesPerLeg" => "سقف تعادل هفتگی برای هر دست",
"Commission.MaxNetworkLevel" => "حداکثر عمق شبکه برای محاسبه کمیسیون",
"Commission.CashWithdrawalEnabled" => "امکان برداشت نقدی",
"Commission.CalculationStrategy" => "روش محاسبه کمیسیون",
"Network.AllowOrphanNodes" => "اجازه حذف والدین با فرزند",
"Network.MaxChildrenPerLeg" => "حداکثر تعداد فرزند مستقیم در هر پا",
"Package.BasePackageAmount" => "مبلغ پکیج پایه (ریال)",
"Package.DayaLoanAmount" => "مبلغ وام دایا (ریال)",
"System.MaintenanceMode" => "حالت تعمیر و نگهداری سیستم",
"System.EnableAuditLog" => "فعال‌سازی لاگ تغییرات",
"Shop.VAT" => "مالیات بر ارزش افزوده",
_ => string.Empty
};
}
private static ConfigurationModel CreateConfigModel(string key, string value, string description, int scope)
{
return new ConfigurationModel
{
Id = key.GetHashCode(),
Key = key,
Value = value,
Description = description,
Scope = scope,
IsActive = true,
Created = Timestamp.FromDateTime(DateTime.UtcNow)
};
}
#endregion
}
@@ -1,9 +1,7 @@
using Hangfire;
using MediatR;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
using CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoans;
using Microsoft.EntityFrameworkCore;
using CMSMicroservice.Infrastructure.Persistence;
using System.Linq;
@@ -11,7 +9,7 @@ using System.Linq;
namespace CMSMicroservice.WebApi.Workers;
/// <summary>
/// Worker برای استعلام خودکار وضعیت وام دایا (هر 15 دقیقه)
/// Worker برای استعلام خودکار وضعیت وام دایا و پردازش خودکار (هر 20 دقیقه)
/// </summary>
public class DayaLoanCheckWorker
{
@@ -39,18 +37,15 @@ public class DayaLoanCheckWorker
try
{
// پیدا کردن کاربرانی که:
// 1. اعتبار دایا را دریافت نکرده‌اند
// 2. کد ملی دارند
// 3. قبلاً شماره قرارداد نگرفته‌اند
var pendingUsers = await _context.Users
.Where(u =>
u.HasReceivedDayaCredit == false &&
u.NationalCode != null &&
u.NationalCode != ""
)
.Select(u => new { u.Id, u.NationalCode })
u.NationalCode != "")
.Select(u => u.NationalCode!)
.ToListAsync();
if (!pendingUsers.Any())
@@ -61,47 +56,19 @@ public class DayaLoanCheckWorker
_logger.LogInformation("Found {Count} users with pending Daya loan status", pendingUsers.Count);
// استعلام از دایا
var checkCommand = new CheckDayaLoanStatusCommand
// استعلام و پردازش یکجا با Command یکپارچه
var command = new CheckAndProcessDayaLoansCommand
{
NationalCodes = pendingUsers.Select(u => u.NationalCode).ToList()
NationalCodes = pendingUsers
};
var checkResult = await _mediator.Send(checkCommand);
var result = await _mediator.Send(command);
// پردازش نتایج
foreach (var result in checkResult.Results)
{
// فقط وضعیت PendingReceive را پردازش می‌کنیم (یعنی وام درخواست شده)
if (result.Status == DayaLoanStatus.PendingReceive && !string.IsNullOrEmpty(result.ContractNumber))
{
var user = pendingUsers.FirstOrDefault(u => u.NationalCode == result.NationalCode);
if (user != null)
{
try
{
// پردازش تایید وام و شارژ کیف پول
var processCommand = new ProcessDayaLoanApprovalCommand
{
UserId = user.Id,
ContractNumber = result.ContractNumber,
};
var processResult = await _mediator.Send(processCommand);
_logger.LogInformation("Daya loan processed for user {UserId}. Contract: {ContractNumber}",
user.Id, result.ContractNumber);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing Daya loan for user {UserId}", user.Id);
}
}
}
}
_logger.LogInformation("DayaLoanCheckWorker completed. Checked: {Total}, Processed: {Success}",
checkResult.TotalChecked, checkResult.SuccessCount);
_logger.LogInformation(
"DayaLoanCheckWorker completed. Checked: {Total}, WithContract: {WithContract}, Processed: {Processed}",
result.TotalChecked,
result.WithContractCount,
result.ProcessedCount);
}
catch (Exception ex)
{
@@ -111,15 +78,14 @@ public class DayaLoanCheckWorker
}
/// <summary>
/// متد برای Schedule کردن Worker (هر 15 دقیقه)
/// متد برای Schedule کردن Worker (هر 20 دقیقه)
/// </summary>
public static void Schedule(IRecurringJobManager recurringJobManager)
{
// هر 15 دقیقه: */15 * * * *
recurringJobManager.AddOrUpdate<DayaLoanCheckWorker>(
"daya-loan-check",
worker => worker.ExecuteAsync(),
"*/20 * * * *", // هر 15 دقیقه
"*/20 * * * *",
TimeZoneInfo.Local
);
}