openapi: 3.0.3
info:
  title: GNA×하나캐피탈 차량금융 견적 오케스트레이터 API
  version: "1.0.0"
  description: |
    하나캐피탈_차량금융상품_연동가이드 기반. GNA(다타랩) 견적 화면이 사용하는 상위 래퍼 API.
    내부에서 하나캐피탈 로컬 릴레이(localhost:11080/inbound)를 2단계로 호출한다:
      1) dvCd=1 잔가조회  → mxRevalRtoCtnt 에서 계약기간·주행거리구간별 최대 잔가율(%) 추출
      2) dvCd=2 렌탈료계산 → rentalFeeInfo 에서 월 렌탈료(rlpnRtfe) 산출
    최종적으로 GNA 견적값 priceResidualRto / priceResidual / priceMonth 를 반환한다.

    ── 하위 릴레이 전문(참고) ──
    - 엔드포인트: POST http://localhost:11080/inbound  (Content-Type: application/json, raw JSON body)
    - serviceID: UCARI00013, version 1.0
    - 인증: Message.reqInfo.ccoCustNo / ccoCustSno (하나캐피탈 발급)
    - 정상 판정: Header.code ∈ {0000,Z000,1200,H000,F000} 그리고 resInfo.rspnsCd=PS000001
    - 취급제한(업무거절): rspnsCd=PS000100 → mxRvInfo/rentalFeeInfo 없음
servers:
  - url: http://localhost:8080
    description: 오케스트레이터 (로컬 기본)
tags:
  - name: quote
    description: 차량금융 견적

paths:
  /api/v1/vehicle-finance/quote:
    post:
      tags: [quote]
      summary: 잔가+렌탈료 통합 견적 (2단계 자동 호출)
      description: |
        vehicle(매칭된 하나코드) + contract(금액·기간)만 넘기면 잔가조회→렌탈료계산을
        서버가 순차 수행하고 GNA 최종 산출값을 반환한다.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QuoteRequest' }
            examples:
              default:
                summary: 쏘나타 디 엣지 가솔린 1.6터보, 3,400만원/36개월/기본구간
                value:
                  vehicle:  { braNo: "111", mdlNo: "11609", linupNo: "115311", trimNo: "1049401" }
                  contract: { puryCarAmt: 34000000, cntrPrid: 36, prrpAmt: 0, drvrAgeDvCd: "04" }
                  options:  { reprPrdtCd: "1", custDvCd: "1", agrTrvgDstnCd: "20000" }
      responses:
        "200":
          description: 정상 견적
          content:
            application/json:
              schema: { $ref: '#/components/schemas/QuoteResponse' }
              example:
                status: "OK"
                priceResidualRto: 61.5
                priceResidual: 20910000
                priceMonth: 1174910
                rentalFeeInfo:
                  rlpnRtfe: "1174910"
                  carRtfe: "986700"
                  addRtfe: "81400"
                  spprcAmt: "1068100"
                  srtxAmt: "106810"
                  acqCamtAmt: "31679602"
                  irrPer: "5.8"
                  jnIscoNm: "디비손해보험주식회사"
                residualDistanceSeg: "00000"
                resInfo: { rspnsCd: "PS000001", rspnsMsg: "정상처리 되었습니다." }
        "400":
          description: 필수값 누락 / 요청 파싱 실패
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        "422":
          description: 취급제한(업무거절, rspnsCd=PS000100)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RestrictedResponse' }
              example:
                status: "RESTRICTED"
                stage: "residual"
                rspnsCd: "PS000100"
                rspnsMsg: "취급제한 차종입니다. 영업점에 문의."
        "502":
          description: 릴레이 접속/연계 오류 (Z***, 1xxx~2xxx 등)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /api/v1/vehicle-finance/residual:
    post:
      tags: [quote]
      summary: 잔가조회만 (dvCd=1)
      description: 잔가율(%)과 적용잔가금액만 필요할 때. 렌탈료 계산은 수행하지 않는다.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QuoteRequest' }
      responses:
        "200":
          description: 잔가 결과
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ResidualResponse' }
        "422": { description: 취급제한 }
        "502": { description: 릴레이/연계 오류 }

  /health:
    get:
      tags: [quote]
      summary: 헬스체크
      responses:
        "200":
          description: UP
          content:
            application/json:
              example: { status: "UP", relay: "http://localhost:11080/inbound" }

