From da1a2fcf8ed046e10478e3e3291dbe79f2817057 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Mon, 18 May 2026 00:36:05 +0330 Subject: [PATCH] =?UTF-8?q?feat(commission):=20add=20BulkCreditPayouts=20?= =?UTF-8?q?=E2=80=94=20credit=20wallets=20for=20pending=20payouts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - commission.proto: add BulkCreditPayouts rpc + request/response messages; bump version to 0.0.198 - BulkCreditPayoutsCommand + Handler: load all Pending payouts for given week, credit UserWallet.Balance for each user, create UserWalletHistory + CommissionPayoutHistory records, set Status=Paid + PaidAt - CommissionService: wire up BulkCreditPayouts gRPC method - CommissionProfile: add Mapster mappings for request/result ↔ proto types Previously ProcessUserPayouts created records with Status=Pending but never credited wallets, making CustomerWithdrawBalance unreachable (it requires Status==Paid). Co-authored-by: Cursor --- .../BulkCreditPayoutsCommand.cs | 12 ++ .../BulkCreditPayoutsCommandHandler.cs | 119 ++++++++++++++++++ .../CMSMicroservice.Protobuf.csproj | 2 +- .../Protos/commission.proto | 21 ++++ .../Common/Mappings/CommissionProfile.cs | 11 ++ .../Services/CommissionService.cs | 6 + 6 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommand.cs create mode 100644 src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommandHandler.cs diff --git a/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommand.cs b/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommand.cs new file mode 100644 index 0000000..a1ddd48 --- /dev/null +++ b/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommand.cs @@ -0,0 +1,12 @@ +namespace CMSMicroservice.Application.CommissionCQ.Commands.BulkCreditPayouts; + +public class BulkCreditPayoutsCommand : IRequest +{ + public long WeekDefinitionId { get; set; } +} + +public class BulkCreditPayoutsResult +{ + public int CreditedCount { get; set; } + public long TotalCredited { get; set; } +} diff --git a/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommandHandler.cs b/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommandHandler.cs new file mode 100644 index 0000000..6d1e840 --- /dev/null +++ b/src/CMSMicroservice.Application/CommissionCQ/Commands/BulkCreditPayouts/BulkCreditPayoutsCommandHandler.cs @@ -0,0 +1,119 @@ +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Entities.Commission; +using CMSMicroservice.Domain.Entities.History; +using CMSMicroservice.Domain.Enums; +using Microsoft.Extensions.Logging; + +namespace CMSMicroservice.Application.CommissionCQ.Commands.BulkCreditPayouts; + +public class BulkCreditPayoutsCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly ICurrentUserService _currentUser; + private readonly ILogger _logger; + + public BulkCreditPayoutsCommandHandler( + IApplicationDbContext context, + ICurrentUserService currentUser, + ILogger logger) + { + _context = context; + _currentUser = currentUser; + _logger = logger; + } + + public async Task Handle(BulkCreditPayoutsCommand request, CancellationToken cancellationToken) + { + // بارگذاری همه پرداخت‌های Pending این هفته + var pendingPayouts = await _context.UserCommissionPayouts + .Where(p => p.WeekDefinitionId == request.WeekDefinitionId + && p.Status == CommissionPayoutStatus.Pending + && !p.IsDeleted) + .ToListAsync(cancellationToken); + + if (!pendingPayouts.Any()) + { + return new BulkCreditPayoutsResult { CreditedCount = 0, TotalCredited = 0 }; + } + + // بارگذاری کیف پول همه کاربران مربوطه (یک query) + var userIds = pendingPayouts.Select(p => 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, + UserId = payout.UserId, + WeekDefinitionId = payout.WeekDefinitionId, + AmountBefore = 0, + AmountAfter = payout.TotalAmount, + OldStatus = CommissionPayoutStatus.Pending, + NewStatus = CommissionPayoutStatus.Paid, + Action = CommissionPayoutAction.Paid, + PerformedBy = 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); + + _logger.LogInformation( + "BulkCreditPayouts completed: Week={WeekId}, Credited={Count}, TotalAmount={Total}, By={By}", + request.WeekDefinitionId, creditedCount, totalCredited, performedBy); + + return new BulkCreditPayoutsResult + { + CreditedCount = creditedCount, + TotalCredited = totalCredited + }; + } +} diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index 2f94a67..b4e054c 100644 --- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj +++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj @@ -3,7 +3,7 @@ net9.0 enable enable - 0.0.197 + 0.0.198 None False False diff --git a/src/CMSMicroservice.Protobuf/Protos/commission.proto b/src/CMSMicroservice.Protobuf/Protos/commission.proto index 87d96cd..96cc28f 100644 --- a/src/CMSMicroservice.Protobuf/Protos/commission.proto +++ b/src/CMSMicroservice.Protobuf/Protos/commission.proto @@ -137,6 +137,14 @@ service CommissionContract get: "/Commission/GetWithdrawalReports" }; }; + + // Bulk credit wallet for all Pending payouts of a week + rpc BulkCreditPayouts(BulkCreditPayoutsRequest) returns (BulkCreditPayoutsResponse){ + option (google.api.http) = { + post: "/Commission/BulkCreditPayouts" + body: "*" + }; + }; } // ============ Commands ============ @@ -608,6 +616,19 @@ message WeekDefinitionItem string end_date_persian = 13; // Frontend expects Persian date string } +// BulkCreditPayouts Command +message BulkCreditPayoutsRequest +{ + int64 week_definition_id = 1; // Credit all Pending payouts for this week +} + +message BulkCreditPayoutsResponse +{ + int32 credited_count = 1; // تعداد پرداخت‌هایی که به کیف پول واریز شد + int64 total_credited = 2; // مجموع مبلغ واریز شده (ریال) + string message = 3; +} + // ============ Customer APIs ============ // GetMyCommissionPayouts - for frontend customer display diff --git a/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs b/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs index ec6daf5..dea7624 100644 --- a/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs +++ b/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs @@ -1,5 +1,6 @@ using System.Linq; using CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation; +using CMSMicroservice.Application.CommissionCQ.Commands.BulkCreditPayouts; using CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools; using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances; using CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances; @@ -20,6 +21,16 @@ public class CommissionProfile : IRegister { public void Register(TypeAdapterConfig config) { + // BulkCreditPayouts Request Mapping (proto → command) + config.NewConfig() + .Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId); + + // BulkCreditPayouts Response Mapping (result → proto) + config.NewConfig() + .Map(dest => dest.CreditedCount, src => src.CreditedCount) + .Map(dest => dest.TotalCredited, src => src.TotalCredited) + .Map(dest => dest.Message, src => $"{src.CreditedCount} کاربر، مجموعاً {src.TotalCredited:N0} ریال به کیف پول واریز شد"); + // GetAvailableWeeks Request Mapping config.NewConfig() .Map(dest => dest.FutureWeeksCount, src => src.FutureWeeksCount > 0 ? src.FutureWeeksCount : 4) diff --git a/src/CMSMicroservice.WebApi/Services/CommissionService.cs b/src/CMSMicroservice.WebApi/Services/CommissionService.cs index 2b7990b..64ebbbf 100644 --- a/src/CMSMicroservice.WebApi/Services/CommissionService.cs +++ b/src/CMSMicroservice.WebApi/Services/CommissionService.cs @@ -3,6 +3,7 @@ using CMSMicroservice.WebApi.Common.Services; using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances; using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool; using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts; +using CMSMicroservice.Application.CommissionCQ.Commands.BulkCreditPayouts; using CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal; using CMSMicroservice.Application.CommissionCQ.Commands.ProcessWithdrawal; using CMSMicroservice.Application.CommissionCQ.Commands.ApproveWithdrawal; @@ -131,6 +132,11 @@ public class CommissionService : CommissionContract.CommissionContractBase return await _dispatchRequestToCQRS.Handle(request, context); } + public override async Task BulkCreditPayouts(BulkCreditPayoutsRequest request, ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle(request, context); + } + public override async Task GetWithdrawalReports(GetWithdrawalReportsRequest request, ServerCallContext context) { return await _dispatchRequestToCQRS.Handle(request, context);