# =============================================================================
#  Payvit Public API — OpenAPI 3.1 specification
# -----------------------------------------------------------------------------
#  외부 가맹점 / 외부 플랫폼 / AI 코드 생성 도구가 참조하는 기계 판독 가능한 스펙.
#  사람용 문서: /guide  +  /docs/payvit-integration-guide.md  +  /docs/guide/README.md
#
#  접근 URL  : https://api.getpayvit.com/openapi.yaml
#  버전 정책 : 후방 호환 변경은 minor, breaking change 는 major
#  업데이트 : API 추가/변경 시 이 파일도 함께 갱신 필수
#
#  white-label 정책 (외부 배포 문서 정책, CLAUDE.md 참조):
#    - 결제망 회사명 / 카드 가맹등록 솔루션 회사명 미언급
#    - "결제망", "카드 가맹등록", "Payvit 운영팀", "결제망 콘솔" 등으로 표현
#    - paymentCompCode 등 enum value 의 내부 식별자는 "참고용, 무시 가능" 코멘트 부여
# =============================================================================

openapi: 3.1.0

info:
  title: Payvit API
  version: "1.0.0"
  summary: 온라인 VAN 결제 시스템 — 카드 단말기 수수료(법정 우대수수료) 그대로 적용되는 카드사 직접 가맹등록 결제 API.
  description: |
    Payvit 은 **온라인 VAN 결제 시스템** 입니다.
    가맹점이 PG 의 하위가맹점이 아니라 **카드사와 직접 계약된 VAN 가맹점** 으로 등록되어,
    여신전문금융업법상 **법정 우대수수료(영세 체크카드 0.1% / 신용카드 0.5%)가 그대로 적용** 됩니다.
    결제금은 카드사에서 가맹점 통장으로 직접 정산되며 (PG 마진 없음),
    카드 가맹등록·간편결제 하위가맹점 등록까지 자동화하여 **사업자등록증 1장** 으로 즉시 시작 가능합니다.

    이 OpenAPI 스펙은 **개발자와 AI 코드 생성 도구(ChatGPT / Claude / Gemini 등)** 가
    Payvit 연동 코드를 자동 생성할 때 참조할 수 있도록 제공됩니다.

    ### 수수료 (영세가맹점 기준)

    | 카드 종류 | Payvit | 일반 PG (토스 / 이니시스) | 격차 |
    |----------|--------|------------------------|------|
    | 체크카드 | **0.1%** | 1.5 ~ 2.5% | 최대 25배 |
    | 신용카드 | **0.5%** | 2.7 ~ 3.3% | 6배 |

    ### 빠른 시작
    1. Payvit 운영팀에 가맹점 등록 요청 (또는 `/apply` 신청) → `merchantId` + `apiKey` 발급
    2. `POST /api/payment/create` 호출 → `paymentLink` 수신
    3. `paymentLink` 를 고객에게 전달 (문자 / 카카오톡 / 앱 푸시 등)
    4. 결제 완료 시 가맹점 `webhookUrl` 로 결과 자동 POST

    ### 인증
    모든 `/api/**` 요청에 `X-API-Key` 헤더 필수. (자세한 모드는 [인증](#section/인증) 참조)

    ### 추가 문서
    - 사람용 가이드: https://api.getpayvit.com/guide
    - LLM 친화 요약: https://api.getpayvit.com/llms.txt
    - 통합 연동 가이드 (md): /docs/payvit-integration-guide.md
    - 모바일 앱 통합 (Android / iOS scheme reference): /docs/payvit-integration-guide.md §8
  contact:
    name: Payvit 운영팀
    url: https://api.getpayvit.com/guide
  license:
    name: Proprietary
  termsOfService: https://api.getpayvit.com/guide

servers:
  - url: https://api.getpayvit.com
    description: 운영 서버

tags:
  - name: payment
    description: 결제 생성 / 조회 / 취소 / 검색
  - name: merchant
    description: 가맹점 등록 / 카드 가맹번호 재조회 / API Key 재발급 (ADMIN 전용)
  - name: merchant-request
    description: 가맹점 등록 신청 (공개 신청 → 운영팀 승인)
  - name: webhook
    description: 결제 완료 시 가맹점이 수신하는 webhook payload schema (참고용)

# =============================================================================
#  보안 스키마
# =============================================================================
security:
  - apiKeyAuth: []

components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        가맹점에 발급된 API Key. 두 가지 모드 지원:

        | 모드 | 키 출처 | 접근 범위 |
        |------|--------|----------|
        | `MERCHANT` | 가맹점별 자동 발급 (32자 hex) | **본인 가맹점** 데이터만 |
        | `ADMIN`    | 글로벌 운영 키 | 모든 가맹점 데이터 (운영팀 전용) |

        **보안**: 서버 사이드에서만 사용. 클라이언트(브라우저/앱)에 노출 금지.

  # ---------------------------------------------------------------------------
  #  공통 응답 wrapper
  # ---------------------------------------------------------------------------
  schemas:

    # ── 응답 wrapper ─────────────────────────────────────────────────────────
    ResponseEnvelope:
      type: object
      description: |
        모든 응답의 표준 wrapper.

        | resultCode | 의미 |
        |-----------|------|
        | `00`  | 성공 |
        | `98`  | 입력 검증 오류 (필수 파라미터 누락 등) |
        | `99`  | 비즈니스 로직 오류 (중복 결제, 상태 불일치 등) |
        | `-1`  | 시스템 오류 (재시도 또는 운영팀 문의) |
      required: [resultCode]
      properties:
        resultCode:
          type: string
          example: "00"
          enum: ["00", "98", "99", "-1"]
        message:
          type: string
          description: 비즈니스/검증 오류 시 사람이 읽을 수 있는 메시지
          example: "이미 승인 완료된 주문입니다"
        data:
          description: 성공 시 endpoint 별 데이터 (각 endpoint 의 response schema 참조)

    SuccessResponse:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            resultCode:
              const: "00"

    ValidationError:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            resultCode:
              const: "98"

    BusinessError:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            resultCode:
              const: "99"

    SystemError:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            resultCode:
              const: "-1"

    # ── 결제 ─────────────────────────────────────────────────────────────────
    PayStatus:
      type: string
      description: |
        결제 상태 코드.

        | 코드 | 상태 | 설명 |
        |------|------|------|
        | `00` | 대기 | 결제 생성됨, 고객 미진입 |
        | `10` | 인증완료 | 결제앱 인증 완료, 승인 대기 |
        | `20` | 승인완료 | 결제 성공 (최종) |
        | `30` | 취소 | 결제 취소/환불 완료 (최종) |
        | `88` | 만료 | 청구서 만료 (스케줄러 자동 처리) |
        | `89` | 파기 | 청구서 수동 파기 (대기 상태에서만 가능) |
        | `97` | 확인필요 | 승인 timeout + 망취소 실패 → 운영팀 수동 확인 필요 |
        | `99` | 실패 | 인증/승인 실패 (재시도 가능) |
      enum: ["00", "10", "20", "30", "88", "89", "97", "99"]

    PaymentMethodCode:
      type: string
      description: |
        결제수단 코드 (`paymentCompCode`).

        | 코드 | 결제수단 |
        |------|---------|
        | `KKP2` | 카카오페이 |
        | `NAVER` | 네이버페이 |
        | `SHINHAN` | 신한카드 |
        | `SAMSUNG` | 삼성카드 |
        | `HYUNDAI` | 현대카드 |
        | `HANA` | 하나카드 |
        | `KBPAY` | KB국민카드 |
        | `WOORI` | 우리카드 |
        | `BC` | BC카드 |
      enum: [KKP2, NAVER, SHINHAN, SAMSUNG, HYUNDAI, HANA, KBPAY, WOORI, BC]

    CreatePaymentRequest:
      type: object
      required: [merchantId, orderId, productName, price]
      properties:
        merchantId:
          type: string
          description: 가맹점 ID (Payvit 발급)
          example: biz-gangnam
        orderId:
          type: string
          description: 가맹점 주문번호. 같은 merchantId+orderId 의 승인완료(`20`) 결제가 이미 있으면 거부됨 (중복 결제 방지)
          example: ORD-2026-001
        productName:
          type: string
          description: 결제 품목명 (결제창에 노출)
          example: 3월 학원비
        price:
          type: integer
          format: int32
          description: 결제 금액 (원, KRW)
          minimum: 100
          example: 250000
        extraData:
          type: string
          description: |
            가맹점 임의 데이터. JSON 문자열로 넣으면 결제창 "상세 정보" 카드에 자동 표시되고,
            웹훅 payload 에서 그대로 돌려받을 수 있어 가맹점 내부 식별자 매핑에 권장.
          example: '{"수강생":"박시훈","반":"심화반","기간":"2026.03.01~03.31"}'

    CreatePaymentData:
      type: object
      description: 결제 생성 성공 시 `data` 필드
      required: [paymentId, paymentLink, price, merchantId]
      properties:
        paymentId:
          type: string
          description: Payvit 발급 결제 식별자 (URL-safe AES256, 결제창/조회/웹훅에 사용)
          example: enc_abc123XYZ
        paymentLink:
          type: string
          format: uri
          description: 고객에게 전달하는 결제창 URL
          example: https://api.getpayvit.com/pay/enc_abc123XYZ
        price:
          type: string
          description: 결제 금액 (원)
          example: "250000"
        merchantId:
          type: string
          example: biz-gangnam
        merchantNm:
          type: string
          description: 가맹점명
          example: 강남지점

    PaymentDetail:
      type: object
      description: 결제 상세 정보 (조회 응답)
      properties:
        paymentId:
          type: string
          example: enc_abc123XYZ
        merchantId:
          type: string
          example: biz-gangnam
        orderId:
          type: string
          example: ORD-2026-001
        productName:
          type: string
          example: 3월 학원비
        price:
          type: integer
          example: 250000
        payStatus:
          $ref: '#/components/schemas/PayStatus'
        paymentCompCode:
          $ref: '#/components/schemas/PaymentMethodCode'
        approvalNum:
          type: string
          description: 승인번호 (승인완료 후)
          example: "12345678"
        approvalTime:
          type: string
          description: 승인 시각 (yyyyMMddHHmmss)
          example: "20260410173215"
        webhookSent:
          type: string
          description: webhook 발송 성공 여부 ("0"=미발송, "1"=발송)
          enum: ["0", "1"]
          example: "1"
        regTs:
          type: string
          format: date-time
          description: 결제 생성 시각
          example: "2026-04-10T17:30:00"
        payTs:
          type: string
          format: date-time
          description: 승인 완료 시각
          example: "2026-04-10T17:32:15"
        extraData:
          type: string
          description: 결제 생성 시 넣은 가맹점 임의 데이터
          example: '{"수강생":"박시훈"}'

    CancelPaymentData:
      type: object
      required: [paymentId]
      properties:
        paymentId:
          type: string
          example: enc_abc123XYZ
        approvalNum:
          type: string
          description: 취소 승인번호 (NicePay 회신값)
          example: "87654321"
        approvalTime:
          type: string
          description: 취소 승인 시각 (yyyyMMddHHmmss)
          example: "20260410180500"

    SearchPaymentResponse:
      type: object
      properties:
        list:
          type: array
          items:
            $ref: '#/components/schemas/PaymentDetail'
        total:
          type: integer
          description: 전체 건수
        page:
          type: integer
        size:
          type: integer

    # ── 통합 가맹점 등록 (register-full) ────────────────────────────────────
    RegisterFullStep:
      type: object
      description: 가맹점 등록 단계별 결과
      required: [success, message]
      properties:
        success:
          type: boolean
        message:
          type: string

    RegisterFullData:
      type: object
      description: 가맹점 등록 결과. 부분 실패해도 `resultCode`는 `00` (가맹점 DB 등록은 성공) — `steps` 로 개별 단계 확인.
      required: [merchantId, apiKey, steps]
      properties:
        merchantId:
          type: string
          example: biz-gangnam
        apiKey:
          type: string
          description: 가맹점에 발급된 API Key (32자 hex). 절대 외부에 노출 금지.
          example: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
        catId:
          type: string
          description: 카드 가맹등록용 단말기 식별자 (참고용, 일반 연동에는 사용 안 함)
          example: "6126530"
        mid:
          type: string
          description: 카드 가맹등록 후 발급된 가맹점 식별자
        naverMerchantId:
          type: string
          description: NaverPay 하위가맹점 식별자
        nicevanStatus:
          type: string
          description: |
            카드 가맹등록 상태 코드.

            | 값 | 의미 | 결제 가능 |
            |----|------|----------|
            | `0` | 미등록 | X |
            | `2` | 등록완료 (즉시 결제 가능) | O |
          enum: ["0", "2"]
        registeredAffInfo:
          type: object
          additionalProperties:
            type: string
          description: 카드사별 실제 등록된 가맹번호 (key=카드사 식별자, value=가맹번호)
        steps:
          type: object
          description: 등록 단계별 결과
          properties:
            merchant:   { $ref: '#/components/schemas/RegisterFullStep' }
            catId:      { $ref: '#/components/schemas/RegisterFullStep' }
            contract:   { $ref: '#/components/schemas/RegisterFullStep' }
            affInfo:    { $ref: '#/components/schemas/RegisterFullStep' }
            nicevan:    { $ref: '#/components/schemas/RegisterFullStep' }
            naverPay:   { $ref: '#/components/schemas/RegisterFullStep' }

    # ── 가맹점 신청 ─────────────────────────────────────────────────────────
    MerchantRequestData:
      type: object
      required: [requestId]
      properties:
        requestId:
          type: string
          description: 신청 ID (운영팀 검토용)
        status:
          type: string
          enum: [PENDING, APPROVED, REJECTED]

    # ── 웹훅 payload (참고용 — Payvit → 가맹점) ──────────────────────────────
    WebhookPayload:
      type: object
      description: |
        결제 완료 시 Payvit 이 가맹점 `webhookUrl` 로 자동 POST 하는 payload.
        가맹점은 HTTP 200 + JSON `{"resultCode":"OK"}` 응답해야 함.
        2xx 가 아니면 5분 후 재시도 (최대 5회, 24시간 이내).
      required: [paymentId, orderId, merchantId, payStatus, price]
      properties:
        paymentId:
          type: string
          example: enc_abc123XYZ
        orderId:
          type: string
          example: ORD-2026-001
        merchantId:
          type: string
          example: biz-gangnam
        payStatus:
          $ref: '#/components/schemas/PayStatus'
        price:
          type: integer
          example: 250000
        approvalNum:
          type: string
          example: "12345678"
        approvalTime:
          type: string
          example: "20260410173215"
        paymentCompCode:
          $ref: '#/components/schemas/PaymentMethodCode'
        productName:
          type: string
        extraData:
          type: string
          description: 결제 생성 시 가맹점이 넣은 데이터 (그대로 echo)

  # ---------------------------------------------------------------------------
  #  공통 response 정의 (재사용)
  # ---------------------------------------------------------------------------
  responses:
    Unauthorized:
      description: API Key 누락 또는 잘못된 키
      content:
        application/json:
          schema:
            type: object
            properties:
              resultCode: { type: string, example: "401" }
              message:    { type: string, example: "Unauthorized" }
    ValidationFail:
      description: 입력 검증 실패 (필수 파라미터 누락 등)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError'
    BusinessFail:
      description: 비즈니스 로직 오류 (중복 결제, 상태 불일치 등)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/BusinessError'
    SystemFail:
      description: 시스템 오류 (재시도 또는 운영팀 문의)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SystemError'

# =============================================================================
#  Endpoints
# =============================================================================
paths:

  # ─── 결제 ─────────────────────────────────────────────────────────────────
  /api/payment/create:
    post:
      tags: [payment]
      operationId: createPayment
      summary: 결제 생성
      description: |
        새 결제를 생성하고 결제창 URL(`paymentLink`)을 반환합니다.
        고객에게 `paymentLink` 만 전달하면 결제수단 선택 → 인증 → 승인 → 결과까지 Payvit 결제창에서 자동 처리됩니다.

        **중복 방지**: 같은 `merchantId+orderId` 의 승인완료(`20`) 결제가 이미 있으면 거부됩니다.
        실패(`99`) / 취소(`30`) 후에는 같은 `orderId` 로 재생성 가능.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePaymentRequest'
      responses:
        "200":
          description: 결제 생성 성공
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/CreatePaymentData'
        "401": { $ref: '#/components/responses/Unauthorized' }

  /api/payment/{paymentId}:
    get:
      tags: [payment]
      operationId: getPayment
      summary: 결제 조회
      parameters:
        - name: paymentId
          in: path
          required: true
          schema: { type: string }
          example: enc_abc123XYZ
      responses:
        "200":
          description: 결제 정보
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/PaymentDetail'
        "401": { $ref: '#/components/responses/Unauthorized' }

  /api/payment/{paymentId}/cancel:
    post:
      tags: [payment]
      operationId: cancelPayment
      summary: 결제 취소 (환불)
      description: |
        승인완료(`payStatus=20`) 상태의 결제를 취소합니다. 부분 취소는 지원하지 않습니다 (전액 환불만 가능).

        **카드 환불 시점**: 카드사 정책에 따라 영업일 1~3일 후 카드 명세서 반영.
      parameters:
        - name: paymentId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: 취소 성공
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/CancelPaymentData'
        "401": { $ref: '#/components/responses/Unauthorized' }
        default:
          description: 비즈니스/시스템 오류
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResponseEnvelope'

  /api/payment/{paymentId}/void:
    post:
      tags: [payment]
      operationId: voidPayment
      summary: 청구서 파기
      description: |
        대기(`payStatus=00`) 상태의 청구서를 파기합니다 (`payStatus=89`).

        **인증완료(`10`) 이후는 파기 불가** — `cancel` API 사용. 동시성 정책 상 인증된 결제를 파기하면 콜백/승인과 데이터 불일치 발생 위험이 있어 거부됩니다.
      parameters:
        - name: paymentId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: 파기 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        "401": { $ref: '#/components/responses/Unauthorized' }

  /api/payment/search:
    get:
      tags: [payment]
      operationId: searchPayments
      summary: 결제 내역 검색
      description: |
        결제 내역을 페이지 단위로 검색합니다.
        `MERCHANT` 키로 호출하면 본인 가맹점 결제만 반환됩니다.
      parameters:
        - name: merchantId
          in: query
          schema: { type: string }
          description: ADMIN 키로 다른 가맹점 검색 시 필요
        - name: orderId
          in: query
          schema: { type: string }
          description: 주문번호 부분 검색
        - name: payStatus
          in: query
          schema: { $ref: '#/components/schemas/PayStatus' }
        - name: startDate
          in: query
          schema: { type: string, format: date }
          example: "2026-04-01"
        - name: endDate
          in: query
          schema: { type: string, format: date }
          example: "2026-04-30"
        - name: page
          in: query
          schema: { type: integer, default: 1, minimum: 1 }
        - name: size
          in: query
          schema: { type: integer, default: 20, minimum: 1, maximum: 100 }
      responses:
        "200":
          description: 검색 결과
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/SearchPaymentResponse'
        "401": { $ref: '#/components/responses/Unauthorized' }

  # ─── 가맹점 등록 (ADMIN 전용) ────────────────────────────────────────────
  /api/merchant/register-full:
    post:
      tags: [merchant]
      operationId: registerMerchantFull
      summary: 통합 가맹점 등록 (ADMIN 전용)
      description: |
        프랜차이즈/플랫폼이 새 업체를 등록할 때 **1회 호출로 가맹점 + 카드 가맹등록 + 간편결제 하위가맹점 등록**까지 완료합니다.
        부분 실패해도 `resultCode` 는 `00` (가맹점 DB 등록은 성공) — `steps` 의 `success` 로 단계별 확인.
      security:
        - apiKeyAuth: []  # ADMIN 키 전용
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [merchantId, merchantNm, bizNo, ownerNm, ownerBirth, ownerPhone, addr, contractFile]
              properties:
                merchantId:    { type: string, example: biz-gangnam }
                merchantNm:    { type: string, example: 강남지점 }
                bizNo:
                  type: string
                  description: 사업자번호 10자 (하이픈 허용)
                  example: "123-45-67890"
                bizType:
                  type: string
                  description: 사업자 유형 (`AN`=개인, `CN`=법인). 기본 `AN`
                  enum: [AN, CN]
                  default: AN
                ownerNm:
                  type: string
                  description: 대표자명. 공동대표여도 첫 번째 1명만 입력
                  example: 홍길동
                ownerBirth:
                  type: string
                  description: |
                    **대표자 개인 생년월일** (YYYYMMDD 8자 또는 YYMMDD 6자).
                    법인이어도 대표이사 본인 생년월일 입력 — 법인등록번호 X
                  example: "19800101"
                corpRegNo:
                  type: string
                  description: 법인등록번호 13자 (`bizType=CN` 일 때 필수, 하이픈 허용)
                ownerPhone:
                  type: string
                  description: 대표자 연락처 (하이픈 허용)
                  example: "010-1234-5678"
                addr:           { type: string, example: 서울 강남구 테헤란로 1 }
                addrDetail:     { type: string }
                zipno:          { type: string }
                email:          { type: string, format: email }
                webhookUrl:
                  type: string
                  format: uri
                  description: 결제 완료 시 결과를 받을 가맹점 endpoint
                merchantGroupId:
                  type: string
                  description: 소속 그룹 ID (선택)
                contractFile:
                  type: string
                  format: binary
                  description: 사업자등록증 파일 (PDF / JPG / PNG)
      responses:
        "200":
          description: |
            전체 또는 부분 성공. `steps` 로 단계별 결과 확인.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/RegisterFullData'
        "401": { $ref: '#/components/responses/Unauthorized' }

  /api/merchant/{merchantId}/register:
    post:
      tags: [merchant]
      operationId: retryNiceVanRegister
      summary: 카드 가맹등록 재시도 (ADMIN 전용)
      description: |
        `register-full` 의 카드 가맹등록 단계가 실패한 경우 사업자등록증을 재첨부하여 3단계(계약서 → 가맹번호 조회 → 자동 등록)를 재실행합니다.
        이미 할당된 단말기 식별자는 재사용됩니다.
      security:
        - apiKeyAuth: []  # ADMIN 키 전용
      parameters:
        - name: merchantId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [contractFile]
              properties:
                contractFile:
                  type: string
                  format: binary
                  description: 사업자등록증 (PDF / JPG / PNG)
                affInfoSelection:
                  type: string
                  description: |
                    한 카드사에 다중 가맹번호 발급 시 운영자가 선택한 카드사별 가맹번호 매핑 (JSON 문자열).
                    단일 후보면 생략 가능 (자동 선택).
      responses:
        "200":
          description: 재시도 결과
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        "401": { $ref: '#/components/responses/Unauthorized' }

  /api/merchant/{merchantId}/register-naver:
    post:
      tags: [merchant]
      operationId: retryNaverPayRegister
      summary: 간편결제(NaverPay) 하위가맹점 단독 등록 (ADMIN 전용)
      description: |
        카드 가맹등록은 성공했으나 NaverPay 하위가맹점만 실패한 경우 사용. JSON 본문 (multipart 아님).
      security:
        - apiKeyAuth: []
      parameters:
        - name: merchantId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: 등록 결과
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        "401": { $ref: '#/components/responses/Unauthorized' }

  /api/merchant/{merchantId}/regenerate-api-key:
    post:
      tags: [merchant]
      operationId: regenerateApiKey
      summary: 가맹점 API Key 재발급 (ADMIN 전용)
      description: |
        기존 API Key 는 즉시 무효화됩니다. 가맹점 시스템에 신규 키를 즉시 반영하지 않으면 결제 불가.
      security:
        - apiKeyAuth: []
      parameters:
        - name: merchantId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: 새 API Key 발급
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          merchantId: { type: string }
                          apiKey:     { type: string }
        "401": { $ref: '#/components/responses/Unauthorized' }

  # ─── 가맹점 신청 (공개) ──────────────────────────────────────────────────
  /api/merchant-request:
    post:
      tags: [merchant-request]
      operationId: createMerchantRequest
      summary: 가맹점 등록 신청 (공개)
      description: |
        가맹점이 직접 신청을 등록합니다. **인증 불필요**. 운영팀 승인 시 가맹점 + 카드 가맹등록 + 간편결제 자동 등록까지 일괄 처리됩니다.
      security: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [merchantNm, bizNo, ownerNm, ownerPhone]
              properties:
                inviteCode:    { type: string, description: 그룹 초대코드 (그룹 소속 신청 시) }
                merchantId:    { type: string, description: 희망 가맹점 ID }
                merchantNm:    { type: string }
                bizNo:         { type: string }
                ownerNm:       { type: string }
                ownerBirth:    { type: string }
                ownerPhone:    { type: string }
                addr:          { type: string }
                addrDetail:    { type: string }
                email:         { type: string, format: email }
                webhookUrl:    { type: string, format: uri }
                contractFile:
                  type: string
                  format: binary
                  description: 사업자등록증
      responses:
        "200":
          description: 신청 접수 성공
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/MerchantRequestData'

  # ─── 웹훅 (참고용 schema 노출) ────────────────────────────────────────────
  /webhook-payload-reference:
    get:
      tags: [webhook]
      operationId: webhookPayloadReference
      summary: "[참고] 웹훅 payload 스키마"
      description: |
        실제로 호출 가능한 endpoint 가 아닙니다. **가맹점이 받는 웹훅 payload 의 schema 를 정의**합니다.

        ### 가맹점 구현 가이드
        - HTTP 메서드: `POST`
        - Content-Type: `application/json`
        - 응답: HTTP 200 + JSON `{"resultCode":"OK"}` (그 외는 실패로 간주)
        - 재시도: 5분 간격, 최대 5회 (24시간 이내)
        - 멱등성: `paymentId` 기준으로 중복 처리 방지 권장
      security: []
      responses:
        "200":
          description: 가맹점이 수신하는 payload 의 schema
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookPayload'
