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.
Files changed (175) hide show
  1. svc_infra/apf_payments/README.md +732 -0
  2. svc_infra/apf_payments/models.py +142 -4
  3. svc_infra/apf_payments/provider/__init__.py +4 -0
  4. svc_infra/apf_payments/provider/aiydan.py +797 -0
  5. svc_infra/apf_payments/provider/base.py +178 -12
  6. svc_infra/apf_payments/provider/stripe.py +757 -48
  7. svc_infra/apf_payments/schemas.py +163 -1
  8. svc_infra/apf_payments/service.py +582 -42
  9. svc_infra/apf_payments/settings.py +22 -2
  10. svc_infra/api/fastapi/admin/__init__.py +3 -0
  11. svc_infra/api/fastapi/admin/add.py +231 -0
  12. svc_infra/api/fastapi/apf_payments/router.py +792 -73
  13. svc_infra/api/fastapi/apf_payments/setup.py +13 -4
  14. svc_infra/api/fastapi/auth/add.py +10 -4
  15. svc_infra/api/fastapi/auth/gaurd.py +67 -5
  16. svc_infra/api/fastapi/auth/routers/oauth_router.py +74 -34
  17. svc_infra/api/fastapi/auth/routers/session_router.py +63 -0
  18. svc_infra/api/fastapi/auth/settings.py +2 -0
  19. svc_infra/api/fastapi/billing/router.py +64 -0
  20. svc_infra/api/fastapi/billing/setup.py +19 -0
  21. svc_infra/api/fastapi/cache/add.py +9 -5
  22. svc_infra/api/fastapi/db/nosql/mongo/add.py +33 -27
  23. svc_infra/api/fastapi/db/sql/add.py +40 -18
  24. svc_infra/api/fastapi/db/sql/crud_router.py +176 -14
  25. svc_infra/api/fastapi/db/sql/session.py +16 -0
  26. svc_infra/api/fastapi/db/sql/users.py +13 -1
  27. svc_infra/api/fastapi/dependencies/ratelimit.py +116 -0
  28. svc_infra/api/fastapi/docs/add.py +160 -0
  29. svc_infra/api/fastapi/docs/landing.py +1 -1
  30. svc_infra/api/fastapi/docs/scoped.py +41 -6
  31. svc_infra/api/fastapi/middleware/errors/handlers.py +45 -7
  32. svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
  33. svc_infra/api/fastapi/middleware/idempotency.py +82 -42
  34. svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
  35. svc_infra/api/fastapi/middleware/optimistic_lock.py +37 -0
  36. svc_infra/api/fastapi/middleware/ratelimit.py +84 -11
  37. svc_infra/api/fastapi/middleware/ratelimit_store.py +84 -0
  38. svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
  39. svc_infra/api/fastapi/middleware/timeout.py +148 -0
  40. svc_infra/api/fastapi/openapi/mutators.py +244 -38
  41. svc_infra/api/fastapi/ops/add.py +73 -0
  42. svc_infra/api/fastapi/pagination.py +133 -32
  43. svc_infra/api/fastapi/routers/ping.py +1 -0
  44. svc_infra/api/fastapi/setup.py +23 -14
  45. svc_infra/api/fastapi/tenancy/add.py +19 -0
  46. svc_infra/api/fastapi/tenancy/context.py +112 -0
  47. svc_infra/api/fastapi/versioned.py +101 -0
  48. svc_infra/app/README.md +5 -5
  49. svc_infra/billing/__init__.py +23 -0
  50. svc_infra/billing/async_service.py +147 -0
  51. svc_infra/billing/jobs.py +230 -0
  52. svc_infra/billing/models.py +131 -0
  53. svc_infra/billing/quotas.py +101 -0
  54. svc_infra/billing/schemas.py +33 -0
  55. svc_infra/billing/service.py +115 -0
  56. svc_infra/bundled_docs/README.md +5 -0
  57. svc_infra/bundled_docs/__init__.py +1 -0
  58. svc_infra/bundled_docs/getting-started.md +6 -0
  59. svc_infra/cache/__init__.py +4 -0
  60. svc_infra/cache/add.py +158 -0
  61. svc_infra/cache/backend.py +5 -2
  62. svc_infra/cache/decorators.py +19 -1
  63. svc_infra/cache/keys.py +24 -4
  64. svc_infra/cli/__init__.py +32 -8
  65. svc_infra/cli/__main__.py +4 -0
  66. svc_infra/cli/cmds/__init__.py +10 -0
  67. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +4 -3
  68. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +4 -4
  69. svc_infra/cli/cmds/db/sql/alembic_cmds.py +80 -11
  70. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
  71. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
  72. svc_infra/cli/cmds/docs/docs_cmds.py +140 -0
  73. svc_infra/cli/cmds/dx/__init__.py +12 -0
  74. svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
  75. svc_infra/cli/cmds/help.py +4 -0
  76. svc_infra/cli/cmds/jobs/__init__.py +1 -0
  77. svc_infra/cli/cmds/jobs/jobs_cmds.py +43 -0
  78. svc_infra/cli/cmds/obs/obs_cmds.py +4 -3
  79. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  80. svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
  81. svc_infra/data/add.py +61 -0
  82. svc_infra/data/backup.py +53 -0
  83. svc_infra/data/erasure.py +45 -0
  84. svc_infra/data/fixtures.py +40 -0
  85. svc_infra/data/retention.py +55 -0
  86. svc_infra/db/inbox.py +67 -0
  87. svc_infra/db/nosql/mongo/README.md +13 -13
  88. svc_infra/db/outbox.py +104 -0
  89. svc_infra/db/sql/repository.py +52 -12
  90. svc_infra/db/sql/resource.py +5 -0
  91. svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
  92. svc_infra/db/sql/templates/setup/env_async.py.tmpl +13 -8
  93. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +9 -5
  94. svc_infra/db/sql/tenant.py +79 -0
  95. svc_infra/db/sql/utils.py +18 -4
  96. svc_infra/db/sql/versioning.py +14 -0
  97. svc_infra/docs/acceptance-matrix.md +71 -0
  98. svc_infra/docs/acceptance.md +44 -0
  99. svc_infra/docs/admin.md +425 -0
  100. svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +40 -0
  101. svc_infra/docs/adr/0003-webhooks-framework.md +24 -0
  102. svc_infra/docs/adr/0004-tenancy-model.md +42 -0
  103. svc_infra/docs/adr/0005-data-lifecycle.md +86 -0
  104. svc_infra/docs/adr/0006-ops-slos-and-metrics.md +47 -0
  105. svc_infra/docs/adr/0007-docs-and-sdks.md +83 -0
  106. svc_infra/docs/adr/0008-billing-primitives.md +143 -0
  107. svc_infra/docs/adr/0009-acceptance-harness.md +40 -0
  108. svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
  109. svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
  110. svc_infra/docs/api.md +59 -0
  111. svc_infra/docs/auth.md +11 -0
  112. svc_infra/docs/billing.md +190 -0
  113. svc_infra/docs/cache.md +76 -0
  114. svc_infra/docs/cli.md +74 -0
  115. svc_infra/docs/contributing.md +34 -0
  116. svc_infra/docs/data-lifecycle.md +52 -0
  117. svc_infra/docs/database.md +14 -0
  118. svc_infra/docs/docs-and-sdks.md +62 -0
  119. svc_infra/docs/environment.md +114 -0
  120. svc_infra/docs/getting-started.md +63 -0
  121. svc_infra/docs/idempotency.md +111 -0
  122. svc_infra/docs/jobs.md +67 -0
  123. svc_infra/docs/observability.md +16 -0
  124. svc_infra/docs/ops.md +37 -0
  125. svc_infra/docs/rate-limiting.md +125 -0
  126. svc_infra/docs/repo-review.md +48 -0
  127. svc_infra/docs/security.md +176 -0
  128. svc_infra/docs/tenancy.md +35 -0
  129. svc_infra/docs/timeouts-and-resource-limits.md +147 -0
  130. svc_infra/docs/versioned-integrations.md +146 -0
  131. svc_infra/docs/webhooks.md +112 -0
  132. svc_infra/dx/add.py +63 -0
  133. svc_infra/dx/changelog.py +74 -0
  134. svc_infra/dx/checks.py +67 -0
  135. svc_infra/http/__init__.py +13 -0
  136. svc_infra/http/client.py +72 -0
  137. svc_infra/jobs/builtins/outbox_processor.py +38 -0
  138. svc_infra/jobs/builtins/webhook_delivery.py +90 -0
  139. svc_infra/jobs/easy.py +32 -0
  140. svc_infra/jobs/loader.py +45 -0
  141. svc_infra/jobs/queue.py +81 -0
  142. svc_infra/jobs/redis_queue.py +191 -0
  143. svc_infra/jobs/runner.py +75 -0
  144. svc_infra/jobs/scheduler.py +41 -0
  145. svc_infra/jobs/worker.py +40 -0
  146. svc_infra/mcp/svc_infra_mcp.py +85 -28
  147. svc_infra/obs/README.md +2 -0
  148. svc_infra/obs/add.py +54 -7
  149. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  150. svc_infra/obs/metrics/__init__.py +53 -0
  151. svc_infra/obs/metrics.py +52 -0
  152. svc_infra/security/add.py +201 -0
  153. svc_infra/security/audit.py +130 -0
  154. svc_infra/security/audit_service.py +73 -0
  155. svc_infra/security/headers.py +52 -0
  156. svc_infra/security/hibp.py +95 -0
  157. svc_infra/security/jwt_rotation.py +53 -0
  158. svc_infra/security/lockout.py +96 -0
  159. svc_infra/security/models.py +255 -0
  160. svc_infra/security/org_invites.py +128 -0
  161. svc_infra/security/passwords.py +77 -0
  162. svc_infra/security/permissions.py +149 -0
  163. svc_infra/security/session.py +98 -0
  164. svc_infra/security/signed_cookies.py +80 -0
  165. svc_infra/webhooks/__init__.py +16 -0
  166. svc_infra/webhooks/add.py +322 -0
  167. svc_infra/webhooks/fastapi.py +37 -0
  168. svc_infra/webhooks/router.py +55 -0
  169. svc_infra/webhooks/service.py +67 -0
  170. svc_infra/webhooks/signing.py +30 -0
  171. svc_infra-0.1.654.dist-info/METADATA +154 -0
  172. {svc_infra-0.1.562.dist-info → svc_infra-0.1.654.dist-info}/RECORD +174 -56
  173. svc_infra-0.1.562.dist-info/METADATA +0 -79
  174. {svc_infra-0.1.562.dist-info → svc_infra-0.1.654.dist-info}/WHEEL +0 -0
  175. {svc_infra-0.1.562.dist-info → svc_infra-0.1.654.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,732 @@
1
+ # APF Payments Integration Guide
2
+
3
+ A unified payments abstraction for FastAPI services supporting multiple payment providers (Stripe, Aiydan, etc.) with consistent APIs for intents, subscriptions, invoices, disputes, and more.
4
+
5
+ **Key Features:**
6
+ - 🚀 **Zero-boilerplate setup** – One function call creates a production-ready service
7
+ - 🔌 **Multi-provider support** – Stripe, Aiydan, or custom adapters
8
+ - 🛡️ **Built-in security** – Idempotency, rate limiting, webhook verification, auth guards
9
+ - 📊 **Observability included** – Prometheus metrics, OpenTelemetry tracing, structured logging
10
+ - 🔄 **Auto-configuration** – Reads environment variables, sensible defaults
11
+ - 📝 **Complete API** – 40+ endpoints for payments, subscriptions, invoices, disputes, refunds
12
+ - 🧪 **Test-friendly** – Mock adapters, comprehensive test coverage
13
+
14
+ ---
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Install & Configure
19
+
20
+ ```bash
21
+ # Install with your provider extras
22
+ pip install svc-infra stripe # for Stripe
23
+ # or
24
+ pip install svc-infra aiydan # for Aiydan (when available)
25
+ ```
26
+
27
+ Set environment variables for your chosen provider:
28
+
29
+ **Stripe:**
30
+ ```bash
31
+ export STRIPE_SECRET="sk_test_..."
32
+ export STRIPE_WH_SECRET="whsec_..." # optional, for webhook verification
33
+ export PAYMENTS_PROVIDER="stripe" # default
34
+ ```
35
+
36
+ **Aiydan:**
37
+ ```bash
38
+ export AIYDAN_API_KEY="aiydan_key_..."
39
+ export AIYDAN_CLIENT_KEY="..." # optional
40
+ export AIYDAN_MERCHANT_ACCOUNT="..." # optional
41
+ export AIYDAN_HMAC_KEY="..." # optional
42
+ export AIYDAN_BASE_URL="https://..." # optional
43
+ export AIYDAN_WH_SECRET="..." # optional
44
+ export PAYMENTS_PROVIDER="aiydan"
45
+ ```
46
+
47
+ ### 2. Create Your FastAPI App with Payments
48
+
49
+ **Option A: The Easiest Way (Recommended)**
50
+
51
+ Use `easy_service_app` for a complete FastAPI service with logging, observability, and payments in one call:
52
+
53
+ ```python
54
+ from svc_infra.api.fastapi.ease import easy_service_app
55
+
56
+ app = easy_service_app(
57
+ name="My Payment Service",
58
+ release="1.0.0",
59
+ versions=[
60
+ ("v1", "myapp.routers.v1", None), # (tag, routers_package, public_base_url)
61
+ ],
62
+ root_routers="myapp.routers.root", # includes your root-level routes
63
+ )
64
+
65
+ # That's it! Logging, metrics, and all svc-infra features are auto-configured from env vars.
66
+ ```
67
+
68
+ Then add payments:
69
+
70
+ ```python
71
+ from svc_infra.api.fastapi.apf_payments.setup import add_payments
72
+
73
+ add_payments(app, prefix="/payments")
74
+ ```
75
+
76
+ **Option B: Manual Setup (More Control)**
77
+
78
+ If you need fine-grained control:
79
+
80
+ ```python
81
+ from fastapi import FastAPI
82
+ from svc_infra.api.fastapi.apf_payments.setup import add_payments
83
+
84
+ app = FastAPI()
85
+
86
+ # Auto-registers default providers (Stripe, Aiydan) based on env config
87
+ add_payments(app, prefix="/payments")
88
+ ```
89
+
90
+ **Option C: Full-Featured Service Setup**
91
+
92
+ Use `setup_service_api` for multi-version APIs with custom routers:
93
+
94
+ ```python
95
+ from svc_infra.api.fastapi.setup import setup_service_api
96
+ from svc_infra.api.fastapi.openapi.models import ServiceInfo, APIVersionSpec
97
+ from svc_infra.api.fastapi.apf_payments.setup import add_payments
98
+
99
+ service = ServiceInfo(name="My Payment Service", release="1.0.0")
100
+ versions = [
101
+ APIVersionSpec(tag="v1", routers_package="myapp.routers.v1"),
102
+ ]
103
+
104
+ app = setup_service_api(
105
+ service=service,
106
+ versions=versions,
107
+ root_routers="myapp.routers.root",
108
+ public_cors_origins=["http://localhost:3000"],
109
+ )
110
+
111
+ add_payments(app, prefix="/payments")
112
+ ```
113
+
114
+ **Tenant Context**
115
+
116
+ All payments endpoints require a tenant identifier. The FastAPI router now
117
+ derives it automatically from the authenticated principal:
118
+
119
+ - API key principals → ``principal.api_key.tenant_id``
120
+ - User principals → ``principal.user.tenant_id``
121
+ - Fallbacks: ``X-Tenant-Id`` request header or ``request.state.tenant_id``
122
+
123
+ If you need custom mapping logic (for example, translating API keys to an
124
+ external tenant registry), register an override during startup:
125
+
126
+ ```python
127
+ from svc_infra.api.fastapi.apf_payments.router import set_payments_tenant_resolver
128
+
129
+ async def resolve_tenant(request, identity, header):
130
+ # return a string tenant id, or None to fall back to the defaults
131
+ return "tenant-from-custom-logic"
132
+
133
+ set_payments_tenant_resolver(resolve_tenant)
134
+ ```
135
+
136
+ If no tenant can be derived (and the override also returns ``None``), the
137
+ router responds with ``400 tenant_context_missing`` so callers can supply the
138
+ missing context explicitly.
139
+
140
+ **Environment-Based Configuration**
141
+
142
+ The `easy_service_app` reads these env vars automatically:
143
+ - `ENABLE_LOGGING` (default: true) – Auto-configures JSON logs in prod, plain logs in dev
144
+ - `ENABLE_OBS` (default: true) – Enables Prometheus metrics at `/metrics`
145
+ - `LOG_LEVEL` – DEBUG, INFO, WARNING, etc.
146
+ - `LOG_FORMAT` – json or plain
147
+ - `CORS_ALLOW_ORIGINS` – Comma-separated CORS origins
148
+
149
+ No boilerplate needed! 🎉
150
+
151
+ ---
152
+
153
+ That's it! Your app now has:
154
+ - `POST /payments/customers` – Create/upsert customers
155
+ - `POST /payments/intents` – Create payment intents
156
+ - `POST /payments/methods/attach` – Attach payment methods
157
+ - `GET /payments/methods` – List customer payment methods
158
+ - `POST /payments/subscriptions` – Create subscriptions
159
+ - `POST /payments/invoices` – Create invoices
160
+ - `POST /payments/webhooks/{provider}` – Handle provider webhooks
161
+ - And many more endpoints for refunds, disputes, payouts, etc.
162
+
163
+ ### 3. Complete Example (app.py)
164
+
165
+ Here's a complete, production-ready service in ~20 lines:
166
+
167
+ ```python
168
+ # app.py
169
+ from svc_infra.api.fastapi.ease import easy_service_app
170
+ from svc_infra.api.fastapi.apf_payments.setup import add_payments
171
+
172
+ # Create service with auto-configured logging, metrics, CORS, middleware
173
+ app = easy_service_app(
174
+ name="Payment API",
175
+ release="1.0.0",
176
+ versions=[
177
+ ("v1", "myapp.routers.v1", None),
178
+ ],
179
+ )
180
+
181
+ # Add payments functionality
182
+ add_payments(app, prefix="/payments")
183
+
184
+ # Optional: Add custom routes
185
+ @app.get("/health")
186
+ async def health():
187
+ return {"status": "healthy"}
188
+
189
+ if __name__ == "__main__":
190
+ import uvicorn
191
+ uvicorn.run(app, host="0.0.0.0", port=8000)
192
+ ```
193
+
194
+ **Run it:**
195
+ ```bash
196
+ export STRIPE_SECRET="sk_test_..."
197
+ export STRIPE_WH_SECRET="whsec_..."
198
+ python app.py
199
+ ```
200
+
201
+ **Access:**
202
+ - Swagger docs: `http://localhost:8000/` (landing page with all doc links)
203
+ - Version v1: `http://localhost:8000/v1/docs`
204
+ - Payments: `http://localhost:8000/payments/`
205
+ - Metrics: `http://localhost:8000/metrics`
206
+
207
+ ### Why Use `easy_service_app`?
208
+
209
+ Without `easy_service_app`, you'd need to manually:
210
+ - Configure logging (JSON in prod, plain in dev)
211
+ - Set up CORS middleware
212
+ - Add request ID middleware
213
+ - Configure error handlers (catch-all exceptions)
214
+ - Add idempotency middleware
215
+ - Add rate limiting middleware
216
+ - Set up OpenAPI documentation
217
+ - Configure Prometheus metrics
218
+ - Add OpenTelemetry instrumentation
219
+ - Mount versioned routers
220
+ - Create a landing page for docs
221
+ - Handle environment-based config
222
+
223
+ **That's 50+ lines of boilerplate!** With `easy_service_app`, it's **3 lines**:
224
+
225
+ ```python
226
+ app = easy_service_app(name="My API", release="1.0.0", versions=[("v1", "myapp.routers.v1", None)])
227
+ add_payments(app, prefix="/payments")
228
+ # Done! Production-ready service with all features.
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Core Concepts
234
+
235
+ ### Providers
236
+ Payment providers are registered adapters that implement the `ProviderAdapter` protocol. Built-in:
237
+ - **Stripe** (`stripe`): Full-featured, production-ready.
238
+ - **Aiydan** (`aiydan`): Custom provider with similar capabilities.
239
+
240
+ The active provider is selected via `PAYMENTS_PROVIDER` env var.
241
+
242
+ ### Router Groups by Auth Posture
243
+ - **Public** (`/payments/webhooks/{provider}`): No auth, for webhooks.
244
+ - **User** (`/payments/customers`, `/payments/intents`, `/payments/methods/attach`): Requires user JWT.
245
+ - **Protected** (`/payments/methods`, `/payments/intents/{id}/confirm`, etc.): Requires auth (user or service).
246
+ - **Service** (`/payments/products`, `/payments/prices`): Service-to-service auth (API key).
247
+
248
+ ### Key Entities
249
+ - **Customers**: Link your app users to provider customer records.
250
+ - **Payment Intents**: Represent a payment attempt (amount, currency, status).
251
+ - **Payment Methods**: Saved cards/bank accounts attached to customers.
252
+ - **Subscriptions**: Recurring billing tied to prices and products.
253
+ - **Invoices**: Generated bills (automatic or manual).
254
+ - **Refunds / Disputes / Payouts**: Post-payment flows.
255
+
256
+ ---
257
+
258
+ ## Common Workflows
259
+
260
+ ### Create a Customer and Attach a Payment Method
261
+
262
+ ```python
263
+ import httpx
264
+
265
+ # 1. Create/upsert customer
266
+ resp = await client.post(
267
+ "/payments/customers",
268
+ json={"email": "user@example.com", "name": "Alice"},
269
+ headers={"Idempotency-Key": "customer-alice-1"}
270
+ )
271
+ customer = resp.json()
272
+ customer_id = customer["provider_customer_id"] # e.g., "cus_123"
273
+
274
+ # 2. Attach payment method (token from frontend)
275
+ resp = await client.post(
276
+ "/payments/methods/attach",
277
+ json={
278
+ "customer_provider_id": customer_id,
279
+ "payment_method_token": "pm_test_...", # from Stripe.js or similar
280
+ "make_default": True
281
+ },
282
+ headers={"Idempotency-Key": "attach-pm-1"}
283
+ )
284
+ method = resp.json()
285
+ ```
286
+
287
+ ### Create a Payment Intent
288
+
289
+ ```python
290
+ # Create an intent for $50.00 USD
291
+ resp = await client.post(
292
+ "/payments/intents",
293
+ json={
294
+ "amount": 5000, # minor units (cents)
295
+ "currency": "USD",
296
+ "description": "Order #12345",
297
+ "capture_method": "automatic"
298
+ },
299
+ headers={"Idempotency-Key": "intent-order-12345"}
300
+ )
301
+ intent = resp.json()
302
+ client_secret = intent["client_secret"] # pass to frontend for confirmation
303
+ ```
304
+
305
+ ### List Customer Payment Methods
306
+
307
+ ```python
308
+ resp = await client.get(
309
+ "/payments/methods",
310
+ params={"customer_provider_id": "cus_123"}
311
+ )
312
+ methods = resp.json()["items"]
313
+ ```
314
+
315
+ ### Create a Subscription
316
+
317
+ ```python
318
+ # 1. Create product & price (service-level)
319
+ product_resp = await client.post(
320
+ "/payments/products",
321
+ json={"name": "Pro Plan", "active": True},
322
+ headers={"Idempotency-Key": "product-pro", "Authorization": "Bearer <service-token>"}
323
+ )
324
+ product_id = product_resp.json()["provider_product_id"]
325
+
326
+ price_resp = await client.post(
327
+ "/payments/prices",
328
+ json={
329
+ "provider_product_id": product_id,
330
+ "currency": "USD",
331
+ "unit_amount": 2000,
332
+ "interval": "month",
333
+ "active": True
334
+ },
335
+ headers={"Idempotency-Key": "price-pro-monthly", "Authorization": "Bearer <service-token>"}
336
+ )
337
+ price_id = price_resp.json()["provider_price_id"]
338
+
339
+ # 2. Subscribe customer
340
+ sub_resp = await client.post(
341
+ "/payments/subscriptions",
342
+ json={
343
+ "customer_provider_id": "cus_123",
344
+ "price_provider_id": price_id,
345
+ "quantity": 1
346
+ },
347
+ headers={"Idempotency-Key": "sub-alice-pro"}
348
+ )
349
+ subscription = sub_resp.json()
350
+ ```
351
+
352
+ ### Handle Webhooks
353
+
354
+ ```python
355
+ # Provider (e.g., Stripe) POSTs to /payments/webhooks/stripe
356
+ # The adapter verifies signature and parses event
357
+ # Events trigger internal actions (e.g., mark payment succeeded, post to ledger)
358
+
359
+ # In your app:
360
+ from svc_infra.apf_payments.service import PaymentsService
361
+
362
+ async def on_payment_succeeded(event_data: dict):
363
+ # Custom business logic after payment
364
+ pass
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Advanced Usage
370
+
371
+ ### Fine-Tuned App Configuration
372
+
373
+ Override env-based defaults with `EasyAppOptions`:
374
+
375
+ ```python
376
+ from svc_infra.api.fastapi.ease import easy_service_app, EasyAppOptions, LoggingOptions, ObservabilityOptions
377
+
378
+ app = easy_service_app(
379
+ name="Payment API",
380
+ release="1.0.0",
381
+ versions=[("v1", "myapp.routers.v1", None)],
382
+ options=EasyAppOptions(
383
+ logging=LoggingOptions(
384
+ enable=True,
385
+ level="DEBUG",
386
+ fmt="json"
387
+ ),
388
+ observability=ObservabilityOptions(
389
+ enable=True,
390
+ db_engines=[engine], # pass SQLAlchemy engines for connection pool metrics
391
+ metrics_path="/metrics",
392
+ skip_metric_paths=["/health", "/metrics"]
393
+ )
394
+ )
395
+ )
396
+ ```
397
+
398
+ Or use flags for quick toggles:
399
+
400
+ ```python
401
+ app = easy_service_app(
402
+ name="Payment API",
403
+ release="1.0.0",
404
+ versions=[("v1", "myapp.routers.v1", None)],
405
+ enable_logging=True,
406
+ enable_observability=False # disable metrics
407
+ )
408
+ ```
409
+
410
+ **Configuration Precedence** (strongest → weakest):
411
+ 1. Function arguments (`enable_logging`, `enable_observability`)
412
+ 2. `options=` parameter
413
+ 3. Environment variables (`ENABLE_LOGGING`, `ENABLE_OBS`, etc.)
414
+
415
+ ### Custom Provider Adapter
416
+
417
+ If you need a provider not included by default:
418
+
419
+ ```python
420
+ from svc_infra.apf_payments.provider.base import ProviderAdapter
421
+ from svc_infra.api.fastapi.apf_payments.setup import add_payments
422
+
423
+ class MyCustomAdapter(ProviderAdapter):
424
+ name = "mycustom"
425
+
426
+ async def ensure_customer(self, data):
427
+ # Your implementation
428
+ ...
429
+
430
+ async def create_intent(self, data, *, user_id):
431
+ # Your implementation
432
+ ...
433
+
434
+ # Implement all required methods...
435
+
436
+ # Register it
437
+ add_payments(
438
+ app,
439
+ register_default_providers=False, # skip Stripe/Aiydan auto-registration
440
+ adapters=[MyCustomAdapter()]
441
+ )
442
+ ```
443
+
444
+ ### Multiple Providers in One App
445
+
446
+ ```python
447
+ from svc_infra.apf_payments.provider.stripe import StripeAdapter
448
+ from svc_infra.apf_payments.provider.aiydan import AiydanAdapter
449
+ from svc_infra.apf_payments.provider.registry import get_provider_registry
450
+
451
+ # Register both
452
+ reg = get_provider_registry()
453
+ reg.register(StripeAdapter())
454
+ reg.register(AiydanAdapter())
455
+
456
+ # Select at runtime via settings or per-request logic
457
+ # (Default provider is controlled by PAYMENTS_PROVIDER env)
458
+ ```
459
+
460
+ ### Database Models
461
+
462
+ The payments module creates these tables (via Alembic migrations):
463
+ - `pay_customers`: Maps app users to provider customer IDs
464
+ - `pay_intents`: Stores payment intent records
465
+ - `pay_payment_methods`: Cached payment methods
466
+ - `pay_products`, `pay_prices`: Product/price catalog
467
+ - `pay_subscriptions`: Active subscriptions
468
+ - `pay_invoices`: Invoice records
469
+ - `pay_events`: Webhook event log
470
+ - `ledger_entries`: Financial ledger (debits/credits for payments, refunds, fees, payouts)
471
+
472
+ To generate migrations:
473
+ ```bash
474
+ # From your project root
475
+ svc-infra db revision -m "Add payments tables"
476
+ svc-infra db upgrade head
477
+ ```
478
+
479
+ ### Observability
480
+
481
+ Payments endpoints are instrumented with OpenTelemetry spans. Webhook processing is logged.
482
+
483
+ ```python
484
+ from svc_infra.obs import add_observability
485
+
486
+ shutdown = add_observability(app, db_engines=[engine])
487
+ # Metrics available at /metrics
488
+ ```
489
+
490
+ ### Idempotency
491
+
492
+ All mutating endpoints (`POST`, `PUT`, `DELETE`) require an `Idempotency-Key` header to prevent duplicate operations:
493
+
494
+ ```python
495
+ headers = {"Idempotency-Key": "unique-key-per-request"}
496
+ ```
497
+
498
+ Implemented via `IdempotencyMiddleware` (uses in-memory store by default; plug in Redis for production).
499
+
500
+ ---
501
+
502
+ ## Configuration Reference
503
+
504
+ ### Environment Variables
505
+
506
+ **FastAPI Service Setup** (used by `easy_service_app`):
507
+
508
+ | Variable | Description | Default |
509
+ |----------|-------------|---------|
510
+ | `ENABLE_LOGGING` | Enable automatic logging setup | `true` |
511
+ | `ENABLE_OBS` | Enable observability (metrics/tracing) | `true` |
512
+ | `LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | INFO (prod/test), DEBUG (dev/local) |
513
+ | `LOG_FORMAT` | Log format (`json` or `plain`) | json (prod/test), plain (dev/local) |
514
+ | `LOG_DROP_PATHS` | Comma-separated paths to exclude from logs | `/metrics` (prod/test) |
515
+ | `CORS_ALLOW_ORIGINS` | Comma-separated CORS origins | `http://localhost:3000` |
516
+ | `METRICS_PATH` | Path for Prometheus metrics endpoint | `/metrics` |
517
+ | `OBS_SKIP_PATHS` | Comma/space-separated paths to skip in metrics | `/metrics,/health` |
518
+ | `APP_ENV` | Environment name (prod, test, dev, local) | Auto-detected from `RAILWAY_ENVIRONMENT_NAME` or defaults to `local` |
519
+
520
+ **Payment Provider Configuration**:
521
+
522
+ | Variable | Description | Default |
523
+ |----------|-------------|---------|
524
+ | `PAYMENTS_PROVIDER` | Active provider name (`stripe`, `aiydan`, etc.) | `stripe` |
525
+ | `STRIPE_SECRET` or `STRIPE_API_KEY` | Stripe secret key | - |
526
+ | `STRIPE_WH_SECRET` | Stripe webhook signing secret | - |
527
+ | `AIYDAN_API_KEY` | Aiydan API key | - |
528
+ | `AIYDAN_CLIENT_KEY` | Aiydan client key (optional) | - |
529
+ | `AIYDAN_MERCHANT_ACCOUNT` | Aiydan merchant account (optional) | - |
530
+ | `AIYDAN_HMAC_KEY` | Aiydan HMAC key for signatures (optional) | - |
531
+ | `AIYDAN_BASE_URL` | Aiydan API base URL (optional) | - |
532
+ | `AIYDAN_WH_SECRET` | Aiydan webhook secret (optional) | - |
533
+
534
+ ### Settings Object
535
+
536
+ ```python
537
+ from svc_infra.apf_payments.settings import get_payments_settings
538
+
539
+ settings = get_payments_settings()
540
+ print(settings.default_provider) # "stripe"
541
+ print(settings.stripe.secret_key.get_secret_value()) # "sk_test_..."
542
+ ```
543
+
544
+ ---
545
+
546
+ ## API Endpoints Summary
547
+
548
+ ### Customers
549
+ - `POST /payments/customers` – Create/upsert customer (user auth)
550
+
551
+ ### Payment Intents
552
+ - `POST /payments/intents` – Create intent (user auth)
553
+ - `POST /payments/intents/{id}/confirm` – Confirm intent (protected)
554
+ - `POST /payments/intents/{id}/cancel` – Cancel intent (protected)
555
+ - `POST /payments/intents/{id}/refund` – Refund intent (protected)
556
+ - `POST /payments/intents/{id}/capture` – Capture manual intent (protected)
557
+ - `GET /payments/intents/{id}` – Retrieve intent (protected)
558
+ - `GET /payments/intents` – List intents (protected)
559
+
560
+ ### Payment Methods
561
+ - `POST /payments/methods/attach` – Attach method to customer (user auth)
562
+ - `GET /payments/methods` – List customer methods (protected, requires `customer_provider_id` query param)
563
+ - `POST /payments/methods/{id}/detach` – Detach method (protected)
564
+ - `POST /payments/methods/{id}/default` – Set as default (protected, requires `customer_provider_id` query param)
565
+ - `GET /payments/methods/{id}` – Retrieve method (protected)
566
+ - `PUT /payments/methods/{id}` – Update method (protected)
567
+
568
+ ### Products & Prices (Service Auth)
569
+ - `POST /payments/products` – Create product
570
+ - `GET /payments/products` – List products
571
+ - `GET /payments/products/{id}` – Retrieve product
572
+ - `PUT /payments/products/{id}` – Update product
573
+ - `POST /payments/prices` – Create price
574
+ - `GET /payments/prices` – List prices
575
+ - `GET /payments/prices/{id}` – Retrieve price
576
+ - `PUT /payments/prices/{id}` – Update price
577
+
578
+ ### Subscriptions
579
+ - `POST /payments/subscriptions` – Create subscription (protected)
580
+ - `POST /payments/subscriptions/{id}` – Update subscription (protected)
581
+ - `POST /payments/subscriptions/{id}/cancel` – Cancel subscription (protected)
582
+ - `GET /payments/subscriptions/{id}` – Retrieve subscription (protected)
583
+ - `GET /payments/subscriptions` – List subscriptions (protected)
584
+
585
+ ### Invoices
586
+ - `POST /payments/invoices` – Create invoice (protected)
587
+ - `POST /payments/invoices/{id}/finalize` – Finalize invoice (protected)
588
+ - `POST /payments/invoices/{id}/void` – Void invoice (protected)
589
+ - `POST /payments/invoices/{id}/pay` – Pay invoice (protected)
590
+ - `POST /payments/invoices/{id}/line-items` – Add line item (protected)
591
+ - `GET /payments/invoices/{id}` – Retrieve invoice (protected)
592
+ - `GET /payments/invoices` – List invoices (protected)
593
+ - `GET /payments/invoices/{id}/line-items` – List line items (protected)
594
+ - `GET /payments/invoices/preview` – Preview upcoming invoice (protected)
595
+
596
+ ### Disputes
597
+ - `GET /payments/disputes` – List disputes (protected)
598
+ - `GET /payments/disputes/{id}` – Retrieve dispute (protected)
599
+ - `POST /payments/disputes/{id}/evidence` – Submit evidence (protected)
600
+
601
+ ### Balance & Payouts
602
+ - `GET /payments/balance` – Get balance snapshot (protected)
603
+ - `GET /payments/payouts` – List payouts (protected)
604
+ - `GET /payments/payouts/{id}` – Retrieve payout (protected)
605
+
606
+ ### Refunds
607
+ - `GET /payments/refunds` – List refunds (protected)
608
+ - `GET /payments/refunds/{id}` – Retrieve refund (protected)
609
+
610
+ ### Transactions & Statements
611
+ - `GET /payments/transactions` – List all ledger transactions (protected)
612
+ - `GET /payments/statements/daily` – Daily rollup statements (protected)
613
+
614
+ ### Usage Records (Metered Billing)
615
+ - `POST /payments/usage-records` – Create usage record (protected)
616
+ - `GET /payments/usage-records` – List usage records (protected)
617
+ - `GET /payments/usage-records/{id}` – Retrieve usage record (protected)
618
+
619
+ ### Setup Intents (Off-Session)
620
+ - `POST /payments/setup-intents` – Create setup intent (user auth)
621
+ - `POST /payments/setup-intents/{id}/confirm` – Confirm setup intent (protected)
622
+ - `GET /payments/setup-intents/{id}` – Retrieve setup intent (protected)
623
+
624
+ ### Webhooks
625
+ - `POST /payments/webhooks/{provider}` – Receive provider webhooks (public, no auth)
626
+
627
+ ### Webhook Replay (Testing)
628
+ - `POST /payments/webhooks/{provider}/replay` – Replay webhook event (protected)
629
+
630
+ ---
631
+
632
+ ## Testing
633
+
634
+ ### Unit Tests
635
+
636
+ ```bash
637
+ pytest tests/payments/
638
+ ```
639
+
640
+ ### Integration Tests
641
+
642
+ Mock the provider adapter:
643
+
644
+ ```python
645
+ from svc_infra.apf_payments.provider.base import ProviderAdapter
646
+
647
+ class FakeAdapter(ProviderAdapter):
648
+ name = "fake"
649
+
650
+ async def ensure_customer(self, data):
651
+ return CustomerOut(
652
+ id="cus_fake",
653
+ provider="fake",
654
+ provider_customer_id="cus_fake",
655
+ email=data.email
656
+ )
657
+ # ... implement other methods
658
+
659
+ add_payments(app, register_default_providers=False, adapters=[FakeAdapter()])
660
+ ```
661
+
662
+ ### Local Webhook Testing
663
+
664
+ Use Stripe CLI or similar:
665
+
666
+ ```bash
667
+ stripe listen --forward-to http://localhost:8000/payments/webhooks/stripe
668
+ ```
669
+
670
+ ---
671
+
672
+ ## Troubleshooting
673
+
674
+ ### "No payments adapter registered for 'xyz'"
675
+ - Ensure you've set the correct `PAYMENTS_PROVIDER` env var.
676
+ - Verify the provider SDK is installed (`pip install stripe` or `pip install aiydan`).
677
+ - Check that the provider credentials are configured.
678
+
679
+ ### "Idempotency-Key required"
680
+ - All mutating operations need an `Idempotency-Key` header.
681
+ - Use a unique key per logical operation (e.g., `f"order-{order_id}-payment"`).
682
+
683
+ ### Webhook signature verification fails
684
+ - Ensure `STRIPE_WH_SECRET` (or equivalent) is set correctly.
685
+ - Check that the webhook endpoint URL in your provider dashboard matches your deployment.
686
+
687
+ ### Database migrations not applied
688
+ ```bash
689
+ svc-infra db upgrade head
690
+ ```
691
+
692
+ ---
693
+
694
+ ## Security Best Practices
695
+
696
+ 1. **Never log or expose secret keys** – Use `SecretStr` in settings.
697
+ 2. **Always verify webhook signatures** – Prevents spoofed events.
698
+ 3. **Use HTTPS in production** – Payment data is sensitive.
699
+ 4. **Implement rate limiting** – Protect webhook endpoints from abuse.
700
+ 5. **Store minimal PII** – Only cache what's needed; rely on provider for full records.
701
+ 6. **Rotate API keys periodically** – Follow provider recommendations.
702
+
703
+ ---
704
+
705
+ ## Performance Tips
706
+
707
+ - **Cache payment methods locally** – Reduce API calls to provider.
708
+ - **Use cursor-based pagination** – More efficient for large result sets.
709
+ - **Process webhooks asynchronously** – Respond quickly (< 5s) to avoid retries.
710
+ - **Monitor provider API usage** – Stay within rate limits.
711
+
712
+ ---
713
+
714
+ ## Roadmap
715
+
716
+ - [ ] Support for additional providers (PayPal, Square, etc.)
717
+ - [ ] Enhanced ledger reconciliation tools
718
+ - [ ] Multi-currency support improvements
719
+ - [ ] Automatic retry logic for failed provider calls
720
+ - [ ] Admin dashboard for payment operations
721
+
722
+ ---
723
+
724
+ ## Support
725
+
726
+ - GitHub Issues: [svc-infra/issues](https://github.com/your-org/svc-infra/issues)
727
+ - Documentation: [Full docs](https://github.com/your-org/svc-infra#readme)
728
+ - Provider docs: [Stripe](https://stripe.com/docs/api), [Aiydan](#) (when available)
729
+
730
+ ---
731
+
732
+ **Happy payments building!** 🎉