openapi: 3.0.3

info:
  title: Gefli API
  version: "1.0.0"
  description: |
    Gefli, Print-on-Demand pazaryeri olarak çalışır. Bu API üzerinden müşterilerimiz
    kendi e-ticaret platformlarını (Shopify, ikas, Trendyol, Hepsiburada) veya
    kendi geliştirdikleri uygulamaları Gefli'ye bağlayabilir; ürünlerini yayınlayıp
    eşleştirebilir, gelen siparişleri Gefli üzerinden ürettirip kargolatabilir.

    ## Authentication

    Tüm istekler **Bearer token** ile yetkilendirilir:

    ```http
    Authorization: Bearer gfk_live_xxxxxxxxxxxxxxxxxxxxxx
    ```

    Token'ı kullanıcı paneli → **API & Entegrasyonlar** sayfasından oluşturabilirsin.
    Token tek seferlik gösterilir; DB'de yalnızca SHA-256 hash saklanır.

    ## Rate Limit

    Token başına dakikada **120 istek**. Limit aşıldığında `429 Too Many Requests`
    döner; `Retry-After` header'ı bekleme süresini bildirir.

    ## Idempotency

    Webhook akışı `platform + external_event_id` çiftine göre idempotent — aynı olay
    iki kez geldiğinde 2. istek `{"duplicate": true}` döner, sipariş mükerrer
    oluşturulmaz.

    ## Hatalar

    Tüm hata yanıtları aynı yapıda gelir:

    ```json
    { "success": false, "error": { "code": "validation_failed", "message": "...", "details": {...} } }
    ```

  contact:
    name: Gefli Destek
    url: https://gefli.com/settings/api
  license:
    name: Proprietary
    url: https://gefli.com

servers:
  - url: https://gefli.com/api/v1
    description: Production
  - url: http://localhost/api/v1
    description: Local development

tags:
  - name: Meta
    description: Token sahibi & cüzdan bilgileri
  - name: Products
    description: Kullanıcının Gefli'de yayınladığı tasarımlar (varyant + SKU)
  - name: Integrations
    description: Platform bağlantıları (Shopify/ikas/Trendyol/Hepsi/public)
  - name: Mappings
    description: Gefli varyantı ↔ harici SKU eşleştirme
  - name: Orders
    description: Harici platformlardan gelen siparişler
  - name: Webhooks
    description: Platform tarafından çağrılan olay endpoint'i (HMAC imzalı)

security:
  - bearerAuth: []

