svc-infra 0.1.562__py3-none-any.whl → 0.1.654__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.
- svc_infra/apf_payments/README.md +732 -0
- svc_infra/apf_payments/models.py +142 -4
- svc_infra/apf_payments/provider/__init__.py +4 -0
- svc_infra/apf_payments/provider/aiydan.py +797 -0
- svc_infra/apf_payments/provider/base.py +178 -12
- svc_infra/apf_payments/provider/stripe.py +757 -48
- svc_infra/apf_payments/schemas.py +163 -1
- svc_infra/apf_payments/service.py +582 -42
- svc_infra/apf_payments/settings.py +22 -2
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +231 -0
- svc_infra/api/fastapi/apf_payments/router.py +792 -73
- svc_infra/api/fastapi/apf_payments/setup.py +13 -4
- svc_infra/api/fastapi/auth/add.py +10 -4
- svc_infra/api/fastapi/auth/gaurd.py +67 -5
- svc_infra/api/fastapi/auth/routers/oauth_router.py +74 -34
- svc_infra/api/fastapi/auth/routers/session_router.py +63 -0
- svc_infra/api/fastapi/auth/settings.py +2 -0
- svc_infra/api/fastapi/billing/router.py +64 -0
- svc_infra/api/fastapi/billing/setup.py +19 -0
- svc_infra/api/fastapi/cache/add.py +9 -5
- svc_infra/api/fastapi/db/nosql/mongo/add.py +33 -27
- svc_infra/api/fastapi/db/sql/add.py +40 -18
- svc_infra/api/fastapi/db/sql/crud_router.py +176 -14
- svc_infra/api/fastapi/db/sql/session.py +16 -0
- svc_infra/api/fastapi/db/sql/users.py +13 -1
- svc_infra/api/fastapi/dependencies/ratelimit.py +116 -0
- svc_infra/api/fastapi/docs/add.py +160 -0
- svc_infra/api/fastapi/docs/landing.py +1 -1
- svc_infra/api/fastapi/docs/scoped.py +41 -6
- svc_infra/api/fastapi/middleware/errors/handlers.py +45 -7
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
- svc_infra/api/fastapi/middleware/idempotency.py +82 -42
- svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
- svc_infra/api/fastapi/middleware/optimistic_lock.py +37 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +84 -11
- svc_infra/api/fastapi/middleware/ratelimit_store.py +84 -0
- svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
- svc_infra/api/fastapi/middleware/timeout.py +148 -0
- svc_infra/api/fastapi/openapi/mutators.py +244 -38
- svc_infra/api/fastapi/ops/add.py +73 -0
- svc_infra/api/fastapi/pagination.py +133 -32
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +23 -14
- svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra/api/fastapi/tenancy/context.py +112 -0
- svc_infra/api/fastapi/versioned.py +101 -0
- svc_infra/app/README.md +5 -5
- svc_infra/billing/__init__.py +23 -0
- svc_infra/billing/async_service.py +147 -0
- svc_infra/billing/jobs.py +230 -0
- svc_infra/billing/models.py +131 -0
- svc_infra/billing/quotas.py +101 -0
- svc_infra/billing/schemas.py +33 -0
- svc_infra/billing/service.py +115 -0
- svc_infra/bundled_docs/README.md +5 -0
- svc_infra/bundled_docs/__init__.py +1 -0
- svc_infra/bundled_docs/getting-started.md +6 -0
- svc_infra/cache/__init__.py +4 -0
- svc_infra/cache/add.py +158 -0
- svc_infra/cache/backend.py +5 -2
- svc_infra/cache/decorators.py +19 -1
- svc_infra/cache/keys.py +24 -4
- svc_infra/cli/__init__.py +32 -8
- svc_infra/cli/__main__.py +4 -0
- svc_infra/cli/cmds/__init__.py +10 -0
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +4 -3
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +4 -4
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +80 -11
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
- svc_infra/cli/cmds/docs/docs_cmds.py +140 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
- svc_infra/cli/cmds/help.py +4 -0
- svc_infra/cli/cmds/jobs/__init__.py +1 -0
- svc_infra/cli/cmds/jobs/jobs_cmds.py +43 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +4 -3
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +53 -0
- svc_infra/data/erasure.py +45 -0
- svc_infra/data/fixtures.py +40 -0
- svc_infra/data/retention.py +55 -0
- svc_infra/db/inbox.py +67 -0
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/outbox.py +104 -0
- svc_infra/db/sql/repository.py +52 -12
- svc_infra/db/sql/resource.py +5 -0
- svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +13 -8
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +9 -5
- svc_infra/db/sql/tenant.py +79 -0
- svc_infra/db/sql/utils.py +18 -4
- svc_infra/db/sql/versioning.py +14 -0
- svc_infra/docs/acceptance-matrix.md +71 -0
- svc_infra/docs/acceptance.md +44 -0
- svc_infra/docs/admin.md +425 -0
- svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +40 -0
- svc_infra/docs/adr/0003-webhooks-framework.md +24 -0
- svc_infra/docs/adr/0004-tenancy-model.md +42 -0
- svc_infra/docs/adr/0005-data-lifecycle.md +86 -0
- svc_infra/docs/adr/0006-ops-slos-and-metrics.md +47 -0
- svc_infra/docs/adr/0007-docs-and-sdks.md +83 -0
- svc_infra/docs/adr/0008-billing-primitives.md +143 -0
- svc_infra/docs/adr/0009-acceptance-harness.md +40 -0
- svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
- svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
- svc_infra/docs/api.md +59 -0
- svc_infra/docs/auth.md +11 -0
- svc_infra/docs/billing.md +190 -0
- svc_infra/docs/cache.md +76 -0
- svc_infra/docs/cli.md +74 -0
- svc_infra/docs/contributing.md +34 -0
- svc_infra/docs/data-lifecycle.md +52 -0
- svc_infra/docs/database.md +14 -0
- svc_infra/docs/docs-and-sdks.md +62 -0
- svc_infra/docs/environment.md +114 -0
- svc_infra/docs/getting-started.md +63 -0
- svc_infra/docs/idempotency.md +111 -0
- svc_infra/docs/jobs.md +67 -0
- svc_infra/docs/observability.md +16 -0
- svc_infra/docs/ops.md +37 -0
- svc_infra/docs/rate-limiting.md +125 -0
- svc_infra/docs/repo-review.md +48 -0
- svc_infra/docs/security.md +176 -0
- svc_infra/docs/tenancy.md +35 -0
- svc_infra/docs/timeouts-and-resource-limits.md +147 -0
- svc_infra/docs/versioned-integrations.md +146 -0
- svc_infra/docs/webhooks.md +112 -0
- svc_infra/dx/add.py +63 -0
- svc_infra/dx/changelog.py +74 -0
- svc_infra/dx/checks.py +67 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +72 -0
- svc_infra/jobs/builtins/outbox_processor.py +38 -0
- svc_infra/jobs/builtins/webhook_delivery.py +90 -0
- svc_infra/jobs/easy.py +32 -0
- svc_infra/jobs/loader.py +45 -0
- svc_infra/jobs/queue.py +81 -0
- svc_infra/jobs/redis_queue.py +191 -0
- svc_infra/jobs/runner.py +75 -0
- svc_infra/jobs/scheduler.py +41 -0
- svc_infra/jobs/worker.py +40 -0
- svc_infra/mcp/svc_infra_mcp.py +85 -28
- svc_infra/obs/README.md +2 -0
- svc_infra/obs/add.py +54 -7
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/obs/metrics/__init__.py +53 -0
- svc_infra/obs/metrics.py +52 -0
- svc_infra/security/add.py +201 -0
- svc_infra/security/audit.py +130 -0
- svc_infra/security/audit_service.py +73 -0
- svc_infra/security/headers.py +52 -0
- svc_infra/security/hibp.py +95 -0
- svc_infra/security/jwt_rotation.py +53 -0
- svc_infra/security/lockout.py +96 -0
- svc_infra/security/models.py +255 -0
- svc_infra/security/org_invites.py +128 -0
- svc_infra/security/passwords.py +77 -0
- svc_infra/security/permissions.py +149 -0
- svc_infra/security/session.py +98 -0
- svc_infra/security/signed_cookies.py +80 -0
- svc_infra/webhooks/__init__.py +16 -0
- svc_infra/webhooks/add.py +322 -0
- svc_infra/webhooks/fastapi.py +37 -0
- svc_infra/webhooks/router.py +55 -0
- svc_infra/webhooks/service.py +67 -0
- svc_infra/webhooks/signing.py +30 -0
- svc_infra-0.1.654.dist-info/METADATA +154 -0
- {svc_infra-0.1.562.dist-info → svc_infra-0.1.654.dist-info}/RECORD +174 -56
- svc_infra-0.1.562.dist-info/METADATA +0 -79
- {svc_infra-0.1.562.dist-info → svc_infra-0.1.654.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.562.dist-info → svc_infra-0.1.654.dist-info}/entry_points.txt +0 -0
|
@@ -5,11 +5,42 @@ from typing import Any, Optional
|
|
|
5
5
|
|
|
6
6
|
import anyio
|
|
7
7
|
|
|
8
|
-
from ..schemas import
|
|
8
|
+
from ..schemas import (
|
|
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
|
+
)
|
|
9
41
|
from ..settings import get_payments_settings
|
|
10
42
|
from .base import ProviderAdapter
|
|
11
43
|
|
|
12
|
-
# Import lazily to avoid hard dependency if not used
|
|
13
44
|
try:
|
|
14
45
|
import stripe
|
|
15
46
|
except Exception: # pragma: no cover
|
|
@@ -20,6 +51,137 @@ async def _acall(fn, /, *args, **kwargs):
|
|
|
20
51
|
return await anyio.to_thread.run_sync(partial(fn, *args, **kwargs))
|
|
21
52
|
|
|
22
53
|
|
|
54
|
+
def _pi_to_out(pi) -> IntentOut:
|
|
55
|
+
return IntentOut(
|
|
56
|
+
id=pi.id,
|
|
57
|
+
provider="stripe",
|
|
58
|
+
provider_intent_id=pi.id,
|
|
59
|
+
status=pi.status,
|
|
60
|
+
amount=int(pi.amount),
|
|
61
|
+
currency=str(pi.currency).upper(),
|
|
62
|
+
client_secret=getattr(pi, "client_secret", None),
|
|
63
|
+
next_action=NextAction(type=getattr(getattr(pi, "next_action", None), "type", None)),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _inv_to_out(inv) -> InvoiceOut:
|
|
68
|
+
return InvoiceOut(
|
|
69
|
+
id=inv.id,
|
|
70
|
+
provider="stripe",
|
|
71
|
+
provider_invoice_id=inv.id,
|
|
72
|
+
provider_customer_id=inv.customer,
|
|
73
|
+
status=inv.status,
|
|
74
|
+
amount_due=int(inv.amount_due or 0),
|
|
75
|
+
currency=str(inv.currency).upper(),
|
|
76
|
+
hosted_invoice_url=getattr(inv, "hosted_invoice_url", None),
|
|
77
|
+
pdf_url=getattr(inv, "invoice_pdf", None),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _pm_to_out(pm, *, is_default: bool = False) -> PaymentMethodOut:
|
|
82
|
+
card = getattr(pm, "card", None) or {}
|
|
83
|
+
return PaymentMethodOut(
|
|
84
|
+
id=pm.id,
|
|
85
|
+
provider="stripe",
|
|
86
|
+
provider_customer_id=getattr(pm, "customer", None) or "",
|
|
87
|
+
provider_method_id=pm.id,
|
|
88
|
+
brand=card.get("brand"),
|
|
89
|
+
last4=card.get("last4"),
|
|
90
|
+
exp_month=card.get("exp_month"),
|
|
91
|
+
exp_year=card.get("exp_year"),
|
|
92
|
+
is_default=bool(is_default),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _product_to_out(p) -> ProductOut:
|
|
97
|
+
return ProductOut(
|
|
98
|
+
id=p.id,
|
|
99
|
+
provider="stripe",
|
|
100
|
+
provider_product_id=p.id,
|
|
101
|
+
name=p.name,
|
|
102
|
+
active=bool(p.active),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _price_to_out(pr) -> PriceOut:
|
|
107
|
+
rec = getattr(pr, "recurring", None) or {}
|
|
108
|
+
return PriceOut(
|
|
109
|
+
id=pr.id,
|
|
110
|
+
provider="stripe",
|
|
111
|
+
provider_price_id=pr.id,
|
|
112
|
+
provider_product_id=(
|
|
113
|
+
pr.product if isinstance(pr.product, str) else getattr(pr.product, "id", "")
|
|
114
|
+
),
|
|
115
|
+
currency=str(pr.currency).upper(),
|
|
116
|
+
unit_amount=int(pr.unit_amount),
|
|
117
|
+
interval=rec.get("interval"),
|
|
118
|
+
trial_days=getattr(pr, "trial_period_days", None),
|
|
119
|
+
active=bool(pr.active),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _sub_to_out(s) -> SubscriptionOut:
|
|
124
|
+
# pick first item’s price/quantity for simple one-item subs
|
|
125
|
+
item = s.items.data[0] if getattr(s.items, "data", []) else None
|
|
126
|
+
price_id = item.price.id if item and getattr(item, "price", None) else ""
|
|
127
|
+
qty = item.quantity if item else 0
|
|
128
|
+
return SubscriptionOut(
|
|
129
|
+
id=s.id,
|
|
130
|
+
provider="stripe",
|
|
131
|
+
provider_subscription_id=s.id,
|
|
132
|
+
provider_price_id=price_id,
|
|
133
|
+
status=s.status,
|
|
134
|
+
quantity=int(qty or 0),
|
|
135
|
+
cancel_at_period_end=bool(s.cancel_at_period_end),
|
|
136
|
+
current_period_end=(
|
|
137
|
+
str(s.current_period_end) if getattr(s, "current_period_end", None) else None
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _refund_to_out(r) -> RefundOut:
|
|
143
|
+
return RefundOut(
|
|
144
|
+
id=r.id,
|
|
145
|
+
provider="stripe",
|
|
146
|
+
provider_refund_id=r.id,
|
|
147
|
+
provider_payment_intent_id=getattr(r, "payment_intent", None),
|
|
148
|
+
amount=int(r.amount),
|
|
149
|
+
currency=str(r.currency).upper(),
|
|
150
|
+
status=r.status,
|
|
151
|
+
reason=getattr(r, "reason", None),
|
|
152
|
+
created_at=str(r.created) if getattr(r, "created", None) else None,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _dispute_to_out(d) -> DisputeOut:
|
|
157
|
+
return DisputeOut(
|
|
158
|
+
id=d.id,
|
|
159
|
+
provider="stripe",
|
|
160
|
+
provider_dispute_id=d.id,
|
|
161
|
+
amount=int(d.amount),
|
|
162
|
+
currency=str(d.currency).upper(),
|
|
163
|
+
reason=getattr(d, "reason", None),
|
|
164
|
+
status=d.status,
|
|
165
|
+
evidence_due_by=(
|
|
166
|
+
str(d.evidence_details.get("due_by")) if getattr(d, "evidence_details", None) else None
|
|
167
|
+
),
|
|
168
|
+
created_at=str(d.created) if getattr(d, "created", None) else None,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _payout_to_out(p) -> PayoutOut:
|
|
173
|
+
return PayoutOut(
|
|
174
|
+
id=p.id,
|
|
175
|
+
provider="stripe",
|
|
176
|
+
provider_payout_id=p.id,
|
|
177
|
+
amount=int(p.amount),
|
|
178
|
+
currency=str(p.currency).upper(),
|
|
179
|
+
status=p.status,
|
|
180
|
+
arrival_date=str(p.arrival_date) if getattr(p, "arrival_date", None) else None,
|
|
181
|
+
type=getattr(p, "type", None),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
23
185
|
class StripeAdapter(ProviderAdapter):
|
|
24
186
|
name = "stripe"
|
|
25
187
|
|
|
@@ -34,8 +196,8 @@ class StripeAdapter(ProviderAdapter):
|
|
|
34
196
|
st.stripe.webhook_secret.get_secret_value() if st.stripe.webhook_secret else None
|
|
35
197
|
)
|
|
36
198
|
|
|
199
|
+
# -------- Customers --------
|
|
37
200
|
async def ensure_customer(self, data: CustomerUpsertIn) -> CustomerOut:
|
|
38
|
-
# try by email (idempotent enough for demo; production can map via your DB)
|
|
39
201
|
if data.email:
|
|
40
202
|
existing = await _acall(stripe.Customer.list, email=data.email, limit=1)
|
|
41
203
|
c = (
|
|
@@ -72,6 +234,378 @@ class StripeAdapter(ProviderAdapter):
|
|
|
72
234
|
name=c.get("name"),
|
|
73
235
|
)
|
|
74
236
|
|
|
237
|
+
async def list_customers(
|
|
238
|
+
self, *, provider: str | None, user_id: str | None, limit: int, cursor: str | None
|
|
239
|
+
) -> tuple[list[CustomerOut], str | None]:
|
|
240
|
+
params = {"limit": int(limit)}
|
|
241
|
+
if cursor:
|
|
242
|
+
params["starting_after"] = cursor
|
|
243
|
+
# Stripe has no direct filter for our custom user_id; many teams store mapping in DB.
|
|
244
|
+
# If 'user_id' was stored in metadata, we could search via /v1/customers?limit=... then filter client-side.
|
|
245
|
+
res = await _acall(stripe.Customer.list, **params)
|
|
246
|
+
items = [
|
|
247
|
+
CustomerOut(
|
|
248
|
+
id=c.id,
|
|
249
|
+
provider="stripe",
|
|
250
|
+
provider_customer_id=c.id,
|
|
251
|
+
email=c.get("email"),
|
|
252
|
+
name=c.get("name"),
|
|
253
|
+
)
|
|
254
|
+
for c in res.data
|
|
255
|
+
]
|
|
256
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
257
|
+
return items, next_cursor
|
|
258
|
+
|
|
259
|
+
# -------- Payment Methods --------
|
|
260
|
+
async def attach_payment_method(self, data: PaymentMethodAttachIn) -> PaymentMethodOut:
|
|
261
|
+
pm = await _acall(
|
|
262
|
+
stripe.PaymentMethod.attach,
|
|
263
|
+
data.payment_method_token,
|
|
264
|
+
customer=data.customer_provider_id,
|
|
265
|
+
)
|
|
266
|
+
is_default = False
|
|
267
|
+
if data.make_default:
|
|
268
|
+
cust = await _acall(
|
|
269
|
+
stripe.Customer.modify,
|
|
270
|
+
data.customer_provider_id,
|
|
271
|
+
invoice_settings={"default_payment_method": pm.id},
|
|
272
|
+
)
|
|
273
|
+
is_default = (
|
|
274
|
+
getattr(getattr(cust, "invoice_settings", None), "default_payment_method", None)
|
|
275
|
+
== pm.id
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
cust = await _acall(stripe.Customer.retrieve, data.customer_provider_id)
|
|
279
|
+
is_default = (
|
|
280
|
+
getattr(getattr(cust, "invoice_settings", None), "default_payment_method", None)
|
|
281
|
+
== pm.id
|
|
282
|
+
)
|
|
283
|
+
return _pm_to_out(pm, is_default=is_default)
|
|
284
|
+
|
|
285
|
+
async def list_payment_methods(self, provider_customer_id: str) -> list[PaymentMethodOut]:
|
|
286
|
+
cust = await _acall(stripe.Customer.retrieve, provider_customer_id)
|
|
287
|
+
default_pm = getattr(
|
|
288
|
+
getattr(cust, "invoice_settings", None), "default_payment_method", None
|
|
289
|
+
)
|
|
290
|
+
res = await _acall(stripe.PaymentMethod.list, customer=provider_customer_id, type="card")
|
|
291
|
+
return [_pm_to_out(pm, is_default=(pm.id == default_pm)) for pm in res.data]
|
|
292
|
+
|
|
293
|
+
async def detach_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
|
|
294
|
+
pm = await _acall(stripe.PaymentMethod.detach, provider_method_id)
|
|
295
|
+
# we no longer know default status reliably—fetch customer if set
|
|
296
|
+
cust_id = getattr(pm, "customer", None)
|
|
297
|
+
default_pm = None
|
|
298
|
+
if cust_id:
|
|
299
|
+
cust = await _acall(stripe.Customer.retrieve, cust_id)
|
|
300
|
+
default_pm = getattr(
|
|
301
|
+
getattr(cust, "invoice_settings", None), "default_payment_method", None
|
|
302
|
+
)
|
|
303
|
+
return _pm_to_out(pm, is_default=(pm.id == default_pm))
|
|
304
|
+
|
|
305
|
+
async def set_default_payment_method(
|
|
306
|
+
self, provider_customer_id: str, provider_method_id: str
|
|
307
|
+
) -> PaymentMethodOut:
|
|
308
|
+
cust = await _acall(
|
|
309
|
+
stripe.Customer.modify,
|
|
310
|
+
provider_customer_id,
|
|
311
|
+
invoice_settings={"default_payment_method": provider_method_id},
|
|
312
|
+
)
|
|
313
|
+
pm = await _acall(stripe.PaymentMethod.retrieve, provider_method_id)
|
|
314
|
+
is_default = (
|
|
315
|
+
getattr(getattr(cust, "invoice_settings", None), "default_payment_method", None)
|
|
316
|
+
== pm.id
|
|
317
|
+
)
|
|
318
|
+
return _pm_to_out(pm, is_default=is_default)
|
|
319
|
+
|
|
320
|
+
async def get_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
|
|
321
|
+
pm = await _acall(stripe.PaymentMethod.retrieve, provider_method_id)
|
|
322
|
+
cust_id = getattr(pm, "customer", None)
|
|
323
|
+
default_pm = None
|
|
324
|
+
if cust_id:
|
|
325
|
+
cust = await _acall(stripe.Customer.retrieve, cust_id)
|
|
326
|
+
default_pm = getattr(
|
|
327
|
+
getattr(cust, "invoice_settings", None), "default_payment_method", None
|
|
328
|
+
)
|
|
329
|
+
return _pm_to_out(pm, is_default=(pm.id == default_pm))
|
|
330
|
+
|
|
331
|
+
async def update_payment_method(
|
|
332
|
+
self, provider_method_id: str, data: PaymentMethodUpdateIn
|
|
333
|
+
) -> PaymentMethodOut:
|
|
334
|
+
update: dict[str, Any] = {}
|
|
335
|
+
if data.name is not None:
|
|
336
|
+
update["billing_details"] = {"name": data.name}
|
|
337
|
+
if data.exp_month is not None or data.exp_year is not None:
|
|
338
|
+
update["card"] = {}
|
|
339
|
+
if data.exp_month is not None:
|
|
340
|
+
update["card"]["exp_month"] = data.exp_month
|
|
341
|
+
if data.exp_year is not None:
|
|
342
|
+
update["card"]["exp_year"] = data.exp_year
|
|
343
|
+
pm = (
|
|
344
|
+
await _acall(stripe.PaymentMethod.modify, provider_method_id, **update)
|
|
345
|
+
if update
|
|
346
|
+
else await _acall(stripe.PaymentMethod.retrieve, provider_method_id)
|
|
347
|
+
)
|
|
348
|
+
cust_id = getattr(pm, "customer", None)
|
|
349
|
+
default_pm = None
|
|
350
|
+
if cust_id:
|
|
351
|
+
cust = await _acall(stripe.Customer.retrieve, cust_id)
|
|
352
|
+
default_pm = getattr(
|
|
353
|
+
getattr(cust, "invoice_settings", None), "default_payment_method", None
|
|
354
|
+
)
|
|
355
|
+
return _pm_to_out(pm, is_default=(pm.id == default_pm))
|
|
356
|
+
|
|
357
|
+
# -------- Products / Prices --------
|
|
358
|
+
async def create_product(self, data: ProductCreateIn) -> ProductOut:
|
|
359
|
+
p = await _acall(stripe.Product.create, name=data.name, active=bool(data.active))
|
|
360
|
+
return _product_to_out(p)
|
|
361
|
+
|
|
362
|
+
async def get_product(self, provider_product_id: str) -> ProductOut:
|
|
363
|
+
p = await _acall(stripe.Product.retrieve, provider_product_id)
|
|
364
|
+
return _product_to_out(p)
|
|
365
|
+
|
|
366
|
+
async def list_products(
|
|
367
|
+
self, *, active: bool | None, limit: int, cursor: str | None
|
|
368
|
+
) -> tuple[list[ProductOut], str | None]:
|
|
369
|
+
params = {"limit": int(limit)}
|
|
370
|
+
if active is not None:
|
|
371
|
+
params["active"] = bool(active)
|
|
372
|
+
if cursor:
|
|
373
|
+
params["starting_after"] = cursor
|
|
374
|
+
res = await _acall(stripe.Product.list, **params)
|
|
375
|
+
items = [_product_to_out(p) for p in res.data]
|
|
376
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
377
|
+
return items, next_cursor
|
|
378
|
+
|
|
379
|
+
async def update_product(self, provider_product_id: str, data: ProductUpdateIn) -> ProductOut:
|
|
380
|
+
update: dict[str, Any] = {}
|
|
381
|
+
if data.name is not None:
|
|
382
|
+
update["name"] = data.name
|
|
383
|
+
if data.active is not None:
|
|
384
|
+
update["active"] = bool(data.active)
|
|
385
|
+
p = (
|
|
386
|
+
await _acall(stripe.Product.modify, provider_product_id, **update)
|
|
387
|
+
if update
|
|
388
|
+
else await _acall(stripe.Product.retrieve, provider_product_id)
|
|
389
|
+
)
|
|
390
|
+
return _product_to_out(p)
|
|
391
|
+
|
|
392
|
+
async def create_price(self, data: PriceCreateIn) -> PriceOut:
|
|
393
|
+
kwargs: dict[str, Any] = dict(
|
|
394
|
+
product=data.provider_product_id,
|
|
395
|
+
currency=data.currency.lower(),
|
|
396
|
+
unit_amount=int(data.unit_amount),
|
|
397
|
+
active=bool(data.active),
|
|
398
|
+
)
|
|
399
|
+
if data.interval:
|
|
400
|
+
kwargs["recurring"] = {"interval": data.interval}
|
|
401
|
+
if data.trial_days is not None:
|
|
402
|
+
kwargs["trial_period_days"] = int(data.trial_days)
|
|
403
|
+
pr = await _acall(stripe.Price.create, **kwargs)
|
|
404
|
+
return _price_to_out(pr)
|
|
405
|
+
|
|
406
|
+
async def get_price(self, provider_price_id: str) -> PriceOut:
|
|
407
|
+
pr = await _acall(stripe.Price.retrieve, provider_price_id)
|
|
408
|
+
return _price_to_out(pr)
|
|
409
|
+
|
|
410
|
+
async def list_prices(
|
|
411
|
+
self,
|
|
412
|
+
*,
|
|
413
|
+
provider_product_id: str | None,
|
|
414
|
+
active: bool | None,
|
|
415
|
+
limit: int,
|
|
416
|
+
cursor: str | None,
|
|
417
|
+
) -> tuple[list[PriceOut], str | None]:
|
|
418
|
+
params = {"limit": int(limit)}
|
|
419
|
+
if provider_product_id:
|
|
420
|
+
params["product"] = provider_product_id
|
|
421
|
+
if active is not None:
|
|
422
|
+
params["active"] = bool(active)
|
|
423
|
+
if cursor:
|
|
424
|
+
params["starting_after"] = cursor
|
|
425
|
+
res = await _acall(stripe.Price.list, **params)
|
|
426
|
+
items = [_price_to_out(p) for p in res.data]
|
|
427
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
428
|
+
return items, next_cursor
|
|
429
|
+
|
|
430
|
+
async def update_price(self, provider_price_id: str, data: PriceUpdateIn) -> PriceOut:
|
|
431
|
+
# Stripe allows toggling `active` and updating metadata, but not amount/currency/product.
|
|
432
|
+
update: dict[str, Any] = {}
|
|
433
|
+
if data.active is not None:
|
|
434
|
+
update["active"] = bool(data.active)
|
|
435
|
+
pr = (
|
|
436
|
+
await _acall(stripe.Price.modify, provider_price_id, **update)
|
|
437
|
+
if update
|
|
438
|
+
else await _acall(stripe.Price.retrieve, provider_price_id)
|
|
439
|
+
)
|
|
440
|
+
return _price_to_out(pr)
|
|
441
|
+
|
|
442
|
+
# -------- Subscriptions --------
|
|
443
|
+
async def create_subscription(self, data: SubscriptionCreateIn) -> SubscriptionOut:
|
|
444
|
+
kwargs: dict[str, Any] = dict(
|
|
445
|
+
customer=data.customer_provider_id,
|
|
446
|
+
items=[{"price": data.price_provider_id, "quantity": int(data.quantity)}],
|
|
447
|
+
proration_behavior=data.proration_behavior,
|
|
448
|
+
)
|
|
449
|
+
if data.trial_days is not None:
|
|
450
|
+
kwargs["trial_period_days"] = int(data.trial_days)
|
|
451
|
+
s = await _acall(stripe.Subscription.create, **kwargs)
|
|
452
|
+
return _sub_to_out(s)
|
|
453
|
+
|
|
454
|
+
async def update_subscription(
|
|
455
|
+
self, provider_subscription_id: str, data: SubscriptionUpdateIn
|
|
456
|
+
) -> SubscriptionOut:
|
|
457
|
+
s = await _acall(stripe.Subscription.retrieve, provider_subscription_id, expand=["items"])
|
|
458
|
+
items = s.items.data
|
|
459
|
+
update_kwargs: dict[str, Any] = {"proration_behavior": data.proration_behavior}
|
|
460
|
+
# update first item (simple plan model)
|
|
461
|
+
if items:
|
|
462
|
+
first_item = items[0]
|
|
463
|
+
item_update = {"id": first_item.id}
|
|
464
|
+
if data.price_provider_id:
|
|
465
|
+
item_update["price"] = data.price_provider_id
|
|
466
|
+
if data.quantity is not None:
|
|
467
|
+
item_update["quantity"] = int(data.quantity)
|
|
468
|
+
update_kwargs["items"] = [item_update]
|
|
469
|
+
if data.cancel_at_period_end is not None:
|
|
470
|
+
update_kwargs["cancel_at_period_end"] = bool(data.cancel_at_period_end)
|
|
471
|
+
s2 = await _acall(stripe.Subscription.modify, provider_subscription_id, **update_kwargs)
|
|
472
|
+
return _sub_to_out(s2)
|
|
473
|
+
|
|
474
|
+
async def cancel_subscription(
|
|
475
|
+
self, provider_subscription_id: str, at_period_end: bool = True
|
|
476
|
+
) -> SubscriptionOut:
|
|
477
|
+
s = await _acall(
|
|
478
|
+
stripe.Subscription.cancel if not at_period_end else stripe.Subscription.modify,
|
|
479
|
+
provider_subscription_id,
|
|
480
|
+
**({} if not at_period_end else {"cancel_at_period_end": True}),
|
|
481
|
+
)
|
|
482
|
+
return _sub_to_out(s)
|
|
483
|
+
|
|
484
|
+
async def get_subscription(self, provider_subscription_id: str) -> SubscriptionOut:
|
|
485
|
+
s = await _acall(stripe.Subscription.retrieve, provider_subscription_id, expand=["items"])
|
|
486
|
+
return _sub_to_out(s)
|
|
487
|
+
|
|
488
|
+
async def list_subscriptions(
|
|
489
|
+
self,
|
|
490
|
+
*,
|
|
491
|
+
customer_provider_id: str | None,
|
|
492
|
+
status: str | None,
|
|
493
|
+
limit: int,
|
|
494
|
+
cursor: str | None,
|
|
495
|
+
) -> tuple[list[SubscriptionOut], str | None]:
|
|
496
|
+
params = {"limit": int(limit)}
|
|
497
|
+
if customer_provider_id:
|
|
498
|
+
params["customer"] = customer_provider_id
|
|
499
|
+
if status:
|
|
500
|
+
params["status"] = status
|
|
501
|
+
if cursor:
|
|
502
|
+
params["starting_after"] = cursor
|
|
503
|
+
res = await _acall(stripe.Subscription.list, **params)
|
|
504
|
+
items = [_sub_to_out(s) for s in res.data]
|
|
505
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
506
|
+
return items, next_cursor
|
|
507
|
+
|
|
508
|
+
# -------- Invoices --------
|
|
509
|
+
async def create_invoice(self, data: InvoiceCreateIn) -> InvoiceOut:
|
|
510
|
+
inv = await _acall(
|
|
511
|
+
stripe.Invoice.create,
|
|
512
|
+
customer=data.customer_provider_id,
|
|
513
|
+
auto_advance=bool(data.auto_advance),
|
|
514
|
+
)
|
|
515
|
+
return _inv_to_out(inv)
|
|
516
|
+
|
|
517
|
+
async def finalize_invoice(self, provider_invoice_id: str) -> InvoiceOut:
|
|
518
|
+
inv = await _acall(stripe.Invoice.finalize_invoice, provider_invoice_id)
|
|
519
|
+
return _inv_to_out(inv)
|
|
520
|
+
|
|
521
|
+
async def void_invoice(self, provider_invoice_id: str) -> InvoiceOut:
|
|
522
|
+
inv = await _acall(stripe.Invoice.void_invoice, provider_invoice_id)
|
|
523
|
+
return _inv_to_out(inv)
|
|
524
|
+
|
|
525
|
+
async def pay_invoice(self, provider_invoice_id: str) -> InvoiceOut:
|
|
526
|
+
inv = await _acall(stripe.Invoice.pay, provider_invoice_id)
|
|
527
|
+
return _inv_to_out(inv)
|
|
528
|
+
|
|
529
|
+
async def add_invoice_line_item(
|
|
530
|
+
self, provider_invoice_id: str, data: InvoiceLineItemIn
|
|
531
|
+
) -> InvoiceOut:
|
|
532
|
+
# attach an item to a DRAFT invoice
|
|
533
|
+
kwargs: dict[str, Any] = dict(
|
|
534
|
+
invoice=provider_invoice_id,
|
|
535
|
+
customer=data.customer_provider_id,
|
|
536
|
+
quantity=int(data.quantity or 1),
|
|
537
|
+
currency=data.currency.lower(),
|
|
538
|
+
description=data.description or None,
|
|
539
|
+
)
|
|
540
|
+
if data.provider_price_id:
|
|
541
|
+
kwargs["price"] = data.provider_price_id
|
|
542
|
+
else:
|
|
543
|
+
kwargs["unit_amount"] = int(data.unit_amount)
|
|
544
|
+
await _acall(
|
|
545
|
+
stripe.InvoiceItem.create, **{k: v for k, v in kwargs.items() if v is not None}
|
|
546
|
+
)
|
|
547
|
+
inv = await _acall(stripe.Invoice.retrieve, provider_invoice_id)
|
|
548
|
+
return _inv_to_out(inv)
|
|
549
|
+
|
|
550
|
+
async def list_invoices(
|
|
551
|
+
self,
|
|
552
|
+
*,
|
|
553
|
+
customer_provider_id: str | None,
|
|
554
|
+
status: str | None,
|
|
555
|
+
limit: int,
|
|
556
|
+
cursor: str | None,
|
|
557
|
+
) -> tuple[list[InvoiceOut], str | None]:
|
|
558
|
+
params = {"limit": int(limit)}
|
|
559
|
+
if customer_provider_id:
|
|
560
|
+
params["customer"] = customer_provider_id
|
|
561
|
+
if status:
|
|
562
|
+
params["status"] = status
|
|
563
|
+
if cursor:
|
|
564
|
+
params["starting_after"] = cursor
|
|
565
|
+
res = await _acall(stripe.Invoice.list, **params)
|
|
566
|
+
items = [_inv_to_out(inv) for inv in res.data]
|
|
567
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
568
|
+
return items, next_cursor
|
|
569
|
+
|
|
570
|
+
async def get_invoice(self, provider_invoice_id: str) -> InvoiceOut:
|
|
571
|
+
inv = await _acall(stripe.Invoice.retrieve, provider_invoice_id)
|
|
572
|
+
return _inv_to_out(inv)
|
|
573
|
+
|
|
574
|
+
async def preview_invoice(
|
|
575
|
+
self, *, customer_provider_id: str, subscription_id: str | None = None
|
|
576
|
+
) -> InvoiceOut:
|
|
577
|
+
params = {"customer": customer_provider_id}
|
|
578
|
+
if subscription_id:
|
|
579
|
+
params["subscription"] = subscription_id
|
|
580
|
+
inv = await _acall(stripe.Invoice.upcoming, **params)
|
|
581
|
+
return _inv_to_out(inv)
|
|
582
|
+
|
|
583
|
+
async def list_invoice_line_items(
|
|
584
|
+
self, provider_invoice_id: str, *, limit: int, cursor: str | None
|
|
585
|
+
) -> tuple[list[InvoiceLineItemOut], str | None]:
|
|
586
|
+
params = {"limit": int(limit)}
|
|
587
|
+
if cursor:
|
|
588
|
+
params["starting_after"] = cursor
|
|
589
|
+
res = await _acall(stripe.Invoice.list_lines, provider_invoice_id, **params)
|
|
590
|
+
items: list[InvoiceLineItemOut] = []
|
|
591
|
+
for li in res.data:
|
|
592
|
+
amount = int(getattr(li, "amount", 0))
|
|
593
|
+
currency = str(getattr(li, "currency", "USD")).upper()
|
|
594
|
+
price_id = getattr(getattr(li, "price", None), "id", None)
|
|
595
|
+
items.append(
|
|
596
|
+
InvoiceLineItemOut(
|
|
597
|
+
id=li.id,
|
|
598
|
+
description=getattr(li, "description", None),
|
|
599
|
+
amount=amount,
|
|
600
|
+
currency=currency,
|
|
601
|
+
quantity=getattr(li, "quantity", 1),
|
|
602
|
+
provider_price_id=price_id,
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
606
|
+
return items, next_cursor
|
|
607
|
+
|
|
608
|
+
# -------- Intents --------
|
|
75
609
|
async def create_intent(self, data: IntentCreateIn, *, user_id: str | None) -> IntentOut:
|
|
76
610
|
kwargs: dict[str, Any] = dict(
|
|
77
611
|
amount=int(data.amount),
|
|
@@ -83,46 +617,19 @@ class StripeAdapter(ProviderAdapter):
|
|
|
83
617
|
if data.payment_method_types:
|
|
84
618
|
kwargs["payment_method_types"] = data.payment_method_types
|
|
85
619
|
pi = await _acall(
|
|
86
|
-
stripe.PaymentIntent.create,
|
|
87
|
-
**{k: v for k, v in kwargs.items() if v is not None},
|
|
88
|
-
)
|
|
89
|
-
return IntentOut(
|
|
90
|
-
id=pi.id,
|
|
91
|
-
provider="stripe",
|
|
92
|
-
provider_intent_id=pi.id,
|
|
93
|
-
status=pi.status,
|
|
94
|
-
amount=int(pi.amount),
|
|
95
|
-
currency=pi.currency.upper(),
|
|
96
|
-
client_secret=pi.client_secret,
|
|
97
|
-
next_action=NextAction(type=getattr(getattr(pi, "next_action", None), "type", None)),
|
|
620
|
+
stripe.PaymentIntent.create, **{k: v for k, v in kwargs.items() if v is not None}
|
|
98
621
|
)
|
|
622
|
+
return _pi_to_out(pi)
|
|
99
623
|
|
|
100
624
|
async def confirm_intent(self, provider_intent_id: str) -> IntentOut:
|
|
101
625
|
pi = await _acall(stripe.PaymentIntent.confirm, provider_intent_id)
|
|
102
|
-
return
|
|
103
|
-
id=pi.id,
|
|
104
|
-
provider="stripe",
|
|
105
|
-
provider_intent_id=pi.id,
|
|
106
|
-
status=pi.status,
|
|
107
|
-
amount=int(pi.amount),
|
|
108
|
-
currency=pi.currency.upper(),
|
|
109
|
-
client_secret=getattr(pi, "client_secret", None),
|
|
110
|
-
next_action=NextAction(type=getattr(getattr(pi, "next_action", None), "type", None)),
|
|
111
|
-
)
|
|
626
|
+
return _pi_to_out(pi)
|
|
112
627
|
|
|
113
628
|
async def cancel_intent(self, provider_intent_id: str) -> IntentOut:
|
|
114
629
|
pi = await _acall(stripe.PaymentIntent.cancel, provider_intent_id)
|
|
115
|
-
return
|
|
116
|
-
id=pi.id,
|
|
117
|
-
provider="stripe",
|
|
118
|
-
provider_intent_id=pi.id,
|
|
119
|
-
status=pi.status,
|
|
120
|
-
amount=int(pi.amount),
|
|
121
|
-
currency=pi.currency.upper(),
|
|
122
|
-
)
|
|
630
|
+
return _pi_to_out(pi)
|
|
123
631
|
|
|
124
632
|
async def refund(self, provider_intent_id: str, data: RefundIn) -> IntentOut:
|
|
125
|
-
# Stripe refunds are created against charges; simplify via PaymentIntent last charge
|
|
126
633
|
pi = await _acall(
|
|
127
634
|
stripe.PaymentIntent.retrieve, provider_intent_id, expand=["latest_charge"]
|
|
128
635
|
)
|
|
@@ -133,10 +640,225 @@ class StripeAdapter(ProviderAdapter):
|
|
|
133
640
|
stripe.Refund.create,
|
|
134
641
|
charge=charge_id,
|
|
135
642
|
amount=int(data.amount) if data.amount else None,
|
|
643
|
+
reason=data.reason or None,
|
|
644
|
+
)
|
|
645
|
+
return await self.hydrate_intent(provider_intent_id)
|
|
646
|
+
|
|
647
|
+
async def hydrate_intent(self, provider_intent_id: str) -> IntentOut:
|
|
648
|
+
pi = await _acall(stripe.PaymentIntent.retrieve, provider_intent_id)
|
|
649
|
+
return _pi_to_out(pi)
|
|
650
|
+
|
|
651
|
+
async def capture_intent(self, provider_intent_id: str, *, amount: int | None) -> IntentOut:
|
|
652
|
+
kwargs = {}
|
|
653
|
+
if amount is not None:
|
|
654
|
+
kwargs["amount_to_capture"] = int(amount)
|
|
655
|
+
pi = await _acall(stripe.PaymentIntent.capture, provider_intent_id, **kwargs)
|
|
656
|
+
return _pi_to_out(pi)
|
|
657
|
+
|
|
658
|
+
async def list_intents(
|
|
659
|
+
self,
|
|
660
|
+
*,
|
|
661
|
+
customer_provider_id: str | None,
|
|
662
|
+
status: str | None,
|
|
663
|
+
limit: int,
|
|
664
|
+
cursor: str | None,
|
|
665
|
+
) -> tuple[list[IntentOut], str | None]:
|
|
666
|
+
params = {"limit": int(limit)}
|
|
667
|
+
if customer_provider_id:
|
|
668
|
+
params["customer"] = customer_provider_id
|
|
669
|
+
if status:
|
|
670
|
+
params["status"] = status
|
|
671
|
+
if cursor:
|
|
672
|
+
params["starting_after"] = cursor
|
|
673
|
+
res = await _acall(stripe.PaymentIntent.list, **params)
|
|
674
|
+
items = [_pi_to_out(pi) for pi in res.data]
|
|
675
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
676
|
+
return items, next_cursor
|
|
677
|
+
|
|
678
|
+
# ---- Setup Intents (off-session readiness) ----
|
|
679
|
+
async def create_setup_intent(self, data: SetupIntentCreateIn) -> SetupIntentOut:
|
|
680
|
+
si = await _acall(
|
|
681
|
+
stripe.SetupIntent.create, payment_method_types=data.payment_method_types or ["card"]
|
|
682
|
+
)
|
|
683
|
+
return SetupIntentOut(
|
|
684
|
+
id=si.id,
|
|
685
|
+
provider="stripe",
|
|
686
|
+
provider_setup_intent_id=si.id,
|
|
687
|
+
status=si.status,
|
|
688
|
+
client_secret=getattr(si, "client_secret", None),
|
|
689
|
+
next_action=NextAction(type=getattr(getattr(si, "next_action", None), "type", None)),
|
|
136
690
|
)
|
|
137
|
-
|
|
691
|
+
|
|
692
|
+
async def confirm_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
|
|
693
|
+
si = await _acall(stripe.SetupIntent.confirm, provider_setup_intent_id)
|
|
694
|
+
return SetupIntentOut(
|
|
695
|
+
id=si.id,
|
|
696
|
+
provider="stripe",
|
|
697
|
+
provider_setup_intent_id=si.id,
|
|
698
|
+
status=si.status,
|
|
699
|
+
client_secret=getattr(si, "client_secret", None),
|
|
700
|
+
next_action=NextAction(type=getattr(getattr(si, "next_action", None), "type", None)),
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
async def get_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
|
|
704
|
+
si = await _acall(stripe.SetupIntent.retrieve, provider_setup_intent_id)
|
|
705
|
+
return SetupIntentOut(
|
|
706
|
+
id=si.id,
|
|
707
|
+
provider="stripe",
|
|
708
|
+
provider_setup_intent_id=si.id,
|
|
709
|
+
status=si.status,
|
|
710
|
+
client_secret=getattr(si, "client_secret", None),
|
|
711
|
+
next_action=NextAction(type=getattr(getattr(si, "next_action", None), "type", None)),
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
# ---- 3DS/SCA resume ----
|
|
715
|
+
async def resume_intent_after_action(self, provider_intent_id: str) -> IntentOut:
|
|
716
|
+
# For Stripe, retrieving after the customer completes next_action is sufficient
|
|
138
717
|
return await self.hydrate_intent(provider_intent_id)
|
|
139
718
|
|
|
719
|
+
# -------- Disputes --------
|
|
720
|
+
async def list_disputes(
|
|
721
|
+
self, *, status: str | None, limit: int, cursor: str | None
|
|
722
|
+
) -> tuple[list[DisputeOut], str | None]:
|
|
723
|
+
params = {"limit": int(limit)}
|
|
724
|
+
if status:
|
|
725
|
+
params["status"] = status
|
|
726
|
+
if cursor:
|
|
727
|
+
params["starting_after"] = cursor
|
|
728
|
+
res = await _acall(stripe.Dispute.list, **params)
|
|
729
|
+
items = [_dispute_to_out(d) for d in res.data]
|
|
730
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
731
|
+
return items, next_cursor
|
|
732
|
+
|
|
733
|
+
async def get_dispute(self, provider_dispute_id: str) -> DisputeOut:
|
|
734
|
+
d = await _acall(stripe.Dispute.retrieve, provider_dispute_id)
|
|
735
|
+
return _dispute_to_out(d)
|
|
736
|
+
|
|
737
|
+
async def submit_dispute_evidence(self, provider_dispute_id: str, evidence: dict) -> DisputeOut:
|
|
738
|
+
d = await _acall(stripe.Dispute.modify, provider_dispute_id, evidence=evidence)
|
|
739
|
+
# Some disputes require explicit submit call:
|
|
740
|
+
try:
|
|
741
|
+
d = await _acall(stripe.Dispute.submit, provider_dispute_id)
|
|
742
|
+
except Exception:
|
|
743
|
+
pass
|
|
744
|
+
return _dispute_to_out(d)
|
|
745
|
+
|
|
746
|
+
# -------- Balance & Payouts --------
|
|
747
|
+
async def get_balance_snapshot(self) -> BalanceSnapshotOut:
|
|
748
|
+
bal = await _acall(stripe.Balance.retrieve)
|
|
749
|
+
|
|
750
|
+
def _bucket(entries):
|
|
751
|
+
out = []
|
|
752
|
+
for b in entries or []:
|
|
753
|
+
out.append({"currency": str(b.currency).upper(), "amount": int(b.amount)})
|
|
754
|
+
return out
|
|
755
|
+
|
|
756
|
+
return BalanceSnapshotOut(
|
|
757
|
+
available=_bucket(getattr(bal, "available", [])),
|
|
758
|
+
pending=_bucket(getattr(bal, "pending", [])),
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
async def list_payouts(
|
|
762
|
+
self, *, limit: int, cursor: str | None
|
|
763
|
+
) -> tuple[list[PayoutOut], str | None]:
|
|
764
|
+
params = {"limit": int(limit)}
|
|
765
|
+
if cursor:
|
|
766
|
+
params["starting_after"] = cursor
|
|
767
|
+
res = await _acall(stripe.Payout.list, **params)
|
|
768
|
+
items = [_payout_to_out(p) for p in res.data]
|
|
769
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
770
|
+
return items, next_cursor
|
|
771
|
+
|
|
772
|
+
async def get_payout(self, provider_payout_id: str) -> PayoutOut:
|
|
773
|
+
p = await _acall(stripe.Payout.retrieve, provider_payout_id)
|
|
774
|
+
return _payout_to_out(p)
|
|
775
|
+
|
|
776
|
+
# -------- Refunds (list/get) --------
|
|
777
|
+
async def list_refunds(
|
|
778
|
+
self, *, provider_payment_intent_id: str | None, limit: int, cursor: str | None
|
|
779
|
+
) -> tuple[list[RefundOut], str | None]:
|
|
780
|
+
params = {"limit": int(limit)}
|
|
781
|
+
if provider_payment_intent_id:
|
|
782
|
+
params["payment_intent"] = provider_payment_intent_id
|
|
783
|
+
if cursor:
|
|
784
|
+
params["starting_after"] = cursor
|
|
785
|
+
res = await _acall(stripe.Refund.list, **params)
|
|
786
|
+
items = [_refund_to_out(r) for r in res.data]
|
|
787
|
+
next_cursor = res.data[-1].id if getattr(res, "has_more", False) and res.data else None
|
|
788
|
+
return items, next_cursor
|
|
789
|
+
|
|
790
|
+
async def get_refund(self, provider_refund_id: str) -> RefundOut:
|
|
791
|
+
r = await _acall(stripe.Refund.retrieve, provider_refund_id)
|
|
792
|
+
return _refund_to_out(r)
|
|
793
|
+
|
|
794
|
+
# -------- Usage (create/list/get) --------
|
|
795
|
+
async def create_usage_record(self, data: UsageRecordIn) -> UsageRecordOut:
|
|
796
|
+
if not data.subscription_item and not data.provider_price_id:
|
|
797
|
+
raise ValueError("subscription_item or provider_price_id is required")
|
|
798
|
+
sub_item = data.subscription_item
|
|
799
|
+
if not sub_item and data.provider_price_id:
|
|
800
|
+
items = await _acall(
|
|
801
|
+
stripe.SubscriptionItem.list, price=data.provider_price_id, limit=1
|
|
802
|
+
)
|
|
803
|
+
sub_item = items.data[0].id if items.data else None
|
|
804
|
+
if not sub_item:
|
|
805
|
+
raise ValueError("No subscription item found for usage record")
|
|
806
|
+
body = {
|
|
807
|
+
"subscription_item": sub_item,
|
|
808
|
+
"quantity": int(data.quantity),
|
|
809
|
+
"action": data.action or "increment",
|
|
810
|
+
}
|
|
811
|
+
if data.timestamp:
|
|
812
|
+
body["timestamp"] = int(data.timestamp)
|
|
813
|
+
rec = await _acall(stripe.UsageRecord.create, **body)
|
|
814
|
+
return UsageRecordOut(
|
|
815
|
+
id=rec.id,
|
|
816
|
+
quantity=int(rec.quantity),
|
|
817
|
+
timestamp=getattr(rec, "timestamp", None),
|
|
818
|
+
subscription_item=sub_item,
|
|
819
|
+
provider_price_id=data.provider_price_id,
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
async def list_usage_records(
|
|
823
|
+
self, f: UsageRecordListFilter
|
|
824
|
+
) -> tuple[list[UsageRecordOut], str | None]:
|
|
825
|
+
# Stripe exposes *summaries* per period. We surface them as list results.
|
|
826
|
+
sub_item = f.subscription_item
|
|
827
|
+
if not sub_item and f.provider_price_id:
|
|
828
|
+
items = await _acall(stripe.SubscriptionItem.list, price=f.provider_price_id, limit=1)
|
|
829
|
+
sub_item = items.data[0].id if items.data else None
|
|
830
|
+
if not sub_item:
|
|
831
|
+
return [], None
|
|
832
|
+
params = {"limit": int(f.limit or 50)}
|
|
833
|
+
if f.cursor:
|
|
834
|
+
params["starting_after"] = f.cursor
|
|
835
|
+
res = await _acall(stripe.SubscriptionItem.list_usage_record_summaries, sub_item, **params)
|
|
836
|
+
items: list[UsageRecordOut] = []
|
|
837
|
+
for s in res.data:
|
|
838
|
+
# No record id in summaries—synthesize a stable id from period start.
|
|
839
|
+
synthesized_id = f"{sub_item}:{getattr(s, 'period', {}).get('start')}"
|
|
840
|
+
items.append(
|
|
841
|
+
UsageRecordOut(
|
|
842
|
+
id=synthesized_id,
|
|
843
|
+
quantity=int(getattr(s, "total_usage", 0)),
|
|
844
|
+
timestamp=getattr(s, "period", {}).get("end"),
|
|
845
|
+
subscription_item=sub_item,
|
|
846
|
+
provider_price_id=f.provider_price_id,
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
next_cursor = (
|
|
850
|
+
res.data[-1].id
|
|
851
|
+
if getattr(res, "has_more", False) and res.data and hasattr(res.data[-1], "id")
|
|
852
|
+
else None
|
|
853
|
+
)
|
|
854
|
+
return items, next_cursor
|
|
855
|
+
|
|
856
|
+
async def get_usage_record(self, usage_record_id: str) -> UsageRecordOut:
|
|
857
|
+
# Stripe has no direct "retrieve usage record by id" API.
|
|
858
|
+
# You can reconstruct via list summaries or store records locally when creating.
|
|
859
|
+
raise NotImplementedError("Stripe does not support retrieving a single usage record by id")
|
|
860
|
+
|
|
861
|
+
# -------- Webhooks --------
|
|
140
862
|
async def verify_and_parse_webhook(
|
|
141
863
|
self, signature: str | None, payload: bytes
|
|
142
864
|
) -> dict[str, Any]:
|
|
@@ -149,16 +871,3 @@ class StripeAdapter(ProviderAdapter):
|
|
|
149
871
|
secret=self._wh_secret,
|
|
150
872
|
)
|
|
151
873
|
return {"id": event.id, "type": event.type, "data": event.data.object}
|
|
152
|
-
|
|
153
|
-
async def hydrate_intent(self, provider_intent_id: str) -> IntentOut:
|
|
154
|
-
pi = await _acall(stripe.PaymentIntent.retrieve, provider_intent_id)
|
|
155
|
-
return IntentOut(
|
|
156
|
-
id=pi.id,
|
|
157
|
-
provider="stripe",
|
|
158
|
-
provider_intent_id=pi.id,
|
|
159
|
-
status=pi.status,
|
|
160
|
-
amount=int(pi.amount),
|
|
161
|
-
currency=pi.currency.upper(),
|
|
162
|
-
client_secret=getattr(pi, "client_secret", None),
|
|
163
|
-
next_action=NextAction(type=getattr(getattr(pi, "next_action", None), "type", None)),
|
|
164
|
-
)
|