components:
  schemas:
    Vehicle:
      type: object
      required: [braNo, mdlNo, linupNo, trimNo]
      properties:
        braNo:   { type: string, description: "브랜드코드 (GNA 매칭값 apiHanaBrand)",  example: "111" }
        mdlNo:   { type: string, description: "모델코드 (apiHanaModel)",   example: "11609" }
        linupNo: { type: string, description: "라인업코드 (apiHanaLineup)", example: "115311" }
        trimNo:  { type: string, description: "트림코드 (apiHanaTrim)",    example: "1049401" }
    Contract:
      type: object
      required: [puryCarAmt, cntrPrid]
      properties:
        puryCarAmt:  { type: integer, description: "차량금액 + 옵션금액", example: 34000000 }
        cntrPrid:    { type: integer, description: "계약기간(개월) 12/24/36/48/60", example: 36 }
        prrpAmt:     { type: integer, description: "선납금", default: 0, example: 0 }
        drvrAgeDvCd: { type: string,  description: "운전자연령구분 02:만21세↑ 04:만26세↑", default: "04", example: "04" }
    Options:
      type: object
      description: "Message.info 고정 기본값 오버라이드(선택). 미지정 시 서버 config.FIXED_INFO 사용."
      properties:
        reprPrdtCd:     { type: string, description: "정비상품 01~07", example: "1" }
        agrTrvgDstnCd:  { type: string, description: "약정주행거리(km) 15000~50000", example: "20000" }
        custDvCd:       { type: string, description: "고객구분 1:개인 2:개인사업자 3:법인", example: "1" }
        dlvyArDvCd:     { type: string, description: "출고지역코드", example: "004" }
        residualDstnCd: { type: string, description: "잔가율 조회 시 사용할 주행거리 구간 키(기본 00000)", example: "00000" }
    QuoteRequest:
      type: object
      required: [vehicle, contract]
      properties:
        vehicle:  { $ref: '#/components/schemas/Vehicle' }
        contract: { $ref: '#/components/schemas/Contract' }
        options:  { $ref: '#/components/schemas/Options' }
    QuoteResponse:
      type: object
      properties:
        status:            { type: string, example: "OK" }
        priceResidualRto:  { type: number, description: "잔가율(%) — 1단계", example: 61.5 }
        priceResidual:     { type: integer, description: "적용잔가금액 = 차량금액 × 잔가율%", example: 20910000 }
        priceMonth:        { type: integer, description: "월 렌탈료(rlpnRtfe) — 2단계", example: 1174910 }
        residualDistanceSeg: { type: string, example: "00000" }
        rentalFeeInfo:     { type: object, description: "렌탈료 응답 원문(carRtfe/addRtfe/spprcAmt/srtxAmt/acqCamtAmt/irrPer/jnIscoNm 등)" }
        resInfo:           { type: object, properties: { rspnsCd: { type: string }, rspnsMsg: { type: string } } }
    ResidualResponse:
      type: object
      properties:
        priceResidualRto: { type: number, example: 61.5 }
        priceResidual:    { type: integer, example: 20910000 }
        residualDistanceSeg: { type: string, example: "00000" }
        mxRevalRtoCtnt:   { type: object, description: "파싱된 기간·구간별 잔가율 맵" }
    RestrictedResponse:
      type: object
      properties:
        status:   { type: string, example: "RESTRICTED" }
        stage:    { type: string, enum: [residual, rentalFee] }
        rspnsCd:  { type: string, example: "PS000100" }
        rspnsMsg: { type: string }
    ErrorResponse:
      type: object
      properties:
        status:  { type: string, example: "ERROR" }
        stage:   { type: string }
        error:   { type: string }
        missing: { type: array, items: { type: string } }
        headerCode: { type: string }
