# Teklif talepleri

Teklif (RFQ) akışı, alıcının doğrudan ödeme yapmak yerine sepeti fiyatlandırılmış bir teklif için satıcıya göndermesini sağlar. Yalnızca mağazada sales modülü etkin ve ayarlarda teklif talepleri açık olduğunda kullanılabilir; bunlardan biri eksik olan bir mağazada endpoint 404 döner. Bazı sepetler bu akıştan geçmek zorundadır: bir teklif tetikleyicisi devreye girdiğinde checkout `409 QUOTE_REQUIRED` ile reddeder ve sepetin `requires_quote` alanı true olur.

## Teklif durum akışı

| Durum | Anlamı |
| --- | --- |
| `submitted` | Bu endpoint tarafından oluşturuldu. Sepet dönüştürülür; satıcı talebi görür. |
| `offered` | Satıcı satırları fiyatlandırdı ve teklifi gönderdi. Toplamlar dondurulur ve 48 karakterlik bir kabul token'ı alıcıya e-postayla gönderilir (sunucu tarafında yalnızca SHA-256 hash'i saklanır). |
| `accepted` | Alıcı token üzerinden kabul etti. Teklif edilen fiyatlarla bir sipariş oluşturuldu; `converted_order_id` ayarlanır. Terminal. |
| `declined` | Alıcı token üzerinden reddetti. Satıcı yine de yeniden teklif verebilir veya iptal edebilir. |
| `expired` | `valid_until` tarihi geçmiş, teklif verilmiş bir teklif, bir sonraki token veya satıcı dokunuşunda tembel (lazy) olarak expired durumuna geçer. |
| `cancelled` | Satıcı tarafından iptal edildi (`submitted`, `offered`, `declined`, `expired` durumlarından mümkündür). Terminal. |

Kabul ve ret, token endpoint'leri üzerinden yapılır (Teklif token'ları sayfasına bakın).

## Teklif tetikleyicileri

Mağazada sales modülü etkin ve teklif talepleri açıkken, aşağıdaki koşullar bir sepeti RFQ akışına zorlar. Aksi halde hiçbir tetikleyici devreye girmez, `requires_quote` false kalır ve checkout hiçbir zaman `QUOTE_REQUIRED` ile reddetmez. Bu koşullar her sepet response'unda `requires_quote` ve `quote_reasons` olarak canlı şekilde görünür:

| Neden | Ne zaman tetiklenir |
| --- | --- |
| `cart_threshold` | Sepet toplamı yapılandırılmış eşik tutarına ulaştığında veya adet toplamı eşik adedine ulaştığında. |
| `configurator_choice` | Seçilen bir configurator seçenek tercihi teklif gerektirecek şekilde işaretlendiğinde. |
| `customer_group` | Müşterinin kişi grubu her zaman teklif olarak ayarlandığında (grup üzerinde bir sales ayarı). |
| `product_purchasability` | Sepetteki bir ürünün `purchasability` değeri `quote` olduğunda. Böyle bir ürün yine de sepete eklenebilir; sepet bu nedeni bildirir ve checkout yerine bu endpoint'ten geçer. |

Hiçbir tetikleyiciyi devreye sokmayan sepetler için de teklif talebi gönderilebilir; alıcı her zaman bir teklif isteyebilir.

## Müşteriye dönük teklif yapısı

