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ön | Kim çağırır | Ne için |
|---|---|---|---|
| A | FG → Posyou (REST) | Baktin | Sipariş gönder, katalog çek, katalog gönder, mağaza durumu |
| B | Posyou → FG (REST) | Posyou | Sipariş çek (pull), durum sür, ürün uygunluk/fiyat, mağaza durum/saat |
| C | Posyou → FG (webhook) | Posyou | Olay-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ır | Nerede | Ne için |
|---|---|---|
api_key | HTTP header X-Api-Key (A yönü) / X-API-Key (B yönü) | REST kimlik doğrulama |
webhook_secret | HMAC 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:
| Header | Açıklama |
|---|---|
X-Baktin-Timestamp | İmzadaki timestamp (ms). Replay penceresi ±5 dakika — dışı reddedilir (401). |
X-Baktin-Signature | Yukarıdaki sha256=... imza. |
X-Baktin-Event | Olay 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>" }.
| Kod | Anlam | Tipik neden |
|---|---|---|
200 / 201 | Başarılı | — |
400 Bad Request | Geçersiz girdi | Eksik/yanlış param, geçersiz since ISO, geçersiz status, gövde tipi hatası |
401 Unauthorized | Kimlik başarısız | X-API-Key yok/geçersiz; webhook HMAC/timestamp geçersiz (replay penceresi dışı) |
403 Forbidden | Yetki yok | Kapsamsız anahtarla /integration/*; izin (read_orders/write_orders) yetmiyor; §3.4 platform-kurye sahiplik ihlali |
404 Not Found | Bulunamadı | Sipariş/ürün bu işletmede yok (cross-tenant erişim de 404 — varlık sızdırmaz) |
409 Conflict | Durum çakışması | Geçersiz state geçişi (ör. delivered→preparing) |
429 Too Many Requests | Rate-limit | Çok sık istek — Retry-After header'ına uyun, exponential backoff |
5xx | Sunucu 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_catalog→ katalog CRUD (ürün/kategori oluştur/güncelle/sil).integration:read_finance→ finans okuma (hakediş ekstresi + payout listele).integration:write_payout→ payout talep (hakediş ödeme talebi — hassas).integration:write_profile→ profil yazma (işletme bilgisi/konum/logo güncelle).integration:write_reservations→ rezervasyon yazma (oluştur/durum/iptal/ayar).integration:read_analytics→ analitik okuma (yorum/rating + müşteri + dashboard stats).integration:write_marketing→ pazarlama yazma (kampanya oluştur/güncelle/durum/sil).integration:manage_credentials→ kredensiyal yönetimi (webhook-secret yenile + API-anahtar listele/üret/iptal) — YALNIZfullscope (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/writeanahtar ü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 kategoriid'sine referans.is_available(≈available, veyastatus:'active'),stock(≈remaining_quantity).images(≈ tekimage).- 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; sonrakiorder.updated/order.cancelledgövdesineexternal_order_idolarak 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.
| Uç | 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}/status | Durum sür. Gövde { "action": "confirm|preparing|ready|on_the_way|delivered|cancel" }. |
POST /integration/orders/{orderId}/reject | Yeni siparişi reddet. Gövde { "reason_code": "..." } (zorunlu). |
POST /integration/orders/{orderId}/confirm-payment | Nakit ödeme onayı (POS kasa). Gövde { payment_method?, calculated_amount? } (dinamik siparişte tutar). |
POST /integration/orders/{orderId}/confirm-split-payment | Bö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/status | Mağ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/profile | Profil 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/categories | Menü 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/categories | Kategori 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-groups | Malzeme grupları listele (toppings/modifiers + seçenekler). |
POST /integration/ingredient-groups | Malzeme 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/availability | TOPLU 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/price | TOPLU 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}/courier | Kurye bilgisi (FG platform-teslimat). |
GET /integration/finance/statement | Hakediş ekstresi (read_finance): bakiye + bekleyen-payout + kullanılabilir + son ledger hareketleri. |
GET /integration/finance/payouts | Payout talepleri listele (read_finance). |
POST /integration/finance/payouts | Payout TALEP et (write_payout, hassas). Gövde { amount, iban? }; kullanılabilir bakiye yeterli olmalı. |
GET /integration/ratings + /ratings/stats | Değerlendirme/yorum listele + özet (read_analytics). |
GET /integration/customers | Müşteri listesi (sipariş-sayısı/harcama/son-sipariş, read_analytics). |
GET /integration/stats | Dashboard istatistikleri (toplam/durum sipariş + ciro + bugün/7-gün, read_analytics). |
GET /integration/reservations | Rezervasyon listele (read_orders). ?status=&date=YYYY-MM-DD&page=&limit=. |
GET /integration/reservations/stats | Rezervasyon istatistik (read_orders). ?startDate=&endDate=. |
GET /integration/reservations/settings + PUT …/settings | Ayar 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/reservations | Rezervasyon 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}/status | Durum güncelle (write_reservations). Gövde: { status, rejection_reason? }. |
PUT /integration/reservations/{id}/cancel | Rezervasyon iptal (write_reservations). |
GET /integration/credentials | Entegrasyon durumu (manage_credentials): bağlantı/health/has_webhook_secret. Secret DÖNMEZ. |
POST /integration/credentials/webhook-secret/rotate | Webhook secret YENİLE (manage_credentials). FG→POS imza anahtarı; düz metin YALNIZ bir kez döner — saklayın. |
GET /integration/credentials/api-keys | API anahtarları listele (manage_credentials, maskeli — secret yok). |
POST /integration/credentials/api-keys | API 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/campaigns | Kampanya listele (read_analytics). ?status=&type=&featured=. |
GET /integration/campaigns/stats | Kampanya istatistik (read_analytics). |
GET /integration/campaigns/{id} | Kampanya detayı (read_analytics). |
POST /integration/campaigns | Kampanya 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}/status | Durum güncelle (write_marketing). { status: pending|active|inactive|deleted }. |
DELETE /integration/campaigns/{id} | Kampanya SİL (write_marketing, soft → status=deleted). |
GET /integration/reports/sales | Satış 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şindeon_the_way/deliveredPosyou'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_type | Zorunlu alanlar | FG 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 status | FG OrderStatus |
|---|---|
confirmed / accepted | confirmed |
preparing | preparing |
ready | ready |
on_the_way / shipped / out_for_delivery | on_the_way |
delivered | delivered |
completed | completed |
cancelled / canceled | cancelled |
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_idYOK (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. Bilinmiyorsacard(food/market/store/flower/pet/service) +answersgö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:
catalogboş bırakılırsa FG,posyou.endpoint_url'den çekmeyi dener (yalnız FG canlı-dispatch açıkken). - Dedup: ürün/kategori
external_idile 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_secretyalnı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=trueile otomatikactive— ancaktax_number+ibaneksikse veya yaş-kısıtlı profilse (alcohol_shop) yine pending zorlanır (mali/yasal güvenlik). - Onaydan sonra Posyou,
scoped_api_keyile 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önenwebhook_secret).
G.7 Onboarding hata kodları
| Kod | Neden |
|---|---|
401 | X-API-Key yok/geçersiz |
403 | Anahtarda integrator:provision yetkisi yok (veya scoped anahtarla provision denendi) |
400 | Eksik/geçersiz alan (merchant.* zorunlular, profile_key/card yok, geçersiz fiyat) |
409/400 | external_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)
- FG admin Posyou'ya platform anahtarı üretti (
integrator:provision, scopeless) → Posyou sakladı. - Posyou POS'a "Baktin'ya katıl" tuşu ekledi → backend
onboardçağırıyor (merchant + profile_key + tüm catalog). - Posyou yanıttaki
merchant_id+scoped_api_key+webhook_secret'i sakladı (yanıt loglanmadı). - FG admin yeni işletmeyi inceledi →
active(veyaPOSYOU_AUTO_APPROVE). - Posyou
scoped_api_keyile sipariş poll (BÖLÜM B) +webhook_secretile 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) veevent_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 (
_tsile 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_DISPATCHaçılana kadar Posyou'ya hiçbir canlı çağrı gitmez (tüm olaylar outbox'ta birikir).
9. Go-live checklist (Baktin ↔ Posyou)
- Onboarding: FG panelinden işletme bağlantısı oluştur →
api_key+webhook_secret+endpoint_url+external_store_idpaylaş. - 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.
- FG tarafı:
capabilities(catalog_sync vb.) ata; tek test-bağlantısı ileINTEGRATION_LIVE_DISPATCH+INTEGRATION_OUTBOX_WORKERaç. - Smoke (uçtan-uca):
- FG'de test siparişi → A.3
POST /ordersPosyou'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.upsertedPosyou'ya düştü mü? - (opsiyonel) Posyou katalog → FG
GET /integration/...ile reconcile.
- FG'de test siparişi → A.3
- İzleme: FG outbox
delivered/failed/dead, inboxprocessed/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.