From 5f8e30905d2dca70ec76604bd426e15e0efbb94d Mon Sep 17 00:00:00 2001 From: m-moghaddam Date: Sat, 20 Jun 2026 16:11:37 +0330 Subject: [PATCH 1/2] refactor(commission): streamline bulk credit payouts handling by removing unused wallet loading and updating payout history logic Removed unnecessary loading of user wallets and adjusted payout history creation to reflect accurate amounts before and after payment. Updated comments for clarity. --- .../BulkCreditPayoutsCommandHandler.cs | 43 ++----------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommandHandler.cs b/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommandHandler.cs index 6d1e840..a968c57 100644 --- a/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommandHandler.cs +++ b/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommandHandler.cs @@ -1,5 +1,4 @@ using CMSMicroservice.Application.Common.Interfaces; -using CMSMicroservice.Domain.Entities; using CMSMicroservice.Domain.Entities.Commission; using CMSMicroservice.Domain.Entities.History; using CMSMicroservice.Domain.Enums; @@ -25,7 +24,6 @@ public class BulkCreditPayoutsCommandHandler : IRequestHandler Handle(BulkCreditPayoutsCommand request, CancellationToken cancellationToken) { - // بارگذاری همه پرداخت‌های Pending این هفته var pendingPayouts = await _context.UserCommissionPayouts .Where(p => p.WeekDefinitionId == request.WeekDefinitionId && p.Status == CommissionPayoutStatus.Pending @@ -37,72 +35,37 @@ public class BulkCreditPayoutsCommandHandler : IRequestHandler p.UserId).Distinct().ToList(); - var wallets = await _context.UserWallets - .Where(w => userIds.Contains(w.UserId) && !w.IsDeleted) - .ToDictionaryAsync(w => w.UserId, cancellationToken); - var performedBy = _currentUser.GetPerformedBy() ?? "System"; var now = DateTime.UtcNow; - var walletHistories = new List(); var payoutHistories = new List(); var creditedCount = 0; long totalCredited = 0; foreach (var payout in pendingPayouts) { - if (!wallets.TryGetValue(payout.UserId, out var wallet)) - { - _logger.LogWarning( - "Wallet not found for UserId={UserId}, PayoutId={PayoutId} — skipping", - payout.UserId, payout.Id); - continue; - } - - var oldBalance = wallet.Balance; - - // واریز مبلغ به کیف پول - wallet.Balance += payout.TotalAmount; - - walletHistories.Add(new UserWalletHistory - { - WalletId = wallet.Id, - CurrentBalance = wallet.Balance, - CurrentNetworkBalance = wallet.NetworkBalance, - CurrentDiscountBalance = wallet.DiscountBalance, - ChangeValue = payout.TotalAmount, - ChangeNerworkValue = 0, - ChangeDiscountValue = 0, - IsIncrease = true, - RefrenceId = payout.Id - }); - - // به‌روزرسانی وضعیت پرداخت payout.Status = CommissionPayoutStatus.Paid; payout.PaidAt = now; payout.LastModified = now; payoutHistories.Add(new CommissionPayoutHistory { - UserCommissionPayoutId = payout.Id, + UserCommissionPayoutId = payout.Id, UserId = payout.UserId, WeekDefinitionId = payout.WeekDefinitionId, - AmountBefore = 0, + AmountBefore = payout.TotalAmount, AmountAfter = payout.TotalAmount, OldStatus = CommissionPayoutStatus.Pending, NewStatus = CommissionPayoutStatus.Paid, Action = CommissionPayoutAction.Paid, PerformedBy = performedBy, - Reason = $"واریز دسته‌جمعی کمیسیون هفته {payout.WeekDefinitionId} توسط {performedBy}" + Reason = $"تایید پرداخت دسته‌جمعی کمیسیون هفته {payout.WeekDefinitionId} توسط {performedBy}" }); creditedCount++; totalCredited += payout.TotalAmount; } - await _context.UserWalletHistories.AddRangeAsync(walletHistories, cancellationToken); await _context.CommissionPayoutHistories.AddRangeAsync(payoutHistories, cancellationToken); await _context.SaveChangesAsync(cancellationToken); From 37820ea25705a7863072f5018ed624509f55855e Mon Sep 17 00:00:00 2001 From: m-moghaddam Date: Sat, 20 Jun 2026 17:27:23 +0330 Subject: [PATCH 2/2] feat(db): add SQL script to fix incorrect bulk credit payouts in user wallet histories Introduced a new SQL script to identify and correct erroneous bulk credit entries in user wallet histories. The script allows for a dry run to preview changes before applying fixes, ensuring accurate balance adjustments and removal of incorrect records. --- .../FixBulkCreditPayoutsWrongBalance.sql | 341 ++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 src/dbbkup/FixBulkCreditPayoutsWrongBalance.sql diff --git a/src/dbbkup/FixBulkCreditPayoutsWrongBalance.sql b/src/dbbkup/FixBulkCreditPayoutsWrongBalance.sql new file mode 100644 index 0000000..ef4dc69 --- /dev/null +++ b/src/dbbkup/FixBulkCreditPayoutsWrongBalance.sql @@ -0,0 +1,341 @@ +/* +================================================================================ +FixBulkCreditPayoutsWrongBalance.sql +================================================================================ +شناسایی لاگ اشتباه BulkCredit در UserWalletHistories: + + RefrenceId IN (payoutهای BulkCredit شده) + AND CreatedBy <> 'SP' -- لاگ SP = NetworkBalance درست، حذف نشود + AND ChangeValue > 0 -- واریز اشتباه به Balance + AND ChangeDiscountValue <= 0 + +اصلاح: + - کسر ChangeValue از Balance همان WalletId + - حذف رکوردهای UserWalletHistories اشتباه + + @DryRun = 1 → فقط گزارش + @DryRun = 0 → اعمال fix +================================================================================ +*/ + +SET NOCOUNT ON; +SET XACT_ABORT ON; + +DECLARE @DryRun BIT = 1; +DECLARE @WeeksBack INT = 4; +DECLARE @SinceDate DATETIME = DATEADD(WEEK, -@WeeksBack, GETUTCDATE()); + +-- کاربرانی که fix برایشان اعمال می‌شود حتی اگر Balance منفی شود +DECLARE @AllowNegativeUserIds TABLE (UserId BIGINT PRIMARY KEY); +INSERT INTO @AllowNegativeUserIds (UserId) VALUES (106); -- مهرداد شجاعی + +PRINT CONCAT('DryRun = ', @DryRun, ' | WeeksBack = ', @WeeksBack); +PRINT ''; + +-- payoutهای BulkCredit شده (از CommissionPayoutHistories) +;WITH BulkCreditedPayoutIds AS ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 + AND cph.Action = 1 + AND cph.Created >= @SinceDate + AND ( + cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' + OR cph.Reason LIKE N'%واریز دسته%جمعی%' + ) +), +WrongWalletLogs AS ( + SELECT + wh.Id AS WalletHistoryId, + wh.WalletId, + wh.RefrenceId AS PayoutId, + p.UserId, + wh.ChangeValue AS WrongBalanceCredit, + wh.Created AS WrongCreditAt, + wh.CreatedBy + FROM CMS.UserWalletHistories wh + INNER JOIN CMS.UserCommissionPayouts p + ON p.Id = wh.RefrenceId AND p.IsDeleted = 0 + WHERE wh.IsDeleted = 0 + AND wh.RefrenceId IN (SELECT PayoutId FROM BulkCreditedPayoutIds) + AND wh.CreatedBy <> N'SP' + AND wh.ChangeValue > 0 + AND wh.ChangeDiscountValue <= 0 +) + +-- 1) خلاصه +SELECT + (SELECT COUNT(*) FROM BulkCreditedPayoutIds) AS BulkCreditedPayoutCount, + (SELECT COUNT(DISTINCT p.UserId) + FROM CMS.UserCommissionPayouts p + WHERE p.Id IN (SELECT PayoutId FROM BulkCreditedPayoutIds)) AS BulkCreditedUserCount, + (SELECT COUNT(*) FROM WrongWalletLogs) AS WrongWalletHistoryCount, + (SELECT COUNT(DISTINCT UserId) FROM WrongWalletLogs) AS AffectedUserCount, + (SELECT ISNULL(SUM(WrongBalanceCredit), 0) FROM WrongWalletLogs) AS TotalWrongBalanceCredit; + +PRINT ''; +PRINT '--- PayoutIdهای BulkCredit (برای مقایسه با لیست دستی) ---'; +SELECT PayoutId FROM ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 AND cph.Action = 1 AND cph.Created >= @SinceDate + AND (cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' OR cph.Reason LIKE N'%واریز دسته%جمعی%') +) x ORDER BY PayoutId; + +PRINT ''; +PRINT '--- UserWalletHistories اشتباه ---'; + +;WITH BulkCreditedPayoutIds AS ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 AND cph.Action = 1 AND cph.Created >= @SinceDate + AND (cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' OR cph.Reason LIKE N'%واریز دسته%جمعی%') +) +SELECT + wh.Id AS WalletHistoryId, + wh.RefrenceId AS PayoutId, + p.UserId, + u.Mobile, + CONCAT(u.FirstName, N' ', u.LastName) AS FullName, + p.WeekDefinitionId, + p.TotalAmount AS PayoutAmount, + wh.ChangeValue AS WrongBalanceCredit, + uw.Balance AS CurrentBalance, + uw.NetworkBalance, + wh.Created AS WrongCreditAt, + wh.CreatedBy +FROM CMS.UserWalletHistories wh +INNER JOIN CMS.UserCommissionPayouts p ON p.Id = wh.RefrenceId AND p.IsDeleted = 0 +INNER JOIN CMS.UserWallets uw ON uw.Id = wh.WalletId AND uw.IsDeleted = 0 +INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0 +WHERE wh.IsDeleted = 0 + AND wh.RefrenceId IN (SELECT PayoutId FROM BulkCreditedPayoutIds) + AND wh.CreatedBy <> N'SP' + AND wh.ChangeValue > 0 + AND wh.ChangeDiscountValue <= 0 +ORDER BY p.UserId, wh.RefrenceId; + +PRINT ''; +PRINT '--- خلاصه به تفکیک کاربر ---'; + +;WITH BulkCreditedPayoutIds AS ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 AND cph.Action = 1 AND cph.Created >= @SinceDate + AND (cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' OR cph.Reason LIKE N'%واریز دسته%جمعی%') +), +WrongWalletLogs AS ( + SELECT wh.Id AS WalletHistoryId, wh.WalletId, wh.RefrenceId AS PayoutId, + p.UserId, wh.ChangeValue AS WrongBalanceCredit + FROM CMS.UserWalletHistories wh + INNER JOIN CMS.UserCommissionPayouts p ON p.Id = wh.RefrenceId AND p.IsDeleted = 0 + WHERE wh.IsDeleted = 0 + AND wh.RefrenceId IN (SELECT PayoutId FROM BulkCreditedPayoutIds) + AND wh.CreatedBy <> N'SP' + AND wh.ChangeValue > 0 + AND wh.ChangeDiscountValue <= 0 +) +SELECT + w.UserId, + u.Mobile, + CONCAT(u.FirstName, N' ', u.LastName) AS FullName, + COUNT(w.WalletHistoryId) AS WrongHistoryCount, + COUNT(DISTINCT w.PayoutId) AS AffectedPayoutCount, + SUM(w.WrongBalanceCredit) AS TotalWrongCredit, + uw.Balance AS CurrentBalance, + uw.Balance - SUM(w.WrongBalanceCredit) AS BalanceAfterFix, + CASE + WHEN w.UserId IN (SELECT UserId FROM @AllowNegativeUserIds) + AND uw.Balance < SUM(w.WrongBalanceCredit) THEN N'OK — منفی مجاز' + WHEN uw.Balance < SUM(w.WrongBalanceCredit) THEN N'⚠ موجودی کافی نیست' + ELSE N'OK' + END AS FixStatus +FROM WrongWalletLogs w +INNER JOIN CMS.Users u ON u.Id = w.UserId AND u.IsDeleted = 0 +INNER JOIN CMS.UserWallets uw ON uw.Id = w.WalletId AND uw.IsDeleted = 0 +GROUP BY w.UserId, u.Mobile, u.FirstName, u.LastName, uw.Id, uw.Balance +ORDER BY TotalWrongCredit DESC; + +PRINT ''; +PRINT '--- blockers (به‌جز کاربران با منفی مجاز) ---'; + +;WITH BulkCreditedPayoutIds AS ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 AND cph.Action = 1 AND cph.Created >= @SinceDate + AND (cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' OR cph.Reason LIKE N'%واریز دسته%جمعی%') +), +WrongPerWallet AS ( + SELECT wh.WalletId, SUM(wh.ChangeValue) AS TotalWrongCredit + FROM CMS.UserWalletHistories wh + WHERE wh.IsDeleted = 0 + AND wh.RefrenceId IN (SELECT PayoutId FROM BulkCreditedPayoutIds) + AND wh.CreatedBy <> N'SP' + AND wh.ChangeValue > 0 + AND wh.ChangeDiscountValue <= 0 + GROUP BY wh.WalletId +) +SELECT uw.UserId, u.Mobile, uw.Balance AS CurrentBalance, + w.TotalWrongCredit, uw.Balance - w.TotalWrongCredit AS BalanceAfterFix +FROM WrongPerWallet w +INNER JOIN CMS.UserWallets uw ON uw.Id = w.WalletId AND uw.IsDeleted = 0 +INNER JOIN CMS.Users u ON u.Id = uw.UserId AND u.IsDeleted = 0 +WHERE uw.Balance < w.TotalWrongCredit + AND uw.UserId NOT IN (SELECT UserId FROM @AllowNegativeUserIds); + +PRINT ''; +PRINT '--- منفی مجاز (fix اعمال می‌شود) ---'; + +;WITH BulkCreditedPayoutIds AS ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 AND cph.Action = 1 AND cph.Created >= @SinceDate + AND (cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' OR cph.Reason LIKE N'%واریز دسته%جمعی%') +), +WrongPerWallet AS ( + SELECT wh.WalletId, SUM(wh.ChangeValue) AS TotalWrongCredit + FROM CMS.UserWalletHistories wh + WHERE wh.IsDeleted = 0 + AND wh.RefrenceId IN (SELECT PayoutId FROM BulkCreditedPayoutIds) + AND wh.CreatedBy <> N'SP' + AND wh.ChangeValue > 0 + AND wh.ChangeDiscountValue <= 0 + GROUP BY wh.WalletId +) +SELECT uw.UserId, u.Mobile, uw.Balance AS CurrentBalance, + w.TotalWrongCredit, uw.Balance - w.TotalWrongCredit AS BalanceAfterFix +FROM WrongPerWallet w +INNER JOIN CMS.UserWallets uw ON uw.Id = w.WalletId AND uw.IsDeleted = 0 +INNER JOIN CMS.Users u ON u.Id = uw.UserId AND u.IsDeleted = 0 +WHERE uw.Balance < w.TotalWrongCredit + AND uw.UserId IN (SELECT UserId FROM @AllowNegativeUserIds); + +-- ── اعمال fix ── +IF @DryRun = 1 +BEGIN + PRINT ''; + PRINT 'DryRun — تغییری اعمال نشد. برای fix: @DryRun = 0'; + RETURN; +END + +DECLARE @BlockerCount INT; +DECLARE @FixedWallets INT; +DECLARE @DeletedHistories INT; +DECLARE @TotalReverted BIGINT; +DECLARE @ErrMsg NVARCHAR(4000); + +;WITH BulkCreditedPayoutIds AS ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 AND cph.Action = 1 AND cph.Created >= @SinceDate + AND (cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' OR cph.Reason LIKE N'%واریز دسته%جمعی%') +), +WrongPerWallet AS ( + SELECT wh.WalletId, SUM(wh.ChangeValue) AS TotalWrongCredit + FROM CMS.UserWalletHistories wh + WHERE wh.IsDeleted = 0 + AND wh.RefrenceId IN (SELECT PayoutId FROM BulkCreditedPayoutIds) + AND wh.CreatedBy <> N'SP' + AND wh.ChangeValue > 0 + AND wh.ChangeDiscountValue <= 0 + GROUP BY wh.WalletId +) +SELECT @BlockerCount = COUNT(*) +FROM WrongPerWallet w +INNER JOIN CMS.UserWallets uw ON uw.Id = w.WalletId AND uw.IsDeleted = 0 +WHERE uw.Balance < w.TotalWrongCredit + AND uw.UserId NOT IN (SELECT UserId FROM @AllowNegativeUserIds); + +IF @BlockerCount > 0 +BEGIN + RAISERROR(N'%d کیف پول Balance منفی می‌شود — fix متوقف شد.', 16, 1, @BlockerCount); + RETURN; +END + +BEGIN TRY + BEGIN TRANSACTION; + + ;WITH BulkCreditedPayoutIds AS ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 AND cph.Action = 1 AND cph.Created >= @SinceDate + AND (cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' OR cph.Reason LIKE N'%واریز دسته%جمعی%') + ), + WrongPerWallet AS ( + SELECT wh.WalletId, SUM(wh.ChangeValue) AS TotalWrongCredit + FROM CMS.UserWalletHistories wh + WHERE wh.IsDeleted = 0 + AND wh.RefrenceId IN (SELECT PayoutId FROM BulkCreditedPayoutIds) + AND wh.CreatedBy <> N'SP' + AND wh.ChangeValue > 0 + AND wh.ChangeDiscountValue <= 0 + GROUP BY wh.WalletId + ) + UPDATE uw + SET uw.Balance = uw.Balance - w.TotalWrongCredit, + uw.LastModified = GETUTCDATE(), + uw.LastModifiedBy = N'FixBulkCreditPayoutsWrongBalance' + FROM CMS.UserWallets uw + INNER JOIN WrongPerWallet w ON w.WalletId = uw.Id + WHERE uw.IsDeleted = 0; + + SET @FixedWallets = @@ROWCOUNT; + + ;WITH BulkCreditedPayoutIds AS ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 AND cph.Action = 1 AND cph.Created >= @SinceDate + AND (cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' OR cph.Reason LIKE N'%واریز دسته%جمعی%') + ) + SELECT @TotalReverted = ISNULL(SUM(wh.ChangeValue), 0) + FROM CMS.UserWalletHistories wh + WHERE wh.IsDeleted = 0 + AND wh.RefrenceId IN (SELECT PayoutId FROM BulkCreditedPayoutIds) + AND wh.CreatedBy <> N'SP' + AND wh.ChangeValue > 0 + AND wh.ChangeDiscountValue <= 0; + + ;WITH BulkCreditedPayoutIds AS ( + SELECT DISTINCT cph.UserCommissionPayoutId AS PayoutId + FROM CMS.CommissionPayoutHistories cph + WHERE cph.IsDeleted = 0 AND cph.Action = 1 AND cph.Created >= @SinceDate + AND (cph.Reason LIKE N'%تایید پرداخت دسته%جمعی%' OR cph.Reason LIKE N'%واریز دسته%جمعی%') + ) + DELETE wh + FROM CMS.UserWalletHistories wh + WHERE wh.IsDeleted = 0 + AND wh.RefrenceId IN (SELECT PayoutId FROM BulkCreditedPayoutIds) + AND wh.CreatedBy <> N'SP' + AND wh.ChangeValue > 0 + AND wh.ChangeDiscountValue <= 0; + + SET @DeletedHistories = @@ROWCOUNT; + + COMMIT TRANSACTION; + + PRINT CONCAT('کیف پول اصلاح‌شده: ', @FixedWallets); + PRINT CONCAT('لاگ حذف‌شده: ', @DeletedHistories); + PRINT CONCAT('مجموع برگردانده‌شده: ', @TotalReverted); +END TRY +BEGIN CATCH + IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; + SET @ErrMsg = ERROR_MESSAGE(); + RAISERROR(@ErrMsg, 16, 1); +END CATCH; + +GO + +/* +-- کوئری ساده دستی (معادل همین منطق با لیست PayoutId ثابت): + +SELECT * +FROM CMS.UserWalletHistories +WHERE RefrenceId IN ( + 115,116,117,118,119,125,126,127,128,129,130,131,132,133,134, + 135,136,137,138,139,140,141,142,143,144,145,146,147,148,149, + 150,151,152,153 +) +AND CreatedBy <> N'SP' +AND ChangeValue > 0 +AND ChangeDiscountValue <= 0; +*/