Webhook.cs 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. using System.Text.Json;
  2. using Application.Abstractions.Messaging;
  3. namespace Web.Api.Endpoints.Payment;
  4. /// <summary>토스 일반결제 웹훅 (PAYMENT_STATUS_CHANGED / DEPOSIT_CALLBACK — 토스 서버→서버)</summary>
  5. internal sealed class Webhook : IEndpoint
  6. {
  7. public void MapEndpoint(IEndpointRouteBuilder app)
  8. {
  9. /// 일반결제 웹훅은 서명 헤더가 없음 — 본문을 신뢰하지 않고 paymentKey 재조회 결과만 반영.
  10. /// 실패(5xx) 응답 시 토스가 재시도한다.
  11. app.MapPost("api/payments/webhook", async (
  12. HttpContext httpContext,
  13. ISender sender,
  14. CancellationToken ct
  15. ) => {
  16. string? eventType = null;
  17. string? paymentKey = null;
  18. string? orderID = null;
  19. try
  20. {
  21. using var doc = await JsonDocument.ParseAsync(httpContext.Request.Body, cancellationToken: ct);
  22. var root = doc.RootElement;
  23. string? Get(JsonElement el, string key) => el.ValueKind == JsonValueKind.Object && el.TryGetProperty(key, out var p) && p.ValueKind == JsonValueKind.String ? p.GetString() : null;
  24. eventType = Get(root, "eventType");
  25. orderID = Get(root, "orderId"); // DEPOSIT_CALLBACK 은 top-level orderId
  26. // PAYMENT_STATUS_CHANGED 는 data 에 Payment 객체
  27. if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("data", out var data))
  28. {
  29. paymentKey = Get(data, "paymentKey");
  30. orderID ??= Get(data, "orderId");
  31. }
  32. }
  33. catch (JsonException)
  34. {
  35. return Results.BadRequest();
  36. }
  37. var result = await sender.Send(new Application.Features.Api.Payment.Toss.Webhook.Command(eventType, paymentKey, orderID), ct);
  38. // 일시 실패(재조회/저장 실패)는 5xx — 토스 재시도로 재처리
  39. if (result.IsFailure)
  40. {
  41. return Results.StatusCode(StatusCodes.Status500InternalServerError);
  42. }
  43. return Results.Ok();
  44. })
  45. .WithTags("Payment")
  46. .AllowAnonymous();
  47. }
  48. }