using System.Security.Claims; using MediatR; using Web.Api.Common; using Web.Api.Extensions; namespace Web.Api.Endpoints.V1.Purchases; internal sealed class Register : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("v1/purchases", async ( Application.Features.Api.V1.Purchases.Register.Command body, ClaimsPrincipal user, ISender sender, CancellationToken ct ) => { var applicationID = user.GetApplicationID(); if (applicationID is null) { return ApiResponse.Fail(StatusCodes.Status403Forbidden, "결제 등록은 OAuth2 앱 토큰 전용입니다. (PAT 사용 불가)"); } var command = body with { ApplicationID = applicationID.Value }; var result = await sender.Send(command, ct); return result.Match( data => ApiResponse.Created(data), CustomResults.Problem ); }) .WithTags("결제 보고") .WithGroupName("public") .WithName("RegisterPurchase") .WithSummary("결제 등록") .WithDescription(""" 게임 내 결제를 DPOT 에 보고합니다. 검증 통과 시 **보류(Pending) 원장**이 생성되고, 등록 시점 +14일(달력일, 주말·공휴일 포함) 후 확정 배치가 채널 소유 회원의 지갑에 판매 수수료(`orderPrice × 게임별 수수료율`)를 적립합니다. ### 필수 scope - `write:purchases` ### 인증 제한 - **OAuth2 Client Credentials 앱 토큰 전용** — PAT 으로 호출 시 `403` ### 요청 본문 | 필드 | 타입 | 필수 | 설명 | |---|---|---|---| | `channelCode` | string | ✅ | 채널 후원 코드 (4~7자 영문+숫자, 게임 내 유저 입력값) | | `orderID` | string(255) | ✅ | **마켓 거래 ID 원문** — Google `GPA.xxxx-xxxx-xxxx-xxxxx`, Apple transactionId 등. 가공·축약 금지 | | `marketplace` | int | ✅ | 1=구글, 2=애플, 3=MS, 4=갤럭시, 5=원스토어, 6=기타 | | `gameCode` | string | ✅ | DPOT 에 등록된 게임 코드 | | `orderPrice` | int | ✅ | 결제 금액 (KRW, 1 이상) | | `productID` | string(100) | 권장 | 인앱 상품 SKU (예: `diamond_100`) — 금액 검증에 사용 | | `subID` | string(100) | 선택 | 파트너 측 보조 식별자 | ### 멱등성 동일 (앱, `marketplace`, `orderID`) 조합은 1회만 등록 가능 — 재시도 시 `409`. ### 응답 (201) `commissionAmount`(채널 수수료), `status`(`Pending`), `confirmDueAt`(확정 예정 시각) 포함. ### 에러 - `400` — 필드 누락/형식 오류 - `403` — PAT 호출 / 앱 비활성 - `404` — 후원 코드(`Channel.NotFound`) 또는 게임(`Game.NotFound`) 없음 - `409` — 중복 주문 (`Purchase.Duplicate`) """) .Produces(StatusCodes.Status201Created) .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"); } }