Paket Ödemeleri — POST /plugin-api/packets/update-payments ✓ Canlı

Paketin ödeme satırlarını yazar. İki mod: mode:'replace' (VARSAYILAN — TÜM ödemeleri değiştirir, FULL REPLACE) ve mode:'append' (mevcut satırlara dokunmadan ekler; lineId zorunlu — 🧪 YALNIZ sandbox; production'da yok ve planlanmıyor). paid yeniden hesaplanır (yalnız price>0 satırlar). Transaction'lı. Hiçbir uç paketi KAPATMAZ; paid==total olsa bile finansal kapanış işletme akışındadır.

← API Uçları · Yazma ailesi: create · update · update-orders.

İstek

POST {RESTOMENUM_BASE}/plugin-api/packets/update-payments
Authorization: Bearer <apiKey>

{
  "packetId": "a72d70cd-...",                    // ZORUNLU
  "mode": "replace",                              // ops — "replace" (VARSAYILAN) | "append"  🧪 append yalnız SANDBOX (prod'da YOK, planlanmıyor)
  "payments": [                                   // ZORUNLU — replace: paketin YENİ ödeme listesi (FULL REPLACE)
    { "price": 240, "id": "cash", "title": "Nakit", "isDiscount": false,
      "lineId": "pay-7c31",                       // ops (replace) / ZORUNLU (append) — ödeme SATIRININ kimliği (id = YÖNTEM id'si!)
      "metadata": [ { "key": "terminal", "value": "kiosk-1" } ] }  // ops — DİZİ (map değil)
  ]
}
  • Scope: orders:write.
  • mode opsiyonel, varsayılan "replace". Göndermezsen davranış bit-bit eskisiyle aynıdır — bu bir ekleme, kırıcı değişiklik değil.
  • "replace": payments paketin tüm ödemelerini değiştirir. Satır: { price, id, title, isDiscount?, lineId?, metadata? }.
  • "append": gönderilen satırlar mevcutların üzerine eklenir — aşağıdaki bölüm.
  • İstek başına en fazla 20 ödeme satırı (payments şema tavanı) — her iki modda da. replace dokümanın satırlarını gönderdiğinle değiştirdiği için bu, bir replace çağrısından sonra dokümanı fiilen 20 satıra indirir: doküman tavanı değil, istek tavanının yan etkisidir.
  • paid yeniden hesaplanır — yalnız price > 0 satırlar saklanır.
  • Boş payments: [] → paid: 0 (ödemeleri sıfırlar). Yalnız replace: append'de geçerli satır yoksa istek reddedilir.

Ödeme yöntemi doğrulaması (ÖNEMLİ)

Ödeme satırları tenant'ın gerçek ödeme yöntemlerine karşı doğrulanır ("ID al, sunucuda türet"). Önce payment-methods/list çağır (payment_methods:read), geçerli id'leri oradan al.

SatırKural
Normal (isDiscount yok/false)id tenant'ın tanımlı bir yöntemi OLMALI. title yok sayılır (yöntem kaydından türetilir; göndermek hata değil). Yöntemin cash/noreport alanları kayda işlenir.
İndirim (isDiscount:true)Doğrulamadan muaf — markalı platform-indirimi (serbest id, ör. "getir-ind"). title bu satırlarda zorunlu; raporda doğru sınıflanır.
Geri uyumluluk: gerçek yöntem id'leri kullanan eklentiler etkilenmez; keyfi/uydurma id gönderenler artık unknown_payment_method alır.

Ekleme modu — mode: "append"

🧪 YALNIZ SANDBOX — production'da YOK ve prod'a alınması PLANLANMIYOR (ürün kararı). Bu geçici bir bekleyiş değil, ekleme modunun kalıcı durumudur: sandbox'ta bugün kullanabilirsin, production entegrasyonunda ise mode alanını hiç gönderme. Ekleme modu sandbox'a deploy edildi ve gerçek HTTP çağrılarıyla uçtan uca doğrulandı (18/18 senaryo: ekleme, tekrar gönderim, tutar/yöntem uyuşmazlığı, lineId eksikliği, geriye uyumluluk; ayrıca sandbox.plugins.restomenum.app hostname'i üzerinden 9/9). Bu sayfadaki yanıt gövdeleri o testlerden yakalanmış gerçek gövdelerdir. Production'da HENÜZ YOK — orada mode göndermek isteği komple 400'e düşürür ({ success:false, message: "\"mode\" is not allowed" }; şema bilinmeyen anahtarı reddeder, eski davranışa düşmez). Durum değişirse değişiklik günlüğünden duyurulur.

Neden var: POS terminali eklentileri ödeme satırını bu uçtan yazıyor ve full-replace onlar için bir tuzak: "kendi satırımı gönderdim" diyen en doğal geliştirici davranışı, aynı hesaptaki nakit tahsilatı, ikinci kartı ve indirim satırlarını SİLER ve paid düşer. Ekleme modu bunu kaldırır.

POST {RESTOMENUM_BASE}/plugin-api/packets/update-payments
Authorization: Bearer <apiKey>

{
  "packetId": "a72d70cd-...",
  "mode": "append",                               // mevcut satırlara DOKUNMAZ — üzerine ekler
  "payments": [
    { "price": 240, "id": "m-card", "lineId": "pay-term-01H9..." }   // lineId ZORUNLU (tekrar gönderim kalkanı)
  ]
}
  • Gönderilen satırlar mevcutların üzerine eklenir; mevcut satırlara dokunulmaz. Mevcut satırları tekrar gönderme (aşağıdaki append_partial_duplicate).
  • lineId ZORUNLUDUR — keyfi bir kural değil, tekrar gönderim kalkanı: platform kimlik göndermeyen satıra her çağrıda FARKLI bir kimlik üretir, yani ağ hatasında tekrarlanan istek ikinci bir satır olarak eklenir = çift tahsilat. Kimliği kendi kaydından türet ve retry'de aynısını gönder.
  • lineId GÜÇLÜ olmalı: en az 8 karakter ve saf rakam DEĞİL. "00000001" sekiz karakterdir ama ayırt etmez — sayaç kalıbı reddedilir. Eski satırlara tanınan grandfather affı ekleme modunda İŞLEMEZ: af yalnız dokümanda zaten var olan kimlikleri bağışlar, ekleme modundaki yeni satırın kimliği ise tanım gereği dokümanda yoktur (olsaydı tekrar gönderim olurdu). Önerilen kalıp: sabit önek + uuid — pay-01H9XYZ…. (replace'te lineId opsiyoneldir; göndermezsen platform üretir.)
  • paid birleşim üzerinden yeniden hesaplanır; paid ≤ total kapısı aynen geçerli.
  • price ≤ 0 satırlar süzülür → süzme sonrası geçerli satır kalmazsa istek reddedilir.
  • İki ayrı tavan: tek istekte en fazla 20 satır gönderebilirsin (şema, her iki modda), ve ekleme modunda birleşim (mevcut + gönderilen) 100'ü aşamaz → too_many_payment_lines. İkincisi iş kuralı değil, kaçak büyümeye karşı emniyet supabıdır.
  • KAPAMA YOK politikası değişmedi: paid == total olsa bile hesap kapanmaz.
20 satır tavanı tuzağı somutlaştırır. Kasiyerin panelden tek tek ödeme alması gibi çekirdek/personel akışları bu uçlardan geçmez ve dokümanı 20 satırın üstüne çıkarabilir. Böyle bir hesaba replace çağırırsan satırlar gönderdiğinle değişir — 20'nin üstündeki her şey gider. Full-replace tuzağının somut hâli budur ve append tam olarak bunun için var.

Tekrar gönderim — replayed

Yanıttaki replayed: true = aynı lineId ile tekrar gönderim algılandı, yazma YAPILMADI, doküman değişmedi. Retry sonrası bunu görüp "ödemem zaten kayıtlı" diyebilirsin. Bu durumda hiçbir event yayılmaz (doküman değişmediği için).

Ekleme moduna özgü hatalar

Kod (400)Anlamı
append_requires_line_idEkleme modunda lineId göndermeyen satır var.
append_requires_payment_lineİstekte geçerli satır yok (price ≤ 0 satırlar süzüldükten sonra boş).
append_partial_duplicateSatırların bir kısmı zaten yazılı, bir kısmı yeni. Sessizce yalnız yenileri eklemek, çağıranın hangi satırının geçtiğini bilmediği bir durumda çift tahsilat üretirdi → istek toptan reddedilir. Tüm isteği aynı lineId kümesiyle tekrar gönder.
append_replay_amount_mismatchAynı lineId, farklı tutar.
append_replay_method_mismatchAynı lineId, farklı ödeme yöntemi (id).
too_many_payment_linesBirleşim sonrası satır sayısı 100'ü aşıyor.
Webhook karşılığı FARKLIDIR. Ekleme modu packet.updated olayını data.changed: "payment_added" ile yayar, "payment_updated" ile değil — payment_updated "ödeme satırları tam değiştirmeyle güncellendi" demektir ve ekleme modunda onu yaymak aboneye "tüm satırlar değişti, yeniden diff'le" dedirtirdi. replace modu eskisi gibi payment_updated yayar.

Yanıt

// ekleme başarılı (dokümandaki 100₺ nakit korundu, 240₺ kart eklendi → 340)
{ "success": true, "data": { "packetId": "e2e-append-packet", "paid": 340, "replayed": false } }

// aynı lineId ikinci kez → YAZMA YOK, doküman değişmedi
{ "success": true, "data": { "packetId": "e2e-append-packet", "paid": 340, "replayed": true } }

// hata — gövde `status` alanını da taşır (HTTP kodunun kopyası)
{ "success": false, "status": 400, "message": "append_replay_amount_mismatch" }

replayed ⏳ ekleme modu ile birlikte gelir; replace modunda anlamı yoktur.

Kurallar & hatalar

Hepsi HTTP 200 + { success:false, message } (rate limit 429 hariç). Gerçek mesajlar:

Kuralmessage (teyitli)
lineId biçim ihlali / rezerve / istekte tekrarInvalid lineId: <id> · Duplicate lineId: <id> (400)
İptal edilmiş (void) satırın kimliği yeniden beyan edildilineId belongs to a cancelled line (409)
metadata düz map gönderildi (dizi bekleniyor)metadata must be an array of {key, value} (400)
Geçersiz key/değer, aynı yazarın tekrar eden anahtarı ya da yazar bütçesi (10 öğe / 256 karakter) aşımıInvalid metadata: payments[i]: <sebep> (400)
İstek başına 20 KB ya da belge geneli 100 KB bütçe aşımıTotal metadata size after merge … (yours: X, other plugins: Y) (400)
Normal satırın id'si tenant'ın yöntemi değilunknown_payment_method (400)
Tenant'ta hiç ödeme yöntemi tanımlı değilno_payment_methods_configured (400)
Yeni paid paketin total'ını aşıyorPaid (999) exceeds total (51.9).
Paket bulunamadıPacket not found
orders:write onaylı değilplugin.scope.denied

payments[] — satır kimliği (lineId) & metadata

İki alan da opsiyoneldir; hiçbiri gönderilmezse davranış bugünküyle aynıdır (mevcut eklentiler etkilenmez). Tam kurallar: Satır Kimliği & metadata.

AlanTipZorunluAçıklama
payments[].lineIdstring–Kararlı satır kimliği — çağıran taşır, platform korur. 8–64 karakter (⏳ alt sınır 1 → 8'e çıktı), alfanümerik başlar, A-Z a-z 0-9 . _ : - içerir — ⚠️ noktalama yalnız İÇERİDE (pay_01H9ABCD geçer; _pay01H9ABCD ve -pay01H9ABCD 400); saf rakamdan oluşan kimlik reddedilir ("1", "42" → 400). kuver ve new rezervedir (büyük/küçük harf farketmez) → 400 Invalid lineId: kuver. Aynı istekteki liste içinde tekrar edemez → 400 Duplicate lineId: <id>; iptal edilmiş (void) bir satırın kimliğini yeniden beyan etmek → 409 lineId belongs to a cancelled line. Gönderilmeyen satıra platform üretir ve üretilen kimlikler beyan edilenlerle çakışmaz. Göndermesen bile her ödeme satırı kalıcı bir lineId alır (platform üretir, pay- önekli — önceden ödeme kaydının kimliği hiç yoktu). ⚠️ Kimlik korunması koşulludur: her istekte yeniden beyan edilmelidir (aşağı bkz.).
payments[].metadataarray–Opak korelasyon alanı — { key, value } dizisi (düz map değil; map gönderen istek 400 metadata must be an array of {key, value} alır). Platform yorumlamaz: saklar ve okuma yanıtında yazar damgasıyla (by) döner. key alfanümerik başlar, A-Za-z0-9_-, ≤64; value string | number | boolean (iç içe obje / dizi / null → 400). Bütçeler: yazar başına ≤10 öğe / satır ve value ≤256 karakter, istek başına ≤20 KB (sepet ve ödeme ayrı), belge geneli birleşme sonrası ≤100 KB.
⏳ Grandfather — PLANLANAN davranış, bugün ETKİN DEĞİL. Af mantığı platformda yazılı ama şema katmanı isteği daha transaction'a girmeden reddettiği için ona ulaşılmıyor — dört yazma ucunun hepsinde ölçüldü. Bugün dokümanda zaten var olan zayıf bir kimlik de 400 alır. Ayrıntı: Satır kimliği → Grandfather. Aşağıdaki akış tasarlanan davranıştır:

Tam-sepet değiştirmede önceki yanıtta aldığın kimlikleri geri gönderirsin; kural konmadan önce yazılmış zayıf bir kimlik dokümanda duruyorsa onu reddetmek ilgisiz satırlar dahil tüm isteğini düşürür ve dokümanı kalıcı kilitlerdi. Bu yüzden:
1️⃣ Biçim geçersizse her zaman 400 — asla affedilmez.
2️⃣ Biçim geçerli ama zayıfsa (<8 karakter ya da saf rakam): kimlik dokümanda zaten varsa kabul edilir (grandfather), yoksa 400 (yeni zayıf beyan).
Kontrol transaction içinde, doküman zaten okunurken yapılır — ek okuma maliyeti yok.
⚠️ packets/create ve tables/create uçlarında grandfather YOKTUR: yeni satış açarlar, karşılaştırılacak mevcut doküman yoktur → her zayıf kimlik yeni beyandır ve 400 alır.
Öneri: anahtarını çifte çevir. Kural bir kalkan; yapısal çözüm (uuid, lineId) çiftini anahtar yapmaktır. uuid (kalıcı satış kimliği) artık her olayda geliyor ve satış bazında kesin ayrık — çift, bu hata sınıfını kuralın da ötesinde kapatır.
payments[].id satır kimliği DEĞİLDİR. O alan ödeme yönteminin kimliğidir (nakit/kart tanımı — bkz. payment-methods/list) ve yerli akışlarda satır uuid'si de taşıyabilir; polimorfiktir, satır silme her yerde onu hedefler. Bu yüzden kalıcı kimlik ayrı lineId alanına yazıldı; id'nin anlamı değişmedi. Okuma yanıtında ödeme satırı lineId (kimlik) ve methodId (yöntem) alanlarını ayrı ayrı döner.
Altın kural: kimliği HER istekte yeniden beyan et. Bu uç tam değiştirme yapar — platform yalnız gelen listedeki kimlikleri dokümandakilerle eşler. Beyan edilmeyen satır "silinmiş" sayılır ve üzerindeki tüm metadata (başka eklentilerin yazdıkları dahil) onunla birlikte gider. İstek reddedilmez (meşru satır silme de bu yola düşer) ama platform kaybı loglar. Kimliği kendi kaydından türet ve kendi tarafında sakla — doğru/yanlış örneği.
metadata'ya PII yazmayın (ad, telefon, e-posta, adres, kart): sözleşme gereği yasaktır. Platform bu alanda desen taraması yapmaz — yanlış pozitif meşru bir mali kaydı reddedip satışı durdururdu; kural sözleşme düzeyindedir, ihlal eklentinin sorumluluğundadır. Müşteri verisi customers:read + rıza üzerinden alınır.
Görünürlük: alan tenant içi korelasyon verisidir (kiosk yazar, fiskal sağlayıcı okur) → her kanalda orders:read yeterlidir, müşteri-PII rızası gerekmez ve alan push gövdelerinden silinmez. note, paymentNote ve iptal reason bu kapsamda değildir — onlar gerçek kullanıcı girdisi taşır ve rıza kapısına tabi olmaya devam eder.
Yazma ve okuma satır şekli FARKLIDIR. Buraya { id, title, price, isDiscount?, lineId?, metadata? } gönderirsiniz; geri okurken packets/get (ve tables/get · packet.created) payments[] dizisini { lineId, methodId, title, amount, cash, isDiscount, metadata } olarak döner — price→amount, yazmadaki id (YÖNTEM id'si) → methodId, ayrıca yöntemin cash bayrağı eklenir (nakit/nakit-dışı ayrımı buradan okunur). Satır kimliği ayrı bir alandır: lineId — eski satırlarda null, id'ye fallback yapılmaz.
Eşzamanlılık: aynı paket için update-orders ile EŞ ZAMANLI çağırma → paid ≤ total invariantı geçici bozulabilir. Sıralı çağır. Nihai finansal güvence kapanışta (paid == total).