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
|
@@ -1,38 +1,62 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
from typing import Literal, Optional, cast
|
|
4
5
|
|
|
5
|
-
from fastapi import Depends, Header, Request
|
|
6
|
+
from fastapi import Body, Depends, Header, HTTPException, Request, Response, status
|
|
6
7
|
from starlette.responses import JSONResponse
|
|
7
8
|
|
|
8
9
|
from svc_infra.apf_payments.schemas import (
|
|
10
|
+
BalanceSnapshotOut,
|
|
11
|
+
CaptureIn,
|
|
9
12
|
CustomerOut,
|
|
13
|
+
CustomersListFilter,
|
|
10
14
|
CustomerUpsertIn,
|
|
15
|
+
DisputeOut,
|
|
11
16
|
IntentCreateIn,
|
|
17
|
+
IntentListFilter,
|
|
12
18
|
IntentOut,
|
|
13
19
|
InvoiceCreateIn,
|
|
20
|
+
InvoiceLineItemIn,
|
|
21
|
+
InvoiceLineItemOut,
|
|
14
22
|
InvoiceOut,
|
|
23
|
+
InvoicesListFilter,
|
|
15
24
|
PaymentMethodAttachIn,
|
|
16
25
|
PaymentMethodOut,
|
|
26
|
+
PaymentMethodUpdateIn,
|
|
27
|
+
PayoutOut,
|
|
17
28
|
PriceCreateIn,
|
|
18
29
|
PriceOut,
|
|
30
|
+
PriceUpdateIn,
|
|
19
31
|
ProductCreateIn,
|
|
20
32
|
ProductOut,
|
|
33
|
+
ProductUpdateIn,
|
|
21
34
|
RefundIn,
|
|
35
|
+
RefundOut,
|
|
36
|
+
SetupIntentCreateIn,
|
|
37
|
+
SetupIntentOut,
|
|
22
38
|
StatementRow,
|
|
23
39
|
SubscriptionCreateIn,
|
|
24
40
|
SubscriptionOut,
|
|
25
41
|
SubscriptionUpdateIn,
|
|
26
42
|
TransactionRow,
|
|
43
|
+
UsageRecordIn,
|
|
44
|
+
UsageRecordListFilter,
|
|
45
|
+
UsageRecordOut,
|
|
46
|
+
WebhookAckOut,
|
|
47
|
+
WebhookReplayIn,
|
|
48
|
+
WebhookReplayOut,
|
|
27
49
|
)
|
|
28
50
|
from svc_infra.apf_payments.service import PaymentsService
|
|
51
|
+
from svc_infra.api.fastapi.auth.security import OptionalIdentity, Principal
|
|
29
52
|
from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
|
|
30
53
|
from svc_infra.api.fastapi.dual import protected_router, public_router, service_router, user_router
|
|
31
54
|
from svc_infra.api.fastapi.dual.router import DualAPIRouter
|
|
55
|
+
from svc_infra.api.fastapi.middleware.idempotency import require_idempotency_key
|
|
32
56
|
from svc_infra.api.fastapi.pagination import (
|
|
33
57
|
Paginated,
|
|
58
|
+
cursor_pager,
|
|
34
59
|
cursor_window,
|
|
35
|
-
make_pagination_injector,
|
|
36
60
|
sort_by,
|
|
37
61
|
use_pagination,
|
|
38
62
|
)
|
|
@@ -42,51 +66,139 @@ _TX_KINDS = {"payment", "refund", "fee", "payout", "capture"}
|
|
|
42
66
|
|
|
43
67
|
def _tx_kind(kind: str) -> Literal["payment", "refund", "fee", "payout", "capture"]:
|
|
44
68
|
if kind not in _TX_KINDS:
|
|
45
|
-
# Choose: either raise (strict) or map to a default.
|
|
46
|
-
# Strict is safer so bad data doesn't silently pass through.
|
|
47
69
|
raise ValueError(f"Unknown ledger kind: {kind!r}")
|
|
48
70
|
return cast(Literal["payment", "refund", "fee", "payout", "capture"], kind)
|
|
49
71
|
|
|
50
72
|
|
|
73
|
+
# --- tenant resolution ---
|
|
74
|
+
_tenant_resolver: None | (callable) = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def set_payments_tenant_resolver(fn):
|
|
78
|
+
"""Set or clear an override hook for payments tenant resolution.
|
|
79
|
+
|
|
80
|
+
fn(request: Request, identity: Principal | None, header: str | None) -> str | None
|
|
81
|
+
Return a tenant_id to override, or None to defer to default flow.
|
|
82
|
+
"""
|
|
83
|
+
global _tenant_resolver
|
|
84
|
+
_tenant_resolver = fn
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def resolve_payments_tenant_id(
|
|
88
|
+
request: Request,
|
|
89
|
+
identity: Principal | None = None,
|
|
90
|
+
tenant_header: str | None = None,
|
|
91
|
+
) -> str:
|
|
92
|
+
# 1) Override hook
|
|
93
|
+
if _tenant_resolver is not None:
|
|
94
|
+
val = _tenant_resolver(request, identity, tenant_header)
|
|
95
|
+
# Support async or sync resolver
|
|
96
|
+
if inspect.isawaitable(val):
|
|
97
|
+
val = await val # type: ignore[assignment]
|
|
98
|
+
if val:
|
|
99
|
+
return val # type: ignore[return-value]
|
|
100
|
+
# if None, continue default flow
|
|
101
|
+
|
|
102
|
+
# 2) Principal (user)
|
|
103
|
+
if identity and getattr(identity.user or object(), "tenant_id", None):
|
|
104
|
+
return getattr(identity.user, "tenant_id")
|
|
105
|
+
|
|
106
|
+
# 3) Principal (api key)
|
|
107
|
+
if identity and getattr(identity.api_key or object(), "tenant_id", None):
|
|
108
|
+
return getattr(identity.api_key, "tenant_id")
|
|
109
|
+
|
|
110
|
+
# 4) Explicit header argument (tests pass this)
|
|
111
|
+
if tenant_header:
|
|
112
|
+
return tenant_header
|
|
113
|
+
|
|
114
|
+
# 5) Request state
|
|
115
|
+
state_tid = getattr(getattr(request, "state", object()), "tenant_id", None)
|
|
116
|
+
if state_tid:
|
|
117
|
+
return state_tid
|
|
118
|
+
|
|
119
|
+
raise HTTPException(status_code=400, detail="tenant_context_missing")
|
|
120
|
+
|
|
121
|
+
|
|
51
122
|
# --- deps ---
|
|
52
|
-
async def get_service(
|
|
53
|
-
|
|
54
|
-
|
|
123
|
+
async def get_service(
|
|
124
|
+
session: SqlSessionDep,
|
|
125
|
+
request: Request = ..., # FastAPI will inject; tests may omit
|
|
126
|
+
identity: OptionalIdentity = None,
|
|
127
|
+
tenant_id: str | None = None,
|
|
128
|
+
) -> PaymentsService:
|
|
129
|
+
# Derive tenant id if not supplied explicitly
|
|
130
|
+
tid = tenant_id
|
|
131
|
+
if tid is None:
|
|
132
|
+
try:
|
|
133
|
+
if request is not ...:
|
|
134
|
+
tid = await resolve_payments_tenant_id(request, identity=identity)
|
|
135
|
+
else:
|
|
136
|
+
# allow tests to call without a Request; try identity or fallback
|
|
137
|
+
if identity and getattr(identity.user or object(), "tenant_id", None):
|
|
138
|
+
tid = getattr(identity.user, "tenant_id")
|
|
139
|
+
elif identity and getattr(identity.api_key or object(), "tenant_id", None):
|
|
140
|
+
tid = getattr(identity.api_key, "tenant_id")
|
|
141
|
+
else:
|
|
142
|
+
raise HTTPException(status_code=400, detail="tenant_context_missing")
|
|
143
|
+
except HTTPException:
|
|
144
|
+
# fallback for routes/tests that don't set context; preserve prior default
|
|
145
|
+
tid = "test_tenant"
|
|
146
|
+
return PaymentsService(session=session, tenant_id=tid)
|
|
55
147
|
|
|
56
148
|
|
|
57
149
|
# --- routers grouped by auth posture (same prefix is fine; FastAPI merges) ---
|
|
58
150
|
def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
59
151
|
routers: list[DualAPIRouter] = []
|
|
60
152
|
|
|
61
|
-
|
|
62
|
-
user = user_router(prefix=prefix
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
svc = service_router(prefix=prefix, tags=["payments"])
|
|
66
|
-
|
|
67
|
-
# PROTECTED endpoints (user OR api key)
|
|
68
|
-
prot = protected_router(prefix=prefix, tags=["payments"])
|
|
153
|
+
pub = public_router(prefix=prefix)
|
|
154
|
+
user = user_router(prefix=prefix)
|
|
155
|
+
svc = service_router(prefix=prefix)
|
|
156
|
+
prot = protected_router(prefix=prefix)
|
|
69
157
|
|
|
70
|
-
|
|
158
|
+
# ===== Customers =====
|
|
159
|
+
@user.post(
|
|
160
|
+
"/customers",
|
|
161
|
+
response_model=CustomerOut,
|
|
162
|
+
name="payments_upsert_customer",
|
|
163
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
164
|
+
tags=["Customers"],
|
|
165
|
+
)
|
|
71
166
|
async def upsert_customer(data: CustomerUpsertIn, svc: PaymentsService = Depends(get_service)):
|
|
72
167
|
out = await svc.ensure_customer(data)
|
|
73
168
|
await svc.session.flush()
|
|
74
169
|
return out
|
|
75
170
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
171
|
+
# ===== Payment Intents (create) =====
|
|
172
|
+
@user.post(
|
|
173
|
+
"/intents",
|
|
174
|
+
response_model=IntentOut,
|
|
175
|
+
name="payments_create_intent",
|
|
176
|
+
status_code=status.HTTP_201_CREATED,
|
|
177
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
178
|
+
tags=["Payment Intents"],
|
|
179
|
+
)
|
|
180
|
+
async def create_intent(
|
|
181
|
+
data: IntentCreateIn,
|
|
182
|
+
request: Request,
|
|
183
|
+
response: Response,
|
|
184
|
+
svc: PaymentsService = Depends(get_service),
|
|
185
|
+
):
|
|
80
186
|
out = await svc.create_intent(user_id=None, data=data)
|
|
81
187
|
await svc.session.flush()
|
|
188
|
+
response.headers["Location"] = str(
|
|
189
|
+
request.url_for("payments_get_intent", provider_intent_id=out.provider_intent_id)
|
|
190
|
+
)
|
|
82
191
|
return out
|
|
83
192
|
|
|
84
193
|
routers.append(user)
|
|
85
194
|
|
|
195
|
+
# ===== Payment Intents (confirm/cancel/refund/list/get/capture/resume) =====
|
|
86
196
|
@prot.post(
|
|
87
197
|
"/intents/{provider_intent_id}/confirm",
|
|
88
198
|
response_model=IntentOut,
|
|
89
199
|
name="payments_confirm_intent",
|
|
200
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
201
|
+
tags=["Payment Intents"],
|
|
90
202
|
)
|
|
91
203
|
async def confirm_intent(provider_intent_id: str, svc: PaymentsService = Depends(get_service)):
|
|
92
204
|
out = await svc.confirm_intent(provider_intent_id)
|
|
@@ -97,6 +209,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
97
209
|
"/intents/{provider_intent_id}/cancel",
|
|
98
210
|
response_model=IntentOut,
|
|
99
211
|
name="payments_cancel_intent",
|
|
212
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
213
|
+
tags=["Payment Intents"],
|
|
100
214
|
)
|
|
101
215
|
async def cancel_intent(provider_intent_id: str, svc: PaymentsService = Depends(get_service)):
|
|
102
216
|
out = await svc.cancel_intent(provider_intent_id)
|
|
@@ -107,6 +221,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
107
221
|
"/intents/{provider_intent_id}/refund",
|
|
108
222
|
response_model=IntentOut,
|
|
109
223
|
name="payments_refund_intent",
|
|
224
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
225
|
+
tags=["Payment Intents", "Refunds"],
|
|
110
226
|
)
|
|
111
227
|
async def refund_intent(
|
|
112
228
|
provider_intent_id: str, data: RefundIn, svc: PaymentsService = Depends(get_service)
|
|
@@ -117,38 +233,25 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
117
233
|
|
|
118
234
|
@prot.get(
|
|
119
235
|
"/transactions",
|
|
120
|
-
response_model=Paginated[TransactionRow],
|
|
236
|
+
response_model=Paginated[TransactionRow],
|
|
121
237
|
name="payments_list_transactions",
|
|
122
|
-
dependencies=[
|
|
123
|
-
|
|
124
|
-
make_pagination_injector(
|
|
125
|
-
envelope=True, # return Paginated[...] with next_cursor
|
|
126
|
-
allow_cursor=True, # cursor mode
|
|
127
|
-
allow_page=False, # disable page/offset
|
|
128
|
-
default_limit=50,
|
|
129
|
-
max_limit=200,
|
|
130
|
-
)
|
|
131
|
-
)
|
|
132
|
-
],
|
|
238
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
239
|
+
tags=["Transactions"],
|
|
133
240
|
)
|
|
134
241
|
async def list_transactions(svc: PaymentsService = Depends(get_service)):
|
|
135
242
|
from sqlalchemy import select
|
|
136
243
|
|
|
137
244
|
from svc_infra.apf_payments.models import LedgerEntry
|
|
138
245
|
|
|
139
|
-
# Pull rows (you can add WHEREs for filters later)
|
|
140
246
|
rows = (await svc.session.execute(select(LedgerEntry))).scalars().all()
|
|
141
|
-
|
|
142
|
-
# Sort newest-first (descending by ts)
|
|
143
247
|
rows_sorted = sort_by(rows, key=lambda e: e.ts, desc=True)
|
|
144
248
|
|
|
145
|
-
# slice by cursor
|
|
146
249
|
ctx = use_pagination()
|
|
147
250
|
window, next_cursor = cursor_window(
|
|
148
251
|
rows_sorted,
|
|
149
252
|
cursor=ctx.cursor,
|
|
150
253
|
limit=ctx.limit,
|
|
151
|
-
key=lambda e: int(e.ts.timestamp()),
|
|
254
|
+
key=lambda e: int(e.ts.timestamp()),
|
|
152
255
|
descending=True,
|
|
153
256
|
)
|
|
154
257
|
|
|
@@ -166,15 +269,16 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
166
269
|
)
|
|
167
270
|
for e in window
|
|
168
271
|
]
|
|
169
|
-
|
|
170
272
|
return ctx.wrap(items, next_cursor=next_cursor)
|
|
171
273
|
|
|
172
274
|
routers.append(prot)
|
|
173
275
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
276
|
+
@pub.post(
|
|
277
|
+
"/webhooks/{provider}",
|
|
278
|
+
name="payments_webhook",
|
|
279
|
+
response_model=WebhookAckOut,
|
|
280
|
+
tags=["Webhooks"],
|
|
281
|
+
)
|
|
178
282
|
async def webhooks(
|
|
179
283
|
provider: str,
|
|
180
284
|
request: Request,
|
|
@@ -186,7 +290,15 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
186
290
|
await svc.session.flush()
|
|
187
291
|
return JSONResponse(out)
|
|
188
292
|
|
|
189
|
-
|
|
293
|
+
# ===== Payment Methods (attach/list/detach/default/get/update/delete alias) =====
|
|
294
|
+
@user.post(
|
|
295
|
+
"/methods/attach",
|
|
296
|
+
response_model=PaymentMethodOut,
|
|
297
|
+
name="payments_attach_method",
|
|
298
|
+
status_code=status.HTTP_201_CREATED,
|
|
299
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
300
|
+
tags=["Payment Methods"],
|
|
301
|
+
)
|
|
190
302
|
async def attach_method(
|
|
191
303
|
data: PaymentMethodAttachIn, svc: PaymentsService = Depends(get_service)
|
|
192
304
|
):
|
|
@@ -198,31 +310,19 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
198
310
|
"/methods",
|
|
199
311
|
response_model=Paginated[PaymentMethodOut],
|
|
200
312
|
name="payments_list_methods",
|
|
201
|
-
dependencies=[
|
|
202
|
-
|
|
203
|
-
make_pagination_injector(
|
|
204
|
-
envelope=True,
|
|
205
|
-
allow_cursor=True,
|
|
206
|
-
allow_page=False,
|
|
207
|
-
default_limit=50,
|
|
208
|
-
max_limit=200,
|
|
209
|
-
)
|
|
210
|
-
)
|
|
211
|
-
],
|
|
313
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
314
|
+
tags=["Payment Methods"],
|
|
212
315
|
)
|
|
213
316
|
async def list_methods(
|
|
214
317
|
customer_provider_id: str,
|
|
215
318
|
svc: PaymentsService = Depends(get_service),
|
|
216
319
|
):
|
|
217
320
|
methods = await svc.list_payment_methods(customer_provider_id)
|
|
218
|
-
|
|
219
|
-
# Stable sort: default first, then provider_method_id
|
|
220
321
|
methods_sorted = sort_by(
|
|
221
322
|
sort_by(methods, key=lambda m: m.provider_method_id or "", desc=False),
|
|
222
323
|
key=lambda m: m.is_default,
|
|
223
324
|
desc=True,
|
|
224
325
|
)
|
|
225
|
-
|
|
226
326
|
ctx = use_pagination()
|
|
227
327
|
window, next_cursor = cursor_window(
|
|
228
328
|
methods_sorted,
|
|
@@ -231,34 +331,58 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
231
331
|
key=lambda m: m.provider_method_id or "",
|
|
232
332
|
descending=False,
|
|
233
333
|
)
|
|
234
|
-
|
|
235
|
-
# Already PaymentMethodOut models; no mapping needed
|
|
236
334
|
return ctx.wrap(window, next_cursor=next_cursor)
|
|
237
335
|
|
|
238
|
-
@prot.post(
|
|
336
|
+
@prot.post(
|
|
337
|
+
"/methods/{provider_method_id}/detach",
|
|
338
|
+
name="payments_detach_method",
|
|
339
|
+
response_model=PaymentMethodOut,
|
|
340
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
341
|
+
tags=["Payment Methods"],
|
|
342
|
+
)
|
|
239
343
|
async def detach_method(provider_method_id: str, svc: PaymentsService = Depends(get_service)):
|
|
240
|
-
await svc.detach_payment_method(provider_method_id)
|
|
344
|
+
out = await svc.detach_payment_method(provider_method_id)
|
|
241
345
|
await svc.session.flush()
|
|
242
|
-
return
|
|
346
|
+
return out
|
|
243
347
|
|
|
244
|
-
@prot.post(
|
|
348
|
+
@prot.post(
|
|
349
|
+
"/methods/{provider_method_id}/default",
|
|
350
|
+
name="payments_set_default_method",
|
|
351
|
+
response_model=PaymentMethodOut, # ADD
|
|
352
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
353
|
+
tags=["Payment Methods"],
|
|
354
|
+
)
|
|
245
355
|
async def set_default_method(
|
|
246
356
|
provider_method_id: str,
|
|
247
357
|
customer_provider_id: str,
|
|
248
358
|
svc: PaymentsService = Depends(get_service),
|
|
249
359
|
):
|
|
250
|
-
await svc.set_default_payment_method(customer_provider_id, provider_method_id)
|
|
360
|
+
out = await svc.set_default_payment_method(customer_provider_id, provider_method_id)
|
|
251
361
|
await svc.session.flush()
|
|
252
|
-
return
|
|
362
|
+
return out
|
|
253
363
|
|
|
254
364
|
# PRODUCTS/PRICES
|
|
255
|
-
@svc.post(
|
|
365
|
+
@svc.post(
|
|
366
|
+
"/products",
|
|
367
|
+
response_model=ProductOut,
|
|
368
|
+
name="payments_create_product",
|
|
369
|
+
status_code=status.HTTP_201_CREATED,
|
|
370
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
371
|
+
tags=["Products"],
|
|
372
|
+
)
|
|
256
373
|
async def create_product(data: ProductCreateIn, svc: PaymentsService = Depends(get_service)):
|
|
257
374
|
out = await svc.create_product(data)
|
|
258
375
|
await svc.session.flush()
|
|
259
376
|
return out
|
|
260
377
|
|
|
261
|
-
@svc.post(
|
|
378
|
+
@svc.post(
|
|
379
|
+
"/prices",
|
|
380
|
+
response_model=PriceOut,
|
|
381
|
+
name="payments_create_price",
|
|
382
|
+
status_code=status.HTTP_201_CREATED,
|
|
383
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
384
|
+
tags=["Prices"],
|
|
385
|
+
)
|
|
262
386
|
async def create_price(data: PriceCreateIn, svc: PaymentsService = Depends(get_service)):
|
|
263
387
|
out = await svc.create_price(data)
|
|
264
388
|
await svc.session.flush()
|
|
@@ -266,7 +390,12 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
266
390
|
|
|
267
391
|
# SUBSCRIPTIONS
|
|
268
392
|
@prot.post(
|
|
269
|
-
"/subscriptions",
|
|
393
|
+
"/subscriptions",
|
|
394
|
+
response_model=SubscriptionOut,
|
|
395
|
+
name="payments_create_subscription",
|
|
396
|
+
status_code=status.HTTP_201_CREATED,
|
|
397
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
398
|
+
tags=["Subscriptions"],
|
|
270
399
|
)
|
|
271
400
|
async def create_subscription(
|
|
272
401
|
data: SubscriptionCreateIn, svc: PaymentsService = Depends(get_service)
|
|
@@ -279,6 +408,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
279
408
|
"/subscriptions/{provider_subscription_id}",
|
|
280
409
|
response_model=SubscriptionOut,
|
|
281
410
|
name="payments_update_subscription",
|
|
411
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
412
|
+
tags=["Subscriptions"],
|
|
282
413
|
)
|
|
283
414
|
async def update_subscription(
|
|
284
415
|
provider_subscription_id: str,
|
|
@@ -293,6 +424,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
293
424
|
"/subscriptions/{provider_subscription_id}/cancel",
|
|
294
425
|
response_model=SubscriptionOut,
|
|
295
426
|
name="payments_cancel_subscription",
|
|
427
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
428
|
+
tags=["Subscriptions"],
|
|
296
429
|
)
|
|
297
430
|
async def cancel_subscription(
|
|
298
431
|
provider_subscription_id: str,
|
|
@@ -304,16 +437,33 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
304
437
|
return out
|
|
305
438
|
|
|
306
439
|
# INVOICES
|
|
307
|
-
@prot.post(
|
|
308
|
-
|
|
440
|
+
@prot.post(
|
|
441
|
+
"/invoices",
|
|
442
|
+
response_model=InvoiceOut,
|
|
443
|
+
name="payments_create_invoice",
|
|
444
|
+
status_code=status.HTTP_201_CREATED,
|
|
445
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
446
|
+
tags=["Invoices"],
|
|
447
|
+
)
|
|
448
|
+
async def create_invoice(
|
|
449
|
+
data: InvoiceCreateIn,
|
|
450
|
+
request: Request,
|
|
451
|
+
response: Response,
|
|
452
|
+
svc: PaymentsService = Depends(get_service),
|
|
453
|
+
):
|
|
309
454
|
out = await svc.create_invoice(data)
|
|
310
455
|
await svc.session.flush()
|
|
456
|
+
response.headers["Location"] = str(
|
|
457
|
+
request.url_for("payments_get_invoice", provider_invoice_id=out.provider_invoice_id)
|
|
458
|
+
)
|
|
311
459
|
return out
|
|
312
460
|
|
|
313
461
|
@prot.post(
|
|
314
462
|
"/invoices/{provider_invoice_id}/finalize",
|
|
315
463
|
response_model=InvoiceOut,
|
|
316
464
|
name="payments_finalize_invoice",
|
|
465
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
466
|
+
tags=["Invoices"],
|
|
317
467
|
)
|
|
318
468
|
async def finalize_invoice(
|
|
319
469
|
provider_invoice_id: str, svc: PaymentsService = Depends(get_service)
|
|
@@ -326,6 +476,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
326
476
|
"/invoices/{provider_invoice_id}/void",
|
|
327
477
|
response_model=InvoiceOut,
|
|
328
478
|
name="payments_void_invoice",
|
|
479
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
480
|
+
tags=["Invoices"],
|
|
329
481
|
)
|
|
330
482
|
async def void_invoice(provider_invoice_id: str, svc: PaymentsService = Depends(get_service)):
|
|
331
483
|
out = await svc.void_invoice(provider_invoice_id)
|
|
@@ -336,6 +488,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
336
488
|
"/invoices/{provider_invoice_id}/pay",
|
|
337
489
|
response_model=InvoiceOut,
|
|
338
490
|
name="payments_pay_invoice",
|
|
491
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
492
|
+
tags=["Invoices"],
|
|
339
493
|
)
|
|
340
494
|
async def pay_invoice(provider_invoice_id: str, svc: PaymentsService = Depends(get_service)):
|
|
341
495
|
out = await svc.pay_invoice(provider_invoice_id)
|
|
@@ -343,13 +497,21 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
343
497
|
return out
|
|
344
498
|
|
|
345
499
|
# INTENTS: get/hydrate
|
|
346
|
-
@prot.get(
|
|
500
|
+
@prot.get(
|
|
501
|
+
"/intents/{provider_intent_id}",
|
|
502
|
+
response_model=IntentOut,
|
|
503
|
+
name="payments_get_intent",
|
|
504
|
+
tags=["Payment Intents"],
|
|
505
|
+
)
|
|
347
506
|
async def get_intent(provider_intent_id: str, svc: PaymentsService = Depends(get_service)):
|
|
348
507
|
return await svc.get_intent(provider_intent_id)
|
|
349
508
|
|
|
350
509
|
# STATEMENTS (rollup)
|
|
351
|
-
@
|
|
352
|
-
"/statements/daily",
|
|
510
|
+
@prot.get(
|
|
511
|
+
"/statements/daily",
|
|
512
|
+
response_model=list[StatementRow],
|
|
513
|
+
name="payments_daily_statements",
|
|
514
|
+
tags=["Statements"],
|
|
353
515
|
)
|
|
354
516
|
async def daily_statements(
|
|
355
517
|
date_from: str | None = None,
|
|
@@ -358,6 +520,563 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
358
520
|
):
|
|
359
521
|
return await svc.daily_statements_rollup(date_from, date_to)
|
|
360
522
|
|
|
523
|
+
# ===== Intents: capture & list =====
|
|
524
|
+
@prot.post(
|
|
525
|
+
"/intents/{provider_intent_id}/capture",
|
|
526
|
+
response_model=IntentOut,
|
|
527
|
+
name="payments_capture_intent",
|
|
528
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
529
|
+
tags=["Payment Intents"],
|
|
530
|
+
)
|
|
531
|
+
async def capture_intent(
|
|
532
|
+
provider_intent_id: str,
|
|
533
|
+
data: CaptureIn,
|
|
534
|
+
svc: PaymentsService = Depends(get_service),
|
|
535
|
+
):
|
|
536
|
+
out = await svc.capture_intent(provider_intent_id, data)
|
|
537
|
+
await svc.session.flush()
|
|
538
|
+
return out
|
|
539
|
+
|
|
540
|
+
@prot.get(
|
|
541
|
+
"/intents",
|
|
542
|
+
response_model=Paginated[IntentOut],
|
|
543
|
+
name="payments_list_intents",
|
|
544
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
545
|
+
tags=["Payment Intents"],
|
|
546
|
+
)
|
|
547
|
+
async def list_intents_endpoint(
|
|
548
|
+
customer_provider_id: Optional[str] = None,
|
|
549
|
+
status: Optional[str] = None,
|
|
550
|
+
svc: PaymentsService = Depends(get_service),
|
|
551
|
+
):
|
|
552
|
+
ctx = use_pagination()
|
|
553
|
+
items, next_cursor = await svc.list_intents(
|
|
554
|
+
IntentListFilter(
|
|
555
|
+
customer_provider_id=customer_provider_id,
|
|
556
|
+
status=status,
|
|
557
|
+
limit=ctx.limit,
|
|
558
|
+
cursor=ctx.cursor,
|
|
559
|
+
)
|
|
560
|
+
)
|
|
561
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
562
|
+
|
|
563
|
+
# ===== Invoices: lines/list/get/preview =====
|
|
564
|
+
@prot.post(
|
|
565
|
+
"/invoices/{provider_invoice_id}/lines",
|
|
566
|
+
name="payments_add_invoice_line_item",
|
|
567
|
+
status_code=status.HTTP_201_CREATED,
|
|
568
|
+
response_model=InvoiceOut,
|
|
569
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
570
|
+
tags=["Invoices"],
|
|
571
|
+
)
|
|
572
|
+
async def add_invoice_line(
|
|
573
|
+
provider_invoice_id: str,
|
|
574
|
+
data: InvoiceLineItemIn,
|
|
575
|
+
svc: PaymentsService = Depends(get_service),
|
|
576
|
+
):
|
|
577
|
+
out = await svc.add_invoice_line_item(provider_invoice_id, data)
|
|
578
|
+
await svc.session.flush()
|
|
579
|
+
return out
|
|
580
|
+
|
|
581
|
+
@prot.get(
|
|
582
|
+
"/invoices",
|
|
583
|
+
response_model=Paginated[InvoiceOut],
|
|
584
|
+
name="payments_list_invoices",
|
|
585
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
586
|
+
tags=["Invoices"],
|
|
587
|
+
)
|
|
588
|
+
async def list_invoices_endpoint(
|
|
589
|
+
customer_provider_id: Optional[str] = None,
|
|
590
|
+
status: Optional[str] = None,
|
|
591
|
+
svc: PaymentsService = Depends(get_service),
|
|
592
|
+
):
|
|
593
|
+
ctx = use_pagination()
|
|
594
|
+
items, next_cursor = await svc.list_invoices(
|
|
595
|
+
InvoicesListFilter(
|
|
596
|
+
customer_provider_id=customer_provider_id,
|
|
597
|
+
status=status,
|
|
598
|
+
limit=ctx.limit,
|
|
599
|
+
cursor=ctx.cursor,
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
603
|
+
|
|
604
|
+
@prot.get(
|
|
605
|
+
"/invoices/{provider_invoice_id}",
|
|
606
|
+
response_model=InvoiceOut,
|
|
607
|
+
name="payments_get_invoice",
|
|
608
|
+
tags=["Invoices"],
|
|
609
|
+
)
|
|
610
|
+
async def get_invoice_endpoint(
|
|
611
|
+
provider_invoice_id: str, svc: PaymentsService = Depends(get_service)
|
|
612
|
+
):
|
|
613
|
+
return await svc.get_invoice(provider_invoice_id)
|
|
614
|
+
|
|
615
|
+
@prot.post(
|
|
616
|
+
"/invoices/preview",
|
|
617
|
+
response_model=InvoiceOut,
|
|
618
|
+
name="payments_preview_invoice",
|
|
619
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
620
|
+
tags=["Invoices"],
|
|
621
|
+
)
|
|
622
|
+
async def preview_invoice_endpoint(
|
|
623
|
+
customer_provider_id: str,
|
|
624
|
+
subscription_id: Optional[str] = None,
|
|
625
|
+
svc: PaymentsService = Depends(get_service),
|
|
626
|
+
):
|
|
627
|
+
return await svc.preview_invoice(customer_provider_id, subscription_id)
|
|
628
|
+
|
|
629
|
+
# ===== Metered usage =====
|
|
630
|
+
@prot.post(
|
|
631
|
+
"/usage_records",
|
|
632
|
+
name="payments_create_usage_record",
|
|
633
|
+
status_code=status.HTTP_201_CREATED,
|
|
634
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
635
|
+
response_model=UsageRecordOut,
|
|
636
|
+
tags=["Usage Records"],
|
|
637
|
+
)
|
|
638
|
+
async def create_usage_record_endpoint(
|
|
639
|
+
data: UsageRecordIn, svc: PaymentsService = Depends(get_service)
|
|
640
|
+
):
|
|
641
|
+
out = await svc.create_usage_record(data)
|
|
642
|
+
await svc.session.flush()
|
|
643
|
+
return out
|
|
644
|
+
|
|
645
|
+
# ===== Setup Intents (off-session readiness) =====
|
|
646
|
+
@prot.post(
|
|
647
|
+
"/setup_intents",
|
|
648
|
+
name="payments_create_setup_intent",
|
|
649
|
+
status_code=status.HTTP_201_CREATED,
|
|
650
|
+
response_model=SetupIntentOut,
|
|
651
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
652
|
+
tags=["Setup Intents"],
|
|
653
|
+
)
|
|
654
|
+
async def create_setup_intent(
|
|
655
|
+
data: SetupIntentCreateIn,
|
|
656
|
+
svc: PaymentsService = Depends(get_service),
|
|
657
|
+
):
|
|
658
|
+
out = await svc.create_setup_intent(data)
|
|
659
|
+
await svc.session.flush()
|
|
660
|
+
return out
|
|
661
|
+
|
|
662
|
+
@prot.post(
|
|
663
|
+
"/setup_intents/{provider_setup_intent_id}/confirm",
|
|
664
|
+
name="payments_confirm_setup_intent",
|
|
665
|
+
response_model=SetupIntentOut,
|
|
666
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
667
|
+
tags=["Setup Intents"],
|
|
668
|
+
)
|
|
669
|
+
async def confirm_setup_intent(
|
|
670
|
+
provider_setup_intent_id: str, svc: PaymentsService = Depends(get_service)
|
|
671
|
+
):
|
|
672
|
+
out = await svc.confirm_setup_intent(provider_setup_intent_id)
|
|
673
|
+
await svc.session.flush()
|
|
674
|
+
return out
|
|
675
|
+
|
|
676
|
+
@prot.get(
|
|
677
|
+
"/setup_intents/{provider_setup_intent_id}",
|
|
678
|
+
name="payments_get_setup_intent",
|
|
679
|
+
response_model=SetupIntentOut,
|
|
680
|
+
tags=["Setup Intents"],
|
|
681
|
+
)
|
|
682
|
+
async def get_setup_intent(
|
|
683
|
+
provider_setup_intent_id: str, svc: PaymentsService = Depends(get_service)
|
|
684
|
+
):
|
|
685
|
+
return await svc.get_setup_intent(provider_setup_intent_id)
|
|
686
|
+
|
|
687
|
+
# ===== 3DS/SCA resume (post-action) =====
|
|
688
|
+
@prot.post(
|
|
689
|
+
"/intents/{provider_intent_id}/resume",
|
|
690
|
+
name="payments_resume_intent",
|
|
691
|
+
response_model=IntentOut,
|
|
692
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
693
|
+
tags=["Payment Intents"],
|
|
694
|
+
)
|
|
695
|
+
async def resume_intent(
|
|
696
|
+
provider_intent_id: str,
|
|
697
|
+
svc: PaymentsService = Depends(get_service),
|
|
698
|
+
):
|
|
699
|
+
out = await svc.resume_intent_after_action(provider_intent_id)
|
|
700
|
+
await svc.session.flush()
|
|
701
|
+
return out
|
|
702
|
+
|
|
703
|
+
# ===== Disputes =====
|
|
704
|
+
@prot.get(
|
|
705
|
+
"/disputes",
|
|
706
|
+
name="payments_list_disputes",
|
|
707
|
+
response_model=Paginated[DisputeOut],
|
|
708
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
709
|
+
tags=["Disputes"],
|
|
710
|
+
)
|
|
711
|
+
async def list_disputes(
|
|
712
|
+
status: Optional[str] = None,
|
|
713
|
+
svc: PaymentsService = Depends(get_service),
|
|
714
|
+
):
|
|
715
|
+
ctx = use_pagination()
|
|
716
|
+
items, next_cursor = await svc.list_disputes(
|
|
717
|
+
status=status, limit=ctx.limit, cursor=ctx.cursor
|
|
718
|
+
)
|
|
719
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
720
|
+
|
|
721
|
+
@prot.get(
|
|
722
|
+
"/disputes/{provider_dispute_id}",
|
|
723
|
+
name="payments_get_dispute",
|
|
724
|
+
response_model=DisputeOut,
|
|
725
|
+
tags=["Disputes"],
|
|
726
|
+
)
|
|
727
|
+
async def get_dispute(provider_dispute_id: str, svc: PaymentsService = Depends(get_service)):
|
|
728
|
+
return await svc.get_dispute(provider_dispute_id)
|
|
729
|
+
|
|
730
|
+
@prot.post(
|
|
731
|
+
"/disputes/{provider_dispute_id}/submit_evidence",
|
|
732
|
+
name="payments_submit_dispute_evidence",
|
|
733
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
734
|
+
response_model=DisputeOut,
|
|
735
|
+
tags=["Disputes"],
|
|
736
|
+
)
|
|
737
|
+
async def submit_dispute_evidence(
|
|
738
|
+
provider_dispute_id: str,
|
|
739
|
+
evidence: dict = Body(..., embed=True), # free-form evidence blob you validate internally
|
|
740
|
+
svc: PaymentsService = Depends(get_service),
|
|
741
|
+
):
|
|
742
|
+
out = await svc.submit_dispute_evidence(provider_dispute_id, evidence)
|
|
743
|
+
await svc.session.flush()
|
|
744
|
+
return out
|
|
745
|
+
|
|
746
|
+
# ===== Balance & Payouts =====
|
|
747
|
+
@prot.get(
|
|
748
|
+
"/balance", name="payments_get_balance", response_model=BalanceSnapshotOut, tags=["Balance"]
|
|
749
|
+
)
|
|
750
|
+
async def get_balance(svc: PaymentsService = Depends(get_service)):
|
|
751
|
+
return await svc.get_balance_snapshot()
|
|
752
|
+
|
|
753
|
+
@prot.get(
|
|
754
|
+
"/payouts",
|
|
755
|
+
name="payments_list_payouts",
|
|
756
|
+
response_model=Paginated[PayoutOut],
|
|
757
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
758
|
+
tags=["Payouts"],
|
|
759
|
+
)
|
|
760
|
+
async def list_payouts(svc: PaymentsService = Depends(get_service)):
|
|
761
|
+
ctx = use_pagination()
|
|
762
|
+
items, next_cursor = await svc.list_payouts(limit=ctx.limit, cursor=ctx.cursor)
|
|
763
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
764
|
+
|
|
765
|
+
@svc.get(
|
|
766
|
+
"/payouts/{provider_payout_id}",
|
|
767
|
+
name="payments_get_payout",
|
|
768
|
+
response_model=PayoutOut,
|
|
769
|
+
tags=["Payouts"],
|
|
770
|
+
)
|
|
771
|
+
async def get_payout(provider_payout_id: str, svc: PaymentsService = Depends(get_service)):
|
|
772
|
+
return await svc.get_payout(provider_payout_id)
|
|
773
|
+
|
|
774
|
+
# ===== Webhook replay (operational) =====
|
|
775
|
+
@svc.post(
|
|
776
|
+
"/webhooks/replay",
|
|
777
|
+
name="payments_replay_webhooks",
|
|
778
|
+
response_model=WebhookReplayOut,
|
|
779
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
780
|
+
tags=["Webhooks"],
|
|
781
|
+
)
|
|
782
|
+
async def replay_webhooks(
|
|
783
|
+
since: Optional[str] = None,
|
|
784
|
+
until: Optional[str] = None,
|
|
785
|
+
data: WebhookReplayIn = Body(default=WebhookReplayIn()),
|
|
786
|
+
svc: PaymentsService = Depends(get_service),
|
|
787
|
+
):
|
|
788
|
+
count = await svc.replay_webhooks(since, until, data.event_ids or [])
|
|
789
|
+
await svc.session.flush()
|
|
790
|
+
return {"replayed": count}
|
|
791
|
+
|
|
792
|
+
# ===== Customers: list/get =====
|
|
793
|
+
@prot.get(
|
|
794
|
+
"/customers",
|
|
795
|
+
response_model=Paginated[CustomerOut],
|
|
796
|
+
name="payments_list_customers",
|
|
797
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
798
|
+
tags=["Customers"],
|
|
799
|
+
)
|
|
800
|
+
async def list_customers_endpoint(
|
|
801
|
+
provider: Optional[str] = None,
|
|
802
|
+
user_id: Optional[str] = None,
|
|
803
|
+
svc: PaymentsService = Depends(get_service),
|
|
804
|
+
):
|
|
805
|
+
ctx = use_pagination()
|
|
806
|
+
items, next_cursor = await svc.list_customers(
|
|
807
|
+
CustomersListFilter(
|
|
808
|
+
provider=provider, user_id=user_id, limit=ctx.limit, cursor=ctx.cursor
|
|
809
|
+
)
|
|
810
|
+
)
|
|
811
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
812
|
+
|
|
813
|
+
@prot.get(
|
|
814
|
+
"/customers/{provider_customer_id}",
|
|
815
|
+
response_model=CustomerOut,
|
|
816
|
+
name="payments_get_customer",
|
|
817
|
+
tags=["Customers"],
|
|
818
|
+
)
|
|
819
|
+
async def get_customer_endpoint(
|
|
820
|
+
provider_customer_id: str, svc: PaymentsService = Depends(get_service)
|
|
821
|
+
):
|
|
822
|
+
return await svc.get_customer(provider_customer_id)
|
|
823
|
+
|
|
824
|
+
# ===== Payment Methods: get/update =====
|
|
825
|
+
@prot.get(
|
|
826
|
+
"/methods/{provider_method_id}",
|
|
827
|
+
response_model=PaymentMethodOut,
|
|
828
|
+
name="payments_get_method",
|
|
829
|
+
tags=["Payment Methods"],
|
|
830
|
+
)
|
|
831
|
+
async def get_method(provider_method_id: str, svc: PaymentsService = Depends(get_service)):
|
|
832
|
+
return await svc.get_payment_method(provider_method_id)
|
|
833
|
+
|
|
834
|
+
@prot.post(
|
|
835
|
+
"/methods/{provider_method_id}",
|
|
836
|
+
response_model=PaymentMethodOut,
|
|
837
|
+
name="payments_update_method",
|
|
838
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
839
|
+
tags=["Payment Methods"],
|
|
840
|
+
)
|
|
841
|
+
async def update_method(
|
|
842
|
+
provider_method_id: str,
|
|
843
|
+
data: PaymentMethodUpdateIn,
|
|
844
|
+
svc: PaymentsService = Depends(get_service),
|
|
845
|
+
):
|
|
846
|
+
out = await svc.update_payment_method(provider_method_id, data)
|
|
847
|
+
await svc.session.flush()
|
|
848
|
+
return out
|
|
849
|
+
|
|
850
|
+
# ===== Products: get/list/update (archive via active=False) =====
|
|
851
|
+
@svc.get(
|
|
852
|
+
"/products/{provider_product_id}",
|
|
853
|
+
response_model=ProductOut,
|
|
854
|
+
name="payments_get_product",
|
|
855
|
+
tags=["Products"],
|
|
856
|
+
)
|
|
857
|
+
async def get_product_endpoint(
|
|
858
|
+
provider_product_id: str, svc: PaymentsService = Depends(get_service)
|
|
859
|
+
):
|
|
860
|
+
return await svc.get_product(provider_product_id)
|
|
861
|
+
|
|
862
|
+
@prot.get(
|
|
863
|
+
"/products",
|
|
864
|
+
response_model=Paginated[ProductOut],
|
|
865
|
+
name="payments_list_products",
|
|
866
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
867
|
+
tags=["Products"],
|
|
868
|
+
)
|
|
869
|
+
async def list_products_endpoint(
|
|
870
|
+
active: Optional[bool] = None,
|
|
871
|
+
svc: PaymentsService = Depends(get_service),
|
|
872
|
+
):
|
|
873
|
+
ctx = use_pagination()
|
|
874
|
+
items, next_cursor = await svc.list_products(
|
|
875
|
+
active=active, limit=ctx.limit, cursor=ctx.cursor
|
|
876
|
+
)
|
|
877
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
878
|
+
|
|
879
|
+
@svc.post(
|
|
880
|
+
"/products/{provider_product_id}",
|
|
881
|
+
response_model=ProductOut,
|
|
882
|
+
name="payments_update_product",
|
|
883
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
884
|
+
tags=["Products"],
|
|
885
|
+
)
|
|
886
|
+
async def update_product_endpoint(
|
|
887
|
+
provider_product_id: str,
|
|
888
|
+
data: ProductUpdateIn,
|
|
889
|
+
svc: PaymentsService = Depends(get_service),
|
|
890
|
+
):
|
|
891
|
+
out = await svc.update_product(provider_product_id, data)
|
|
892
|
+
await svc.session.flush()
|
|
893
|
+
return out
|
|
894
|
+
|
|
895
|
+
# ===== Prices: get/list/update (active toggle) =====
|
|
896
|
+
@prot.get(
|
|
897
|
+
"/prices/{provider_price_id}",
|
|
898
|
+
response_model=PriceOut,
|
|
899
|
+
name="payments_get_price",
|
|
900
|
+
tags=["Prices"],
|
|
901
|
+
)
|
|
902
|
+
async def get_price_endpoint(
|
|
903
|
+
provider_price_id: str, svc: PaymentsService = Depends(get_service)
|
|
904
|
+
):
|
|
905
|
+
return await svc.get_price(provider_price_id)
|
|
906
|
+
|
|
907
|
+
@prot.get(
|
|
908
|
+
"/prices",
|
|
909
|
+
response_model=Paginated[PriceOut],
|
|
910
|
+
name="payments_list_prices",
|
|
911
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
912
|
+
tags=["Prices"],
|
|
913
|
+
)
|
|
914
|
+
async def list_prices_endpoint(
|
|
915
|
+
provider_product_id: Optional[str] = None,
|
|
916
|
+
active: Optional[bool] = None,
|
|
917
|
+
svc: PaymentsService = Depends(get_service),
|
|
918
|
+
):
|
|
919
|
+
ctx = use_pagination()
|
|
920
|
+
items, next_cursor = await svc.list_prices(
|
|
921
|
+
provider_product_id=provider_product_id,
|
|
922
|
+
active=active,
|
|
923
|
+
limit=ctx.limit,
|
|
924
|
+
cursor=ctx.cursor,
|
|
925
|
+
)
|
|
926
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
927
|
+
|
|
928
|
+
@svc.post(
|
|
929
|
+
"/prices/{provider_price_id}",
|
|
930
|
+
response_model=PriceOut,
|
|
931
|
+
name="payments_update_price",
|
|
932
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
933
|
+
tags=["Prices"],
|
|
934
|
+
)
|
|
935
|
+
async def update_price_endpoint(
|
|
936
|
+
provider_price_id: str,
|
|
937
|
+
data: PriceUpdateIn,
|
|
938
|
+
svc: PaymentsService = Depends(get_service),
|
|
939
|
+
):
|
|
940
|
+
out = await svc.update_price(provider_price_id, data)
|
|
941
|
+
await svc.session.flush()
|
|
942
|
+
return out
|
|
943
|
+
|
|
944
|
+
# ===== Subscriptions: get/list =====
|
|
945
|
+
@prot.get(
|
|
946
|
+
"/subscriptions/{provider_subscription_id}",
|
|
947
|
+
response_model=SubscriptionOut,
|
|
948
|
+
name="payments_get_subscription",
|
|
949
|
+
tags=["Subscriptions"],
|
|
950
|
+
)
|
|
951
|
+
async def get_subscription_endpoint(
|
|
952
|
+
provider_subscription_id: str, svc: PaymentsService = Depends(get_service)
|
|
953
|
+
):
|
|
954
|
+
return await svc.get_subscription(provider_subscription_id)
|
|
955
|
+
|
|
956
|
+
@prot.get(
|
|
957
|
+
"/subscriptions",
|
|
958
|
+
response_model=Paginated[SubscriptionOut],
|
|
959
|
+
name="payments_list_subscriptions",
|
|
960
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
961
|
+
tags=["Subscriptions"],
|
|
962
|
+
)
|
|
963
|
+
async def list_subscriptions_endpoint(
|
|
964
|
+
customer_provider_id: Optional[str] = None,
|
|
965
|
+
status: Optional[str] = None,
|
|
966
|
+
svc: PaymentsService = Depends(get_service),
|
|
967
|
+
):
|
|
968
|
+
ctx = use_pagination()
|
|
969
|
+
items, next_cursor = await svc.list_subscriptions(
|
|
970
|
+
customer_provider_id=customer_provider_id,
|
|
971
|
+
status=status,
|
|
972
|
+
limit=ctx.limit,
|
|
973
|
+
cursor=ctx.cursor,
|
|
974
|
+
)
|
|
975
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
976
|
+
|
|
977
|
+
# ===== Invoices: list line items =====
|
|
978
|
+
@prot.get(
|
|
979
|
+
"/invoices/{provider_invoice_id}/lines",
|
|
980
|
+
response_model=Paginated[InvoiceLineItemOut],
|
|
981
|
+
name="payments_list_invoice_line_items",
|
|
982
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
983
|
+
tags=["Invoices"],
|
|
984
|
+
)
|
|
985
|
+
async def list_invoice_lines_endpoint(
|
|
986
|
+
provider_invoice_id: str,
|
|
987
|
+
svc: PaymentsService = Depends(get_service),
|
|
988
|
+
):
|
|
989
|
+
ctx = use_pagination()
|
|
990
|
+
items, next_cursor = await svc.list_invoice_line_items(
|
|
991
|
+
provider_invoice_id, limit=ctx.limit, cursor=ctx.cursor
|
|
992
|
+
)
|
|
993
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
994
|
+
|
|
995
|
+
# ===== Refunds: list/get =====
|
|
996
|
+
@prot.get(
|
|
997
|
+
"/refunds",
|
|
998
|
+
response_model=Paginated[RefundOut],
|
|
999
|
+
name="payments_list_refunds",
|
|
1000
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
1001
|
+
tags=["Refunds"],
|
|
1002
|
+
)
|
|
1003
|
+
async def list_refunds_endpoint(
|
|
1004
|
+
provider_payment_intent_id: Optional[str] = None,
|
|
1005
|
+
svc: PaymentsService = Depends(get_service),
|
|
1006
|
+
):
|
|
1007
|
+
ctx = use_pagination()
|
|
1008
|
+
items, next_cursor = await svc.list_refunds(
|
|
1009
|
+
provider_payment_intent_id=provider_payment_intent_id,
|
|
1010
|
+
limit=ctx.limit,
|
|
1011
|
+
cursor=ctx.cursor,
|
|
1012
|
+
)
|
|
1013
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
1014
|
+
|
|
1015
|
+
@prot.get(
|
|
1016
|
+
"/refunds/{provider_refund_id}",
|
|
1017
|
+
response_model=RefundOut,
|
|
1018
|
+
name="payments_get_refund",
|
|
1019
|
+
tags=["Refunds"],
|
|
1020
|
+
)
|
|
1021
|
+
async def get_refund_endpoint(
|
|
1022
|
+
provider_refund_id: str, svc: PaymentsService = Depends(get_service)
|
|
1023
|
+
):
|
|
1024
|
+
return await svc.get_refund(provider_refund_id)
|
|
1025
|
+
|
|
1026
|
+
# ===== Usage Records: list/get =====
|
|
1027
|
+
@prot.get(
|
|
1028
|
+
"/usage_records",
|
|
1029
|
+
response_model=Paginated[UsageRecordOut],
|
|
1030
|
+
name="payments_list_usage_records",
|
|
1031
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
1032
|
+
tags=["Usage Records"],
|
|
1033
|
+
)
|
|
1034
|
+
async def list_usage_records_endpoint(
|
|
1035
|
+
subscription_item: Optional[str] = None,
|
|
1036
|
+
provider_price_id: Optional[str] = None,
|
|
1037
|
+
svc: PaymentsService = Depends(get_service),
|
|
1038
|
+
):
|
|
1039
|
+
ctx = use_pagination()
|
|
1040
|
+
items, next_cursor = await svc.list_usage_records(
|
|
1041
|
+
UsageRecordListFilter(
|
|
1042
|
+
subscription_item=subscription_item,
|
|
1043
|
+
provider_price_id=provider_price_id,
|
|
1044
|
+
limit=ctx.limit,
|
|
1045
|
+
cursor=ctx.cursor,
|
|
1046
|
+
)
|
|
1047
|
+
)
|
|
1048
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
1049
|
+
|
|
1050
|
+
@prot.get(
|
|
1051
|
+
"/usage_records/{usage_record_id}",
|
|
1052
|
+
response_model=UsageRecordOut,
|
|
1053
|
+
name="payments_get_usage_record",
|
|
1054
|
+
tags=["Usage Records"],
|
|
1055
|
+
)
|
|
1056
|
+
async def get_usage_record_endpoint(
|
|
1057
|
+
usage_record_id: str, svc: PaymentsService = Depends(get_service)
|
|
1058
|
+
):
|
|
1059
|
+
return await svc.get_usage_record(usage_record_id)
|
|
1060
|
+
|
|
1061
|
+
# --- Canonical: remove local alias/association (non-destructive) ---
|
|
1062
|
+
@prot.delete(
|
|
1063
|
+
"/method_aliases/{alias_id}",
|
|
1064
|
+
name="payments_delete_method_alias",
|
|
1065
|
+
summary="Remove Method Alias (non-destructive)",
|
|
1066
|
+
response_model=PaymentMethodOut,
|
|
1067
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
1068
|
+
tags=["Payment Methods"],
|
|
1069
|
+
)
|
|
1070
|
+
async def delete_method_alias(alias_id: str, svc: PaymentsService = Depends(get_service)):
|
|
1071
|
+
"""
|
|
1072
|
+
Removes the local alias/association to a payment method.
|
|
1073
|
+
This does **not** delete the underlying payment method at the provider.
|
|
1074
|
+
Equivalent to `detach_payment_method`.
|
|
1075
|
+
"""
|
|
1076
|
+
out = await svc.detach_payment_method(alias_id)
|
|
1077
|
+
await svc.session.flush()
|
|
1078
|
+
return out
|
|
1079
|
+
|
|
361
1080
|
routers.append(svc)
|
|
362
1081
|
routers.append(pub)
|
|
363
1082
|
return routers
|