openapi: 3.1.0
info:
  title: Global Auto Hub — Unified Used-Car Listings API
  version: "2.0.0"
  description: |
    Unified, **normalized** REST API over multiple car marketplaces. Every offer is mapped to
    one consistent schema with currency normalization (raw + USD + EUR) — unlike per-platform
    raw feeds. Source platforms: `encar` (🇰🇷), `che168` (🇨🇳), `dongchedi` (🇨🇳),
    `dubizzle` (🇦🇪), `dubicars` (🇦🇪), `autoscout24` (🇪🇺).

    Each offer is returned in an **envelope** `{id, inner_id, change_type, created_at, data}`
    where `id` is the global monotonic change-id (use it to drive incremental sync) and `data`
    is the normalized car card. `change_type` ∈ `added | changed | removed`.
servers:
  - url: https://api.45-8-99-137.sslip.io
    description: Production (HTTPS)
  - url: https://global-auto-hub.ru
    description: Production (custom domain — once DNS is pointed)
  - url: http://45.8.99.137:8080
    description: Production (direct)
tags:
  - name: offers
    description: Browse & fetch normalized listings.
paths:
  /api/v2/{source}/offers:
    get:
      tags: [offers]
      operationId: listOffers
      summary: List offers (filtered, paginated, newest-changed first)
      parameters:
        - { name: source, in: path, required: true, description: Marketplace code, schema: { $ref: "#/components/schemas/Source" } }
        - { name: page, in: query, description: Page number (1-based), schema: { type: integer, minimum: 1, default: 1 } }
        - { name: mark, in: query, description: "Make (case-insensitive; alias: make)", schema: { type: string } }
        - { name: make, in: query, description: Alias of mark, schema: { type: string } }
        - { name: model, in: query, schema: { type: string } }
        - { name: complectation, in: query, description: Trim, schema: { type: string } }
        - { name: transmission_type, in: query, schema: { type: string } }
        - { name: color, in: query, schema: { type: string } }
        - { name: body_type, in: query, schema: { type: string } }
        - { name: engine_type, in: query, description: "Fuel/engine type (alias: fuel_type)", schema: { type: string } }
        - { name: year_from, in: query, schema: { type: integer } }
        - { name: year_to, in: query, schema: { type: integer } }
        - { name: price_from, in: query, description: Min price in EUR, schema: { type: integer } }
        - { name: price_to, in: query, description: Max price in EUR, schema: { type: integer } }
        - { name: km_age_from, in: query, schema: { type: integer } }
        - { name: km_age_to, in: query, schema: { type: integer } }
      responses:
        "200":
          description: A page of offers.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ListResponse" }
        "500": { $ref: "#/components/responses/Error" }
  /api/v2/{source}/offer:
    get:
      tags: [offers]
      operationId: getOffer
      summary: Fetch a single offer by its marketplace id (inner_id)
      parameters:
        - { name: source, in: path, required: true, schema: { $ref: "#/components/schemas/Source" } }
        - { name: inner_id, in: query, required: true, description: The id on the source platform, schema: { type: string } }
      responses:
        "200":
          description: The offer.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OfferEnvelope" }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
  /api/v2/{source}/change_id:
    get:
      tags: [offers]
      operationId: changeIdByDate
      summary: Start change-id cursor for a date (for the /changes feed)
      parameters:
        - { name: source, in: path, required: true, schema: { $ref: "#/components/schemas/Source" } }
        - { name: date, in: query, required: true, description: "yyyy-mm-dd", schema: { type: string } }
      responses:
        "200":
          description: The first change_id at/after the date.
          content:
            application/json:
              schema: { type: object, properties: { change_id: { type: integer, format: int64 } } }
        "400": { $ref: "#/components/responses/Error" }
  /api/v2/{source}/changes:
    get:
      tags: [offers]
      operationId: listChanges
      summary: Delta-sync feed — added/changed/removed events from a change_id cursor
      parameters:
        - { name: source, in: path, required: true, schema: { $ref: "#/components/schemas/Source" } }
        - { name: change_id, in: query, description: "Start change_id (inclusive); 0 = from the beginning", schema: { type: integer, format: int64 } }
        - { name: limit, in: query, schema: { type: integer, default: 100, maximum: 1000 } }
      responses:
        "200":
          description: A page of changes with a next-cursor.
          content:
            application/json:
              schema:
                type: object
                properties:
                  result: { type: array, items: { $ref: "#/components/schemas/OfferEnvelope" } }
                  meta: { type: object, properties: { cur_change_id: { type: integer, format: int64 }, next_change_id: { type: integer, format: int64 }, limit: { type: integer } } }
  /api/v2/{source}/filters:
    get:
      tags: [offers]
      operationId: getFilters
      summary: Available filter values (mark→models tree + flat lists)
      parameters:
        - { name: source, in: path, required: true, schema: { $ref: "#/components/schemas/Source" } }
      responses:
        "200":
          description: Filter values.
          content: { application/json: { schema: { type: object } } }
  /healthz:
    get:
      summary: Liveness probe
      responses: { "200": { description: ok } }
