svc-infra 0.1.591__py3-none-any.whl → 0.1.593__py3-none-any.whl

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.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

@@ -0,0 +1,771 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from datetime import date, datetime, timezone
5
+ from typing import Any, Optional, Sequence, Tuple
6
+
7
+ from svc_infra.apf_payments.schemas import (
8
+ BalanceSnapshotOut,
9
+ CustomerOut,
10
+ CustomerUpsertIn,
11
+ DisputeOut,
12
+ IntentCreateIn,
13
+ IntentOut,
14
+ InvoiceCreateIn,
15
+ InvoiceLineItemIn,
16
+ InvoiceLineItemOut,
17
+ InvoiceOut,
18
+ NextAction,
19
+ PaymentMethodAttachIn,
20
+ PaymentMethodOut,
21
+ PaymentMethodUpdateIn,
22
+ PayoutOut,
23
+ PriceCreateIn,
24
+ PriceOut,
25
+ PriceUpdateIn,
26
+ ProductCreateIn,
27
+ ProductOut,
28
+ ProductUpdateIn,
29
+ RefundIn,
30
+ RefundOut,
31
+ SetupIntentCreateIn,
32
+ SetupIntentOut,
33
+ SubscriptionCreateIn,
34
+ SubscriptionOut,
35
+ SubscriptionUpdateIn,
36
+ UsageRecordIn,
37
+ UsageRecordListFilter,
38
+ UsageRecordOut,
39
+ )
40
+ from svc_infra.apf_payments.settings import get_payments_settings
41
+
42
+ from .base import ProviderAdapter
43
+
44
+ try: # pragma: no cover - optional dependency
45
+ import aiydan # type: ignore
46
+ except Exception: # pragma: no cover - handled at runtime
47
+ aiydan = None # type: ignore
48
+
49
+
50
+ async def _maybe_await(result: Any) -> Any:
51
+ if inspect.isawaitable(result):
52
+ return await result
53
+ return result
54
+
55
+
56
+ def _coerce_id(data: dict[str, Any], *candidates: str) -> str:
57
+ for key in candidates:
58
+ value = data.get(key)
59
+ if isinstance(value, str) and value:
60
+ return value
61
+ raise RuntimeError(f"Aiydan payload missing id fields: {candidates}")
62
+
63
+
64
+ def _ensure_utc_isoformat(value: Any) -> Optional[str]:
65
+ if value is None:
66
+ return None
67
+ if isinstance(value, str):
68
+ return value
69
+ if isinstance(value, datetime):
70
+ if value.tzinfo is None:
71
+ value = value.replace(tzinfo=timezone.utc)
72
+ return value.astimezone(timezone.utc).isoformat()
73
+ if isinstance(value, date):
74
+ return datetime(value.year, value.month, value.day, tzinfo=timezone.utc).isoformat()
75
+ try:
76
+ parsed = datetime.fromisoformat(str(value))
77
+ if parsed.tzinfo is None:
78
+ parsed = parsed.replace(tzinfo=timezone.utc)
79
+ return parsed.astimezone(timezone.utc).isoformat()
80
+ except Exception:
81
+ return str(value)
82
+
83
+
84
+ def _customer_to_out(data: dict[str, Any]) -> CustomerOut:
85
+ cust_id = _coerce_id(data, "provider_customer_id", "customer_id", "id")
86
+ return CustomerOut(
87
+ id=cust_id,
88
+ provider="aiydan",
89
+ provider_customer_id=cust_id,
90
+ email=data.get("email"),
91
+ name=data.get("name"),
92
+ )
93
+
94
+
95
+ def _intent_to_out(data: dict[str, Any]) -> IntentOut:
96
+ intent_id = _coerce_id(data, "provider_intent_id", "intent_id", "id")
97
+ return IntentOut(
98
+ id=intent_id,
99
+ provider="aiydan",
100
+ provider_intent_id=intent_id,
101
+ status=str(data.get("status", "")),
102
+ amount=int(data.get("amount", 0)),
103
+ currency=str(data.get("currency", "")).upper(),
104
+ client_secret=data.get("client_secret"),
105
+ next_action=NextAction(type=(data.get("next_action") or {}).get("type")),
106
+ )
107
+
108
+
109
+ def _payment_method_to_out(data: dict[str, Any]) -> PaymentMethodOut:
110
+ method_id = _coerce_id(data, "provider_method_id", "payment_method_id", "id")
111
+ card = data.get("card") or {}
112
+ return PaymentMethodOut(
113
+ id=method_id,
114
+ provider="aiydan",
115
+ provider_customer_id=str(data.get("provider_customer_id") or data.get("customer_id") or ""),
116
+ provider_method_id=method_id,
117
+ brand=card.get("brand") or data.get("brand"),
118
+ last4=card.get("last4") or data.get("last4"),
119
+ exp_month=card.get("exp_month") or data.get("exp_month"),
120
+ exp_year=card.get("exp_year") or data.get("exp_year"),
121
+ is_default=bool(data.get("is_default")),
122
+ )
123
+
124
+
125
+ def _product_to_out(data: dict[str, Any]) -> ProductOut:
126
+ product_id = _coerce_id(data, "provider_product_id", "product_id", "id")
127
+ return ProductOut(
128
+ id=product_id,
129
+ provider="aiydan",
130
+ provider_product_id=product_id,
131
+ name=str(data.get("name", "")),
132
+ active=bool(data.get("active", True)),
133
+ )
134
+
135
+
136
+ def _price_to_out(data: dict[str, Any]) -> PriceOut:
137
+ price_id = _coerce_id(data, "provider_price_id", "price_id", "id")
138
+ recurring = data.get("recurring") or {}
139
+ return PriceOut(
140
+ id=price_id,
141
+ provider="aiydan",
142
+ provider_price_id=price_id,
143
+ provider_product_id=str(
144
+ data.get("provider_product_id")
145
+ or data.get("product_id")
146
+ or getattr(data.get("product"), "id", "")
147
+ ),
148
+ currency=str(data.get("currency", "")).upper(),
149
+ unit_amount=int(data.get("unit_amount", data.get("amount", 0) or 0)),
150
+ interval=str(recurring.get("interval")) if recurring.get("interval") else None,
151
+ trial_days=data.get("trial_days"),
152
+ active=bool(data.get("active", True)),
153
+ )
154
+
155
+
156
+ def _subscription_to_out(data: dict[str, Any]) -> SubscriptionOut:
157
+ sub_id = _coerce_id(data, "provider_subscription_id", "subscription_id", "id")
158
+ items = data.get("items") or {}
159
+ first_item = None
160
+ if isinstance(items, dict):
161
+ first_item = (items.get("data") or [None])[0]
162
+ elif isinstance(items, Sequence):
163
+ first_item = items[0] if items else None
164
+ price_id = (
165
+ first_item.get("price")
166
+ if isinstance(first_item, dict)
167
+ else getattr(first_item, "price", None)
168
+ )
169
+ if isinstance(price_id, dict):
170
+ price_id = price_id.get("id")
171
+ elif price_id is not None and not isinstance(price_id, str):
172
+ price_id = getattr(price_id, "id", None)
173
+ quantity = (
174
+ first_item.get("quantity")
175
+ if isinstance(first_item, dict)
176
+ else getattr(first_item, "quantity", 0)
177
+ )
178
+ return SubscriptionOut(
179
+ id=sub_id,
180
+ provider="aiydan",
181
+ provider_subscription_id=sub_id,
182
+ provider_price_id=price_id or "",
183
+ status=str(data.get("status", "")),
184
+ quantity=int(quantity or 0),
185
+ cancel_at_period_end=bool(data.get("cancel_at_period_end", False)),
186
+ current_period_end=_ensure_utc_isoformat(data.get("current_period_end")),
187
+ )
188
+
189
+
190
+ def _invoice_to_out(data: dict[str, Any]) -> InvoiceOut:
191
+ invoice_id = _coerce_id(data, "provider_invoice_id", "invoice_id", "id")
192
+ return InvoiceOut(
193
+ id=invoice_id,
194
+ provider="aiydan",
195
+ provider_invoice_id=invoice_id,
196
+ provider_customer_id=str(data.get("provider_customer_id") or data.get("customer_id") or ""),
197
+ status=str(data.get("status", "")),
198
+ amount_due=int(data.get("amount_due", data.get("amount") or 0) or 0),
199
+ currency=str(data.get("currency", "")).upper(),
200
+ hosted_invoice_url=data.get("hosted_invoice_url") or data.get("hosted_url"),
201
+ pdf_url=data.get("pdf_url") or data.get("invoice_pdf"),
202
+ )
203
+
204
+
205
+ def _invoice_line_item_to_out(data: dict[str, Any]) -> InvoiceLineItemOut:
206
+ line_id = _coerce_id(data, "provider_invoice_line_item_id", "line_id", "id")
207
+ price = data.get("price") or {}
208
+ if not isinstance(price, dict):
209
+ price = {"id": getattr(price, "id", None)}
210
+ return InvoiceLineItemOut(
211
+ id=line_id,
212
+ description=data.get("description"),
213
+ currency=str(data.get("currency", price.get("currency", ""))).upper(),
214
+ quantity=int(data.get("quantity", 0) or 0),
215
+ unit_amount=int(data.get("unit_amount", data.get("amount", 0) or 0)),
216
+ provider_price_id=price.get("id"),
217
+ )
218
+
219
+
220
+ def _refund_to_out(data: dict[str, Any]) -> RefundOut:
221
+ refund_id = _coerce_id(data, "provider_refund_id", "refund_id", "id")
222
+ return RefundOut(
223
+ id=refund_id,
224
+ provider="aiydan",
225
+ provider_refund_id=refund_id,
226
+ provider_payment_intent_id=str(
227
+ data.get("provider_payment_intent_id") or data.get("payment_intent_id") or ""
228
+ ),
229
+ amount=int(data.get("amount", 0) or 0),
230
+ currency=str(data.get("currency", "")).upper(),
231
+ status=str(data.get("status", "")),
232
+ reason=data.get("reason"),
233
+ created_at=_ensure_utc_isoformat(data.get("created_at") or data.get("created")),
234
+ )
235
+
236
+
237
+ def _dispute_to_out(data: dict[str, Any]) -> DisputeOut:
238
+ dispute_id = _coerce_id(data, "provider_dispute_id", "dispute_id", "id")
239
+ evidence = data.get("evidence") or {}
240
+ return DisputeOut(
241
+ id=dispute_id,
242
+ provider="aiydan",
243
+ provider_dispute_id=dispute_id,
244
+ amount=int(data.get("amount", 0) or 0),
245
+ currency=str(data.get("currency", "")).upper(),
246
+ reason=data.get("reason"),
247
+ status=str(data.get("status", "")),
248
+ evidence_due_by=_ensure_utc_isoformat(
249
+ evidence.get("due_by") or data.get("evidence_due_by")
250
+ ),
251
+ created_at=_ensure_utc_isoformat(data.get("created_at") or data.get("created")),
252
+ )
253
+
254
+
255
+ def _payout_to_out(data: dict[str, Any]) -> PayoutOut:
256
+ payout_id = _coerce_id(data, "provider_payout_id", "payout_id", "id")
257
+ return PayoutOut(
258
+ id=payout_id,
259
+ provider="aiydan",
260
+ provider_payout_id=payout_id,
261
+ amount=int(data.get("amount", 0) or 0),
262
+ currency=str(data.get("currency", "")).upper(),
263
+ status=str(data.get("status", "")),
264
+ arrival_date=_ensure_utc_isoformat(data.get("arrival_date")),
265
+ type=data.get("type"),
266
+ )
267
+
268
+
269
+ def _usage_record_to_out(data: dict[str, Any]) -> UsageRecordOut:
270
+ return UsageRecordOut(
271
+ id=str(data.get("id")),
272
+ quantity=int(data.get("quantity", 0) or 0),
273
+ timestamp=data.get("timestamp"),
274
+ subscription_item=(
275
+ str(data.get("subscription_item")) if data.get("subscription_item") else None
276
+ ),
277
+ provider_price_id=(
278
+ str(data.get("provider_price_id")) if data.get("provider_price_id") else None
279
+ ),
280
+ )
281
+
282
+
283
+ def _balance_snapshot_to_out(data: dict[str, Any]) -> BalanceSnapshotOut:
284
+ return BalanceSnapshotOut(
285
+ available=data.get("available", {}),
286
+ pending=data.get("pending", {}),
287
+ )
288
+
289
+
290
+ def _ensure_sequence(result: Any) -> Sequence[dict[str, Any]]:
291
+ if isinstance(result, Sequence):
292
+ return result # type: ignore[arg-type]
293
+ if isinstance(result, dict):
294
+ items = result.get("items")
295
+ if isinstance(items, Sequence):
296
+ return items # type: ignore[arg-type]
297
+ raise RuntimeError("Expected sequence payload from Aiydan client")
298
+
299
+
300
+ def _ensure_list_response(result: Any) -> Tuple[Sequence[dict[str, Any]], Optional[str]]:
301
+ if isinstance(result, tuple) and len(result) == 2:
302
+ items, cursor = result
303
+ if isinstance(items, Sequence) or items is None:
304
+ return (items or []), cursor # type: ignore[arg-type]
305
+ if isinstance(result, dict):
306
+ items = result.get("items")
307
+ cursor = result.get("next_cursor") or result.get("cursor")
308
+ if isinstance(items, Sequence):
309
+ return items, cursor
310
+ if isinstance(result, Sequence):
311
+ return result, None # type: ignore[arg-type]
312
+ raise RuntimeError("Expected iterable response from Aiydan client")
313
+
314
+
315
+ class AiydanAdapter(ProviderAdapter):
316
+ name = "aiydan"
317
+
318
+ def __init__(self, *, client: Optional[Any] = None):
319
+ settings = get_payments_settings()
320
+ cfg = settings.aiydan
321
+ if client is not None:
322
+ self._client = client
323
+ self._webhook_secret = (
324
+ cfg.webhook_secret.get_secret_value() if cfg and cfg.webhook_secret else None
325
+ )
326
+ return
327
+ if cfg is None:
328
+ raise RuntimeError("Aiydan settings not configured")
329
+ if aiydan is None:
330
+ raise RuntimeError("aiydan SDK is not installed. pip install aiydan")
331
+ client_class = getattr(aiydan, "Client", None)
332
+ if client_class is None:
333
+ raise RuntimeError("aiydan SDK missing 'Client' class")
334
+ kwargs: dict[str, Any] = {"api_key": cfg.api_key.get_secret_value()}
335
+ if cfg.client_key:
336
+ kwargs["client_key"] = cfg.client_key.get_secret_value()
337
+ if cfg.merchant_account:
338
+ kwargs["merchant_account"] = cfg.merchant_account
339
+ if cfg.hmac_key:
340
+ kwargs["hmac_key"] = cfg.hmac_key.get_secret_value()
341
+ if cfg.base_url:
342
+ kwargs["base_url"] = cfg.base_url
343
+ self._client = client_class(**kwargs)
344
+ self._webhook_secret = cfg.webhook_secret.get_secret_value() if cfg.webhook_secret else None
345
+
346
+ async def ensure_customer(self, data: CustomerUpsertIn) -> CustomerOut:
347
+ payload = data.model_dump(exclude_none=True)
348
+ result = await _maybe_await(self._client.ensure_customer(payload))
349
+ return _customer_to_out(result)
350
+
351
+ async def attach_payment_method(self, data: PaymentMethodAttachIn) -> PaymentMethodOut:
352
+ payload = data.model_dump(exclude_none=True)
353
+ result = await _maybe_await(self._client.attach_payment_method(payload))
354
+ return _payment_method_to_out(result)
355
+
356
+ async def list_payment_methods(self, provider_customer_id: str) -> list[PaymentMethodOut]:
357
+ result = await _maybe_await(self._client.list_payment_methods(provider_customer_id))
358
+ methods = _ensure_sequence(result)
359
+ return [_payment_method_to_out(method) for method in methods]
360
+
361
+ async def detach_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
362
+ result = await _maybe_await(self._client.detach_payment_method(provider_method_id))
363
+ return _payment_method_to_out(result)
364
+
365
+ async def set_default_payment_method(
366
+ self, provider_customer_id: str, provider_method_id: str
367
+ ) -> PaymentMethodOut:
368
+ result = await _maybe_await(
369
+ self._client.set_default_payment_method(provider_customer_id, provider_method_id)
370
+ )
371
+ return _payment_method_to_out(result)
372
+
373
+ async def get_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
374
+ result = await _maybe_await(self._client.get_payment_method(provider_method_id))
375
+ return _payment_method_to_out(result)
376
+
377
+ async def update_payment_method(
378
+ self, provider_method_id: str, data: PaymentMethodUpdateIn
379
+ ) -> PaymentMethodOut:
380
+ payload = data.model_dump(exclude_none=True)
381
+ result = await _maybe_await(self._client.update_payment_method(provider_method_id, payload))
382
+ return _payment_method_to_out(result)
383
+
384
+ async def create_product(self, data: ProductCreateIn) -> ProductOut:
385
+ payload = data.model_dump(exclude_none=True)
386
+ result = await _maybe_await(self._client.create_product(payload))
387
+ return _product_to_out(result)
388
+
389
+ async def get_product(self, provider_product_id: str) -> ProductOut:
390
+ result = await _maybe_await(self._client.get_product(provider_product_id))
391
+ return _product_to_out(result)
392
+
393
+ async def list_products(
394
+ self, *, active: bool | None, limit: int, cursor: str | None
395
+ ) -> tuple[list[ProductOut], str | None]:
396
+ result = await _maybe_await(
397
+ self._client.list_products(active=active, limit=limit, cursor=cursor)
398
+ )
399
+ items, next_cursor = _ensure_list_response(result)
400
+ return [_product_to_out(item) for item in items], next_cursor
401
+
402
+ async def update_product(self, provider_product_id: str, data: ProductUpdateIn) -> ProductOut:
403
+ payload = data.model_dump(exclude_none=True)
404
+ result = await _maybe_await(self._client.update_product(provider_product_id, payload))
405
+ return _product_to_out(result)
406
+
407
+ async def create_price(self, data: PriceCreateIn) -> PriceOut:
408
+ payload = data.model_dump(exclude_none=True)
409
+ result = await _maybe_await(self._client.create_price(payload))
410
+ return _price_to_out(result)
411
+
412
+ async def get_price(self, provider_price_id: str) -> PriceOut:
413
+ result = await _maybe_await(self._client.get_price(provider_price_id))
414
+ return _price_to_out(result)
415
+
416
+ async def list_prices(
417
+ self,
418
+ *,
419
+ provider_product_id: str | None,
420
+ active: bool | None,
421
+ limit: int,
422
+ cursor: str | None,
423
+ ) -> tuple[list[PriceOut], str | None]:
424
+ result = await _maybe_await(
425
+ self._client.list_prices(
426
+ provider_product_id=provider_product_id,
427
+ active=active,
428
+ limit=limit,
429
+ cursor=cursor,
430
+ )
431
+ )
432
+ items, next_cursor = _ensure_list_response(result)
433
+ return [_price_to_out(item) for item in items], next_cursor
434
+
435
+ async def update_price(self, provider_price_id: str, data: PriceUpdateIn) -> PriceOut:
436
+ payload = data.model_dump(exclude_none=True)
437
+ result = await _maybe_await(self._client.update_price(provider_price_id, payload))
438
+ return _price_to_out(result)
439
+
440
+ async def create_subscription(self, data: SubscriptionCreateIn) -> SubscriptionOut:
441
+ payload = data.model_dump(exclude_none=True)
442
+ result = await _maybe_await(self._client.create_subscription(payload))
443
+ return _subscription_to_out(result)
444
+
445
+ async def update_subscription(
446
+ self, provider_subscription_id: str, data: SubscriptionUpdateIn
447
+ ) -> SubscriptionOut:
448
+ payload = data.model_dump(exclude_none=True)
449
+ result = await _maybe_await(
450
+ self._client.update_subscription(provider_subscription_id, payload)
451
+ )
452
+ return _subscription_to_out(result)
453
+
454
+ async def cancel_subscription(
455
+ self, provider_subscription_id: str, at_period_end: bool = True
456
+ ) -> SubscriptionOut:
457
+ result = await _maybe_await(
458
+ self._client.cancel_subscription(provider_subscription_id, at_period_end)
459
+ )
460
+ return _subscription_to_out(result)
461
+
462
+ async def get_subscription(self, provider_subscription_id: str) -> SubscriptionOut:
463
+ result = await _maybe_await(self._client.get_subscription(provider_subscription_id))
464
+ return _subscription_to_out(result)
465
+
466
+ async def list_subscriptions(
467
+ self,
468
+ *,
469
+ customer_provider_id: str | None,
470
+ status: str | None,
471
+ limit: int,
472
+ cursor: str | None,
473
+ ) -> tuple[list[SubscriptionOut], str | None]:
474
+ result = await _maybe_await(
475
+ self._client.list_subscriptions(
476
+ customer_provider_id=customer_provider_id,
477
+ status=status,
478
+ limit=limit,
479
+ cursor=cursor,
480
+ )
481
+ )
482
+ items, next_cursor = _ensure_list_response(result)
483
+ return [_subscription_to_out(item) for item in items], next_cursor
484
+
485
+ async def create_invoice(self, data: InvoiceCreateIn) -> InvoiceOut:
486
+ payload = data.model_dump(exclude_none=True)
487
+ result = await _maybe_await(self._client.create_invoice(payload))
488
+ return _invoice_to_out(result)
489
+
490
+ async def finalize_invoice(self, provider_invoice_id: str) -> InvoiceOut:
491
+ result = await _maybe_await(self._client.finalize_invoice(provider_invoice_id))
492
+ return _invoice_to_out(result)
493
+
494
+ async def void_invoice(self, provider_invoice_id: str) -> InvoiceOut:
495
+ result = await _maybe_await(self._client.void_invoice(provider_invoice_id))
496
+ return _invoice_to_out(result)
497
+
498
+ async def pay_invoice(self, provider_invoice_id: str) -> InvoiceOut:
499
+ result = await _maybe_await(self._client.pay_invoice(provider_invoice_id))
500
+ return _invoice_to_out(result)
501
+
502
+ async def add_invoice_line_item(
503
+ self, provider_invoice_id: str, data: InvoiceLineItemIn
504
+ ) -> InvoiceOut:
505
+ payload = data.model_dump(exclude_none=True)
506
+ result = await _maybe_await(
507
+ self._client.add_invoice_line_item(provider_invoice_id, payload)
508
+ )
509
+ return _invoice_to_out(result)
510
+
511
+ async def list_invoices(
512
+ self,
513
+ *,
514
+ customer_provider_id: str | None,
515
+ status: str | None,
516
+ limit: int,
517
+ cursor: str | None,
518
+ ) -> tuple[list[InvoiceOut], str | None]:
519
+ result = await _maybe_await(
520
+ self._client.list_invoices(
521
+ customer_provider_id=customer_provider_id,
522
+ status=status,
523
+ limit=limit,
524
+ cursor=cursor,
525
+ )
526
+ )
527
+ items, next_cursor = _ensure_list_response(result)
528
+ return [_invoice_to_out(item) for item in items], next_cursor
529
+
530
+ async def get_invoice(self, provider_invoice_id: str) -> InvoiceOut:
531
+ result = await _maybe_await(self._client.get_invoice(provider_invoice_id))
532
+ return _invoice_to_out(result)
533
+
534
+ async def preview_invoice(
535
+ self, *, customer_provider_id: str, subscription_id: str | None = None
536
+ ) -> InvoiceOut:
537
+ result = await _maybe_await(
538
+ self._client.preview_invoice(
539
+ customer_provider_id=customer_provider_id,
540
+ subscription_id=subscription_id,
541
+ )
542
+ )
543
+ return _invoice_to_out(result)
544
+
545
+ async def list_invoice_line_items(
546
+ self, provider_invoice_id: str, *, limit: int, cursor: str | None
547
+ ) -> tuple[list[InvoiceLineItemOut], str | None]:
548
+ result = await _maybe_await(
549
+ self._client.list_invoice_line_items(
550
+ provider_invoice_id,
551
+ limit=limit,
552
+ cursor=cursor,
553
+ )
554
+ )
555
+ items, next_cursor = _ensure_list_response(result)
556
+ return [_invoice_line_item_to_out(item) for item in items], next_cursor
557
+
558
+ async def create_intent(self, data: IntentCreateIn, *, user_id: str | None) -> IntentOut:
559
+ payload = data.model_dump(exclude_none=True)
560
+ if user_id is not None:
561
+ payload["user_id"] = user_id
562
+ result = await _maybe_await(self._client.create_intent(payload))
563
+ return _intent_to_out(result)
564
+
565
+ async def confirm_intent(self, provider_intent_id: str) -> IntentOut:
566
+ result = await _maybe_await(self._client.confirm_intent(provider_intent_id))
567
+ return _intent_to_out(result)
568
+
569
+ async def cancel_intent(self, provider_intent_id: str) -> IntentOut:
570
+ result = await _maybe_await(self._client.cancel_intent(provider_intent_id))
571
+ return _intent_to_out(result)
572
+
573
+ async def refund(self, provider_intent_id: str, data: RefundIn) -> IntentOut:
574
+ payload = data.model_dump(exclude_none=True)
575
+ result = await _maybe_await(self._client.refund_intent(provider_intent_id, payload))
576
+ return _intent_to_out(result)
577
+
578
+ async def hydrate_intent(self, provider_intent_id: str) -> IntentOut:
579
+ result = await _maybe_await(self._client.get_intent(provider_intent_id))
580
+ return _intent_to_out(result)
581
+
582
+ async def capture_intent(self, provider_intent_id: str, *, amount: int | None) -> IntentOut:
583
+ result = await _maybe_await(self._client.capture_intent(provider_intent_id, amount=amount))
584
+ return _intent_to_out(result)
585
+
586
+ async def list_intents(
587
+ self,
588
+ *,
589
+ customer_provider_id: str | None,
590
+ status: str | None,
591
+ limit: int,
592
+ cursor: str | None,
593
+ ) -> tuple[list[IntentOut], str | None]:
594
+ result = await _maybe_await(
595
+ self._client.list_intents(
596
+ customer_provider_id=customer_provider_id,
597
+ status=status,
598
+ limit=limit,
599
+ cursor=cursor,
600
+ )
601
+ )
602
+ items, next_cursor = _ensure_list_response(result)
603
+ return [_intent_to_out(item) for item in items], next_cursor
604
+
605
+ async def verify_and_parse_webhook(
606
+ self, signature: str | None, payload: bytes
607
+ ) -> dict[str, Any]:
608
+ if hasattr(self._client, "verify_and_parse_webhook"):
609
+ result = await _maybe_await(
610
+ self._client.verify_and_parse_webhook(
611
+ signature=signature,
612
+ payload=payload,
613
+ secret=self._webhook_secret,
614
+ )
615
+ )
616
+ elif hasattr(self._client, "verify_webhook"):
617
+ result = await _maybe_await(
618
+ self._client.verify_webhook(
619
+ payload=payload,
620
+ signature=signature,
621
+ secret=self._webhook_secret,
622
+ )
623
+ )
624
+ else:
625
+ raise RuntimeError("Aiydan client missing webhook verification method")
626
+ if not isinstance(result, dict):
627
+ raise RuntimeError("Aiydan client returned unexpected webhook payload")
628
+ return result
629
+
630
+ async def list_disputes(
631
+ self, *, status: str | None, limit: int, cursor: str | None
632
+ ) -> tuple[list[DisputeOut], str | None]:
633
+ result = await _maybe_await(
634
+ self._client.list_disputes(status=status, limit=limit, cursor=cursor)
635
+ )
636
+ items, next_cursor = _ensure_list_response(result)
637
+ return [_dispute_to_out(item) for item in items], next_cursor
638
+
639
+ async def get_dispute(self, provider_dispute_id: str) -> DisputeOut:
640
+ result = await _maybe_await(self._client.get_dispute(provider_dispute_id))
641
+ return _dispute_to_out(result)
642
+
643
+ async def submit_dispute_evidence(self, provider_dispute_id: str, evidence: dict) -> DisputeOut:
644
+ result = await _maybe_await(
645
+ self._client.submit_dispute_evidence(provider_dispute_id, evidence)
646
+ )
647
+ return _dispute_to_out(result)
648
+
649
+ async def get_balance_snapshot(self) -> BalanceSnapshotOut:
650
+ result = await _maybe_await(self._client.get_balance_snapshot())
651
+ if isinstance(result, BalanceSnapshotOut):
652
+ return result
653
+ if not isinstance(result, dict):
654
+ raise RuntimeError("Aiydan client returned unexpected balance payload")
655
+ return _balance_snapshot_to_out(result)
656
+
657
+ async def list_payouts(
658
+ self, *, limit: int, cursor: str | None
659
+ ) -> tuple[list[PayoutOut], str | None]:
660
+ result = await _maybe_await(self._client.list_payouts(limit=limit, cursor=cursor))
661
+ items, next_cursor = _ensure_list_response(result)
662
+ return [_payout_to_out(item) for item in items], next_cursor
663
+
664
+ async def get_payout(self, provider_payout_id: str) -> PayoutOut:
665
+ result = await _maybe_await(self._client.get_payout(provider_payout_id))
666
+ return _payout_to_out(result)
667
+
668
+ async def list_refunds(
669
+ self,
670
+ *,
671
+ provider_payment_intent_id: str | None,
672
+ limit: int,
673
+ cursor: str | None,
674
+ ) -> tuple[list[RefundOut], str | None]:
675
+ result = await _maybe_await(
676
+ self._client.list_refunds(
677
+ provider_payment_intent_id=provider_payment_intent_id,
678
+ limit=limit,
679
+ cursor=cursor,
680
+ )
681
+ )
682
+ items, next_cursor = _ensure_list_response(result)
683
+ return [_refund_to_out(item) for item in items], next_cursor
684
+
685
+ async def get_refund(self, provider_refund_id: str) -> RefundOut:
686
+ result = await _maybe_await(self._client.get_refund(provider_refund_id))
687
+ return _refund_to_out(result)
688
+
689
+ async def create_usage_record(self, data: UsageRecordIn) -> UsageRecordOut:
690
+ payload = data.model_dump(exclude_none=True)
691
+ result = await _maybe_await(self._client.create_usage_record(payload))
692
+ return _usage_record_to_out(result)
693
+
694
+ async def list_usage_records(
695
+ self, f: UsageRecordListFilter
696
+ ) -> tuple[list[UsageRecordOut], str | None]:
697
+ payload = f.model_dump(exclude_none=True)
698
+ result = await _maybe_await(self._client.list_usage_records(payload))
699
+ items, next_cursor = _ensure_list_response(result)
700
+ return [_usage_record_to_out(item) for item in items], next_cursor
701
+
702
+ async def get_usage_record(self, usage_record_id: str) -> UsageRecordOut:
703
+ result = await _maybe_await(self._client.get_usage_record(usage_record_id))
704
+ return _usage_record_to_out(result)
705
+
706
+ async def create_setup_intent(self, data: SetupIntentCreateIn) -> SetupIntentOut:
707
+ payload = data.model_dump(exclude_none=True)
708
+ result = await _maybe_await(self._client.create_setup_intent(payload))
709
+ return SetupIntentOut(
710
+ id=_coerce_id(result, "provider_setup_intent_id", "setup_intent_id", "id"),
711
+ provider="aiydan",
712
+ provider_setup_intent_id=_coerce_id(
713
+ result, "provider_setup_intent_id", "setup_intent_id", "id"
714
+ ),
715
+ status=str(result.get("status", "")),
716
+ client_secret=result.get("client_secret"),
717
+ next_action=NextAction(type=(result.get("next_action") or {}).get("type")),
718
+ )
719
+
720
+ async def confirm_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
721
+ result = await _maybe_await(self._client.confirm_setup_intent(provider_setup_intent_id))
722
+ return SetupIntentOut(
723
+ id=_coerce_id(result, "provider_setup_intent_id", "setup_intent_id", "id"),
724
+ provider="aiydan",
725
+ provider_setup_intent_id=_coerce_id(
726
+ result, "provider_setup_intent_id", "setup_intent_id", "id"
727
+ ),
728
+ status=str(result.get("status", "")),
729
+ client_secret=result.get("client_secret"),
730
+ next_action=NextAction(type=(result.get("next_action") or {}).get("type")),
731
+ )
732
+
733
+ async def get_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
734
+ result = await _maybe_await(self._client.get_setup_intent(provider_setup_intent_id))
735
+ return SetupIntentOut(
736
+ id=_coerce_id(result, "provider_setup_intent_id", "setup_intent_id", "id"),
737
+ provider="aiydan",
738
+ provider_setup_intent_id=_coerce_id(
739
+ result, "provider_setup_intent_id", "setup_intent_id", "id"
740
+ ),
741
+ status=str(result.get("status", "")),
742
+ client_secret=result.get("client_secret"),
743
+ next_action=NextAction(type=(result.get("next_action") or {}).get("type")),
744
+ )
745
+
746
+ async def resume_intent_after_action(self, provider_intent_id: str) -> IntentOut:
747
+ if hasattr(self._client, "resume_intent_after_action"):
748
+ result = await _maybe_await(self._client.resume_intent_after_action(provider_intent_id))
749
+ else:
750
+ result = await _maybe_await(self._client.get_intent(provider_intent_id))
751
+ return _intent_to_out(result)
752
+
753
+ async def list_customers(
754
+ self, *, provider: str | None, user_id: str | None, limit: int, cursor: str | None
755
+ ) -> tuple[list[CustomerOut], str | None]:
756
+ result = await _maybe_await(
757
+ self._client.list_customers(
758
+ provider=provider,
759
+ user_id=user_id,
760
+ limit=limit,
761
+ cursor=cursor,
762
+ )
763
+ )
764
+ items, next_cursor = _ensure_list_response(result)
765
+ return [_customer_to_out(item) for item in items], next_cursor
766
+
767
+ async def get_customer(self, provider_customer_id: str) -> Optional[CustomerOut]:
768
+ result = await _maybe_await(self._client.get_customer(provider_customer_id))
769
+ if result is None:
770
+ return None
771
+ return _customer_to_out(result)