svc-infra 0.1.506__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/alembic.py +11 -0
- svc_infra/apf_payments/models.py +339 -0
- 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 +270 -0
- svc_infra/apf_payments/provider/registry.py +31 -0
- svc_infra/apf_payments/provider/stripe.py +873 -0
- svc_infra/apf_payments/schemas.py +333 -0
- svc_infra/apf_payments/service.py +892 -0
- svc_infra/apf_payments/settings.py +67 -0
- svc_infra/api/fastapi/__init__.py +6 -0
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +231 -0
- svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
- svc_infra/api/fastapi/apf_payments/router.py +1082 -0
- svc_infra/api/fastapi/apf_payments/setup.py +73 -0
- svc_infra/api/fastapi/auth/add.py +15 -6
- svc_infra/api/fastapi/auth/gaurd.py +67 -5
- svc_infra/api/fastapi/auth/mfa/router.py +18 -9
- svc_infra/api/fastapi/auth/routers/account.py +3 -2
- svc_infra/api/fastapi/auth/routers/apikey_router.py +11 -5
- svc_infra/api/fastapi/auth/routers/oauth_router.py +82 -37
- svc_infra/api/fastapi/auth/routers/session_router.py +63 -0
- svc_infra/api/fastapi/auth/security.py +3 -1
- svc_infra/api/fastapi/auth/settings.py +2 -0
- svc_infra/api/fastapi/auth/state.py +1 -1
- 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 +14 -2
- 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 +254 -0
- svc_infra/api/fastapi/dual/dualize.py +38 -33
- svc_infra/api/fastapi/dual/router.py +48 -1
- svc_infra/api/fastapi/dx.py +3 -3
- svc_infra/api/fastapi/http/__init__.py +0 -0
- svc_infra/api/fastapi/http/concurrency.py +14 -0
- svc_infra/api/fastapi/http/conditional.py +33 -0
- svc_infra/api/fastapi/http/deprecation.py +21 -0
- 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 +116 -0
- 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 +119 -0
- svc_infra/api/fastapi/middleware/ratelimit_store.py +84 -0
- svc_infra/api/fastapi/middleware/request_id.py +23 -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 +768 -55
- svc_infra/api/fastapi/ops/add.py +73 -0
- svc_infra/api/fastapi/pagination.py +363 -0
- svc_infra/api/fastapi/paths/auth.py +14 -14
- svc_infra/api/fastapi/paths/prefix.py +0 -1
- svc_infra/api/fastapi/paths/user.py +1 -1
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +48 -15
- 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 +120 -14
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +5 -4
- 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/apikey.py +1 -1
- svc_infra/db/sql/authref.py +61 -0
- svc_infra/db/sql/core.py +2 -2
- svc_infra/db/sql/repository.py +52 -12
- svc_infra/db/sql/resource.py +5 -0
- svc_infra/db/sql/scaffold.py +16 -4
- svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +199 -76
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +231 -79
- 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.654.dist-info/RECORD +352 -0
- svc_infra/api/fastapi/deps.py +0 -3
- svc_infra-0.1.506.dist-info/METADATA +0 -78
- svc_infra-0.1.506.dist-info/RECORD +0 -213
- /svc_infra/{api/fastapi/schemas → apf_payments}/__init__.py +0 -0
- {svc_infra-0.1.506.dist-info → svc_infra-0.1.654.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.506.dist-info → svc_infra-0.1.654.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Literal, Optional, cast
|
|
5
|
+
|
|
6
|
+
from fastapi import Body, Depends, Header, HTTPException, Request, Response, status
|
|
7
|
+
from starlette.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
from svc_infra.apf_payments.schemas import (
|
|
10
|
+
BalanceSnapshotOut,
|
|
11
|
+
CaptureIn,
|
|
12
|
+
CustomerOut,
|
|
13
|
+
CustomersListFilter,
|
|
14
|
+
CustomerUpsertIn,
|
|
15
|
+
DisputeOut,
|
|
16
|
+
IntentCreateIn,
|
|
17
|
+
IntentListFilter,
|
|
18
|
+
IntentOut,
|
|
19
|
+
InvoiceCreateIn,
|
|
20
|
+
InvoiceLineItemIn,
|
|
21
|
+
InvoiceLineItemOut,
|
|
22
|
+
InvoiceOut,
|
|
23
|
+
InvoicesListFilter,
|
|
24
|
+
PaymentMethodAttachIn,
|
|
25
|
+
PaymentMethodOut,
|
|
26
|
+
PaymentMethodUpdateIn,
|
|
27
|
+
PayoutOut,
|
|
28
|
+
PriceCreateIn,
|
|
29
|
+
PriceOut,
|
|
30
|
+
PriceUpdateIn,
|
|
31
|
+
ProductCreateIn,
|
|
32
|
+
ProductOut,
|
|
33
|
+
ProductUpdateIn,
|
|
34
|
+
RefundIn,
|
|
35
|
+
RefundOut,
|
|
36
|
+
SetupIntentCreateIn,
|
|
37
|
+
SetupIntentOut,
|
|
38
|
+
StatementRow,
|
|
39
|
+
SubscriptionCreateIn,
|
|
40
|
+
SubscriptionOut,
|
|
41
|
+
SubscriptionUpdateIn,
|
|
42
|
+
TransactionRow,
|
|
43
|
+
UsageRecordIn,
|
|
44
|
+
UsageRecordListFilter,
|
|
45
|
+
UsageRecordOut,
|
|
46
|
+
WebhookAckOut,
|
|
47
|
+
WebhookReplayIn,
|
|
48
|
+
WebhookReplayOut,
|
|
49
|
+
)
|
|
50
|
+
from svc_infra.apf_payments.service import PaymentsService
|
|
51
|
+
from svc_infra.api.fastapi.auth.security import OptionalIdentity, Principal
|
|
52
|
+
from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
|
|
53
|
+
from svc_infra.api.fastapi.dual import protected_router, public_router, service_router, user_router
|
|
54
|
+
from svc_infra.api.fastapi.dual.router import DualAPIRouter
|
|
55
|
+
from svc_infra.api.fastapi.middleware.idempotency import require_idempotency_key
|
|
56
|
+
from svc_infra.api.fastapi.pagination import (
|
|
57
|
+
Paginated,
|
|
58
|
+
cursor_pager,
|
|
59
|
+
cursor_window,
|
|
60
|
+
sort_by,
|
|
61
|
+
use_pagination,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
_TX_KINDS = {"payment", "refund", "fee", "payout", "capture"}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _tx_kind(kind: str) -> Literal["payment", "refund", "fee", "payout", "capture"]:
|
|
68
|
+
if kind not in _TX_KINDS:
|
|
69
|
+
raise ValueError(f"Unknown ledger kind: {kind!r}")
|
|
70
|
+
return cast(Literal["payment", "refund", "fee", "payout", "capture"], kind)
|
|
71
|
+
|
|
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
|
+
|
|
122
|
+
# --- deps ---
|
|
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)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# --- routers grouped by auth posture (same prefix is fine; FastAPI merges) ---
|
|
150
|
+
def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
151
|
+
routers: list[DualAPIRouter] = []
|
|
152
|
+
|
|
153
|
+
pub = public_router(prefix=prefix)
|
|
154
|
+
user = user_router(prefix=prefix)
|
|
155
|
+
svc = service_router(prefix=prefix)
|
|
156
|
+
prot = protected_router(prefix=prefix)
|
|
157
|
+
|
|
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
|
+
)
|
|
166
|
+
async def upsert_customer(data: CustomerUpsertIn, svc: PaymentsService = Depends(get_service)):
|
|
167
|
+
out = await svc.ensure_customer(data)
|
|
168
|
+
await svc.session.flush()
|
|
169
|
+
return out
|
|
170
|
+
|
|
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
|
+
):
|
|
186
|
+
out = await svc.create_intent(user_id=None, data=data)
|
|
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
|
+
)
|
|
191
|
+
return out
|
|
192
|
+
|
|
193
|
+
routers.append(user)
|
|
194
|
+
|
|
195
|
+
# ===== Payment Intents (confirm/cancel/refund/list/get/capture/resume) =====
|
|
196
|
+
@prot.post(
|
|
197
|
+
"/intents/{provider_intent_id}/confirm",
|
|
198
|
+
response_model=IntentOut,
|
|
199
|
+
name="payments_confirm_intent",
|
|
200
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
201
|
+
tags=["Payment Intents"],
|
|
202
|
+
)
|
|
203
|
+
async def confirm_intent(provider_intent_id: str, svc: PaymentsService = Depends(get_service)):
|
|
204
|
+
out = await svc.confirm_intent(provider_intent_id)
|
|
205
|
+
await svc.session.flush()
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
@prot.post(
|
|
209
|
+
"/intents/{provider_intent_id}/cancel",
|
|
210
|
+
response_model=IntentOut,
|
|
211
|
+
name="payments_cancel_intent",
|
|
212
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
213
|
+
tags=["Payment Intents"],
|
|
214
|
+
)
|
|
215
|
+
async def cancel_intent(provider_intent_id: str, svc: PaymentsService = Depends(get_service)):
|
|
216
|
+
out = await svc.cancel_intent(provider_intent_id)
|
|
217
|
+
await svc.session.flush()
|
|
218
|
+
return out
|
|
219
|
+
|
|
220
|
+
@prot.post(
|
|
221
|
+
"/intents/{provider_intent_id}/refund",
|
|
222
|
+
response_model=IntentOut,
|
|
223
|
+
name="payments_refund_intent",
|
|
224
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
225
|
+
tags=["Payment Intents", "Refunds"],
|
|
226
|
+
)
|
|
227
|
+
async def refund_intent(
|
|
228
|
+
provider_intent_id: str, data: RefundIn, svc: PaymentsService = Depends(get_service)
|
|
229
|
+
):
|
|
230
|
+
out = await svc.refund(provider_intent_id, data)
|
|
231
|
+
await svc.session.flush()
|
|
232
|
+
return out
|
|
233
|
+
|
|
234
|
+
@prot.get(
|
|
235
|
+
"/transactions",
|
|
236
|
+
response_model=Paginated[TransactionRow],
|
|
237
|
+
name="payments_list_transactions",
|
|
238
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
239
|
+
tags=["Transactions"],
|
|
240
|
+
)
|
|
241
|
+
async def list_transactions(svc: PaymentsService = Depends(get_service)):
|
|
242
|
+
from sqlalchemy import select
|
|
243
|
+
|
|
244
|
+
from svc_infra.apf_payments.models import LedgerEntry
|
|
245
|
+
|
|
246
|
+
rows = (await svc.session.execute(select(LedgerEntry))).scalars().all()
|
|
247
|
+
rows_sorted = sort_by(rows, key=lambda e: e.ts, desc=True)
|
|
248
|
+
|
|
249
|
+
ctx = use_pagination()
|
|
250
|
+
window, next_cursor = cursor_window(
|
|
251
|
+
rows_sorted,
|
|
252
|
+
cursor=ctx.cursor,
|
|
253
|
+
limit=ctx.limit,
|
|
254
|
+
key=lambda e: int(e.ts.timestamp()),
|
|
255
|
+
descending=True,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
items = [
|
|
259
|
+
TransactionRow(
|
|
260
|
+
id=e.id,
|
|
261
|
+
ts=e.ts.isoformat(),
|
|
262
|
+
type=_tx_kind(e.kind),
|
|
263
|
+
amount=int(e.amount),
|
|
264
|
+
currency=e.currency,
|
|
265
|
+
status=e.status,
|
|
266
|
+
provider=e.provider,
|
|
267
|
+
provider_ref=e.provider_ref or "",
|
|
268
|
+
user_id=e.user_id,
|
|
269
|
+
)
|
|
270
|
+
for e in window
|
|
271
|
+
]
|
|
272
|
+
return ctx.wrap(items, next_cursor=next_cursor)
|
|
273
|
+
|
|
274
|
+
routers.append(prot)
|
|
275
|
+
|
|
276
|
+
@pub.post(
|
|
277
|
+
"/webhooks/{provider}",
|
|
278
|
+
name="payments_webhook",
|
|
279
|
+
response_model=WebhookAckOut,
|
|
280
|
+
tags=["Webhooks"],
|
|
281
|
+
)
|
|
282
|
+
async def webhooks(
|
|
283
|
+
provider: str,
|
|
284
|
+
request: Request,
|
|
285
|
+
svc: PaymentsService = Depends(get_service),
|
|
286
|
+
signature: Optional[str] = Header(None, alias="Stripe-Signature"),
|
|
287
|
+
):
|
|
288
|
+
payload = await request.body()
|
|
289
|
+
out = await svc.handle_webhook(provider.lower(), signature, payload)
|
|
290
|
+
await svc.session.flush()
|
|
291
|
+
return JSONResponse(out)
|
|
292
|
+
|
|
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
|
+
)
|
|
302
|
+
async def attach_method(
|
|
303
|
+
data: PaymentMethodAttachIn, svc: PaymentsService = Depends(get_service)
|
|
304
|
+
):
|
|
305
|
+
out = await svc.attach_payment_method(data)
|
|
306
|
+
await svc.session.flush()
|
|
307
|
+
return out
|
|
308
|
+
|
|
309
|
+
@prot.get(
|
|
310
|
+
"/methods",
|
|
311
|
+
response_model=Paginated[PaymentMethodOut],
|
|
312
|
+
name="payments_list_methods",
|
|
313
|
+
dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
|
|
314
|
+
tags=["Payment Methods"],
|
|
315
|
+
)
|
|
316
|
+
async def list_methods(
|
|
317
|
+
customer_provider_id: str,
|
|
318
|
+
svc: PaymentsService = Depends(get_service),
|
|
319
|
+
):
|
|
320
|
+
methods = await svc.list_payment_methods(customer_provider_id)
|
|
321
|
+
methods_sorted = sort_by(
|
|
322
|
+
sort_by(methods, key=lambda m: m.provider_method_id or "", desc=False),
|
|
323
|
+
key=lambda m: m.is_default,
|
|
324
|
+
desc=True,
|
|
325
|
+
)
|
|
326
|
+
ctx = use_pagination()
|
|
327
|
+
window, next_cursor = cursor_window(
|
|
328
|
+
methods_sorted,
|
|
329
|
+
cursor=ctx.cursor,
|
|
330
|
+
limit=ctx.limit,
|
|
331
|
+
key=lambda m: m.provider_method_id or "",
|
|
332
|
+
descending=False,
|
|
333
|
+
)
|
|
334
|
+
return ctx.wrap(window, next_cursor=next_cursor)
|
|
335
|
+
|
|
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
|
+
)
|
|
343
|
+
async def detach_method(provider_method_id: str, svc: PaymentsService = Depends(get_service)):
|
|
344
|
+
out = await svc.detach_payment_method(provider_method_id)
|
|
345
|
+
await svc.session.flush()
|
|
346
|
+
return out
|
|
347
|
+
|
|
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
|
+
)
|
|
355
|
+
async def set_default_method(
|
|
356
|
+
provider_method_id: str,
|
|
357
|
+
customer_provider_id: str,
|
|
358
|
+
svc: PaymentsService = Depends(get_service),
|
|
359
|
+
):
|
|
360
|
+
out = await svc.set_default_payment_method(customer_provider_id, provider_method_id)
|
|
361
|
+
await svc.session.flush()
|
|
362
|
+
return out
|
|
363
|
+
|
|
364
|
+
# PRODUCTS/PRICES
|
|
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
|
+
)
|
|
373
|
+
async def create_product(data: ProductCreateIn, svc: PaymentsService = Depends(get_service)):
|
|
374
|
+
out = await svc.create_product(data)
|
|
375
|
+
await svc.session.flush()
|
|
376
|
+
return out
|
|
377
|
+
|
|
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
|
+
)
|
|
386
|
+
async def create_price(data: PriceCreateIn, svc: PaymentsService = Depends(get_service)):
|
|
387
|
+
out = await svc.create_price(data)
|
|
388
|
+
await svc.session.flush()
|
|
389
|
+
return out
|
|
390
|
+
|
|
391
|
+
# SUBSCRIPTIONS
|
|
392
|
+
@prot.post(
|
|
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"],
|
|
399
|
+
)
|
|
400
|
+
async def create_subscription(
|
|
401
|
+
data: SubscriptionCreateIn, svc: PaymentsService = Depends(get_service)
|
|
402
|
+
):
|
|
403
|
+
out = await svc.create_subscription(data)
|
|
404
|
+
await svc.session.flush()
|
|
405
|
+
return out
|
|
406
|
+
|
|
407
|
+
@prot.post(
|
|
408
|
+
"/subscriptions/{provider_subscription_id}",
|
|
409
|
+
response_model=SubscriptionOut,
|
|
410
|
+
name="payments_update_subscription",
|
|
411
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
412
|
+
tags=["Subscriptions"],
|
|
413
|
+
)
|
|
414
|
+
async def update_subscription(
|
|
415
|
+
provider_subscription_id: str,
|
|
416
|
+
data: SubscriptionUpdateIn,
|
|
417
|
+
svc: PaymentsService = Depends(get_service),
|
|
418
|
+
):
|
|
419
|
+
out = await svc.update_subscription(provider_subscription_id, data)
|
|
420
|
+
await svc.session.flush()
|
|
421
|
+
return out
|
|
422
|
+
|
|
423
|
+
@prot.post(
|
|
424
|
+
"/subscriptions/{provider_subscription_id}/cancel",
|
|
425
|
+
response_model=SubscriptionOut,
|
|
426
|
+
name="payments_cancel_subscription",
|
|
427
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
428
|
+
tags=["Subscriptions"],
|
|
429
|
+
)
|
|
430
|
+
async def cancel_subscription(
|
|
431
|
+
provider_subscription_id: str,
|
|
432
|
+
at_period_end: bool = True,
|
|
433
|
+
svc: PaymentsService = Depends(get_service),
|
|
434
|
+
):
|
|
435
|
+
out = await svc.cancel_subscription(provider_subscription_id, at_period_end)
|
|
436
|
+
await svc.session.flush()
|
|
437
|
+
return out
|
|
438
|
+
|
|
439
|
+
# INVOICES
|
|
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
|
+
):
|
|
454
|
+
out = await svc.create_invoice(data)
|
|
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
|
+
)
|
|
459
|
+
return out
|
|
460
|
+
|
|
461
|
+
@prot.post(
|
|
462
|
+
"/invoices/{provider_invoice_id}/finalize",
|
|
463
|
+
response_model=InvoiceOut,
|
|
464
|
+
name="payments_finalize_invoice",
|
|
465
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
466
|
+
tags=["Invoices"],
|
|
467
|
+
)
|
|
468
|
+
async def finalize_invoice(
|
|
469
|
+
provider_invoice_id: str, svc: PaymentsService = Depends(get_service)
|
|
470
|
+
):
|
|
471
|
+
out = await svc.finalize_invoice(provider_invoice_id)
|
|
472
|
+
await svc.session.flush()
|
|
473
|
+
return out
|
|
474
|
+
|
|
475
|
+
@prot.post(
|
|
476
|
+
"/invoices/{provider_invoice_id}/void",
|
|
477
|
+
response_model=InvoiceOut,
|
|
478
|
+
name="payments_void_invoice",
|
|
479
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
480
|
+
tags=["Invoices"],
|
|
481
|
+
)
|
|
482
|
+
async def void_invoice(provider_invoice_id: str, svc: PaymentsService = Depends(get_service)):
|
|
483
|
+
out = await svc.void_invoice(provider_invoice_id)
|
|
484
|
+
await svc.session.flush()
|
|
485
|
+
return out
|
|
486
|
+
|
|
487
|
+
@prot.post(
|
|
488
|
+
"/invoices/{provider_invoice_id}/pay",
|
|
489
|
+
response_model=InvoiceOut,
|
|
490
|
+
name="payments_pay_invoice",
|
|
491
|
+
dependencies=[Depends(require_idempotency_key)],
|
|
492
|
+
tags=["Invoices"],
|
|
493
|
+
)
|
|
494
|
+
async def pay_invoice(provider_invoice_id: str, svc: PaymentsService = Depends(get_service)):
|
|
495
|
+
out = await svc.pay_invoice(provider_invoice_id)
|
|
496
|
+
await svc.session.flush()
|
|
497
|
+
return out
|
|
498
|
+
|
|
499
|
+
# INTENTS: get/hydrate
|
|
500
|
+
@prot.get(
|
|
501
|
+
"/intents/{provider_intent_id}",
|
|
502
|
+
response_model=IntentOut,
|
|
503
|
+
name="payments_get_intent",
|
|
504
|
+
tags=["Payment Intents"],
|
|
505
|
+
)
|
|
506
|
+
async def get_intent(provider_intent_id: str, svc: PaymentsService = Depends(get_service)):
|
|
507
|
+
return await svc.get_intent(provider_intent_id)
|
|
508
|
+
|
|
509
|
+
# STATEMENTS (rollup)
|
|
510
|
+
@prot.get(
|
|
511
|
+
"/statements/daily",
|
|
512
|
+
response_model=list[StatementRow],
|
|
513
|
+
name="payments_daily_statements",
|
|
514
|
+
tags=["Statements"],
|
|
515
|
+
)
|
|
516
|
+
async def daily_statements(
|
|
517
|
+
date_from: str | None = None,
|
|
518
|
+
date_to: str | None = None,
|
|
519
|
+
svc: PaymentsService = Depends(get_service),
|
|
520
|
+
):
|
|
521
|
+
return await svc.daily_statements_rollup(date_from, date_to)
|
|
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
|
+
|
|
1080
|
+
routers.append(svc)
|
|
1081
|
+
routers.append(pub)
|
|
1082
|
+
return routers
|