feat(commission): add BulkCreditPayouts — credit wallets for pending payouts
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 10m35s

- 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 <cursoragent@cursor.com>
This commit is contained in:
masoodafar-web
2026-05-18 00:36:05 +03:30
parent cdd124ac25
commit da1a2fcf8e
6 changed files with 170 additions and 1 deletions
@@ -0,0 +1,12 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.BulkCreditPayouts;
public class BulkCreditPayoutsCommand : IRequest<BulkCreditPayoutsResult>
{
public long WeekDefinitionId { get; set; }
}
public class BulkCreditPayoutsResult
{
public int CreditedCount { get; set; }
public long TotalCredited { get; set; }
}
@@ -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<BulkCreditPayoutsCommand, BulkCreditPayoutsResult>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
private readonly ILogger<BulkCreditPayoutsCommandHandler> _logger;
public BulkCreditPayoutsCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser,
ILogger<BulkCreditPayoutsCommandHandler> logger)
{
_context = context;
_currentUser = currentUser;
_logger = logger;
}
public async Task<BulkCreditPayoutsResult> 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<UserWalletHistory>();
var payoutHistories = new List<CommissionPayoutHistory>();
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
};
}
}
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.197</Version>
<Version>0.0.198</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -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
@@ -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<BulkCreditPayoutsRequest, BulkCreditPayoutsCommand>()
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId);
// BulkCreditPayouts Response Mapping (result → proto)
config.NewConfig<BulkCreditPayoutsResult, BulkCreditPayoutsResponse>()
.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<GetAvailableWeeksRequest, GetAvailableWeeksQuery>()
.Map(dest => dest.FutureWeeksCount, src => src.FutureWeeksCount > 0 ? src.FutureWeeksCount : 4)
@@ -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<GetWeekDefinitionsRequest, GetWeekDefinitionsQuery, GetWeekDefinitionsResponse>(request, context);
}
public override async Task<BulkCreditPayoutsResponse> BulkCreditPayouts(BulkCreditPayoutsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<BulkCreditPayoutsRequest, BulkCreditPayoutsCommand, BulkCreditPayoutsResponse>(request, context);
}
public override async Task<GetWithdrawalReportsResponse> GetWithdrawalReports(GetWithdrawalReportsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetWithdrawalReportsRequest, GetWithdrawalReportsQuery, GetWithdrawalReportsResponse>(request, context);