Ana içeriğe geç

FirsatGO POS Entegrasyonu — MARKET (retail) Sektörü Geliştirici Dokümanı

engine: retail · kapsanan sektörler: market, alcohol_shop (tekel), houseware, hardware, lighting, building_materials, general_retail Ortak sözleşme (HMAC, idempotency, finansal kırılım, retry, tenant-izolasyon) için POSYOU_INTEGRATION_SPEC + Başlangıç. Burada yalnız sektör-farkları.

Genel Bakış

MARKET (retail) sektörü; market, tekel (alcohol_shop), züccaciye (houseware), tesisat/hırdavat (hardware), avize/aydınlatma (lighting), yapı malzemesi (building_materials) ve genel perakende (general_retail) işletmelerini kapsar. Tümü fulfillment_engine: retail ile çalışır. Katalog SKU/barkod/stok merkezlidir: kalemler modifiersız, adet+barkod bazlı hızlı-ticaret kalemleridir. Sipariş akışı, restoranın "mutfak hazırlığı" yerine toplama → paketleme → kargo/teslimat üzerine kuruludur; ürün stokta yoksa devreye giren substitution (ikame) politikası bu sektörün ayırt edici akışıdır.

Para birimi TRY'dir; her finansal alan hem major (TL) hem *_minor (kuruş tamsayı) olarak taşınır. Tüm yüzeyler idempotency (Delivery-Id/event_id), retry (6 deneme → dead-letter) ve tenant-izolasyonu (IDOR-güvenli) ile korunur. Sözleşmenin ortak yüzeyleri, güvenliği (HMAC sha256=, ±5dk replay), durum olayları ve finansal kasa-fişi kırılımı tüm sektörlerle aynıdır — bu doküman yalnız retail farkını anlatır.

Sipariş Yaşam-Döngüsü

engine=retail state-machine'i (kaynak: lifecycle-registry.tsRETAIL). Durum enum'u ortaktır; anlam ve izinli geçişler retail'e özgüdür. Restoranın payment_pending ve ready→completed (pickup short-circuit dahil bazı yolları) yerine retail düz bir lojistik hattıdır.

DurumAnlam (retail)Aktörİzinli sonraki geçişler
pendingOnay bekliyorsystemconfirmed, preparing, cancelled
confirmedOnaylandı (sipariş kabul)merchantpreparing, cancelled
preparingToplanıyor / paketleniyor (raf toplama)merchantready, cancelled
readyPaketlendi (sevke hazır)merchanton_the_way, completed, cancelled
on_the_wayKargoda / yola çıktıcourierdelivered, cancelled
deliveredTeslim edildicouriercompleted
completedTamamlandısystem— (terminal)
cancelledİptal edildi— (terminal)

Notlar:

  • preparing retail'de toplama/paketleme demektir (food'daki "mutfak" değil). on_the_way kargo/yola çıkıştır.
  • ready → completed geçişi pickup (gel-al) siparişlerinde geçerlidir; teslimatta ready → on_the_way → delivered → completed izlenir.
  • Geçersiz geçiş Invalid status transition from {from} to {to} (retail) hatası döner. Engine çözülemezse food'a düşer (non-regression).
  • POS→FG inbound durum eşlemesi (ortak): confirmed/preparing/ready/on_the_way/delivered/completed/cancelled, order_id veya external_order_id ile.

Sektöre-Özel Sipariş Alanları

fulfillment{} bloğu profile-driven valide edilir (fulfillment.util.ts). Profil desteklemiyorsa alan reddedilir (fail-closed). Market profilinin order_features listesi: cart, stock, repeat_purchase, substitution_policy, delivery_slot. Tekel ek olarak age_verification taşır. Mağaza profilleri (züccaciye/hırdavat/avize/yapı/genel) cart, stock, shipment, local_delivery taşır.

fulfillment{} — checkout (müşteri) alanları

Alanorder_featureTipGeçerli profillerAçıklama
substitution_policysubstitution_policyenum: none | similar | call_first | refundmarket, alcohol_shopÜrün yoksa ne yapılsın: ikame yok / benzeriyle değiştir / önce ara / iade
delivery_slotdelivery_slot{ date, window }market, alcohol_shopPlanlı teslimat penceresi. date doğrulanır (geçmiş değil, ≤90 gün) ve üst seviye scheduled_for'a taşınır
is_repeatrepeat_purchasebooleanmarket, alcohol_shopDüzenli/abonelik sipariş işareti
repeat_interval_daysrepeat_purchasenumbermarket, alcohol_shopTekrar aralığı (gün)
age_verifiedage_verificationbooleanalcohol_shop (tekel) — zorunluYaş doğrulaması. Sunucu fail-closed: true değilse sipariş reddedilir
local_deliverylocal_deliverybooleanhouseware, hardware, lighting, building_materials, general_retailYerel teslimat (mağaza profilleri)

