Müşteri Event'leri — customer.* PII

Müşteri (CRM) yaşam döngüsü ve cari hesap (veresiye/tahsilat) event'leri. Dördü de eklentinin webhookUrl'ine async imzalı POST edilir, customers:read scope + events:subscribe ister. PII içerir — customers:read + tenant consent gerektirir; aksi halde içerik maskelenir/boş gelir.

← Event Kataloğu

Ne zaman tetiklenir?

EventTetikDurum
customer.createdMüşteri dokümanı ilk kez yazıldığında (lifecycle)✓ Canlı
customer.updatedMevcut müşteri her güncellendiğinde (onWrite; tam snapshot, diff değil)✓ Canlı
customer.order_addedCari hesaba sipariş yazıldığında (veresiye/açık hesap)✓ Canlı
customer.payment_addedCari hesaptan tahsilat yapıldığında✓ Canlı
Satış event'i ≠ cari hesap event'i. Müşteri etiketli normal bir masa/paket satışı customer.order_added TETİKLEMEZ — bu event yalnız açık "hesaba sipariş yaz" (veresiye) eylemi içindir (cari bakiyeyi değiştirir). Aynı şekilde ters işlemler (sipariş geri alma / ödeme iptali) hiçbir event tetiklemez. Normal satışları table.* / packet.* event'lerinden izle; double-count etme.

Abonelik

  • Manifest: events: ["customer.created", "customer.updated", "customer.order_added", "customer.payment_added"] + events:subscribe.
  • customers:read (PII) — dördü de bu scope'u ister; kurulumda açık consent ister. Yoksa aşağıdaki scope & consent kuralı uygulanır.

HTTP, imza & teslimat

POST {webhookUrl}
Content-Type: application/json
X-Restomenum-Signature: t=<unixSec>,v1=<HMAC_SHA256(webhookSecret, "<t>.<rawBody>")>
X-Restomenum-Event: customer.created | customer.updated | customer.order_added | customer.payment_added
X-Restomenum-Delivery: <deliveryId>   // teslim-bazlı (debug) — idempotency anahtarı DEĞİL
  • İmzayı ham gövde üzerinden doğrula ("<t>.<rawBody>"), ±5 dk replay penceresi — bkz. imza şeması. Webhook endpoint'in HTTPS olmalı (private/loopback reddedilir — SSRF).
  • Zarf: { id, type, version, tenantId, occurredAt, data }. version string "1"; occurredAt epoch milisaniye (imza t= ise saniye); tenantId = restoran ID'si.
  • Idempotency: envelope id ile dedup (at-least-once → aynı id tekrar gelebilir; ikinci kez işleme, hızlı 2xx dön). X-Restomenum-Delivery teslim-bazlı debug header'ıdır — idempotency anahtarı olarak kullanma (her teslimde farklıdır → retry'ı asla yakalamaz).
  • Ack: 2xx → işlendi. 5xx/timeout → retry → dead-letter.

Scope & consent (PII)

Müşteri PII'si yalnız iki koşul birden sağlanınca paylaşılır: PII scope (customers:read) VE tenant consent'i (dataConsent.piiShared === true, kesin boolean1/"true"/"yes" reddedilir).

DurumWebhook davranışı
customers:read yokEnvelope yine teslim edilir, ama data tamamen boş ({}) — event'in varlığını/id'sini bilirsin, içeriğini görmezsin.
Scope var, consent yokcustomer → yalnız { id } (+ region varsa — non-PII). PII (name/phone/address) + total + serbest-metin (note) düşer.
Scope + consentTam customer objesi (PII dahil) + tüm alanlar.
Callback API'den farkı: Veri API'si scope yoksa 403 reddeder; webhook ise envelope'u teslim eder ama data={} bırakır. Yani "scope yok → {}" (webhook) ile "scope yok → 403" (customers/get) aynı değildir. Yalnız consent katmanı (scope var, consent yok → { id }, region varsa eklenir) iki tarafta da aynıdır.

Redaction iki katmandır: (1) customer objesi allowlist ile { id }'a indirgenir (region varsa korunur — non-PII; yeni alanlar consent yoksa otomatik düşer — güvenli); (2) diğer iç içe nesneler (örn. orders[], ödeme method) alan-adı bazlı recursive temizlenir (name/phone/address/email/tckn/vergino+ serbest-metin note/paymentNote/description/aciklama/desc silinir; orders[].id/lineTotalgibi yapısal alanlar korunur). Tam model: customers:read.

customer.created · updated — müşteri kaydı

