Cüzdan
Harcama Kısıtı

Harcama Kısıtı (Spend Policy)

Bu uçları kim çağırır?Merchant panel
Taban yol
/api/panel/v1/wallet
Kimlik
Panel oturumu (JWT) — Authorization: Bearer <panel access token>. Token panel girişinden gelir; s2s imza anahtarı bu uçları AÇMAZ.
Yetki
RBAC izin kodu: wallet:limit:manage · wallet:enduser:manage
Çapraz yüzey
Bu sayfadaki uçların s2s API'de karşılığı YOKTUR; kendi panelinizden çağıramazsınız. Bu yetenekler yalnız merchant panelde, panel oturumuyla kullanılabilir.

Harcama kısıtı, bir cüzdandan para çıkışını kural kümesiyle sınırlar: hangi kanalda, hangi kategoride, hangi işyerinde ve hangi saatlerde harcanabilir.

En yaygın kullanımı kurumsal cüzdandır — yemek kartının yalnız restoranlarda, mesai saatlerinde ve belirli işyerlerinde geçmesi gibi. Ancak motor kuruma özel değildir: program kapsamında da tanımlanabilir.

Harcama kısıtı yalnız panelden yönetilir; imzalı s2s yüzeyinde kural yazma/okuma ucu yoktur. Kurallar operasyon anında motor tarafından otomatik değerlendirilir.

Uçlar

GET spend-policy-categories            # kategori kataloğu
GET spend-policy-operations            # kısıtlanabilir operasyonlar + merchantBearing
GET mcc-categories                     # gerçek MCC → kategori eşlemesi (salt-okuma)
GET programs/:id/spend-policies        # kuralları listele
PUT programs/:id/spend-policies        # kuralları yaz (upsert)
PUT end-users/:id/merchant-category    # bir işyerine kategori ata

Kural modeli

Bir kural iki şey söyler: hangi kapsamda geçerli ve ne yapıyor.

AlanAçıklama
modeALLOW_LIST (yalnız bunlara izin ver) veya DENY_LIST (bunları engelle)
corporateAccountIdDoluysa kapsam kurum, boşsa kapsam program
operations[]Operasyon/kanal ekseni
categories[]Kategori ekseni
merchantEndUserIds[]İşyeri ekseni
timeWindows[] + timeZoneZaman penceresi ekseni
statusACTIVE | INACTIVE
descriptionDenetim için açıklama (en çok 240)
⚠️

mode değerleri ALLOW_LIST ve DENY_LIST'tir — ALLOW / DENY değil. Ayrı bir "etki" alanı yoktur; etkiyi liste modu taşır.

Bir kural en az bir ekseni kısıtlamak zorundadır. Dört eksenin de boş bırakıldığı bir kural veritabanı düzeyinde reddedilir — hiçbir şeyi kısıtlamayan bir kural ya anlamsızdır ya da (ALLOW modunda) her şeyi serbest bırakır.

Dört eksen

1. Operasyon / kanal

Kısıtlanabilir operasyonlar (GET spend-policy-operations ile okunur — listeyi kendi tarafınıza kopyalamayın):

W2W_TRANSFER · W2IBAN_WITHDRAW · WITHDRAW_BANK · PAYMENT_BUY · BILL_PAY · CASHOUT_ATM · CASHOUT_QR · CRYPTO_SWAP · CARD_AUTH · BULK_PAYOUT_ITEM_IBAN

Kart otorizasyonu (CARD_AUTH) da bu motora bağlıdır. Kart tarafında bir kısıt reddi istisna değil, terminal bir DECLINED kaydı üretir: ihraççı webhook'u bir hatayı geçici arıza sanıp yeniden denerdi.

Cüzdan içi hedefe giden toplu ödeme kalemi (BULK_PAYOUT_ITEM_WALLET) bilinçli olarak listede yoktur — o bir fonlama hareketidir, harcama değil.

2. Kategori

Katalog sabittir ve GET spend-policy-categories ile okunur:

FOOD_BEVERAGE · GROCERY · FUEL · TRANSPORT · HEALTH · PHARMACY · EDUCATION · CLOTHING · ELECTRONICS · ENTERTAINMENT · TRAVEL · STATIONERY · SERVICES · OTHER