substitution_policy geçersiz değer alırsa: Geçersiz substitution_policy. Beklenen: none|similar|call_first|refund. Tekelde age_verified !== true ise: Bu ürünler yaş doğrulaması gerektirir (age_verified=true zorunlu).

fulfillment{} — işletme (operasyon) alanları

applyMerchantFulfillment ile sipariş sonrası eklenir. Mağaza profilleri için:

Alanorder_featureTipAçıklama
shipmentshipment{ carrier, tracking_no, status }Kargo takibi. status=shippedshipped_at, status=delivereddelivered_at otomatik damgalanır

Market/tekel profilleri shipment taşımaz (yalnız mağaza). delivery_proof/route/workroom_note/arrangement_cost çiçek sektörüne aittir; retail'de gönderilirse reddedilir.

items[] — retail kalemleri

Hızlı-ticaret kalemleri: modifier (ingredient_groups) yok, adet+barkod merkezli. Kasa-fişi kalem yapısı ortaktır (order-receipt.util.ts):

AlanAçıklama
name, quantityÜrün adı + adet
original_unit_price / _minorOrijinal/etiket birim fiyat
discounted_price / _minorİndirimli (tahsil edilen) birim fiyat
line_total / _minororiginal × qty
line_discount / _minor(original − charged) × qty
reference_idFG ürün referansı

Retail'de selected_ingredients/added_ingredients/removed_ingredients, ingredient_groups, extra_price boş/0 kalır — bu alanlar food'a özgüdür.

Örnek fulfillment (tekel + planlı teslim)

{
"fulfillment": {
"engine": "retail",
"substitution_policy": "call_first", // ürün yoksa önce müşteriyi ara
"delivery_slot": { "date": "2026-06-29", "window": "18:00-20:00" },
"age_verified": true, // tekel — zorunlu
"is_repeat": false
}
}

Katalog / Ürün

Retail ürünü SKU/barkod/stok merkezlidir. Stok remaining_quantity (tamsayı, negatif olamaz) ile tutulur; available_quantity=0 sınırsız demektir. item_kind: retail_sku.

Sektör-özel attribute'lar sector_attributes{} bloğunda, profilin item_attributes listesi + ATTRIBUTE_REGISTRY'ye göre valide edilir (sector-attributes.util.ts). Retail için izinli attribute'lar:

AttributeTipGeçerli profillerAçıklama
barcodestringmarket, alcohol_shop, houseware, hardware, lighting, building_materials, general_retailEAN/UPC — POS arama + stok takibi
skustring(aynı)İşletme iç ürün kodu
brandstring(aynı)Marka/üretici (görüntü + filtre)
weightnumbermarket, alcohol_shop, houseware, hardware, lighting, building_materials, general_retailkg/g — kargo maliyeti
variantobject (first_class)houseware, hardware, lighting, building_materials, general_retailAdlandırılmış seçenek + fiyat modifikatörü (market/tekelde yok)

Profil item_attributes örnekleri: market → barcode, sku, brand, weight; tekel → barcode, sku, brand. Listede olmayan attribute reddedilir.

Barkod araması için sparse index mevcuttur: { restaurant_id, 'sector_attributes.barcode' } (yalnız barkodlu ürünler).

Katalog senkron + stok/fiyat olayları

  • FG→POS çekme/upsert (ortak REST, auth X-Api-Key): GET {posBase}/catalog/categories, GET /catalog/products, POST /catalog/products (upsert).
  • POS→FG stok/fiyat (stock.* / inventory.* / catalog.product.upserted olayları): esnek payload — stock | remaining_quantity | quantity (≥0), price (≥0), is_available/status. POS otoriterdir. *.deletedstatus: inactive (hard-delete yok). Eşlenmemiş ürün (external_catalog_maps yoksa) atlanır → önce katalog senkronu gerekir.
  • POS→FG ürün availability/price (işletme-kapsamlı X-API-Key): GET/POST /integration/products/:id/availability, .../price.
// POS→FG stock.updated webhook gövdesi (esnek)
{
"external_id": "8690000000001", // barkod/POS ürün kimliği — eşleme anahtarı
"remaining_quantity": 24,
"price": 89.90,
"is_available": true
}

