using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Abstractions.Payment; using Domain.Entities.Payments; using Domain.Entities.Payments.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Payment.Toss.CreateOrder; internal sealed class Handler( IAppDbContext db, ITossPaymentService tossPay ) : ICommandHandler> { /// 토스에서 사용할 MerchantID 표기 (PaymentOrder.MerchantID 필수 컬럼) private const string TossMerchantMark = "TOSS"; public async Task> Handle(Command r, CancellationToken ct) { if (r.Amount <= 0) { return Result.Failure(new Error("Payment.InvalidAmount", "충전 금액이 올바르지 않습니다.", ErrorType.Validation)); } var member = await db.Member.FirstOrDefaultAsync(c => c.ID == r.MemberID, ct); if (member is null) { return Result.Failure(Error.NotFound("Payment.MemberNotFound", "회원 정보를 찾을 수 없습니다.")); } // customerKey — 예측 가능 값(SID/이메일/PK) 금지, 회원별 UUID 발급·저장 (Guid 문자열은 '-' 포함 = 특수문자 조건 충족) var customerKey = member.EnsurePaymentCustomerKey(); // 주문번호 생성 — 6~64자, 영숫자와 -_ 만 (기존 prefix 방식 미러링) var orderID = $"ANTOOZA-{DateTime.UtcNow:yyyyMMddHHmmss}-{r.MemberID}-{Random.Shared.Next(1000, 9999)}"; if (orderID.Length is < 6 or > 64 || orderID.Any(c => !char.IsAsciiLetterOrDigit(c) && c != '-' && c != '_')) { return Result.Failure(new Error("Payment.InvalidOrderID", "주문번호 생성에 실패했습니다.", ErrorType.Validation)); } // 토스 클라이언트 설정 (ClientKey 획득 — 설정 미비 시 예외로 조기 실패) var config = await tossPay.GetClientConfigAsync(ct); // 서버가 금액·orderName 확정 저장 — Confirm 시 클라이언트 금액과 대조해 변조 차단 var order = PaymentOrder.Create(r.MemberID, orderID, TossMerchantMark, r.Amount, PaymentMethod.Integrated, "개미투자 캐시 충전"); await db.PaymentOrder.AddAsync(order, ct); await db.SaveChangesAsync(ct); return new Response( orderID, order.OrderName, order.Amount, order.PointAmount, order.VatAmount, config.ClientKey, customerKey.ToString() ); } }