Kategori, işyerine PUT end-users/:id/merchant-category ile atanır.

⚠️

Kategori ekseni bugün yalnız PAYMENT_BUY operasyonunda koşar. Diğer operasyonlarda ortada kategorisi olan bir işyeri yoktur (IBAN çekiminin, fatura ödemesinin ya da ATM çekiminin bir "restoran" karşılığı yok).

Bunun iki yönlü sonucu vardır ve ikisi de sessiz değildir:

  • Kategori kısıtlayan bir ALLOW_LIST kuralı, kategori taşımayan bir kanalı serbest bırakmaz — o kanal varsayılan redde katkı yapar.
  • Kategori kısıtlayan bir DENY_LIST kuralı, kategori taşımayan bir kanalı engellemez.

"Yemek kartı yalnız restoranda geçsin" demek istiyorsanız kategori kuralının yanına operasyon eksenini de koymalısınız; aksi hâlde IBAN çekimini kapattığınızı sanırsınız.

Hangi operasyonun kategori/işyeri eksenlerini gerçekten çalıştırdığını GET spend-policy-operations yanıtındaki merchantBearing alanı söyler.

Gerçek MCC eşlemesi

Yukarıdaki kategoriler MCC değildir, ama gerçek şebeke MCC'leri (ISO 18245, 4 hane) bu kategorilere eşlenir. Eşleme tablosu GET mcc-categories ile okunur ve panelde kategori seçerken gösterilir:

[
  { "mcc": "3000-3299", "category": "TRAVEL", "isRange": true },
  { "mcc": "5411", "category": "GROCERY", "isRange": false },
  { "mcc": "5812", "category": "FOOD_BEVERAGE", "isRange": false }
]

Satırlar ya tek bir kod ya da ISO blok aralığıdır (isRange). Eşleme platform genelinde tek kaynaktır: aynı tablo dolandırıcılık kurallarında da kullanılır.

⚠️

Tabloda olmayan bir MCC hiçbir kategoriye düşmez — bilinçli olarak OTHER'a indirgenmez.

OTHER "operatör baktı ve diğer dedi" demektir ve bir ALLOW_LIST'e eklendiğinde geçer. Sınıflandırılmamış bir MCC'yi OTHER saymak, tanımadığınız her işyerini sessizce izin listenizden geçirirdi. Bilinmeyen MCC = kategori yok ⇒ kategori ekseni fail-closed davranır.

Kart otorizasyonunda (CARD_AUTH) kategori ekseni bugün koşmaz — kart tarafında gerçek MCC issuer entegrasyonuyla akmaya başlayacak; o güne dek kart harcaması yalnız operasyon ekseninden ve limitlerden kısıtlanır. Eşleme tablosu hazır bekler: kart MCC'si aktığında aynı kategorilere çözülür.

3. İşyeri

merchantEndUserIds[], kısıtın uygulanacağı işyeri son kullanıcı kimliklerinin listesidir. Kategori ekseniyle aynı merchantBearing kuralına tabidir.

4. Zaman penceresi

Pencere; ISO hafta günü (1 = Pazartesi … 7 = Pazar) ve gün içi [başlangıç, bitiş) aralığı ile tanımlanır. Bir kuraldaki pencereler VEYA'lanır: herhangi biri tutarsa eksen geçer.

🚫

Her kural kendi IANA saat dilimini (timeZone) taşır ve pencere varsa bu alan ZORUNLUDUR. Varsayılan YOKTUR.

Sunucunun saat dilimine, kullanıcının cihazına ya da UTC'ye düşülmez. Aynı mutlak an, farklı timeZone taşıyan iki kuralda farklı karar üretir — istenen davranış budur: bir kurum İstanbul mesaisine, bir başkası Londra mesaisine göre çalışır.

Pencere tanımlı ama saat dilimi eksik bir satır fail-closed değerlendirilir (eksen geçmez). Sessiz bir UTC varsayımı yoktur.

Europe/Istanbul gibi geçerli bir IANA kimliği verilmelidir; geçersiz kimlik yazma anında reddedilir. Pencere temizlendiğinde saat dilimi de temizlenir.

Değerlendirme kuralları

Kural içinde: VE

