Ana içeriğe geç

Posyou ↔ FirsatGO Entegrasyon Spesifikasyonu (Posyou uygular)

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

Bu doküman "as-built"tır: aşağıdaki yollar/şekiller/imza FirsatGO 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)FirsatGOSipariş 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 FirsatGO 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-FirsatGo-Timestampİmzadaki timestamp (ms). Replay penceresi ±5 dakika — dışı reddedilir (401).
X-FirsatGo-SignatureYukarıdaki sha256=... imza.
X-FirsatGo-EventOlay türü (order.created, order.cancelled, catalog.product.upserted …).
X-FirsatGo-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 FirsatGO 401 döner ve işlemez. Posyou da kendi tarafında aynı sıkılıkta doğrulamalıdır.


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ı). FirsatGO 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.2 Katalog çekme (pull) — GET /catalog/categories + GET /catalog/products

Query: ?storeId={external_store_id} (varsa). FirsatGO 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

FirsatGO 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-FirsatGo-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-FirsatGo-Delivery-Id'yi iki kez işleme.

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

FirsatGO ü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.firsatgo.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).
GET / POST /integration/store/statusMağaza aç/kapa. POST gövde { "is_open": true|false }.
GET / POST /integration/store/hoursÇalışma saatleri.
POST /integration/products/{productId}/availabilityÜrün uygunluk. Gövde { "is_available": bool }.
POST /integration/products/{productId}/priceÜrün fiyat.
GET /integration/orders/{orderId}/courierKurye bilgisi (FG platform-teslimat).

§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.firsatgo.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-FirsatGo-Timestamp: 1782580000000
X-FirsatGo-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)

FirsatGO 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). FirsatGO 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
}

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

  • İdempotency (her iki yön): X-FirsatGo-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 (FirsatGO ↔ 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: FirsatGO src/integrations/*. Soru/değişiklik → FirsatGO entegrasyon ekibi.