using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Developers.ValueObject; using Microsoft.EntityFrameworkCore; namespace Application.Features.Admin.Developers.Purchases.Search; internal sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { var page = Math.Max(1, request.PageNum); var size = Math.Clamp(request.PerPage, 1, 100); var query = db.ApiPurchase.AsNoTracking().AsQueryable(); if (request.Status.HasValue) { query = query.Where(c => c.Status == request.Status.Value); } if (!string.IsNullOrWhiteSpace(request.Keyword)) { var keyword = request.Keyword.Trim(); var upper = keyword.ToUpperInvariant(); query = query.Where(c => c.OrderID == keyword || c.Channel!.DonationCode == upper || c.Game!.Code == keyword || c.Channel!.Name.Contains(keyword)); } if (request.From.HasValue) { query = query.Where(c => c.CreatedAt >= request.From.Value); } if (request.To.HasValue) { var to = request.To.Value.Date.AddDays(1); query = query.Where(c => c.CreatedAt < to); } if (request.OnlyMismatch) { query = query.Where(c => c.PriceMismatched); } var total = await query.CountAsync(ct); // 집계 (검색 조건 범위) — 이상 패턴 검토용 var summaryRaw = await query .GroupBy(c => 1) .Select(g => new { TotalOrderPrice = g.Sum(c => (long)c.OrderPrice), TotalCommission = g.Sum(c => (long)c.CommissionAmount), PendingCount = g.Count(c => c.Status == ApiPurchaseStatus.Pending), ConfirmedCount = g.Count(c => c.Status == ApiPurchaseStatus.Confirmed), CanceledCount = g.Count(c => c.Status == ApiPurchaseStatus.Canceled), MismatchCount = g.Count(c => c.PriceMismatched) }) .FirstOrDefaultAsync(ct); var totalShortfall = await db.ApiPurchaseCancel.AsNoTracking() .Where(x => query.Any(c => c.ID == x.PurchaseID)) .SumAsync(x => (long?)x.ShortfallAmount, ct) ?? 0; var summary = new Response.SummaryData( summaryRaw?.TotalOrderPrice ?? 0, summaryRaw?.TotalCommission ?? 0, summaryRaw?.PendingCount ?? 0, summaryRaw?.ConfirmedCount ?? 0, summaryRaw?.CanceledCount ?? 0, summaryRaw?.MismatchCount ?? 0, totalShortfall ); var list = await query .OrderByDescending(c => c.ID) .Skip((page - 1) * size) .Take(size) .Select(c => new Response.Row( c.ID, c.Application!.Name, c.Channel!.Name, c.Channel!.DonationCode, c.Game!.KorName, c.OrderID, c.Marketplace, c.ProductID, c.OrderPrice, c.CommissionRate, c.CommissionAmount, c.CatalogPrice, c.PriceMismatched, c.Status, c.ConfirmDueAt, c.ConfirmedAt, c.CanceledAt, c.CreatedAt, db.ApiPurchaseCancel.Where(x => x.PurchaseID == c.ID).Select(x => (bool?)x.WasConfirmed).FirstOrDefault(), db.ApiPurchaseCancel.Where(x => x.PurchaseID == c.ID).Select(x => (int?)x.RecoveredAmount).FirstOrDefault(), db.ApiPurchaseCancel.Where(x => x.PurchaseID == c.ID).Select(x => (int?)x.ShortfallAmount).FirstOrDefault() )) .ToListAsync(ct); return new Response(total, summary, list); } }