Skip to main content

Posyou ↔ Baktin Entegrasyon Spesifikasyonu (Posyou uygular)

Model: Baktin platform/talep ağıdır (Getir/Trendyol Yemek konumu); Posyou entegratördür (POS). Kanonik sözleşmeyi Baktin belirler — Posyou bu dokümana göre uyum sağlar. Yeni alan/uç eklenmesi gerekirse Baktin sürümler; Posyou geriye-uyumlu kalır.

Bu doküman "as-built"tır: aşağıdaki yollar/şekiller/imza Baktin kodunda uygulanmış haldedir (src/integrations/adapters/posyou.adapter.ts, inbound-webhook.service.ts, external-api/).


0. Üç entegrasyon yüzeyi (özet)

#YönKim çağırırNe için
AFG → Posyou (REST)BaktinSipariş gönder, katalog çek, katalog gönder, mağaza durumu
BPosyou → FG (REST)PosyouSipariş çek (pull), durum sür, ürün uygunluk/fiyat, mağaza durum/saat
CPosyou → FG (webhook)PosyouOlay-bazlı bildirim (durum/iade/mağaza-aç-kapa/katalog)

A ve C, FG→Posyou ve Posyou→FG yönlerinin gerçek-zamanlı halidir; B ise Posyou'nun pull/reconcile yüzeyidir. Tam çift-yön tutarlılık için A + C önerilir (B opsiyonel reconcile/yedek).


1. Kimlik & güvenlik

