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ön | Kim çağırır | Ne için |
|---|---|---|---|
| A | FG → Posyou (REST) | FirsatGO | 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 FirsatGO 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-FirsatGo-Timestamp | İmzadaki timestamp (ms). Replay penceresi ±5 dakika — dışı reddedilir (401). |
X-FirsatGo-Signature | Yukarıdaki sha256=... imza. |
X-FirsatGo-Event | Olay 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 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
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; sonrakiorder.updated/order.cancelledgövdesineexternal_order_idolarak 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.
| 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). |
GET / POST /integration/store/status | Mağ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}/courier | Kurye bilgisi (FG platform-teslimat). |
§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.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_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-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 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)
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) 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 (FirsatGO ↔ 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: FirsatGO src/integrations/*. Soru/değişiklik → FirsatGO entegrasyon ekibi.