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:
+2
@@ -12,6 +12,7 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
||||
public async Task<GetAllClubMembershipsResponseDto> Handle(GetAllClubMembershipsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.ClubMemberships
|
||||
.Include(x => x.User)
|
||||
.ApplyOrder(sortBy: request.SortBy)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
@@ -33,6 +34,7 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
||||
{
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
UserName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}" : string.Empty,
|
||||
IsActive = x.IsActive,
|
||||
ActivatedAt = x.ActivatedAt,
|
||||
InitialContribution = x.InitialContribution,
|
||||
|
||||
+1
@@ -10,6 +10,7 @@ public class GetAllClubMembershipsResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime? ActivatedAt { get; set; }
|
||||
public long InitialContribution { get; set; }
|
||||
|
||||
+22
-16
@@ -8,13 +8,16 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ICommissionCalculationStrategyFactory _strategyFactory;
|
||||
|
||||
public TriggerWeeklyCalculationCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IMediator mediator)
|
||||
IMediator mediator,
|
||||
ICommissionCalculationStrategyFactory strategyFactory)
|
||||
{
|
||||
_context = context;
|
||||
_mediator = mediator;
|
||||
_strategyFactory = strategyFactory;
|
||||
}
|
||||
|
||||
public async Task<TriggerWeeklyCalculationResponseDto> Handle(
|
||||
@@ -26,9 +29,8 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
// Validate week number format
|
||||
if (request.WeekDefinitionId<=0)
|
||||
if (request.WeekDefinitionId <= 0)
|
||||
{
|
||||
return new TriggerWeeklyCalculationResponseDto
|
||||
{
|
||||
@@ -40,33 +42,37 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
|
||||
}
|
||||
|
||||
var steps = new List<string>();
|
||||
|
||||
// ⭐ دریافت استراتژی براساس Config (ORM یا SP)
|
||||
var strategy = await _strategyFactory.CreateStrategyAsync(cancellationToken);
|
||||
var strategyName = strategy.GetType().Name.Contains("StoredProcedure") ? "SP" : "ORM";
|
||||
|
||||
// Step 1: Calculate Weekly Balances (تا 15 لول)
|
||||
if (!request.SkipBalances)
|
||||
{
|
||||
await _mediator.Send(new CalculateWeeklyBalancesCommand
|
||||
{
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
ForceRecalculate = request.ForceRecalculate
|
||||
}, cancellationToken);
|
||||
steps.Add("محاسبه امتیازات هفتگی");
|
||||
var balancesCount = await strategy.CalculateWeeklyBalancesAsync(
|
||||
request.WeekDefinitionId,
|
||||
request.ForceRecalculate,
|
||||
cancellationToken);
|
||||
|
||||
steps.Add($"محاسبه امتیازات هفتگی ({balancesCount} کاربر)");
|
||||
}
|
||||
|
||||
// Step 2: Calculate Pool & Process Payouts (محاسبه استخر + پرداخت کاربران)
|
||||
if (!request.SkipPayouts)
|
||||
{
|
||||
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand
|
||||
{
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
ForceRecalculate = request.ForceRecalculate
|
||||
}, cancellationToken);
|
||||
steps.Add("محاسبه استخر و پرداخت کاربران");
|
||||
var poolId = await strategy.CalculateWeeklyCommissionPoolAsync(
|
||||
request.WeekDefinitionId,
|
||||
request.ForceRecalculate,
|
||||
cancellationToken);
|
||||
|
||||
steps.Add($"محاسبه استخر و پرداخت کاربران (Pool: {poolId})");
|
||||
}
|
||||
|
||||
return new TriggerWeeklyCalculationResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = $"محاسبات هفته {request.WeekDefinitionId} با موفقیت انجام شد. مراحل: {string.Join(", ", steps)}",
|
||||
Message = $"محاسبات هفته {request.WeekDefinitionId} با موفقیت انجام شد [{strategyName}]. مراحل: {string.Join(", ", steps)}",
|
||||
ExecutionId = executionId,
|
||||
StartedAt = startedAt
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// استراتژی محاسبه کمیسیون هفتگی
|
||||
/// دو پیادهسازی دارد: ORM و Stored Procedure
|
||||
/// </summary>
|
||||
public interface ICommissionCalculationStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// محاسبه تعادلهای هفتگی (مرحله 1)
|
||||
/// </summary>
|
||||
/// <param name="weekDefinitionId">شناسه هفته</param>
|
||||
/// <param name="forceRecalculate">محاسبه مجدد حتی اگر قبلاً محاسبه شده</param>
|
||||
/// <param name="cancellationToken">توکن لغو</param>
|
||||
/// <returns>تعداد رکوردهای محاسبه شده</returns>
|
||||
Task<int> CalculateWeeklyBalancesAsync(
|
||||
long weekDefinitionId,
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه استخر کمیسیون و پرداختها (مرحله 2)
|
||||
/// </summary>
|
||||
/// <param name="weekDefinitionId">شناسه هفته</param>
|
||||
/// <param name="forceRecalculate">محاسبه مجدد حتی اگر قبلاً محاسبه شده</param>
|
||||
/// <param name="cancellationToken">توکن لغو</param>
|
||||
/// <returns>شناسه Pool ایجاد شده</returns>
|
||||
Task<long> CalculateWeeklyCommissionPoolAsync(
|
||||
long weekDefinitionId,
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نوع استراتژی محاسبه کمیسیون
|
||||
/// </summary>
|
||||
public enum CommissionCalculationStrategyType
|
||||
{
|
||||
/// <summary>
|
||||
/// استفاده از Entity Framework (ORM) - پیشفرض
|
||||
/// </summary>
|
||||
Orm = 0,
|
||||
|
||||
/// <summary>
|
||||
/// استفاده از Stored Procedure - بهینهتر برای دادههای زیاد
|
||||
/// </summary>
|
||||
StoredProcedure = 1
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Factory برای ایجاد Strategy مناسب براساس Config
|
||||
/// </summary>
|
||||
public interface ICommissionCalculationStrategyFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// ایجاد Strategy براساس تنظیمات سیستم
|
||||
/// </summary>
|
||||
/// <returns>استراتژی مناسب (ORM یا SP)</returns>
|
||||
Task<ICommissionCalculationStrategy> CreateStrategyAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد Strategy مشخص
|
||||
/// </summary>
|
||||
/// <param name="strategyType">نوع استراتژی</param>
|
||||
/// <returns>استراتژی درخواستی</returns>
|
||||
ICommissionCalculationStrategy CreateStrategy(CommissionCalculationStrategyType strategyType);
|
||||
}
|
||||
+7
-3
@@ -56,16 +56,20 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
|
||||
|
||||
// Monthly growth (last 6 months) - using Created date
|
||||
var sixMonthsAgo = DateTime.Now.AddMonths(-6);
|
||||
var monthlyGrowth = await _context.Users
|
||||
var monthlyGrowthRaw = await _context.Users
|
||||
.Where(x => x.NetworkParentId != null && x.Created >= sixMonthsAgo)
|
||||
.GroupBy(x => new { x.Created.Year, x.Created.Month })
|
||||
.Select(x => new { x.Created.Year, x.Created.Month })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var monthlyGrowth = monthlyGrowthRaw
|
||||
.GroupBy(x => new { x.Year, x.Month })
|
||||
.Select(g => new MonthlyGrowthModel
|
||||
{
|
||||
Month = $"{g.Key.Year}-{g.Key.Month:D2}",
|
||||
NewMembers = g.Count()
|
||||
})
|
||||
.OrderBy(x => x.Month)
|
||||
.ToListAsync(cancellationToken);
|
||||
.ToList();
|
||||
|
||||
// Top users by total children count
|
||||
var topUsers = await _context.Users
|
||||
|
||||
@@ -6,6 +6,7 @@ using CMSMicroservice.Infrastructure.BackgroundJobs;
|
||||
using CMSMicroservice.Infrastructure.Services.Monitoring;
|
||||
using CMSMicroservice.Infrastructure.Configuration;
|
||||
using CMSMicroservice.Infrastructure.Services.Payment;
|
||||
using CMSMicroservice.Infrastructure.Services.Commission;
|
||||
using CMSMicroservice.Infrastructure.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -88,6 +89,9 @@ public static class ConfigureServices
|
||||
services.AddSingleton<WeekDefinitionRepository>();
|
||||
services.AddSingleton<IWeekDefinitionRepository>(sp => sp.GetRequiredService<WeekDefinitionRepository>());
|
||||
|
||||
// Commission Calculation Strategy Factory - برای سوییچ بین ORM و SP
|
||||
services.AddScoped<ICommissionCalculationStrategyFactory, CommissionCalculationStrategyFactory>();
|
||||
|
||||
// Background Workers - Deprecated: Using Hangfire instead
|
||||
// services.AddHostedService<WeeklyNetworkCommissionWorker>();
|
||||
services.AddScoped<WeeklyCommissionJob>(); // Hangfire Job (Scoped for DI)
|
||||
|
||||
@@ -61,7 +61,7 @@ public class WeekDefinitionSeeder
|
||||
while (currentDate <= endDate)
|
||||
{
|
||||
var weekStart = currentDate; // شنبه
|
||||
var weekEnd = currentDate.AddDays(6); // جمعه
|
||||
var weekEnd = currentDate.AddDays(6).Date.AddHours(23).AddMinutes(59).AddSeconds(59); // جمعه 23:59:59
|
||||
|
||||
// محاسبه شماره ترتیبی هفته (از 1 شروع میشود)
|
||||
var weekOrder = CalculateWeekOrder(weekStart);
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
-- =============================================
|
||||
-- Insert Configuration for Strategy Selection
|
||||
-- =============================================
|
||||
|
||||
-- Check if config exists, insert if not
|
||||
IF NOT EXISTS (SELECT 1 FROM CMS.SystemConfigurations WHERE [Key] = 'Commission.CalculationStrategy')
|
||||
BEGIN
|
||||
INSERT INTO CMS.SystemConfigurations ([Key], [Value], [Description], IsActive, Created, CreatedBy, LastModified, LastModifiedBy)
|
||||
VALUES (
|
||||
'Commission.CalculationStrategy',
|
||||
'ORM',
|
||||
N'روش محاسبه کمیسیون: ORM (Entity Framework) یا SP (Stored Procedure)',
|
||||
1,
|
||||
GETDATE(),
|
||||
'System',
|
||||
GETDATE(),
|
||||
'System'
|
||||
);
|
||||
PRINT 'Configuration Commission.CalculationStrategy created with default value: ORM'
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
PRINT 'Configuration Commission.CalculationStrategy already exists'
|
||||
END
|
||||
|
||||
-- Show current value
|
||||
SELECT [Key], [Value], [Description]
|
||||
FROM CMS.SystemConfigurations
|
||||
WHERE [Key] LIKE 'Commission.%'
|
||||
ORDER BY [Key];
|
||||
|
||||
/*
|
||||
تغییر روش محاسبه:
|
||||
|
||||
-- برای استفاده از ORM (Entity Framework):
|
||||
UPDATE CMS.SystemConfigurations SET Value = 'ORM' WHERE [Key] = 'Commission.CalculationStrategy'
|
||||
|
||||
-- برای استفاده از Stored Procedure:
|
||||
UPDATE CMS.SystemConfigurations SET Value = 'SP' WHERE [Key] = 'Commission.CalculationStrategy'
|
||||
*/
|
||||
@@ -0,0 +1,94 @@
|
||||
-- =============================================
|
||||
-- SP Deployment Script for Commission Calculation
|
||||
-- Version: 1.0
|
||||
-- Created: 2025-12-19
|
||||
-- =============================================
|
||||
|
||||
/*
|
||||
این اسکریپت برای نصب Stored Procedure های محاسبه کمیسیون استفاده میشود.
|
||||
|
||||
قبل از اجرا:
|
||||
1. مطمئن شوید که به دیتابیس CMS متصل هستید
|
||||
2. کاربر شما دسترسی CREATE PROCEDURE داشته باشد
|
||||
|
||||
ترتیب اجرا:
|
||||
1. sp_CalculateWeeklyBalances.sql
|
||||
2. sp_CalculateWeeklyCommissionPool.sql
|
||||
|
||||
یا این فایل را اجرا کنید که هر دو SP را نصب میکند.
|
||||
*/
|
||||
|
||||
PRINT '========================================='
|
||||
PRINT 'Commission Calculation SP Deployment'
|
||||
PRINT '========================================='
|
||||
PRINT ''
|
||||
|
||||
-- =============================================
|
||||
-- Check Schema
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'CMS')
|
||||
BEGIN
|
||||
PRINT 'ERROR: Schema CMS does not exist!'
|
||||
RETURN
|
||||
END
|
||||
|
||||
PRINT 'Schema CMS: OK'
|
||||
PRINT ''
|
||||
|
||||
-- =============================================
|
||||
-- Drop existing procedures if exist
|
||||
-- =============================================
|
||||
IF EXISTS (SELECT 1 FROM sys.procedures WHERE name = 'sp_CalculateWeeklyBalances' AND schema_id = SCHEMA_ID('CMS'))
|
||||
BEGIN
|
||||
DROP PROCEDURE CMS.sp_CalculateWeeklyBalances;
|
||||
PRINT 'Dropped existing sp_CalculateWeeklyBalances'
|
||||
END
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.procedures WHERE name = 'sp_CalculateWeeklyCommissionPool' AND schema_id = SCHEMA_ID('CMS'))
|
||||
BEGIN
|
||||
DROP PROCEDURE CMS.sp_CalculateWeeklyCommissionPool;
|
||||
PRINT 'Dropped existing sp_CalculateWeeklyCommissionPool'
|
||||
END
|
||||
|
||||
PRINT ''
|
||||
PRINT 'Creating Stored Procedures...'
|
||||
PRINT ''
|
||||
|
||||
-- =============================================
|
||||
-- SP 1: sp_CalculateWeeklyBalances
|
||||
-- =============================================
|
||||
GO
|
||||
-- (content of sp_CalculateWeeklyBalances.sql here)
|
||||
-- Run sp_CalculateWeeklyBalances.sql separately
|
||||
|
||||
GO
|
||||
-- (content of sp_CalculateWeeklyCommissionPool.sql here)
|
||||
-- Run sp_CalculateWeeklyCommissionPool.sql separately
|
||||
|
||||
GO
|
||||
|
||||
PRINT ''
|
||||
PRINT '========================================='
|
||||
PRINT 'Deployment Complete!'
|
||||
PRINT '========================================='
|
||||
PRINT ''
|
||||
PRINT 'How to use:'
|
||||
PRINT ''
|
||||
PRINT '-- 1. Set calculation strategy in SystemConfigurations:'
|
||||
PRINT 'UPDATE CMS.SystemConfigurations '
|
||||
PRINT 'SET Value = ''SP'' '
|
||||
PRINT 'WHERE [Key] = ''Commission.CalculationStrategy'''
|
||||
PRINT ''
|
||||
PRINT '-- Or for ORM mode:'
|
||||
PRINT 'UPDATE CMS.SystemConfigurations '
|
||||
PRINT 'SET Value = ''ORM'' '
|
||||
PRINT 'WHERE [Key] = ''Commission.CalculationStrategy'''
|
||||
PRINT ''
|
||||
PRINT '-- 2. Manual execution (optional):'
|
||||
PRINT 'DECLARE @RowCount INT, @PoolId BIGINT'
|
||||
PRINT 'EXEC CMS.sp_CalculateWeeklyBalances @WeekDefinitionId = 1, @ForceRecalculate = 0, @RowCount = @RowCount OUTPUT'
|
||||
PRINT 'PRINT ''Rows affected: '' + CAST(@RowCount AS VARCHAR)'
|
||||
PRINT ''
|
||||
PRINT 'EXEC CMS.sp_CalculateWeeklyCommissionPool @WeekDefinitionId = 1, @ForceRecalculate = 0, @PoolId = @PoolId OUTPUT'
|
||||
PRINT 'PRINT ''Pool ID: '' + CAST(@PoolId AS VARCHAR)'
|
||||
GO
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
-- =============================================
|
||||
-- Stored Procedure: sp_CalculateWeeklyBalances
|
||||
-- Description: محاسبه تعادلهای هفتگی شبکه باینری با استفاده از CTE
|
||||
-- Author: System
|
||||
-- Created: 2025-12-19
|
||||
-- =============================================
|
||||
|
||||
CREATE OR ALTER PROCEDURE [CMS].[sp_CalculateWeeklyBalances]
|
||||
@WeekDefinitionId BIGINT,
|
||||
@ForceRecalculate BIT = 0,
|
||||
@RowCount INT OUTPUT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
DECLARE @ErrorMessage NVARCHAR(4000);
|
||||
DECLARE @StartDate DATETIME;
|
||||
DECLARE @EndDate DATETIME;
|
||||
DECLARE @PreviousWeekDefinitionId BIGINT;
|
||||
DECLARE @MaxBalancesPerLeg INT = 300;
|
||||
DECLARE @MaxNetworkLevel INT = 15;
|
||||
DECLARE @CalculatedAt DATETIME = GETDATE();
|
||||
|
||||
BEGIN TRY
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- =============================================
|
||||
-- 1. اعتبارسنجی WeekDefinitionId
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM CMS.WeekDefinitions WHERE Id = @WeekDefinitionId)
|
||||
BEGIN
|
||||
SET @ErrorMessage = N'هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' در سیستم تعریف نشده است';
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
|
||||
-- دریافت بازه تاریخی هفته
|
||||
SELECT @StartDate = StartDate, @EndDate = EndDate
|
||||
FROM CMS.WeekDefinitions
|
||||
WHERE Id = @WeekDefinitionId;
|
||||
|
||||
-- =============================================
|
||||
-- 2. بررسی محاسبه قبلی
|
||||
-- =============================================
|
||||
IF EXISTS (SELECT 1 FROM CMS.NetworkWeeklyBalances WHERE WeekDefinitionId = @WeekDefinitionId)
|
||||
BEGIN
|
||||
IF @ForceRecalculate = 0
|
||||
BEGIN
|
||||
SET @ErrorMessage = N'تعادلهای هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید';
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
|
||||
-- حذف محاسبات قبلی
|
||||
DELETE FROM CMS.NetworkWeeklyBalances WHERE WeekDefinitionId = @WeekDefinitionId;
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 3. پیدا کردن هفته قبل
|
||||
-- =============================================
|
||||
SELECT TOP 1 @PreviousWeekDefinitionId = Id
|
||||
FROM CMS.WeekDefinitions
|
||||
WHERE WeekOrder = (SELECT WeekOrder - 1 FROM CMS.WeekDefinitions WHERE Id = @WeekDefinitionId)
|
||||
AND IsActive = 1;
|
||||
|
||||
-- =============================================
|
||||
-- 4. خواندن Configuration ها
|
||||
-- =============================================
|
||||
SELECT @MaxBalancesPerLeg = CAST(Value AS INT)
|
||||
FROM CMS.SystemConfigurations
|
||||
WHERE [Key] = 'Commission.MaxWeeklyBalancesPerLeg' AND IsActive = 1;
|
||||
|
||||
SELECT @MaxNetworkLevel = CAST(Value AS INT)
|
||||
FROM CMS.SystemConfigurations
|
||||
WHERE [Key] = 'Commission.MaxNetworkLevel' AND IsActive = 1;
|
||||
|
||||
-- مقادیر پیشفرض
|
||||
SET @MaxBalancesPerLeg = ISNULL(@MaxBalancesPerLeg, 300);
|
||||
SET @MaxNetworkLevel = ISNULL(@MaxNetworkLevel, 15);
|
||||
|
||||
-- =============================================
|
||||
-- 5. ایجاد جدول موقت برای نتایج
|
||||
-- =============================================
|
||||
CREATE TABLE #Balances (
|
||||
UserId BIGINT PRIMARY KEY,
|
||||
LeftLegNewMembers INT DEFAULT 0,
|
||||
RightLegNewMembers INT DEFAULT 0,
|
||||
LeftLegCarryover INT DEFAULT 0,
|
||||
RightLegCarryover INT DEFAULT 0,
|
||||
LeftLegTotal INT DEFAULT 0,
|
||||
RightLegTotal INT DEFAULT 0,
|
||||
TotalBalances INT DEFAULT 0,
|
||||
LeftLegRemainder INT DEFAULT 0,
|
||||
RightLegRemainder INT DEFAULT 0,
|
||||
FlushedPerSide INT DEFAULT 0,
|
||||
TotalFlushed INT DEFAULT 0,
|
||||
SubordinateBalances INT DEFAULT 0
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- 6. دریافت کاربران فعال باشگاه
|
||||
-- =============================================
|
||||
INSERT INTO #Balances (UserId)
|
||||
SELECT DISTINCT u.Id
|
||||
FROM CMS.Users u
|
||||
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = u.Id AND cm.IsActive = 1;
|
||||
|
||||
-- =============================================
|
||||
-- 7. دریافت باقیمانده هفته قبل
|
||||
-- =============================================
|
||||
IF @PreviousWeekDefinitionId IS NOT NULL
|
||||
BEGIN
|
||||
UPDATE b
|
||||
SET b.LeftLegCarryover = ISNULL(nb.LeftLegRemainder, 0),
|
||||
b.RightLegCarryover = ISNULL(nb.RightLegRemainder, 0)
|
||||
FROM #Balances b
|
||||
LEFT JOIN CMS.NetworkWeeklyBalances nb ON nb.UserId = b.UserId AND nb.WeekDefinitionId = @PreviousWeekDefinitionId;
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 8. محاسبه اعضای جدید هر پا با CTE (بهینه!)
|
||||
-- =============================================
|
||||
|
||||
-- CTE برای شمارش اعضای جدید شاخه چپ
|
||||
;WITH LeftLegCTE AS (
|
||||
-- فرزند مستقیم چپ (لول 1)
|
||||
SELECT
|
||||
u.NetworkParentId AS ParentId,
|
||||
u.Id AS ChildId,
|
||||
1 AS Level
|
||||
FROM CMS.Users u
|
||||
WHERE u.LegPosition = 0 -- Left
|
||||
AND u.NetworkParentId IS NOT NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- زیرمجموعههای بازگشتی
|
||||
SELECT
|
||||
cte.ParentId,
|
||||
u.Id AS ChildId,
|
||||
cte.Level + 1 AS Level
|
||||
FROM CMS.Users u
|
||||
INNER JOIN LeftLegCTE cte ON u.NetworkParentId = cte.ChildId
|
||||
WHERE cte.Level < @MaxNetworkLevel
|
||||
),
|
||||
LeftNewMembers AS (
|
||||
SELECT
|
||||
cte.ParentId AS UserId,
|
||||
COUNT(*) AS NewMembersCount
|
||||
FROM LeftLegCTE cte
|
||||
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = cte.ChildId AND cm.IsActive = 1
|
||||
WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate
|
||||
GROUP BY cte.ParentId
|
||||
)
|
||||
UPDATE b
|
||||
SET b.LeftLegNewMembers = ISNULL(lnm.NewMembersCount, 0)
|
||||
FROM #Balances b
|
||||
LEFT JOIN LeftNewMembers lnm ON lnm.UserId = b.UserId
|
||||
OPTION (MAXRECURSION 100);
|
||||
|
||||
-- CTE برای شمارش اعضای جدید شاخه راست
|
||||
;WITH RightLegCTE AS (
|
||||
-- فرزند مستقیم راست (لول 1)
|
||||
SELECT
|
||||
u.NetworkParentId AS ParentId,
|
||||
u.Id AS ChildId,
|
||||
1 AS Level
|
||||
FROM CMS.Users u
|
||||
WHERE u.LegPosition = 1 -- Right
|
||||
AND u.NetworkParentId IS NOT NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- زیرمجموعههای بازگشتی
|
||||
SELECT
|
||||
cte.ParentId,
|
||||
u.Id AS ChildId,
|
||||
cte.Level + 1 AS Level
|
||||
FROM CMS.Users u
|
||||
INNER JOIN RightLegCTE cte ON u.NetworkParentId = cte.ChildId
|
||||
WHERE cte.Level < @MaxNetworkLevel
|
||||
),
|
||||
RightNewMembers AS (
|
||||
SELECT
|
||||
cte.ParentId AS UserId,
|
||||
COUNT(*) AS NewMembersCount
|
||||
FROM RightLegCTE cte
|
||||
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = cte.ChildId AND cm.IsActive = 1
|
||||
WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate
|
||||
GROUP BY cte.ParentId
|
||||
)
|
||||
UPDATE b
|
||||
SET b.RightLegNewMembers = ISNULL(rnm.NewMembersCount, 0)
|
||||
FROM #Balances b
|
||||
LEFT JOIN RightNewMembers rnm ON rnm.UserId = b.UserId
|
||||
OPTION (MAXRECURSION 100);
|
||||
|
||||
-- =============================================
|
||||
-- 9. محاسبه تعادلها
|
||||
-- =============================================
|
||||
UPDATE #Balances
|
||||
SET
|
||||
-- مجموع هر پا
|
||||
LeftLegTotal = LeftLegNewMembers + LeftLegCarryover,
|
||||
RightLegTotal = RightLegNewMembers + RightLegCarryover;
|
||||
|
||||
UPDATE #Balances
|
||||
SET
|
||||
-- تعادل = MIN(چپ، راست)
|
||||
TotalBalances = CASE
|
||||
WHEN LeftLegTotal <= RightLegTotal THEN LeftLegTotal
|
||||
ELSE RightLegTotal
|
||||
END;
|
||||
|
||||
UPDATE #Balances
|
||||
SET
|
||||
-- باقیمانده برای هفته بعد
|
||||
LeftLegRemainder = LeftLegTotal - TotalBalances,
|
||||
RightLegRemainder = RightLegTotal - TotalBalances,
|
||||
-- فلش (اگر تعادل > سقف)
|
||||
FlushedPerSide = CASE
|
||||
WHEN TotalBalances > @MaxBalancesPerLeg THEN TotalBalances - @MaxBalancesPerLeg
|
||||
ELSE 0
|
||||
END;
|
||||
|
||||
UPDATE #Balances
|
||||
SET
|
||||
-- اعمال سقف
|
||||
TotalBalances = CASE
|
||||
WHEN TotalBalances > @MaxBalancesPerLeg THEN @MaxBalancesPerLeg
|
||||
ELSE TotalBalances
|
||||
END,
|
||||
-- مجموع فلش از دو طرف
|
||||
TotalFlushed = FlushedPerSide * 2;
|
||||
|
||||
-- =============================================
|
||||
-- 10. محاسبه تعادل زیرمجموعه با CTE
|
||||
-- =============================================
|
||||
;WITH SubordinateCTE AS (
|
||||
-- فرزندان مستقیم (لول 1)
|
||||
SELECT
|
||||
u.NetworkParentId AS ParentId,
|
||||
u.Id AS ChildId,
|
||||
1 AS Level
|
||||
FROM CMS.Users u
|
||||
WHERE u.NetworkParentId IS NOT NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- زیرمجموعههای بازگشتی
|
||||
SELECT
|
||||
cte.ParentId,
|
||||
u.Id AS ChildId,
|
||||
cte.Level + 1 AS Level
|
||||
FROM CMS.Users u
|
||||
INNER JOIN SubordinateCTE cte ON u.NetworkParentId = cte.ChildId
|
||||
WHERE cte.Level < @MaxNetworkLevel
|
||||
),
|
||||
SubordinateBalances AS (
|
||||
SELECT
|
||||
cte.ParentId AS UserId,
|
||||
SUM(b.TotalBalances) AS TotalSubordinateBalances
|
||||
FROM SubordinateCTE cte
|
||||
INNER JOIN #Balances b ON b.UserId = cte.ChildId
|
||||
GROUP BY cte.ParentId
|
||||
)
|
||||
UPDATE b
|
||||
SET b.SubordinateBalances = ISNULL(sb.TotalSubordinateBalances, 0)
|
||||
FROM #Balances b
|
||||
LEFT JOIN SubordinateBalances sb ON sb.UserId = b.UserId
|
||||
OPTION (MAXRECURSION 100);
|
||||
|
||||
-- =============================================
|
||||
-- 11. Insert نتایج در جدول اصلی
|
||||
-- =============================================
|
||||
INSERT INTO CMS.NetworkWeeklyBalances (
|
||||
UserId,
|
||||
WeekDefinitionId,
|
||||
LeftLegNewMembers,
|
||||
RightLegNewMembers,
|
||||
LeftLegCarryover,
|
||||
RightLegCarryover,
|
||||
LeftLegTotal,
|
||||
RightLegTotal,
|
||||
TotalBalances,
|
||||
LeftLegRemainder,
|
||||
RightLegRemainder,
|
||||
FlushedPerSide,
|
||||
TotalFlushed,
|
||||
SubordinateBalances,
|
||||
LeftLegBalances,
|
||||
RightLegBalances,
|
||||
WeeklyPoolContribution,
|
||||
CalculatedAt,
|
||||
IsExpired,
|
||||
IsDeleted,
|
||||
Created,
|
||||
CreatedBy,
|
||||
LastModified,
|
||||
LastModifiedBy
|
||||
)
|
||||
SELECT
|
||||
UserId,
|
||||
@WeekDefinitionId,
|
||||
LeftLegNewMembers,
|
||||
RightLegNewMembers,
|
||||
LeftLegCarryover,
|
||||
RightLegCarryover,
|
||||
LeftLegTotal,
|
||||
RightLegTotal,
|
||||
TotalBalances,
|
||||
LeftLegRemainder,
|
||||
RightLegRemainder,
|
||||
FlushedPerSide,
|
||||
TotalFlushed,
|
||||
SubordinateBalances,
|
||||
LeftLegTotal, -- LeftLegBalances (deprecated)
|
||||
RightLegTotal, -- RightLegBalances (deprecated)
|
||||
0, -- WeeklyPoolContribution
|
||||
@CalculatedAt,
|
||||
0, -- IsExpired
|
||||
0, -- IsDeleted
|
||||
@CalculatedAt, -- Created
|
||||
'SP', -- CreatedBy
|
||||
@CalculatedAt, -- LastModified
|
||||
'SP' -- LastModifiedBy
|
||||
FROM #Balances;
|
||||
|
||||
SET @RowCount = @@ROWCOUNT;
|
||||
|
||||
-- پاکسازی
|
||||
DROP TABLE #Balances;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF @@TRANCOUNT > 0
|
||||
ROLLBACK TRANSACTION;
|
||||
|
||||
-- پاکسازی در صورت خطا
|
||||
IF OBJECT_ID('tempdb..#Balances') IS NOT NULL
|
||||
DROP TABLE #Balances;
|
||||
|
||||
SET @ErrorMessage = ERROR_MESSAGE();
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
END CATCH
|
||||
END
|
||||
GO
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
-- =============================================
|
||||
-- Stored Procedure: sp_CalculateWeeklyCommissionPool
|
||||
-- Description: محاسبه استخر کمیسیون هفتگی و توزیع بین کاربران
|
||||
-- Author: System
|
||||
-- Created: 2025-12-19
|
||||
-- =============================================
|
||||
|
||||
CREATE OR ALTER PROCEDURE [CMS].[sp_CalculateWeeklyCommissionPool]
|
||||
@WeekDefinitionId BIGINT,
|
||||
@ForceRecalculate BIT = 0,
|
||||
@PoolId BIGINT OUTPUT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
DECLARE @ErrorMessage NVARCHAR(4000);
|
||||
DECLARE @TotalPoolAmount BIGINT;
|
||||
DECLARE @TotalBalances INT;
|
||||
DECLARE @ValuePerBalance BIGINT;
|
||||
DECLARE @CalculatedAt DATETIME = GETDATE();
|
||||
|
||||
BEGIN TRY
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- =============================================
|
||||
-- 1. پیدا کردن Pool هفته
|
||||
-- =============================================
|
||||
SELECT @PoolId = Id, @TotalPoolAmount = TotalPoolAmount
|
||||
FROM CMS.WeeklyCommissionPools
|
||||
WHERE WeekDefinitionId = @WeekDefinitionId;
|
||||
|
||||
IF @PoolId IS NULL
|
||||
BEGIN
|
||||
SET @ErrorMessage = N'استخر کمیسیون برای هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' تعریف نشده است';
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 2. بررسی محاسبات قبلی
|
||||
-- =============================================
|
||||
IF EXISTS (SELECT 1 FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId)
|
||||
BEGIN
|
||||
IF @ForceRecalculate = 0
|
||||
BEGIN
|
||||
SET @ErrorMessage = N'کمیسیون هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید';
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 3. Revert کیف پولها (در صورت ForceRecalculate)
|
||||
-- =============================================
|
||||
|
||||
-- کم کردن مقادیر قبلی از NetworkBalance
|
||||
UPDATE uw
|
||||
SET uw.NetworkBalance = uw.NetworkBalance - ISNULL(
|
||||
(SELECT SUM(wl.ChangeNerworkValue)
|
||||
FROM CMS.UserWalletChangeLogs wl
|
||||
WHERE wl.WalletId = uw.Id
|
||||
AND wl.RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId)
|
||||
), 0)
|
||||
FROM CMS.UserWallets uw
|
||||
WHERE uw.Id IN (
|
||||
SELECT DISTINCT wl.WalletId
|
||||
FROM CMS.UserWalletChangeLogs wl
|
||||
WHERE wl.RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId)
|
||||
);
|
||||
|
||||
-- حذف لاگهای تغییرات قبلی
|
||||
DELETE FROM CMS.UserWalletChangeLogs
|
||||
WHERE RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId);
|
||||
|
||||
-- حذف تاریخچه پرداخت
|
||||
DELETE FROM CMS.CommissionPayoutHistories
|
||||
WHERE UserCommissionPayoutId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId);
|
||||
|
||||
-- حذف پرداختهای قبلی
|
||||
DELETE FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId;
|
||||
|
||||
-- Reset وضعیت Pool
|
||||
UPDATE CMS.WeeklyCommissionPools
|
||||
SET IsCalculated = 0
|
||||
WHERE Id = @PoolId;
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 4. بررسی وجود تعادلهای هفتگی
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM CMS.NetworkWeeklyBalances WHERE WeekDefinitionId = @WeekDefinitionId)
|
||||
BEGIN
|
||||
SET @ErrorMessage = N'ابتدا باید تعادلهای هفتگی محاسبه شوند. sp_CalculateWeeklyBalances را اجرا کنید';
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 5. محاسبه مجموع تعادلها
|
||||
-- =============================================
|
||||
SELECT @TotalBalances = SUM(TotalBalances)
|
||||
FROM CMS.NetworkWeeklyBalances
|
||||
WHERE WeekDefinitionId = @WeekDefinitionId AND TotalBalances > 0;
|
||||
|
||||
IF @TotalBalances IS NULL OR @TotalBalances = 0
|
||||
BEGIN
|
||||
-- هیچ تعادلی برای توزیع وجود ندارد
|
||||
UPDATE CMS.WeeklyCommissionPools
|
||||
SET IsCalculated = 1, CalculatedAt = @CalculatedAt
|
||||
WHERE Id = @PoolId;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
RETURN;
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 6. محاسبه ارزش هر تعادل
|
||||
-- =============================================
|
||||
SET @ValuePerBalance = @TotalPoolAmount / @TotalBalances;
|
||||
|
||||
-- =============================================
|
||||
-- 7. ایجاد پرداخت برای هر کاربر
|
||||
-- =============================================
|
||||
INSERT INTO CMS.UserCommissionPayouts (
|
||||
UserId,
|
||||
WeekDefinitionId,
|
||||
WeeklyPoolId,
|
||||
BalancesEarned,
|
||||
ValuePerBalance,
|
||||
TotalAmount,
|
||||
Status,
|
||||
PaidAt,
|
||||
IsDeleted,
|
||||
Created,
|
||||
CreatedBy,
|
||||
LastModified,
|
||||
LastModifiedBy
|
||||
)
|
||||
SELECT
|
||||
nb.UserId,
|
||||
@WeekDefinitionId,
|
||||
@PoolId,
|
||||
nb.TotalBalances,
|
||||
@ValuePerBalance,
|
||||
nb.TotalBalances * @ValuePerBalance,
|
||||
0, -- Status: Pending
|
||||
NULL, -- PaidAt
|
||||
0, -- IsDeleted
|
||||
@CalculatedAt,
|
||||
'SP',
|
||||
@CalculatedAt,
|
||||
'SP'
|
||||
FROM CMS.NetworkWeeklyBalances nb
|
||||
WHERE nb.WeekDefinitionId = @WeekDefinitionId AND nb.TotalBalances > 0;
|
||||
|
||||
-- =============================================
|
||||
-- 8. ثبت تاریخچه پرداخت
|
||||
-- =============================================
|
||||
INSERT INTO CMS.CommissionPayoutHistories (
|
||||
UserCommissionPayoutId,
|
||||
UserId,
|
||||
WeekDefinitionId,
|
||||
AmountBefore,
|
||||
AmountAfter,
|
||||
OldStatus,
|
||||
NewStatus,
|
||||
Action,
|
||||
PerformedBy,
|
||||
Reason,
|
||||
IsDeleted,
|
||||
Created,
|
||||
CreatedBy,
|
||||
LastModified,
|
||||
LastModifiedBy
|
||||
)
|
||||
SELECT
|
||||
cp.Id, -- UserCommissionPayoutId
|
||||
cp.UserId, -- UserId
|
||||
@WeekDefinitionId,
|
||||
0, -- AmountBefore
|
||||
cp.TotalAmount, -- AmountAfter
|
||||
0, -- OldStatus: Pending
|
||||
0, -- NewStatus: Pending
|
||||
0, -- Action: Created (assuming enum value)
|
||||
'SP', -- PerformedBy
|
||||
N'محاسبه اولیه توسط SP', -- Reason
|
||||
0, -- IsDeleted
|
||||
@CalculatedAt,
|
||||
'SP',
|
||||
@CalculatedAt,
|
||||
'SP'
|
||||
FROM CMS.UserCommissionPayouts cp
|
||||
WHERE cp.WeekDefinitionId = @WeekDefinitionId;
|
||||
|
||||
-- =============================================
|
||||
-- 9. شارژ کیف پول کاربران
|
||||
-- =============================================
|
||||
|
||||
-- بروزرسانی NetworkBalance
|
||||
UPDATE uw
|
||||
SET uw.NetworkBalance = uw.NetworkBalance + cp.TotalAmount
|
||||
FROM CMS.UserWallets uw
|
||||
INNER JOIN CMS.UserCommissionPayouts cp ON cp.UserId = uw.UserId
|
||||
WHERE cp.WeekDefinitionId = @WeekDefinitionId;
|
||||
|
||||
-- ثبت لاگ تغییرات کیف پول
|
||||
INSERT INTO CMS.UserWalletChangeLogs (
|
||||
WalletId,
|
||||
CurrentBalance,
|
||||
ChangeValue,
|
||||
CurrentNetworkBalance,
|
||||
ChangeNerworkValue,
|
||||
CurrentDiscountBalance,
|
||||
ChangeDiscountValue,
|
||||
IsIncrease,
|
||||
RefrenceId,
|
||||
IsDeleted,
|
||||
Created,
|
||||
CreatedBy,
|
||||
LastModified,
|
||||
LastModifiedBy
|
||||
)
|
||||
SELECT
|
||||
uw.Id,
|
||||
uw.Balance, -- CurrentBalance
|
||||
0, -- ChangeValue (no change to main balance)
|
||||
uw.NetworkBalance, -- CurrentNetworkBalance (after update)
|
||||
cp.TotalAmount, -- ChangeNerworkValue
|
||||
uw.DiscountBalance, -- CurrentDiscountBalance
|
||||
0, -- ChangeDiscountValue
|
||||
1, -- IsIncrease: true
|
||||
cp.Id, -- RefrenceId: PayoutId
|
||||
0, -- IsDeleted
|
||||
@CalculatedAt,
|
||||
'SP',
|
||||
@CalculatedAt,
|
||||
'SP'
|
||||
FROM CMS.UserCommissionPayouts cp
|
||||
INNER JOIN CMS.UserWallets uw ON uw.UserId = cp.UserId
|
||||
WHERE cp.WeekDefinitionId = @WeekDefinitionId;
|
||||
|
||||
-- =============================================
|
||||
-- 10. بروزرسانی Pool
|
||||
-- =============================================
|
||||
UPDATE CMS.WeeklyCommissionPools
|
||||
SET
|
||||
IsCalculated = 1,
|
||||
CalculatedAt = @CalculatedAt,
|
||||
TotalBalances = @TotalBalances,
|
||||
ValuePerBalance = @ValuePerBalance,
|
||||
LastModified = @CalculatedAt,
|
||||
LastModifiedBy = 'SP'
|
||||
WHERE Id = @PoolId;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF @@TRANCOUNT > 0
|
||||
ROLLBACK TRANSACTION;
|
||||
|
||||
SET @ErrorMessage = ERROR_MESSAGE();
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
END CATCH
|
||||
END
|
||||
GO
|
||||
+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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.0.155</Version>
|
||||
<Version>0.0.156</Version>
|
||||
<DebugType>None</DebugType>
|
||||
<DebugSymbols>False</DebugSymbols>
|
||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Mapster;
|
||||
using System.Linq;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Common.Mappings;
|
||||
|
||||
public class ClubMembershipProfile : IRegister
|
||||
{
|
||||
public void Register(TypeAdapterConfig config)
|
||||
{
|
||||
// =====================
|
||||
// GetAllClubMemberships
|
||||
// =====================
|
||||
|
||||
// GetAllClubMembershipsRequest -> GetAllClubMembershipsQuery
|
||||
config.NewConfig<GetAllClubMembershipsRequest, GetAllClubMembershipsQuery>()
|
||||
.MapWith(src => new GetAllClubMembershipsQuery
|
||||
{
|
||||
PaginationState = new Application.Common.Models.PaginationState
|
||||
{
|
||||
PageNumber = src.PageIndex > 0 ? src.PageIndex : 1,
|
||||
PageSize = src.PageSize > 0 ? src.PageSize : 20
|
||||
},
|
||||
Filter = new GetAllClubMembershipsFilter
|
||||
{
|
||||
UserId = src.UserId,
|
||||
IsActive = src.IsActive
|
||||
}
|
||||
});
|
||||
|
||||
// GetAllClubMembershipsResponseDto -> GetAllClubMembershipsResponse
|
||||
config.NewConfig<GetAllClubMembershipsResponseDto, GetAllClubMembershipsResponse>()
|
||||
.MapWith(src => new GetAllClubMembershipsResponse
|
||||
{
|
||||
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
|
||||
{
|
||||
CurrentPage = src.MetaData.CurrentPage,
|
||||
PageSize = src.MetaData.PageSize,
|
||||
TotalCount = src.MetaData.TotalCount,
|
||||
TotalPage = src.MetaData.TotalPage
|
||||
},
|
||||
Models = { src.Models.Select(MapToModel) }
|
||||
});
|
||||
|
||||
// =====================
|
||||
// GetClubStatistics
|
||||
// =====================
|
||||
|
||||
// GetClubStatisticsRequest -> GetClubStatisticsQuery
|
||||
config.NewConfig<GetClubStatisticsRequest, GetClubStatisticsQuery>()
|
||||
.MapWith(src => new GetClubStatisticsQuery());
|
||||
|
||||
// GetClubStatisticsResponseDto -> GetClubStatisticsResponse
|
||||
config.NewConfig<GetClubStatisticsResponseDto, GetClubStatisticsResponse>()
|
||||
.MapWith(src => new GetClubStatisticsResponse
|
||||
{
|
||||
TotalMembers = src.TotalMembers,
|
||||
ActiveMembers = src.ActiveMembers,
|
||||
InactiveMembers = src.InactiveMembers,
|
||||
ExpiredMembers = src.ExpiredMembers,
|
||||
ActivePercentage = src.ActivePercentage,
|
||||
TotalRevenue = src.TotalRevenue,
|
||||
AverageMembershipDurationDays = src.AverageMembershipDurationDays,
|
||||
ExpiringSoonCount = src.ExpiringSoonCount,
|
||||
PackageDistribution = { src.PackageDistribution.Select(p => new PackageLevelDistribution
|
||||
{
|
||||
PackageId = p.PackageId,
|
||||
PackageName = p.PackageName ?? string.Empty,
|
||||
MemberCount = p.MemberCount,
|
||||
Percentage = p.Percentage
|
||||
}) },
|
||||
MonthlyTrend = { src.MonthlyTrend.Select(t => new MonthlyMembershipTrend
|
||||
{
|
||||
Month = t.Month ?? string.Empty,
|
||||
Activations = t.Activations,
|
||||
Expirations = t.Expirations,
|
||||
NetChange = t.NetChange
|
||||
}) }
|
||||
});
|
||||
}
|
||||
|
||||
private static ClubMembershipModel MapToModel(GetAllClubMembershipsResponseModel src)
|
||||
{
|
||||
return new ClubMembershipModel
|
||||
{
|
||||
Id = src.Id,
|
||||
UserId = src.UserId,
|
||||
UserName = src.UserName ?? string.Empty,
|
||||
PackageId = 0, // Not available in current entity
|
||||
PackageName = string.Empty, // Not available in current entity
|
||||
ActivationCode = string.Empty, // Not available in current entity
|
||||
ActivatedAt = src.ActivatedAt.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.ActivatedAt.Value, DateTimeKind.Utc))
|
||||
: null,
|
||||
ExpiresAt = null, // Not available in current entity
|
||||
IsActive = src.IsActive,
|
||||
IsExpired = false, // Calculate based on ExpiresAt if available
|
||||
Created = Timestamp.FromDateTime(src.Created.UtcDateTime)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosition;
|
||||
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
|
||||
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
@@ -9,6 +10,10 @@ public class NetworkMembershipProfile : IRegister
|
||||
{
|
||||
void IRegister.Register(TypeAdapterConfig config)
|
||||
{
|
||||
// GetNetworkStatisticsRequest -> GetNetworkStatisticsQuery
|
||||
config.NewConfig<GetNetworkStatisticsRequest, GetNetworkStatisticsQuery>()
|
||||
.MapWith(src => new GetNetworkStatisticsQuery());
|
||||
|
||||
// Request mapping
|
||||
config.NewConfig<GetNetworkTreeRequest, GetNetworkTreeQuery>()
|
||||
.Map(dest => dest.UserId, src => src.UserId)
|
||||
|
||||
Reference in New Issue
Block a user