paths:

  /me:
    get:
      tags: [Meta]
      summary: Token sahibi & cüzdan
      description: Authenticated user'ın temel bilgilerini, cüzdan bakiyesini ve aktif token meta'sını döner.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
        "401":
          $ref: '#/components/responses/Unauthorized'

  /products:
    get:
      tags: [Products]
      summary: Kullanıcının yayınladığı tasarımları listele
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/ProductSummary' }
        "401":
          $ref: '#/components/responses/Unauthorized'

  /products/{slug}:
    get:
      tags: [Products]
      summary: Tek tasarım detayı + varyantlar
      parameters:
        - { in: path, name: slug, required: true, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: '#/components/schemas/ProductDetail' }
        "404":
          $ref: '#/components/responses/NotFound'

  /integrations:
    get:
      tags: [Integrations]
      summary: Bağlı entegrasyonları listele
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Integration' }
    post:
      tags: [Integrations]
      summary: Yeni entegrasyon bağla
      description: |
        Platform credentials (API key, access token vb.) JSON gövdesinde gönderilir.
        Sunucu bunları AES-256-GCM ile şifreleyerek saklar; plaintext asla yatmaz.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [platform, display_name, credentials]
              properties:
                platform:
                  type: string
                  enum: [shopify, ikas, trendyol, hepsiburada, public]
                display_name:
                  type: string
                  example: "shop.gefli.com"
                external_id:
                  type: string
                  example: "shop.myshopify.com"
                credentials:
                  type: object
                  additionalProperties: true
                  example:
                    access_token: "shpat_xxx"
                    api_key: "yyy"
                auto_approve: { type: boolean, default: false }
                auto_pay: { type: boolean, default: false }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 42 }
        "422":
          $ref: '#/components/responses/Validation'

  /integrations/{id}:
    get:
      tags: [Integrations]
      summary: Tek entegrasyon
      parameters: [{ in: path, name: id, required: true, schema: { type: integer } }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: '#/components/schemas/Integration' }
        "404": { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Integrations]
      summary: Ayarları güncelle
      description: Yalnızca verilen alanlar güncellenir (PATCH semantiği).
      parameters: [{ in: path, name: id, required: true, schema: { type: integer } }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                display_name: { type: string }
                auto_approve: { type: boolean }
                auto_pay: { type: boolean }
                status: { type: string, enum: [active, paused] }
                credentials:
                  type: object
                  additionalProperties: true
                  description: Tam set credentials (replace).
      responses:
        "200": { description: OK }
        "404": { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Integrations]
      summary: Entegrasyonu kaldır
      parameters: [{ in: path, name: id, required: true, schema: { type: integer } }]
      responses:
        "204": { description: Silindi }
        "404": { $ref: '#/components/responses/NotFound' }

  /mappings:
    get:
      tags: [Mappings]
      summary: Eşleştirmeleri listele
      parameters:
        - { in: query, name: integration_id, schema: { type: integer }, description: "Belirli bir entegrasyonun eşleştirmeleri için filtre" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Mapping' }
    post:
      tags: [Mappings]
      summary: Eşleştirme oluştur veya güncelle (upsert)
      description: |
        Aynı `(integration_id, external_sku)` çifti tekrar gönderilirse mevcut kayıt güncellenir.
        Webhook'ta gelen `line_item.sku` ile Gefli `user_design_variant_id` arasındaki köprü budur.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [integration_id, user_design_id, external_product_id, external_sku]
              properties:
                integration_id: { type: integer }
                user_design_id: { type: integer }
                user_design_variant_id:
                  type: integer
                  nullable: true
                  description: "Boş bırakılırsa design seviyesinde eşleşme (tek varyantlı ürün)."
                external_product_id: { type: string }
                external_variant_id: { type: string, nullable: true }
                external_sku: { type: string }
                external_url: { type: string, format: uri, nullable: true }
      responses:
        "201":
          description: Created/Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
        "422": { $ref: '#/components/responses/Validation' }

  /mappings/{id}:
    delete:
      tags: [Mappings]
      summary: Eşleştirmeyi sil
      parameters: [{ in: path, name: id, required: true, schema: { type: integer } }]
      responses:
        "204": { description: Silindi }
        "404": { $ref: '#/components/responses/NotFound' }

  /orders:
    get:
      tags: [Orders]
      summary: Harici siparişleri listele
      parameters:
        - { in: query, name: status, schema: { type: string, enum: [received, dropped, pending_approval, pending_funds, approved, rejected, failed] } }
        - { in: query, name: integration_id, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/ExternalOrder' }

  /orders/{id}:
    get:
      tags: [Orders]
      summary: Tek harici sipariş detayı (ham + parsed)
      parameters: [{ in: path, name: id, required: true, schema: { type: integer } }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: '#/components/schemas/ExternalOrderDetail' }
        "404": { $ref: '#/components/responses/NotFound' }

  /orders/{id}/approve:
    post:
      tags: [Orders]
      summary: Bekleyen siparişi onayla (Gefli order oluştur, cüzdandan düş)
      description: |
        Yalnızca `pending_approval` veya `pending_funds` durumundaki siparişler onaylanabilir.
        Bakiye yetmezse `402 insufficient_funds` döner.
      parameters: [{ in: path, name: id, required: true, schema: { type: integer } }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      gefli_order_id: { type: integer, example: 12345 }
        "402":
          description: Insufficient funds
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        "409":
          description: Onaylanamaz durum
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /orders/{id}/reject:
    post:
      tags: [Orders]
      summary: Bekleyen siparişi reddet
      parameters: [{ in: path, name: id, required: true, schema: { type: integer } }]
      responses:
        "200": { description: OK }

  /webhooks/{platform}:
    post:
      tags: [Webhooks]
      summary: Platform → Gefli olay endpoint'i
      description: |
        Platform tarafından çağrılır. Bearer token GEREKTİRMEZ; integration'ın
        `webhook_secret`'i ile HMAC imza doğrulaması yapılır.

        ### Signature header sözleşmesi
        | Platform | Header | Algoritma |
        |---|---|---|
        | shopify | `X-Shopify-Hmac-Sha256` | HMAC-SHA256(body, secret) base64 |
        | ikas / public | `X-Webhook-Signature` | HMAC-SHA256(body, secret) hex |
        | trendyol / hepsiburada | `X-Marketplace-Signature` | HMAC-SHA256(body, secret) hex |

        ### Generic payload (public integration)
        ```json
        {
          "external_order_id": "ORD-1024",
          "external_order_no": "1024",
          "customer_name": "Ahmet Y.",
          "shipping_address": {"city": "İstanbul", "line1": "..."},
          "tracking_number": "TY1234567890",
          "shipping_label_url": "https://...",
          "line_items": [
            {"sku": "P42-DTFB-S-SIY", "title": "Sonic Tee", "quantity": 2, "price": 250.0},
            {"sku": "UNKNOWN-SKU",   "title": "Başka", "quantity": 1}
          ]
        }
        ```

        ### Kombo sipariş kuralı
        Mapping'i bulunmayan kalemler `external_orders.dropped_items`'a kaydedilir,
        Gefli'ye sipariş olarak düşmez. Mapping olan kalemler `auto_approve`
        ayarına göre ya hemen onaylanır (cüzdandan düşülür) ya da `pending_approval`
        durumunda bekletilir.

      parameters:
        - { in: path, name: platform, required: true, schema: { type: string, enum: [shopify, ikas, trendyol, hepsiburada, public] } }
        - { in: query, name: integration_id, schema: { type: integer }, description: "Integration tespiti için (Faz 1+'da platform-spesifik parser bunu kendi çıkarır)" }
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: İşlendi veya idempotent duplicate
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    oneOf:
                      - { type: object, properties: { duplicate: { type: boolean }, ignored: { type: string }, dropped: { type: boolean }, pending_approval: { type: boolean }, gefli_order_id: { type: integer } } }
        "401":
          description: Invalid signature
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        "422":
          description: Parse hatası
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "gfk_live_*"

  schemas:

    User:
      type: object
      properties:
        id: { type: integer, example: 12 }
        name: { type: string, example: "Ahmet Yılmaz" }
        email: { type: string, format: email }

    Wallet:
      type: object
      properties:
        balance: { type: number, format: float, example: 1250.75 }

    TokenMeta:
      type: object
      properties:
        id: { type: integer }
        name: { type: string, example: "Shopify mağazam" }
        scopes:
          oneOf:
            - { type: string, example: "full" }
            - { type: array, items: { type: string } }

    MeResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          type: object
          properties:
            user:   { $ref: '#/components/schemas/User' }
            wallet: { $ref: '#/components/schemas/Wallet' }
            token:  { $ref: '#/components/schemas/TokenMeta' }

    ProductSummary:
      type: object
      properties:
        id: { type: integer }
        slug: { type: string }
        name: { type: string }
        status: { type: string, enum: [draft, completed] }
        preview_image: { type: string, nullable: true }
        product:
          type: object
          properties:
            id: { type: integer }
            name: { type: string }
            slug: { type: string }

    Variant:
      type: object
      properties:
        id: { type: integer, description: "user_design_variant_id" }
        sku: { type: string, example: "P42-DTFB-S-SIY" }
        price: { type: number, format: float }
        status: { type: string, enum: [active, inactive] }
        color: { type: string, nullable: true }
        color_code: { type: string, nullable: true }
        size: { type: string, nullable: true }
        print_method: { type: string, nullable: true }
        product_variant_id: { type: integer, nullable: true }

    ProductDetail:
      allOf:
        - $ref: '#/components/schemas/ProductSummary'
        - type: object
          properties:
            premium_discount: { type: number, format: float, description: "Gefli Plus üyelerine toplam fiyat üzerinden uygulanacak indirim yüzdesi" }
            variants:
              type: array
              items: { $ref: '#/components/schemas/Variant' }

    Integration:
      type: object
      properties:
        id: { type: integer }
        platform: { type: string, enum: [shopify, ikas, trendyol, hepsiburada, public] }
        display_name: { type: string }
        external_id: { type: string, nullable: true }
        auto_approve: { type: boolean }
        auto_pay: { type: boolean }
        status: { type: string, enum: [active, paused, disconnected, error] }
        last_sync_at: { type: string, format: date-time, nullable: true }
        last_error: { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    Mapping:
      type: object
      properties:
        id: { type: integer }
        integration_id: { type: integer }
        user_design_id: { type: integer }
        user_design_variant_id: { type: integer, nullable: true }
        external_product_id: { type: string }
        external_variant_id: { type: string, nullable: true }
        external_sku: { type: string }
        external_url: { type: string, nullable: true }
        sync_status: { type: string, enum: [pending, published, out_of_sync, error] }
        last_synced_at: { type: string, format: date-time, nullable: true }
        design_name: { type: string, nullable: true }
        design_slug: { type: string, nullable: true }

    ExternalOrder:
      type: object
      properties:
        id: { type: integer }
        integration_id: { type: integer }
        external_order_id: { type: string }
        external_order_no: { type: string, nullable: true }
        customer_name: { type: string, nullable: true }
        external_tracking_number: { type: string, nullable: true, description: "Pazaryeri kargo barkodu (Trendyol/Hepsi)" }
        external_shipping_label_url: { type: string, nullable: true }
        gefli_order_id: { type: integer, nullable: true, description: "Onaylandıysa oluşturulan Gefli order id" }
        status: { type: string, enum: [received, dropped, pending_approval, pending_funds, approved, rejected, failed] }
        received_at: { type: string, format: date-time }
        processed_at: { type: string, format: date-time, nullable: true }

    ExternalOrderDetail:
      allOf:
        - $ref: '#/components/schemas/ExternalOrder'
        - type: object
          properties:
            raw_payload:
              type: object
              description: "Webhook'tan gelen orijinal JSON (debug için)"
              additionalProperties: true
            matched_items:
              type: array
              description: "Mapping bulunan kalemler — Gefli order'a düştü"
              items: { type: object, additionalProperties: true }
            dropped_items:
              type: array
              description: "Mapping bulunamayan kalemler — Gefli'ye düşmedi (kombo sipariş kuralı)"
              items: { type: object, additionalProperties: true }
            shipping_address:
              type: object
              nullable: true
              additionalProperties: true

    Error:
      type: object
      properties:
        success: { type: boolean, example: false }
        error:
          type: object
          properties:
            code: { type: string, example: "validation_failed" }
            message: { type: string, example: "..." }
            details:
              type: object
              additionalProperties: true
              nullable: true

  responses:
    Unauthorized:
      description: Missing or invalid token
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Validation:
      description: Validation failed
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Too many requests
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
