using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Donations.ValueObject; using Domain.Entities.Wallets.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Studio.Wallet.GetBalance; internal sealed class Handler(IAppDbContext db) : IQueryHandler> { private const int RecentLimit = 20; private static readonly Response Empty = new(0, 0, 0, 0, 0, []); public async Task> Handle(Query request, CancellationToken ct) { var channel = await db.Channel.AsNoTracking().FirstOrDefaultAsync(c => c.MemberID == request.MemberID && c.IsActive, ct); if (channel is null) { return Result.Success(Empty); } var wallet = await db.Wallet.AsNoTracking() .Include(w => w.Balances) .FirstOrDefaultAsync(w => w.MemberID == request.MemberID, ct); // 출금 가능 잔액 = Donation 잔액 (M) var withdrawableBalance = 0; if (wallet is not null) { var donationBal = wallet.Balances.FirstOrDefault(b => b.Type == WalletBalanceType.Donation); if (donationBal is not null) { withdrawableBalance = (int)donationBal.Amount.Value; } } // 누적 수익 (Donation NetAmount 합계) var totalEarned = await db.Donation.AsNoTracking() .Where(d => d.ReceiverMemberID == request.MemberID && !d.IsTest) .SumAsync(d => d.NetAmount, ct); // 누적 출금 (완료된 출금) var totalWithdrawn = await db.WithdrawalRequest.AsNoTracking() .Where(w => w.MemberID == request.MemberID && w.Status == WithdrawalStatus.Completed) .SumAsync(w => w.NetAmount, ct); // 누적 판매 수수료 (채널 보상 입금 합계) var totalStoreCommission = await db.WalletTransaction.AsNoTracking() .Where(t => t.Wallet!.MemberID == request.MemberID && t.BalanceType == WalletBalanceType.StoreRevenue && t.TxType == WalletTransactionType.OrderChannelReward) .SumAsync(t => (int)t.Amount.Value, ct); // 대기 출금 var pendingWithdrawal = await db.WithdrawalRequest.AsNoTracking() .Where(w => w.MemberID == request.MemberID && (w.Status == WithdrawalStatus.Pending || w.Status == WithdrawalStatus.Processing)) .SumAsync(w => w.RequestedAmount, ct); // 최근 거래 통합 (Donation 잔액 + StoreRevenue 잔액) var recentTx = await db.WalletTransaction.AsNoTracking() .Where(t => t.Wallet!.MemberID == request.MemberID && (t.BalanceType == WalletBalanceType.Donation || t.BalanceType == WalletBalanceType.StoreRevenue)) .OrderByDescending(t => t.CreatedAt) .Take(RecentLimit) .Select(t => new { t.ID, t.TxType, t.BalanceType, Amount = (int)t.Amount.Value, Balance = (int)t.BalanceAfter.Value, t.RefID, t.Memo, t.CreatedAt }) .ToListAsync(ct); var unified = recentTx.Select(t => new UnifiedTransactionItem( t.ID, MapCategory(t.TxType, t.BalanceType), t.TxType.ToString(), t.Amount, t.Balance, t.RefID, t.Memo ?? "", t.CreatedAt )).ToList(); return Result.Success(new Response( withdrawableBalance, totalEarned, totalWithdrawn, totalStoreCommission, pendingWithdrawal, unified )); } // 거래 카테고리 분류 — Frontend 필터 키 (전체/후원/수수료) private static string MapCategory(WalletTransactionType txType, WalletBalanceType balanceType) => txType switch { WalletTransactionType.DonationIn => "donation", WalletTransactionType.OrderChannelReward or WalletTransactionType.OrderRefundChannelReward or WalletTransactionType.WithdrawalStoreRevenue => "commission", _ when balanceType == WalletBalanceType.Donation => "donation", _ when balanceType == WalletBalanceType.StoreRevenue => "commission", _ => "other" }; }