feat: implement commission calculation strategy with ORM and SP options
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m43s
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m43s
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services.Commission;
|
||||
|
||||
/// <summary>
|
||||
/// Factory برای ایجاد Strategy مناسب براساس Config
|
||||
/// </summary>
|
||||
public class CommissionCalculationStrategyFactory : ICommissionCalculationStrategyFactory
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekRepository;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// کلید Config برای انتخاب استراتژی
|
||||
/// مقدار: "ORM" یا "SP"
|
||||
/// پیشفرض: "ORM"
|
||||
/// </summary>
|
||||
private const string ConfigKey = "Commission.CalculationStrategy";
|
||||
|
||||
public CommissionCalculationStrategyFactory(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekRepository,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_context = context;
|
||||
_weekRepository = weekRepository;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ICommissionCalculationStrategy> CreateStrategyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// خواندن Config از دیتابیس
|
||||
var config = await _context.SystemConfigurations
|
||||
.FirstOrDefaultAsync(x => x.Key == ConfigKey && x.IsActive, cancellationToken);
|
||||
|
||||
var strategyValue = config?.Value?.ToUpperInvariant() ?? "ORM";
|
||||
|
||||
var strategyType = strategyValue switch
|
||||
{
|
||||
"SP" or "STOREDPROCEDURE" or "STORED_PROCEDURE" => CommissionCalculationStrategyType.StoredProcedure,
|
||||
_ => CommissionCalculationStrategyType.Orm
|
||||
};
|
||||
|
||||
return CreateStrategy(strategyType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICommissionCalculationStrategy CreateStrategy(CommissionCalculationStrategyType strategyType)
|
||||
{
|
||||
return strategyType switch
|
||||
{
|
||||
CommissionCalculationStrategyType.StoredProcedure =>
|
||||
new StoredProcedureCommissionCalculationStrategy(_context),
|
||||
|
||||
_ => // Orm is default
|
||||
new OrmCommissionCalculationStrategy(_context, _weekRepository)
|
||||
};
|
||||
}
|
||||
}
|
||||
+538
@@ -0,0 +1,538 @@
|
||||
using System.Collections.Generic;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Club;
|
||||
using CMSMicroservice.Domain.Entities.Commission;
|
||||
using CMSMicroservice.Domain.Entities.Network;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services.Commission;
|
||||
|
||||
/// <summary>
|
||||
/// پیادهسازی محاسبه کمیسیون با استفاده از Entity Framework (ORM)
|
||||
/// این روش برای دادههای کم مناسب است و دیباگ و تست راحتتری دارد
|
||||
/// </summary>
|
||||
public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IWeekDefinitionRepository _weekRepository;
|
||||
|
||||
public OrmCommissionCalculationStrategy(
|
||||
IApplicationDbContext context,
|
||||
IWeekDefinitionRepository weekRepository)
|
||||
{
|
||||
_context = context;
|
||||
_weekRepository = weekRepository;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> CalculateWeeklyBalancesAsync(
|
||||
long weekDefinitionId,
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var regorianWeekNumber = _weekRepository.GetGregorianWeekNumber(weekDefinitionId);
|
||||
if (regorianWeekNumber == null)
|
||||
{
|
||||
throw new InvalidOperationException($"هفته {weekDefinitionId} در سیستم تعریف نشده است");
|
||||
}
|
||||
|
||||
// بررسی وجود محاسبه قبلی
|
||||
var existingBalances = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekDefinitionId == weekDefinitionId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (existingBalances.Any() && !forceRecalculate)
|
||||
{
|
||||
throw new InvalidOperationException($"تعادلهای هفته {weekDefinitionId} قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید");
|
||||
}
|
||||
|
||||
// حذف محاسبات قبلی در صورت ForceRecalculate
|
||||
if (existingBalances.Any())
|
||||
{
|
||||
_context.NetworkWeeklyBalances.RemoveRange(existingBalances);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// دریافت همه کاربرانی که عضو فعال باشگاه هستند
|
||||
var activeClubMemberUserIds = await _context.ClubMemberships
|
||||
.Where(c => c.IsActive)
|
||||
.Select(c => c.UserId)
|
||||
.ToHashSetAsync(cancellationToken);
|
||||
|
||||
// دریافت کاربران فعال در شبکه که عضو باشگاه هستند
|
||||
var usersInNetwork = await _context.Users
|
||||
.Where(x => activeClubMemberUserIds.Contains(x.Id))
|
||||
.Select(x => new { x.Id })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// دریافت باقیماندههای هفته قبل
|
||||
var previousWeekDefinitionId = GetPreviousWeekDefinitionId(weekDefinitionId);
|
||||
Dictionary<long, (int LeftLegRemainder, int RightLegRemainder)> previousWeekCarryovers;
|
||||
|
||||
if (previousWeekDefinitionId.HasValue)
|
||||
{
|
||||
previousWeekCarryovers = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekDefinitionId == previousWeekDefinitionId.Value)
|
||||
.ToDictionaryAsync(
|
||||
x => x.UserId,
|
||||
x => (x.LeftLegRemainder, x.RightLegRemainder),
|
||||
cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
previousWeekCarryovers = new Dictionary<long, (int, int)>();
|
||||
}
|
||||
|
||||
var balancesList = new List<NetworkWeeklyBalance>();
|
||||
var calculatedAt = DateTime.Now;
|
||||
|
||||
// خواندن یکباره Configuration ها
|
||||
var configs = await _context.SystemConfigurations
|
||||
.Where(x => x.IsActive && (
|
||||
x.Key == "Commission.MaxWeeklyBalancesPerLeg" ||
|
||||
x.Key == "Commission.MaxNetworkLevel"))
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken);
|
||||
|
||||
var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300"));
|
||||
var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15"));
|
||||
|
||||
foreach (var user in usersInNetwork.OrderBy(o => o.Id))
|
||||
{
|
||||
// دریافت باقیمانده هفته قبل
|
||||
var leftCarryover = 0;
|
||||
var rightCarryover = 0;
|
||||
if (previousWeekCarryovers.ContainsKey(user.Id))
|
||||
{
|
||||
leftCarryover = previousWeekCarryovers[user.Id].LeftLegRemainder;
|
||||
rightCarryover = previousWeekCarryovers[user.Id].RightLegRemainder;
|
||||
}
|
||||
|
||||
// محاسبه تعداد اعضای جدید در این هفته (تا maxNetworkLevel لول پایینتر)
|
||||
var leftNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Left, weekDefinitionId, maxNetworkLevel, cancellationToken);
|
||||
var rightNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Right, weekDefinitionId, maxNetworkLevel, cancellationToken);
|
||||
|
||||
// محاسبه مجموع هر پا (جدید + باقیمانده)
|
||||
var leftTotal = leftNewMembers + leftCarryover;
|
||||
var rightTotal = rightNewMembers + rightCarryover;
|
||||
|
||||
// تعادل = کمترین مقدار بین چپ و راست
|
||||
var totalBalances = Math.Min(leftTotal, rightTotal);
|
||||
|
||||
// باقیمانده برای هفته بعد
|
||||
var leftRemainder = leftTotal - totalBalances;
|
||||
var rightRemainder = rightTotal - totalBalances;
|
||||
|
||||
// اعمال سقف 300
|
||||
var cappedBalances = Math.Min(totalBalances, maxBalancesPerLeg);
|
||||
|
||||
// محاسبه فلش
|
||||
var flushedPerSide = totalBalances - cappedBalances;
|
||||
var totalFlushed = flushedPerSide * 2;
|
||||
|
||||
var balance = new NetworkWeeklyBalance
|
||||
{
|
||||
UserId = user.Id,
|
||||
WeekDefinitionId = weekDefinitionId,
|
||||
LeftLegNewMembers = leftNewMembers,
|
||||
RightLegNewMembers = rightNewMembers,
|
||||
LeftLegCarryover = leftCarryover,
|
||||
RightLegCarryover = rightCarryover,
|
||||
LeftLegTotal = leftTotal,
|
||||
RightLegTotal = rightTotal,
|
||||
TotalBalances = cappedBalances,
|
||||
LeftLegRemainder = leftRemainder,
|
||||
RightLegRemainder = rightRemainder,
|
||||
FlushedPerSide = flushedPerSide,
|
||||
TotalFlushed = totalFlushed,
|
||||
SubordinateBalances = 0,
|
||||
#pragma warning disable CS0618
|
||||
LeftLegBalances = leftTotal,
|
||||
RightLegBalances = rightTotal,
|
||||
#pragma warning restore CS0618
|
||||
WeeklyPoolContribution = 0,
|
||||
CalculatedAt = calculatedAt,
|
||||
IsExpired = false
|
||||
};
|
||||
|
||||
balancesList.Add(balance);
|
||||
}
|
||||
|
||||
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// محاسبه تعادل زیرمجموعه
|
||||
var balancesDictionary = balancesList.ToDictionary(x => x.UserId);
|
||||
|
||||
foreach (var balance in balancesList)
|
||||
{
|
||||
var subordinateBalances = await CalculateSubordinateBalancesAsync(
|
||||
balance.UserId,
|
||||
balancesDictionary,
|
||||
maxNetworkLevel,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
balance.SubordinateBalances = subordinateBalances;
|
||||
}
|
||||
|
||||
_context.NetworkWeeklyBalances.UpdateRange(balancesList);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return balancesList.Count;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<long> CalculateWeeklyCommissionPoolAsync(
|
||||
long weekDefinitionId,
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var weekDefinition = _weekRepository.GetGregorianWeekNumber(weekDefinitionId);
|
||||
if (weekDefinition == null)
|
||||
{
|
||||
throw new InvalidOperationException($"هفته {weekDefinitionId} در سیستم تعریف نشده است");
|
||||
}
|
||||
|
||||
// بررسی وجود استخر
|
||||
var existingPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(x => x.WeekDefinitionId == weekDefinitionId, cancellationToken);
|
||||
|
||||
if (existingPool == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Pool هفته {weekDefinitionId} وجود ندارد. " +
|
||||
"Pool باید در هنگام فعالسازی باشگاه مشتریان ایجاد شده باشد"
|
||||
);
|
||||
}
|
||||
|
||||
if (existingPool.IsCalculated && !forceRecalculate)
|
||||
{
|
||||
throw new InvalidOperationException($"استخر کمیسیون هفته {weekDefinitionId} قبلاً محاسبه شده است");
|
||||
}
|
||||
|
||||
// بررسی وجود تعادلهای هفتگی
|
||||
var weeklyBalances = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekDefinitionId == weekDefinitionId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (!weeklyBalances.Any())
|
||||
{
|
||||
throw new InvalidOperationException($"تعادلهای هفته {weekDefinitionId} هنوز محاسبه نشده است. ابتدا CalculateWeeklyBalances را اجرا کنید");
|
||||
}
|
||||
|
||||
var totalPoolAmount = existingPool.TotalPoolAmount;
|
||||
var totalBalancesInNetwork = weeklyBalances.Sum(x => x.TotalBalances);
|
||||
|
||||
long valuePerBalance = 0;
|
||||
if (totalBalancesInNetwork > 0)
|
||||
{
|
||||
valuePerBalance = totalPoolAmount / totalBalancesInNetwork;
|
||||
}
|
||||
|
||||
// بهروزرسانی Pool
|
||||
existingPool.TotalBalances = totalBalancesInNetwork;
|
||||
existingPool.ValuePerBalance = valuePerBalance;
|
||||
existingPool.IsCalculated = true;
|
||||
existingPool.CalculatedAt = DateTime.Now;
|
||||
|
||||
_context.WeeklyCommissionPools.Update(existingPool);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// حذف پرداختهای قبلی در صورت ForceRecalculate
|
||||
if (forceRecalculate)
|
||||
{
|
||||
var oldPayouts = await _context.UserCommissionPayouts
|
||||
.Where(p => p.WeekDefinitionId == weekDefinitionId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (oldPayouts.Any())
|
||||
{
|
||||
var oldPayoutIds = oldPayouts.Select(p => p.Id).ToList();
|
||||
|
||||
// برگرداندن NetworkBalance به حالت قبل (Revert)
|
||||
await RevertUserWalletsAsync(oldPayoutIds, cancellationToken);
|
||||
|
||||
// حذف تاریخچهها
|
||||
var oldHistories = await _context.CommissionPayoutHistories
|
||||
.Where(h => oldPayoutIds.Contains(h.UserCommissionPayoutId))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (oldHistories.Any())
|
||||
{
|
||||
_context.CommissionPayoutHistories.RemoveRange(oldHistories);
|
||||
}
|
||||
|
||||
_context.UserCommissionPayouts.RemoveRange(oldPayouts);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
// ثبت پرداخت برای کاربرانی که تعادل دارند
|
||||
var payouts = new List<UserCommissionPayout>();
|
||||
|
||||
foreach (var balance in weeklyBalances)
|
||||
{
|
||||
var userBalance = balance.TotalBalances;
|
||||
if (userBalance <= 0) continue;
|
||||
|
||||
var totalAmount = (long)(userBalance * valuePerBalance);
|
||||
|
||||
var payout = new UserCommissionPayout
|
||||
{
|
||||
UserId = balance.UserId,
|
||||
WeekDefinitionId = weekDefinitionId,
|
||||
WeeklyPoolId = existingPool.Id,
|
||||
BalancesEarned = userBalance,
|
||||
ValuePerBalance = valuePerBalance,
|
||||
TotalAmount = totalAmount,
|
||||
Status = CommissionPayoutStatus.Pending,
|
||||
PaidAt = null,
|
||||
WithdrawalMethod = null,
|
||||
IbanNumber = null,
|
||||
WithdrawnAt = null
|
||||
};
|
||||
|
||||
payouts.Add(payout);
|
||||
}
|
||||
|
||||
if (payouts.Any())
|
||||
{
|
||||
await _context.UserCommissionPayouts.AddRangeAsync(payouts, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// ثبت تاریخچه
|
||||
var historyList = payouts.Select(payout => new CommissionPayoutHistory
|
||||
{
|
||||
UserCommissionPayoutId = payout.Id,
|
||||
UserId = payout.UserId,
|
||||
WeekDefinitionId = weekDefinitionId,
|
||||
AmountBefore = 0,
|
||||
AmountAfter = payout.TotalAmount,
|
||||
OldStatus = default(CommissionPayoutStatus),
|
||||
NewStatus = CommissionPayoutStatus.Pending,
|
||||
Action = CommissionPayoutAction.Created,
|
||||
PerformedBy = "System",
|
||||
Reason = "پردازش خودکار کمیسیون هفتگی (ORM)"
|
||||
}).ToList();
|
||||
|
||||
await _context.CommissionPayoutHistories.AddRangeAsync(historyList, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// شارژ کیف پول کاربران
|
||||
await ChargeUserWalletsAsync(payouts, weekDefinitionId, cancellationToken);
|
||||
}
|
||||
|
||||
return existingPool.Id;
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private long? GetPreviousWeekDefinitionId(long currentWeekDefinitionId)
|
||||
{
|
||||
var currentWeekNumber = _weekRepository.GetGregorianWeekNumber(currentWeekDefinitionId);
|
||||
if (currentWeekNumber == null) return null;
|
||||
|
||||
var previousWeekNumber = _weekRepository.GetPreviousWeekNumber(currentWeekNumber);
|
||||
if (previousWeekNumber == null) return null;
|
||||
|
||||
return _weekRepository.GetWeekDefinitionId(previousWeekNumber);
|
||||
}
|
||||
|
||||
private async Task<int> CountNewMembersInLeg(long userId, NetworkLeg leg, long weekDefinitionId, int maxLevel, CancellationToken cancellationToken)
|
||||
{
|
||||
var startDateEndDate = _weekRepository.GetWeekDateRange(weekDefinitionId);
|
||||
var count = await CountNewMembersRecursive(userId, leg, startDateEndDate.Value.startDate, startDateEndDate.Value.endDate, 0, maxLevel, cancellationToken);
|
||||
return count;
|
||||
}
|
||||
|
||||
private async Task<int> CountNewMembersRecursive(
|
||||
long userId,
|
||||
NetworkLeg leg,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
int currentLevel,
|
||||
int maxLevel,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentLevel >= maxLevel)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var child = await _context.Users
|
||||
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == leg, cancellationToken);
|
||||
|
||||
if (child == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
var membership = await _context.ClubMemberships
|
||||
.FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsActive, cancellationToken);
|
||||
|
||||
if (membership?.ActivatedAt >= startDate && membership?.ActivatedAt <= endDate)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var childLeft = await CountNewMembersRecursive(child.Id, NetworkLeg.Left, startDate, endDate, currentLevel + 1, maxLevel, cancellationToken);
|
||||
var childRight = await CountNewMembersRecursive(child.Id, NetworkLeg.Right, startDate, endDate, currentLevel + 1, maxLevel, cancellationToken);
|
||||
|
||||
return count + childLeft + childRight;
|
||||
}
|
||||
|
||||
private async Task<int> CalculateSubordinateBalancesAsync(
|
||||
long userId,
|
||||
Dictionary<long, NetworkWeeklyBalance> allBalances,
|
||||
int maxLevel,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var subordinates = await GetSubordinatesRecursive(userId, 1, maxLevel, cancellationToken);
|
||||
var totalSubordinateBalances = 0;
|
||||
|
||||
foreach (var subordinateId in subordinates)
|
||||
{
|
||||
if (allBalances.ContainsKey(subordinateId))
|
||||
{
|
||||
totalSubordinateBalances += allBalances[subordinateId].TotalBalances;
|
||||
}
|
||||
}
|
||||
|
||||
return totalSubordinateBalances;
|
||||
}
|
||||
|
||||
private async Task<List<long>> GetSubordinatesRecursive(
|
||||
long userId,
|
||||
int currentLevel,
|
||||
int maxLevel,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentLevel > maxLevel)
|
||||
{
|
||||
return new List<long>();
|
||||
}
|
||||
|
||||
var result = new List<long>();
|
||||
|
||||
var children = await _context.Users
|
||||
.Where(x => x.NetworkParentId == userId)
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
result.AddRange(children);
|
||||
|
||||
foreach (var childId in children)
|
||||
{
|
||||
var grandChildren = await GetSubordinatesRecursive(childId, currentLevel + 1, maxLevel, cancellationToken);
|
||||
result.AddRange(grandChildren);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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];
|
||||
wallet.NetworkBalance += payout.TotalAmount;
|
||||
|
||||
var walletLog = new UserWalletChangeLog
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = 0,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = payout.TotalAmount,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = 0,
|
||||
IsIncrease = true,
|
||||
RefrenceId = payout.Id
|
||||
};
|
||||
|
||||
walletLogs.Add(walletLog);
|
||||
}
|
||||
|
||||
await _context.UserWalletChangeLogs.AddRangeAsync(walletLogs, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
foreach (var log in oldWalletLogs)
|
||||
{
|
||||
if (wallets.TryGetValue(log.WalletId, out var wallet))
|
||||
{
|
||||
wallet.NetworkBalance -= log.ChangeNerworkValue;
|
||||
if (wallet.NetworkBalance < 0)
|
||||
{
|
||||
wallet.NetworkBalance = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_context.UserWalletChangeLogs.RemoveRange(oldWalletLogs);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services.Commission;
|
||||
|
||||
/// <summary>
|
||||
/// پیادهسازی محاسبه کمیسیون با استفاده از Stored Procedure
|
||||
/// این روش برای دادههای زیاد بهینهتر است و از CTE استفاده میکند
|
||||
/// </summary>
|
||||
public class StoredProcedureCommissionCalculationStrategy : ICommissionCalculationStrategy
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public StoredProcedureCommissionCalculationStrategy(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> CalculateWeeklyBalancesAsync(
|
||||
long weekDefinitionId,
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// دسترسی به DbContext واقعی برای اجرای SP
|
||||
var dbContext = _context as DbContext;
|
||||
if (dbContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("DbContext برای اجرای Stored Procedure در دسترس نیست");
|
||||
}
|
||||
|
||||
var connection = dbContext.Database.GetDbConnection();
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "CMS.sp_CalculateWeeklyBalances";
|
||||
command.CommandType = System.Data.CommandType.StoredProcedure;
|
||||
command.CommandTimeout = 300; // 5 دقیقه timeout
|
||||
|
||||
// پارامترها
|
||||
var paramWeekDefinitionId = command.CreateParameter();
|
||||
paramWeekDefinitionId.ParameterName = "@WeekDefinitionId";
|
||||
paramWeekDefinitionId.Value = weekDefinitionId;
|
||||
command.Parameters.Add(paramWeekDefinitionId);
|
||||
|
||||
var paramForceRecalculate = command.CreateParameter();
|
||||
paramForceRecalculate.ParameterName = "@ForceRecalculate";
|
||||
paramForceRecalculate.Value = forceRecalculate;
|
||||
command.Parameters.Add(paramForceRecalculate);
|
||||
|
||||
// پارامتر خروجی
|
||||
var paramRowCount = command.CreateParameter();
|
||||
paramRowCount.ParameterName = "@RowCount";
|
||||
paramRowCount.Direction = System.Data.ParameterDirection.Output;
|
||||
paramRowCount.DbType = System.Data.DbType.Int32;
|
||||
command.Parameters.Add(paramRowCount);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
|
||||
var rowCount = (int)(paramRowCount.Value ?? 0);
|
||||
return rowCount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await connection.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<long> CalculateWeeklyCommissionPoolAsync(
|
||||
long weekDefinitionId,
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dbContext = _context as DbContext;
|
||||
if (dbContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("DbContext برای اجرای Stored Procedure در دسترس نیست");
|
||||
}
|
||||
|
||||
var connection = dbContext.Database.GetDbConnection();
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "CMS.sp_CalculateWeeklyCommissionPool";
|
||||
command.CommandType = System.Data.CommandType.StoredProcedure;
|
||||
command.CommandTimeout = 300; // 5 دقیقه timeout
|
||||
|
||||
// پارامترها
|
||||
var paramWeekDefinitionId = command.CreateParameter();
|
||||
paramWeekDefinitionId.ParameterName = "@WeekDefinitionId";
|
||||
paramWeekDefinitionId.Value = weekDefinitionId;
|
||||
command.Parameters.Add(paramWeekDefinitionId);
|
||||
|
||||
var paramForceRecalculate = command.CreateParameter();
|
||||
paramForceRecalculate.ParameterName = "@ForceRecalculate";
|
||||
paramForceRecalculate.Value = forceRecalculate;
|
||||
command.Parameters.Add(paramForceRecalculate);
|
||||
|
||||
// پارامتر خروجی - شناسه Pool
|
||||
var paramPoolId = command.CreateParameter();
|
||||
paramPoolId.ParameterName = "@PoolId";
|
||||
paramPoolId.Direction = System.Data.ParameterDirection.Output;
|
||||
paramPoolId.DbType = System.Data.DbType.Int64;
|
||||
command.Parameters.Add(paramPoolId);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
|
||||
var poolId = (long)(paramPoolId.Value ?? 0L);
|
||||
return poolId;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await connection.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user