İki ayrı sır kullanılır (onboarding'de Baktin panelinden işletme başına üretilir):

SırNeredeNe için
api_keyHTTP header X-Api-Key (A yönü) / X-API-Key (B yönü)REST kimlik doğrulama
webhook_secretHMAC imzası (A ve C yönü)Gövde bütünlüğü/orijin doğrulama

HMAC imza şeması (her iki yön — A ve C):

signature = "sha256=" + HMAC_SHA256(webhook_secret, "{timestamp}.{rawBody}")
  • timestamp = Unix milisaniye epoch (string).
  • rawBody = HTTP gövdesinin birebir ham hali (re-serialize ETME — byte-byte aynı string imzalanmalı).
  • İmza timing-safe karşılaştırılır.

Zorunlu header'lar:

HeaderAçıklama
X-Baktin-Timestampİmzadaki timestamp (ms). Replay penceresi ±5 dakika — dışı reddedilir (401).
X-Baktin-SignatureYukarıdaki sha256=... imza.
X-Baktin-EventOlay türü (order.created, order.cancelled, catalog.product.upserted …).
X-Baktin-Delivery-Idİdempotency anahtarı (en-az-bir-kez teslim → aynı id'yi iki kez işleme).
X-Store-Id(varsa) Posyou tarafı mağaza kimliği (external_store_id).
X-Api-Key(A yönü REST) api anahtarı.

Fail-closed: İmza/secret/zaman geçersizse Baktin 401 döner ve işlemez. Posyou da kendi tarafında aynı sıkılıkta doğrulamalıdır.

HMAC imza üretimi — kod örneği:

// Node.js
const crypto = require('crypto');
function signWebhook(webhookSecret, rawBody) {
const ts = Date.now().toString(); // ms epoch (string)
const sig = 'sha256=' + crypto.createHmac('sha256', webhookSecret)
.update(`${ts}.${rawBody}`).digest('hex'); // imza = ts + "." + ham gövde
return { 'X-Baktin-Timestamp': ts, 'X-Baktin-Signature': sig };
}
// rawBody = JSON.stringify(payload) — AYNI string'i hem imzala hem gönder (re-serialize etme).
# Python
import time, hmac, hashlib
def sign_webhook(webhook_secret: str, raw_body: str):
ts = str(int(time.time() * 1000)) # ms epoch
sig = 'sha256=' + hmac.new(webhook_secret.encode(), f'{ts}.{raw_body}'.encode(), hashlib.sha256).hexdigest()
return {'X-Baktin-Timestamp': ts, 'X-Baktin-Signature': sig}
# raw_body = json.dumps(payload) — aynı string'i imzala ve gönder.

1.1 Hata kodları (HTTP)

Tüm uçlar standart HTTP durum kodları döner. Hata gövdesi: { "statusCode": <n>, "message": "<açıklama>", "error": "<tip>" }.

KodAnlamTipik neden
200 / 201Başarılı
400 Bad RequestGeçersiz girdiEksik/yanlış param, geçersiz since ISO, geçersiz status, gövde tipi hatası
401 UnauthorizedKimlik başarısızX-API-Key yok/geçersiz; webhook HMAC/timestamp geçersiz (replay penceresi dışı)
403 ForbiddenYetki yokKapsamsız anahtarla /integration/*; izin (read_orders/write_orders) yetmiyor; §3.4 platform-kurye sahiplik ihlali
404 Not FoundBulunamadıSipariş/ürün bu işletmede yok (cross-tenant erişim de 404 — varlık sızdırmaz)
409 ConflictDurum çakışmasıGeçersiz state geçişi (ör. deliveredpreparing)
429 Too Many RequestsRate-limitÇok sık istek — Retry-After header'ına uyun, exponential backoff
5xxSunucu hatasıGeçici — idempotent retry (webhook'ta non-2xx → FG yeniden gönderir)

Retry rehberi: 429/5xx → exponential backoff + jitter ile tekrar dene. 4xx (429 hariç) → kalıcı, tekrar deneme (girdiyi düzelt).


1.2 Sandbox (test-mode) & API versiyonlama

API versiyonu: Mevcut kontrat v1'dir. Her /integration/* yanıtı X-API-Version: 1 header'ı döner. İleride geriye-uyumsuz (breaking) bir değişiklik olursa yeni sürüm olarak yayımlanır + changelog'da duyurulur; mevcut v1 uçları stabildir. (URL'de versiyon yok — header ile gösterilir; entegratör bu header'ı loglamalı.)

Sandbox / test anahtarı: FG panelinden test-mode API anahtarı üretilebilir (POST .../api-keys gövdesine { "test_mode": true }). Test anahtarıyla:

  • OKUMA uçları (orders/products/categories/store/...) normal çalışır → gerçek şekli görürsünüz.
  • YAZMA uçları (status/reject/availability/price/store) doğrulanır ama UYGULANMAZ — yanıt { "test_mode": true, "simulated": true, "applied": false, ... }. Gerçek veri/dispatch değişmez.

Böylece entegratör, canlı veriyi etkilemeden yazma kod-yolunu + auth + routing'i güvenle test eder. Canlıya geçişte test_mode=false anahtar kullanın.

İzin kapsamı (least-privilege): Anahtar üretirken { "scope": "read" | "write" | "full" } (vars. full). İzin string'leri:

  • integration:read_orders → çekme (orders/products/categories/store-status/hours/courier).
  • integration:write_orders → sipariş/operasyon push (status/reject/availability/price/store/hours).
  • integration:write_catalogkatalog CRUD (ürün/kategori oluştur/güncelle/sil).
  • integration:read_financefinans okuma (hakediş ekstresi + payout listele).
  • integration:write_payoutpayout talep (hakediş ödeme talebi — hassas).
  • integration:write_profileprofil yazma (işletme bilgisi/konum/logo güncelle).
  • integration:write_reservationsrezervasyon yazma (oluştur/durum/iptal/ayar).
  • integration:read_analyticsanalitik okuma (yorum/rating + müşteri + dashboard stats).
  • integration:write_marketingpazarlama yazma (kampanya oluştur/güncelle/durum/sil).
  • integration:manage_credentialskredensiyal yönetimi (webhook-secret yenile + API-anahtar listele/üret/iptal) — YALNIZ full scope (privilege-escalation/lockout koruması).

Scope eşlemesi: read = [read_orders, read_finance, read_analytics]; write = [write_orders, write_catalog, write_payout, write_profile, write_reservations, write_marketing]; full = hepsi (10 izin, manage_credentials dahil). Onboarding'in döndürdüğü scoped anahtar full'dur (tüm izinler dahil). Yetki yetmeyen uçta 403. Salt-okur / yalnız-sipariş senaryolarında en-az-yetki kullanın. Kredensiyal uçları yalnız full anahtarla çağrılır — read/write anahtar 403 alır.

Not: ESKİ anahtarlar (yalnız read_orders/write_orders) katalog-CRUD için 403 alır → yeni full/write anahtar üretin.

Kanonik örnek şekiller: GET /integration/sandbox/sample (geçerli anahtar yeter) → örnek sipariş + ürün şekli döner; gerçek sipariş/ürün gerekmeden entegrasyonu kodlamaya başlayabilirsiniz.


2. BÖLÜM A — Posyou'nun FG'ye açacağı REST uçları (FG → Posyou)

Base URL: endpoint_url (Posyou onboarding'de verir). Auth: X-Api-Key: {api_key} (tüm A uçları). Baktin yalnız INTEGRATION_LIVE_DISPATCH açıkken çağırır; aksi halde olaylar FG outbox'ta beklenir.

A.1 GET /store/status?storeId={external_store_id}

Bağlantı/sağlık testi. 200 (2xx) = sağlıklı. Gövde serbest.

A.1b POST /store/status — FG→POS mağaza aç/kapa (FG yazar)

İşletme FG panelinden manuel aç/kapa yaptığında FG bu uca POST eder (tek-yön desync kapatıldı). HMAC imzalı (pushOrder ile aynı: X-Baktin-Timestamp/X-Baktin-Signature/X-Baktin-Event: store.open|store.close/ X-Baktin-Delivery-Id/X-Store-Id). Gövde: { "is_open": bool, "merchant_id": "...", "at": "ISO" }. 2xx beklenir; non-2xx → FG retry/dead-letter. (Generic-webhook entegratörlerinde aynı olay tek webhook URL'ine _event: store.* ile gider.)

A.2 Katalog çekme (pull) — GET /catalog/categories + GET /catalog/products

Query: ?storeId={external_store_id} (varsa). Baktin ikisini paralel çeker.

Kategori dizisi (FG esnek normalize eder; tercih edilen alan adları kalın):

[ { "id": "CAT1", "name": "Pizzalar", "display_order": 1 } ]
  • id (≈ _id/external_id/code) — Posyou kategori kimliği (string).
  • name — string veya { "tr": "...", "en": "..." }.
  • display_order (≈ order) — opsiyonel.

Ürün dizisi:

[ {
"id": "PRD1", "name": "Margherita", "description": "...",
"price": 120.0, "discounted_price": 99.0,
"category_id": "CAT1", "is_available": true, "stock": 25,
"images": ["https://..."]
} ]
  • id (≈ _id/external_id/sku), name (string/çok-dilli).
  • price (≈ sellingPrice/normal_price), discounted_price (opsiyonel).
  • category_id (≈ categoryId) — A.2 kategori id'sine referans.
  • is_available (≈ available, veya status:'active'), stock (≈ remaining_quantity).
  • images (≈ tek image).
  • Sarmalama serbest: düz dizi veya { "data":[...] } / { "items":[...] } / { "results":[...] }.

A.3 Sipariş alma — POST /orders

Baktin yeni/güncel siparişi buraya POST eder. HMAC header'lı (Bölüm 1). Gövde = kasa-fişi (Bölüm E). X-Baktin-Event ile ayırt et: order.created | order.updated | order.cancelled.

Yanıt (FG eşleme için kullanır):

{ "id": "POS_ORDER_123" } // Posyou'nun kendi sipariş kimliği (≈ "orderId")

FG bu id'yi saklar; sonraki order.updated/order.cancelled gövdesine external_order_id olarak ekler → Posyou kendi kaydını eşleyebilir. İdempotency: aynı X-Baktin-Delivery-Id'yi iki kez işleme.

A.4 Katalog gönderme — POST /catalog/products

Baktin ürünü Posyou'ya iter (FG menüsü değişince + sipariş/stok değişince). HMAC header'lı. Gövde = Bölüm F ürün şekli. payload.external_id varsa güncelle, yoksa oluştur (upsert). Yanıt: { "id": "POS_PRD_1" } (≈ external_id/sku) — FG eşlemeye yazar (kopya ürün oluşmaz).


3. BÖLÜM B — FG'nin açtığı uçlar (Posyou → FG; pull/push REST)

Base URL: https://api.baktin.com (onboarding'de teyit). Auth: X-API-Key: {fg_api_key} (FG panelinden üretilir, DAİMA işletme-kapsamlı — kapsamsız anahtar 403). Rate-limit'li.

Açıklama
GET /integration/orders?since={ISO}&since_id={id}&limit={1..200}Sipariş çek (keyset cursor). Yanıt: {orders, count, next_since, next_since_id, has_more}. Bir sonraki poll iki cursor'u da geri gönder.
GET /integration/orders/{orderId}Tek sipariş reconcile (kasa-fişi şekli).
POST /integration/orders/{orderId}/statusDurum sür. Gövde { "action": "confirm|preparing|ready|on_the_way|delivered|cancel" }.
POST /integration/orders/{orderId}/rejectYeni siparişi reddet. Gövde { "reason_code": "..." } (zorunlu).
POST /integration/orders/{orderId}/confirm-paymentNakit ödeme onayı (POS kasa). Gövde { payment_method?, calculated_amount? } (dinamik siparişte tutar).
POST /integration/orders/{orderId}/confirm-split-paymentBölüştürülen ödeme (bir katılımcı öder). Gövde { user_id, payment_method? }.
PUT /integration/orders/{orderId}/payment-amountÖdeme tutarını güncelle (dinamik sipariş). Gövde { amount, calculation_details? }.
GET / POST /integration/store/statusMağaza aç/kapa. POST gövde { "is_open": true|false }.
GET /integration/store/profileİşletme profilini getir (ad/açıklama/iletişim/konum/logo/banner/min-sepet).
PUT /integration/store/profileProfil GÜNCELLE (write_profile). Whitelist: { name?, description?, phone?, email?, website?, address?, city?, district?, logo?, banner_image?, latitude?, longitude?, minimum_order_amount? }. logo/banner URL.
GET / POST /integration/store/hoursÇalışma saatleri.
GET /integration/products?since={ISO}&since_id={id}&status={active|inactive|out_of_stock}&limit={1..200}Ürün/katalog ÇEK (keyset cursor — POS başlangıç-senkron + artımlı). Yanıt: {products, count, next_since, next_since_id, has_more}. Ürün alanları: id, name, description, price, discounted_price, status, available, category_id, images[], ingredients[], removable_ingredients[], preparation_time, calories, updated_at.
GET /integration/categoriesMenü kategorileri ÇEK (ürün category_id ile eşleşir). Yanıt: {categories:[{id,name,status,display_order}], count}.
GET /integration/products/{productId}Tek ürün getir (reconcile).
POST /integration/productsÜrün OLUŞTUR (write_catalog). Gövde: { name, price, description?, discounted_price?, category_id?|external_category_id?, status?, images?, ingredients?, external_id? }. external_id ile idempotent (varsa günceller).
PUT /integration/products/{productId}Ürün GÜNCELLE (write_catalog). Verilen alanlar set edilir.
DELETE /integration/products/{productId}Ürün SİL (write_catalog). external_id eşlemesi de temizlenir.
POST /integration/categoriesKategori OLUŞTUR (write_catalog). Gövde: { name, display_order?, external_id? }.
PUT /integration/categories/{categoryId}Kategori GÜNCELLE (write_catalog). { name?, status?, display_order? }.
DELETE /integration/categories/{categoryId}Kategori SİL (write_catalog). Kategorideki ürünler kategorisiz kalır (silinmez).
GET /integration/ingredient-groupsMalzeme grupları listele (toppings/modifiers + seçenekler).
POST /integration/ingredient-groupsMalzeme grubu OLUŞTUR (write_catalog). Gövde: { name, selection_type?(radio|checkbox), is_required?, min_selections?, max_selections?, display_order?, items:[{name, extra_price, is_active?, display_order?, description?, is_default?}] }. Tüm per-malzeme alanları korunur.
PUT/DELETE /integration/ingredient-groups/{groupId}Güncelle/Sil (write_catalog).
POST /integration/products/{productId}/availabilityÜrün uygunluk (86'lama). Gövde { "is_available": bool }.
POST /integration/products/{productId}/priceÜrün fiyat. Gövde { "price": number, "discounted_price"?: number }. discounted_price opsiyonel (0..price; indirimli fiyatı da senkronlar).
POST /integration/products/availabilityTOPLU uygunluk (rate-limit baskısını azaltır). Gövde { "updates": [{ "product_id", "is_available" }] } (max 200). Yanıt: {requested, matched, modified, not_found, invalid, invalid_items} (kısmi-başarı).
POST /integration/products/priceTOPLU fiyat. Gövde { "updates": [{ "product_id", "price", "discounted_price"? }] } (max 200). discounted_price opsiyonel (0..price). Kısmi-başarı raporu.
GET /integration/orders/{orderId}/courierKurye bilgisi (FG platform-teslimat).
GET /integration/finance/statementHakediş ekstresi (read_finance): bakiye + bekleyen-payout + kullanılabilir + son ledger hareketleri.
GET /integration/finance/payoutsPayout talepleri listele (read_finance).
POST /integration/finance/payoutsPayout TALEP et (write_payout, hassas). Gövde { amount, iban? }; kullanılabilir bakiye yeterli olmalı.
GET /integration/ratings + /ratings/statsDeğerlendirme/yorum listele + özet (read_analytics).
GET /integration/customersMüşteri listesi (sipariş-sayısı/harcama/son-sipariş, read_analytics).
GET /integration/statsDashboard istatistikleri (toplam/durum sipariş + ciro + bugün/7-gün, read_analytics).
GET /integration/reservationsRezervasyon listele (read_orders). ?status=&date=YYYY-MM-DD&page=&limit=.
GET /integration/reservations/statsRezervasyon istatistik (read_orders). ?startDate=&endDate=.
GET /integration/reservations/settings + PUT …/settingsAyar oku (read_orders) / güncelle (write_reservations): müsaitlik, kişi-limiti, açılış/kapanış, ön-rezervasyon-günü.
GET /integration/reservations/slots?date=Müsait saat aralıkları (read_orders, date zorunlu).
GET /integration/reservations/{id}Rezervasyon detayı (read_orders).
POST /integration/reservationsRezervasyon OLUŞTUR (write_reservations). Gövde: { date, time, people_count(1-20), notes?, user_id? | customer_phone? }. user_id ya da KAYITLI FG kullanıcısına çözülen customer_phone zorunlu (guest desteklenmez).
PUT /integration/reservations/{id}/statusDurum güncelle (write_reservations). Gövde: { status, rejection_reason? }.
PUT /integration/reservations/{id}/cancelRezervasyon iptal (write_reservations).
GET /integration/credentialsEntegrasyon durumu (manage_credentials): bağlantı/health/has_webhook_secret. Secret DÖNMEZ.
POST /integration/credentials/webhook-secret/rotateWebhook secret YENİLE (manage_credentials). FG→POS imza anahtarı; düz metin YALNIZ bir kez döner — saklayın.
GET /integration/credentials/api-keysAPI anahtarları listele (manage_credentials, maskeli — secret yok).
POST /integration/credentials/api-keysAPI anahtarı ÜRET (manage_credentials). Gövde: { name?, test_mode?, scope?(read|write|full) }. Düz metin YALNIZ bir kez döner; kapsam = bu işletme.
DELETE /integration/credentials/api-keys/{keyId}API anahtarı İPTAL (manage_credentials, fail-closed: yalnız kendi işletmesinin anahtarı).
GET /integration/campaignsKampanya listele (read_analytics). ?status=&type=&featured=.
GET /integration/campaigns/statsKampanya istatistik (read_analytics).
GET /integration/campaigns/{id}Kampanya detayı (read_analytics).
POST /integration/campaignsKampanya OLUŞTUR (write_marketing). status=pending → platform onayına düşer. Gövde: { title*, description?, discount?, campaign_type?(percentage_discount|price_discount|buy_x_pay_y|free_product_offer), start_date?, end_date?, ... }.
PUT /integration/campaigns/{id}Kampanya GÜNCELLE (write_marketing).
PUT /integration/campaigns/{id}/statusDurum güncelle (write_marketing). { status: pending|active|inactive|deleted }.
DELETE /integration/campaigns/{id}Kampanya SİL (write_marketing, soft → status=deleted).
GET /integration/reports/salesSatış raporu (read_analytics). ?startDate=&endDate=&groupBy=day|week|month. Dönem-bazlı: orders, gross_revenue (iptal dahil brüt), net_revenue (teslim/tamamlanan = gerçekleşen satış), delivered_orders, cancelled_orders + toplamlar (TRY, Europe/Istanbul). Net satış için net_revenue kullanın.

Pazarlama kapsamı: Yalnız işletme-kapsamlı kampanyalar entegrasyona açıktır. Kupon / sponsorlu reklam / platform duyuruları platform-seviyesi pazarlama olduğundan (tenant-scope yok → IDOR) tenant-API'de bilerek açılmamıştır; bunlar FG panelinden/admin'den yönetilir.

§3.4 sahiplik kuralı: delivery_provider="platform" (FG kuryesi) siparişinde on_the_way/delivered Posyou'dan reddedilir (terminal teslimatı FG kuryesi sahiplenir). delivery_provider="merchant" ise serbest.


4. BÖLÜM C — Posyou'nun FG'ye gönderdiği webhook (Posyou → FG; ÖNERİLEN)

URL: POST https://api.baktin.com/integrations/webhooks/posyou/{merchantId} merchantId = FG işletme kimliği (onboarding'de verilir). HMAC zorunlu (Bölüm 1; webhook_secret). İdempotency: gövdede event_id gönder (yoksa FG türetir). Gövde her zaman { "event_type": "...", ... }.

event_typeZorunlu alanlarFG tepkisi
order.confirmed / .preparing / .ready / .delivered / .completed / .cancelled (veya tek event_type:"order.updated" + status)order_id veya external_order_id; status; reason_code?Sipariş durum geçişi (state-machine; ileri-atlamada ara geçişler sırayla uygulanır).
refund.created (veya order.refunded)sipariş referansı; refund_amount_minor veya refund_amount (yoksa tam iade)Settlement ters-kaydı (komisyon/hakediş geri al). Idempotent; settle-edilmemişse no-op; sipariş durumunu değiştirmez.
store.open / store.close (veya store.status + is_open)(status için) is_open: boolİşletme aç/kapat → FG yeni sipariş almaz (kapalıyken checkout reddeder).
catalog.product.upserted / catalog.* / stock.* / inventory.*ürün alanları (Bölüm F)FG kataloğuna upsert / stok güncelle (external_id↔FG internal_id eşleme ile dedup).
diğer (payment.*, courier.*)Kaydedilir, şimdilik işlenmez (IGNORED).

Sipariş referansı: order_id (FG kimliği — order.created gövdesinde aldığın order_id) veya external_order_id (senin POS kimliğin — A.3 yanıtında id olarak döndüğün). FG ikisini de çözer; map yalnız ilgili işletmeye scope'ludur (başka işletmenin siparişine erişilemez).

Örnek (durum):

POST /integrations/webhooks/posyou/665f...e90
X-Baktin-Timestamp: 1782580000000
X-Baktin-Signature: sha256=ab12...
Content-Type: application/json

{ "event_type": "order.preparing", "external_order_id": "POS_ORDER_123",
"status": "preparing", "event_id": "posyou-evt-987" }

Örnek (iade):

{ "event_type": "refund.created", "order_id": "665f...aa1",
"refund_amount": 45.50, "event_id": "posyou-refund-5" }

5. BÖLÜM D — Durum & para eşlemeleri

POS durumu → FG durumu (büyük/küçük harf duyarsız):

Posyou statusFG OrderStatus
confirmed / acceptedconfirmed
preparingpreparing
readyready
on_the_way / shipped / out_for_deliveryon_the_way
delivereddelivered
completedcompleted
cancelled / canceledcancelled

Para birimi: TRY. Tutarlar hem major (TL, ondalıklı) hem *_minor (kuruş, tam sayı) gönderilir/beklenir. İade refund_amount_minor (kuruş) tercih edilir; refund_amount (TL) verilirse FG ×100 yapar.


6. BÖLÜM E — Sipariş kasa-fişi payload (A.3 POST /orders gövdesi)

Baktin buildOrderReceipt çıktısı (PUSH webhook + PULL reconcile aynı şekil). Başlıca alanlar:

{
"order_id": "665f...aa1", "order_number": "FG-250627-0042",
"status": "confirmed", "type": "delivery", // delivery|pickup|dine_in
"cancellation_reason": null, // yalnız order.cancelled'da dolu

// --- FİNANSAL KIRILIM (değişmez: original_subtotal − discount_amount + ücretler = total) ---
"original_subtotal": 110.0, "subtotal": 110.0, "subtotal_charged": 107.0,
"item_discount": 3.0, "additional_discount": 5.35, "discount_amount": 8.35, // = item+additional
"coupon_code": "HOSGELDIN", "tax": 0.0,
"delivery_fee": 0.0, "service_fee": 0.0, "tip_amount": 0.0, "total": 101.65,
"currency": "TRY", // + her alanın *_minor (kuruş) karşılığı: original_subtotal_minor, total_minor, ...

// --- MÜŞTERİ / ADRES ---
"customer": { "user_id": "...", "name": "Burak", "phone": "905319222001" },
"delivery_address": { "street": "...", "city": "...", "district": "...", "directions": "...", "name": "...", "phone": "...", "coordinates": {...} },
"delivery_directions": "Kapıda zili çalmayın", // ADRES TARİFİ (düz)
"table_number": null,

// --- ÖDEME ---
"payment_method": "cash", "payment_method_label": "Nakit",
"payment_status": "unpaid", "delivery_provider": "merchant",

// --- NOT / İLERİ TARİH / SEKTÖR ---
"notes": "Sipariş notu", "scheduled_for": null, "estimated_time": null,
"fulfillment": null, // sektör-bilinçli (çiçek recipient/card_note, tekel age_verified, market substitution…)

// --- KALEMLER + MALZEME ---
"items": [ {
"reference_id": "...", "name": "Hamburger", "type": "product", "quantity": 2,
"original_unit_price": 60.0, "discounted_price": 55.0, "extra_price": 10.0,
"line_total": 140.0, "line_discount": 10.0, // line_total = original*qty + extra*qty
"notes": "az pişmiş",
"selected_ingredients": [ { "name": "Orta boy", "group": "Boyut", "extra_price": 10 } ],
"added_ingredients": [ { "name": "Ekstra peynir", "group": "Ekstralar", "extra_price": 0 } ],
"removed_ingredients": [ { "name": "Soğan", "group": null } ] // ÇIKARILAN
} ],
"created_at": "2026-06-27T...Z", "updated_at": "2026-06-27T...Z"
}

Posyou bilmediği alanları yok saymalı (ileri-uyum). Baktin yeni alan eklerse mevcut alanlar değişmez.


7. BÖLÜM F — Katalog ürün payload (A.4 POST /catalog/products gövdesi)

{
"op": "upsert",
"internal_id": "665f...prd", // FG ürün kimliği (değişmez referans)
"external_id": "POS_PRD_1", // VARSA Posyou günceller; yoksa oluşturur + yanıt id'sini döndür
"name": "Margherita", "description": "...",
"price": 120.0, "discounted_price": 99.0,
"external_category_id": "CAT1", "category_name": "Pizzalar",
"is_available": true, "stock": 25, "images": ["https://..."],
"_ts": 1782580000000
}

7.5 BÖLÜM G — POS-BAŞLATAN İŞLETME ONBOARDING (Posyou → FG; TEK-TUŞ)

Amaç: Posyou kullanan bir işletme, POS ekranından tek tuşla Baktin partner hesabı açar. Posyou backend, işletmenin (POS'ta zaten var olan) bilgilerini + tüm ürün kataloğunu TEK çağrıda FG'ye gönderir. FG; işletme sahibi User'ı + Restaurant'ı (inceleme-bekliyor) + POS bağlantısını + kataloğu oluşturur ve Posyou'ya sipariş çekmek için bir scoped api_key + webhook_secret döndürür. Bu, kullanıcı-başlatan panel başvurusundan (/restaurants/apply, OTP) FARKLIDIR — burada Posyou-backend başlatır, kullanıcı etkileşimi gerekmez.

G.1 Kimlik — PLATFORM-seviyeli anahtar

POS-başlatan onboarding, işletme-kapsamsız (global) bir X-API-Key ile yapılır. FG admin Posyou'ya bunu BİR KEZ üretir:

  • İzin: integrator:provision (yalnızca yeni işletme yaratma + katalog import; mevcut başka işletmelerin siparişi/verisi bu anahtarla OKUNAMAZ/YAZILAMAZ — her uç ayrı yetki ister).
  • restaurant_id YOK (kapsamsız). Header: X-API-Key: {platform_key}.
  • Onboarding sonrası FG'nin döndürdüğü scoped_api_key ise YALNIZ o yeni işletmeye kapsanır (sipariş poll için BÖLÜM B).

G.2 POST /integration/provisioning/onboard — tek-tuş onboarding

Header: X-API-Key: {platform_key}. Gövde:

{
"merchant": {
"name": "Lezzet Durağı", // marka/görünen ad (zorunlu)
"company_name": "Lezzet Gıda Ltd.", // ticari unvan (zorunlu)
"phone": "+905551234567", // İŞLETME SAHİBİ telefonu — panele giriş + owner hesabı (zorunlu)
"city": "İstanbul", // zorunlu
"address": "Kadıköy, ... No:12", // zorunlu
"district": "Kadıköy", // opsiyonel
"tax_number": "1234567890", // opsiyonel (auto-approve için gerekli)
"iban": "TR...", // opsiyonel (hakediş; auto-approve için gerekli)
"email": "info@...", "latitude": 40.99, "longitude": 29.02 // opsiyonel (lat/lng → yakınlık keşfi)
},
"profile_key": "restaurant", // sektör profili (aşağıdaki listeden). VEYA card+answers gönderin.
"posyou": {
"external_store_id": "POSYOU_STORE_42", // POS mağaza kimliği — İDEMPOTENCY + bağlantı eşleme (zorunlu, UNIQUE)
"endpoint_url": "https://api.posyou.com/...", // POS REST base (FG→POS pull/push); opsiyonel
"api_key": "posyou_store_key", // POS API anahtarı (FG→POS REST kimlik); opsiyonel
"webhook_secret": "..." // opsiyonel — VERİLMEZSE FG üretir + yanıtta BİR KEZ döner
},
"catalog": { // opsiyonel ama TEK-TUŞ için ÖNERİLEN — tüm ürünleri inline gönder
"categories": [ { "external_id": "CAT1", "name": "Çorbalar", "display_order": 1 } ],
"products": [
{ "external_id": "PRD1", "name": "Mercimek Çorbası", "price": 45.0,
"external_category_id": "CAT1", "is_available": true, "description": "...", "images": ["https://..."] }
]
},
"posyou_request_id": "req-uuid" // opsiyonel — ağ-retry idempotency
}
  • profile_key geçerli değerler (seed): restaurant, cafe, fast_food, bakery, market, alcohol_shop, petshop, florist, flower_arrangement, event_flower, field_service, professional_service, hybrid_service, houseware, hardware, lighting, building_materials, general_retail. Bilinmiyorsa card (food/market/store/flower/pet/service) + answers gönderin; FG çözer.
  • catalog inline (önerilen): FG ürünleri doğrudan yazar — POS'a geri çağrı YAPMAZ (kill-switch'ten bağımsız). Alternatif: catalog boş bırakılırsa FG, posyou.endpoint_url'den çekmeyi dener (yalnız FG canlı-dispatch açıkken).
  • Dedup: ürün/kategori external_id ile eşlenir → onboarding tekrar edilse bile ürün çoğalmaz (idempotent).

Yanıt (200):

{
"success": true,
"merchant_id": "665f...e90", // FG işletme kimliği — sakla
"owner_user_id": "...",
"status": "pending", // admin inceleme bekliyor (auto-approve kapalıyken)
"scoped_api_key": "fg_xxx", // BİR KEZ — Posyou bundan sonra sipariş poll için kullanır (BÖLÜM B)
"webhook_secret": "yyy", // BİR KEZ — POS→FG webhook HMAC imzası (BÖLÜM C/1)
"inbound_webhook_url": "https://api.baktin.com/integrations/webhooks/posyou/665f...e90",
"import": { "categories_imported": 8, "products_imported": 142, "failed": [ { "external_id": "PRDx", "kind": "product", "reason": "..." } ] }
}

⚠️ scoped_api_key + webhook_secret yalnız bu yanıtta döner — Posyou bunları güvenli saklamalı (yanıtı LOGLAMAYIN).

G.3 İdempotency & soft-provision

Aynı posyou.external_store_id ile ikinci kez onboard çağrılırsa FG yeni işletme YARATMAZ — var olanı bulur ve yalnız kataloğu re-import eder ({ "success": true, "soft_provision": true, "merchant_id", "import": {...} }). Ağ-retry / tekrarlı tuş güvenli. Bir POS mağazası (external_store_id) yalnız bir FG işletmesine bağlanır (başka tenant aynı store ile bağlanamaz → 409).

G.4 Durum sorgulama — GET /integration/provisioning/status/{external_store_id}

X-API-Key: {platform_key}. Döner: { "exists": bool, "merchant_id", "restaurant_status": "pending|active|...", "connection_status", "last_catalog_sync_at", "inbound_webhook_url" }. Posyou onboarding sonrası onay durumunu izler.

G.5 Katalog re-import — POST /integration/provisioning/catalog/{external_store_id}

Onboarding SONRASI yeni ürün eklendiğinde toplu güncelleme. Gövde: { "categories": [...], "products": [...] } (G.2 ile aynı şekil). Dedup (external_id) → mevcut ürünleri günceller, yenileri ekler. Alternatif: artımlı tekil güncelleme için BÖLÜM C inbound webhook'u (catalog.product.upserted) veya BÖLÜM B batch uçları kullanılabilir.

G.6 Onay & yaşam döngüsü

  • Yeni işletme status=pending (admin inceleme) ile yaratılır → keşifte görünmez, sipariş almaz. FG admin onaylar → active.
  • (Opsiyonel) FG tarafında POSYOU_AUTO_APPROVE=true ile otomatik active — ancak tax_number+iban eksikse veya yaş-kısıtlı profilse (alcohol_shop) yine pending zorlanır (mali/yasal güvenlik).
  • Onaydan sonra Posyou, scoped_api_key ile BÖLÜM B'den sipariş çeker / durum sürer; BÖLÜM C webhook'u ile POS→FG durum/stok/iade gönderir (HMAC: dönen webhook_secret).

G.7 Onboarding hata kodları

KodNeden
401X-API-Key yok/geçersiz
403Anahtarda integrator:provision yetkisi yok (veya scoped anahtarla provision denendi)
400Eksik/geçersiz alan (merchant.* zorunlular, profile_key/card yok, geçersiz fiyat)
409/400external_store_id başka FG işletmesine bağlı (mağaza-tenant çakışması)

G.8 curl örneği

curl -s -X POST https://api.baktin.com/integration/provisioning/onboard \
-H "X-API-Key: $FG_PLATFORM_KEY" -H "Content-Type: application/json" -d '{
"merchant": { "name":"Lezzet Durağı","company_name":"Lezzet Gıda Ltd.","phone":"+905551234567","city":"İstanbul","address":"Kadıköy No:12","tax_number":"1234567890","iban":"TR000000000000000000000000" },
"profile_key": "restaurant",
"posyou": { "external_store_id":"POSYOU_STORE_42","endpoint_url":"https://api.posyou.com/...","api_key":"posyou_store_key" },
"catalog": { "categories":[{"external_id":"CAT1","name":"Çorbalar"}],
"products":[{"external_id":"PRD1","name":"Mercimek Çorbası","price":45,"external_category_id":"CAT1"}] }
}'
# → { "merchant_id":"...", "scoped_api_key":"fg_...", "webhook_secret":"...", "import":{...} }

G.9 Go-live checklist (POS-başlatan onboarding)

  1. FG admin Posyou'ya platform anahtarı üretti (integrator:provision, scopeless) → Posyou sakladı.
  2. Posyou POS'a "Baktin'ya katıl" tuşu ekledi → backend onboard çağırıyor (merchant + profile_key + tüm catalog).
  3. Posyou yanıttaki merchant_id + scoped_api_key + webhook_secret'i sakladı (yanıt loglanmadı).
  4. FG admin yeni işletmeyi inceledi → active (veya POSYOU_AUTO_APPROVE).
  5. Posyou scoped_api_key ile sipariş poll (BÖLÜM B) + webhook_secret ile POS→FG webhook (BÖLÜM C) akışına geçti.

8. İdempotency, retry, güvenlik özeti

  • İdempotency (her iki yön): X-Baktin-Delivery-Id (A/C) ve event_id (C). Aynı kimlik tekrar gelirse işleme.
  • Retry/backoff (FG→Posyou): exponential, 6 denemede dead-letter. Posyou 2xx dönmeli; 5xx/timeout → FG yeniden dener.
  • Sıra: Her durum geçişi ayrı olaydır (_ts ile sıralanır). İleri-atlanan durumda FG ara geçişleri sırayla uygular.
  • Tenant-izolasyon: Tüm çözümleme işletme-kapsamlı (IDOR-güvenli) — bir işletmenin anahtarı/webhook'u başka işletmeye dokunamaz.
  • Kill-switch: FG tarafında INTEGRATION_LIVE_DISPATCH açılana kadar Posyou'ya hiçbir canlı çağrı gitmez (tüm olaylar outbox'ta birikir).

9. Go-live checklist (Baktin ↔ Posyou)

  1. Onboarding: FG panelinden işletme bağlantısı oluştur → api_key + webhook_secret + endpoint_url + external_store_id paylaş.
  2. Posyou tarafı: Bölüm A uçlarını (A.1–A.4) yayına al; HMAC doğrulamayı uygula; Bölüm C webhook'unu gönderecek üretici hazırla.
  3. FG tarafı: capabilities (catalog_sync vb.) ata; tek test-bağlantısı ile INTEGRATION_LIVE_DISPATCH + INTEGRATION_OUTBOX_WORKER aç.
  4. Smoke (uçtan-uca):
    • FG'de test siparişi → A.3 POST /orders Posyou'ya düştü mü? (order.created, imza geçerli mi, {id} döndü mü?)
    • Posyou'dan C webhook order.preparing → FG durumu güncellendi mi?
    • Posyou'dan store.close → FG checkout reddediyor mu?
    • FG menü düzenle → A.4 catalog.product.upserted Posyou'ya düştü mü?
    • (opsiyonel) Posyou katalog → FG GET /integration/... ile reconcile.
  5. İzleme: FG outbox delivered/failed/dead, inbox processed/ignored; hata → last_error.

Sürüm: as-built 2026-06-27. Kaynak-of-truth: Baktin src/integrations/*. Soru/değişiklik → Baktin entegrasyon ekibi.