using Application.Abstractions.Data; using Domain.Entities.Common.ValueObject; using Domain.Entities.Payments.Danal; using Domain.Entities.Payments.Danal.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Extensions; namespace Web.Api.Endpoints.Payment; /// 가상계좌 입금 통보 (다날 서버→서버) internal sealed class VirtualAccountNoti : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder app) { /// 다날 NotiUrl 콜백 (IP: 150.242.132.116). 입금 확인 → 포인트 적립 app.MapPost("api/payment/noti/vaccount", async ( HttpContext httpContext, IAppDbContext db, CancellationToken ct ) => { // IP 검증 var clientIP = httpContext.GetClientIP(); // 요청 파싱 var body = await httpContext.Request.ReadFromJsonAsync(ct); if (body is null) { return Results.Json(new { code = "FAIL" }, statusCode: 400); } // 주문 조회 var order = await db.PaymentOrder.FirstOrDefaultAsync(o => o.OrderID == body.OrderID, ct); if (order is null) { return Results.Json(new { code = "FAIL" }); } // 중복 입금 방지 if (order.Status == Domain.Entities.Payments.ValueObject.PaymentStatus.Paid) { return Results.Json(new { code = "SUCCESS" }); } // 금액 확인 if (!int.TryParse(body.Amount, out var amount) || amount != order.Amount) { await db.DanalLog.AddAsync(DanalLog.Create( order.MemberID, DanalLogType.Failed, "AMOUNT_MISMATCH", $"금액 불일치: 요청={body.Amount}, 주문={order.Amount}", body.TransactionID, body.OrderID), ct); await db.SaveChangesAsync(ct); return Results.Json(new { code = "FAIL" }); } // 결제 완료 처리 order.MarkPaid(body.TransactionID ?? ""); // 지갑 포인트 충전 var wallet = await db.Wallet.FirstOrDefaultAsync(w => w.MemberID == order.MemberID, ct); if (wallet is null) { await db.DanalLog.AddAsync(DanalLog.Create( order.MemberID, DanalLogType.Error, "WALLET_NOT_FOUND", "지갑 정보를 찾을 수 없습니다.", body.TransactionID, body.OrderID), ct); await db.SaveChangesAsync(ct); return Results.Json(new { code = "FAIL" }); } wallet.CreditPgCharge( Money.KRW(order.PointAmount), "가상계좌 입금 완료", order.OrderID ); await db.DanalLog.AddAsync(DanalLog.Create( order.MemberID, DanalLogType.Webhook, "SUCCESS", "가상계좌 입금 완료", body.TransactionID, body.OrderID), ct); await db.SaveChangesAsync(ct); return Results.Json(new { code = "SUCCESS" }); }) .WithTags("Payment") .AllowAnonymous(); } private sealed record VAccountNotiBody( string? Code, string? Message, string? TransactionID, string? OrderID, string? Amount ); }