| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657 |
- using System.Text.Json;
- using Application.Abstractions.Messaging;
- namespace Web.Api.Endpoints.Payment;
- /// <summary>토스 일반결제 웹훅 (PAYMENT_STATUS_CHANGED / DEPOSIT_CALLBACK — 토스 서버→서버)</summary>
- internal sealed class Webhook : IEndpoint
- {
- public void MapEndpoint(IEndpointRouteBuilder app)
- {
- /// 일반결제 웹훅은 서명 헤더가 없음 — 본문을 신뢰하지 않고 paymentKey 재조회 결과만 반영.
- /// 실패(5xx) 응답 시 토스가 재시도한다.
- app.MapPost("api/payments/webhook", async (
- HttpContext httpContext,
- ISender sender,
- CancellationToken ct
- ) => {
- string? eventType = null;
- string? paymentKey = null;
- string? orderID = null;
- try
- {
- using var doc = await JsonDocument.ParseAsync(httpContext.Request.Body, cancellationToken: ct);
- var root = doc.RootElement;
- string? Get(JsonElement el, string key) => el.ValueKind == JsonValueKind.Object && el.TryGetProperty(key, out var p) && p.ValueKind == JsonValueKind.String ? p.GetString() : null;
- eventType = Get(root, "eventType");
- orderID = Get(root, "orderId"); // DEPOSIT_CALLBACK 은 top-level orderId
- // PAYMENT_STATUS_CHANGED 는 data 에 Payment 객체
- if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("data", out var data))
- {
- paymentKey = Get(data, "paymentKey");
- orderID ??= Get(data, "orderId");
- }
- }
- catch (JsonException)
- {
- return Results.BadRequest();
- }
- var result = await sender.Send(new Application.Features.Api.Payment.Toss.Webhook.Command(eventType, paymentKey, orderID), ct);
- // 일시 실패(재조회/저장 실패)는 5xx — 토스 재시도로 재처리
- if (result.IsFailure)
- {
- return Results.StatusCode(StatusCodes.Status500InternalServerError);
- }
- return Results.Ok();
- })
- .WithTags("Payment")
- .AllowAnonymous();
- }
- }
|