using System.Security.Claims; using MediatR; using Web.Api.Common; using Web.Api.Extensions; namespace Web.Api.Endpoints.V1.Purchases; internal sealed class Cancel : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("v1/purchases/{orderID}/cancel", async ( string orderID, ClaimsPrincipal user, ISender sender, CancellationToken ct, int? marketplace = null ) => { var applicationID = user.GetApplicationID(); if (applicationID is null) { return ApiResponse.Fail(StatusCodes.Status403Forbidden, "결제 취소는 OAuth2 앱 토큰 전용입니다. (PAT 사용 불가)"); } var command = new Application.Features.Api.V1.Purchases.Cancel.Command(applicationID.Value, orderID, marketplace); var result = await sender.Send(command, ct); return result.Match( data => ApiResponse.Ok(data), CustomResults.Problem ); }) .WithTags("결제 보고") .WithGroupName("public") .WithName("CancelPurchase") .WithSummary("결제 취소") .WithDescription(""" 등록된 결제를 취소합니다. **등록한 앱만 취소 가능**합니다. - **보류(Pending) 중 취소**: 지갑 변동 없이 취소 처리 (`recoveredAmount=0`) - **확정(Confirmed) 후 취소**: 적립된 채널 수수료를 회수. 채널주가 이미 출금해 잔액이 부족하면 가용분만 회수하고 부족분을 `shortfallAmount` 로 응답 (DPOT 운영진 검토 대상) ### 필수 scope - `write:purchases` ### 인증 제한 - **OAuth2 Client Credentials 앱 토큰 전용** — PAT 으로 호출 시 `403` ### 경로/쿼리 | 이름 | 위치 | 필수 | 설명 | |---|---|---|---| | `orderID` | path | ✅ | 등록 시 사용한 마켓 거래 ID | | `marketplace` | query | 조건 | 동일 orderID 가 여러 마켓에 등록된 경우만 필수 | ### 에러 - `400` — orderID 누락 / 마켓 모호 (`Purchase.AmbiguousOrderID`) - `403` — PAT 호출 - `404` — 등록 내역 없음 (타 앱 등록 건 포함 — 조회 불가) - `409` — 이미 취소됨 (`Purchase.AlreadyCanceled`) """) .Produces(StatusCodes.Status200OK) .ProducesProblem(StatusCodes.Status400BadRequest) .ProducesProblem(StatusCodes.Status401Unauthorized) .ProducesProblem(StatusCodes.Status403Forbidden) .ProducesProblem(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status409Conflict) .RequireAuthorization(policy => policy .AddAuthenticationSchemes("ApiKey", "OAuth2Bearer") .RequireAuthenticatedUser()) .RequireScope("write:purchases"); } }