openapi: 3.1.0
info:
  title: SsanQuick API
  version: 1.0.0-beta
  description: 이륜 퀵서비스 견적, 등록 카드 건별 결제 주문, 조회 및 취소 API
  license:
    name: 싼퀵 API 이용조건
    url: https://ssanquick.com/developers/terms
servers:
  - url: https://ssanquick.com/api/v1
security:
  - bearerAuth: []
paths:
  /quotes:
    post:
      summary: 실시간 견적 조회
      operationId: createQuote
      x-scope: quotes:read
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QuoteRequest' }
      responses:
        '200':
          description: 견적 조회 성공
          headers: { X-Request-Id: { $ref: '#/components/headers/RequestId' } }
          content: { application/json: { schema: { $ref: '#/components/schemas/QuoteResponse' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ApiError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/ApiError' }
  /orders:
    post:
      summary: 등록 카드 결제 후 주문 생성
      operationId: createOrder
      x-scope: orders:write
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OrderCreateRequest' }
      responses:
        '201':
          description: 결제 및 배차 성공
          headers: { X-Request-Id: { $ref: '#/components/headers/RequestId' } }
          content: { application/json: { schema: { $ref: '#/components/schemas/OrderResponse' } } }
        '200':
          description: 동일 멱등 요청의 기존 주문
          content: { application/json: { schema: { $ref: '#/components/schemas/OrderResponse' } } }
        '402': { $ref: '#/components/responses/ApiError' }
        '409': { $ref: '#/components/responses/ApiError' }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      summary: 주문 목록
      operationId: listOrders
      x-scope: orders:read
      parameters:
        - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
      responses:
        '200': { description: 주문 목록 }
        '429': { $ref: '#/components/responses/RateLimited' }
  /orders/{orderId}:
    get:
      summary: 주문 상세
      operationId: getOrder
      x-scope: orders:read
      parameters:
        - $ref: '#/components/parameters/OrderId'
      responses:
        '200': { description: 주문 상세, content: { application/json: { schema: { $ref: '#/components/schemas/OrderResponse' } } } }
        '404': { $ref: '#/components/responses/ApiError' }
  /orders/{orderId}/cancel-fee:
    post:
      summary: 취소 수수료 조회
      operationId: getCancelFee
      x-scope: orders:write
      parameters:
        - $ref: '#/components/parameters/OrderId'
      responses:
        '200': { description: 취소 가능 여부와 수수료 }
        '404': { $ref: '#/components/responses/ApiError' }
  /orders/{orderId}/cancel:
    post:
      summary: 배차 취소 및 원 카드 결제 취소
      operationId: cancelOrder
      x-scope: orders:write
      parameters:
        - $ref: '#/components/parameters/OrderId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expectedFee, cancelReason]
              properties:
                expectedFee: { type: integer, minimum: 0 }
                cancelReason: { type: string, minLength: 1, maxLength: 200 }
      responses:
        '200': { description: 취소 및 환불 처리 성공 }
        '409': { description: 취소 수수료 변경, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '502': { description: 배차 취소 후 카드 환불 실패, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /usage:
    get:
      summary: API 사용량 조회
      operationId: getUsage
      x-scope: orders:read
      responses:
        '200': { description: 엔드포인트별 호출 및 오류 수 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_live
  headers:
    RequestId:
      description: 장애 문의 및 로그 추적용 요청 ID
      schema: { type: string, format: uuid }
  parameters:
    OrderId:
      name: orderId
      in: path
      required: true
      schema: { type: string, pattern: '^ord_' }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 1, maxLength: 128 }
  responses:
    Unauthorized:
      description: 인증 실패
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    RateLimited:
      description: 요청 한도 초과
      headers:
        Retry-After: { schema: { type: integer } }
        X-RateLimit-Remaining: { schema: { type: integer } }
        X-RateLimit-Reset: { schema: { type: integer } }
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    ApiError:
      description: API 오류
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
  schemas:
    QuoteRequest:
      type: object
      required: [departureAddress, destinationAddress, vehicleType]
      properties:
        departureAddress: { type: string, minLength: 5 }
        destinationAddress: { type: string, minLength: 5 }
        departureLat: { type: number, minimum: -90, maximum: 90 }
        departureLng: { type: number, minimum: -180, maximum: 180 }
        destinationLat: { type: number, minimum: -90, maximum: 90 }
        destinationLng: { type: number, minimum: -180, maximum: 180 }
        vehicleType: { type: string, const: motorcycle }
        isRoundtrip: { type: boolean, default: false }
    QuoteOffer:
      type: object
      required: [carrier, carrierCode, serviceType, price]
      properties:
        carrier: { type: string }
        carrierCode: { type: string }
        serviceType: { type: string, enum: [economy, standard, express] }
        price: { type: integer, minimum: 100 }
        estimatedTime: { type: [string, 'null'] }
        estimatedTimeSeconds: { type: [integer, 'null'] }
    QuoteResponse:
      type: object
      required: [success, data]
      properties:
        success: { type: boolean, const: true }
        data:
          type: object
          required: [quoteId, resultStatus, offers, expiresAt]
          properties:
            quoteId: { type: string, pattern: '^qt_' }
            resultStatus: { type: string, enum: [complete, partial, unavailable] }
            offers: { type: array, items: { $ref: '#/components/schemas/QuoteOffer' } }
            expiresAt: { type: string, format: date-time }
    OrderCreateRequest:
      type: object
      required: [quoteId, serviceType, senderName, senderPhone, receiverName, receiverPhone]
      properties:
        quoteId: { type: string, pattern: '^qt_' }
        serviceType: { type: string, enum: [economy, standard, express] }
        senderName: { type: string }
        senderPhone: { type: string }
        receiverName: { type: string }
        receiverPhone: { type: string }
        clientOrderId: { type: string }
        departureAddressDetail: { type: string }
        destinationAddressDetail: { type: string }
        driverNote: { type: string, maxLength: 50 }
    OrderResponse:
      type: object
      required: [success, data]
      properties:
        success: { type: boolean, const: true }
        data:
          type: object
          required: [orderId, status, paymentStatus, providerStatus, price, payment]
          properties:
            orderId: { type: string, pattern: '^ord_' }
            status: { type: string, enum: [pending, processing, confirmed, in_progress, delivered, cancelled, cancel_partial, cancel_failed] }
            paymentStatus: { type: string, enum: [pending, paid, failed, cancelled, refunded] }
            providerStatus: { type: string }
            price: { type: integer }
            receiptUrl: { type: string, format: uri }
            payment:
              type: object
              properties:
                mode: { type: string, const: registered_billing_card }
                charged: { type: boolean }
    ErrorResponse:
      type: object
      required: [success, error]
      properties:
        success: { type: boolean, const: false }
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details: { type: object, additionalProperties: true }
