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_retailOrtak 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.ts → RETAIL). 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.
| Durum | Anlam (retail) | Aktör | İzinli sonraki geçişler |
|---|---|---|---|
pending | Onay bekliyor | system | confirmed, preparing, cancelled |
confirmed | Onaylandı (sipariş kabul) | merchant | preparing, cancelled |
preparing | Toplanıyor / paketleniyor (raf toplama) | merchant | ready, cancelled |
ready | Paketlendi (sevke hazır) | merchant | on_the_way, completed, cancelled |
on_the_way | Kargoda / yola çıktı | courier | delivered, cancelled |
delivered | Teslim edildi | courier | completed |
completed | Tamamlandı | system | — (terminal) |
cancelled | İptal edildi | — | — (terminal) |
Notlar:
preparingretail'de toplama/paketleme demektir (food'daki "mutfak" değil).on_the_waykargo/yola çıkıştır.ready → completedgeçişi pickup (gel-al) siparişlerinde geçerlidir; teslimattaready → on_the_way → delivered → completedizlenir.- Geçersiz geçiş
Invalid status transition from {from} to {to} (retail)hatası döner. Engine çözülemezsefood'a düşer (non-regression). - POS→FG inbound durum eşlemesi (ortak):
confirmed/preparing/ready/on_the_way/delivered/completed/cancelled,order_idveyaexternal_order_idile.
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ı
| Alan | order_feature | Tip | Geçerli profiller | Açıklama |
|---|---|---|---|---|
substitution_policy | substitution_policy | enum: none | similar | call_first | refund | market, alcohol_shop | Ürün yoksa ne yapılsın: ikame yok / benzeriyle değiştir / önce ara / iade |
delivery_slot | delivery_slot | { date, window } | market, alcohol_shop | Planlı teslimat penceresi. date doğrulanır (geçmiş değil, ≤90 gün) ve üst seviye scheduled_for'a taşınır |
is_repeat | repeat_purchase | boolean | market, alcohol_shop | Düzenli/abonelik sipariş işareti |
repeat_interval_days | repeat_purchase | number | market, alcohol_shop | Tekrar aralığı (gün) |
age_verified | age_verification | boolean | alcohol_shop (tekel) — zorunlu | Yaş doğrulaması. Sunucu fail-closed: true değilse sipariş reddedilir |
local_delivery | local_delivery | boolean | houseware, hardware, lighting, building_materials, general_retail | Yerel teslimat (mağaza profilleri) |
substitution_policygeçersiz değer alırsa:Geçersiz substitution_policy. Beklenen: none|similar|call_first|refund. Tekeldeage_verified !== trueise: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:
| Alan | order_feature | Tip | Açıklama |
|---|---|---|---|
shipment | shipment | { carrier, tracking_no, status } | Kargo takibi. status=shipped → shipped_at, status=delivered → delivered_at otomatik damgalanır |
Market/tekel profilleri
shipmenttaşı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):
| Alan | Açıklama |
|---|---|
name, quantity | Ürün adı + adet |
original_unit_price / _minor | Orijinal/etiket birim fiyat |
discounted_price / _minor | İndirimli (tahsil edilen) birim fiyat |
line_total / _minor | original × qty |
line_discount / _minor | (original − charged) × qty |
reference_id | FG ürün referansı |
Retail'de
selected_ingredients/added_ingredients/removed_ingredients,ingredient_groups,extra_priceboş/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:
| Attribute | Tip | Geçerli profiller | Açıklama |
|---|---|---|---|
barcode | string | market, alcohol_shop, houseware, hardware, lighting, building_materials, general_retail | EAN/UPC — POS arama + stok takibi |
sku | string | (aynı) | İşletme iç ürün kodu |
brand | string | (aynı) | Marka/üretici (görüntü + filtre) |
weight | number | market, alcohol_shop, houseware, hardware, lighting, building_materials, general_retail | kg/g — kargo maliyeti |
variant | object (first_class) | houseware, hardware, lighting, building_materials, general_retail | Adlandı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.upsertedolayları): esnek payload —stock|remaining_quantity|quantity(≥0),price(≥0),is_available/status. POS otoriterdir.*.deleted→status: inactive(hard-delete yok). Eşlenmemiş ürün (external_catalog_mapsyoksa) 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 yoksasubstitution_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 ortakrefund.created/order.refundedolayı ile settlement ters-kayıt yapılır; kısmi tutarrefund_amount_minor/refund_amountile bildirilir. - Yaş doğrulama (tekel):
age_verifiedsunucu-otoriter ve fail-closed. Client'a güvenilmez;truegönderilmeden tekel siparişi oluşmaz. - Planlı teslimat:
delivery_slot.dategeçmiş olamaz ve en çok 90 gün ileri olabilir; doğrulanan tarihscheduled_for'a yansır.store.close(kepenk) olayı FG checkout'unu kapatır. - Tekrar-alış:
is_repeat+repeat_interval_daysdüzenli/abonelik siparişi işaretler (supports_repeat_purchase: true). - Stok negatif olamaz: hem push uçları hem inbound webhook
remaining_quantity/priceiçin≥0zorunlu kılar. - Kargo (mağaza):
shipment.statusshipped/deliveredolduğunda zaman damgaları otomatik eklenir; market/tekel bu alanı kullanmaz. - Geçiş disiplini: retail'de food'un
payment_pendingdurumu yoktur;preparing → ready → on_the_wayzinciri 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).