openapi: 3.0.3
info:
  title: PARKING 24 Partner API
  version: "1.0"
  description: |
    파킹24 외부 제휴사 통합 API. 검색 / 예약 / 취소 / 쿠폰 / webhook 지원.

    - 인증: Bearer (`pk24_live_` 운영 / `pk24_test_` sandbox)
    - 타임존: KST (UTC+09:00)
    - 통화: KRW (정수)
    - 권장 경로: `/api/partner-api/v1/...` (무버전 경로는 2026-11-14 deprecation)

    가이드: https://parking24.me/docs/partner-api
    Changelog: https://parking24.me/docs/partner-api/changelog
  contact:
    name: PARKING 24 파트너 지원팀
    url: https://parking24.me/docs/partner-api#%ED%86%B5%ED%95%A9-%EB%AC%B8%EC%9D%98--%EC%97%B0%EB%9D%BD%EC%B2%98

servers:
  - url: https://parking24.me/api/partner-api/v1
    description: Production (v1, 권장)
  - url: https://parking24.me/api/partner-api
    description: Production (무버전 alias, 2026-11-14 deprecation)

security:
  - BearerAuth: []

tags:
  - name: search
    description: 이용권 검색
  - name: reservations
    description: 예약 생성 / 조회 / 취소
  - name: coupon
    description: 쿠폰 검증 / 적용

