| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Messaging;
- using Application.Abstractions.Payment;
- using Domain.Entities.Common.ValueObject;
- using Domain.Entities.Payments.Danal;
- using Domain.Entities.Payments.Danal.ValueObject;
- using Domain.Entities.Payments.ValueObject;
- using Microsoft.EntityFrameworkCore;
- using SharedKernel.Results;
- namespace Application.Features.Admin.Payment.Danal.Confirm.Cancel;
- public sealed class Handler(
- IAppDbContext db,
- IDanalPayService danalPay
- ) : ICommandHandler<Command, Result>
- {
- public async Task<Result> Handle(Command request, CancellationToken ct)
- {
- var confirm = await db.DanalConfirm.FirstOrDefaultAsync(x => x.ID == request.ID, ct);
- if (confirm is null)
- {
- return Result.Failure(Error.NotFound("Danal.ConfirmNotFound", "결제 내역을 찾을 수 없습니다."));
- }
- var order = await db.PaymentOrder.FirstOrDefaultAsync(o => o.ID == confirm.PaymentOrderID, ct);
- if (order is null)
- {
- return Result.Failure(Error.NotFound("Payment.OrderNotFound", "주문을 찾을 수 없습니다."));
- }
- // 진실의 근원은 PaymentOrder.Status — DanalConfirm.ResponseCode 가 비어있거나 "0000" 이 아니어도
- // PG 측에 실제 결제가 잡혀있다면(=Paid) 운영자 강제 환불이 필요. confirm.IsSuccess 단독 가드는 너무 엄격.
- if (order.Status != PaymentStatus.Paid)
- {
- return Result.Failure(Error.Problem("Payment.InvalidStatusForCancel", "취소할 수 없는 주문 상태입니다."));
- }
- // 이미 성공 취소된 경우 방지
- var hasSuccessfulCancel = await db.DanalCancel.AnyAsync(
- c => c.PaymentOrderID == order.ID && c.ResponseCode == "0000", ct);
- if (hasSuccessfulCancel)
- {
- return Result.Failure(Error.Problem("Danal.AlreadyCancelled", "이미 결제가 취소되었습니다."));
- }
- // 다날 결제수단 코드 매핑
- var methodCode = order.PaymentMethod switch
- {
- PaymentMethod.Integrated => "INTEGRATED",
- PaymentMethod.Card => "CARD",
- PaymentMethod.VirtualAccount => "VACCOUNT",
- PaymentMethod.Mobile => "MOBILE",
- PaymentMethod.Transfer => "TRANSFER",
- PaymentMethod.NaverPay => "NAVERPAY",
- PaymentMethod.KakaoPay => "KAKAOPAY",
- PaymentMethod.Payco => "PAYCO",
- _ => "CARD"
- };
- // 취소 요청 기록 저장 (전체 취소)
- var cancel = DanalCancel.CreateRequest(
- memberID: order.MemberID,
- paymentOrderID: order.ID,
- method: order.PaymentMethod,
- transactionID: order.TransactionID!,
- orderID: order.OrderID,
- amount: order.Amount,
- merchantID: order.MerchantID,
- cancelType: DanalCancelType.Full,
- cancelRequester: request.CancelRequester,
- cancelReason: request.CancelReason
- );
- await db.DanalCancel.AddAsync(cancel, ct);
- await db.SaveChangesAsync(ct);
- // 다날 API 호출
- var result = await danalPay.CancelAsync(methodCode, order.TransactionID!, order.Amount, "C", ct);
- if (!result.Success)
- {
- cancel.SetResponse(result.Code ?? "", result.Message ?? "취소 실패");
- await db.DanalLog.AddAsync(DanalLog.Create(
- order.MemberID,
- DanalLogType.Failed,
- result.Code ?? "FAIL",
- result.Message ?? "취소 실패",
- order.TransactionID,
- order.OrderID), ct);
- await db.SaveChangesAsync(ct);
- return Result.Failure(Error.Problem("Danal.CancelFailed", result.Message ?? "결제 취소에 실패했습니다."));
- }
- // 취소 응답 저장
- cancel.SetResponse(
- result.Code ?? "",
- result.Message ?? "",
- result.OriginalTransactionID,
- result.CancelledAmount,
- result.TransDate,
- result.TransTime,
- int.TryParse(result.Balance, out var balanceVal) ? balanceVal : null,
- int.TryParse(result.RemainedAmount, out var remainedVal) ? remainedVal : null,
- result.ApprovalDateTime
- );
- order.MarkCancelled();
- // 지갑에서 환불 처리 (잔액 부족 시 음수 방지 위해 예외 흡수)
- var wallet = await db.Wallet.Include(w => w.Balances)
- .FirstOrDefaultAsync(w => w.MemberID == order.MemberID, ct);
- if (wallet is not null)
- {
- try
- {
- wallet.AdjustDecrease(Money.KRW(order.PointAmount), "결제 취소 환불", order.OrderID);
- }
- catch (InvalidOperationException)
- {
- // 잔액 부족 — 음수 방지 (0 처리)
- }
- }
- await db.SaveChangesAsync(ct);
- return Result.Success();
- }
- }
|