data = { customerId, customer }. customerId opak referanstır (consent'ten bağımsız akar); customer ise customers/getile aynı allowlist şeklidir ve consent ile redact edilir. customer.updated tam güncel snapshot taşır — değişen-alan (diff) listesi içermez (bugün changedFields yok; ileride eklenebilir). Diff istiyorsan önceki state'i kendin cache'le.

AlanTipZorunluAçıklama
idstringMüşteri ID'si — her zaman gelir, asla null değil.
regionstring | nullBölge/şube etiketi. PII değildir (kaba coğrafi referans) → consent olmadan da akar. Opsiyonel: müşteride tanımlıysa gelir, aksi halde alan hiç gelmez (canlı örneklerde yoktu).
namestring | nullPII — müşteri adı. Yalnız consent ile; ayarlanmadıysa null.
phonestring | nullPII — telefon. Yalnız consent ile; ayarlanmadıysa null.
addressstring | nullPII — adres. Kaynakta adress (typo) veya address olabilir; dışa daima address. Yalnız consent ile.
totalnumberÖmür boyu toplam harcama (CRM/sadakat) — ONDALIK TL (kuruş değil; ör. 22.61). Finansal — PII değil ama consent yoksa düşer. Asla null değil — geçersiz değer → 0.
Asla dönmeyen iç/finansal alanlar (allowlist gereği): kalan, paid, log, closed, beforeClosed, groupId/group*, category, new — consent verilmiş bir eklentiye dahi sızmaz.
customer.created (consent ile — tam)
{
  "id": "evt_<deterministik-firestore-trigger-id>",
  "type": "customer.created",
  "version": "1",
  "tenantId": "<tenantId>",
  "occurredAt": 1781000000000,
  "data": {
    "customerId": "cust_001",
    "customer": {
      "id": "cust_001",
      "name": "Ahmet Yılmaz",
      "phone": "5551234567",
      "address": "Atatürk Cad. No:5",
      "total": 0
    }
  }
}

customer.updated aynı şekildir, yalnız type + güncel total farklı. Consent yoksa customer{ id } (region varsa eklenir); scope yoksa data{}.

customer.order_added — cari hesaba sipariş (veresiye)

✓ CanlıMüşterinin cari hesabına sipariş yazılınca (total += tutar, kalan = total − paid).

AlanTipZorunluAçıklama
customerIdstringMüşteri ID (opak; consent'siz akar).
customerobject§ müşteri nesnesi (consent ile redact).
ordersobject[]Hesaba yazılan sipariş satırları — müşteri-event satırı: id, title, quantity, options[], extra, note, lineTotal (note VAR, discount YOK). Paket/masa satırından FARKLI (packets/get: discount var, note yok). lineTotal ONDALIK TL, platform hesaplar.
amountnumberBu işlemde eklenen toplam tutar (ondalık TL).
balancenumberİşlem sonrası kalan borç (customer.total − customer.paid). Muhasebe özeti — PII redaction'dan bağımsız daima gönderilir.
customer.order_added (consent ile)
{
  "id": "cust_001_addorder_order_line_001order_line_002",
  "type": "customer.order_added",
  "version": "1",
  "tenantId": "<tenantId>",
  "occurredAt": 1781000000000,
  "data": {
    "customerId": "cust_001",
    "customer": {
      "id": "cust_001",
      "name": "Ahmet Yılmaz",
      "phone": "5551234567",
      "address": "Atatürk Cad. No:5",
      "total": 46.41
    },
    "orders": [
      { "id": "order_line_001", "title": "Espresso", "quantity": 1, "options": [], "extra": 0, "note": "", "lineTotal": 9.9 },
      { "id": "order_line_002", "title": "Cortado", "quantity": 1, "options": [], "extra": 0, "note": "", "lineTotal": 13.9 }
    ],
    "amount": 23.8,
    "balance": 36.41
  }
}
Idempotency id: <customerId>_addorder_<sıralı satır id'leri, ayraçsız> — aynı işlem aynı id'yi belirlenimci üretir; dedup için kullan. note serbest-metindir → consent yoksa silinir.

customer.payment_added — cari hesaptan tahsilat

✓ CanlıCari hesaptan ödeme/tahsilat yapılınca (paid += tutar). Tutar ≤ 0 ise event tetiklenmez.

AlanTipZorunluAçıklama
customerIdstringMüşteri ID (opak; consent'siz akar).
customerobject§ müşteri nesnesi (consent ile redact). total ödeme ile değişmez.
amountnumberÖdeme tutarı.
methodobject | nullÖdeme yöntemi { id, title } (ör. "Nakit"). İç flag'ler (noreport/cash) çıkarılmıştır.
balancenumberÖdeme sonrası kalan borç (total − paid). Daima gönderilir. Kendi kayıtlarınla karşılaştır; uyuşmazlık → bize bildir.
ordersstring[]Toplu tahsilat[] (lump-sum). Kalem-bazlı → ödenen satır ID'leri.
notestringÖdeme notu (serbest-metin → consent yoksa silinir).
customer.payment_added (kalem-bazlı, consent ile)
{
  "id": "cust_001_addpayment_pay_77",
  "type": "customer.payment_added",
  "version": "1",
  "tenantId": "<tenantId>",
  "occurredAt": 1781000000000,
  "data": {
    "customerId": "cust_001",
    "customer": {
      "id": "cust_001",
      "name": "Ahmet Yılmaz",
      "phone": "5551234567",
      "address": "Atatürk Cad. No:5",
      "total": 46.41
    },
    "amount": 36.41,
    "method": { "id": "a2-cash", "title": "nakit" },
    "balance": 0,
    "orders": [],
    "note": ""
  }
}
Idempotency id: toplu tahsilat <customerId>_addpayment_<sıralı ödeme id'leri>; kalem-bazlı payorders_<paymentId>_<ödenen satır id'leri> (ayraçsız birleşik). Aynı işlem aynı id → dedup et.

Müşteri silme — customer.deleted YOK

Abone olunabilir bir customer.deleted event'i yoktur — müşteri dokümanı silindiğinde bu kanaldan bildirim almazsın. Eklentini silme bildirimi gelmeyecek varsayımıyla tasarla; silinen müşterileri customers/list'i periyodik çalıştırıp artık dönmeyenleri temizleyerek mutabık kıl.

GDPR/KVKK silme propagasyonu ayrıdır: tenant bir müşteriyi silince, PII consent'li kurulumlar zorunlu customer.redact lifecycle webhook'unu alır → o customerId'ye ait tüm PII'yi kendi tarafında silmelisin. Bu, abone olunan bir customer.* event'i değil, her bağlı kuruluma düşen bir lifecycle sinyalidir.