Handler.cs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Domain.Entities.Store.ValueObject;
  4. using Microsoft.EntityFrameworkCore;
  5. using SharedKernel.Results;
  6. namespace Application.Features.Admin.Store.Order.Get;
  7. public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Result<Response>>
  8. {
  9. public async Task<Result<Response>> Handle(Query request, CancellationToken ct)
  10. {
  11. var order = await db.Order
  12. .AsNoTracking()
  13. .Include(o => o.Member)
  14. .Include(o => o.Channel)
  15. .Include(o => o.Items).ThenInclude(i => i.Product)
  16. .Include(o => o.Items).ThenInclude(i => i.IssuedCouponCode)
  17. .Include(o => o.Refunds)
  18. .Where(o => o.ID == request.OrderID)
  19. .FirstOrDefaultAsync(ct);
  20. if (order is null)
  21. {
  22. return Result.Failure<Response>(Error.NotFound("Order.NotFound", "주문을 찾을 수 없습니다."));
  23. }
  24. var shipment = await db.Shipment
  25. .AsNoTracking()
  26. .Where(s => s.OrderID == order.ID)
  27. .Select(s => new Response.ShipmentInfo(
  28. s.ID,
  29. s.Status,
  30. s.Carrier,
  31. s.TrackingNumber,
  32. s.ShippedAt,
  33. s.DeliveredAt
  34. ))
  35. .FirstOrDefaultAsync(ct);
  36. // OrderItem 별 발급 쿠폰 코드 일괄 조회 (N+1 방지).
  37. // MemberInventory 에 OrderItemID 별 Quantity 만큼의 행이 있음. CouponCode FK 조인.
  38. var orderItemIds = order.Items.Select(i => i.ID).ToList();
  39. 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
  40. {
  41. m.OrderItemID,
  42. CouponCodeID = m.CouponCode!.ID,
  43. Code = m.CouponCode.Code,
  44. m.CouponCode.UsedAt,
  45. IsUsed = m.CouponCode.Status == CouponCodeStatus.Used
  46. }).ToListAsync(ct);
  47. // 환불 요청에 이미 포함된 CouponCodeID → OrderRefund.ID 매핑 (체크박스 disable 용)
  48. var orderRefundIds = order.Refunds.Select(r => r.ID).ToList();
  49. var refundedCouponMap = await db.OrderRefundItem.AsNoTracking()
  50. .Where(x => orderRefundIds.Contains(x.OrderRefundID))
  51. .Select(x => new { x.CouponCodeID, x.OrderRefundID })
  52. .ToDictionaryAsync(x => x.CouponCodeID, x => (int?)x.OrderRefundID, ct);
  53. var couponMap = inventoryRows.GroupBy(x => x.OrderItemID).ToDictionary(g => g.Key, g => (IReadOnlyList<Response.CouponDetail>)g.Select(x => new Response.CouponDetail(
  54. CouponCodeID: x.CouponCodeID,
  55. Code: x.Code,
  56. UsedAt: x.UsedAt,
  57. IsUsed: x.IsUsed,
  58. RefundRequestID: refundedCouponMap.GetValueOrDefault(x.CouponCodeID)
  59. )).ToList());
  60. var items = order.Items.Select(i => new Response.ItemRow(
  61. ID: i.ID,
  62. ProductID: i.ProductID,
  63. ProductName: i.Product?.Name ?? "-",
  64. TypeText: i.Product?.Type == ProductType.Digital ? "쿠폰" : "실물",
  65. Quantity: i.Quantity,
  66. UnitPrice: i.UnitPrice,
  67. LineTotal: i.UnitPrice * i.Quantity,
  68. IssuedCouponCodeID: i.IssuedCouponCodeID,
  69. IssuedCouponCode: i.IssuedCouponCode?.Code,
  70. CouponCodes: couponMap.GetValueOrDefault(i.ID, [])
  71. )).ToList();
  72. // 환불 이력의 OrderRefundItem (포함 쿠폰) 일괄 조회.
  73. var refundItemRows = await db.OrderRefundItem.AsNoTracking()
  74. .Include(x => x.CouponCode)
  75. .Where(x => orderRefundIds.Contains(x.OrderRefundID))
  76. .Select(x => new
  77. {
  78. x.OrderRefundID,
  79. Code = x.CouponCode!.Code,
  80. x.PostRefundAction
  81. })
  82. .ToListAsync(ct);
  83. var refundItemMap = refundItemRows.GroupBy(x => x.OrderRefundID).ToDictionary(g => g.Key, g => (IReadOnlyList<Response.RefundCoupon>)g.Select(x => new Response.RefundCoupon(x.Code, x.PostRefundAction)).ToList());
  84. var refunds = order.Refunds.OrderByDescending(r => r.RequestedAt).Select(r => new Response.RefundRow(
  85. ID: r.ID,
  86. Type: r.Type,
  87. Status: r.Status,
  88. Amount: r.Amount,
  89. Reason: r.Reason,
  90. AdminMemo: r.AdminMemo,
  91. RequestedAt: r.RequestedAt,
  92. ResolvedAt: r.ResolvedAt,
  93. Coupons: refundItemMap.GetValueOrDefault(r.ID, [])
  94. )).ToList();
  95. return Result.Success(new Response(
  96. ID: order.ID,
  97. OrderNumber: order.OrderNumber,
  98. MemberID: order.MemberID,
  99. MemberEmail: order.Member?.Email ?? "-",
  100. ChannelID: order.ChannelID,
  101. ChannelName: order.Channel?.Name,
  102. TotalAmount: order.TotalAmount,
  103. PlatformFeeAmount: order.PlatformFeeAmount,
  104. GameRevenueAmount: order.GameRevenueAmount,
  105. ChannelRewardAmount: order.ChannelRewardAmount,
  106. Status: order.Status,
  107. CancelReason: order.CancelReason,
  108. RefundReason: order.RefundReason,
  109. CreatedAt: order.CreatedAt,
  110. PaidAt: order.PaidAt,
  111. Items: items,
  112. Refunds: refunds,
  113. Shipment: shipment
  114. ));
  115. }
  116. }