Handler.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Application.Abstractions.Payment;
  4. using Domain.Entities.Common.ValueObject;
  5. using Domain.Entities.Payments.Danal;
  6. using Domain.Entities.Payments.Danal.ValueObject;
  7. using Domain.Entities.Payments.ValueObject;
  8. using Microsoft.EntityFrameworkCore;
  9. using SharedKernel.Results;
  10. namespace Application.Features.Admin.Payment.Danal.Confirm.Cancel;
  11. public sealed class Handler(
  12. IAppDbContext db,
  13. IDanalPayService danalPay
  14. ) : ICommandHandler<Command, Result>
  15. {
  16. public async Task<Result> Handle(Command request, CancellationToken ct)
  17. {
  18. var confirm = await db.DanalConfirm.FirstOrDefaultAsync(x => x.ID == request.ID, ct);
  19. if (confirm is null)
  20. {
  21. return Result.Failure(Error.NotFound("Danal.ConfirmNotFound", "결제 내역을 찾을 수 없습니다."));
  22. }
  23. var order = await db.PaymentOrder.FirstOrDefaultAsync(o => o.ID == confirm.PaymentOrderID, ct);
  24. if (order is null)
  25. {
  26. return Result.Failure(Error.NotFound("Payment.OrderNotFound", "주문을 찾을 수 없습니다."));
  27. }
  28. // 진실의 근원은 PaymentOrder.Status — DanalConfirm.ResponseCode 가 비어있거나 "0000" 이 아니어도
  29. // PG 측에 실제 결제가 잡혀있다면(=Paid) 운영자 강제 환불이 필요. confirm.IsSuccess 단독 가드는 너무 엄격.
  30. if (order.Status != PaymentStatus.Paid)
  31. {
  32. return Result.Failure(Error.Problem("Payment.InvalidStatusForCancel", "취소할 수 없는 주문 상태입니다."));
  33. }
  34. // 이미 성공 취소된 경우 방지
  35. var hasSuccessfulCancel = await db.DanalCancel.AnyAsync(
  36. c => c.PaymentOrderID == order.ID && c.ResponseCode == "0000", ct);
  37. if (hasSuccessfulCancel)
  38. {
  39. return Result.Failure(Error.Problem("Danal.AlreadyCancelled", "이미 결제가 취소되었습니다."));
  40. }
  41. // 다날 결제수단 코드 매핑
  42. var methodCode = order.PaymentMethod switch
  43. {
  44. PaymentMethod.Integrated => "INTEGRATED",
  45. PaymentMethod.Card => "CARD",
  46. PaymentMethod.VirtualAccount => "VACCOUNT",
  47. PaymentMethod.Mobile => "MOBILE",
  48. PaymentMethod.Transfer => "TRANSFER",
  49. PaymentMethod.NaverPay => "NAVERPAY",
  50. PaymentMethod.KakaoPay => "KAKAOPAY",
  51. PaymentMethod.Payco => "PAYCO",
  52. _ => "CARD"
  53. };
  54. // 취소 요청 기록 저장 (전체 취소)
  55. var cancel = DanalCancel.CreateRequest(
  56. memberID: order.MemberID,
  57. paymentOrderID: order.ID,
  58. method: order.PaymentMethod,
  59. transactionID: order.TransactionID!,
  60. orderID: order.OrderID,
  61. amount: order.Amount,
  62. merchantID: order.MerchantID,
  63. cancelType: DanalCancelType.Full,
  64. cancelRequester: request.CancelRequester,
  65. cancelReason: request.CancelReason
  66. );
  67. await db.DanalCancel.AddAsync(cancel, ct);
  68. await db.SaveChangesAsync(ct);
  69. // 다날 API 호출
  70. var result = await danalPay.CancelAsync(methodCode, order.TransactionID!, order.Amount, "C", ct);
  71. if (!result.Success)
  72. {
  73. cancel.SetResponse(result.Code ?? "", result.Message ?? "취소 실패");
  74. await db.DanalLog.AddAsync(DanalLog.Create(
  75. order.MemberID,
  76. DanalLogType.Failed,
  77. result.Code ?? "FAIL",
  78. result.Message ?? "취소 실패",
  79. order.TransactionID,
  80. order.OrderID), ct);
  81. await db.SaveChangesAsync(ct);
  82. return Result.Failure(Error.Problem("Danal.CancelFailed", result.Message ?? "결제 취소에 실패했습니다."));
  83. }
  84. // 취소 응답 저장
  85. cancel.SetResponse(
  86. result.Code ?? "",
  87. result.Message ?? "",
  88. result.OriginalTransactionID,
  89. result.CancelledAmount,
  90. result.TransDate,
  91. result.TransTime,
  92. int.TryParse(result.Balance, out var balanceVal) ? balanceVal : null,
  93. int.TryParse(result.RemainedAmount, out var remainedVal) ? remainedVal : null,
  94. result.ApprovalDateTime
  95. );
  96. order.MarkCancelled();
  97. // 지갑에서 환불 처리 (잔액 부족 시 음수 방지 위해 예외 흡수)
  98. var wallet = await db.Wallet.Include(w => w.Balances)
  99. .FirstOrDefaultAsync(w => w.MemberID == order.MemberID, ct);
  100. if (wallet is not null)
  101. {
  102. try
  103. {
  104. wallet.AdjustDecrease(Money.KRW(order.PointAmount), "결제 취소 환불", order.OrderID);
  105. }
  106. catch (InvalidOperationException)
  107. {
  108. // 잔액 부족 — 음수 방지 (0 처리)
  109. }
  110. }
  111. await db.SaveChangesAsync(ct);
  112. return Result.Success();
  113. }
  114. }