components:
  responses:
    Error:
      description: Error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Source:
      type: string
      enum: [encar, autoscout24, che168, dongchedi, dubizzle, dubicars, mobilede, guazi]
    ListResponse:
      type: object
      properties:
        result: { type: array, items: { $ref: "#/components/schemas/OfferEnvelope" } }
        meta: { $ref: "#/components/schemas/Meta" }
    Meta:
      type: object
      properties:
        page: { type: integer }
        next_page: { type: integer, description: 0 when there is no next page }
        limit: { type: integer }
    OfferEnvelope:
      type: object
      properties:
        id: { type: integer, format: int64, description: Global monotonic change-id (incremental-sync cursor) }
        inner_id: { type: string, description: Id on the source platform }
        change_type: { type: string, enum: [added, changed, removed] }
        created_at: { type: [string, "null"], format: date-time }
        data: { $ref: "#/components/schemas/Data" }
    Data:
      type: object
      description: Normalized car card.
      properties:
        id: { type: integer, format: int64 }
        inner_id: { type: string }
        url: { type: string }
        source: { $ref: "#/components/schemas/Source" }
        mark: { type: string, description: Make (brand) }
        model: { type: string }
        generation: { type: string }
        complectation: { type: string, description: Trim }
        year: { type: integer }
        year_month: { type: string }
        body_type: { type: string }
        fuel_type: { type: string, description: "enum: petrol|diesel|hybrid|phev|ev|lpg|..." }
        transmission: { type: string, description: "enum: automatic|manual|cvt|dct|amt" }
        drive_type: { type: string, description: "fwd|rwd|awd" }
        engine_cc: { type: integer }
        power_hp: { type: integer }
        km_age: { type: integer, description: Mileage in km }
        color: { type: string }
        vin: { type: string }
        price: { $ref: "#/components/schemas/Price" }
        country: { type: string }
        region: { type: string }
        city: { type: string }
        seller_type: { type: string }
        is_dealer: { type: boolean }
        seller_name: { type: string }
        salon_id: { type: string, description: Dealer / seller id }
        title: { type: string }
        description: { type: string }
        reg_date: { type: [string, "null"], format: date-time, description: Listing publish/registration date }
        images: { type: array, items: { type: string }, description: ALL photo URLs }
        equipment: { type: array, items: { type: string }, description: Equipment / features list }
        condition: { type: object, additionalProperties: true, description: "Accident / owners / emission / inspection" }
        extra_prep: { type: object, additionalProperties: true, description: "All detail specs (doors/seats/full spec table/...)" }
        status: { type: string, enum: [active, removed] }
    Price:
      type: object
      description: Source-currency amount plus normalized USD/EUR.
      properties:
        raw: { type: integer, format: int64, description: Amount in raw currency }
        currency: { type: string, description: "ISO code: KRW|CNY|AED|EUR" }
        usd: { type: integer }
        eur: { type: integer }
    Error:
      type: object
      properties:
        error: { type: string }