Bir kuraldaki kısıtlanmış eksenlerin hepsi aynı anda tutmalıdır. Operasyon, kategori, işyeri ve zaman sırayla sınanır; ilk tutmayan eksende kural eşleşmez.

Kısıtlanmamış (verilmemiş) eksen sınanmaz.

Kurallar arasında: DENY > ALLOW

⚠️

DENY_LIST her zaman kazanır ve özgüllük önceliği yoktur. Tüm DENY_LIST kuralları, herhangi bir ALLOW_LIST kuralı değerlendirilmeden önce koşar. Dar bir işyeri temelli DENY_LIST, geniş bir kategori temelli ALLOW_LIST'i yener — "daha spesifik kural kazanır" mantığı yoktur.

Kapsamlar arasında: KESİŞİM

İki kapsam vardır: program (corporateAccountId boş) ve kurum (corporateAccountId dolu). Kullanıcı için yalnız programın kuralları ve kendi kurumunun kuralları yüklenir.

🚫

Aynı kapsam içindeki ALLOW_LIST kuralları birleşir (VEYA); farklı kapsamlar KESİŞİR (VE).

ALLOW_LIST kuralı bulunan her kapsamda en az bir eşleşme gerekir. Yani bir kurum, program operatörünün dar ALLOW listesini genişletemez — dar kapsam geniş kapsamı gevşetemez, yalnız daha da daraltabilir.

Tek havuz olsaydı bir kurumun geniş bir ALLOW kuralı yazması, program operatörünün koyduğu sınırı kaldırırdı.

"Politika yok" ile "boş liste" aynı şey DEĞİLDİR

🚫

Bu ayrım yazılmazsa operatör tam tersini yapar:

  • Hiç ACTIVE kural yoksa motor ATILDIR. Hiçbir eksen çözümlenmez bile, her operasyon geçer. INACTIVE bir kural yok sayılır; başka bir programın kuralı bağlamaz.
  • Boş bir dizi ([]) ise "kısıtsız" DEĞİL, "hiçbir şeyle eşleşme" demektir ve fail-closed okunur. Boş listeli bir ALLOW_LIST kuralı hiçbir zaman eşleşmez ⇒ o kapsamda her şey reddedilir.

Boş dizi yazmak zaten üç katmanda birden (istek doğrulama, servis ve veritabanı kısıtı) reddedilir — ama davranışı bilmek, "listeyi boşaltayım, kısıt kalksın" refleksini engeller. Kısıtı kaldırmak istiyorsanız kuralı INACTIVE yapın veya silin; listesini boşaltmayın.

Çözülemeyen eksen = fail-closed

Bir operasyon kategori/işyeri taşıyorsa ama değer çözülemiyorsa ve en az bir kural o ekseni kısıtlıyorsa, işlem CATEGORY_UNRESOLVED / MERCHANT_UNRESOLVED gerekçesiyle reddedilir. Hiçbir kural o ekseni kısıtlamıyorsa ret yoktur.

Ret gerekçeleri: DENIED_BY_RULE · NO_MATCHING_ALLOW_RULE · CATEGORY_UNRESOLVED · MERCHANT_UNRESOLVED.

Motorun zincirdeki yeri

Harcama kısıtı, KYC ve yetenek kapılarıyla birlikte; ücret hesabından ve limit rezervasyonundan ÖNCE koşar — yani para bloke edilmeden önce. Reddedilen bir işlem hiçbir bakiyeyi tutmaz.

Limitlerle karıştırmayın: limit "ne kadar", harcama kısıtı "nerede/ne zaman/hangi kanalda" sorusunu cevaplar. İkisi ayrı motorlardır ve ikisi de geçilmelidir. Bkz. Limitler.

Kontrol listesi

  • Kategori kuralının yanında operasyon ekseni de tanımlı (yoksa yalnız PAYMENT_BUY etkilenir)
  • Her zaman penceresi kuralında timeZone açıkça verilmiş
  • Kısıt kaldırırken liste boşaltılmıyor, kural INACTIVE yapılıyor
  • Kurum kurallarının programı gevşetemeyeceği ürün tasarımında varsayılmış
  • DENY_LIST kurallarının her zaman kazandığı operatöre anlatılmış
  • Kısıt reddi kullanıcıya "bakiye yetersiz" değil, gerçek sebebiyle gösteriliyor