using Domain.Entities.Common.ValueObject; using Domain.Entities.Payments.Danal; using Domain.Entities.Payments.ValueObject; using Domain.Entities.Wallets; using Domain.Entities.Wallets.ValueObject; using Microsoft.EntityFrameworkCore; using AdminCancelCommand = Application.Features.Admin.Payment.Danal.Confirm.Cancel.Command; using AdminCancelHandler = Application.Features.Admin.Payment.Danal.Confirm.Cancel.Handler; namespace Application.Tests; [TestClass] public sealed class AdminDanalCancelHandlerTests { private static async Task SeedConfirmAsync(Infrastructure.Persistence.AppDbContext db, Domain.Entities.Payments.PaymentOrder order) { var confirm = DanalConfirm.CreateRequest(order.MemberID, order.ID, order.PaymentMethod, order.TransactionID ?? "TX-SEED", order.OrderID, order.Amount, order.MerchantID); confirm.SetResponse("0000", "OK"); await db.DanalConfirm.AddAsync(confirm); await db.SaveChangesAsync(default); return confirm; } [TestMethod] public async Task AdminForceCancel_RecoversPgChargedPoints() { // 리뷰 #3 재현: Admin 강제취소가 Adjusted 버킷 차감(항상 0) + 예외 흡수로, // PG 현금은 환불됐는데 회원 포인트가 회수되지 않았다. // 기대 행위: 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 FakeDanalPayService(); var handler = new AdminCancelHandler(db, fake); var result = await handler.Handle(new AdminCancelCommand(confirm.ID, "admin", "운영자 강제취소"), default); Assert.IsTrue(result.IsSuccess, "운영자 강제취소가 성공해야 한다"); using var verify = TestDb.Create(); var savedOrder = await verify.PaymentOrder.SingleAsync(o => o.OrderID == order.OrderID); Assert.AreEqual(PaymentStatus.Cancelled, savedOrder.Status); 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 FakeDanalPayService(); var handler = new AdminCancelHandler(db, fake); var result = await handler.Handle(new AdminCancelCommand(confirm.ID, "admin", "강제취소"), default); Assert.IsFalse(result.IsSuccess, "사용분이 있으면 자동 취소를 거절(수동 조정 유도)해야 한다"); Assert.AreEqual(0, fake.CancelCallCount, "거절은 PG 호출 전에 이뤄져야 한다"); } }