Handler.cs 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Domain.Entities.Common.ValueObject;
  4. using Domain.Entities.Donations;
  5. using Domain.Entities.Donations.ValueObject;
  6. using Domain.Entities.Wallets;
  7. using Domain.Entities.Wallets.ValueObject;
  8. using Microsoft.EntityFrameworkCore;
  9. using SharedKernel.Results;
  10. namespace Application.Features.Api.Studio.Wallet.RequestWithdraw;
  11. internal sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Result<Response>>
  12. {
  13. private const int MinWithdrawAmount = 40000;
  14. public async Task<Result<Response>> Handle(Command request, CancellationToken ct)
  15. {
  16. // 금액 검증
  17. if (request.Amount < MinWithdrawAmount)
  18. {
  19. return Result.Failure<Response>(Error.Problem("Amount", $"최소 출금 금액은 {MinWithdrawAmount:N0}원입니다."));
  20. }
  21. // 채널 확인
  22. var channel = await db.Channel.AsNoTracking().FirstOrDefaultAsync(c => c.MemberID == request.MemberID && c.IsActive, ct);
  23. if (channel is null)
  24. {
  25. return Result.Failure<Response>(Error.NotFound("Channel", "채널 정보를 찾을 수 없습니다."));
  26. }
  27. // 계좌 확인
  28. var account = await db.SettlementAccount.AsNoTracking().FirstOrDefaultAsync(a => a.ID == request.AccountID && a.MemberID == request.MemberID, ct);
  29. if (account is null)
  30. {
  31. return Result.Failure<Response>(Error.Problem("Account", "정산 계좌를 선택해 주세요."));
  32. }
  33. // Wallet 조회 (Tracked — 잔액 변경 필요)
  34. var wallet = await db.Wallet
  35. .Include(w => w.Balances)
  36. .FirstOrDefaultAsync(w => w.MemberID == request.MemberID, ct);
  37. if (wallet is null)
  38. {
  39. return Result.Failure<Response>(Error.Problem("Wallet", "지갑 정보를 찾을 수 없습니다."));
  40. }
  41. var donationBal = wallet.Balances.FirstOrDefault(b => b.Type == WalletBalanceType.Donation);
  42. if (donationBal is null || (int)donationBal.Amount.Value < request.Amount)
  43. {
  44. return Result.Failure<Response>(Error.Problem("Amount", "출금 가능 잔액이 부족합니다."));
  45. }
  46. // 이미 대기 중인 출금이 있는지 확인
  47. var hasPending = await db.WithdrawalRequest.AnyAsync(
  48. w => w.MemberID == request.MemberID && (w.Status == WithdrawalStatus.Pending || w.Status == WithdrawalStatus.Processing), ct);
  49. if (hasPending)
  50. {
  51. return Result.Failure<Response>(Error.Problem("Withdrawal", "이미 처리 중인 출금 요청이 있습니다."));
  52. }
  53. // Wallet 잔액 Lock
  54. wallet.LockForWithdrawal(Money.KRW(request.Amount));
  55. // 출금 요청 생성
  56. var withdrawal = WithdrawalRequest.Create(
  57. channel.ID, request.MemberID, request.Amount,
  58. account.BankCode, account.BankName, account.AccountNumber, account.AccountHolder
  59. );
  60. db.WithdrawalRequest.Add(withdrawal);
  61. await db.SaveChangesAsync(ct);
  62. return Result.Success(new Response("출금 신청이 완료되었습니다."));
  63. }
  64. }