odemehub 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,481 @@
1
+ Metadata-Version: 2.4
2
+ Name: odemehub
3
+ Version: 1.0.0
4
+ Summary: ödemehub ödeme geçidi için Python istemcisi.
5
+ License: Proprietary
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # ödemehub Python SDK
10
+
11
+ ödemehub ödeme geçidini kendi uygulamanızdan kullanmak için hazırlanmış Python istemcisi. Kart çekmek, 3D ödeme başlatmak, müşteriyi ödeme sayfasına yollamak, ürün kataloğunuzu eşlemek, abonelik açmak, kart saklamak, iade ve iptal yapmak ve bir kartın taksit seçeneklerini sormak için gereken her şey burada.
12
+
13
+ İstemci her isteği takımınızın gizli anahtarıyla imzalar, gelen her yanıtın imzasını doğrular. Siz imza, başlık ya da JSON ayrıntılarıyla uğraşmazsınız. Yalnızca standart kütüphaneyi kullanır; kurulacak başka paket yoktur. Tip ipuçları pakettedir.
14
+
15
+ ## Kurulum
16
+
17
+ Python 3.10 ve üzeri gerekir.
18
+
19
+ ```bash
20
+ pip install odemehub
21
+ ```
22
+
23
+ ## Yapılandırma
24
+
25
+ Dört bilgiye ihtiyacınız var. Hepsi paneldeki **Entegrasyon** sayfasındadır (menünün en altında): API anahtarı, gizli anahtar, Çalışma Alanı Kimliğiniz ve kanallarınızla ödeme hesaplarınızın token'ları.
26
+
27
+ ```python
28
+ import os
29
+
30
+ from odemehub import Client, Options
31
+
32
+ client = Client(Options(
33
+ base_url="https://odeme.gurmehub.com",
34
+ team="4829301756", # Çalışma Alanı Kimliğiniz
35
+ channel_token="6f1c2e7a-4b3d-4c8e-9a61-2f5d7b0c3e14", # müşterinin size ulaştığı kanal
36
+ api_key=os.environ["ODEMEHUB_API_KEY"],
37
+ api_secret=os.environ["ODEMEHUB_API_SECRET"],
38
+ ))
39
+ ```
40
+
41
+ Gizli anahtar hiçbir zaman tel üzerinden gitmez; yalnızca imza üretmekte kullanılır. Anahtarları kodun içine yazmayın, ortam değişkeninde tutun.
42
+
43
+ Kanal token'ı entegrasyonun tamamı için bir kez verilir. Birden çok kanalda satıyorsanız tek bir istekte `channel_token` vererek o isteği başka kanala yazdırabilirsiniz. Geçit hiçbir yerde veritabanı numarası kullanmaz: kanal, ödeme hesabı, işlem, kayıtlı kart, abonelik ve sipariş her zaman token'ıyla adlanır.
44
+
45
+ İstek bir dakika içinde yanıt almazsa kesilir; süreyi `Options(timeout=...)` (saniye) ile değiştirebilirsiniz. İstekler standart kütüphanenin `urllib`'iyle gider; `requests` ya da `httpx` kullanmak isterseniz `post(url, headers, body, timeout)` metodu olan bir nesneyi `Client(options, transport=...)` ile verebilirsiniz.
46
+
47
+ İstekler `odemehub.request` modülündeki değişmez nesnelerdir ve alanları yalnızca adla verilir. İsteğe bağlı bir alanı vermezseniz gövdeye hiç yazılmaz. Kart numarası ve güvenlik kodu `repr()` çıktısına hiç girmez, böylece kart nesnesi yanlışlıkla loglansa da kart bilgisi görünmez.
48
+
49
+ ## Karttan doğrudan çekim
50
+
51
+ Müşteriyi bankasına göndermeden çekim yapar. Başarılı yanıt, paranın alındığı anlamına gelir.
52
+
53
+ ```python
54
+ from odemehub.request import Card, Customer, RegularPayment
55
+
56
+ payment = client.regular_payment(RegularPayment(
57
+ channel_reference="SIP-10231", # işlemin sizdeki referansı
58
+ amount="450.00",
59
+ installment_number=1,
60
+ ip=request.remote_addr,
61
+ customer=Customer(
62
+ channel_reference="musteri-88",
63
+ firstname="Ahmet",
64
+ lastname="Yılmaz",
65
+ email="ahmet@ornek.com",
66
+ phone="05551112233",
67
+ address="Kızılırmak Mah. Dumlupınar Blv. No:3",
68
+ district="Çankaya",
69
+ province="Ankara",
70
+ country="Türkiye",
71
+ ),
72
+ card=Card(
73
+ holder_name="AHMET YILMAZ",
74
+ number="5400360000000003",
75
+ expiry_month="12",
76
+ expiry_year="2030",
77
+ security_code="000",
78
+ ),
79
+ ))
80
+
81
+ if payment.result.successful:
82
+ ... # payment.transaction_token — ödemenin geçitteki token'ı; iade ve iptalde bununla adlandırılır
83
+ ```
84
+
85
+ Tutarlar her zaman metindir (`"450.00"`): imzalanıp gönderildiği gibi kalır, yolda yuvarlanmaz.
86
+
87
+ ## 3D ödeme
88
+
89
+ 3D'de çekim iki adımdır: siz ödemeyi başlatırsınız, müşteri bankasına gider, banka sonucu sizin adresinize gönderir.
90
+
91
+ ```python
92
+ from odemehub.request import SecurePayment
93
+
94
+ payment = client.secure_payment(SecurePayment(
95
+ channel_reference="SIP-10232",
96
+ amount="450.00",
97
+ installment_number=1,
98
+ ip=request.remote_addr,
99
+ callback_url="https://magazam.com/odeme/donus",
100
+ customer=customer,
101
+ card=card,
102
+ ))
103
+
104
+ if payment.result.successful:
105
+ return redirect(payment.redirect_url) # müşteriyi bankaya gönderin
106
+ ```
107
+
108
+ Başarılı yanıt **ödeme alındı demek değildir**; yalnızca müşterinin gideceği adres hazır demektir.
109
+
110
+ Müşteriyi **15 dakika içinde** bu adrese yönlendirin. Sayfası o süre içinde açılmayan ödemenin süresi dolar (`expired`). Süresi dolmuş bağlantıyı açan müşteri doğrudan `callback_url` adresinize, `successful=0` ile geri gönderilir; `retrieve_payment()` sorgusu da başarısız sonucu ve nedenini döner.
111
+
112
+ Banka işini bitirince müşteri, tarayıcısı üzerinden `callback_url` adresinize döner. O POST (form gövdesi) **sonucu taşımaz**, yalnızca sonucun hazır olduğunu haber verir:
113
+
114
+ | Alan | Anlamı |
115
+ | --- | --- |
116
+ | `transaction_token` | ödemenin geçitteki token'ı |
117
+ | `channel_reference` | sizin kendi referansınız |
118
+ | `successful` | `1` / `0` — yalnızca ipucu, **güvenilmez** |
119
+
120
+ Sonucu kendi imzalı bağlantınızdan sorun:
121
+
122
+ ```python
123
+ from odemehub.request import RetrievePayment
124
+
125
+ @app.post("/odeme/donus")
126
+ def odeme_donus():
127
+ outcome = client.retrieve_payment(RetrievePayment(
128
+ transaction_token=request.form["transaction_token"],
129
+ ))
130
+
131
+ if outcome.result.successful:
132
+ ... # siparişi ödendi olarak işaretleyin
133
+ ```
134
+
135
+ Neden böyle: o POST'u bizim sunucumuz değil, müşterinin tarayıcısı gönderir; tarayıcıya imzalayacak bir sır verilemez. `successful` alanına bakıp sipariş kapatmayın — onu herkes gönderebilir; yalnız "başarısız" ipucunda gereksiz sorgudan kaçınmak için kullanın. Geçide sorduğunuz yanıt ise her zaman imzalıdır ve SDK imzayı sizin için doğrular. Başkasının işlemini sorarsanız `ValidationError` alırsınız.
136
+
137
+ ## Ürünler
138
+
139
+ Sipariş kalemleri ve abonelikler ürünleri **sizdeki referanslarıyla** adlandırır. Ürünü panelde (Ürünler sayfası) tanımlayabilir ya da kendi kataloğunuzdan geçide yazabilirsiniz:
140
+
141
+ ```python
142
+ from odemehub.request import SaveProduct
143
+
144
+ product = client.save_product(SaveProduct(
145
+ channel_reference="KAHVE-MAKINESI",
146
+ name="Kahve makinesi",
147
+ type="simple", # simple | recurring
148
+ amount="450.00",
149
+ tax_rate="20", # fiyatın içindeki KDV oranı
150
+ ))
151
+
152
+ client.save_product(SaveProduct(
153
+ channel_reference="PREMIUM-AYLIK",
154
+ name="Premium üyelik",
155
+ type="recurring",
156
+ amount="149.90",
157
+ tax_rate="20",
158
+ period="monthly", # monthly | yearly — yalnız recurring için zorunlu
159
+ ))
160
+ ```
161
+
162
+ Aynı kanalda aynı referans aynı üründür: tekrar gönderirseniz ikinci ürün açılmaz, mevcut olan güncellenir. `currency` verilmezse TRY, `is_active` verilmezse `True` kabul edilir. Ürün silinmez; `is_active=False` ile satışa kapatılır.
163
+
164
+ Ödeme istekleri ürünü hiçbir zaman değiştirmez; ürünün tek yazıldığı yer bu çağrı ve panel.
165
+
166
+ ## Ödeme sayfası
167
+
168
+ Kart bilgisini hiç görmek istemiyorsanız sipariş açıp müşteriyi geçidin kendi sayfasına yollayabilirsiniz.
169
+
170
+ ```python
171
+ from odemehub.request import OrderItem, OrderPayment
172
+
173
+ order = client.order_payment(OrderPayment(
174
+ channel_reference="SIPARIS-10233",
175
+ success_url="https://magazam.com/tesekkurler",
176
+ cancel_url="https://magazam.com/sepet",
177
+ customer=customer,
178
+ items=[
179
+ OrderItem(channel_reference="KAHVE-MAKINESI"),
180
+ OrderItem(channel_reference="KAHVE-500G", quantity=2, unit_amount="180.00"),
181
+ OrderItem(channel_reference="HEDIYE-PAKETI", name="Hediye paketi", unit_amount="25.00"),
182
+ ],
183
+ ))
184
+
185
+ return redirect(order.checkout_url)
186
+ ```
187
+
188
+ Sipariş tutarını göndermezsiniz; geçit kalemleri toplar ve `order.amount` olarak döner. Bir kalemin boş bıraktığı ad, fiyat ve KDV oranı kayıtlı üründen gelir; kalemde verdiğiniz değerler yalnızca o sipariş için geçerlidir, ürünü değiştirmez. Kayıtlı olmayan bir referansla da kalem gönderebilirsiniz, ama o zaman `name` ve `unit_amount` zorunludur.
189
+
190
+ Ödeme tamamlanınca müşteri, 3D'dekiyle aynı biçimde `success_url` adresinize döner: aynı üç alan gelir, sonucu yine `retrieve_payment()` ile sorarsınız. Müşteri ödeme sayfasında karttan kaynaklı bir hata alırsa size dönmez, sayfada kalıp başka kartla dener.
191
+
192
+ ## Abonelikler
193
+
194
+ Müşteriden dönem dönem tahsilat yapmak için abonelik açarsınız. Neye abone olunduğu bir ya da birkaç **abonelik ürünüdür** (`type="recurring"`), sizdeki referanslarıyla adlandırılır; fiyatı, para birimini ve dönemini ürün taşır. Aynı aboneliğe konan ürünlerin dönemi ve para birimi aynı olmalıdır.
195
+
196
+ ```python
197
+ from odemehub.request import SubscriptionItem, SubscriptionPayment
198
+
199
+ subscription = client.subscription_payment(SubscriptionPayment(
200
+ channel_reference="UYELIK-4471",
201
+ items=[
202
+ SubscriptionItem(channel_reference="PREMIUM-AYLIK"),
203
+ SubscriptionItem(channel_reference="EK-KULLANICI", quantity=3),
204
+ ],
205
+ success_url="https://magazam.com/tesekkurler",
206
+ customer=customer,
207
+ ))
208
+
209
+ subscription.token # aboneliği sonra sorgulamak ve iptal etmek için saklayın
210
+
211
+ return redirect(subscription.checkout_url)
212
+ ```
213
+
214
+ Bir kaleme `unit_amount` verirseniz o fiyat **yalnızca ilk dönem** için geçerlidir (ör. ilk ay yarı fiyat); sonraki dönemler ürünün kendi fiyatından çekilir.
215
+
216
+ İlk ödeme her zaman geçidin kendi sayfasında yapılır ve kart zorunlu olarak saklanır: sonraki dönemler o karttan çekilir. Ödeme tamamlanınca müşteri `success_url` adresinize döner ve sonucu yine `retrieve_payment()` ile sorarsınız; abonelik `active` olur ve aşağıdaki bildirim de gider.
217
+
218
+ Dönem bitince yeni dönem açılır ve müşterinin varsayılan kartından çekilir. Banka kabul etmezse çekim bir buçuk gün içinde beş kez denenir (araları 3, 6, 9 ve 12 saat); bu sırada abonelik `active` kalır. Beşinci deneme de olmazsa abonelik `past_due` olur; çalışma alanı yöneticilerinize e-posta, `webhook_url` adresinize bildirim gider. İkisi de o dönemin dilediği kartla ödenebileceği bağlantıyı taşır; bağlantıyı müşterinize siz iletirsiniz. Süre sınırı yoktur; müşteri ödediği anda abonelik kaldığı yerden devam eder.
219
+
220
+ Aboneliğin durumunu sorabilirsiniz:
221
+
222
+ ```python
223
+ from odemehub.request import RetrieveSubscription
224
+
225
+ subscription = client.retrieve_subscription(RetrieveSubscription(subscription_token=token))
226
+
227
+ subscription.status # pending | active | past_due | cancelled
228
+ subscription.amount # 149.90 — içinde bulunulan dönemin fiyatı
229
+ subscription.ends_at # sonraki tahsilat zamanı
230
+ subscription.checkout_url # ödenmemiş dönem varsa müşteriye verilecek adres
231
+
232
+ for item in subscription.items:
233
+ print(f"{item.quantity} x {item.name} ({item.channel_reference})")
234
+
235
+ if subscription.is_past_due():
236
+ ... # müşteriyi kendi ödeme sayfanızda uyarabilirsiniz
237
+ ```
238
+
239
+ Tutar, aboneliğin **içinde bulunduğu dönemin** fiyatıdır. Ürünün fiyatını yükseltirseniz yürüyen dönem çekildiği fiyatta kalır, yeni fiyat sonraki dönemden itibaren işler.
240
+
241
+ İptalde ödenmiş günler yanmaz:
242
+
243
+ ```python
244
+ from odemehub.request import CancelSubscription
245
+
246
+ subscription = client.cancel_subscription(CancelSubscription(subscription_token=token))
247
+
248
+ subscription.cancelled_at # iptal edildiği an
249
+ subscription.ends_at # hizmetin süreceği son gün
250
+ subscription.is_cancelled() # ödenmiş dönem sürüyorsa henüz False
251
+ ```
252
+
253
+ Müşteri, ödediği dönemin sonuna kadar hizmeti almaya devam eder; o güne kadar abonelik `active` görünür, dönem bitince `cancelled` olur ve bir daha tahsilat yapılmaz. Ödenmemiş bir aboneliğin (ilk ödemesi yapılmamış ya da `past_due`) iptali hemen geçerlidir. İade yapılmaz.
254
+
255
+ Aboneliğin açılabilmesi için varsayılan ödeme hesabınızın kart saklayabiliyor olması gerekir; saklamayan bir hesapla açmaya çalışırsanız istek `subscription.payment_provider_token` alanında reddedilir.
256
+
257
+ ### Abonelik bildirimleri (webhook)
258
+
259
+ Abonelik açarken `webhook_url` verirseniz, aboneliğin durumu her değiştiğinde o adrese imzalı bir POST gönderilir. Gövde düz JSON'dur ve imza `X-Signature` başlığındadır — yani geçidin API yanıtlarıyla aynı yöntem.
260
+
261
+ ```python
262
+ subscription = client.subscription_payment(SubscriptionPayment(
263
+ channel_reference="UYELIK-4471",
264
+ items=[SubscriptionItem(channel_reference="PREMIUM-AYLIK")],
265
+ success_url="https://magazam.com/tesekkurler",
266
+ customer=customer,
267
+ webhook_url="https://magazam.com/odemehub/abonelik",
268
+ ))
269
+ ```
270
+
271
+ Bildirimi karşılayan uçta gövdeyi **ham** okuyup imzayla birlikte SDK'ya verin. İmza gövdenin bayt bayt kendisini kapsar; gövdeyi ayrıştırıp yeniden yazarsanız imza tutmaz.
272
+
273
+ ```python
274
+ from odemehub import SignatureError
275
+
276
+ @app.post("/odemehub/abonelik")
277
+ def abonelik_bildirimi():
278
+ try:
279
+ webhook = client.subscription_webhook(
280
+ request.get_data(), # Django: request.body
281
+ request.headers.get("X-Signature"),
282
+ )
283
+ except SignatureError:
284
+ return "", 400
285
+
286
+ subscription = webhook.subscription # sorgudakiyle aynı nesne
287
+
288
+ if webhook.is_active():
289
+ abonelige_erisim_ac(subscription.channel_reference, subscription.ends_at)
290
+ elif webhook.is_past_due():
291
+ musteriyi_uyar(subscription.checkout_url)
292
+ elif webhook.is_cancelled():
293
+ yenilemeyi_durdur(subscription.ends_at)
294
+ elif webhook.is_ended():
295
+ erisimi_kapat(subscription.channel_reference)
296
+
297
+ return "", 200
298
+ ```
299
+
300
+ Gönderilen olaylar aboneliğin **durumudur**, yapılan işlem değil:
301
+
302
+ | Olay | Ne zaman gider |
303
+ | --- | --- |
304
+ | `active` | bir dönem ödendi (ilk ödeme ya da yenileme) |
305
+ | `past_due` | dönem kayıtlı karttan tahsil edilemedi, müşteriden bekleniyor |
306
+ | `cancelled` | abonelik iptal edildi; müşteri `ends_at` tarihine kadar hizmeti almaya devam eder |
307
+ | `ended` | ödenmiş dönem doldu, abonelik kapandı |
308
+
309
+ 2xx dışında bir yanıt (ya da yanıtsızlık) başarısız sayılır; bildirim 5 dakika sonra bir kez daha denenir. Ulaşmayan bildirimler panelde aboneliğin sayfasında HTTP kodu ve yanıtıyla listelenir.
310
+
311
+ ## Ödeme hangi hesaptan geçer
312
+
313
+ `payment_provider_token` verirseniz ödeme o hesaptan geçer; sipariş ve abonelik açarken de aynı alan vardır ve müşteri ödeme sayfasında o hesaptan öder. Vermezseniz hesabı çalışma alanınız seçer: panelde **Ödeme Ayarları → Gate (Yönlendirme)** altındaki kurallar sırayla denenir ve ödemenin karşıladığı ilk kural hesabı belirler. Kurallar kartın bankasına, şemasına, programına, tipine, ticari kart olup olmadığına, tutara ve para birimine bakabilir. Hiçbir kural tutmazsa ödeme varsayılan hesaptan geçer.
314
+
315
+ - Kuralın hesabı ödemeyi alamıyorsa (ödeme türünü ya da para birimini desteklemiyorsa) o kural atlanır.
316
+ - Kayıtlı kartla ödeme her zaman kartın saklandığı hesaptan geçer.
317
+ - Taksitleri `retrieve_bin()` ile gösteriyorsanız orada da hesap vermeyin: taksitler ödemenin gideceği hesaptan gelir ve çekilen tutar gösterdiğinizle aynı olur.
318
+
319
+ ## Kur çevirisi
320
+
321
+ Panelde **Ödeme Ayarları → Kur Çevirici** altında bir kural tanımladıysanız, o para biriminde gelen ödeme karttan kuralın para biriminde çekilir. Örneğin 100 USD istersiniz, karttan 4.985,56 TRY çekilir. Kur, TCMB'nin güncel döviz satış kuru ve üzerine eklediğiniz marjdır ya da sizin girdiğiniz sabit kurdur.
322
+
323
+ İsteğinizde hiçbir şey değişmez: tutarı ve para birimini her zamanki gibi gönderirsiniz. Yanıttaki `conversion` karttan ne çekildiğini söyler:
324
+
325
+ ```python
326
+ payment = client.regular_payment(RegularPayment(
327
+ amount="100.00",
328
+ currency="USD",
329
+ # ...
330
+ ))
331
+
332
+ if payment.conversion is not None:
333
+ payment.conversion.amount # 4985.56
334
+ payment.conversion.currency # TRY
335
+ payment.conversion.rate # 49.855560
336
+ ```
337
+
338
+ - Çevrilmeyen ödemede `conversion` `None` gelir. `retrieve_payment()` aynı bilgiyi yeniden verir.
339
+ - İade tutarını çekilen para biriminde gönderin (yukarıdaki örnekte TRY).
340
+ - Güncel kur alınamıyorsa ödeme alınmaz; `422` ile `transaction.currency` alanında hata döner. Birkaç dakika sonra tekrar deneyin.
341
+
342
+ ## Kart sorgusu ve taksitler
343
+
344
+ Kart numarasının ilk hanelerinden kartın kim tarafından verildiğini, hangi programa ait olduğunu ve tutarın kaç taksite bölünebileceğini sorar. Hiçbir şey çekilmez.
345
+
346
+ ```python
347
+ from odemehub.request import RetrieveBin
348
+
349
+ bin = client.retrieve_bin(RetrieveBin(bin="54003600", amount="450.00"))
350
+
351
+ if bin.result.successful:
352
+ bin.issuer_name # Garanti Bankası
353
+ bin.program # Bonus
354
+ bin.scheme # mastercard
355
+ bin.type # credit
356
+ bin.is_commercial
357
+
358
+ for installment in bin.installments:
359
+ # 3 taksitte ayda 157.87, toplam 473.60
360
+ print(f"{installment.number} x {installment.amount} = {installment.total}")
361
+ ```
362
+
363
+ Kartın tamamını göndermeyin; ilk 6-8 hane yeter ve yalnızca o kadarı kabul edilir.
364
+
365
+ Sorgu başarısız dönebilir: kart tanınmıyor olabilir ya da hesabınızın sağlayıcısı taksit vermiyor olabilir. İki durumda da satışı durdurmayın, tek çekimle devam edin.
366
+
367
+ ## Tutarlar ve taksit
368
+
369
+ İki tutar vardır ve karıştırılmamalıdır:
370
+
371
+ | Alan | Anlamı |
372
+ | --- | --- |
373
+ | `amount` | **Karttan çekilecek** tutar. Vade farkı varsa içindedir. |
374
+ | `base_amount` | **Sattığınız** tutar, vade farkından önceki hâli. Gönderilmezse `amount` ile aynı kabul edilir. |
375
+
376
+ Taksit yalnızca Türk Lirası ödemelerde yapılır. USD, EUR ya da GBP ödemede `installment_number` `1` olmalıdır ve `retrieve_bin()` taksit listesini boş döner; kur çevirisiyle TRY'den başka bir para birimine çekilen ödeme için de aynısı geçerlidir.
377
+
378
+ Taksitsiz satışta ikisi eşittir ve `base_amount` göndermenize gerek yoktur. Taksitli satışta `retrieve_bin` size o taksidin toplamını verir; onu `amount` olarak, sattığınız tutarı `base_amount` olarak gönderin:
379
+
380
+ ```python
381
+ payment = client.regular_payment(RegularPayment(
382
+ channel_reference="SIP-10234",
383
+ amount="473.60", # 3 taksitin toplamı
384
+ base_amount="450.00", # satılan tutar
385
+ installment_number=3,
386
+ # ...
387
+ ))
388
+ ```
389
+
390
+ Bazı sağlayıcılar vade farkını kendileri ekler; geçit bunu bilir ve gerekirse sağlayıcıya taban tutarı gönderir. Sizin tarafınızda değişen bir şey yoktur.
391
+
392
+ ## Kayıtlı kartlar
393
+
394
+ Müşterinin kartını saklayıp sonraki ödemelerde numara sormadan çekim yapabilirsiniz.
395
+
396
+ ```python
397
+ from odemehub.request import DefaultSavedCard, DeleteSavedCard, NamedCustomer, SaveCard, SavedCards
398
+
399
+ # Ödeme sırasında saklamak için: Card'a should_save=True verin.
400
+ # Ödeme olmadan saklamak için:
401
+ kept = client.save_card(SaveCard(customer=customer, card=card))
402
+
403
+ musteri = NamedCustomer(channel_reference="musteri-88")
404
+
405
+ # Müşterinin kartları
406
+ cards = client.saved_cards(SavedCards(customer=musteri))
407
+
408
+ # Varsayılan yapma / silme
409
+ token = cards.saved_cards[0].token
410
+
411
+ client.default_saved_card(DefaultSavedCard(customer=musteri, saved_card_token=token))
412
+ client.delete_saved_card(DeleteSavedCard(customer=musteri, saved_card_token=token))
413
+ ```
414
+
415
+ Kayıtlı kartla ödeme alırken `card` yerine kartın token'ını verin:
416
+
417
+ ```python
418
+ payment = client.regular_payment(RegularPayment(
419
+ channel_reference="SIP-10235",
420
+ amount="120.00",
421
+ installment_number=1,
422
+ ip=request.remote_addr,
423
+ customer=customer,
424
+ saved_card_token=token,
425
+ ))
426
+ ```
427
+
428
+ `card` ile `saved_card_token` birlikte ya da hiçbiri verilmezse istek nesnesi oluşturulurken `ValueError` fırlatılır.
429
+
430
+ Kart saklayan bir ödemenin yanıtında `payment.saved_card` dolu gelir; kartın token'ını oradan öğrenirsiniz. Kart her yerde token ile adlandırılır.
431
+
432
+ ## İade ve iptal
433
+
434
+ ```python
435
+ from odemehub.request import CancelPayment, RefundPayment
436
+
437
+ # Gün sonu almamış ödemenin tamamını geri alır
438
+ client.cancel_payment(CancelPayment(transaction_token=payment.transaction_token))
439
+
440
+ # Tutar verilirse kısmi, verilmezse kalanın tamamı iade edilir.
441
+ # Kur çevirisiyle çekilen ödemede tutar çekilen para birimindedir.
442
+ client.refund_payment(RefundPayment(transaction_token=payment.transaction_token, amount="100.00"))
443
+ ```
444
+
445
+ ## Hatalar
446
+
447
+ Bütün hatalar `OdemehubError`'dan türer; tek bir `except` hepsini yakalar.
448
+
449
+ | Hata | Ne demek |
450
+ | --- | --- |
451
+ | `ValidationError` | Gönderdiğiniz alanlar kabul edilmedi. Ödeme denenmedi. `error.errors` alan alan söyler. |
452
+ | `AuthenticationError` | API anahtarı bu takıma ait değil ya da imza gizli anahtarla tutmuyor. |
453
+ | `SignatureError` | Gelen yanıtın ya da bildirimin imzası tutmadı. Geçitten geldiği kanıtlanamaz; **işleme almayın**. |
454
+ | `TransportError` | Geçide ulaşılamadı ya da yanıt okunamadı. Ödemenin ne olduğu belirsizdir; geçitteki kayıt asıl doğruyu söyler. |
455
+ | `UnexpectedResponseError` | Beklenmeyen bir yanıt geldi. `error.status` HTTP kodunu verir. |
456
+
457
+ ```python
458
+ from odemehub import OdemehubError, ValidationError
459
+
460
+ try:
461
+ client.regular_payment(payment)
462
+ except ValidationError as error:
463
+ print(error.errors) # {'transaction.amount': ['...']}
464
+ except OdemehubError as error:
465
+ print(error)
466
+ ```
467
+
468
+ Ağ hatasında ödemeyi körlemesine tekrarlamayın: `TransportError` "olmadı" demek değil, "bilmiyorum" demektir.
469
+
470
+ ## İmzayı elle doğrulamak
471
+
472
+ İmza, gövdenin tam metninin gizli anahtarla HMAC-SHA256'sıdır, küçük harf hex olarak yazılır. SDK bunu `Signature` sınıfıyla yapar; aynı hesabı kendiniz de yapabilirsiniz:
473
+
474
+ ```python
475
+ import hashlib
476
+ import hmac
477
+
478
+ hmac.new(api_secret.encode(), body, hashlib.sha256).hexdigest()
479
+ ```
480
+
481
+ Test vektörü: `secret_test` anahtarıyla `{"a":1}` gövdesinin imzası `6d0c951564cdd2b6b70e75b214293a8cd2542815ba54fe91c7f6ce105bc3d592`'dir.