paths:
  /search:
    get:
      tags: [search]
      summary: 이용권 검색
      description: |
        공항 + 입출차 날짜로 구매 가능한 이용권 목록 조회.
        Exclusive 이용권은 해당 날짜 잔여 수량이 있을 때만 반환.
      parameters:
        - { name: airport_id, in: query, required: true, schema: { type: string, format: uuid }, description: 공항 UUID }
        - { name: checkin_date, in: query, required: true, schema: { type: string, format: date }, description: 입차일 (KST, YYYY-MM-DD) }
        - { name: checkout_date, in: query, required: true, schema: { type: string, format: date }, description: 출차일 (KST, YYYY-MM-DD) }
      responses:
        "200":
          description: 성공
          headers:
            X-P24-Timing: { $ref: "#/components/headers/XP24Timing" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchSuccessResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /reservations:
    post:
      tags: [reservations]
      summary: 예약 생성
      description: |
        새 예약 생성. **실 예약** 이 생성되므로 sandbox 키(`pk24_test_`)로 테스트
        후 live 키로 전환 권장.

        `Idempotency-Key` 헤더로 24시간 내 재시도 안전성 확보.
      parameters:
        - { name: Idempotency-Key, in: header, required: false, schema: { type: string, maxLength: 255 }, description: 멱등성 키 (UUID v4 권장) }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReservationCreateRequest" }
      responses:
        "200":
          description: 성공
          headers:
            X-P24-Timing: { $ref: "#/components/headers/XP24Timing" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReservationCreateResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/IdempotencyBodyMismatch" }
        "429": { $ref: "#/components/responses/RateLimited" }

    get:
      tags: [reservations]
      summary: 예약 조회
      description: 파트너 예약 목록 조회. `reservation_number` / `status` 로 필터.
      parameters:
        - { name: reservation_number, in: query, schema: { type: string }, description: 특정 예약 단건 조회 }
        - { name: status, in: query, schema: { type: string, enum: [CONFIRMED, CHECKED_IN, COMPLETED, CANCELLED] } }
        - { name: page, in: query, schema: { type: integer, default: 1, minimum: 1 } }
        - { name: limit, in: query, schema: { type: integer, default: 20, minimum: 1, maximum: 100 } }
      responses:
        "200":
          description: 성공
          headers:
            X-P24-Timing: { $ref: "#/components/headers/XP24Timing" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReservationListResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    patch:
      tags: [reservations]
      summary: 예약 취소
      description: CONFIRMED / CHECKED_IN 상태만 취소 가능. COMPLETED 는 불가.
      parameters:
        - { name: Idempotency-Key, in: header, required: false, schema: { type: string, maxLength: 255 } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReservationCancelRequest" }
      responses:
        "200":
          description: 성공
          headers:
            X-P24-Timing: { $ref: "#/components/headers/XP24Timing" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReservationCancelResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/IdempotencyBodyMismatch" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /coupon/validate:
    post:
      tags: [coupon]
      summary: 쿠폰 검증
      description: 쿠폰 코드 유효성 확인 + 적용 후 금액 계산. mutation 없음.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CouponValidateRequest" }
      responses:
        "200":
          description: 성공
          headers:
            X-P24-Timing: { $ref: "#/components/headers/XP24Timing" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CouponValidateResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /coupon/apply:
    post:
      tags: [coupon]
      summary: 쿠폰 적용
      description: |
        예약에 쿠폰 할인 적용. 예약 1건당 쿠폰 1개만 가능.
        `Idempotency-Key` 헤더 지원.
      parameters:
        - { name: Idempotency-Key, in: header, required: false, schema: { type: string, maxLength: 255 } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CouponApplyRequest" }
      responses:
        "200":
          description: 성공
          headers:
            X-P24-Timing: { $ref: "#/components/headers/XP24Timing" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CouponApplyResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/IdempotencyBodyMismatch" }
        "429": { $ref: "#/components/responses/RateLimited" }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "pk24_live_... 또는 pk24_test_..."

  headers:
    XP24Timing:
      description: 단계별 처리 시간 (`name;dur=ms` CSV)
      schema: { type: string, example: "auth;dur=22.7, total;dur=84.0" }

  responses:
    BadRequest:
      description: 잘못된 요청 (MISSING_FIELD / INVALID_*  등)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Unauthorized:
      description: 인증 실패 (AUTH_REQUIRED / INVALID_API_KEY / API_KEY_REVOKED / API_KEY_EXPIRED)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Forbidden:
      description: 권한 없음 (PARTNER_INACTIVE / IP_NOT_ALLOWED / EXCLUSIVE_ACCESS_DENIED)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    NotFound:
      description: 리소스 없음 (RESERVATION_NOT_FOUND)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Conflict:
      description: 충돌 (RESERVATION_DUPLICATE / IDEMPOTENCY_KEY_IN_PROGRESS / RESERVATION_ALREADY_CANCELLED / COUPON_ALREADY_APPLIED)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    IdempotencyBodyMismatch:
      description: 같은 Idempotency-Key 로 다른 body 재호출
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    RateLimited:
      description: Rate Limit 초과
      headers:
        Retry-After:
          description: 재시도 대기 시간 (초)
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }

  schemas:
    ApiResponseEnvelope:
      type: object
      required: [success, data, error]
      properties:
        success: { type: boolean }
        data: { nullable: true }
        error: { type: string, nullable: true }

    ErrorResponse:
      allOf:
        - $ref: "#/components/schemas/ApiResponseEnvelope"
        - type: object
          required: [error_code]
          properties:
            success: { type: boolean, enum: [false] }
            data: { type: object, nullable: true, enum: [null] }
            error: { type: string }
            error_code: { $ref: "#/components/schemas/ErrorCode" }

    ErrorCode:
      type: string
      description: stable error code for branching (외부 자동화용)
      enum:
        - AUTH_REQUIRED
        - INVALID_API_KEY
        - API_KEY_REVOKED
        - API_KEY_EXPIRED
        - PARTNER_INACTIVE
        - IP_NOT_ALLOWED
        - RATE_LIMITED
        - MISSING_FIELD
        - INVALID_FIELD
        - INVALID_UUID
        - INVALID_DATE
        - INVALID_TIME
        - FIELD_TOO_LONG
        - MAX_DURATION_EXCEEDED
        - PLAN_NOT_FOUND
        - EV_NOT_ALLOWED
        - AIRPORT_NOT_AVAILABLE
        - PLAN_NOT_AVAILABLE
        - EXCLUSIVE_SOLD_OUT
        - EXCLUSIVE_ACCESS_DENIED
        - RESERVATION_DUPLICATE
        - RESERVATION_NOT_FOUND
        - RESERVATION_ALREADY_CANCELLED
        - RESERVATION_NOT_CANCELLABLE
        - IDEMPOTENCY_KEY_INVALID
        - IDEMPOTENCY_KEY_IN_PROGRESS
        - IDEMPOTENCY_BODY_MISMATCH
        - COUPON_INVALID
        - COUPON_ALREADY_APPLIED
        - INTERNAL_ERROR

    SearchSuccessResponse:
      allOf:
        - $ref: "#/components/schemas/ApiResponseEnvelope"
        - type: object
          properties:
            success: { type: boolean, enum: [true] }
            error: { type: string, nullable: true, enum: [null] }
            data:
              type: object
              required: [plans, total_days, surcharges]
              properties:
                plans:
                  type: array
                  items: { $ref: "#/components/schemas/SearchPlan" }
                total_days: { type: integer, description: 입출차 양쪽 포함 일수 }
                surcharges:
                  type: object
                  nullable: true
                  description: 공항별 터미널 추가요금 설정 (JSON, 미설정 시 null)
                  additionalProperties: true

    SearchPlan:
      type: object
      required: [plan_id, plan_name, original_price, discounted_price, available_quantity, services, ev_restricted]
      properties:
        plan_id: { type: string, format: uuid }
        plan_name: { type: string, description: 이용권 이름 (DB 설정값 그대로) }
        original_price: { type: integer, description: KRW, 할인 전 }
        discounted_price: { type: integer, description: KRW, 파트너 할인 후 }
        available_quantity: { type: integer, nullable: true, description: Exclusive 만 정수, 그 외 null = 무제한 }
        services:
          description: 이용권 features JSON (자유 형식)
          nullable: true
        ev_restricted: { type: boolean, description: 전기차 불가 plan 이면 true }

    ReservationCreateRequest:
      type: object
      required: [airport_id, plan_id, checkin_date, checkout_date, customer_name, customer_phone, car_number]
      properties:
        airport_id: { type: string, format: uuid }
        plan_id: { type: string, format: uuid }
        exclusive_lot_id: { type: string, format: uuid, nullable: true, description: Exclusive 이용권 시 lot UUID }
        checkin_date: { type: string, format: date }
        checkout_date: { type: string, format: date }
        checkin_time: { type: string, pattern: "^[0-9]{2}:[0-9]{2}$", default: "10:00", description: KST HH:MM }
        checkout_time: { type: string, pattern: "^[0-9]{2}:[0-9]{2}$", default: "18:00" }
        customer_name: { type: string, maxLength: 100 }
        customer_phone: { type: string, maxLength: 20 }
        car_number: { type: string, maxLength: 20 }
        terminal: { type: string, nullable: true, description: "예: T1 / T2 — 공항별 정의" }
        memo: { type: string, maxLength: 500, nullable: true }
        is_electric_vehicle: { type: boolean, default: false, description: "true 이고 plan.ev_restricted 이면 400 EV_NOT_ALLOWED" }

    ReservationCreateResponse:
      allOf:
        - $ref: "#/components/schemas/ApiResponseEnvelope"
        - type: object
          properties:
            success: { type: boolean, enum: [true] }
            data:
              type: object
              required: [reservation_number, status, checkin_date, checkin_time, checkout_date, checkout_time, total_price, plan_name, customer_name, car_number, memo]
              properties:
                reservation_number: { type: string, example: "P24-20260310-001" }
                status: { type: string, enum: [CONFIRMED] }
                checkin_date: { type: string, format: date }
                checkin_time: { type: string, pattern: "^[0-9]{2}:[0-9]{2}$", example: "10:00" }
                checkout_date: { type: string, format: date }
                checkout_time: { type: string, pattern: "^[0-9]{2}:[0-9]{2}$", example: "18:00" }
                total_price: { type: integer }
                plan_name: { type: string }
                customer_name: { type: string }
                car_number: { type: string }
                memo: { type: string, nullable: true }

    ReservationListResponse:
      allOf:
        - $ref: "#/components/schemas/ApiResponseEnvelope"
        - type: object
          properties:
            success: { type: boolean, enum: [true] }
            data:
              type: object
              required: [reservations, total, page, limit]
              properties:
                reservations:
                  type: array
                  items: { $ref: "#/components/schemas/ReservationListItem" }
                total: { type: integer }
                page: { type: integer }
                limit: { type: integer }

    ReservationListItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        reservation_number: { type: string }
        airport_id: { type: string, format: uuid }
        plan_id: { type: string, format: uuid }
        exclusive_lot_id: { type: string, format: uuid, nullable: true }
        checkin_date: { type: string, format: date }
        checkin_time: { type: string, pattern: "^[0-9]{2}:[0-9]{2}$", example: "10:00" }
        checkout_date: { type: string, format: date }
        checkout_time: { type: string, pattern: "^[0-9]{2}:[0-9]{2}$", example: "18:00" }
        customer_name: { type: string }
        car_number: { type: string }
        memo: { type: string, nullable: true }
        total_days: { type: integer }
        price_per_day: { type: integer }
        partner_discount_amount: { type: integer }
        total_price: { type: integer }
        cancellation_fee: { type: integer }
        status: { type: string, enum: [CONFIRMED, CHECKED_IN, COMPLETED, CANCELLED] }
        booking_source: { type: string, enum: [PARTNER_API] }
        terminal: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ReservationCancelRequest:
      type: object
      required: [reservation_number]
      properties:
        reservation_number: { type: string }

    ReservationCancelResponse:
      allOf:
        - $ref: "#/components/schemas/ApiResponseEnvelope"
        - type: object
          properties:
            success: { type: boolean, enum: [true] }
            data:
              type: object
              required: [reservation_id, reservation_number, status, cancellation_fee, total_price, refund_amount]
              properties:
                reservation_id: { type: string, format: uuid }
                reservation_number: { type: string }
                status: { type: string, enum: [CANCELLED] }
                cancellation_fee: { type: integer, description: "취소 수수료 (KRW)" }
                total_price: { type: integer, description: "원 결제 총액 (KRW)" }
                refund_amount: { type: integer, description: "환불 예정 금액 = total_price - cancellation_fee (KRW)" }

    CouponValidateRequest:
      type: object
      required: [code, original_price]
      properties:
        code: { type: string, description: 쿠폰 코드 }
        original_price: { type: integer, minimum: 0 }

    CouponValidateResponse:
      allOf:
        - $ref: "#/components/schemas/ApiResponseEnvelope"
        - type: object
          properties:
            success: { type: boolean, enum: [true] }
            data:
              type: object
              required: [valid, coupon_id, coupon_code_id, discount_amount, parking24_cost, partner_cost, final_price]
              properties:
                valid: { type: boolean }
                coupon_id: { type: string, format: uuid, nullable: true }
                coupon_code_id: { type: string, format: uuid, nullable: true }
                discount_amount: { type: integer }
                parking24_cost: { type: integer, description: 본사 분담액 }
                partner_cost: { type: integer, description: 파트너 분담액 (정산 차감) }
                final_price: { type: integer, description: 할인 후 금액 }

    CouponApplyRequest:
      type: object
      required: [code, reservation_id, original_price]
      properties:
        code: { type: string }
        reservation_id: { type: string, format: uuid }
        original_price: { type: integer, minimum: 0 }

    CouponApplyResponse:
      allOf:
        - $ref: "#/components/schemas/ApiResponseEnvelope"
        - type: object
          properties:
            success: { type: boolean, enum: [true] }
            data:
              type: object
              required: [reservation_id, coupon_id, coupon_code_id, discount_amount, parking24_cost, partner_cost, final_price]
              properties:
                reservation_id: { type: string, format: uuid }
                coupon_id: { type: string, format: uuid }
                coupon_code_id: { type: string, format: uuid, nullable: true }
                discount_amount: { type: integer }
                parking24_cost: { type: integer }
                partner_cost: { type: integer }
                final_price: { type: integer }

    WebhookPayload:
      type: object
      description: PARKING 24 → 파트너 webhook 발송 payload
      required: [event, data, timestamp]
      properties:
        event:
          type: string
          enum: [reservation.confirmed, reservation.cancelled]
        data:
          type: object
          required: [reservation_id, reservation_number, airport, plan, checkin_date, checkout_date, total_price, customer_name, status, is_sandbox]
          properties:
            reservation_id: { type: string, format: uuid }
            reservation_number: { type: string }
            airport: { type: string }
            plan: { type: string }
            checkin_date: { type: string, format: date }
            checkout_date: { type: string, format: date }
            total_price: { type: integer }
            cancellation_fee: { type: integer, description: cancelled 만 포함 }
            customer_name: { type: string }
            status: { type: string, enum: [CONFIRMED, CANCELLED] }
            is_sandbox: { type: boolean, description: sandbox 키로 만든 예약이면 true }
        timestamp: { type: string, format: date-time, description: UTC ISO 8601 }
