using Domain.Entities.Common.ValueObject; using Domain.Entities.Payments; using Domain.Entities.Payments.Toss; using Domain.Entities.Payments.ValueObject; using Domain.Entities.Wallets; using Domain.Entities.Wallets.ValueObject; using Microsoft.EntityFrameworkCore; using AdminCancelCommand = Application.Features.Admin.Payment.Toss.Confirm.Cancel.Command; using AdminCancelHandler = Application.Features.Admin.Payment.Toss.Confirm.Cancel.Handler; namespace Application.Tests; [TestClass] public sealed class AdminTossCancelHandlerTests { private static async Task SeedConfirmAsync(Infrastructure.Persistence.AppDbContext db, PaymentOrder order) { var confirm = TossConfirm.CreateRequest(order.MemberID, order.ID, order.TransactionID ?? $"PK-{Guid.NewGuid():N}", order.OrderID, order.Amount); confirm.SetResponse("DONE", "카드", order.Amount, DateTime.UtcNow); await db.TossConfirm.AddAsync(confirm); await db.SaveChangesAsync(default); return confirm; } [TestMethod] public async Task AdminForceCancel_RecoversPgChargedPoints() { // 기대 행위: 운영자 강제취소 성공 시 PgCharged 에서 회수하고 ChargeCancelled 원장을 남긴다. using var db = TestDb.Create(); var memberID = await TestDb.CreateMemberAsync(db); var wallet = Wallet.Create(memberID); wallet.CreditPgCharge(Money.KRW(10000), "충전", "SEED"); await db.Wallet.AddAsync(wallet); var order = await TestDb.CreateOrderAsync(db, memberID, 10000, paid: true); var confirm = await SeedConfirmAsync(db, order); var fake = new FakeTossPaymentService(); var handler = new AdminCancelHandler(db, fake, FakeAppDbContextFactory.Instance); var result = await handler.Handle(new AdminCancelCommand(confirm.ID, "admin", "운영자 강제취소"), default); Assert.IsTrue(result.IsSuccess, "운영자 강제취소가 성공해야 한다"); Assert.AreEqual(1, fake.CancelCallCount); using var verify = TestDb.Create(); var savedOrder = await verify.PaymentOrder.SingleAsync(o => o.OrderID == order.OrderID); Assert.AreEqual(PaymentStatus.Cancelled, savedOrder.Status); var savedCancel = await verify.TossCancel.SingleAsync(c => c.OrderID == order.OrderID); Assert.IsTrue(savedCancel.IsSuccess); Assert.AreEqual("admin", savedCancel.CancelRequester); var savedWallet = await verify.Wallet.Include(w => w.Balances).Include(w => w.Transactions).AsSplitQuery().SingleAsync(w => w.MemberID == memberID); Assert.AreEqual(0m, savedWallet.Balances.Single(b => b.Type == WalletBalanceType.PgCharged).Amount.Value, "환불 시 PgCharged 캐시가 회수되어야 한다"); Assert.IsTrue(savedWallet.Transactions.Any(t => t.TxType == WalletTransactionType.ChargeCancelled && t.Amount.Value == 10000m), "ChargeCancelled 원장이 있어야 한다"); } [TestMethod] public async Task AdminForceCancel_PointsSpent_Fails_BeforePgCall() { using var db = TestDb.Create(); var memberID = await TestDb.CreateMemberAsync(db); var wallet = Wallet.Create(memberID); wallet.CreditPgCharge(Money.KRW(10000), "충전", "SEED"); wallet.Spend(Money.KRW(6000), "사용"); await db.Wallet.AddAsync(wallet); var order = await TestDb.CreateOrderAsync(db, memberID, 10000, paid: true); var confirm = await SeedConfirmAsync(db, order); var fake = new FakeTossPaymentService(); var handler = new AdminCancelHandler(db, fake, FakeAppDbContextFactory.Instance); var result = await handler.Handle(new AdminCancelCommand(confirm.ID, "admin", "강제취소"), default); Assert.IsFalse(result.IsSuccess, "사용분이 있으면 자동 취소를 거절(수동 조정 유도)해야 한다"); Assert.AreEqual(0, fake.CancelCallCount, "거절은 PG 호출 전에 이뤄져야 한다"); } [TestMethod] public async Task AdminForceCancel_AlreadyCancelled_IsRejected_WithoutPgCall() { // 이미 성공 취소 기록이 있으면 중복 PG 취소 호출을 막는다. using var db = TestDb.Create(); var memberID = await TestDb.CreateMemberAsync(db); var wallet = Wallet.Create(memberID); wallet.CreditPgCharge(Money.KRW(10000), "충전", "SEED"); await db.Wallet.AddAsync(wallet); var order = await TestDb.CreateOrderAsync(db, memberID, 10000, paid: true); var confirm = await SeedConfirmAsync(db, order); var priorCancel = TossCancel.CreateRequest(memberID, order.ID, confirm.PaymentKey, order.OrderID, order.Amount, "선행 취소", "member"); priorCancel.SetResponse("CANCELED"); await db.TossCancel.AddAsync(priorCancel); await db.SaveChangesAsync(default); var fake = new FakeTossPaymentService(); var handler = new AdminCancelHandler(db, fake, FakeAppDbContextFactory.Instance); var result = await handler.Handle(new AdminCancelCommand(confirm.ID, "admin", "중복 시도"), default); Assert.IsFalse(result.IsSuccess, "이미 취소된 결제는 거절되어야 한다"); Assert.AreEqual(0, fake.CancelCallCount, "중복 취소는 PG 를 호출하지 않는다"); } }