VirtualAccountNoti.cs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. using Application.Abstractions.Data;
  2. using Domain.Entities.Common.ValueObject;
  3. using Domain.Entities.Payments.Danal;
  4. using Domain.Entities.Payments.Danal.ValueObject;
  5. using Microsoft.EntityFrameworkCore;
  6. using SharedKernel.Extensions;
  7. namespace Web.Api.Endpoints.Payment;
  8. /// <summary>가상계좌 입금 통보 (다날 서버→서버)</summary>
  9. internal sealed class VirtualAccountNoti : IEndpoint
  10. {
  11. public void MapEndpoint(IEndpointRouteBuilder app)
  12. {
  13. /// 다날 NotiUrl 콜백 (IP: 150.242.132.116). 입금 확인 → 포인트 적립
  14. app.MapPost("api/payment/noti/vaccount", async (
  15. HttpContext httpContext,
  16. IAppDbContext db,
  17. CancellationToken ct
  18. ) => {
  19. // IP 검증
  20. var clientIP = httpContext.GetClientIP();
  21. // 요청 파싱
  22. var body = await httpContext.Request.ReadFromJsonAsync<VAccountNotiBody>(ct);
  23. if (body is null)
  24. {
  25. return Results.Json(new { code = "FAIL" }, statusCode: 400);
  26. }
  27. // 주문 조회
  28. var order = await db.PaymentOrder.FirstOrDefaultAsync(o => o.OrderID == body.OrderID, ct);
  29. if (order is null)
  30. {
  31. return Results.Json(new { code = "FAIL" });
  32. }
  33. // 중복 입금 방지
  34. if (order.Status == Domain.Entities.Payments.ValueObject.PaymentStatus.Paid)
  35. {
  36. return Results.Json(new { code = "SUCCESS" });
  37. }
  38. // 금액 확인
  39. if (!int.TryParse(body.Amount, out var amount) || amount != order.Amount)
  40. {
  41. await db.DanalLog.AddAsync(DanalLog.Create(
  42. order.MemberID, DanalLogType.Failed,
  43. "AMOUNT_MISMATCH", $"금액 불일치: 요청={body.Amount}, 주문={order.Amount}",
  44. body.TransactionID, body.OrderID),
  45. ct
  46. );
  47. await db.SaveChangesAsync(ct);
  48. return Results.Json(new { code = "FAIL" });
  49. }
  50. // 결제 완료 처리
  51. order.MarkPaid(body.TransactionID ?? "");
  52. // 지갑 포인트 충전
  53. var wallet = await db.Wallet.FirstOrDefaultAsync(w => w.MemberID == order.MemberID, ct);
  54. if (wallet is null)
  55. {
  56. await db.DanalLog.AddAsync(DanalLog.Create(
  57. order.MemberID, DanalLogType.Error,
  58. "WALLET_NOT_FOUND", "지갑 정보를 찾을 수 없습니다.",
  59. body.TransactionID, body.OrderID), ct
  60. );
  61. await db.SaveChangesAsync(ct);
  62. return Results.Json(new { code = "FAIL" });
  63. }
  64. wallet.CreditPgCharge(
  65. Money.KRW(order.PointAmount),
  66. "가상계좌 입금 완료",
  67. order.OrderID
  68. );
  69. await db.DanalLog.AddAsync(DanalLog.Create(
  70. order.MemberID, DanalLogType.Webhook,
  71. "SUCCESS", "가상계좌 입금 완료",
  72. body.TransactionID, body.OrderID),
  73. ct
  74. );
  75. await db.SaveChangesAsync(ct);
  76. return Results.Json(new { code = "SUCCESS" });
  77. })
  78. .WithTags("Payment")
  79. .AllowAnonymous();
  80. }
  81. private sealed record VAccountNotiBody(
  82. string? Code,
  83. string? Message,
  84. string? TransactionID,
  85. string? OrderID,
  86. string? Amount
  87. );
  88. }