Akış Notları

  • Substitution (ikame) akışı: Toplama (preparing) sırasında ürün stokta yoksa substitution_policy'ye göre hareket edilir — none (kalem düşülür), similar (benzeriyle değiştir), call_first (müşteriyi ara), refund (kısmi iade). İade durumunda ortak refund.created/order.refunded olayı ile settlement ters-kayıt yapılır; kısmi tutar refund_amount_minor/refund_amount ile bildirilir.
  • Yaş doğrulama (tekel): age_verified sunucu-otoriter ve fail-closed. Client'a güvenilmez; true gönderilmeden tekel siparişi oluşmaz.
  • Planlı teslimat: delivery_slot.date geçmiş olamaz ve en çok 90 gün ileri olabilir; doğrulanan tarih scheduled_for'a yansır. store.close (kepenk) olayı FG checkout'unu kapatır.
  • Tekrar-alış: is_repeat+repeat_interval_days düzenli/abonelik siparişi işaretler (supports_repeat_purchase: true).
  • Stok negatif olamaz: hem push uçları hem inbound webhook remaining_quantity/price için ≥0 zorunlu kılar.
  • Kargo (mağaza): shipment.status shipped/delivered olduğunda zaman damgaları otomatik eklenir; market/tekel bu alanı kullanmaz.
  • Geçiş disiplini: retail'de food'un payment_pending durumu yoktur; preparing → ready → on_the_way zinciri atlanamaz (her adım ayrı geçiş).

Örnek

Tekel (alcohol_shop) — planlı teslimat, yaş doğrulamalı, ikame "önce ara". Yalnız retail alanlarıyla:

{
"order_id": "665fa1c2e8b4a30012ab77f1",
"order_number": "FG-2026-008842",
"status": "confirmed",
"type": "delivery",

// ===== FİNANSAL (TRY, major + minor) =====
"original_subtotal": 320.00, "original_subtotal_minor": 32000,
"item_discount": 0, "item_discount_minor": 0,
"additional_discount": 20.00, "additional_discount_minor": 2000, // kupon
"discount_amount": 20.00, "discount_amount_minor": 2000,
"coupon_code": "ICKI20",
"tax": 0, "tax_minor": 0,
"delivery_fee": 15.00, "delivery_fee_minor": 1500,
"service_fee": 0, "service_fee_minor": 0,
"tip_amount": 0, "tip_amount_minor": 0,
"total": 315.00, "total_minor": 31500,
"currency": "TRY", "currency_minor_unit": "kurus",

"customer": { "user_id": "6610...", "name": "Mehmet K.", "phone": "+905551112233" },
"delivery_address": {
"street": "Bağdat Cad. No:120 D:4", "city": "İstanbul", "district": "Kadıköy",
"directions": "Apartman girişi sağda, zili çalışmıyor — arayın",
"name": "Mehmet K.", "phone": "+905551112233"
},
"delivery_directions": "Apartman girişi sağda, zili çalışmıyor — arayın",

"payment_method": "cash",
"payment_method_label": "Nakit",
"notes": "Buzlu teslim alabilir miyim?",
"scheduled_for": "2026-06-29T15:00:00.000Z",

// ===== RETAIL SEKTÖR-ÖZEL fulfillment =====
"fulfillment": {
"engine": "retail",
"substitution_policy": "call_first",
"delivery_slot": { "date": "2026-06-29", "window": "18:00-20:00" },
"age_verified": true,
"is_repeat": false
},

// ===== KALEMLER (modifiersız, barkod+adet) =====
"items": [
{
"reference_id": "665f...aa01",
"name": "Yeni Rakı 70cl",
"quantity": 2,
"original_unit_price": 130.00, "original_unit_price_minor": 13000,
"discounted_price": 130.00, "discounted_price_minor": 13000,
"line_total": 260.00, "line_total_minor": 26000,
"line_discount": 0, "line_discount_minor": 0
},
{
"reference_id": "665f...aa02",
"name": "Efes Pilsen 50cl x6",
"quantity": 1,
"original_unit_price": 60.00, "original_unit_price_minor": 6000,
"discounted_price": 60.00, "discounted_price_minor": 6000,
"line_total": 60.00, "line_total_minor": 6000,
"line_discount": 0, "line_discount_minor": 0
}
],

"created_at": "2026-06-28T11:20:00.000Z",
"updated_at": "2026-06-28T11:22:00.000Z"
}

Doğrulanan kaynak dosyalar: /home/firsatgo/firsatgo-backendv2/src/orders/lifecycle/lifecycle-registry.ts (RETAIL state-machine), /home/firsatgo/firsatgo-backendv2/src/orders/fulfillment/fulfillment.util.ts (fulfillment alan validasyonu), /home/firsatgo/firsatgo-backendv2/src/merchants/seed/sector-definitions.data.ts (market/tekel/mağaza profilleri + order_features), /home/firsatgo/firsatgo-backendv2/src/merchants/registry/attribute-registry.ts (barcode/sku/brand/weight/variant), /home/firsatgo/firsatgo-backendv2/src/integrations/inbound-webhook.service.ts (stok/fiyat senkron), /home/firsatgo/firsatgo-backendv2/src/integrations/utils/order-receipt.util.ts (kasa-fişi/finansal kırılım).