using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Store.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Admin.Store.Order.Get; public sealed class Handler(IAppDbContext db) : IQueryHandler> { public async Task> Handle(Query request, CancellationToken ct) { var order = await db.Order .AsNoTracking() .Include(o => o.Member) .Include(o => o.Channel) .Include(o => o.Items).ThenInclude(i => i.Product) .Include(o => o.Items).ThenInclude(i => i.IssuedCouponCode) .Include(o => o.Refunds) .Where(o => o.ID == request.OrderID) .FirstOrDefaultAsync(ct); if (order is null) { return Result.Failure(Error.NotFound("Order.NotFound", "주문을 찾을 수 없습니다.")); } var shipment = await db.Shipment .AsNoTracking() .Where(s => s.OrderID == order.ID) .Select(s => new Response.ShipmentInfo( s.ID, s.Status, s.Carrier, s.TrackingNumber, s.ShippedAt, s.DeliveredAt )) .FirstOrDefaultAsync(ct); // OrderItem 별 발급 쿠폰 코드 일괄 조회 (N+1 방지). // MemberInventory 에 OrderItemID 별 Quantity 만큼의 행이 있음. CouponCode FK 조인. var orderItemIds = order.Items.Select(i => i.ID).ToList(); var inventoryRows = await db.MemberInventory.AsNoTracking().Where(m => orderItemIds.Contains(m.OrderItemID) && m.CouponCode != null).OrderBy(m => m.OrderItemID).ThenBy(m => m.ID).Select(m => new { m.OrderItemID, CouponCodeID = m.CouponCode!.ID, Code = m.CouponCode.Code, m.CouponCode.UsedAt, IsUsed = m.CouponCode.Status == CouponCodeStatus.Used }).ToListAsync(ct); // 환불 요청에 이미 포함된 CouponCodeID → OrderRefund.ID 매핑 (체크박스 disable 용) var orderRefundIds = order.Refunds.Select(r => r.ID).ToList(); var refundedCouponMap = await db.OrderRefundItem.AsNoTracking() .Where(x => orderRefundIds.Contains(x.OrderRefundID)) .Select(x => new { x.CouponCodeID, x.OrderRefundID }) .ToDictionaryAsync(x => x.CouponCodeID, x => (int?)x.OrderRefundID, ct); var couponMap = inventoryRows.GroupBy(x => x.OrderItemID).ToDictionary(g => g.Key, g => (IReadOnlyList)g.Select(x => new Response.CouponDetail( CouponCodeID: x.CouponCodeID, Code: x.Code, UsedAt: x.UsedAt, IsUsed: x.IsUsed, RefundRequestID: refundedCouponMap.GetValueOrDefault(x.CouponCodeID) )).ToList()); var items = order.Items.Select(i => new Response.ItemRow( ID: i.ID, ProductID: i.ProductID, ProductName: i.Product?.Name ?? "-", TypeText: i.Product?.Type == ProductType.Digital ? "쿠폰" : "실물", Quantity: i.Quantity, UnitPrice: i.UnitPrice, LineTotal: i.UnitPrice * i.Quantity, IssuedCouponCodeID: i.IssuedCouponCodeID, IssuedCouponCode: i.IssuedCouponCode?.Code, CouponCodes: couponMap.GetValueOrDefault(i.ID, []) )).ToList(); // 환불 이력의 OrderRefundItem (포함 쿠폰) 일괄 조회. var refundItemRows = await db.OrderRefundItem.AsNoTracking() .Include(x => x.CouponCode) .Where(x => orderRefundIds.Contains(x.OrderRefundID)) .Select(x => new { x.OrderRefundID, Code = x.CouponCode!.Code, x.PostRefundAction }) .ToListAsync(ct); var refundItemMap = refundItemRows.GroupBy(x => x.OrderRefundID).ToDictionary(g => g.Key, g => (IReadOnlyList)g.Select(x => new Response.RefundCoupon(x.Code, x.PostRefundAction)).ToList()); var refunds = order.Refunds.OrderByDescending(r => r.RequestedAt).Select(r => new Response.RefundRow( ID: r.ID, Type: r.Type, Status: r.Status, Amount: r.Amount, Reason: r.Reason, AdminMemo: r.AdminMemo, RequestedAt: r.RequestedAt, ResolvedAt: r.ResolvedAt, Coupons: refundItemMap.GetValueOrDefault(r.ID, []) )).ToList(); return Result.Success(new Response( ID: order.ID, OrderNumber: order.OrderNumber, MemberID: order.MemberID, MemberEmail: order.Member?.Email ?? "-", ChannelID: order.ChannelID, ChannelName: order.Channel?.Name, TotalAmount: order.TotalAmount, PlatformFeeAmount: order.PlatformFeeAmount, GameRevenueAmount: order.GameRevenueAmount, ChannelRewardAmount: order.ChannelRewardAmount, Status: order.Status, CancelReason: order.CancelReason, RefundReason: order.RefundReason, CreatedAt: order.CreatedAt, PaidAt: order.PaidAt, Items: items, Refunds: refunds, Shipment: shipment )); } }