Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15044c66cb | |||
| 8518c7b3ec |
+48
-1
@@ -221,7 +221,54 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
_context.ClubMembershipHistories.Add(history);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 9. اضافه کردن مبلغ به Pool هفته جاری
|
||||
// 8.5. آپدیت PackagePurchasedAt سایکل جاری به زمان امضای قرارداد
|
||||
// دلیل: Pool در همین لحظه شارژ میشود — Validation باید به همین هفته اشاره کند
|
||||
// سناریوی cross-week: کاربر هفته ۱۸ پکیج خریده، هفته ۱۹ قرارداد امضا کرده
|
||||
// بدون این Fix: Cycle.PackagePurchasedAt = هفته ۱۸ ولی Pool هفته ۱۹ → اختلاف
|
||||
// با این Fix: Cycle.PackagePurchasedAt = هفته ۱۹ = Pool هفته ۱۹ → درست
|
||||
var currentCycle = await _context.ClubMembershipCycles
|
||||
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, cancellationToken);
|
||||
|
||||
if (currentCycle != null)
|
||||
{
|
||||
currentCycle.PackagePurchasedAt = activationDate;
|
||||
_context.ClubMembershipCycles.Update(currentCycle);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Updated Cycle #{CycleNumber} PackagePurchasedAt to contract-signing time for UserId {UserId}",
|
||||
currentCycle.CycleNumber,
|
||||
userId
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// در صورتی که Cycle وجود ندارد (legacy یا مسیر مستقیم AcceptContract)
|
||||
// Cycle را همینجا بساز تا Validation درست کار کند
|
||||
_logger.LogWarning(
|
||||
"No IsCurrentCycle found for UserId {UserId} at AcceptContract — creating Cycle now",
|
||||
userId
|
||||
);
|
||||
|
||||
var maxCycleNumber = await _context.ClubMembershipCycles
|
||||
.Where(c => c.ClubMembershipId == clubMembership.Id)
|
||||
.MaxAsync(c => (int?)c.CycleNumber, cancellationToken) ?? 0;
|
||||
|
||||
var newCycle = new ClubMembershipCycle
|
||||
{
|
||||
UserId = userId,
|
||||
ClubMembershipId = clubMembership.Id,
|
||||
CycleNumber = maxCycleNumber + 1,
|
||||
PackagePurchasedAt = activationDate,
|
||||
PurchaseMethod = user.PackagePurchaseMethod,
|
||||
PackageAmount = package.Price,
|
||||
PackageId = package.Id,
|
||||
IsCurrentCycle = true
|
||||
};
|
||||
|
||||
_context.ClubMembershipCycles.Add(newCycle);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||
var weeklyPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
|
||||
|
||||
+6
@@ -17,4 +17,10 @@ public record ActivateClubMembershipCommand : IRequest<bool>
|
||||
/// فعالسازی اجباری توسط ادمین — بدون بررسی سفارش و کیفپول
|
||||
/// </summary>
|
||||
public bool ForceActivation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه پکیج — فقط در ForceActivation استفاده میشود.
|
||||
/// اگر 0 باشد، پکیج پایه (IsBasePackage) استفاده میشود.
|
||||
/// </summary>
|
||||
public long PackageId { get; init; }
|
||||
}
|
||||
|
||||
+29
-7
@@ -170,11 +170,20 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// بارگذاری پکیج پایه برای فعالسازی اجباری
|
||||
// بارگذاری پکیج — اگر PackageId مشخص شده همان را لود کن، وگرنه پکیج پایه
|
||||
if (request.PackageId > 0)
|
||||
{
|
||||
package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.Id == request.PackageId && !p.IsDeleted, cancellationToken)
|
||||
?? throw new NotFoundException($"پکیج {request.PackageId} یافت نشد");
|
||||
}
|
||||
else
|
||||
{
|
||||
package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
||||
?? throw new NotFoundException("پکیج پایه یافت نشد");
|
||||
}
|
||||
}
|
||||
|
||||
// 6. بررسی عضویت فعلی
|
||||
var existingMembership = await _context.ClubMemberships
|
||||
@@ -226,10 +235,11 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
var hasCurrentCycle = await _context.ClubMembershipCycles
|
||||
.AnyAsync(c => c.UserId == user.Id && c.IsCurrentCycle, cancellationToken);
|
||||
|
||||
if (existingMembership.IsActive && hasCurrentCycle)
|
||||
// اگر چرخه فعال دارد — یعنی Activate قبلاً برای این خرید اجرا شده؛ جلوگیری از ثبت Cycle تکراری
|
||||
if (hasCurrentCycle)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"User {UserId} is already an active club member with a running cycle",
|
||||
"User {UserId} already has a running cycle — skipping duplicate activation",
|
||||
user.Id
|
||||
);
|
||||
return true;
|
||||
@@ -307,6 +317,11 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
|
||||
// عضو جدید (isNewMembership=true): Pool توسط AcceptClubMembershipContract شارژ میشود
|
||||
// چون کاربر قبل از امضای قرارداد IsActive=false است و قرارداد فقط یکبار امضا میشود.
|
||||
// خرید مجدد (isNewMembership=false): قرارداد مجدد امضا نمیشود — Pool همینجا شارژ میشود.
|
||||
if (!isNewMembership)
|
||||
{
|
||||
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||
var weeklyPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
|
||||
@@ -319,8 +334,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
WeekDefinitionId = currentWeekDefinitionId,
|
||||
PackageId = package.Id,
|
||||
TotalPoolAmount = activationFeeValue,
|
||||
TotalBalances = 0, // در CalculateWeeklyBalances محاسبه میشه
|
||||
ValuePerBalance = 0, // در CalculateWeeklyCommissionPool محاسبه میشه
|
||||
TotalBalances = 0,
|
||||
ValuePerBalance = 0,
|
||||
IsCalculated = false,
|
||||
CalculatedAt = null
|
||||
};
|
||||
@@ -330,12 +345,11 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
_logger.LogInformation(
|
||||
"Created new WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId} with initial amount: {Amount}",
|
||||
currentWeekDefinitionId,
|
||||
giftValue
|
||||
activationFeeValue
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// اضافه کردن به Pool موجود
|
||||
weeklyPool.TotalPoolAmount += activationFeeValue;
|
||||
_context.WeeklyCommissionPools.Update(weeklyPool);
|
||||
|
||||
@@ -348,6 +362,14 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"New member — Pool charge deferred to AcceptClubMembershipContract for UserId {UserId}",
|
||||
user.Id
|
||||
);
|
||||
}
|
||||
|
||||
// 9. اعمال ویژگیهای پکیج — DIFF/تفاضل (Q20)
|
||||
await ApplyFeatureDiffAsync(user.Id, entity.Id, package.Id, isNewMembership, activationDate, cancellationToken);
|
||||
|
||||
+9
-5
@@ -266,12 +266,16 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
|
||||
var count = 0;
|
||||
|
||||
// اگر فرزند در این هفته پکیج خریده (Cycle جدید ساخته شده)، 1 امتیاز
|
||||
// ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد)
|
||||
var currentCycle = await _context.ClubMembershipCycles
|
||||
.FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsCurrentCycle, cancellationToken);
|
||||
// ⭐ بر اساس بازه تاریخی هفته، نه IsCurrentCycle
|
||||
// IsCurrentCycle فقط آخرین خرید را نشان میدهد — برای تاریخچهای (هفتههای گذشته)
|
||||
// باید از PackagePurchasedAt استفاده کرد که تاریخ دقیق خرید را نگه میدارد
|
||||
var hadCycleInWeek = await _context.ClubMembershipCycles
|
||||
.AnyAsync(x => x.UserId == child.Id
|
||||
&& x.PackagePurchasedAt >= startDate
|
||||
&& x.PackagePurchasedAt <= endDate,
|
||||
cancellationToken);
|
||||
|
||||
if (currentCycle?.PackagePurchasedAt >= startDate && currentCycle?.PackagePurchasedAt <= endDate)
|
||||
if (hadCycleInWeek)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
+56
-1
@@ -41,6 +41,61 @@ public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler<GetWeeklyComm
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return pool ?? new WeeklyCommissionPoolDto();
|
||||
pool ??= new WeeklyCommissionPoolDto();
|
||||
|
||||
// ── Q29: اعتبارسنجی TotalPoolAmount در برابر فعالسازیهای واقعی ──
|
||||
await ValidatePoolAmountAsync(pool, request, cancellationToken);
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TotalPoolAmount استخر را با تعداد فعالسازیهای ثبتشده (AcceptContract + ActivateClubMembership) مقایسه میکند.
|
||||
/// نتیجه در فیلدهای Validation... ذخیره میشود.
|
||||
/// </summary>
|
||||
private async Task ValidatePoolAmountAsync(WeeklyCommissionPoolDto pool, GetWeeklyCommissionPoolQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var packageId = pool.PackageId > 0 ? pool.PackageId : request.PackageId;
|
||||
if (packageId is null or 0)
|
||||
return;
|
||||
|
||||
var weekDef = _weekDefinitionRepository.GetWeekById(request.WeekDefinitionId);
|
||||
if (weekDef is null)
|
||||
return;
|
||||
|
||||
var activationFee = await _context.Packages
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == packageId.Value)
|
||||
.Select(p => p.ActivationFee)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (activationFee <= 0)
|
||||
return;
|
||||
|
||||
// شمارش Cycleهایی که Pool آنها واقعاً شارژ شده:
|
||||
// - عضو جدید: Pool توسط AcceptContract شارژ میشود → UserContract وجود دارد
|
||||
// - خرید مجدد: Pool در ActivateClubMembership شارژ میشود → IsCurrentCycle نبوده (قبلاً active بوده)
|
||||
// - نباید از IsActive استفاده شود: وضعیت تاریخی نیست (عضو ممکن است بعداً غیرفعال شده باشد)
|
||||
//
|
||||
// قانون: Cycle شارژ شده = یا قرارداد دارد (عضو جدید) یا IsActive قبلاً true بوده (خرید مجدد)
|
||||
// سادهترین معادل: Cycle هایی که ClubMembership.ActivatedAt در بازه هفته است (زمان واقعی شارژ Pool)
|
||||
var totalActivations = await _context.ClubMembershipCycles
|
||||
.AsNoTracking()
|
||||
.Where(c => c.PackageId == packageId.Value
|
||||
&& c.PackagePurchasedAt >= weekDef.StartDate
|
||||
&& c.PackagePurchasedAt < weekDef.EndDate
|
||||
&& _context.UserContracts.Any(uc =>
|
||||
uc.UserId == c.UserId
|
||||
&& _context.Contracts.Any(con =>
|
||||
con.Id == uc.ContractId
|
||||
&& con.Type == ContractType.ClubMembership)))
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
var expectedAmount = (long)totalActivations * activationFee;
|
||||
|
||||
pool.ValidationActivationCount = totalActivations;
|
||||
pool.ValidationExpectedAmount = expectedAmount;
|
||||
pool.ValidationDiscrepancy = pool.TotalPoolAmount - expectedAmount;
|
||||
pool.ValidationIsValid = pool.TotalPoolAmount == expectedAmount;
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -16,4 +16,14 @@ public class WeeklyCommissionPoolDto
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public long PackageId { get; set; }
|
||||
public string PackageTitle { get; set; } = string.Empty;
|
||||
|
||||
// ── Validation Fields (Q29) ──
|
||||
/// <summary>آیا TotalPoolAmount با تعداد فعالسازیهای ثبتشده در دیتابیس مطابقت دارد؟</summary>
|
||||
public bool ValidationIsValid { get; set; } = true;
|
||||
/// <summary>مبلغ مورد انتظار بر اساس شمارش فعالسازیها × ActivationFee</summary>
|
||||
public long ValidationExpectedAmount { get; set; }
|
||||
/// <summary>اختلاف: TotalPoolAmount - ValidationExpectedAmount</summary>
|
||||
public long ValidationDiscrepancy { get; set; }
|
||||
/// <summary>تعداد فعالسازیهای شناساییشده برای این هفته و پکیج</summary>
|
||||
public int ValidationActivationCount { get; set; }
|
||||
}
|
||||
|
||||
+27
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
@@ -14,15 +15,18 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly ISender _sender;
|
||||
private readonly ILogger<CreateManualPaymentCommandHandler> _logger;
|
||||
|
||||
public CreateManualPaymentCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUser,
|
||||
ISender sender,
|
||||
ILogger<CreateManualPaymentCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
_sender = sender;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -152,6 +156,29 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
|
||||
// 10. ذخیره همه تغییرات
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 11. فعالسازی باشگاه مشتریان — ساخت Cycle و آمادهسازی برای AcceptContract
|
||||
// این مرحله تضمین میکند که:
|
||||
// الف) ClubMembershipCycle ساخته شود تا والدین امتیاز «عضو جدید» بگیرند
|
||||
// ب) Pool توسط AcceptContract (که کاربر در FO امضا میکند) شارژ شود
|
||||
try
|
||||
{
|
||||
await _sender.Send(new ActivateClubMembershipCommand
|
||||
{
|
||||
UserId = request.UserId,
|
||||
ForceActivation = true,
|
||||
PackageId = package.Id
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (Exception activateEx)
|
||||
{
|
||||
// عضویت باشگاه اختیاری است — خطا نباید پرداخت دستی را برگرداند
|
||||
_logger.LogWarning(
|
||||
activateEx,
|
||||
"Club membership activation failed after manual payment for UserId {UserId} — payment recorded successfully",
|
||||
request.UserId
|
||||
);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Manual membership payment created successfully. " +
|
||||
"ManualPaymentId: {Id}, UserId: {UserId}, TransactionId: {TransactionId}, " +
|
||||
|
||||
+8
-2
@@ -118,7 +118,10 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
||||
ActivationWeekDefinitionId = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDefinitionId")) ? null : reader.GetInt64(reader.GetOrdinal("ActivationWeekDefinitionId")),
|
||||
ActivationWeekDisplayName = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDisplayName")) ? null : reader.GetString(reader.GetOrdinal("ActivationWeekDisplayName")),
|
||||
IsActivatedInTargetWeek = Convert.ToBoolean(reader.GetValue(reader.GetOrdinal("IsActivatedInTargetWeek"))),
|
||||
UserCreated = new DateTimeOffset(reader.GetDateTime(reader.GetOrdinal("UserCreated")))
|
||||
UserCreated = new DateTimeOffset(reader.GetDateTime(reader.GetOrdinal("UserCreated"))),
|
||||
IsNewActivation = reader.IsDBNull(reader.GetOrdinal("IsNewActivation")) ? null : (bool?)(reader.GetInt32(reader.GetOrdinal("IsNewActivation")) == 1),
|
||||
PackageName = reader.IsDBNull(reader.GetOrdinal("PackageName")) ? null : reader.GetString(reader.GetOrdinal("PackageName")),
|
||||
PackageId = reader.IsDBNull(reader.GetOrdinal("PackageId")) ? null : reader.GetInt64(reader.GetOrdinal("PackageId"))
|
||||
};
|
||||
|
||||
results.Add(node);
|
||||
@@ -160,7 +163,10 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
||||
ActivationWeekDefinitionId = flatNode.ActivationWeekDefinitionId,
|
||||
ActivationWeekDisplayName = flatNode.ActivationWeekDisplayName,
|
||||
IsActivatedInTargetWeek = flatNode.IsActivatedInTargetWeek,
|
||||
UserCreated = flatNode.UserCreated
|
||||
UserCreated = flatNode.UserCreated,
|
||||
IsNewActivation = flatNode.IsNewActivation,
|
||||
PackageName = flatNode.PackageName,
|
||||
PackageId = flatNode.PackageId
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -48,6 +48,18 @@ public class NetworkTreeDto
|
||||
/// </summary>
|
||||
public DateTimeOffset UserCreated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا فعالسازی در هفته هدف اولین بار بوده؟ null = بدون Cycle
|
||||
/// </summary>
|
||||
public bool? IsNewActivation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام پکیج موثر (هفته هدف یا پکیج فعلی)
|
||||
/// </summary>
|
||||
public string? PackageName { get; set; }
|
||||
|
||||
public long? PackageId { get; set; }
|
||||
|
||||
public NetworkTreeDto? LeftChild { get; set; }
|
||||
public NetworkTreeDto? RightChild { get; set; }
|
||||
}
|
||||
|
||||
+13
@@ -21,6 +21,19 @@ public class NetworkTreeNodeDto
|
||||
public bool IsActivatedInTargetWeek { get; set; }
|
||||
public DateTimeOffset UserCreated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا فعالسازی در هفته هدف اولین بار بوده (true) یا خرید مجدد (false)؟
|
||||
/// null یعنی Cycleای ندارد
|
||||
/// </summary>
|
||||
public bool? IsNewActivation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام پکیج موثر (هفته هدف یا پکیج فعلی)
|
||||
/// </summary>
|
||||
public string? PackageName { get; set; }
|
||||
|
||||
public long? PackageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام کامل کاربر
|
||||
/// </summary>
|
||||
|
||||
+6
-6
@@ -166,10 +166,10 @@ BEGIN
|
||||
LeftNewMembers AS (
|
||||
SELECT
|
||||
cte.ParentId AS UserId,
|
||||
COUNT(*) AS NewMembersCount
|
||||
COUNT(DISTINCT cte.ChildId) 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
|
||||
INNER JOIN CMS.ClubMembershipCycles cc ON cc.UserId = cte.ChildId
|
||||
WHERE cc.PackagePurchasedAt >= @StartDate AND cc.PackagePurchasedAt <= @EndDate
|
||||
GROUP BY cte.ParentId
|
||||
)
|
||||
UPDATE b
|
||||
@@ -203,10 +203,10 @@ BEGIN
|
||||
RightNewMembers AS (
|
||||
SELECT
|
||||
cte.ParentId AS UserId,
|
||||
COUNT(*) AS NewMembersCount
|
||||
COUNT(DISTINCT cte.ChildId) 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
|
||||
INNER JOIN CMS.ClubMembershipCycles cc ON cc.UserId = cte.ChildId
|
||||
WHERE cc.PackagePurchasedAt >= @StartDate AND cc.PackagePurchasedAt <= @EndDate
|
||||
GROUP BY cte.ParentId
|
||||
)
|
||||
UPDATE b
|
||||
|
||||
+48
-33
@@ -3,10 +3,12 @@
|
||||
-- Description: محاسبه استخر کمیسیون هفتگی و توزیع بین کاربران
|
||||
-- Author: System
|
||||
-- Created: 2025-12-19
|
||||
-- Updated: Q29 — اضافه کردن @PackageId، fix فیلتر pool، مدیریت pool خالی
|
||||
-- =============================================
|
||||
|
||||
CREATE OR ALTER PROCEDURE [CMS].[sp_CalculateWeeklyCommissionPool]
|
||||
@WeekDefinitionId BIGINT,
|
||||
@PackageId BIGINT, -- هر بار به ازای یک پکیج اجرا میشود
|
||||
@ForceRecalculate BIT = 0,
|
||||
@PoolId BIGINT OUTPUT
|
||||
AS
|
||||
@@ -15,7 +17,7 @@ BEGIN
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
DECLARE @ErrorMessage NVARCHAR(4000);
|
||||
DECLARE @TotalPoolAmount BIGINT;
|
||||
DECLARE @TotalPoolAmount BIGINT = 0;
|
||||
DECLARE @TotalBalances INT;
|
||||
DECLARE @ValuePerBalance BIGINT;
|
||||
DECLARE @CalculatedAt DATETIME = GETDATE();
|
||||
@@ -24,33 +26,42 @@ BEGIN
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- =============================================
|
||||
-- 1. پیدا کردن Pool هفته
|
||||
-- 1. پیدا کردن Pool هفته (فیلتر بر اساس PackageId)
|
||||
-- =============================================
|
||||
SELECT @PoolId = Id, @TotalPoolAmount = TotalPoolAmount
|
||||
FROM CMS.WeeklyCommissionPools
|
||||
WHERE WeekDefinitionId = @WeekDefinitionId;
|
||||
WHERE WeekDefinitionId = @WeekDefinitionId AND PackageId = @PackageId;
|
||||
|
||||
-- اگر Pool وجود ندارد (هیچ فعالسازی در این هفته نبوده)، یک رکورد خالی ایجاد میکنیم
|
||||
IF @PoolId IS NULL
|
||||
BEGIN
|
||||
SET @ErrorMessage = N'استخر کمیسیون برای هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' تعریف نشده است';
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
RETURN;
|
||||
INSERT INTO CMS.WeeklyCommissionPools (
|
||||
WeekDefinitionId, PackageId, TotalPoolAmount, TotalBalances,
|
||||
ValuePerBalance, IsCalculated, CalculatedAt,
|
||||
IsDeleted, Created, CreatedBy, LastModified, LastModifiedBy
|
||||
) VALUES (
|
||||
@WeekDefinitionId, @PackageId, 0, 0, 0, 0, NULL,
|
||||
0, @CalculatedAt, 'SP', @CalculatedAt, 'SP'
|
||||
);
|
||||
SET @PoolId = SCOPE_IDENTITY();
|
||||
SET @TotalPoolAmount = 0;
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 2. بررسی محاسبات قبلی
|
||||
-- 2. بررسی محاسبات قبلی (بر اساس همین Pool نه کل هفته)
|
||||
-- =============================================
|
||||
IF EXISTS (SELECT 1 FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId)
|
||||
IF EXISTS (SELECT 1 FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId)
|
||||
BEGIN
|
||||
IF @ForceRecalculate = 0
|
||||
BEGIN
|
||||
SET @ErrorMessage = N'کمیسیون هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید';
|
||||
SET @ErrorMessage = N'کمیسیون هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20))
|
||||
+ N' (پکیج ' + CAST(@PackageId AS NVARCHAR(20)) + N') قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید';
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 3. Revert کیف پولها (در صورت ForceRecalculate)
|
||||
-- 3. Revert کیف پولها (در صورت ForceRecalculate، فقط پرداختهای این Pool)
|
||||
-- =============================================
|
||||
|
||||
-- کم کردن مقادیر قبلی از NetworkBalance
|
||||
@@ -59,25 +70,25 @@ BEGIN
|
||||
(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)
|
||||
AND wl.RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId)
|
||||
), 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)
|
||||
WHERE wl.RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId)
|
||||
);
|
||||
|
||||
-- حذف لاگهای تغییرات قبلی
|
||||
DELETE FROM CMS.UserWalletChangeLogs
|
||||
WHERE RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId);
|
||||
WHERE RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId);
|
||||
|
||||
-- حذف تاریخچه پرداخت
|
||||
DELETE FROM CMS.CommissionPayoutHistories
|
||||
WHERE UserCommissionPayoutId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId);
|
||||
WHERE UserCommissionPayoutId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId);
|
||||
|
||||
-- حذف پرداختهای قبلی
|
||||
DELETE FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId;
|
||||
-- حذف پرداختهای قبلی این Pool
|
||||
DELETE FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId;
|
||||
|
||||
-- Reset وضعیت Pool
|
||||
UPDATE CMS.WeeklyCommissionPools
|
||||
@@ -90,8 +101,12 @@ BEGIN
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM CMS.NetworkWeeklyBalances WHERE WeekDefinitionId = @WeekDefinitionId)
|
||||
BEGIN
|
||||
SET @ErrorMessage = N'ابتدا باید تعادلهای هفتگی محاسبه شوند. sp_CalculateWeeklyBalances را اجرا کنید';
|
||||
RAISERROR(@ErrorMessage, 16, 1);
|
||||
-- بالانسی محاسبه نشده — pool را به عنوان "محاسبه شده بدون پرداخت" علامت میزنیم
|
||||
UPDATE CMS.WeeklyCommissionPools
|
||||
SET IsCalculated = 1, CalculatedAt = @CalculatedAt
|
||||
WHERE Id = @PoolId;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
RETURN;
|
||||
END
|
||||
|
||||
@@ -104,7 +119,7 @@ BEGIN
|
||||
|
||||
IF @TotalBalances IS NULL OR @TotalBalances = 0
|
||||
BEGIN
|
||||
-- هیچ تعادلی برای توزیع وجود ندارد
|
||||
-- هیچ تعادلی برای توزیع وجود ندارد — pool محاسبه شده اما پرداختی ندارد
|
||||
UPDATE CMS.WeeklyCommissionPools
|
||||
SET IsCalculated = 1, CalculatedAt = @CalculatedAt
|
||||
WHERE Id = @PoolId;
|
||||
@@ -174,23 +189,23 @@ BEGIN
|
||||
LastModifiedBy
|
||||
)
|
||||
SELECT
|
||||
cp.Id, -- UserCommissionPayoutId
|
||||
cp.UserId, -- UserId
|
||||
cp.Id,
|
||||
cp.UserId,
|
||||
@WeekDefinitionId,
|
||||
0, -- AmountBefore
|
||||
cp.TotalAmount, -- AmountAfter
|
||||
0,
|
||||
cp.TotalAmount,
|
||||
0, -- OldStatus: Pending
|
||||
0, -- NewStatus: Pending
|
||||
0, -- Action: Created (assuming enum value)
|
||||
'SP', -- PerformedBy
|
||||
N'محاسبه اولیه توسط SP', -- Reason
|
||||
0, -- IsDeleted
|
||||
0, -- Action: Created
|
||||
'SP',
|
||||
N'محاسبه اولیه توسط SP',
|
||||
0,
|
||||
@CalculatedAt,
|
||||
'SP',
|
||||
@CalculatedAt,
|
||||
'SP'
|
||||
FROM CMS.UserCommissionPayouts cp
|
||||
WHERE cp.WeekDefinitionId = @WeekDefinitionId;
|
||||
WHERE cp.WeeklyPoolId = @PoolId;
|
||||
|
||||
-- =============================================
|
||||
-- 9. شارژ کیف پول کاربران
|
||||
@@ -201,7 +216,7 @@ BEGIN
|
||||
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;
|
||||
WHERE cp.WeeklyPoolId = @PoolId;
|
||||
|
||||
-- ثبت لاگ تغییرات کیف پول
|
||||
INSERT INTO CMS.UserWalletChangeLogs (
|
||||
@@ -222,22 +237,22 @@ BEGIN
|
||||
)
|
||||
SELECT
|
||||
uw.Id,
|
||||
uw.Balance, -- CurrentBalance
|
||||
uw.Balance,
|
||||
0, -- ChangeValue (no change to main balance)
|
||||
uw.NetworkBalance, -- CurrentNetworkBalance (after update)
|
||||
cp.TotalAmount, -- ChangeNerworkValue
|
||||
uw.DiscountBalance, -- CurrentDiscountBalance
|
||||
uw.DiscountBalance,
|
||||
0, -- ChangeDiscountValue
|
||||
1, -- IsIncrease: true
|
||||
cp.Id, -- RefrenceId: PayoutId
|
||||
0, -- IsDeleted
|
||||
0,
|
||||
@CalculatedAt,
|
||||
'SP',
|
||||
@CalculatedAt,
|
||||
'SP'
|
||||
FROM CMS.UserCommissionPayouts cp
|
||||
INNER JOIN CMS.UserWallets uw ON uw.UserId = cp.UserId
|
||||
WHERE cp.WeekDefinitionId = @WeekDefinitionId;
|
||||
WHERE cp.WeeklyPoolId = @PoolId;
|
||||
|
||||
-- =============================================
|
||||
-- 10. بروزرسانی Pool
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.0.193</Version>
|
||||
<Version>0.0.195</Version>
|
||||
<DebugType>None</DebugType>
|
||||
<DebugSymbols>False</DebugSymbols>
|
||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||
|
||||
@@ -212,6 +212,11 @@ message GetWeeklyCommissionPoolResponse
|
||||
google.protobuf.Timestamp created = 9;
|
||||
int64 package_id = 10;
|
||||
string package_title = 11;
|
||||
// Q29: اعتبارسنجی TotalPoolAmount
|
||||
bool validation_is_valid = 12;
|
||||
int64 validation_expected_amount = 13;
|
||||
int64 validation_discrepancy = 14;
|
||||
int32 validation_activation_count = 15;
|
||||
}
|
||||
|
||||
// GetUserCommissionPayouts Query
|
||||
|
||||
@@ -205,6 +205,10 @@ message NetworkTreeNodeModel
|
||||
string position = 16; // Root/Left/Right position indicator
|
||||
NetworkTreeNodeModel left_child = 17; // Left child node
|
||||
NetworkTreeNodeModel right_child = 18; // Right child node
|
||||
// فیلدهای اطلاعات پکیج و نوع فعالسازی
|
||||
google.protobuf.BoolValue is_new_activation = 22; // true=اولین فعالسازی، false=خرید مجدد، null=بدون Cycle
|
||||
string package_name = 23; // نام پکیج موثر
|
||||
google.protobuf.Int64Value package_id = 24; // شناسه پکیج
|
||||
}
|
||||
|
||||
// GetHistory Query
|
||||
|
||||
@@ -51,7 +51,11 @@ public class CommissionProfile : IRegister
|
||||
: null)
|
||||
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created))
|
||||
.Map(dest => dest.PackageId, src => src.PackageId)
|
||||
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
|
||||
.Map(dest => dest.PackageTitle, src => src.PackageTitle)
|
||||
.Map(dest => dest.ValidationIsValid, src => src.ValidationIsValid)
|
||||
.Map(dest => dest.ValidationExpectedAmount, src => src.ValidationExpectedAmount)
|
||||
.Map(dest => dest.ValidationDiscrepancy, src => src.ValidationDiscrepancy)
|
||||
.Map(dest => dest.ValidationActivationCount, src => src.ValidationActivationCount);
|
||||
|
||||
// GetAvailableWeeks Response Mapping
|
||||
config.NewConfig<GetAvailableWeeksResponseDto, GetAvailableWeeksResponse>()
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProducts;
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Mapster;
|
||||
using ProtoDiscountProductDto = CMSMicroservice.Protobuf.Protos.DiscountProduct.DiscountProductDto;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Common.Mappings;
|
||||
|
||||
public class DiscountProductProfile : IRegister
|
||||
{
|
||||
void IRegister.Register(TypeAdapterConfig config)
|
||||
{
|
||||
// GetDiscountProductsRequest (proto) → GetDiscountProductsQuery (CQRS)
|
||||
config.NewConfig<GetDiscountProductsRequest, GetDiscountProductsQuery>()
|
||||
.Map(dest => dest.SearchTerm,
|
||||
src => string.IsNullOrEmpty(src.SearchQuery) ? null : src.SearchQuery)
|
||||
.Map(dest => dest.CategoryId,
|
||||
src => src.CategoryId != null ? src.CategoryId.Value : (long?)null)
|
||||
.Map(dest => dest.IsActive,
|
||||
src => src.IsActive != null ? src.IsActive.Value : (bool?)null)
|
||||
.Map(dest => dest.MinPrice,
|
||||
src => src.MinPrice != null ? (int?)src.MinPrice.Value : null)
|
||||
.Map(dest => dest.MaxPrice,
|
||||
src => src.MaxPrice != null ? (int?)src.MaxPrice.Value : null)
|
||||
.Map(dest => dest.PaginationQuery, src => new PaginationState
|
||||
{
|
||||
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
|
||||
PageSize = src.PageSize > 0 ? src.PageSize : 12
|
||||
});
|
||||
|
||||
// GetDiscountProductsResponseDto (CQRS) → GetDiscountProductsResponse (proto)
|
||||
config.NewConfig<GetDiscountProductsResponseDto, GetDiscountProductsResponse>()
|
||||
.MapWith(src => MapToResponse(src));
|
||||
}
|
||||
|
||||
private static GetDiscountProductsResponse MapToResponse(GetDiscountProductsResponseDto src)
|
||||
{
|
||||
var response = new GetDiscountProductsResponse();
|
||||
|
||||
if (src.MetaData != null)
|
||||
{
|
||||
response.MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
|
||||
{
|
||||
TotalCount = src.MetaData.TotalCount,
|
||||
PageSize = src.MetaData.PageSize,
|
||||
CurrentPage = src.MetaData.CurrentPage,
|
||||
TotalPage = src.MetaData.TotalPage
|
||||
};
|
||||
}
|
||||
|
||||
if (src.Models != null)
|
||||
{
|
||||
foreach (var p in src.Models)
|
||||
{
|
||||
response.Models.Add(new ProtoDiscountProductDto
|
||||
{
|
||||
Id = p.Id,
|
||||
Title = p.Title ?? string.Empty,
|
||||
ShortInfomation = p.ShortInfomation ?? string.Empty,
|
||||
Price = p.Price,
|
||||
MaxDiscountPercent = p.MaxDiscountPercent,
|
||||
ImagePath = p.ImagePath ?? string.Empty,
|
||||
ThumbnailPath = p.ThumbnailPath ?? string.Empty,
|
||||
RemainingCount = p.RemainingCount,
|
||||
ViewCount = p.ViewCount,
|
||||
IsActive = p.IsActive,
|
||||
Created = Timestamp.FromDateTime(DateTime.UtcNow)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,10 @@ public class NetworkMembershipProfile : IRegister
|
||||
IsActive = true,
|
||||
IsClubActive = node.IsClubActive,
|
||||
ActivationWeekDefinitionId = node.ActivationWeekDefinitionId ?? 0,
|
||||
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek
|
||||
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek,
|
||||
PackageName = node.PackageName ?? string.Empty,
|
||||
PackageId = node.PackageId.HasValue ? node.PackageId.Value : null,
|
||||
IsNewActivation = node.IsNewActivation.HasValue ? node.IsNewActivation.Value : null
|
||||
};
|
||||
|
||||
if (parentId.HasValue)
|
||||
|
||||
Reference in New Issue
Block a user