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
|
@@ -8,33 +8,54 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
8
8
|
from .models import (
|
|
9
9
|
LedgerEntry,
|
|
10
10
|
PayCustomer,
|
|
11
|
+
PayDispute,
|
|
11
12
|
PayEvent,
|
|
12
13
|
PayIntent,
|
|
13
14
|
PayInvoice,
|
|
14
15
|
PayPaymentMethod,
|
|
16
|
+
PayPayout,
|
|
15
17
|
PayPrice,
|
|
16
18
|
PayProduct,
|
|
19
|
+
PaySetupIntent,
|
|
17
20
|
PaySubscription,
|
|
18
21
|
)
|
|
19
22
|
from .provider.registry import get_provider_registry
|
|
20
23
|
from .schemas import (
|
|
24
|
+
BalanceSnapshotOut,
|
|
25
|
+
CaptureIn,
|
|
21
26
|
CustomerOut,
|
|
27
|
+
CustomersListFilter,
|
|
22
28
|
CustomerUpsertIn,
|
|
29
|
+
DisputeOut,
|
|
23
30
|
IntentCreateIn,
|
|
31
|
+
IntentListFilter,
|
|
24
32
|
IntentOut,
|
|
25
33
|
InvoiceCreateIn,
|
|
34
|
+
InvoiceLineItemIn,
|
|
35
|
+
InvoiceLineItemOut,
|
|
26
36
|
InvoiceOut,
|
|
37
|
+
InvoicesListFilter,
|
|
27
38
|
PaymentMethodAttachIn,
|
|
28
39
|
PaymentMethodOut,
|
|
40
|
+
PaymentMethodUpdateIn,
|
|
41
|
+
PayoutOut,
|
|
29
42
|
PriceCreateIn,
|
|
30
43
|
PriceOut,
|
|
44
|
+
PriceUpdateIn,
|
|
31
45
|
ProductCreateIn,
|
|
32
46
|
ProductOut,
|
|
47
|
+
ProductUpdateIn,
|
|
33
48
|
RefundIn,
|
|
49
|
+
RefundOut,
|
|
50
|
+
SetupIntentCreateIn,
|
|
51
|
+
SetupIntentOut,
|
|
34
52
|
StatementRow,
|
|
35
53
|
SubscriptionCreateIn,
|
|
36
54
|
SubscriptionOut,
|
|
37
55
|
SubscriptionUpdateIn,
|
|
56
|
+
UsageRecordIn,
|
|
57
|
+
UsageRecordListFilter,
|
|
58
|
+
UsageRecordOut,
|
|
38
59
|
)
|
|
39
60
|
from .settings import get_payments_settings
|
|
40
61
|
|
|
@@ -44,9 +65,24 @@ def _default_provider_name() -> str:
|
|
|
44
65
|
|
|
45
66
|
|
|
46
67
|
class PaymentsService:
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
"""Payments service facade wrapping provider adapters and persisting key rows.
|
|
69
|
+
|
|
70
|
+
NOTE: tenant_id is now required for all persistence operations. This is a breaking
|
|
71
|
+
change; callers must supply a valid tenant scope. (Future: could allow multi-tenant
|
|
72
|
+
mapping via adapter registry.)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
session: AsyncSession,
|
|
78
|
+
*,
|
|
79
|
+
tenant_id: str,
|
|
80
|
+
provider_name: Optional[str] = None,
|
|
81
|
+
):
|
|
82
|
+
if not tenant_id:
|
|
83
|
+
raise ValueError("tenant_id is required for PaymentsService")
|
|
49
84
|
self.session = session
|
|
85
|
+
self.tenant_id = tenant_id
|
|
50
86
|
self._provider_name = (provider_name or _default_provider_name()).lower()
|
|
51
87
|
self._adapter = None # resolved on first use
|
|
52
88
|
|
|
@@ -68,6 +104,19 @@ class PaymentsService:
|
|
|
68
104
|
) from e
|
|
69
105
|
return self._adapter
|
|
70
106
|
|
|
107
|
+
# --- internal event dispatcher (shared by webhook + replay) ---------------
|
|
108
|
+
async def _dispatch_event(self, provider: str, parsed: dict) -> None:
|
|
109
|
+
typ = parsed.get("type", "")
|
|
110
|
+
obj = parsed.get("data") or {}
|
|
111
|
+
|
|
112
|
+
if provider == "stripe":
|
|
113
|
+
if typ == "payment_intent.succeeded":
|
|
114
|
+
await self._post_sale(obj)
|
|
115
|
+
elif typ == "charge.refunded":
|
|
116
|
+
await self._post_refund(obj)
|
|
117
|
+
elif typ == "charge.captured":
|
|
118
|
+
await self._post_capture(obj)
|
|
119
|
+
|
|
71
120
|
# --- Customers ------------------------------------------------------------
|
|
72
121
|
|
|
73
122
|
async def ensure_customer(self, data: CustomerUpsertIn) -> CustomerOut:
|
|
@@ -84,6 +133,7 @@ class PaymentsService:
|
|
|
84
133
|
# If your PayCustomer model has additional columns (email/name), include them here.
|
|
85
134
|
self.session.add(
|
|
86
135
|
PayCustomer(
|
|
136
|
+
tenant_id=self.tenant_id,
|
|
87
137
|
provider=out.provider,
|
|
88
138
|
provider_customer_id=out.provider_customer_id,
|
|
89
139
|
user_id=data.user_id,
|
|
@@ -98,6 +148,7 @@ class PaymentsService:
|
|
|
98
148
|
out = await adapter.create_intent(data, user_id=user_id)
|
|
99
149
|
self.session.add(
|
|
100
150
|
PayIntent(
|
|
151
|
+
tenant_id=self.tenant_id,
|
|
101
152
|
provider=out.provider,
|
|
102
153
|
provider_intent_id=out.provider_intent_id,
|
|
103
154
|
user_id=user_id,
|
|
@@ -133,35 +184,50 @@ class PaymentsService:
|
|
|
133
184
|
async def refund(self, provider_intent_id: str, data: RefundIn) -> IntentOut:
|
|
134
185
|
adapter = self._get_adapter()
|
|
135
186
|
out = await adapter.refund(provider_intent_id, data)
|
|
187
|
+
# Create ledger entry if amount present and not already recorded
|
|
188
|
+
pi = await self.session.scalar(
|
|
189
|
+
select(PayIntent).where(PayIntent.provider_intent_id == provider_intent_id)
|
|
190
|
+
)
|
|
191
|
+
if pi:
|
|
192
|
+
amount = int(data.amount) if data.amount is not None else out.amount
|
|
193
|
+
# Guard against duplicates (same provider_ref + kind)
|
|
194
|
+
existing = await self.session.scalar(
|
|
195
|
+
select(LedgerEntry).where(
|
|
196
|
+
LedgerEntry.provider_ref == provider_intent_id,
|
|
197
|
+
LedgerEntry.kind == "refund",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
if amount > 0 and not existing:
|
|
201
|
+
self.session.add(
|
|
202
|
+
LedgerEntry(
|
|
203
|
+
tenant_id=self.tenant_id,
|
|
204
|
+
provider=pi.provider,
|
|
205
|
+
provider_ref=provider_intent_id,
|
|
206
|
+
user_id=pi.user_id,
|
|
207
|
+
amount=+amount,
|
|
208
|
+
currency=out.currency,
|
|
209
|
+
kind="refund",
|
|
210
|
+
status="posted",
|
|
211
|
+
)
|
|
212
|
+
)
|
|
136
213
|
return out
|
|
137
214
|
|
|
138
215
|
# --- Webhooks -------------------------------------------------------------
|
|
139
216
|
|
|
140
217
|
async def handle_webhook(self, provider: str, signature: str | None, payload: bytes) -> dict:
|
|
141
|
-
# Webhooks also require provider adapter
|
|
142
218
|
adapter = self._get_adapter()
|
|
143
219
|
parsed = await adapter.verify_and_parse_webhook(signature, payload)
|
|
144
|
-
|
|
145
|
-
# Save raw event (keep JSON column/shape aligned with your model)
|
|
146
220
|
self.session.add(
|
|
147
221
|
PayEvent(
|
|
222
|
+
tenant_id=self.tenant_id,
|
|
148
223
|
provider=provider,
|
|
149
224
|
provider_event_id=parsed["id"],
|
|
150
|
-
|
|
225
|
+
type=parsed.get("type", ""),
|
|
226
|
+
payload_json=parsed,
|
|
151
227
|
)
|
|
152
228
|
)
|
|
153
229
|
|
|
154
|
-
|
|
155
|
-
obj = parsed.get("data") or {}
|
|
156
|
-
|
|
157
|
-
if provider == "stripe":
|
|
158
|
-
if typ == "payment_intent.succeeded":
|
|
159
|
-
await self._post_sale(obj)
|
|
160
|
-
elif typ == "charge.refunded":
|
|
161
|
-
await self._post_refund(obj)
|
|
162
|
-
elif typ == "charge.captured":
|
|
163
|
-
await self._post_capture(obj)
|
|
164
|
-
|
|
230
|
+
await self._dispatch_event(provider, parsed)
|
|
165
231
|
return {"ok": True}
|
|
166
232
|
|
|
167
233
|
# --- Ledger postings ------------------------------------------------------
|
|
@@ -177,6 +243,7 @@ class PaymentsService:
|
|
|
177
243
|
intent.status = "succeeded"
|
|
178
244
|
self.session.add(
|
|
179
245
|
LedgerEntry(
|
|
246
|
+
tenant_id=self.tenant_id,
|
|
180
247
|
provider=intent.provider,
|
|
181
248
|
provider_ref=provider_intent_id,
|
|
182
249
|
user_id=intent.user_id,
|
|
@@ -195,17 +262,27 @@ class PaymentsService:
|
|
|
195
262
|
select(PayIntent).where(PayIntent.provider_intent_id == pi_id)
|
|
196
263
|
)
|
|
197
264
|
if intent:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
provider_ref
|
|
202
|
-
|
|
203
|
-
amount=+amount,
|
|
204
|
-
currency=currency,
|
|
205
|
-
kind="capture",
|
|
206
|
-
status="posted",
|
|
265
|
+
# Avoid duplicate capture entries
|
|
266
|
+
existing = await self.session.scalar(
|
|
267
|
+
select(LedgerEntry).where(
|
|
268
|
+
LedgerEntry.provider_ref == charge_obj.get("id"),
|
|
269
|
+
LedgerEntry.kind == "capture",
|
|
207
270
|
)
|
|
208
271
|
)
|
|
272
|
+
if not existing:
|
|
273
|
+
self.session.add(
|
|
274
|
+
LedgerEntry(
|
|
275
|
+
tenant_id=self.tenant_id,
|
|
276
|
+
provider=intent.provider,
|
|
277
|
+
provider_ref=charge_obj.get("id"),
|
|
278
|
+
user_id=intent.user_id,
|
|
279
|
+
amount=+amount,
|
|
280
|
+
currency=currency,
|
|
281
|
+
kind="capture",
|
|
282
|
+
status="posted",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
intent.captured = True
|
|
209
286
|
|
|
210
287
|
async def _post_refund(self, charge_obj: dict):
|
|
211
288
|
amount = int(charge_obj.get("amount_refunded") or 0)
|
|
@@ -215,22 +292,31 @@ class PaymentsService:
|
|
|
215
292
|
select(PayIntent).where(PayIntent.provider_intent_id == pi_id)
|
|
216
293
|
)
|
|
217
294
|
if intent and amount > 0:
|
|
218
|
-
self.session.
|
|
219
|
-
LedgerEntry(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
user_id=intent.user_id,
|
|
223
|
-
amount=+amount,
|
|
224
|
-
currency=currency,
|
|
225
|
-
kind="refund",
|
|
226
|
-
status="posted",
|
|
295
|
+
existing = await self.session.scalar(
|
|
296
|
+
select(LedgerEntry).where(
|
|
297
|
+
LedgerEntry.provider_ref == charge_obj.get("id"),
|
|
298
|
+
LedgerEntry.kind == "refund",
|
|
227
299
|
)
|
|
228
300
|
)
|
|
301
|
+
if not existing:
|
|
302
|
+
self.session.add(
|
|
303
|
+
LedgerEntry(
|
|
304
|
+
tenant_id=self.tenant_id,
|
|
305
|
+
provider=intent.provider,
|
|
306
|
+
provider_ref=charge_obj.get("id"),
|
|
307
|
+
user_id=intent.user_id,
|
|
308
|
+
amount=+amount,
|
|
309
|
+
currency=currency,
|
|
310
|
+
kind="refund",
|
|
311
|
+
status="posted",
|
|
312
|
+
)
|
|
313
|
+
)
|
|
229
314
|
|
|
230
315
|
async def attach_payment_method(self, data: PaymentMethodAttachIn) -> PaymentMethodOut:
|
|
231
316
|
out = await self._get_adapter().attach_payment_method(data)
|
|
232
317
|
# Upsert locally for quick listing
|
|
233
318
|
pm = PayPaymentMethod(
|
|
319
|
+
tenant_id=self.tenant_id,
|
|
234
320
|
provider=out.provider,
|
|
235
321
|
provider_customer_id=out.provider_customer_id,
|
|
236
322
|
provider_method_id=out.provider_method_id,
|
|
@@ -246,13 +332,13 @@ class PaymentsService:
|
|
|
246
332
|
async def list_payment_methods(self, provider_customer_id: str) -> list[PaymentMethodOut]:
|
|
247
333
|
return await self._get_adapter().list_payment_methods(provider_customer_id)
|
|
248
334
|
|
|
249
|
-
async def detach_payment_method(self, provider_method_id: str) ->
|
|
250
|
-
await self._get_adapter().detach_payment_method(provider_method_id)
|
|
335
|
+
async def detach_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
|
|
336
|
+
return await self._get_adapter().detach_payment_method(provider_method_id)
|
|
251
337
|
|
|
252
338
|
async def set_default_payment_method(
|
|
253
339
|
self, provider_customer_id: str, provider_method_id: str
|
|
254
|
-
) ->
|
|
255
|
-
await self._get_adapter().set_default_payment_method(
|
|
340
|
+
) -> PaymentMethodOut:
|
|
341
|
+
return await self._get_adapter().set_default_payment_method(
|
|
256
342
|
provider_customer_id, provider_method_id
|
|
257
343
|
)
|
|
258
344
|
|
|
@@ -261,6 +347,7 @@ class PaymentsService:
|
|
|
261
347
|
out = await self._get_adapter().create_product(data)
|
|
262
348
|
self.session.add(
|
|
263
349
|
PayProduct(
|
|
350
|
+
tenant_id=self.tenant_id,
|
|
264
351
|
provider=out.provider,
|
|
265
352
|
provider_product_id=out.provider_product_id,
|
|
266
353
|
name=out.name,
|
|
@@ -273,6 +360,7 @@ class PaymentsService:
|
|
|
273
360
|
out = await self._get_adapter().create_price(data)
|
|
274
361
|
self.session.add(
|
|
275
362
|
PayPrice(
|
|
363
|
+
tenant_id=self.tenant_id,
|
|
276
364
|
provider=out.provider,
|
|
277
365
|
provider_price_id=out.provider_price_id,
|
|
278
366
|
provider_product_id=out.provider_product_id,
|
|
@@ -290,6 +378,7 @@ class PaymentsService:
|
|
|
290
378
|
out = await self._get_adapter().create_subscription(data)
|
|
291
379
|
self.session.add(
|
|
292
380
|
PaySubscription(
|
|
381
|
+
tenant_id=self.tenant_id,
|
|
293
382
|
provider=out.provider,
|
|
294
383
|
provider_subscription_id=out.provider_subscription_id,
|
|
295
384
|
provider_price_id=out.provider_price_id,
|
|
@@ -318,6 +407,7 @@ class PaymentsService:
|
|
|
318
407
|
out = await self._get_adapter().create_invoice(data)
|
|
319
408
|
self.session.add(
|
|
320
409
|
PayInvoice(
|
|
410
|
+
tenant_id=self.tenant_id,
|
|
321
411
|
provider=out.provider,
|
|
322
412
|
provider_invoice_id=out.provider_invoice_id,
|
|
323
413
|
provider_customer_id=out.provider_customer_id,
|
|
@@ -347,6 +437,456 @@ class PaymentsService:
|
|
|
347
437
|
async def daily_statements_rollup(
|
|
348
438
|
self, date_from: str | None = None, date_to: str | None = None
|
|
349
439
|
) -> list[StatementRow]:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
440
|
+
from datetime import datetime
|
|
441
|
+
|
|
442
|
+
from sqlalchemy import func
|
|
443
|
+
|
|
444
|
+
q = select(
|
|
445
|
+
func.date_trunc("day", LedgerEntry.ts).label("day"),
|
|
446
|
+
LedgerEntry.currency,
|
|
447
|
+
func.sum(func.case((LedgerEntry.kind == "payment", LedgerEntry.amount), else_=0)).label(
|
|
448
|
+
"gross"
|
|
449
|
+
),
|
|
450
|
+
func.sum(func.case((LedgerEntry.kind == "refund", LedgerEntry.amount), else_=0)).label(
|
|
451
|
+
"refunds"
|
|
452
|
+
),
|
|
453
|
+
func.sum(func.case((LedgerEntry.kind == "fee", LedgerEntry.amount), else_=0)).label(
|
|
454
|
+
"fees"
|
|
455
|
+
),
|
|
456
|
+
func.count().label("count"),
|
|
457
|
+
)
|
|
458
|
+
if date_from:
|
|
459
|
+
try:
|
|
460
|
+
q = q.where(LedgerEntry.ts >= datetime.fromisoformat(date_from))
|
|
461
|
+
except Exception:
|
|
462
|
+
pass
|
|
463
|
+
if date_to:
|
|
464
|
+
try:
|
|
465
|
+
q = q.where(LedgerEntry.ts <= datetime.fromisoformat(date_to))
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
q = q.group_by(func.date_trunc("day", LedgerEntry.ts), LedgerEntry.currency).order_by(
|
|
469
|
+
func.date_trunc("day", LedgerEntry.ts).desc()
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
rows = (await self.session.execute(q)).all()
|
|
473
|
+
out: list[StatementRow] = []
|
|
474
|
+
for day, currency, gross, refunds, fees, count in rows:
|
|
475
|
+
gross = int(gross or 0)
|
|
476
|
+
refunds = int(refunds or 0)
|
|
477
|
+
fees = int(fees or 0)
|
|
478
|
+
out.append(
|
|
479
|
+
StatementRow(
|
|
480
|
+
period_start=day.isoformat(),
|
|
481
|
+
period_end=day.isoformat(),
|
|
482
|
+
currency=str(currency).upper(),
|
|
483
|
+
gross=gross,
|
|
484
|
+
refunds=refunds,
|
|
485
|
+
fees=fees,
|
|
486
|
+
net=gross - refunds - fees,
|
|
487
|
+
count=int(count or 0),
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
return out
|
|
491
|
+
|
|
492
|
+
async def capture_intent(self, provider_intent_id: str, data: CaptureIn) -> IntentOut:
|
|
493
|
+
out = await self._get_adapter().capture_intent(
|
|
494
|
+
provider_intent_id, amount=int(data.amount) if data.amount is not None else None
|
|
495
|
+
)
|
|
496
|
+
pi = await self.session.scalar(
|
|
497
|
+
select(PayIntent).where(PayIntent.provider_intent_id == provider_intent_id)
|
|
498
|
+
)
|
|
499
|
+
if pi:
|
|
500
|
+
pi.status = out.status
|
|
501
|
+
if out.status in ("succeeded", "requires_capture"): # Stripe specifics vary
|
|
502
|
+
pi.captured = True if out.status == "succeeded" else pi.captured
|
|
503
|
+
# Add capture ledger entry if succeeded and not already posted
|
|
504
|
+
if out.status == "succeeded":
|
|
505
|
+
existing = await self.session.scalar(
|
|
506
|
+
select(LedgerEntry).where(
|
|
507
|
+
LedgerEntry.provider_ref == provider_intent_id,
|
|
508
|
+
LedgerEntry.kind == "capture",
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
if not existing:
|
|
512
|
+
self.session.add(
|
|
513
|
+
LedgerEntry(
|
|
514
|
+
tenant_id=self.tenant_id,
|
|
515
|
+
provider=pi.provider,
|
|
516
|
+
provider_ref=provider_intent_id,
|
|
517
|
+
user_id=pi.user_id,
|
|
518
|
+
amount=+out.amount,
|
|
519
|
+
currency=out.currency,
|
|
520
|
+
kind="capture",
|
|
521
|
+
status="posted",
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
return out
|
|
525
|
+
|
|
526
|
+
async def list_intents(self, f: IntentListFilter) -> tuple[list[IntentOut], str | None]:
|
|
527
|
+
return await self._get_adapter().list_intents(
|
|
528
|
+
customer_provider_id=f.customer_provider_id,
|
|
529
|
+
status=f.status,
|
|
530
|
+
limit=f.limit or 50,
|
|
531
|
+
cursor=f.cursor,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# ---- Invoices: lines/list/get/preview ----
|
|
535
|
+
async def add_invoice_line_item(
|
|
536
|
+
self, provider_invoice_id: str, data: InvoiceLineItemIn
|
|
537
|
+
) -> InvoiceOut:
|
|
538
|
+
return await self._get_adapter().add_invoice_line_item(provider_invoice_id, data)
|
|
539
|
+
|
|
540
|
+
async def list_invoices(self, f: InvoicesListFilter) -> tuple[list[InvoiceOut], str | None]:
|
|
541
|
+
return await self._get_adapter().list_invoices(
|
|
542
|
+
customer_provider_id=f.customer_provider_id,
|
|
543
|
+
status=f.status,
|
|
544
|
+
limit=f.limit or 50,
|
|
545
|
+
cursor=f.cursor,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
async def get_invoice(self, provider_invoice_id: str) -> InvoiceOut:
|
|
549
|
+
return await self._get_adapter().get_invoice(provider_invoice_id)
|
|
550
|
+
|
|
551
|
+
async def preview_invoice(
|
|
552
|
+
self, customer_provider_id: str, subscription_id: str | None
|
|
553
|
+
) -> InvoiceOut:
|
|
554
|
+
return await self._get_adapter().preview_invoice(
|
|
555
|
+
customer_provider_id=customer_provider_id, subscription_id=subscription_id
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# ---- Metered usage ----
|
|
559
|
+
async def create_usage_record(self, data: UsageRecordIn) -> UsageRecordOut:
|
|
560
|
+
return await self._get_adapter().create_usage_record(data)
|
|
561
|
+
|
|
562
|
+
# --- Setup Intents --------------------------------------------------------
|
|
563
|
+
async def create_setup_intent(self, data: SetupIntentCreateIn) -> SetupIntentOut:
|
|
564
|
+
out = await self._get_adapter().create_setup_intent(data)
|
|
565
|
+
self.session.add(
|
|
566
|
+
PaySetupIntent(
|
|
567
|
+
tenant_id=self.tenant_id,
|
|
568
|
+
provider=out.provider,
|
|
569
|
+
provider_setup_intent_id=out.provider_setup_intent_id,
|
|
570
|
+
user_id=None,
|
|
571
|
+
status=out.status,
|
|
572
|
+
client_secret=out.client_secret,
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
return out
|
|
576
|
+
|
|
577
|
+
async def confirm_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
|
|
578
|
+
out = await self._get_adapter().confirm_setup_intent(provider_setup_intent_id)
|
|
579
|
+
row = await self.session.scalar(
|
|
580
|
+
select(PaySetupIntent).where(
|
|
581
|
+
PaySetupIntent.provider_setup_intent_id == provider_setup_intent_id
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
if row:
|
|
585
|
+
row.status = out.status
|
|
586
|
+
row.client_secret = out.client_secret or row.client_secret
|
|
587
|
+
return out
|
|
588
|
+
|
|
589
|
+
async def get_setup_intent(self, provider_setup_intent_id: str) -> SetupIntentOut:
|
|
590
|
+
out = await self._get_adapter().get_setup_intent(provider_setup_intent_id)
|
|
591
|
+
# opportunistic upsert
|
|
592
|
+
row = await self.session.scalar(
|
|
593
|
+
select(PaySetupIntent).where(
|
|
594
|
+
PaySetupIntent.provider_setup_intent_id == provider_setup_intent_id
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
if row:
|
|
598
|
+
row.status = out.status
|
|
599
|
+
row.client_secret = out.client_secret or row.client_secret
|
|
600
|
+
else:
|
|
601
|
+
self.session.add(
|
|
602
|
+
PaySetupIntent(
|
|
603
|
+
tenant_id=self.tenant_id,
|
|
604
|
+
provider=out.provider,
|
|
605
|
+
provider_setup_intent_id=out.provider_setup_intent_id,
|
|
606
|
+
user_id=None,
|
|
607
|
+
status=out.status,
|
|
608
|
+
client_secret=out.client_secret,
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
return out
|
|
612
|
+
|
|
613
|
+
# --- SCA / 3DS resume -----------------------------------------------------
|
|
614
|
+
async def resume_intent_after_action(self, provider_intent_id: str) -> IntentOut:
|
|
615
|
+
out = await self._get_adapter().resume_intent_after_action(provider_intent_id)
|
|
616
|
+
pi = await self.session.scalar(
|
|
617
|
+
select(PayIntent).where(PayIntent.provider_intent_id == provider_intent_id)
|
|
618
|
+
)
|
|
619
|
+
if pi:
|
|
620
|
+
pi.status = out.status
|
|
621
|
+
pi.client_secret = out.client_secret or pi.client_secret
|
|
622
|
+
return out
|
|
623
|
+
|
|
624
|
+
# --- Disputes -------------------------------------------------------------
|
|
625
|
+
async def list_disputes(
|
|
626
|
+
self, *, status: Optional[str], limit: int, cursor: Optional[str]
|
|
627
|
+
) -> tuple[list[DisputeOut], Optional[str]]:
|
|
628
|
+
return await self._get_adapter().list_disputes(status=status, limit=limit, cursor=cursor)
|
|
629
|
+
|
|
630
|
+
async def get_dispute(self, provider_dispute_id: str) -> DisputeOut:
|
|
631
|
+
out = await self._get_adapter().get_dispute(provider_dispute_id)
|
|
632
|
+
# Upsert locally
|
|
633
|
+
row = await self.session.scalar(
|
|
634
|
+
select(PayDispute).where(PayDispute.provider_dispute_id == provider_dispute_id)
|
|
635
|
+
)
|
|
636
|
+
if row:
|
|
637
|
+
row.status = out.status
|
|
638
|
+
row.amount = out.amount
|
|
639
|
+
row.currency = out.currency
|
|
640
|
+
else:
|
|
641
|
+
self.session.add(
|
|
642
|
+
PayDispute(
|
|
643
|
+
tenant_id=self.tenant_id,
|
|
644
|
+
provider=out.provider,
|
|
645
|
+
provider_dispute_id=out.provider_dispute_id,
|
|
646
|
+
provider_charge_id=None, # set if adapter returns it
|
|
647
|
+
amount=out.amount,
|
|
648
|
+
currency=out.currency,
|
|
649
|
+
reason=out.reason,
|
|
650
|
+
status=out.status,
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
return out
|
|
654
|
+
|
|
655
|
+
async def submit_dispute_evidence(self, provider_dispute_id: str, evidence: dict) -> DisputeOut:
|
|
656
|
+
out = await self._get_adapter().submit_dispute_evidence(provider_dispute_id, evidence)
|
|
657
|
+
# reflect status
|
|
658
|
+
row = await self.session.scalar(
|
|
659
|
+
select(PayDispute).where(PayDispute.provider_dispute_id == provider_dispute_id)
|
|
660
|
+
)
|
|
661
|
+
if row:
|
|
662
|
+
row.status = out.status
|
|
663
|
+
return out
|
|
664
|
+
|
|
665
|
+
# --- Balance --------------------------------------------------------------
|
|
666
|
+
async def get_balance_snapshot(self) -> BalanceSnapshotOut:
|
|
667
|
+
return await self._get_adapter().get_balance_snapshot()
|
|
668
|
+
|
|
669
|
+
# --- Payouts --------------------------------------------------------------
|
|
670
|
+
async def list_payouts(
|
|
671
|
+
self, *, limit: int, cursor: Optional[str]
|
|
672
|
+
) -> tuple[list[PayoutOut], Optional[str]]:
|
|
673
|
+
return await self._get_adapter().list_payouts(limit=limit, cursor=cursor)
|
|
674
|
+
|
|
675
|
+
async def get_payout(self, provider_payout_id: str) -> PayoutOut:
|
|
676
|
+
out = await self._get_adapter().get_payout(provider_payout_id)
|
|
677
|
+
# Upsert locally
|
|
678
|
+
row = await self.session.scalar(
|
|
679
|
+
select(PayPayout).where(PayPayout.provider_payout_id == provider_payout_id)
|
|
680
|
+
)
|
|
681
|
+
if row:
|
|
682
|
+
row.status = out.status
|
|
683
|
+
row.amount = out.amount
|
|
684
|
+
row.currency = out.currency
|
|
685
|
+
# arrival_date/type optional; update if present
|
|
686
|
+
else:
|
|
687
|
+
self.session.add(
|
|
688
|
+
PayPayout(
|
|
689
|
+
tenant_id=self.tenant_id,
|
|
690
|
+
provider=out.provider,
|
|
691
|
+
provider_payout_id=out.provider_payout_id,
|
|
692
|
+
amount=out.amount,
|
|
693
|
+
currency=out.currency,
|
|
694
|
+
status=out.status,
|
|
695
|
+
# arrival_date/out.type if you add them onto PayoutOut
|
|
696
|
+
)
|
|
697
|
+
)
|
|
698
|
+
return out
|
|
699
|
+
|
|
700
|
+
# --- Webhook replay -------------------------------------------------------
|
|
701
|
+
async def replay_webhooks(
|
|
702
|
+
self, since: Optional[str], until: Optional[str], event_ids: list[str]
|
|
703
|
+
) -> int:
|
|
704
|
+
from datetime import datetime
|
|
705
|
+
|
|
706
|
+
q = select(PayEvent).where(PayEvent.provider == self._provider_name)
|
|
707
|
+
if event_ids:
|
|
708
|
+
q = q.where(PayEvent.provider_event_id.in_(event_ids))
|
|
709
|
+
else:
|
|
710
|
+
# ISO8601 strings expected; ignore parsing errors safely
|
|
711
|
+
if since:
|
|
712
|
+
try:
|
|
713
|
+
q = q.where(PayEvent.received_at >= datetime.fromisoformat(since))
|
|
714
|
+
except Exception:
|
|
715
|
+
pass
|
|
716
|
+
if until:
|
|
717
|
+
try:
|
|
718
|
+
q = q.where(PayEvent.received_at <= datetime.fromisoformat(until))
|
|
719
|
+
except Exception:
|
|
720
|
+
pass
|
|
721
|
+
|
|
722
|
+
rows = (await self.session.execute(q)).scalars().all()
|
|
723
|
+
for ev in rows:
|
|
724
|
+
await self._dispatch_event(ev.provider, ev.payload_json)
|
|
725
|
+
|
|
726
|
+
return len(rows)
|
|
727
|
+
|
|
728
|
+
# ---- Customers ----
|
|
729
|
+
async def list_customers(self, f: CustomersListFilter) -> tuple[list[CustomerOut], str | None]:
|
|
730
|
+
adapter = self._get_adapter()
|
|
731
|
+
try:
|
|
732
|
+
return await adapter.list_customers(
|
|
733
|
+
provider=f.provider, user_id=f.user_id, limit=f.limit or 50, cursor=f.cursor
|
|
734
|
+
)
|
|
735
|
+
except NotImplementedError:
|
|
736
|
+
# Fallback to local DB listing
|
|
737
|
+
q = select(PayCustomer).order_by(PayCustomer.provider_customer_id.asc())
|
|
738
|
+
if f.provider:
|
|
739
|
+
q = q.where(PayCustomer.provider == f.provider)
|
|
740
|
+
if f.user_id:
|
|
741
|
+
q = q.where(PayCustomer.user_id == f.user_id)
|
|
742
|
+
rows = (await self.session.execute(q)).scalars().all()
|
|
743
|
+
# simple cursor by provider_customer_id; production can optimize
|
|
744
|
+
next_cursor = None
|
|
745
|
+
if f.limit and len(rows) > f.limit:
|
|
746
|
+
rows = rows[: f.limit]
|
|
747
|
+
next_cursor = rows[-1].provider_customer_id
|
|
748
|
+
return (
|
|
749
|
+
[
|
|
750
|
+
CustomerOut(
|
|
751
|
+
id=r.id,
|
|
752
|
+
provider=r.provider,
|
|
753
|
+
provider_customer_id=r.provider_customer_id,
|
|
754
|
+
email=None,
|
|
755
|
+
name=None,
|
|
756
|
+
)
|
|
757
|
+
for r in rows
|
|
758
|
+
],
|
|
759
|
+
next_cursor,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
async def get_customer(self, provider_customer_id: str) -> CustomerOut:
|
|
763
|
+
adapter = self._get_adapter()
|
|
764
|
+
out = await adapter.get_customer(provider_customer_id)
|
|
765
|
+
if out is None:
|
|
766
|
+
raise RuntimeError("Customer not found")
|
|
767
|
+
# upsert locally
|
|
768
|
+
row = await self.session.scalar(
|
|
769
|
+
select(PayCustomer).where(PayCustomer.provider_customer_id == provider_customer_id)
|
|
770
|
+
)
|
|
771
|
+
if not row:
|
|
772
|
+
self.session.add(
|
|
773
|
+
PayCustomer(
|
|
774
|
+
tenant_id=self.tenant_id,
|
|
775
|
+
provider=out.provider,
|
|
776
|
+
provider_customer_id=out.provider_customer_id,
|
|
777
|
+
user_id=None,
|
|
778
|
+
)
|
|
779
|
+
)
|
|
780
|
+
return out
|
|
781
|
+
|
|
782
|
+
# ---- Products / Prices ----
|
|
783
|
+
async def get_product(self, provider_product_id: str) -> ProductOut:
|
|
784
|
+
return await self._get_adapter().get_product(provider_product_id)
|
|
785
|
+
|
|
786
|
+
async def list_products(
|
|
787
|
+
self, *, active: bool | None, limit: int, cursor: str | None
|
|
788
|
+
) -> tuple[list[ProductOut], str | None]:
|
|
789
|
+
return await self._get_adapter().list_products(active=active, limit=limit, cursor=cursor)
|
|
790
|
+
|
|
791
|
+
async def update_product(self, provider_product_id: str, data: ProductUpdateIn) -> ProductOut:
|
|
792
|
+
out = await self._get_adapter().update_product(provider_product_id, data)
|
|
793
|
+
# reflect DB
|
|
794
|
+
row = await self.session.scalar(
|
|
795
|
+
select(PayProduct).where(PayProduct.provider_product_id == provider_product_id)
|
|
796
|
+
)
|
|
797
|
+
if row:
|
|
798
|
+
if data.name is not None:
|
|
799
|
+
row.name = data.name
|
|
800
|
+
if data.active is not None:
|
|
801
|
+
row.active = data.active
|
|
802
|
+
return out
|
|
803
|
+
|
|
804
|
+
async def get_price(self, provider_price_id: str) -> PriceOut:
|
|
805
|
+
return await self._get_adapter().get_price(provider_price_id)
|
|
806
|
+
|
|
807
|
+
async def list_prices(
|
|
808
|
+
self,
|
|
809
|
+
*,
|
|
810
|
+
provider_product_id: str | None,
|
|
811
|
+
active: bool | None,
|
|
812
|
+
limit: int,
|
|
813
|
+
cursor: str | None,
|
|
814
|
+
) -> tuple[list[PriceOut], str | None]:
|
|
815
|
+
return await self._get_adapter().list_prices(
|
|
816
|
+
provider_product_id=provider_product_id, active=active, limit=limit, cursor=cursor
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
async def update_price(self, provider_price_id: str, data: PriceUpdateIn) -> PriceOut:
|
|
820
|
+
out = await self._get_adapter().update_price(provider_price_id, data)
|
|
821
|
+
row = await self.session.scalar(
|
|
822
|
+
select(PayPrice).where(PayPrice.provider_price_id == provider_price_id)
|
|
823
|
+
)
|
|
824
|
+
if row and data.active is not None:
|
|
825
|
+
row.active = data.active
|
|
826
|
+
return out
|
|
827
|
+
|
|
828
|
+
# ---- Subscriptions ----
|
|
829
|
+
async def get_subscription(self, provider_subscription_id: str) -> SubscriptionOut:
|
|
830
|
+
return await self._get_adapter().get_subscription(provider_subscription_id)
|
|
831
|
+
|
|
832
|
+
async def list_subscriptions(
|
|
833
|
+
self,
|
|
834
|
+
*,
|
|
835
|
+
customer_provider_id: str | None,
|
|
836
|
+
status: str | None,
|
|
837
|
+
limit: int,
|
|
838
|
+
cursor: str | None,
|
|
839
|
+
) -> tuple[list[SubscriptionOut], str | None]:
|
|
840
|
+
return await self._get_adapter().list_subscriptions(
|
|
841
|
+
customer_provider_id=customer_provider_id, status=status, limit=limit, cursor=cursor
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
# ---- Payment Methods (get/update) ----
|
|
845
|
+
async def get_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
|
|
846
|
+
return await self._get_adapter().get_payment_method(provider_method_id)
|
|
847
|
+
|
|
848
|
+
async def update_payment_method(
|
|
849
|
+
self, provider_method_id: str, data: PaymentMethodUpdateIn
|
|
850
|
+
) -> PaymentMethodOut:
|
|
851
|
+
out = await self._get_adapter().update_payment_method(provider_method_id, data)
|
|
852
|
+
row = await self.session.scalar(
|
|
853
|
+
select(PayPaymentMethod).where(
|
|
854
|
+
PayPaymentMethod.provider_method_id == provider_method_id
|
|
855
|
+
)
|
|
856
|
+
)
|
|
857
|
+
if row:
|
|
858
|
+
if data.name is not None:
|
|
859
|
+
pass # keep local-only if/when you add column
|
|
860
|
+
if data.exp_month is not None:
|
|
861
|
+
row.exp_month = data.exp_month
|
|
862
|
+
if data.exp_year is not None:
|
|
863
|
+
row.exp_year = data.exp_year
|
|
864
|
+
return out
|
|
865
|
+
|
|
866
|
+
# ---- Refunds list/get ----
|
|
867
|
+
async def list_refunds(
|
|
868
|
+
self, *, provider_payment_intent_id: str | None, limit: int, cursor: str | None
|
|
869
|
+
) -> tuple[list[RefundOut], str | None]:
|
|
870
|
+
return await self._get_adapter().list_refunds(
|
|
871
|
+
provider_payment_intent_id=provider_payment_intent_id, limit=limit, cursor=cursor
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
async def get_refund(self, provider_refund_id: str) -> RefundOut:
|
|
875
|
+
return await self._get_adapter().get_refund(provider_refund_id)
|
|
876
|
+
|
|
877
|
+
# ---- Invoice line items list ----
|
|
878
|
+
async def list_invoice_line_items(
|
|
879
|
+
self, provider_invoice_id: str, *, limit: int, cursor: str | None
|
|
880
|
+
) -> tuple[list[InvoiceLineItemOut], str | None]:
|
|
881
|
+
return await self._get_adapter().list_invoice_line_items(
|
|
882
|
+
provider_invoice_id, limit=limit, cursor=cursor
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
# ---- Usage records list/get ----
|
|
886
|
+
async def list_usage_records(
|
|
887
|
+
self, f: UsageRecordListFilter
|
|
888
|
+
) -> tuple[list[UsageRecordOut], str | None]:
|
|
889
|
+
return await self._get_adapter().list_usage_records(f)
|
|
890
|
+
|
|
891
|
+
async def get_usage_record(self, usage_record_id: str) -> UsageRecordOut:
|
|
892
|
+
return await self._get_adapter().get_usage_record(usage_record_id)
|