feat: implement user wallet charging and transaction logging for Daya loan processing
This commit is contained in:
+138
-2
@@ -84,7 +84,10 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
|||||||
{
|
{
|
||||||
var oldPayoutIds = oldPayouts.Select(p => p.Id).ToList();
|
var oldPayoutIds = oldPayouts.Select(p => p.Id).ToList();
|
||||||
|
|
||||||
// ⭐ اول باید تاریخچهها حذف بشن (به خاطر FK constraint)
|
// ⭐ ۱. برگرداندن NetworkBalance به حالت قبل (Revert)
|
||||||
|
await RevertUserWalletsAsync(oldPayoutIds, cancellationToken);
|
||||||
|
|
||||||
|
// ⭐ ۲. حذف تاریخچهها (به خاطر FK constraint)
|
||||||
var oldHistories = await _context.CommissionPayoutHistories
|
var oldHistories = await _context.CommissionPayoutHistories
|
||||||
.Where(h => oldPayoutIds.Contains(h.UserCommissionPayoutId))
|
.Where(h => oldPayoutIds.Contains(h.UserCommissionPayoutId))
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
@@ -94,7 +97,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
|||||||
_context.CommissionPayoutHistories.RemoveRange(oldHistories);
|
_context.CommissionPayoutHistories.RemoveRange(oldHistories);
|
||||||
}
|
}
|
||||||
|
|
||||||
// بعد پرداختها حذف میشن
|
// ⭐ ۳. حذف پرداختها
|
||||||
_context.UserCommissionPayouts.RemoveRange(oldPayouts);
|
_context.UserCommissionPayouts.RemoveRange(oldPayouts);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -164,8 +167,141 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
|||||||
|
|
||||||
await _context.CommissionPayoutHistories.AddRangeAsync(historyList, cancellationToken);
|
await _context.CommissionPayoutHistories.AddRangeAsync(historyList, cancellationToken);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// ⭐ شارژ کیف پول کاربران (NetworkBalance) و ثبت لاگ
|
||||||
|
await ChargeUserWalletsAsync(payouts, request.WeekDefinitionId, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return existingPool.Id;
|
return existingPool.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شارژ کیف پول کاربران با مبلغ کمیسیون (به NetworkBalance اضافه میشود)
|
||||||
|
/// و ثبت لاگ تغییرات کیف پول
|
||||||
|
/// </summary>
|
||||||
|
private async Task ChargeUserWalletsAsync(
|
||||||
|
List<UserCommissionPayout> payouts,
|
||||||
|
long weekDefinitionId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userIds = payouts.Select(p => p.UserId).Distinct().ToList();
|
||||||
|
|
||||||
|
// دریافت کیف پولهای موجود
|
||||||
|
var existingWallets = await _context.UserWallets
|
||||||
|
.Where(w => userIds.Contains(w.UserId))
|
||||||
|
.ToDictionaryAsync(w => w.UserId, cancellationToken);
|
||||||
|
|
||||||
|
var newWallets = new List<UserWallet>();
|
||||||
|
var walletLogs = new List<UserWalletChangeLog>();
|
||||||
|
|
||||||
|
foreach (var payout in payouts)
|
||||||
|
{
|
||||||
|
if (payout.TotalAmount <= 0) continue;
|
||||||
|
|
||||||
|
UserWallet wallet;
|
||||||
|
|
||||||
|
// پیدا کردن یا ایجاد کیف پول
|
||||||
|
if (existingWallets.TryGetValue(payout.UserId, out var existingWallet))
|
||||||
|
{
|
||||||
|
wallet = existingWallet;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ایجاد کیف پول جدید
|
||||||
|
wallet = new UserWallet
|
||||||
|
{
|
||||||
|
UserId = payout.UserId,
|
||||||
|
Balance = 0,
|
||||||
|
NetworkBalance = 0,
|
||||||
|
DiscountBalance = 0
|
||||||
|
};
|
||||||
|
newWallets.Add(wallet);
|
||||||
|
existingWallets[payout.UserId] = wallet; // برای جلوگیری از تکرار
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ذخیره کیف پولهای جدید
|
||||||
|
if (newWallets.Any())
|
||||||
|
{
|
||||||
|
await _context.UserWallets.AddRangeAsync(newWallets, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// شارژ کیف پولها و ثبت لاگ
|
||||||
|
foreach (var payout in payouts)
|
||||||
|
{
|
||||||
|
if (payout.TotalAmount <= 0) continue;
|
||||||
|
|
||||||
|
var wallet = existingWallets[payout.UserId];
|
||||||
|
|
||||||
|
// ذخیره موجودی قبلی برای لاگ
|
||||||
|
var previousNetworkBalance = wallet.NetworkBalance;
|
||||||
|
|
||||||
|
// شارژ NetworkBalance
|
||||||
|
wallet.NetworkBalance += payout.TotalAmount;
|
||||||
|
|
||||||
|
// ایجاد لاگ تغییر کیف پول
|
||||||
|
var walletLog = new UserWalletChangeLog
|
||||||
|
{
|
||||||
|
WalletId = wallet.Id,
|
||||||
|
CurrentBalance = wallet.Balance,
|
||||||
|
ChangeValue = 0, // Balance اصلی تغییر نمیکند
|
||||||
|
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||||
|
ChangeNerworkValue = payout.TotalAmount,
|
||||||
|
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||||
|
ChangeDiscountValue = 0,
|
||||||
|
IsIncrease = true,
|
||||||
|
RefrenceId = payout.Id // ارجاع به UserCommissionPayout
|
||||||
|
};
|
||||||
|
|
||||||
|
walletLogs.Add(walletLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ذخیره تغییرات کیف پول و لاگها
|
||||||
|
await _context.UserWalletChangeLogs.AddRangeAsync(walletLogs, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// برگرداندن NetworkBalance به حالت قبل از شارژ کمیسیون
|
||||||
|
/// این متد قبل از محاسبه مجدد (ForceRecalculate) اجرا میشود
|
||||||
|
/// </summary>
|
||||||
|
private async Task RevertUserWalletsAsync(
|
||||||
|
List<long> oldPayoutIds,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// پیدا کردن لاگهای کیف پول مرتبط با پرداختهای قبلی
|
||||||
|
var oldWalletLogs = await _context.UserWalletChangeLogs
|
||||||
|
.Where(l => l.RefrenceId.HasValue && oldPayoutIds.Contains(l.RefrenceId.Value))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!oldWalletLogs.Any()) return;
|
||||||
|
|
||||||
|
// پیدا کردن کیف پولهای مرتبط
|
||||||
|
var walletIds = oldWalletLogs.Select(l => l.WalletId).Distinct().ToList();
|
||||||
|
var wallets = await _context.UserWallets
|
||||||
|
.Where(w => walletIds.Contains(w.Id))
|
||||||
|
.ToDictionaryAsync(w => w.Id, cancellationToken);
|
||||||
|
|
||||||
|
// برگرداندن NetworkBalance
|
||||||
|
foreach (var log in oldWalletLogs)
|
||||||
|
{
|
||||||
|
if (wallets.TryGetValue(log.WalletId, out var wallet))
|
||||||
|
{
|
||||||
|
// کم کردن مبلغ شارژ شده قبلی از NetworkBalance
|
||||||
|
wallet.NetworkBalance -= log.ChangeNerworkValue;
|
||||||
|
|
||||||
|
// اطمینان از منفی نشدن
|
||||||
|
if (wallet.NetworkBalance < 0)
|
||||||
|
{
|
||||||
|
wallet.NetworkBalance = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// حذف لاگهای قبلی
|
||||||
|
_context.UserWalletChangeLogs.RemoveRange(oldWalletLogs);
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-1
@@ -10,6 +10,11 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
|
|||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IDayaLoanApiService _dayaApiService;
|
private readonly IDayaLoanApiService _dayaApiService;
|
||||||
private readonly ILogger<CheckDayaLoanStatusCommandHandler> _logger;
|
private readonly ILogger<CheckDayaLoanStatusCommandHandler> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مبلغ وام دایا - 56 میلیون ریال
|
||||||
|
/// </summary>
|
||||||
|
private const long DAYA_LOAN_AMOUNT = 56_000_000;
|
||||||
|
|
||||||
public CheckDayaLoanStatusCommandHandler(
|
public CheckDayaLoanStatusCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
@@ -48,9 +53,24 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
|
|||||||
|
|
||||||
if (existingContract != null)
|
if (existingContract != null)
|
||||||
{
|
{
|
||||||
|
var previousStatus = existingContract.Status;
|
||||||
existingContract.LastCheckDate = DateTime.Now;
|
existingContract.LastCheckDate = DateTime.Now;
|
||||||
existingContract.Status = dayaResult.Status;
|
existingContract.Status = dayaResult.Status;
|
||||||
existingContract.ContractNumber = dayaResult.ContractNumber;
|
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, DAYA_LOAN_AMOUNT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -59,6 +79,20 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
|
|||||||
|
|
||||||
if (user != null)
|
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, DAYA_LOAN_AMOUNT);
|
||||||
|
}
|
||||||
|
|
||||||
var newContract = new DayaLoanContract
|
var newContract = new DayaLoanContract
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
@@ -66,7 +100,7 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
|
|||||||
Status = dayaResult.Status,
|
Status = dayaResult.Status,
|
||||||
ContractNumber = dayaResult.ContractNumber,
|
ContractNumber = dayaResult.ContractNumber,
|
||||||
LastCheckDate = DateTime.Now,
|
LastCheckDate = DateTime.Now,
|
||||||
IsProcessed = false
|
IsProcessed = isProcessed
|
||||||
};
|
};
|
||||||
|
|
||||||
await _context.DayaLoanContracts.AddAsync(newContract, cancellationToken);
|
await _context.DayaLoanContracts.AddAsync(newContract, cancellationToken);
|
||||||
@@ -105,4 +139,101 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
|
|||||||
SuccessCount = results.Count(r => r.ContractNumber != null)
|
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 += DAYA_LOAN_AMOUNT;
|
||||||
|
wallet.DiscountBalance += DAYA_LOAN_AMOUNT;
|
||||||
|
|
||||||
|
// 2. ثبت تراکنش - RefId = شماره قرارداد، Status = 0 (Success)، Type = 2 (DepositExternal1)
|
||||||
|
var transaction = new Transaction
|
||||||
|
{
|
||||||
|
Amount = DAYA_LOAN_AMOUNT,
|
||||||
|
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 = DAYA_LOAN_AMOUNT,
|
||||||
|
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||||
|
ChangeNerworkValue = 0,
|
||||||
|
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||||
|
ChangeDiscountValue = DAYA_LOAN_AMOUNT,
|
||||||
|
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
-1
@@ -23,5 +23,5 @@ public record GetNetworkTreeQuery : IRequest<NetworkTreeDto?>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// فیلتر براساس شماره هفته فعالسازی (مثال: 2025-W05)
|
/// فیلتر براساس شماره هفته فعالسازی (مثال: 2025-W05)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ActivationWeekNumber { get; init; }
|
public long? ActivationWeekDefinitionId { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-18
@@ -42,19 +42,21 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
|||||||
}
|
}
|
||||||
|
|
||||||
// محاسبه شماره هفته فعالسازی
|
// محاسبه شماره هفته فعالسازی
|
||||||
string? activationWeekNumber = null;
|
long? activationWeekDefinitionId = null;
|
||||||
string? activationWeekDisplayName = null;
|
string? activationWeekDisplayName = null;
|
||||||
bool isActivatedInTargetWeek = false;
|
bool isActivatedInTargetWeek = false;
|
||||||
|
|
||||||
if (user.ClubMembership?.ActivatedAt != null)
|
if (user.ClubMembership?.ActivatedAt != null)
|
||||||
{
|
{
|
||||||
activationWeekNumber = CalculateWeekNumber(user.ClubMembership.ActivatedAt.Value);
|
// activationWeekDefinitionId = CalculateWeekNumber(user.ClubMembership.ActivatedAt.Value);
|
||||||
activationWeekDisplayName = _weekDefinitionRepository.GetDisplayNameByGregorianWeekNumber(activationWeekNumber);
|
var week = _weekDefinitionRepository.GetWeekByDate(user.ClubMembership.ActivatedAt.Value);
|
||||||
|
activationWeekDefinitionId = week.Id;
|
||||||
|
activationWeekDisplayName = week.DisplayName;
|
||||||
|
|
||||||
// بررسی آیا در هفته هدف فعال شده است
|
// بررسی آیا در هفته هدف فعال شده است
|
||||||
if (!string.IsNullOrEmpty(request.ActivationWeekNumber))
|
if (request.ActivationWeekDefinitionId!=null)
|
||||||
{
|
{
|
||||||
isActivatedInTargetWeek = activationWeekNumber == request.ActivationWeekNumber;
|
isActivatedInTargetWeek = activationWeekDefinitionId == request.ActivationWeekDefinitionId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
|||||||
CurrentDepth = currentDepth,
|
CurrentDepth = currentDepth,
|
||||||
ClubActivatedAt = user.ClubMembership?.ActivatedAt,
|
ClubActivatedAt = user.ClubMembership?.ActivatedAt,
|
||||||
IsClubActive = user.ClubMembership?.IsActive ?? false,
|
IsClubActive = user.ClubMembership?.IsActive ?? false,
|
||||||
ActivationWeekNumber = activationWeekNumber,
|
ActivationWeekDefinitionId = activationWeekDefinitionId,
|
||||||
ActivationWeekDisplayName = activationWeekDisplayName,
|
ActivationWeekDisplayName = activationWeekDisplayName,
|
||||||
IsActivatedInTargetWeek = isActivatedInTargetWeek,
|
IsActivatedInTargetWeek = isActivatedInTargetWeek,
|
||||||
UserCreated = user.Created
|
UserCreated = user.Created
|
||||||
@@ -123,18 +125,19 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// محاسبه شماره هفته از تاریخ
|
/// محاسبه شماره هفته از تاریخ
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string CalculateWeekNumber(DateTime date)
|
// private long CalculateWeekNumber(DateTime date)
|
||||||
{
|
// {
|
||||||
// First try to get from repository cache
|
// // First try to get from repository cache
|
||||||
var weekDef = _weekDefinitionRepository.GetWeekByDate(date);
|
// var weekDef = _weekDefinitionRepository.GetWeekByDate(date);
|
||||||
if (weekDef != null)
|
// if (weekDef != null)
|
||||||
{
|
// {
|
||||||
return $"{weekDef.GregorianYear}-W{weekDef.GregorianWeekNumber:D2}";
|
// return weekDef.Id;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Fallback: use repository's calculation method
|
// // Fallback: use repository's calculation method
|
||||||
return _weekDefinitionRepository.CalculateGregorianWeekNumber(date);
|
// // return _weekDefinitionRepository.CalculateGregorianWeekNumber(date);
|
||||||
}
|
// return 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ public class NetworkTreeDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// شماره هفته فعالسازی
|
/// شماره هفته فعالسازی
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ActivationWeekNumber { get; set; }
|
public long? ActivationWeekDefinitionId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// نام نمایشی هفته فعالسازی (مثل: هفته یکم)
|
/// نام نمایشی هفته فعالسازی (مثل: هفته یکم)
|
||||||
|
|||||||
+53
-24
@@ -10,6 +10,7 @@ public class
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly ILogger<SubmitShopBuyOrderCommandHandler> _logger;
|
private readonly ILogger<SubmitShopBuyOrderCommandHandler> _logger;
|
||||||
|
private float _vatRate;
|
||||||
|
|
||||||
public SubmitShopBuyOrderCommandHandler(
|
public SubmitShopBuyOrderCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
@@ -35,7 +36,7 @@ public class
|
|||||||
// چک موجودی محصولات
|
// چک موجودی محصولات
|
||||||
var outOfStockProducts = new List<string>();
|
var outOfStockProducts = new List<string>();
|
||||||
var insufficientStockProducts = new List<(string Title, int Requested, int Available)>();
|
var insufficientStockProducts = new List<(string Title, int Requested, int Available)>();
|
||||||
|
|
||||||
foreach (var cartItem in user.UserCarts)
|
foreach (var cartItem in user.UserCarts)
|
||||||
{
|
{
|
||||||
if (cartItem.Product.RemainingCount <= 0)
|
if (cartItem.Product.RemainingCount <= 0)
|
||||||
@@ -44,33 +45,55 @@ public class
|
|||||||
}
|
}
|
||||||
else if (cartItem.Product.RemainingCount < cartItem.Count)
|
else if (cartItem.Product.RemainingCount < cartItem.Count)
|
||||||
{
|
{
|
||||||
insufficientStockProducts.Add((cartItem.Product.Title, cartItem.Count, cartItem.Product.RemainingCount));
|
insufficientStockProducts.Add((cartItem.Product.Title, cartItem.Count,
|
||||||
|
cartItem.Product.RemainingCount));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outOfStockProducts.Any())
|
if (outOfStockProducts.Any())
|
||||||
{
|
{
|
||||||
throw new Exception($"محصولات زیر ناموجود شدهاند: {string.Join("، ", outOfStockProducts)}");
|
throw new Exception($"محصولات زیر ناموجود شدهاند: {string.Join("، ", outOfStockProducts)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (insufficientStockProducts.Any())
|
if (insufficientStockProducts.Any())
|
||||||
{
|
{
|
||||||
var messages = insufficientStockProducts.Select(p =>
|
var messages = insufficientStockProducts.Select(p =>
|
||||||
$"«{p.Title}»: درخواست {p.Requested} عدد، موجودی {p.Available} عدد");
|
$"«{p.Title}»: درخواست {p.Requested} عدد، موجودی {p.Available} عدد");
|
||||||
throw new Exception($"موجودی محصولات زیر کافی نیست: {string.Join(" | ", messages)}");
|
throw new Exception($"موجودی محصولات زیر کافی نیست: {string.Join(" | ", messages)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.UserCarts.Sum(s => s.Count * s.Product.Price) != request.TotalAmount)
|
long finalAmount = 0;
|
||||||
|
var vatIsEnable =
|
||||||
|
_context.SystemConfigurations
|
||||||
|
.FirstOrDefault(f => f.Scope == ConfigurationScope.VAT && f.Key == "IsEnabled")?.Value ??
|
||||||
|
"0";
|
||||||
|
if (vatIsEnable=="1")
|
||||||
|
{
|
||||||
|
_vatRate = float.Parse(
|
||||||
|
_context.SystemConfigurations
|
||||||
|
.FirstOrDefault(f => f.Scope == ConfigurationScope.VAT && f.Key == "Shop.VAT")?.Value ??
|
||||||
|
throw new InvalidOperationException());
|
||||||
|
finalAmount = AddVAT(user.UserCarts.Sum(s => s.Count * s.Product.Price));
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Calculating final amount with VAT. Base Amount: {BaseAmount}, VAT Rate: {VATRate}, Final Amount: {FinalAmount}",
|
||||||
|
user.UserCarts.Sum(s => s.Count * s.Product.Price), _vatRate, finalAmount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
finalAmount = user.UserCarts.Sum(s => s.Count * s.Product.Price);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalAmount != request.TotalAmount)
|
||||||
throw new Exception("مبلغ سفارش با مجموع سبد خرید مطابقت ندارد.");
|
throw new Exception("مبلغ سفارش با مجموع سبد خرید مطابقت ندارد.");
|
||||||
|
|
||||||
|
|
||||||
var userWallet = user.UserWallets.FirstOrDefault();
|
var userWallet = user.UserWallets.FirstOrDefault();
|
||||||
if (userWallet == null)
|
if (userWallet == null)
|
||||||
throw new Exception("کیف پول کاربر یافت نشد.");
|
throw new Exception("کیف پول کاربر یافت نشد.");
|
||||||
|
|
||||||
if (userWallet.Balance<=0 || userWallet.Balance<request.TotalAmount)
|
if (userWallet.Balance <= 0 || userWallet.Balance < request.TotalAmount)
|
||||||
throw new Exception("موجودی کیف پول کاربر برای انجام این تراکنش کافی نیست.");
|
throw new Exception("موجودی کیف پول کاربر برای انجام این تراکنش کافی نیست.");
|
||||||
|
|
||||||
var newTransaction = new Transaction()
|
var newTransaction = new Transaction()
|
||||||
{
|
{
|
||||||
Amount = request.TotalAmount,
|
Amount = request.TotalAmount,
|
||||||
@@ -82,18 +105,18 @@ public class
|
|||||||
};
|
};
|
||||||
await _context.Transactions.AddAsync(newTransaction, cancellationToken);
|
await _context.Transactions.AddAsync(newTransaction, cancellationToken);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
var newWalletLog = new UserWalletChangeLog()
|
var newWalletLog = new UserWalletChangeLog()
|
||||||
{
|
{
|
||||||
CurrentBalance = userWallet.Balance,
|
CurrentBalance = userWallet.Balance,
|
||||||
CurrentNetworkBalance = userWallet.NetworkBalance,
|
CurrentNetworkBalance = userWallet.NetworkBalance,
|
||||||
WalletId = userWallet.Id,
|
WalletId = userWallet.Id,
|
||||||
ChangeValue = -1*request.TotalAmount,
|
ChangeValue = -1 * request.TotalAmount,
|
||||||
IsIncrease = false,
|
IsIncrease = false,
|
||||||
RefrenceId = newTransaction.Id,
|
RefrenceId = newTransaction.Id,
|
||||||
};
|
};
|
||||||
userWallet.Balance -= request.TotalAmount;
|
userWallet.Balance -= request.TotalAmount;
|
||||||
|
|
||||||
await _context.UserWalletChangeLogs.AddAsync(newWalletLog, cancellationToken);
|
await _context.UserWalletChangeLogs.AddAsync(newWalletLog, cancellationToken);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -111,7 +134,7 @@ public class
|
|||||||
};
|
};
|
||||||
await _context.UserOrders.AddAsync(newOrder, cancellationToken);
|
await _context.UserOrders.AddAsync(newOrder, cancellationToken);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// محاسبه و ثبت VAT (اگر فعال باشد)
|
// محاسبه و ثبت VAT (اگر فعال باشد)
|
||||||
var vatCreated = await CalculateAndSaveVAT(newOrder.Id, request.TotalAmount, cancellationToken);
|
var vatCreated = await CalculateAndSaveVAT(newOrder.Id, request.TotalAmount, cancellationToken);
|
||||||
if (vatCreated)
|
if (vatCreated)
|
||||||
@@ -119,37 +142,43 @@ public class
|
|||||||
newOrder.HasVAT = true;
|
newOrder.HasVAT = true;
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
var factorDetailsList = user.UserCarts.Select(s => new FactorDetails()
|
var factorDetailsList = user.UserCarts.Select(s => new FactorDetails()
|
||||||
{
|
{
|
||||||
ProductId = s.ProductId,
|
ProductId = s.ProductId,
|
||||||
Count = s.Count,
|
Count = s.Count,
|
||||||
UnitPrice = s.Product.Price,
|
UnitPrice =vatIsEnable=="1" ?AddVAT(s.Product.Price): s.Product.Price,
|
||||||
OrderId = newOrder.Id
|
OrderId = newOrder.Id
|
||||||
});
|
});
|
||||||
await _context.FactorDetails.AddRangeAsync(factorDetailsList, cancellationToken);
|
await _context.FactorDetails.AddRangeAsync(factorDetailsList, cancellationToken);
|
||||||
|
|
||||||
// کاهش موجودی محصولات و افزایش تعداد فروش
|
// کاهش موجودی محصولات و افزایش تعداد فروش
|
||||||
foreach (var cartItem in user.UserCarts)
|
foreach (var cartItem in user.UserCarts)
|
||||||
{
|
{
|
||||||
cartItem.Product.RemainingCount -= cartItem.Count;
|
cartItem.Product.RemainingCount -= cartItem.Count;
|
||||||
cartItem.Product.SaleCount += cartItem.Count;
|
cartItem.Product.SaleCount += cartItem.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
user.UserCarts.Clear();
|
user.UserCarts.Clear();
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("Order {OrderId} completed. Stock updated for {ProductCount} products.",
|
_logger.LogInformation("Order {OrderId} completed. Stock updated for {ProductCount} products.",
|
||||||
newOrder.Id, factorDetailsList.Count());
|
newOrder.Id, factorDetailsList.Count());
|
||||||
|
|
||||||
var finalResult = new SubmitShopBuyOrderResponseDto()
|
var finalResult = new SubmitShopBuyOrderResponseDto()
|
||||||
{
|
{
|
||||||
Id = newOrder.Id,
|
Id = newOrder.Id,
|
||||||
|
|
||||||
};
|
};
|
||||||
return finalResult;
|
return finalResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long CalculateVAT(long amount) => (long)(amount * _vatRate);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// محاسبه قیمت با احتساب مالیات
|
||||||
|
/// </summary>
|
||||||
|
public long AddVAT(long amount) => amount + CalculateVAT(amount);
|
||||||
|
|
||||||
private async Task<bool> CalculateAndSaveVAT(long orderId, long orderAmount, CancellationToken cancellationToken)
|
private async Task<bool> CalculateAndSaveVAT(long orderId, long orderAmount, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -166,7 +195,7 @@ public class
|
|||||||
|
|
||||||
// دریافت نرخ VAT
|
// دریافت نرخ VAT
|
||||||
var vatRateConfig = await _context.SystemConfigurations
|
var vatRateConfig = await _context.SystemConfigurations
|
||||||
.FirstOrDefaultAsync(x => x.Scope == ConfigurationScope.VAT && x.Key == "Rate", cancellationToken);
|
.FirstOrDefaultAsync(x => x.Scope == ConfigurationScope.VAT && x.Key == "Shop.VAT", cancellationToken);
|
||||||
|
|
||||||
if (vatRateConfig == null || !decimal.TryParse(vatRateConfig.Value, out var vatRate))
|
if (vatRateConfig == null || !decimal.TryParse(vatRateConfig.Value, out var vatRate))
|
||||||
{
|
{
|
||||||
@@ -201,7 +230,7 @@ public class
|
|||||||
vatAmount,
|
vatAmount,
|
||||||
totalAmount
|
totalAmount
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -211,4 +240,4 @@ public class
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,6 +347,7 @@ message WeeklyCommissionPoolModel
|
|||||||
{
|
{
|
||||||
int64 id = 1;
|
int64 id = 1;
|
||||||
int64 week_definition_id = 2;
|
int64 week_definition_id = 2;
|
||||||
|
string week_display_name = 3;
|
||||||
int64 total_pool_amount = 4;
|
int64 total_pool_amount = 4;
|
||||||
int32 total_balances = 5;
|
int32 total_balances = 5;
|
||||||
int64 value_per_balance = 6;
|
int64 value_per_balance = 6;
|
||||||
@@ -433,6 +434,7 @@ message WorkerExecutionLogModel
|
|||||||
{
|
{
|
||||||
string execution_id = 1;
|
string execution_id = 1;
|
||||||
int64 week_definition_id = 2;
|
int64 week_definition_id = 2;
|
||||||
|
string week_display_name = 3;
|
||||||
string step = 4; // "Balances" | "Pool" | "Payouts" | "Full"
|
string step = 4; // "Balances" | "Pool" | "Payouts" | "Full"
|
||||||
bool success = 5;
|
bool success = 5;
|
||||||
google.protobuf.StringValue error_message = 6;
|
google.protobuf.StringValue error_message = 6;
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ message GetNetworkTreeRequest
|
|||||||
google.protobuf.Int32Value max_depth = 2;
|
google.protobuf.Int32Value max_depth = 2;
|
||||||
google.protobuf.BoolValue only_active = 3;
|
google.protobuf.BoolValue only_active = 3;
|
||||||
google.protobuf.BoolValue is_club_active = 4;
|
google.protobuf.BoolValue is_club_active = 4;
|
||||||
google.protobuf.StringValue activation_week_number = 5;
|
google.protobuf.StringValue activation_week_definition_id = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetNetworkTreeResponse
|
message GetNetworkTreeResponse
|
||||||
@@ -175,7 +175,7 @@ message NetworkTreeNodeModel
|
|||||||
google.protobuf.Timestamp joined_at = 7;
|
google.protobuf.Timestamp joined_at = 7;
|
||||||
google.protobuf.Timestamp club_activated_at = 8; // تاریخ فعالسازی در باشگاه
|
google.protobuf.Timestamp club_activated_at = 8; // تاریخ فعالسازی در باشگاه
|
||||||
bool is_club_active = 9; // فعال بودن در باشگاه
|
bool is_club_active = 9; // فعال بودن در باشگاه
|
||||||
string activation_week_number = 10; // شماره هفته فعالسازی
|
google.protobuf.Int64Value activation_week_definition_id = 10; // شماره هفته فعالسازی
|
||||||
bool is_activated_in_target_week = 11; // آیا در هفته هدف فعال شده
|
bool is_activated_in_target_week = 11; // آیا در هفته هدف فعال شده
|
||||||
google.protobuf.Timestamp user_created = 12; // تاریخ ایجاد کاربر
|
google.protobuf.Timestamp user_created = 12; // تاریخ ایجاد کاربر
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class NetworkMembershipProfile : IRegister
|
|||||||
.Map(dest => dest.UserId, src => src.UserId)
|
.Map(dest => dest.UserId, src => src.UserId)
|
||||||
.Map(dest => dest.MaxDepth, src => src.MaxDepth != null && src.MaxDepth.Value > 0 ? src.MaxDepth.Value : 3)
|
.Map(dest => dest.MaxDepth, src => src.MaxDepth != null && src.MaxDepth.Value > 0 ? src.MaxDepth.Value : 3)
|
||||||
.Map(dest => dest.IsClubActive, src => src.IsClubActive)
|
.Map(dest => dest.IsClubActive, src => src.IsClubActive)
|
||||||
.Map(dest => dest.ActivationWeekNumber, src => src.ActivationWeekNumber);
|
.Map(dest => dest.ActivationWeekDefinitionId, src => src.ActivationWeekDefinitionId);
|
||||||
|
|
||||||
// Response mapping: تبدیل درخت به لیست مسطح
|
// Response mapping: تبدیل درخت به لیست مسطح
|
||||||
config.NewConfig<NetworkTreeDto, GetNetworkTreeResponse>()
|
config.NewConfig<NetworkTreeDto, GetNetworkTreeResponse>()
|
||||||
@@ -85,7 +85,7 @@ public class NetworkMembershipProfile : IRegister
|
|||||||
NetworkLevel = node.CurrentDepth,
|
NetworkLevel = node.CurrentDepth,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsClubActive = node.IsClubActive,
|
IsClubActive = node.IsClubActive,
|
||||||
ActivationWeekNumber = node.ActivationWeekNumber ?? string.Empty,
|
ActivationWeekDefinitionId = node.ActivationWeekDefinitionId ?? 0,
|
||||||
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek
|
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user