using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Features.Api.Studio.Helpers; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Studio.Wallet.GetRevenue; internal sealed class Handler(IAppDbContext db) : IQueryHandler> { public async Task> Handle(Query request, CancellationToken ct) { var (start, end) = PeriodHelper.GetRange(request.Period); var baseQuery = db.Donation.AsNoTracking() .Where(d => d.ReceiverMemberID == request.MemberID && !d.IsTest && d.CreatedAt >= start && d.CreatedAt <= end); // Summary var total = await baseQuery.CountAsync(ct); var grossSum = total > 0 ? await baseQuery.SumAsync(d => d.Amount, ct) : 0; var feeSum = total > 0 ? await baseQuery.SumAsync(d => d.FeeAmount, ct) : 0; var netSum = total > 0 ? await baseQuery.SumAsync(d => d.NetAmount, ct) : 0; // 누적 판매 수수료 (같은 기간 — 채널 보상 입금) var totalStoreCommission = await db.WalletTransaction.AsNoTracking() .Where(t => t.Wallet!.MemberID == request.MemberID && t.BalanceType == Domain.Entities.Wallets.ValueObject.WalletBalanceType.StoreRevenue && t.TxType == Domain.Entities.Wallets.ValueObject.WalletTransactionType.OrderChannelReward && t.CreatedAt >= start && t.CreatedAt <= end) .SumAsync(t => (int)t.Amount.Value, ct); // API 결제 보고 수수료 — 기간 내 확정 입금분 var totalApiCommission = await db.WalletTransaction.AsNoTracking() .Where(t => t.Wallet!.MemberID == request.MemberID && t.BalanceType == Domain.Entities.Wallets.ValueObject.WalletBalanceType.StoreRevenue && t.TxType == Domain.Entities.Wallets.ValueObject.WalletTransactionType.ApiCommissionEarned && t.CreatedAt >= start && t.CreatedAt <= end) .SumAsync(t => (int?)t.Amount.Value, ct) ?? 0; // 보류 중(미확정) 수수료 — 기간 무관 실시간 잔여 전체. 확정 시 지갑 입금, 14일 내 취소 시 소멸 var pendingQuery = db.ApiPurchase.AsNoTracking() .Where(c => c.CreditedMemberID == request.MemberID && c.Status == Domain.Entities.Developers.ValueObject.ApiPurchaseStatus.Pending); var pendingApiCommission = await pendingQuery.SumAsync(c => (int?)c.CommissionAmount, ct) ?? 0; var pendingList = await pendingQuery .OrderBy(c => c.ConfirmDueAt) .Take(50) .Select(c => new PendingCommissionItem( c.ID, c.Game!.KorName, c.CommissionAmount, c.ConfirmDueAt, c.CreatedAt )) .ToListAsync(ct); var summary = new RevenueSummary(grossSum, feeSum, netSum, totalStoreCommission, totalApiCommission, pendingApiCommission); // Paged list var list = await baseQuery .OrderByDescending(d => d.CreatedAt) .Skip((request.Page - 1) * request.PerPage) .Take(request.PerPage) .Select(d => new RevenueItem( d.ID, d.SendName, d.Sponsor != null ? d.Sponsor.SID : null, d.Amount, d.FeeAmount, d.NetAmount, d.CrewSessionID != null ? "crew_donation" : "donation", d.CrewMember != null ? d.CrewMember.Nickname : null, d.CreatedAt )) .ToListAsync(ct); // Chart data — daily grouping var chartRaw = await baseQuery .GroupBy(d => d.CreatedAt.Date) .Select(g => new { Date = g.Key, GrossAmount = g.Sum(d => d.Amount), PlatformFee = g.Sum(d => d.FeeAmount), NetAmount = g.Sum(d => d.NetAmount) }) .OrderBy(x => x.Date) .ToListAsync(ct); var chartData = chartRaw.Select(c => new ChartItem(c.Date.ToString("MM/dd"), c.GrossAmount, c.PlatformFee, c.NetAmount)).ToList(); return Result.Success(new Response(total, summary, list, chartData, pendingList)); } }