Cancel.cs 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. using System.Security.Claims;
  2. using MediatR;
  3. using Web.Api.Common;
  4. using Web.Api.Extensions;
  5. namespace Web.Api.Endpoints.V1.Purchases;
  6. internal sealed class Cancel : IEndpoint
  7. {
  8. public void MapEndpoint(IEndpointRouteBuilder app)
  9. {
  10. app.MapPost("v1/purchases/{orderID}/cancel", async (
  11. string orderID,
  12. ClaimsPrincipal user,
  13. ISender sender,
  14. CancellationToken ct,
  15. int? marketplace = null
  16. ) => {
  17. var applicationID = user.GetApplicationID();
  18. if (applicationID is null)
  19. {
  20. return ApiResponse.Fail(StatusCodes.Status403Forbidden, "결제 취소는 OAuth2 앱 토큰 전용입니다. (PAT 사용 불가)");
  21. }
  22. var command = new Application.Features.Api.V1.Purchases.Cancel.Command(applicationID.Value, orderID, marketplace);
  23. var result = await sender.Send(command, ct);
  24. return result.Match(
  25. data => ApiResponse.Ok(data),
  26. CustomResults.Problem
  27. );
  28. })
  29. .WithTags("결제 보고")
  30. .WithGroupName("public")
  31. .WithName("CancelPurchase")
  32. .WithSummary("결제 취소")
  33. .WithDescription("""
  34. 등록된 결제를 취소합니다. **등록한 앱만 취소 가능**합니다.
  35. - **보류(Pending) 중 취소**: 지갑 변동 없이 취소 처리 (`recoveredAmount=0`)
  36. - **확정(Confirmed) 후 취소**: 적립된 채널 수수료를 회수. 채널주가 이미 출금해 잔액이
  37. 부족하면 가용분만 회수하고 부족분을 `shortfallAmount` 로 응답 (DPOT 운영진 검토 대상)
  38. ### 필수 scope
  39. - `write:purchases`
  40. ### 인증 제한
  41. - **OAuth2 Client Credentials 앱 토큰 전용** — PAT 으로 호출 시 `403`
  42. ### 경로/쿼리
  43. | 이름 | 위치 | 필수 | 설명 |
  44. |---|---|---|---|
  45. | `orderID` | path | ✅ | 등록 시 사용한 마켓 거래 ID |
  46. | `marketplace` | query | 조건 | 동일 orderID 가 여러 마켓에 등록된 경우만 필수 |
  47. ### 에러
  48. - `400` — orderID 누락 / 마켓 모호 (`Purchase.AmbiguousOrderID`)
  49. - `403` — PAT 호출
  50. - `404` — 등록 내역 없음 (타 앱 등록 건 포함 — 조회 불가)
  51. - `409` — 이미 취소됨 (`Purchase.AlreadyCanceled`)
  52. """)
  53. .Produces<ApiResponse>(StatusCodes.Status200OK)
  54. .ProducesProblem(StatusCodes.Status400BadRequest)
  55. .ProducesProblem(StatusCodes.Status401Unauthorized)
  56. .ProducesProblem(StatusCodes.Status403Forbidden)
  57. .ProducesProblem(StatusCodes.Status404NotFound)
  58. .ProducesProblem(StatusCodes.Status409Conflict)
  59. .RequireAuthorization(policy => policy
  60. .AddAuthenticationSchemes("ApiKey", "OAuth2Bearer")
  61. .RequireAuthenticatedUser())
  62. .RequireScope("write:purchases");
  63. }
  64. }