Teklif talebi endpoint'i ve iki token okuma endpoint'i aynı zarfı döner: `{"data": {...quote...}}`. Bu, müşteriye dönük serileştirmedir: `merchant_note`, `cart_id`, `customer_id` ve `contact_id` asla bulunmaz; kabul token'ı (veya hash'i) asla açığa çıkarılmaz. Teklif nesnesi ayrıca `origin` (burada oluşturulan bir teklif için `"storefront"`), `intro`, `terms`, `signed_at`, `acceptance_mode`, `pdf_url` ve `signed_pdf_url` alanlarını taşır; bunlar [Teklif token'ları](/tr/api/orders-and-quotes/quote-tokens) sayfasında belgelenmiştir. Sepet ve sipariş response'larının aksine, tüm teklif tutarları money nesnesi değil, minör birim cinsinden düz tamsayılardır. Tarihler ISO-8601 dizeleridir.

| Ad | Tür | Zorunlu | Açıklama |
| --- | --- | --- | --- |
| `id` | string (UUID) | her zaman | Teklif id'si. |
| `number` | string | her zaman | Kullanıcıya gösterilen teklif numarası; `"Q-"` artı mağaza başına bir tamsayı, örneğin `"Q-17"`. |
| `status` | string | her zaman | Yukarıdaki durum tablosuna bakın. |
| `email` | string | her zaman | Talep sahibinin e-postası. |
| `name` | string | her zaman | Talep sahibinin adı. |
| `phone` | string or null | her zaman | Talep sahibinin telefonu. |
| `billing_address` | object or null | her zaman | Gönderildiği haliyle. |
| `shipping_address` | object or null | her zaman | Gönderildiği haliyle. |
| `currency` | string | her zaman | `EUR`. |
| `customer_note` | string or null | her zaman | Alıcının talepteki notu. |
| `message_to_customer` | string or null | her zaman | Satıcının mesajı; teklif verilirken ayarlanır. |
| `valid_until` | string (ISO-8601) or null | her zaman | Teklifin geçerlilik sonu; teklif verilirken ayarlanır. |
| `subtotal_amount` | integer or null | her zaman | Teklif anında dondurulur; `submitted` durumundayken null. Minör birim. |
| `tax_total_amount` | integer or null | her zaman | Teklif anında dondurulur; `submitted` durumundayken null. |
| `shipping_total_amount` | integer or null | her zaman | Teklif anında dondurulur; `submitted` durumundayken null. |
| `total_amount` | integer or null | her zaman | Teklif anında dondurulur; `submitted` durumundayken null. |
| `tax_breakdown` | array or null | her zaman | Oran bazında döküm `{"rate": float, "base": int, "tax": int}`; `submitted` durumundayken null. |
| `converted_order_id` | string (UUID) or null | her zaman | Kabul üzerine oluşturulan sipariş, aksi halde null. |
| `submitted_at` | string (ISO-8601) or null | her zaman | Talebin gönderildiği zaman. |
| `offered_at` | string (ISO-8601) or null | her zaman | Satıcının teklifi gönderdiği zaman. |
| `accepted_at` | string (ISO-8601) or null | her zaman | Alıcının kabul ettiği zaman. |
| `declined_at` | string (ISO-8601) or null | her zaman | Alıcının reddettiği zaman. |
| `cancelled_at` | string (ISO-8601) or null | her zaman | Satıcının iptal ettiği zaman. |
| `created_at` | string (ISO-8601) or null | her zaman | Satırın oluşturulma zamanı. |
| `updated_at` | string (ISO-8601) or null | her zaman | Son güncelleme zamanı. |
| `lines` | array | her zaman | Teklif satırları, aşağıya bakın. |

### `lines[]`

| Ad | Tür | Zorunlu | Açıklama |
| --- | --- | --- | --- |
| `id` | string (UUID) | her zaman | Teklif satırı id'si. |
| `product_id` | string (UUID) or null | her zaman | Ürün; satıcının eklediği özel satırlarda null. |
| `variant_id` | string (UUID) or null | her zaman | Varsa varyant. |
| `is_custom` | boolean | her zaman | Satıcının eklediği özel satırlarda true. |
| `title` | string | her zaman | Satır başlığı (ürün adı snapshot'ı veya özel satırlarda serbest metin). |
| `sku` | string or null | her zaman | SKU snapshot'ı. |
| `quantity` | number | her zaman | Sepet satırından kopyalanan miktar: `piece` olarak satılan bir ürün için tam sayı, diğer tüm birimler için en fazla üç ondalık basamak (örneğin `0.75`). Satıcının hizmet ve malzeme satırları da ondalıklı olabilir. |
| `indicative_unit_price` | integer or null | her zaman | Gönderim anında talep sahibinin fiyatlandırma bağlamından snapshot'lanan birim fiyat (minör birim). |
| `quoted_unit_price` | integer or null | her zaman | Satıcının teklif ettiği birim fiyat; fiyatlandırılana kadar null. |
| `tax_class_id` | string (UUID) or null | her zaman | Varsa satıcının atadığı vergi sınıfı. |
| `attributes` | object or null | her zaman | Yapılandırılabilir satırlarda yapılandırma snapshot'ı, aksi halde null. |
| `sort_order` | integer | her zaman | Görüntüleme sırası. |
| `kind` | string | her zaman | Sepetten oluşturulan satırlar için `product`; satıcı satırları `service`, `material` veya `other` olabilir. |
| `unit` | string or null | her zaman | Birim etiketi. Sepetten oluşturulan bir satır, `piece` olarak satılan ürün için `pcs`, aksi halde ürünün birimini (`kg`, `m2`, `hour`, ...) taşır. |
| `description` | string or null | her zaman | Satır düzeyinde isteğe bağlı not. |
| `tax_rate` | string or null | her zaman | Satıcı teklifi gönderdiğinde kaydedilen ondalık vergi oranı (örneğin `"21.00"`); o zamana kadar null. |
| `unit_price` | integer or null | her zaman | `quoted_unit_price` alanının takma adı. |

## POST /v1/api/commerce/carts/{id}/quote-request

Sepeti bir teklif talebi olarak gönderir. `commerce:write` scope'unu ve `active`, boş olmayan bir sepet gerektirir; sepet sahipliği denetiminden geçer. Başarı durumunda sepet `converted` olur, her sepet satırı talep sahibinin fiyatlandırma bağlamından gelen gösterge niteliğinde bir birim fiyatla snapshot'lanarak `submitted` durumunda bir teklif oluşturulur, talep sahibine alındı onayı e-postası kuyruğa alınır ve müşteriye dönük teklif 201 durum koduyla döner.

İletişim bilgisi çözümlemesi: `email` ve `name` toplamda zorunludur, ancak her biri ya request body'den ya da geçerli bir `X-Customer-Token` gönderildiğinde müşterinin profilinden gelebilir. Geçerli bir token teklifi müşteriye de bağlar.

### Parametreler

**Path**

| Ad | Tür | Zorunlu | Açıklama |
| --- | --- | --- | --- |
| `id` | string (UUID) | evet | Sepet id'si. `active` ve boş olmayan bir sepet olmalıdır. |

**Body**

Tüm body alanları isteğe bağlı ve nullable'dır; müşteri profili sağlayamadığında `email` ve `name` zorunlu hale gelir.

| Ad | Tür | Zorunlu | Açıklama |
| --- | --- | --- | --- |
| `email` | string | koşullu | Geçerli e-posta. Token'lı müşterinin profil e-postasına geri düşer. |
| `name` | string | koşullu | Talep sahibinin adı. Müşteri profilindeki ada geri düşer. |
| `phone` | string | hayır | İletişim telefonu. |
| `billing_address` | object | hayır | Serbest biçimli adres nesnesi. |
| `shipping_address` | object | hayır | Serbest biçimli adres nesnesi. |
| `customer_note` | string | hayır | Satıcıya mesaj. |

**Headers**

| Ad | Tür | Zorunlu | Açıklama |
| --- | --- | --- | --- |
| `X-Customer-Token` | string | hayır | Varsa çözümlenmesi gerekir, aksi halde 401. Teklifi müşteriye bağlar, iletişim bilgisi geri düşüşlerini sağlar ve gösterge fiyatlar için fiyatlandırma bağlamını seçer. |

```bash title="Request"
curl -X POST https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/quote-request \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "buyer@corp.example",
    "name": "Bob Buyer",
    "phone": "+31 6 12345678",
    "customer_note": "Need delivery before October.",
    "billing_address": {
      "name": "Corp BV",
      "street": "Main St 1",
      "postal_code": "1000 AA",
      "city": "Amsterdam",
      "country": "NL"
    }
  }'
```

```javascript title="Request (JS)"
const res = await fetch(
  `https://api.borkol.com/v1/api/commerce/carts/${cartId}/quote-request`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      email: "buyer@corp.example",
      name: "Bob Buyer",
      customer_note: "Need delivery before October.",
    }),
  }
);
const { data: quote } = await res.json();
```

```json title="Response"
{
  "data": {
    "id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
    "number": "Q-17",
    "status": "submitted",
    "email": "buyer@corp.example",
    "name": "Bob Buyer",
    "phone": "+31 6 12345678",
    "billing_address": {
      "name": "Corp BV",
      "street": "Main St 1",
      "postal_code": "1000 AA",
      "city": "Amsterdam",
      "country": "NL"
    },
    "shipping_address": null,
    "currency": "EUR",
    "customer_note": "Need delivery before October.",
    "message_to_customer": null,
    "valid_until": null,
    "subtotal_amount": null,
    "tax_total_amount": null,
    "shipping_total_amount": null,
    "total_amount": null,
    "tax_breakdown": null,
    "converted_order_id": null,
    "submitted_at": "2026-08-14T10:15:30+00:00",
    "offered_at": null,
    "accepted_at": null,
    "declined_at": null,
    "cancelled_at": null,
    "created_at": "2026-08-14T10:15:30+00:00",
    "updated_at": "2026-08-14T10:15:30+00:00",
    "lines": [
      {
        "id": "9b0c1d2e-3f4a-4b5c-d6e7-f8a9b0c1d2e3",
        "product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "variant_id": null,
        "is_custom": false,
        "title": "Garden Chair",
        "sku": "CHAIR-GRN",
        "quantity": 25,
        "indicative_unit_price": 2250,
        "quoted_unit_price": null,
        "tax_class_id": null,
        "attributes": null,
        "sort_order": 0,
        "kind": "product",
        "unit": "pcs",
        "description": null,
        "tax_rate": null,
        "unit_price": null
      }
    ],
    "origin": "storefront",
    "intro": null,
    "terms": null,
    "signed_at": null,
    "acceptance_mode": "click",
    "pdf_url": null,
    "signed_pdf_url": null
  }
}
```

Gönderimden sonra teklifi id ile sorgulayacak bir storefront endpoint'i yoktur: alıcı teklifi e-postayla alır ve oradan itibaren token endpoint'leri devreye girer.

### Hatalar

| Durum | Gövde | Ne zaman |
| --- | --- | --- |
| 404 | `{"error": "Module not enabled: sales"}` | Mağazada sales modülü etkin değil. `error` anahtarına ve 404 durumuna dikkat edin (diğer modül kapıları 403 döner). |
| 404 | `{"message": "..."}` | Mağaza için teklif talepleri kapalı, ya da sepet bilinmiyor, süresi dolmuş veya `active` değil. |
| 401 | `{"message": "Invalid customer token."}` | `X-Customer-Token` mevcut ancak çözümlenemiyor. |
| 422 | `{"message": "...", "errors": {"email": ["Email is required."], "name": ["Name is required."]}}` | Ne body ne de müşteri profili email veya name sağlıyor. |
| 422 | `{"message": "...", "errors": {"cart": ["Cart is empty."]}}` | Sepette satır yok. |
| 403 | sahiplik, scope, commerce modülü veya askıya alma gövdesi | Sepetler sayfasına bakın. |
| 429 | `{"message": "Too Many Attempts."}` | Rate limit aşıldı. |
