Register.cs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  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 Register : IEndpoint
  7. {
  8. public void MapEndpoint(IEndpointRouteBuilder app)
  9. {
  10. app.MapPost("v1/purchases", async (
  11. Application.Features.Api.V1.Purchases.Register.Command body,
  12. ClaimsPrincipal user,
  13. ISender sender,
  14. CancellationToken ct
  15. ) => {
  16. var applicationID = user.GetApplicationID();
  17. if (applicationID is null)
  18. {
  19. return ApiResponse.Fail(StatusCodes.Status403Forbidden, "결제 등록은 OAuth2 앱 토큰 전용입니다. (PAT 사용 불가)");
  20. }
  21. var command = body with { ApplicationID = applicationID.Value };
  22. var result = await sender.Send(command, ct);
  23. return result.Match(
  24. data => ApiResponse.Created(data),
  25. CustomResults.Problem
  26. );
  27. })
  28. .WithTags("결제 보고")
  29. .WithGroupName("public")
  30. .WithName("RegisterPurchase")
  31. .WithSummary("결제 등록")
  32. .WithDescription("""
  33. 게임 내 결제를 DPOT 에 보고합니다. 검증 통과 시 **보류(Pending) 원장**이 생성되고,
  34. 등록 시점 +14일(달력일, 주말·공휴일 포함) 후 확정 배치가 채널 소유 회원의 지갑에
  35. 판매 수수료(`orderPrice × 게임별 수수료율`)를 적립합니다.
  36. ### 필수 scope
  37. - `write:purchases`
  38. ### 인증 제한
  39. - **OAuth2 Client Credentials 앱 토큰 전용** — PAT 으로 호출 시 `403`
  40. ### 요청 본문
  41. | 필드 | 타입 | 필수 | 설명 |
  42. |---|---|---|---|
  43. | `channelCode` | string | ✅ | 채널 후원 코드 (4~7자 영문+숫자, 게임 내 유저 입력값) |
  44. | `orderID` | string(255) | ✅ | **마켓 거래 ID 원문** — Google `GPA.xxxx-xxxx-xxxx-xxxxx`, Apple transactionId 등. 가공·축약 금지 |
  45. | `marketplace` | int | ✅ | 1=구글, 2=애플, 3=MS, 4=갤럭시, 5=원스토어, 6=기타 |
  46. | `gameCode` | string | ✅ | DPOT 에 등록된 게임 코드 |
  47. | `orderPrice` | int | ✅ | 결제 금액 (KRW, 1 이상) |
  48. | `productID` | string(100) | 권장 | 인앱 상품 SKU (예: `diamond_100`) — 금액 검증에 사용 |
  49. | `subID` | string(100) | 선택 | 파트너 측 보조 식별자 |
  50. ### 멱등성
  51. 동일 (앱, `marketplace`, `orderID`) 조합은 1회만 등록 가능 — 재시도 시 `409`.
  52. ### 응답 (201)
  53. `commissionAmount`(채널 수수료), `status`(`Pending`), `confirmDueAt`(확정 예정 시각) 포함.
  54. ### 에러
  55. - `400` — 필드 누락/형식 오류
  56. - `403` — PAT 호출 / 앱 비활성
  57. - `404` — 후원 코드(`Channel.NotFound`) 또는 게임(`Game.NotFound`) 없음
  58. - `409` — 중복 주문 (`Purchase.Duplicate`)
  59. """)
  60. .Produces<ApiResponse>(StatusCodes.Status201Created)
  61. .ProducesProblem(StatusCodes.Status400BadRequest)
  62. .ProducesProblem(StatusCodes.Status401Unauthorized)
  63. .ProducesProblem(StatusCodes.Status403Forbidden)
  64. .ProducesProblem(StatusCodes.Status404NotFound)
  65. .ProducesProblem(StatusCodes.Status409Conflict)
  66. .RequireAuthorization(policy => policy
  67. .AddAuthenticationSchemes("ApiKey", "OAuth2Bearer")
  68. .RequireAuthenticatedUser())
  69. .RequireScope("write:purchases");
  70. }
  71. }