svc-infra 0.1.589__py3-none-any.whl → 0.1.706__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.

Files changed (260) hide show
  1. svc_infra/__init__.py +58 -2
  2. svc_infra/apf_payments/README.md +732 -0
  3. svc_infra/apf_payments/models.py +133 -42
  4. svc_infra/apf_payments/provider/__init__.py +4 -0
  5. svc_infra/apf_payments/provider/aiydan.py +871 -0
  6. svc_infra/apf_payments/provider/base.py +30 -9
  7. svc_infra/apf_payments/provider/stripe.py +156 -62
  8. svc_infra/apf_payments/schemas.py +19 -10
  9. svc_infra/apf_payments/service.py +211 -68
  10. svc_infra/apf_payments/settings.py +27 -3
  11. svc_infra/api/__init__.py +61 -0
  12. svc_infra/api/fastapi/__init__.py +15 -0
  13. svc_infra/api/fastapi/admin/__init__.py +3 -0
  14. svc_infra/api/fastapi/admin/add.py +245 -0
  15. svc_infra/api/fastapi/apf_payments/router.py +145 -46
  16. svc_infra/api/fastapi/apf_payments/setup.py +26 -8
  17. svc_infra/api/fastapi/auth/__init__.py +65 -0
  18. svc_infra/api/fastapi/auth/_cookies.py +6 -2
  19. svc_infra/api/fastapi/auth/add.py +27 -14
  20. svc_infra/api/fastapi/auth/gaurd.py +104 -13
  21. svc_infra/api/fastapi/auth/mfa/models.py +3 -1
  22. svc_infra/api/fastapi/auth/mfa/pre_auth.py +10 -6
  23. svc_infra/api/fastapi/auth/mfa/router.py +15 -8
  24. svc_infra/api/fastapi/auth/mfa/security.py +1 -2
  25. svc_infra/api/fastapi/auth/mfa/utils.py +2 -1
  26. svc_infra/api/fastapi/auth/mfa/verify.py +9 -2
  27. svc_infra/api/fastapi/auth/policy.py +0 -1
  28. svc_infra/api/fastapi/auth/providers.py +3 -1
  29. svc_infra/api/fastapi/auth/routers/apikey_router.py +6 -6
  30. svc_infra/api/fastapi/auth/routers/oauth_router.py +214 -75
  31. svc_infra/api/fastapi/auth/routers/session_router.py +67 -0
  32. svc_infra/api/fastapi/auth/security.py +31 -10
  33. svc_infra/api/fastapi/auth/sender.py +8 -1
  34. svc_infra/api/fastapi/auth/settings.py +2 -0
  35. svc_infra/api/fastapi/auth/state.py +3 -1
  36. svc_infra/api/fastapi/auth/ws_security.py +275 -0
  37. svc_infra/api/fastapi/billing/router.py +73 -0
  38. svc_infra/api/fastapi/billing/setup.py +19 -0
  39. svc_infra/api/fastapi/cache/add.py +9 -5
  40. svc_infra/api/fastapi/db/__init__.py +5 -1
  41. svc_infra/api/fastapi/db/http.py +3 -1
  42. svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
  43. svc_infra/api/fastapi/db/nosql/mongo/add.py +47 -32
  44. svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +30 -11
  45. svc_infra/api/fastapi/db/sql/__init__.py +5 -1
  46. svc_infra/api/fastapi/db/sql/add.py +71 -26
  47. svc_infra/api/fastapi/db/sql/crud_router.py +210 -22
  48. svc_infra/api/fastapi/db/sql/health.py +3 -1
  49. svc_infra/api/fastapi/db/sql/session.py +18 -0
  50. svc_infra/api/fastapi/db/sql/users.py +29 -5
  51. svc_infra/api/fastapi/dependencies/ratelimit.py +130 -0
  52. svc_infra/api/fastapi/docs/add.py +173 -0
  53. svc_infra/api/fastapi/docs/landing.py +4 -2
  54. svc_infra/api/fastapi/docs/scoped.py +62 -15
  55. svc_infra/api/fastapi/dual/__init__.py +12 -2
  56. svc_infra/api/fastapi/dual/dualize.py +1 -1
  57. svc_infra/api/fastapi/dual/protected.py +126 -4
  58. svc_infra/api/fastapi/dual/public.py +25 -0
  59. svc_infra/api/fastapi/dual/router.py +40 -13
  60. svc_infra/api/fastapi/dx.py +33 -2
  61. svc_infra/api/fastapi/ease.py +10 -2
  62. svc_infra/api/fastapi/http/concurrency.py +2 -1
  63. svc_infra/api/fastapi/http/conditional.py +3 -1
  64. svc_infra/api/fastapi/middleware/debug.py +4 -1
  65. svc_infra/api/fastapi/middleware/errors/catchall.py +6 -2
  66. svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -1
  67. svc_infra/api/fastapi/middleware/errors/handlers.py +54 -8
  68. svc_infra/api/fastapi/middleware/graceful_shutdown.py +104 -0
  69. svc_infra/api/fastapi/middleware/idempotency.py +197 -70
  70. svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
  71. svc_infra/api/fastapi/middleware/optimistic_lock.py +42 -0
  72. svc_infra/api/fastapi/middleware/ratelimit.py +143 -31
  73. svc_infra/api/fastapi/middleware/ratelimit_store.py +111 -0
  74. svc_infra/api/fastapi/middleware/request_id.py +27 -11
  75. svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
  76. svc_infra/api/fastapi/middleware/timeout.py +177 -0
  77. svc_infra/api/fastapi/openapi/apply.py +5 -3
  78. svc_infra/api/fastapi/openapi/conventions.py +9 -2
  79. svc_infra/api/fastapi/openapi/mutators.py +165 -20
  80. svc_infra/api/fastapi/openapi/pipeline.py +1 -1
  81. svc_infra/api/fastapi/openapi/security.py +3 -1
  82. svc_infra/api/fastapi/ops/add.py +75 -0
  83. svc_infra/api/fastapi/pagination.py +47 -20
  84. svc_infra/api/fastapi/routers/__init__.py +43 -15
  85. svc_infra/api/fastapi/routers/ping.py +1 -0
  86. svc_infra/api/fastapi/setup.py +188 -56
  87. svc_infra/api/fastapi/tenancy/add.py +19 -0
  88. svc_infra/api/fastapi/tenancy/context.py +112 -0
  89. svc_infra/api/fastapi/versioned.py +101 -0
  90. svc_infra/app/README.md +5 -5
  91. svc_infra/app/__init__.py +3 -1
  92. svc_infra/app/env.py +69 -1
  93. svc_infra/app/logging/add.py +9 -2
  94. svc_infra/app/logging/formats.py +12 -5
  95. svc_infra/billing/__init__.py +23 -0
  96. svc_infra/billing/async_service.py +147 -0
  97. svc_infra/billing/jobs.py +241 -0
  98. svc_infra/billing/models.py +177 -0
  99. svc_infra/billing/quotas.py +103 -0
  100. svc_infra/billing/schemas.py +36 -0
  101. svc_infra/billing/service.py +123 -0
  102. svc_infra/bundled_docs/README.md +5 -0
  103. svc_infra/bundled_docs/__init__.py +1 -0
  104. svc_infra/bundled_docs/getting-started.md +6 -0
  105. svc_infra/cache/__init__.py +9 -0
  106. svc_infra/cache/add.py +170 -0
  107. svc_infra/cache/backend.py +7 -6
  108. svc_infra/cache/decorators.py +81 -15
  109. svc_infra/cache/demo.py +2 -2
  110. svc_infra/cache/keys.py +24 -4
  111. svc_infra/cache/recache.py +26 -14
  112. svc_infra/cache/resources.py +14 -5
  113. svc_infra/cache/tags.py +19 -44
  114. svc_infra/cache/utils.py +3 -1
  115. svc_infra/cli/__init__.py +52 -8
  116. svc_infra/cli/__main__.py +4 -0
  117. svc_infra/cli/cmds/__init__.py +39 -2
  118. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +7 -4
  119. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +7 -5
  120. svc_infra/cli/cmds/db/ops_cmds.py +270 -0
  121. svc_infra/cli/cmds/db/sql/alembic_cmds.py +103 -18
  122. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +88 -0
  123. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
  124. svc_infra/cli/cmds/docs/docs_cmds.py +142 -0
  125. svc_infra/cli/cmds/dx/__init__.py +12 -0
  126. svc_infra/cli/cmds/dx/dx_cmds.py +116 -0
  127. svc_infra/cli/cmds/health/__init__.py +179 -0
  128. svc_infra/cli/cmds/health/health_cmds.py +8 -0
  129. svc_infra/cli/cmds/help.py +4 -0
  130. svc_infra/cli/cmds/jobs/__init__.py +1 -0
  131. svc_infra/cli/cmds/jobs/jobs_cmds.py +47 -0
  132. svc_infra/cli/cmds/obs/obs_cmds.py +36 -15
  133. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  134. svc_infra/cli/cmds/sdk/sdk_cmds.py +112 -0
  135. svc_infra/cli/foundation/runner.py +6 -2
  136. svc_infra/data/add.py +61 -0
  137. svc_infra/data/backup.py +58 -0
  138. svc_infra/data/erasure.py +45 -0
  139. svc_infra/data/fixtures.py +42 -0
  140. svc_infra/data/retention.py +61 -0
  141. svc_infra/db/__init__.py +15 -0
  142. svc_infra/db/crud_schema.py +9 -9
  143. svc_infra/db/inbox.py +67 -0
  144. svc_infra/db/nosql/__init__.py +3 -0
  145. svc_infra/db/nosql/core.py +30 -9
  146. svc_infra/db/nosql/indexes.py +3 -1
  147. svc_infra/db/nosql/management.py +1 -1
  148. svc_infra/db/nosql/mongo/README.md +13 -13
  149. svc_infra/db/nosql/mongo/client.py +19 -2
  150. svc_infra/db/nosql/mongo/settings.py +6 -2
  151. svc_infra/db/nosql/repository.py +35 -15
  152. svc_infra/db/nosql/resource.py +20 -3
  153. svc_infra/db/nosql/scaffold.py +9 -3
  154. svc_infra/db/nosql/service.py +3 -1
  155. svc_infra/db/nosql/types.py +6 -2
  156. svc_infra/db/ops.py +384 -0
  157. svc_infra/db/outbox.py +108 -0
  158. svc_infra/db/sql/apikey.py +37 -9
  159. svc_infra/db/sql/authref.py +9 -3
  160. svc_infra/db/sql/constants.py +12 -8
  161. svc_infra/db/sql/core.py +2 -2
  162. svc_infra/db/sql/management.py +11 -8
  163. svc_infra/db/sql/repository.py +99 -26
  164. svc_infra/db/sql/resource.py +5 -0
  165. svc_infra/db/sql/scaffold.py +6 -2
  166. svc_infra/db/sql/service.py +15 -5
  167. svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
  168. svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
  169. svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
  170. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
  171. svc_infra/db/sql/tenant.py +88 -0
  172. svc_infra/db/sql/uniq_hooks.py +9 -3
  173. svc_infra/db/sql/utils.py +138 -51
  174. svc_infra/db/sql/versioning.py +14 -0
  175. svc_infra/deploy/__init__.py +538 -0
  176. svc_infra/documents/__init__.py +100 -0
  177. svc_infra/documents/add.py +264 -0
  178. svc_infra/documents/ease.py +233 -0
  179. svc_infra/documents/models.py +114 -0
  180. svc_infra/documents/storage.py +264 -0
  181. svc_infra/dx/add.py +65 -0
  182. svc_infra/dx/changelog.py +74 -0
  183. svc_infra/dx/checks.py +68 -0
  184. svc_infra/exceptions.py +141 -0
  185. svc_infra/health/__init__.py +864 -0
  186. svc_infra/http/__init__.py +13 -0
  187. svc_infra/http/client.py +105 -0
  188. svc_infra/jobs/builtins/outbox_processor.py +40 -0
  189. svc_infra/jobs/builtins/webhook_delivery.py +95 -0
  190. svc_infra/jobs/easy.py +33 -0
  191. svc_infra/jobs/loader.py +50 -0
  192. svc_infra/jobs/queue.py +116 -0
  193. svc_infra/jobs/redis_queue.py +256 -0
  194. svc_infra/jobs/runner.py +79 -0
  195. svc_infra/jobs/scheduler.py +53 -0
  196. svc_infra/jobs/worker.py +40 -0
  197. svc_infra/loaders/__init__.py +186 -0
  198. svc_infra/loaders/base.py +142 -0
  199. svc_infra/loaders/github.py +311 -0
  200. svc_infra/loaders/models.py +147 -0
  201. svc_infra/loaders/url.py +235 -0
  202. svc_infra/logging/__init__.py +374 -0
  203. svc_infra/mcp/svc_infra_mcp.py +91 -33
  204. svc_infra/obs/README.md +2 -0
  205. svc_infra/obs/add.py +65 -9
  206. svc_infra/obs/cloud_dash.py +2 -1
  207. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  208. svc_infra/obs/metrics/__init__.py +52 -0
  209. svc_infra/obs/metrics/asgi.py +13 -7
  210. svc_infra/obs/metrics/http.py +9 -5
  211. svc_infra/obs/metrics/sqlalchemy.py +13 -9
  212. svc_infra/obs/metrics.py +53 -0
  213. svc_infra/obs/settings.py +6 -2
  214. svc_infra/security/add.py +217 -0
  215. svc_infra/security/audit.py +212 -0
  216. svc_infra/security/audit_service.py +74 -0
  217. svc_infra/security/headers.py +52 -0
  218. svc_infra/security/hibp.py +101 -0
  219. svc_infra/security/jwt_rotation.py +105 -0
  220. svc_infra/security/lockout.py +102 -0
  221. svc_infra/security/models.py +287 -0
  222. svc_infra/security/oauth_models.py +73 -0
  223. svc_infra/security/org_invites.py +130 -0
  224. svc_infra/security/passwords.py +79 -0
  225. svc_infra/security/permissions.py +171 -0
  226. svc_infra/security/session.py +98 -0
  227. svc_infra/security/signed_cookies.py +100 -0
  228. svc_infra/storage/__init__.py +93 -0
  229. svc_infra/storage/add.py +253 -0
  230. svc_infra/storage/backends/__init__.py +11 -0
  231. svc_infra/storage/backends/local.py +339 -0
  232. svc_infra/storage/backends/memory.py +216 -0
  233. svc_infra/storage/backends/s3.py +353 -0
  234. svc_infra/storage/base.py +239 -0
  235. svc_infra/storage/easy.py +185 -0
  236. svc_infra/storage/settings.py +195 -0
  237. svc_infra/testing/__init__.py +685 -0
  238. svc_infra/utils.py +7 -3
  239. svc_infra/webhooks/__init__.py +69 -0
  240. svc_infra/webhooks/add.py +339 -0
  241. svc_infra/webhooks/encryption.py +115 -0
  242. svc_infra/webhooks/fastapi.py +39 -0
  243. svc_infra/webhooks/router.py +55 -0
  244. svc_infra/webhooks/service.py +70 -0
  245. svc_infra/webhooks/signing.py +34 -0
  246. svc_infra/websocket/__init__.py +79 -0
  247. svc_infra/websocket/add.py +140 -0
  248. svc_infra/websocket/client.py +282 -0
  249. svc_infra/websocket/config.py +69 -0
  250. svc_infra/websocket/easy.py +76 -0
  251. svc_infra/websocket/exceptions.py +61 -0
  252. svc_infra/websocket/manager.py +344 -0
  253. svc_infra/websocket/models.py +49 -0
  254. svc_infra-0.1.706.dist-info/LICENSE +21 -0
  255. svc_infra-0.1.706.dist-info/METADATA +356 -0
  256. svc_infra-0.1.706.dist-info/RECORD +357 -0
  257. svc_infra-0.1.589.dist-info/METADATA +0 -79
  258. svc_infra-0.1.589.dist-info/RECORD +0 -234
  259. {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/WHEEL +0 -0
  260. {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,871 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from datetime import date, datetime, timezone
5
+ from typing import Any, Literal, Optional, Sequence, Tuple, cast
6
+
7
+ from svc_infra.apf_payments.schemas import (
8
+ BalanceAmount,
9
+ BalanceSnapshotOut,
10
+ CustomerOut,
11
+ CustomerUpsertIn,
12
+ DisputeOut,
13
+ IntentCreateIn,
14
+ IntentOut,
15
+ InvoiceCreateIn,
16
+ InvoiceLineItemIn,
17
+ InvoiceLineItemOut,
18
+ InvoiceOut,
19
+ NextAction,
20
+ PaymentMethodAttachIn,
21
+ PaymentMethodOut,
22
+ PaymentMethodUpdateIn,
23
+ PayoutOut,
24
+ PriceCreateIn,
25
+ PriceOut,
26
+ PriceUpdateIn,
27
+ ProductCreateIn,
28
+ ProductOut,
29
+ ProductUpdateIn,
30
+ RefundIn,
31
+ RefundOut,
32
+ SetupIntentCreateIn,
33
+ SetupIntentOut,
34
+ SubscriptionCreateIn,
35
+ SubscriptionOut,
36
+ SubscriptionUpdateIn,
37
+ UsageRecordIn,
38
+ UsageRecordListFilter,
39
+ UsageRecordOut,
40
+ )
41
+ from svc_infra.apf_payments.settings import get_payments_settings
42
+
43
+ from .base import ProviderAdapter
44
+
45
+ try: # pragma: no cover - optional dependency
46
+ import aiydan
47
+ except Exception: # pragma: no cover - handled at runtime
48
+ aiydan = None
49
+
50
+
51
+ async def _maybe_await(result: Any) -> Any:
52
+ if inspect.isawaitable(result):
53
+ return await result
54
+ return result
55
+
56
+
57
+ def _coerce_id(data: dict[str, Any], *candidates: str) -> str:
58
+ for key in candidates:
59
+ value = data.get(key)
60
+ if isinstance(value, str) and value:
61
+ return value
62
+ raise RuntimeError(f"Aiydan payload missing id fields: {candidates}")
63
+
64
+
65
+ def _ensure_utc_isoformat(value: Any) -> str | None:
66
+ if value is None:
67
+ return None
68
+ if isinstance(value, str):
69
+ return value
70
+ if isinstance(value, datetime):
71
+ dt: datetime = value
72
+ if dt.tzinfo is None:
73
+ dt = dt.replace(tzinfo=timezone.utc)
74
+ return dt.astimezone(timezone.utc).isoformat()
75
+ if isinstance(value, date):
76
+ return datetime(
77
+ value.year, value.month, value.day, tzinfo=timezone.utc
78
+ ).isoformat()
79
+ try:
80
+ parsed = datetime.fromisoformat(str(value))
81
+ if parsed.tzinfo is None:
82
+ parsed = parsed.replace(tzinfo=timezone.utc)
83
+ return parsed.astimezone(timezone.utc).isoformat()
84
+ except Exception:
85
+ return cast(str, str(value)) # Cast needed since value is Any
86
+
87
+
88
+ def _customer_to_out(data: dict[str, Any]) -> CustomerOut:
89
+ cust_id = _coerce_id(data, "provider_customer_id", "customer_id", "id")
90
+ return CustomerOut(
91
+ id=cust_id,
92
+ provider="aiydan",
93
+ provider_customer_id=cust_id,
94
+ email=data.get("email"),
95
+ name=data.get("name"),
96
+ )
97
+
98
+
99
+ def _intent_to_out(data: dict[str, Any]) -> IntentOut:
100
+ intent_id = _coerce_id(data, "provider_intent_id", "intent_id", "id")
101
+ return IntentOut(
102
+ id=intent_id,
103
+ provider="aiydan",
104
+ provider_intent_id=intent_id,
105
+ status=str(data.get("status", "")),
106
+ amount=int(data.get("amount", 0)),
107
+ currency=str(data.get("currency", "")).upper(),
108
+ client_secret=data.get("client_secret"),
109
+ next_action=NextAction(type=(data.get("next_action") or {}).get("type")),
110
+ )
111
+
112
+
113
+ def _payment_method_to_out(data: dict[str, Any]) -> PaymentMethodOut:
114
+ method_id = _coerce_id(data, "provider_method_id", "payment_method_id", "id")
115
+ card = data.get("card") or {}
116
+ return PaymentMethodOut(
117
+ id=method_id,
118
+ provider="aiydan",
119
+ provider_customer_id=str(
120
+ data.get("provider_customer_id") or data.get("customer_id") or ""
121
+ ),
122
+ provider_method_id=method_id,
123
+ brand=card.get("brand") or data.get("brand"),
124
+ last4=card.get("last4") or data.get("last4"),
125
+ exp_month=card.get("exp_month") or data.get("exp_month"),
126
+ exp_year=card.get("exp_year") or data.get("exp_year"),
127
+ is_default=bool(data.get("is_default")),
128
+ )
129
+
130
+
131
+ def _product_to_out(data: dict[str, Any]) -> ProductOut:
132
+ product_id = _coerce_id(data, "provider_product_id", "product_id", "id")
133
+ return ProductOut(
134
+ id=product_id,
135
+ provider="aiydan",
136
+ provider_product_id=product_id,
137
+ name=str(data.get("name", "")),
138
+ active=bool(data.get("active", True)),
139
+ )
140
+
141
+
142
+ def _price_to_out(data: dict[str, Any]) -> PriceOut:
143
+ price_id = _coerce_id(data, "provider_price_id", "price_id", "id")
144
+ recurring = data.get("recurring") or {}
145
+ return PriceOut(
146
+ id=price_id,
147
+ provider="aiydan",
148
+ provider_price_id=price_id,
149
+ provider_product_id=str(
150
+ data.get("provider_product_id")
151
+ or data.get("product_id")
152
+ or getattr(data.get("product"), "id", "")
153
+ ),
154
+ currency=str(data.get("currency", "")).upper(),
155
+ unit_amount=int(data.get("unit_amount", data.get("amount", 0) or 0)),
156
+ interval=str(recurring.get("interval")) if recurring.get("interval") else None,
157
+ trial_days=data.get("trial_days"),
158
+ active=bool(data.get("active", True)),
159
+ )
160
+
161
+
162
+ def _subscription_to_out(data: dict[str, Any]) -> SubscriptionOut:
163
+ sub_id = _coerce_id(data, "provider_subscription_id", "subscription_id", "id")
164
+ items = data.get("items") or {}
165
+ first_item = None
166
+ if isinstance(items, dict):
167
+ first_item = (items.get("data") or [None])[0]
168
+ elif isinstance(items, Sequence):
169
+ first_item = items[0] if items else None
170
+ price_id = (
171
+ first_item.get("price")
172
+ if isinstance(first_item, dict)
173
+ else getattr(first_item, "price", None)
174
+ )
175
+ if isinstance(price_id, dict):
176
+ price_id = price_id.get("id")
177
+ elif price_id is not None and not isinstance(price_id, str):
178
+ price_id = getattr(price_id, "id", None)
179
+ quantity = (
180
+ first_item.get("quantity")
181
+ if isinstance(first_item, dict)
182
+ else getattr(first_item, "quantity", 0)
183
+ )
184
+ return SubscriptionOut(
185
+ id=sub_id,
186
+ provider="aiydan",
187
+ provider_subscription_id=sub_id,
188
+ provider_price_id=price_id or "",
189
+ status=str(data.get("status", "")),
190
+ quantity=int(quantity or 0),
191
+ cancel_at_period_end=bool(data.get("cancel_at_period_end", False)),
192
+ current_period_end=_ensure_utc_isoformat(data.get("current_period_end")),
193
+ )
194
+
195
+
196
+ def _invoice_to_out(data: dict[str, Any]) -> InvoiceOut:
197
+ invoice_id = _coerce_id(data, "provider_invoice_id", "invoice_id", "id")
198
+ return InvoiceOut(
199
+ id=invoice_id,
200
+ provider="aiydan",
201
+ provider_invoice_id=invoice_id,
202
+ provider_customer_id=str(
203
+ data.get("provider_customer_id") or data.get("customer_id") or ""
204
+ ),
205
+ status=str(data.get("status", "")),
206
+ amount_due=int(data.get("amount_due", data.get("amount") or 0) or 0),
207
+ currency=str(data.get("currency", "")).upper(),
208
+ hosted_invoice_url=data.get("hosted_invoice_url") or data.get("hosted_url"),
209
+ pdf_url=data.get("pdf_url") or data.get("invoice_pdf"),
210
+ )
211
+
212
+
213
+ def _invoice_line_item_to_out(data: dict[str, Any]) -> InvoiceLineItemOut:
214
+ line_id = _coerce_id(data, "provider_invoice_line_item_id", "line_id", "id")
215
+ price = data.get("price") or {}
216
+ if not isinstance(price, dict):
217
+ price = {"id": getattr(price, "id", None)}
218
+ quantity = int(data.get("quantity", 0) or 0)
219
+ unit_amount = int(data.get("unit_amount", 0) or 0)
220
+ amount = int(data.get("amount", unit_amount * quantity) or 0)
221
+ return InvoiceLineItemOut(
222
+ id=line_id,
223
+ description=data.get("description"),
224
+ currency=str(data.get("currency", price.get("currency", ""))).upper(),
225
+ quantity=quantity,
226
+ amount=amount,
227
+ provider_price_id=price.get("id"),
228
+ )
229
+
230
+
231
+ def _refund_to_out(data: dict[str, Any]) -> RefundOut:
232
+ refund_id = _coerce_id(data, "provider_refund_id", "refund_id", "id")
233
+ return RefundOut(
234
+ id=refund_id,
235
+ provider="aiydan",
236
+ provider_refund_id=refund_id,
237
+ provider_payment_intent_id=str(
238
+ data.get("provider_payment_intent_id")
239
+ or data.get("payment_intent_id")
240
+ or ""
241
+ ),
242
+ amount=int(data.get("amount", 0) or 0),
243
+ currency=str(data.get("currency", "")).upper(),
244
+ status=str(data.get("status", "")),
245
+ reason=data.get("reason"),
246
+ created_at=_ensure_utc_isoformat(data.get("created_at") or data.get("created")),
247
+ )
248
+
249
+
250
+ def _dispute_to_out(data: dict[str, Any]) -> DisputeOut:
251
+ dispute_id = _coerce_id(data, "provider_dispute_id", "dispute_id", "id")
252
+ evidence = data.get("evidence") or {}
253
+ return DisputeOut(
254
+ id=dispute_id,
255
+ provider="aiydan",
256
+ provider_dispute_id=dispute_id,
257
+ amount=int(data.get("amount", 0) or 0),
258
+ currency=str(data.get("currency", "")).upper(),
259
+ reason=data.get("reason"),
260
+ status=str(data.get("status", "")),
261
+ evidence_due_by=_ensure_utc_isoformat(
262
+ evidence.get("due_by") or data.get("evidence_due_by")
263
+ ),
264
+ created_at=_ensure_utc_isoformat(data.get("created_at") or data.get("created")),
265
+ )
266
+
267
+
268
+ def _payout_to_out(data: dict[str, Any]) -> PayoutOut:
269
+ payout_id = _coerce_id(data, "provider_payout_id", "payout_id", "id")
270
+ return PayoutOut(
271
+ id=payout_id,
272
+ provider="aiydan",
273
+ provider_payout_id=payout_id,
274
+ amount=int(data.get("amount", 0) or 0),
275
+ currency=str(data.get("currency", "")).upper(),
276
+ status=str(data.get("status", "")),
277
+ arrival_date=_ensure_utc_isoformat(data.get("arrival_date")),
278
+ type=data.get("type"),
279
+ )
280
+
281
+
282
+ def _usage_record_to_out(data: dict[str, Any]) -> UsageRecordOut:
283
+ action_raw = data.get("action")
284
+ action: Literal["increment", "set"] | None = None
285
+ if action_raw in ("increment", "set"):
286
+ action = cast(Literal["increment", "set"], action_raw)
287
+ return UsageRecordOut(
288
+ id=str(data.get("id")),
289
+ quantity=int(data.get("quantity", 0) or 0),
290
+ timestamp=data.get("timestamp"),
291
+ subscription_item=(
292
+ str(data.get("subscription_item"))
293
+ if data.get("subscription_item")
294
+ else None
295
+ ),
296
+ provider_price_id=(
297
+ str(data.get("provider_price_id"))
298
+ if data.get("provider_price_id")
299
+ else None
300
+ ),
301
+ action=action,
302
+ )
303
+
304
+
305
+ def _balance_snapshot_to_out(data: dict[str, Any]) -> BalanceSnapshotOut:
306
+ def _normalize(side: Any) -> list[dict[str, Any]]:
307
+ if isinstance(side, list):
308
+ out: list[dict[str, Any]] = []
309
+ for item in side:
310
+ if isinstance(item, dict) and "currency" in item and "amount" in item:
311
+ out.append(
312
+ {
313
+ "currency": str(item["currency"]).upper(),
314
+ "amount": int(item["amount"] or 0),
315
+ }
316
+ )
317
+ return out
318
+ if isinstance(side, dict):
319
+ return [
320
+ {"currency": str(cur).upper(), "amount": int(amt or 0)}
321
+ for cur, amt in side.items()
322
+ ]
323
+ return []
324
+
325
+ return BalanceSnapshotOut(
326
+ available=[
327
+ BalanceAmount(currency=i["currency"], amount=i["amount"])
328
+ for i in _normalize(data.get("available"))
329
+ ],
330
+ pending=[
331
+ BalanceAmount(currency=i["currency"], amount=i["amount"])
332
+ for i in _normalize(data.get("pending"))
333
+ ],
334
+ )
335
+
336
+
337
+ def _ensure_sequence(result: Any) -> Sequence[dict[str, Any]]:
338
+ if isinstance(result, Sequence):
339
+ return result
340
+ if isinstance(result, dict):
341
+ items = result.get("items")
342
+ if isinstance(items, Sequence):
343
+ return items
344
+ raise RuntimeError("Expected sequence payload from Aiydan client")
345
+
346
+
347
+ def _ensure_list_response(
348
+ result: Any,
349
+ ) -> Tuple[Sequence[dict[str, Any]], Optional[str]]:
350
+ if isinstance(result, tuple) and len(result) == 2:
351
+ items, cursor = result
352
+ if isinstance(items, Sequence) or items is None:
353
+ return (items or []), cursor
354
+ if isinstance(result, dict):
355
+ items = result.get("items")
356
+ cursor = result.get("next_cursor") or result.get("cursor")
357
+ if isinstance(items, Sequence):
358
+ return items, cursor
359
+ if isinstance(result, Sequence):
360
+ return result, None
361
+ raise RuntimeError("Expected iterable response from Aiydan client")
362
+
363
+
364
+ class AiydanAdapter(ProviderAdapter):
365
+ name = "aiydan"
366
+
367
+ def __init__(self, *, client: Optional[Any] = None):
368
+ settings = get_payments_settings()
369
+ cfg = settings.aiydan
370
+ if client is not None:
371
+ self._client = client
372
+ self._webhook_secret = (
373
+ cfg.webhook_secret.get_secret_value()
374
+ if cfg and cfg.webhook_secret
375
+ else None
376
+ )
377
+ return
378
+ if cfg is None:
379
+ raise RuntimeError("Aiydan settings not configured")
380
+ if aiydan is None:
381
+ raise RuntimeError("aiydan SDK is not installed. pip install aiydan")
382
+ client_class = getattr(aiydan, "Client", None)
383
+ if client_class is None:
384
+ raise RuntimeError("aiydan SDK missing 'Client' class")
385
+ kwargs: dict[str, Any] = {"api_key": cfg.api_key.get_secret_value()}
386
+ if cfg.client_key:
387
+ kwargs["client_key"] = cfg.client_key.get_secret_value()
388
+ if cfg.merchant_account:
389
+ kwargs["merchant_account"] = cfg.merchant_account
390
+ if cfg.hmac_key:
391
+ kwargs["hmac_key"] = cfg.hmac_key.get_secret_value()
392
+ if cfg.base_url:
393
+ kwargs["base_url"] = cfg.base_url
394
+ self._client = client_class(**kwargs)
395
+ self._webhook_secret = (
396
+ cfg.webhook_secret.get_secret_value() if cfg.webhook_secret else None
397
+ )
398
+
399
+ async def ensure_customer(self, data: CustomerUpsertIn) -> CustomerOut:
400
+ payload = data.model_dump(exclude_none=True)
401
+ result = await _maybe_await(self._client.ensure_customer(payload))
402
+ return _customer_to_out(result)
403
+
404
+ async def attach_payment_method(
405
+ self, data: PaymentMethodAttachIn
406
+ ) -> PaymentMethodOut:
407
+ payload = data.model_dump(exclude_none=True)
408
+ result = await _maybe_await(self._client.attach_payment_method(payload))
409
+ return _payment_method_to_out(result)
410
+
411
+ async def list_payment_methods(
412
+ self, provider_customer_id: str
413
+ ) -> list[PaymentMethodOut]:
414
+ result = await _maybe_await(
415
+ self._client.list_payment_methods(provider_customer_id)
416
+ )
417
+ methods = _ensure_sequence(result)
418
+ return [_payment_method_to_out(method) for method in methods]
419
+
420
+ async def detach_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
421
+ result = await _maybe_await(
422
+ self._client.detach_payment_method(provider_method_id)
423
+ )
424
+ return _payment_method_to_out(result)
425
+
426
+ async def set_default_payment_method(
427
+ self, provider_customer_id: str, provider_method_id: str
428
+ ) -> PaymentMethodOut:
429
+ result = await _maybe_await(
430
+ self._client.set_default_payment_method(
431
+ provider_customer_id, provider_method_id
432
+ )
433
+ )
434
+ return _payment_method_to_out(result)
435
+
436
+ async def get_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
437
+ result = await _maybe_await(self._client.get_payment_method(provider_method_id))
438
+ return _payment_method_to_out(result)
439
+
440
+ async def update_payment_method(
441
+ self, provider_method_id: str, data: PaymentMethodUpdateIn
442
+ ) -> PaymentMethodOut:
443
+ payload = data.model_dump(exclude_none=True)
444
+ result = await _maybe_await(
445
+ self._client.update_payment_method(provider_method_id, payload)
446
+ )
447
+ return _payment_method_to_out(result)
448
+
449
+ async def create_product(self, data: ProductCreateIn) -> ProductOut:
450
+ payload = data.model_dump(exclude_none=True)
451
+ result = await _maybe_await(self._client.create_product(payload))
452
+ return _product_to_out(result)
453
+
454
+ async def get_product(self, provider_product_id: str) -> ProductOut:
455
+ result = await _maybe_await(self._client.get_product(provider_product_id))
456
+ return _product_to_out(result)
457
+
458
+ async def list_products(
459
+ self, *, active: bool | None, limit: int, cursor: str | None
460
+ ) -> tuple[list[ProductOut], str | None]:
461
+ result = await _maybe_await(
462
+ self._client.list_products(active=active, limit=limit, cursor=cursor)
463
+ )
464
+ items, next_cursor = _ensure_list_response(result)
465
+ return [_product_to_out(item) for item in items], next_cursor
466
+
467
+ async def update_product(
468
+ self, provider_product_id: str, data: ProductUpdateIn
469
+ ) -> ProductOut:
470
+ payload = data.model_dump(exclude_none=True)
471
+ result = await _maybe_await(
472
+ self._client.update_product(provider_product_id, payload)
473
+ )
474
+ return _product_to_out(result)
475
+
476
+ async def create_price(self, data: PriceCreateIn) -> PriceOut:
477
+ payload = data.model_dump(exclude_none=True)
478
+ result = await _maybe_await(self._client.create_price(payload))
479
+ return _price_to_out(result)
480
+
481
+ async def get_price(self, provider_price_id: str) -> PriceOut:
482
+ result = await _maybe_await(self._client.get_price(provider_price_id))
483
+ return _price_to_out(result)
484
+
485
+ async def list_prices(
486
+ self,
487
+ *,
488
+ provider_product_id: str | None,
489
+ active: bool | None,
490
+ limit: int,
491
+ cursor: str | None,
492
+ ) -> tuple[list[PriceOut], str | None]:
493
+ result = await _maybe_await(
494
+ self._client.list_prices(
495
+ provider_product_id=provider_product_id,
496
+ active=active,
497
+ limit=limit,
498
+ cursor=cursor,
499
+ )
500
+ )
501
+ items, next_cursor = _ensure_list_response(result)
502
+ return [_price_to_out(item) for item in items], next_cursor
503
+
504
+ async def update_price(
505
+ self, provider_price_id: str, data: PriceUpdateIn
506
+ ) -> PriceOut:
507
+ payload = data.model_dump(exclude_none=True)
508
+ result = await _maybe_await(
509
+ self._client.update_price(provider_price_id, payload)
510
+ )
511
+ return _price_to_out(result)
512
+
513
+ async def create_subscription(self, data: SubscriptionCreateIn) -> SubscriptionOut:
514
+ payload = data.model_dump(exclude_none=True)
515
+ result = await _maybe_await(self._client.create_subscription(payload))
516
+ return _subscription_to_out(result)
517
+
518
+ async def update_subscription(
519
+ self, provider_subscription_id: str, data: SubscriptionUpdateIn
520
+ ) -> SubscriptionOut:
521
+ payload = data.model_dump(exclude_none=True)
522
+ result = await _maybe_await(
523
+ self._client.update_subscription(provider_subscription_id, payload)
524
+ )
525
+ return _subscription_to_out(result)
526
+
527
+ async def cancel_subscription(
528
+ self, provider_subscription_id: str, at_period_end: bool = True
529
+ ) -> SubscriptionOut:
530
+ result = await _maybe_await(
531
+ self._client.cancel_subscription(provider_subscription_id, at_period_end)
532
+ )
533
+ return _subscription_to_out(result)
534
+
535
+ async def get_subscription(self, provider_subscription_id: str) -> SubscriptionOut:
536
+ result = await _maybe_await(
537
+ self._client.get_subscription(provider_subscription_id)
538
+ )
539
+ return _subscription_to_out(result)
540
+
541
+ async def list_subscriptions(
542
+ self,
543
+ *,
544
+ customer_provider_id: str | None,
545
+ status: str | None,
546
+ limit: int,
547
+ cursor: str | None,
548
+ ) -> tuple[list[SubscriptionOut], str | None]:
549
+ result = await _maybe_await(
550
+ self._client.list_subscriptions(
551
+ customer_provider_id=customer_provider_id,
552
+ status=status,
553
+ limit=limit,
554
+ cursor=cursor,
555
+ )
556
+ )
557
+ items, next_cursor = _ensure_list_response(result)
558
+ return [_subscription_to_out(item) for item in items], next_cursor
559
+
560
+ async def create_invoice(self, data: InvoiceCreateIn) -> InvoiceOut:
561
+ payload = data.model_dump(exclude_none=True)
562
+ result = await _maybe_await(self._client.create_invoice(payload))
563
+ return _invoice_to_out(result)
564
+
565
+ async def finalize_invoice(self, provider_invoice_id: str) -> InvoiceOut:
566
+ result = await _maybe_await(self._client.finalize_invoice(provider_invoice_id))
567
+ return _invoice_to_out(result)
568
+
569
+ async def void_invoice(self, provider_invoice_id: str) -> InvoiceOut:
570
+ result = await _maybe_await(self._client.void_invoice(provider_invoice_id))
571
+ return _invoice_to_out(result)
572
+
573
+ async def pay_invoice(self, provider_invoice_id: str) -> InvoiceOut:
574
+ result = await _maybe_await(self._client.pay_invoice(provider_invoice_id))
575
+ return _invoice_to_out(result)
576
+
577
+ async def add_invoice_line_item(
578
+ self, provider_invoice_id: str, data: InvoiceLineItemIn
579
+ ) -> InvoiceOut:
580
+ payload = data.model_dump(exclude_none=True)
581
+ result = await _maybe_await(
582
+ self._client.add_invoice_line_item(provider_invoice_id, payload)
583
+ )
584
+ return _invoice_to_out(result)
585
+
586
+ async def list_invoices(
587
+ self,
588
+ *,
589
+ customer_provider_id: str | None,
590
+ status: str | None,
591
+ limit: int,
592
+ cursor: str | None,
593
+ ) -> tuple[list[InvoiceOut], str | None]:
594
+ result = await _maybe_await(
595
+ self._client.list_invoices(
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 [_invoice_to_out(item) for item in items], next_cursor
604
+
605
+ async def get_invoice(self, provider_invoice_id: str) -> InvoiceOut:
606
+ result = await _maybe_await(self._client.get_invoice(provider_invoice_id))
607
+ return _invoice_to_out(result)
608
+
609
+ async def preview_invoice(
610
+ self, *, customer_provider_id: str, subscription_id: str | None = None
611
+ ) -> InvoiceOut:
612
+ result = await _maybe_await(
613
+ self._client.preview_invoice(
614
+ customer_provider_id=customer_provider_id,
615
+ subscription_id=subscription_id,
616
+ )
617
+ )
618
+ return _invoice_to_out(result)
619
+
620
+ async def list_invoice_line_items(
621
+ self, provider_invoice_id: str, *, limit: int, cursor: str | None
622
+ ) -> tuple[list[InvoiceLineItemOut], str | None]:
623
+ result = await _maybe_await(
624
+ self._client.list_invoice_line_items(
625
+ provider_invoice_id,
626
+ limit=limit,
627
+ cursor=cursor,
628
+ )
629
+ )
630
+ items, next_cursor = _ensure_list_response(result)
631
+ return [_invoice_line_item_to_out(item) for item in items], next_cursor
632
+
633
+ async def create_intent(
634
+ self, data: IntentCreateIn, *, user_id: str | None
635
+ ) -> IntentOut:
636
+ payload = data.model_dump(exclude_none=True)
637
+ if user_id is not None:
638
+ payload["user_id"] = user_id
639
+ result = await _maybe_await(self._client.create_intent(payload))
640
+ return _intent_to_out(result)
641
+
642
+ async def confirm_intent(self, provider_intent_id: str) -> IntentOut:
643
+ result = await _maybe_await(self._client.confirm_intent(provider_intent_id))
644
+ return _intent_to_out(result)
645
+
646
+ async def cancel_intent(self, provider_intent_id: str) -> IntentOut:
647
+ result = await _maybe_await(self._client.cancel_intent(provider_intent_id))
648
+ return _intent_to_out(result)
649
+
650
+ async def refund(self, provider_intent_id: str, data: RefundIn) -> IntentOut:
651
+ payload = data.model_dump(exclude_none=True)
652
+ result = await _maybe_await(
653
+ self._client.refund_intent(provider_intent_id, payload)
654
+ )
655
+ return _intent_to_out(result)
656
+
657
+ async def hydrate_intent(self, provider_intent_id: str) -> IntentOut:
658
+ result = await _maybe_await(self._client.get_intent(provider_intent_id))
659
+ return _intent_to_out(result)
660
+
661
+ async def capture_intent(
662
+ self, provider_intent_id: str, *, amount: int | None
663
+ ) -> IntentOut:
664
+ result = await _maybe_await(
665
+ self._client.capture_intent(provider_intent_id, amount=amount)
666
+ )
667
+ return _intent_to_out(result)
668
+
669
+ async def list_intents(
670
+ self,
671
+ *,
672
+ customer_provider_id: str | None,
673
+ status: str | None,
674
+ limit: int,
675
+ cursor: str | None,
676
+ ) -> tuple[list[IntentOut], str | None]:
677
+ result = await _maybe_await(
678
+ self._client.list_intents(
679
+ customer_provider_id=customer_provider_id,
680
+ status=status,
681
+ limit=limit,
682
+ cursor=cursor,
683
+ )
684
+ )
685
+ items, next_cursor = _ensure_list_response(result)
686
+ return [_intent_to_out(item) for item in items], next_cursor
687
+
688
+ async def verify_and_parse_webhook(
689
+ self, signature: str | None, payload: bytes
690
+ ) -> dict[str, Any]:
691
+ if hasattr(self._client, "verify_and_parse_webhook"):
692
+ result = await _maybe_await(
693
+ self._client.verify_and_parse_webhook(
694
+ signature=signature,
695
+ payload=payload,
696
+ secret=self._webhook_secret,
697
+ )
698
+ )
699
+ elif hasattr(self._client, "verify_webhook"):
700
+ result = await _maybe_await(
701
+ self._client.verify_webhook(
702
+ payload=payload,
703
+ signature=signature,
704
+ secret=self._webhook_secret,
705
+ )
706
+ )
707
+ else:
708
+ raise RuntimeError("Aiydan client missing webhook verification method")
709
+ if not isinstance(result, dict):
710
+ raise RuntimeError("Aiydan client returned unexpected webhook payload")
711
+ return result
712
+
713
+ async def list_disputes(
714
+ self, *, status: str | None, limit: int, cursor: str | None
715
+ ) -> tuple[list[DisputeOut], str | None]:
716
+ result = await _maybe_await(
717
+ self._client.list_disputes(status=status, limit=limit, cursor=cursor)
718
+ )
719
+ items, next_cursor = _ensure_list_response(result)
720
+ return [_dispute_to_out(item) for item in items], next_cursor
721
+
722
+ async def get_dispute(self, provider_dispute_id: str) -> DisputeOut:
723
+ result = await _maybe_await(self._client.get_dispute(provider_dispute_id))
724
+ return _dispute_to_out(result)
725
+
726
+ async def submit_dispute_evidence(
727
+ self, provider_dispute_id: str, evidence: dict
728
+ ) -> DisputeOut:
729
+ result = await _maybe_await(
730
+ self._client.submit_dispute_evidence(provider_dispute_id, evidence)
731
+ )
732
+ return _dispute_to_out(result)
733
+
734
+ async def get_balance_snapshot(self) -> BalanceSnapshotOut:
735
+ result = await _maybe_await(self._client.get_balance_snapshot())
736
+ if isinstance(result, BalanceSnapshotOut):
737
+ return result
738
+ if not isinstance(result, dict):
739
+ raise RuntimeError("Aiydan client returned unexpected balance payload")
740
+ return _balance_snapshot_to_out(result)
741
+
742
+ async def list_payouts(
743
+ self, *, limit: int, cursor: str | None
744
+ ) -> tuple[list[PayoutOut], str | None]:
745
+ result = await _maybe_await(
746
+ self._client.list_payouts(limit=limit, cursor=cursor)
747
+ )
748
+ items, next_cursor = _ensure_list_response(result)
749
+ return [_payout_to_out(item) for item in items], next_cursor
750
+
751
+ async def get_payout(self, provider_payout_id: str) -> PayoutOut:
752
+ result = await _maybe_await(self._client.get_payout(provider_payout_id))
753
+ return _payout_to_out(result)
754
+
755
+ async def list_refunds(
756
+ self,
757
+ *,
758
+ provider_payment_intent_id: str | None,
759
+ limit: int,
760
+ cursor: str | None,
761
+ ) -> tuple[list[RefundOut], str | None]:
762
+ result = await _maybe_await(
763
+ self._client.list_refunds(
764
+ provider_payment_intent_id=provider_payment_intent_id,
765
+ limit=limit,
766
+ cursor=cursor,
767
+ )
768
+ )
769
+ items, next_cursor = _ensure_list_response(result)
770
+ return [_refund_to_out(item) for item in items], next_cursor
771
+
772
+ async def get_refund(self, provider_refund_id: str) -> RefundOut:
773
+ result = await _maybe_await(self._client.get_refund(provider_refund_id))
774
+ return _refund_to_out(result)
775
+
776
+ async def create_usage_record(self, data: UsageRecordIn) -> UsageRecordOut:
777
+ payload = data.model_dump(exclude_none=True)
778
+ result = await _maybe_await(self._client.create_usage_record(payload))
779
+ return _usage_record_to_out(result)
780
+
781
+ async def list_usage_records(
782
+ self, f: UsageRecordListFilter
783
+ ) -> tuple[list[UsageRecordOut], str | None]:
784
+ payload = f.model_dump(exclude_none=True)
785
+ result = await _maybe_await(self._client.list_usage_records(payload))
786
+ items, next_cursor = _ensure_list_response(result)
787
+ return [_usage_record_to_out(item) for item in items], next_cursor
788
+
789
+ async def get_usage_record(self, usage_record_id: str) -> UsageRecordOut:
790
+ result = await _maybe_await(self._client.get_usage_record(usage_record_id))
791
+ return _usage_record_to_out(result)
792
+
793
+ async def create_setup_intent(self, data: SetupIntentCreateIn) -> SetupIntentOut:
794
+ payload = data.model_dump(exclude_none=True)
795
+ result = await _maybe_await(self._client.create_setup_intent(payload))
796
+ return SetupIntentOut(
797
+ id=_coerce_id(result, "provider_setup_intent_id", "setup_intent_id", "id"),
798
+ provider="aiydan",
799
+ provider_setup_intent_id=_coerce_id(
800
+ result, "provider_setup_intent_id", "setup_intent_id", "id"
801
+ ),
802
+ status=str(result.get("status", "")),
803
+ client_secret=result.get("client_secret"),
804
+ next_action=NextAction(type=(result.get("next_action") or {}).get("type")),
805
+ )
806
+
807
+ async def confirm_setup_intent(
808
+ self, provider_setup_intent_id: str
809
+ ) -> SetupIntentOut:
810
+ result = await _maybe_await(
811
+ self._client.confirm_setup_intent(provider_setup_intent_id)
812
+ )
813
+ return SetupIntentOut(
814
+ id=_coerce_id(result, "provider_setup_intent_id", "setup_intent_id", "id"),
815
+ provider="aiydan",
816
+ provider_setup_intent_id=_coerce_id(
817
+ result, "provider_setup_intent_id", "setup_intent_id", "id"
818
+ ),
819
+ status=str(result.get("status", "")),
820
+ client_secret=result.get("client_secret"),
821
+ next_action=NextAction(type=(result.get("next_action") or {}).get("type")),
822
+ )
823
+
824
+ async def get_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
825
+ result = await _maybe_await(
826
+ self._client.get_setup_intent(provider_setup_intent_id)
827
+ )
828
+ return SetupIntentOut(
829
+ id=_coerce_id(result, "provider_setup_intent_id", "setup_intent_id", "id"),
830
+ provider="aiydan",
831
+ provider_setup_intent_id=_coerce_id(
832
+ result, "provider_setup_intent_id", "setup_intent_id", "id"
833
+ ),
834
+ status=str(result.get("status", "")),
835
+ client_secret=result.get("client_secret"),
836
+ next_action=NextAction(type=(result.get("next_action") or {}).get("type")),
837
+ )
838
+
839
+ async def resume_intent_after_action(self, provider_intent_id: str) -> IntentOut:
840
+ if hasattr(self._client, "resume_intent_after_action"):
841
+ result = await _maybe_await(
842
+ self._client.resume_intent_after_action(provider_intent_id)
843
+ )
844
+ else:
845
+ result = await _maybe_await(self._client.get_intent(provider_intent_id))
846
+ return _intent_to_out(result)
847
+
848
+ async def list_customers(
849
+ self,
850
+ *,
851
+ provider: str | None,
852
+ user_id: str | None,
853
+ limit: int,
854
+ cursor: str | None,
855
+ ) -> tuple[list[CustomerOut], str | None]:
856
+ result = await _maybe_await(
857
+ self._client.list_customers(
858
+ provider=provider,
859
+ user_id=user_id,
860
+ limit=limit,
861
+ cursor=cursor,
862
+ )
863
+ )
864
+ items, next_cursor = _ensure_list_response(result)
865
+ return [_customer_to_out(item) for item in items], next_cursor
866
+
867
+ async def get_customer(self, provider_customer_id: str) -> Optional[CustomerOut]:
868
+ result = await _maybe_await(self._client.get_customer(provider_customer_id))
869
+ if result is None:
870
+ return None
871
+ return _customer_to_out(result)