openapi: 3.1.0
info:
  title: TourMAX Partner API
  version: 1.0.0
  summary: Alt acenteler için B2B rezervasyon API'si
  description: |
    TourMAX Partner API, alt acentelerin otel/tur sorgulama, fiyat teklifi ve
    rezervasyon işlemlerini programatik olarak yapmasını sağlar. 

    ## Temel ilkeler
    - **Kimlik doğrulama:** OAuth 2.0 `client_credentials` akışı, Bearer JWT.
    - **Environment:** `live` (varsayılan) / `sandbox` — `X-Environment` header'ı.
    - **Idempotency:** Yazma uçlarında `Idempotency-Key` header'ı zorunlu.
    - **Rate Limit:** `X-RateLimit-*` başlıkları her yanıtta döner.
    - **Locale/Currency hint:** `Accept-Language` + `X-Currency`; sunucu
      `Content-Language` ve `X-Currency` başlıklarıyla tercihi teyit eder.
    - **Enum gösterim:** Tüm durum/tip/cinsiyet değerleri **UPPERCASE**
      (ör. `HOTEL`, `CONFIRMED`, `ADULT`, `MALE`).
    - **Para birimi:** `price.original` ve `price.try_equivalent` paralel
      döner. Kur kaynağı `rate_source = REALTIME`.

  contact:
    name: TourMAX Entegrasyon
    email: bilgi@woxteknoloji.com
  license:
    name: Proprietary
servers:
  - url: https://example.com/apiv2/partner-api/v1
    description: Production

tags:
  - name: OAuth
    description: Kimlik doğrulama (client_credentials)
  - name: Me
    description: Kimlik & hesap bilgisi
  - name: Hotels
    description: Otel listeleme, detay, müsaitlik & teklif
  - name: Tours
    description: Tur listeleme, detay, müsaitlik & teklif
  - name: Reservations
    description: Rezervasyon oluştur / listele / detay / iptal
  - name: Content
    description: Referans kataloglar (ülke, şehir, döviz, pansiyon, tesis özellikleri)

security:
  - bearerAuth: []

