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); 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; +*/