Handler.cs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Application.Abstractions.Payment;
  4. using SharedKernel.Results;
  5. using Domain.Entities.Common.ValueObject;
  6. using Domain.Entities.Payments.Danal;
  7. using Domain.Entities.Payments.Danal.ValueObject;
  8. using Domain.Entities.Payments.ValueObject;
  9. using Domain.Entities.Wallets.ValueObject;
  10. using Microsoft.EntityFrameworkCore;
  11. namespace Application.Features.Api.Payment.Cancel;
  12. internal sealed class Handler(
  13. IAppDbContext db,
  14. IDanalPayService danalPay
  15. ) : ICommandHandler<Command, Result>
  16. {
  17. public async Task<Result> Handle(Command r, CancellationToken ct)
  18. {
  19. var order = await db.PaymentOrder.FirstOrDefaultAsync(o => o.OrderID == r.OrderID && o.MemberID == r.MemberID, ct);
  20. if (order is null)
  21. {
  22. return Result.Failure(Error.NotFound("Payment.OrderNotFound", "주문을 찾을 수 없습니다."));
  23. }
  24. if (order.Status != PaymentStatus.Paid)
  25. {
  26. return Result.Failure(Error.Problem("Payment.InvalidStatusForCancel", "취소할 수 없는 주문 상태입니다."));
  27. }
  28. // 환불 가능성 선검증 — PG 취소가 성공한 뒤 지갑 회수가 실패하면 PG-지갑 desync 가 되므로
  29. // 지갑 존재와 충전 잔액(PgCharged)을 PG 호출 전에 확정한다.
  30. var wallet = await db.Wallet.Include(w => w.Balances).Include(w => w.Transactions).AsSplitQuery().FirstOrDefaultAsync(w => w.MemberID == order.MemberID, ct);
  31. if (wallet is null)
  32. {
  33. return Result.Failure(Error.NotFound("Payment.WalletNotFound", "지갑 정보를 찾을 수 없습니다."));
  34. }
  35. var pgCharged = wallet.Balances.Where(b => b.Type == WalletBalanceType.PgCharged).Sum(b => b.Amount.Value);
  36. if (pgCharged < order.PointAmount)
  37. {
  38. return Result.Failure(Error.Problem("Payment.PointAlreadySpent", "이미 사용한 포인트가 있어 결제를 취소할 수 없습니다."));
  39. }
  40. // 다날 결제수단 코드 매핑
  41. var method = order.PaymentMethod switch
  42. {
  43. PaymentMethod.Integrated => "INTEGRATED",
  44. PaymentMethod.Card => "CARD",
  45. PaymentMethod.VirtualAccount => "VACCOUNT",
  46. PaymentMethod.Mobile => "MOBILE",
  47. PaymentMethod.Transfer => "TRANSFER",
  48. PaymentMethod.NaverPay => "NAVERPAY",
  49. PaymentMethod.KakaoPay => "KAKAOPAY",
  50. PaymentMethod.Payco => "PAYCO",
  51. _ => "CARD"
  52. };
  53. // 취소 요청 기록 저장
  54. var cancel = DanalCancel.CreateRequest(r.MemberID, order.ID, order.PaymentMethod, order.TransactionID!, order.OrderID, order.Amount, order.MerchantID, DanalCancelType.Full);
  55. await db.DanalCancel.AddAsync(cancel, ct);
  56. await db.SaveChangesAsync(ct);
  57. // 다날 취소 API 호출 — 통신 예외도 기록을 남긴다
  58. DanalCancelResult result;
  59. try
  60. {
  61. result = await danalPay.CancelAsync(method, order.TransactionID!, order.Amount, "C", ct);
  62. }
  63. catch (Exception ex)
  64. {
  65. cancel.SetResponse("EXCEPTION", ex.Message);
  66. await db.DanalLog.AddAsync(DanalLog.Create(r.MemberID, DanalLogType.Error, "EXCEPTION", ex.Message, order.TransactionID, order.OrderID), ct);
  67. await db.SaveChangesAsync(ct);
  68. return Result.Failure(Error.Problem("Payment.CancelFailed", "결제 취소 처리 중 오류가 발생했습니다."));
  69. }
  70. if (!result.Success)
  71. {
  72. cancel.SetResponse(result.Code ?? "", result.Message ?? "취소 실패");
  73. await db.DanalLog.AddAsync(DanalLog.Create(r.MemberID, DanalLogType.Failed, result.Code ?? "FAIL", result.Message ?? "취소 실패", order.TransactionID, order.OrderID), ct);
  74. await db.SaveChangesAsync(ct);
  75. return Result.Failure(Error.Problem("Payment.CancelFailed", result.Message ?? "결제 취소에 실패했습니다."));
  76. }
  77. // 취소 응답 저장 (Balance, RemainedAmount는 string? → int? 변환)
  78. cancel.SetResponse(
  79. result.Code ?? "", result.Message ?? "",
  80. result.OriginalTransactionID,
  81. result.CancelledAmount,
  82. result.TransDate, result.TransTime,
  83. int.TryParse(result.Balance, out var balanceVal) ? balanceVal : null,
  84. int.TryParse(result.RemainedAmount, out var remainedVal) ? remainedVal : null,
  85. result.ApprovalDateTime
  86. );
  87. order.MarkCancelled();
  88. // 충전분(PgCharged) 회수 — ChargeCancelled 원장 기록으로 출처를 보존한다
  89. wallet.DebitChargeCancel(Money.KRW(order.PointAmount), "결제 취소 환불", order.OrderID);
  90. await db.SaveChangesAsync(ct);
  91. return Result.Success();
  92. }
  93. }