paths:
  # ---------------- OAUTH ----------------
  /oauth/token:
    post:
      tags: [OAuth]
      summary: Erişim jetonu al
      description: Client credentials grant. Ömrü 3600 sn. Scope alanı boşluk ayraçlı liste.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [grant_type, client_id, client_secret]
              properties:
                grant_type: { type: string, enum: [client_credentials] }
                client_id:  { type: string, example: cid_xxxxxxxx }
                client_secret: { type: string, example: sk_test_xxxxxx }
                scope:
                  type: string
                  example: "hotels:read reservations:write content:read"
      responses:
        '200':
          description: Başarılı
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'
              examples:
                ok:
                  value:
                    data:
                      access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
                      token_type: "Bearer"
                      expires_in: 3600
                      scope: "hotels:read reservations:write"
        '401': { $ref: '#/components/responses/AuthFailed' }

  # ---------------- ME ----------------
  /me:
    get:
      tags: [Me]
      summary: Bağlı alt acente ve client bilgisi
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }

  # ---------------- HOTELS ----------------
  /hotels:
    get:
      tags: [Hotels]
      summary: Otel listesi
      parameters:
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50, maximum: 100 } }
        - { in: query, name: region, schema: { type: integer } }
        - { in: query, name: city, schema: { type: integer } }
        - { in: query, name: star, schema: { type: integer, minimum: 1, maximum: 5 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
  /hotels/{id}:
    get:
      tags: [Hotels]
      summary: Otel detayı
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /hotels/{id}/availability:
    post:
      tags: [Hotels]
      summary: Müsaitlik sorgula ve teklif (offer) oluştur
      description: |
        Başarılı sorgu sonucunda bir **offer** üretilir. Offer 15 dakika süreyle
        rezervasyona kilitlenir (fiyat & kur). Booking için `offer_id` kullanılır.
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [check_in, check_out, rooms]
              properties:
                check_in:   { type: string, format: date, example: "2026-07-01" }
                check_out:  { type: string, format: date, example: "2026-07-05" }
                currency:   { type: string, example: EUR, enum: [TRY, EUR, USD, GBP] }
                rooms:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required: [room_id, adults]
                    properties:
                      room_id:        { type: string, example: "1" }
                      adults:         { type: integer, minimum: 1, maximum: 4 }
                      children_ages:  { type: array, items: { type: integer, minimum: 0, maximum: 17 } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ---------------- TOURS ----------------
  /tours:
    get:
      tags: [Tours]
      summary: Tur listesi
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /tours/{id}:
    get:
      tags: [Tours]
      summary: Tur detayı
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses: { '200': { description: OK } }
  /tours/{id}/availability:
    post:
      tags: [Tours]
      summary: Tur müsaitlik & teklif
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [start_date, adults]
              properties:
                start_date:    { type: string, format: date }
                adults:        { type: integer, minimum: 1 }
                children_ages: { type: array, items: { type: integer } }
      responses:
        '200': { description: OK }

  # ---------------- RESERVATIONS ----------------
  /reservations:
    get:
      tags: [Reservations]
      summary: Rezervasyon listesi (filtreli)
      parameters:
        - in: query
          name: type
          description: HOTEL | TOUR | ALL
          schema: { type: string, enum: [ALL, HOTEL, TOUR], default: ALL }
        - in: query
          name: status
          schema: { $ref: '#/components/schemas/ReservationStatus' }
        - in: query
          name: filter_type
          description: Tarih filtresinin uygulanacağı alan.
          schema: { type: string, enum: [creation, check_in, check_out], default: creation }
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
        - in: query
          name: client_reference_id
          description: Partner iç referansı (tam eşleşme).
          schema: { type: string, maxLength: 128 }
        - in: query
          name: pnr
          description: Rezervasyon kodu (tam eşleşme).
          schema: { type: string }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/ReservationSummary' }

  /reservations/hotel:
    post:
      tags: [Reservations]
      summary: Otel rezervasyonu oluştur
      description: |
        Geçerli bir `offer_id` ile çağrılır. `guests[]` dizisi düz (flat) formattadır;
        odalara dağıtım, teklifin `rooms_breakdown` sırası (2A+1C → ADULT, ADULT, CHILD)
        kullanılarak yapılır.

        **Hesap cari (`mode=account`)** ise doğrudan `CONFIRMED` döner.
        **Sanal POS (`mode=instant`)** ise `PENDING_PAYMENT` + `payment.url` döner.
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/HotelReservationRequest' }
            examples:
              minimal:
                summary: 2 yetişkin + hesap cari
                value:
                  offer_id: "off_b773206702facc54d21aa1b4"
                  client_reference_id: "BOOK-2026-00123"
                  client_nationality: "TR"
                  contact:
                    name: "Ahmet"
                    surname: "Yılmaz"
                    phone_country_code: "+90"
                    phone_number: "5551234567"
                    email: "a@test.com"
                  guests:
                    - type: ADULT
                      name: Ahmet
                      surname: Yılmaz
                      birth_date: "1985-06-15"
                      gender: MALE
                      nationality: TR
                      identity: { type: TC, number: "10000000146" }
                      is_lead: true
                    - type: ADULT
                      name: Ayşe
                      surname: Yılmaz
                      birth_date: "1988-03-22"
                      gender: FEMALE
                      nationality: TR
                      identity: { type: TC, number: "10000000154" }
                  payment: { mode: account }
                  billing:
                    type: INDIVIDUAL
                    full_name: "Ahmet Yılmaz"
                    tax_id: "10000000146"
                    address: "Atatürk Cd."
                    city: "İstanbul"
                    country: TR
                  additional_info: "Non-smoking, high floor"
                  user_notes:
                    no_smoke_room: true
                    upstairs_room: true
                    honey_moon_couple: true
      responses:
        '201':
          description: Oluşturuldu
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/ReservationDetail' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /reservations/tour:
    post:
      tags: [Reservations]
      summary: Tur rezervasyonu oluştur
      parameters: [ { $ref: '#/components/parameters/IdempotencyKey' } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [offer_id, payment_mode, lead_guest]
              properties:
                offer_id:     { type: string }
                payment_mode: { type: string, enum: [account, instant] }
                lead_guest:
                  type: object
                  required: [first_name, last_name, email, phone, tc]
                  properties:
                    first_name: { type: string }
                    last_name:  { type: string }
                    email:      { type: string, format: email }
                    phone:      { type: string }
                    tc:         { type: string, minLength: 11, maxLength: 11 }
      responses:
        '201': { description: Oluşturuldu }

  /reservations/{ref}:
    get:
      tags: [Reservations]
      summary: Rezervasyon detayı (guests[], rooms[], payment, billing, cancellation, voucher)
      parameters:
        - { in: path, name: ref, required: true, schema: { type: string }, description: "PNR (rxpnr) veya T-{id}" }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/ReservationDetail' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Reservations]
      summary: Rezervasyon güncelle (fiyat etkilemeyen alanlar)
      parameters:
        - { in: path, name: ref, required: true, schema: { type: string } }
      responses: { '200': { description: OK } }

  /reservations/{ref}/voucher:
    get:
      tags: [Reservations]
      summary: Voucher bağlantısı/pdf
      parameters:
        - { in: path, name: ref, required: true, schema: { type: string } }
      responses: { '200': { description: OK } }

  /reservations/{ref}/cancel:
    post:
      tags: [Reservations]
      summary: Rezervasyonu iptal et
      description: |
        **Karar mantığı:**
        - Eğer **ücretsiz iptal süresi** içindeyse (check_in - 24s > now):
          otomatik onaylanır → `status=CANCELLED`, `auto_approved=true`, `200 OK`.
        - Aksi halde iptal **talep** olarak kaydedilir →
          `status=CANCELLATION_REQUESTED`, `auto_approved=false`, `202 Accepted`.
        - Zaten iptal edilmişse `409 CONFLICT` döner.

        `/reservations/{ref}/cancellation-requests` alias'ı aynı uca bağlıdır.
      parameters:
        - { in: path, name: ref, required: true, schema: { type: string } }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:           { type: string, example: "Plan değişikliği" }
                requested_refund: { type: boolean, default: false }
      responses:
        '200':
          description: Otomatik iptal (ücretsiz periyot)
          content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } }
        '202':
          description: İptal talebi oluşturuldu
        '409': { $ref: '#/components/responses/Conflict' }

  /reservations/{ref}/cancellation-requests:
    post:
      tags: [Reservations]
      summary: /cancel ile aynı davranış (geriye dönük uyumluluk alias'ı)
      parameters:
        - { in: path, name: ref, required: true, schema: { type: string } }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      responses:
        '200': { description: OK }
        '202': { description: İptal talebi oluşturuldu }

  # ---------------- CONTENT / CATALOG ----------------
  /content/currencies:
    get: { tags: [Content], summary: Desteklenen para birimleri, responses: { '200': { description: OK } } }
  /content/board-types:
    get: { tags: [Content], summary: "Pansiyon/board tipleri (RO, BB, HB, HB+, FB, FB+, AI, UAI, SC)", responses: { '200': { description: OK } } }
  /content/hotel-facilities:
    get: { tags: [Content], summary: Otel tesis özellikleri (kanonik liste), responses: { '200': { description: OK } } }
  /content/room-facilities:
    get: { tags: [Content], summary: Oda içi donanım özellikleri (kanonik liste), responses: { '200': { description: OK } } }
  /content/countries:
    get: { tags: [Content], summary: Ülkeler, responses: { '200': { description: OK } } }
  /content/cities:
    get:
      tags: [Content]
      summary: Şehirler
      parameters:
        - { in: query, name: country_id, schema: { type: integer } }
      responses: { '200': { description: OK } }
  /content/regions:
    get: { tags: [Content], summary: Bölgeler, responses: { '200': { description: OK } } }
  /content/hotel-concepts:
    get: { tags: [Content], summary: Otel konseptleri, responses: { '200': { description: OK } } }
  /content/tour-themes:
    get: { tags: [Content], summary: Tur temaları, responses: { '200': { description: OK } } }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      description: |
        Aynı istek tekrarlanırsa önceki yanıt 24 saat boyunca tekrar döner.
        En fazla 64 karakter; `[a-zA-Z0-9_\-]` karakter sınıfı.
      schema: { type: string, maxLength: 64 }
    Environment:
      in: header
      name: X-Environment
      description: "live | sandbox (varsayılan: client türüne göre)"
      schema: { type: string, enum: [live, sandbox] }
    AcceptLanguage:
      in: header
      name: Accept-Language
      schema: { type: string, example: "tr" }
      description: Sunucu `Content-Language` header'ı ile tercihi echo eder.
    XCurrency:
      in: header
      name: X-Currency
      schema: { type: string, enum: [TRY, EUR, USD, GBP] }
      description: Fiyat gösteriminde tercih; `rate_source=REALTIME`.

  responses:
    AuthFailed:
      description: Kimlik doğrulama başarısız
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Yetki/scope yetersiz
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: Kayıt bulunamadı
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Conflict:
      description: Çakışma (örn. zaten iptal edilmiş)
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ValidationError:
      description: Doğrulama hatası
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            sample:
              value:
                error:
                  code: VALIDATION_ERROR
                  message: "One or more fields failed validation."
                  details:
                    - { field: "guests[0].identity.number", issue: "invalid_tc_checksum" }

  schemas:
    Envelope:
      type: object
      properties:
        data: {}
        meta:
          type: object
          additionalProperties: true
      required: [data]

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - AUTHENTICATION_FAILED
                - INVALID_TOKEN
                - TOKEN_EXPIRED
                - PERMISSION_DENIED
                - IP_NOT_ALLOWED
                - RATE_LIMITED
                - VALIDATION_ERROR
                - RESOURCE_NOT_FOUND
                - CONFLICT
                - NO_AVAILABILITY
                - OFFER_EXPIRED
                - INTERNAL_ERROR
            message: { type: string }
            request_id: { type: string }
            documentation_url: { type: string, format: uri }
            details:
              oneOf:
                - type: array
                  items:
                    type: object
                    properties:
                      field: { type: string }
                      issue: { type: string }
                - type: object
                  additionalProperties: true

    ReservationStatus:
      type: string
      enum: [PENDING, PENDING_PAYMENT, CONFIRMED, CANCELLED, FAILED]

    Price:
      type: object
      properties:
        original:
          type: object
          properties:
            amount:   { type: number, example: 2173.85 }
            currency: { type: string, example: EUR }
        try_equivalent:
          type: object
          properties:
            amount:   { type: number, example: 114741.24 }
            currency: { type: string, example: TRY }
        exchange_rate: { type: number, example: 52.7825 }
        rate_date:     { type: string, format: date }
        rate_source:
          type: string
          enum: [REALTIME, TCMB]

    Contact:
      type: object
      required: [name, surname, phone_country_code, phone_number, email]
      properties:
        name:               { type: string }
        surname:            { type: string }
        phone_country_code: { type: string, example: "+90" }
        phone_number:       { type: string, example: "5551234567" }
        email:              { type: string, format: email }

    Identity:
      type: object
      properties:
        type:   { type: string, enum: [TC, PASSPORT, ID] }
        number: { type: string }
        expiry: { type: string, format: date, nullable: true }

    Guest:
      type: object
      required: [type, name, surname]
      properties:
        type:        { type: string, enum: [ADULT, CHILD] }
        name:        { type: string }
        surname:     { type: string }
        birth_date:  { type: string, format: date, description: "CHILD için zorunlu." }
        gender:      { type: string, enum: [MALE, FEMALE, UNSPECIFIED] }
        nationality: { type: string, description: "ISO 3166-1 alpha-2" }
        email:       { type: string, format: email, nullable: true }
        phone_country_code: { type: string, nullable: true }
        phone_number:       { type: string, nullable: true }
        identity:    { $ref: '#/components/schemas/Identity' }
        is_lead:     { type: boolean, default: false }

    Billing:
      type: object
      required: [type, full_name]
      properties:
        type:       { type: string, enum: [INDIVIDUAL, CORPORATE] }
        full_name:  { type: string }
        tax_id:     { type: string, description: "Bireyse TC, kurumsalsa VKN." }
        tax_office: { type: string }
        address:    { type: string }
        city:       { type: string }
        country:    { type: string }

    UserNotes:
      type: object
      description: Serbest "otel talep bayrakları". Hepsi opsiyonel.
      properties:
        no_smoke_room:      { type: boolean }
        upstairs_room:      { type: boolean }
        downstairs_room:    { type: boolean }
        quiet_room:         { type: boolean }
        connected_rooms:    { type: boolean }
        honey_moon_couple:  { type: boolean }
        king_bed_room:      { type: boolean }
        twin_bed_room:      { type: boolean }
        sea_view_room:      { type: boolean }
        allergy_free_room:  { type: boolean }

    HotelReservationRequest:
      type: object
      required: [offer_id, contact, guests, payment]
      properties:
        offer_id:            { type: string, example: "off_b773206702facc54d21aa1b4" }
        client_reference_id: { type: string, maxLength: 128, description: "Partner iç referansı." }
        client_nationality:  { type: string, minLength: 2, maxLength: 2 }
        contact:             { $ref: '#/components/schemas/Contact' }
        guests:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/Guest' }
        payment:
          type: object
          required: [mode]
          properties:
            mode: { type: string, enum: [account, instant] }
        billing:         { $ref: '#/components/schemas/Billing' }
        additional_info: { type: string, description: "Serbest not (müşteri ricası)." }
        user_notes:      { $ref: '#/components/schemas/UserNotes' }

    ReservationSummary:
      type: object
      properties:
        reservation_ref:     { type: string }
        type:                { type: string, enum: [HOTEL, TOUR] }
        environment:         { type: string, enum: [live, sandbox] }
        status:              { $ref: '#/components/schemas/ReservationStatus' }
        hotel:
          type: object
          properties: { id: { type: integer } }
        check_in:            { type: string, format: date }
        check_out:           { type: string, format: date }
        nights:              { type: integer }
        adults:              { type: integer }
        children:            { type: integer }
        client_reference_id: { type: string, nullable: true }
        client_nationality:  { type: string, nullable: true }
        contact:
          type: object
          properties:
            full_name: { type: string }
            email:     { type: string }
            phone:     { type: string }
        price:   { $ref: '#/components/schemas/Price' }
        created_at: { type: string, format: date-time }

    ReservationDetail:
      allOf:
        - $ref: '#/components/schemas/ReservationSummary'
        - type: object
          properties:
            rooms:
              type: array
              items:
                type: object
                properties:
                  room_index: { type: integer }
                  room_id:    { type: integer }
                  room_name:  { type: string, nullable: true }
                  adults:     { type: integer }
                  children:   { type: integer }
                  nights:     { type: integer }
                  room_fee:   { type: number }
                  guests:
                    type: array
                    items: { $ref: '#/components/schemas/Guest' }
            guests:
              type: array
              description: Tüm odalardaki misafirlerin flat listesi.
              items: { $ref: '#/components/schemas/Guest' }
            payment:
              type: object
              properties:
                mode:   { type: string, enum: [ACCOUNT, INSTANT] }
                status: { type: string, enum: [ON_ACCOUNT, PENDING, PAID, FAILED] }
                url:    { type: string, format: uri, nullable: true }
            billing: { $ref: '#/components/schemas/Billing' }
            cancellation_policy:
              type: object
              properties:
                free_until:   { type: string, format: date-time, nullable: true }
                cancelled_at: { type: string, format: date-time, nullable: true }
                note:         { type: string }
            voucher:
              type: object
              properties:
                ready: { type: boolean }
                url:   { type: string, format: uri, nullable: true }
            notes:        { type: string, nullable: true }
            confirmed_at: { type: string, format: date-time, nullable: true }
            cancelled_at: { type: string, format: date-time, nullable: true }
