| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Messaging;
- using Domain.Entities.Store;
- using Domain.Entities.Store.ValueObject;
- using Microsoft.EntityFrameworkCore;
- using SharedKernel.Results;
- namespace Application.Features.Admin.Store.Order.RequestCouponsRefund;
- public sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Result<int>>
- {
- public async Task<Result<int>> Handle(Command request, CancellationToken ct)
- {
- if (request.Lines is null || request.Lines.Count == 0)
- {
- return Result.Failure<int>(Error.Problem("Refund.NoCoupons", "환불할 쿠폰을 1개 이상 선택해 주세요."));
- }
- if (string.IsNullOrWhiteSpace(request.Reason))
- {
- return Result.Failure<int>(Error.Problem("Refund.ReasonRequired", "환불 사유는 필수입니다."));
- }
- var order = await db.Order
- .Include(o => o.Items)
- .FirstOrDefaultAsync(o => o.ID == request.OrderID, ct);
- if (order is null)
- {
- return Result.Failure<int>(Error.NotFound("Order.NotFound", "주문을 찾을 수 없습니다."));
- }
- // 입력 line 들의 OrderItem/CouponCode 가 모두 해당 Order 에 속하는지 + 상태 검증.
- var couponCodeIds = request.Lines.Select(l => l.CouponCodeID).Distinct().ToList();
- var orderItemIds = order.Items.Select(i => i.ID).ToHashSet();
- var inventories = await db.MemberInventory.AsNoTracking()
- .Include(m => m.CouponCode)
- .Where(m => couponCodeIds.Contains(m.CouponCodeID))
- .ToListAsync(ct);
- var inventoryByCouponCode = inventories.ToDictionary(m => m.CouponCodeID);
- // 이미 다른 OrderRefundItem 에 등록된 CouponCodeID — unique index 위반 사전 차단.
- var alreadyRefundedCouponIds = await db.OrderRefundItem.AsNoTracking()
- .Where(x => couponCodeIds.Contains(x.CouponCodeID))
- .Select(x => x.CouponCodeID)
- .ToListAsync(ct);
- if (alreadyRefundedCouponIds.Count > 0)
- {
- return Result.Failure<int>(Error.Problem("Refund.CouponAlreadyRequested", $"이미 환불 요청에 포함된 쿠폰이 있습니다: {alreadyRefundedCouponIds.Count}건"));
- }
- // OrderItem.UnitPrice 캐싱 (Line 별 단가)
- var unitPriceByItem = order.Items.ToDictionary(i => i.ID, i => i.UnitPrice);
- var totalAmount = 0;
- foreach (var line in request.Lines)
- {
- if (!inventoryByCouponCode.TryGetValue(line.CouponCodeID, out var inv) || inv.CouponCode is null)
- {
- return Result.Failure<int>(Error.NotFound("Refund.CouponNotFound", $"쿠폰 코드 #{line.CouponCodeID} 의 발급 정보가 없습니다."));
- }
- if (inv.OrderItemID != line.OrderItemID)
- {
- return Result.Failure<int>(Error.Problem("Refund.CouponItemMismatch", $"쿠폰 코드 #{line.CouponCodeID} 가 OrderItem #{line.OrderItemID} 에 속하지 않습니다."));
- }
- if (!orderItemIds.Contains(line.OrderItemID))
- {
- return Result.Failure<int>(Error.Problem("Refund.OrderItemMismatch", $"OrderItem #{line.OrderItemID} 가 주문 #{request.OrderID} 에 속하지 않습니다."));
- }
- // PostRefundAction=Restore 인데 이미 Used → 거부 (도메인 메서드와 동일 정책)
- if (line.PostRefundAction == CouponPostRefundAction.Restore && inv.CouponCode.Status == CouponCodeStatus.Used)
- {
- return Result.Failure<int>(Error.Problem("Refund.CannotRestoreUsed", $"이미 사용된 쿠폰 코드는 '회수' 처리할 수 없습니다: {inv.CouponCode.Code}"));
- }
- // PostRefundAction=Expire 는 Available/Issued 만 가능 (CouponCode.Expire() 참조)
- if (line.PostRefundAction == CouponPostRefundAction.Expire
- && inv.CouponCode.Status != CouponCodeStatus.Available
- && inv.CouponCode.Status != CouponCodeStatus.Issued)
- {
- return Result.Failure<int>(Error.Problem("Refund.CannotExpire", $"쿠폰 코드 상태가 '{inv.CouponCode.Status}' 라서 '만료' 처리할 수 없습니다: {inv.CouponCode.Code}"));
- }
- totalAmount += unitPriceByItem[line.OrderItemID];
- }
- if (totalAmount <= 0 || totalAmount > order.TotalAmount)
- {
- return Result.Failure<int>(Error.Problem("Refund.InvalidAmount", $"환불 금액 합계({totalAmount:N0}원)가 비정상입니다."));
- }
- var refund = OrderRefund.Create(request.OrderID, RefundType.Refund, request.ReasonType, totalAmount, request.Reason);
- db.OrderRefund.Add(refund);
- await db.SaveChangesAsync(ct); // refund.ID 확보
- foreach (var line in request.Lines)
- {
- var unit = unitPriceByItem[line.OrderItemID];
- var item = OrderRefundItem.Create(refund.ID, line.OrderItemID, line.CouponCodeID, unit, line.PostRefundAction);
- db.OrderRefundItem.Add(item);
- }
- await db.SaveChangesAsync(ct);
- return Result.Success(refund.ID);
- }
- }
|