svc-infra 0.1.592__py3-none-any.whl → 0.1.593__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.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

@@ -0,0 +1,706 @@
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
+ **Environment-Based Configuration**
115
+
116
+ The `easy_service_app` reads these env vars automatically:
117
+ - `ENABLE_LOGGING` (default: true) – Auto-configures JSON logs in prod, plain logs in dev
118
+ - `ENABLE_OBS` (default: true) – Enables Prometheus metrics at `/metrics`
119
+ - `LOG_LEVEL` – DEBUG, INFO, WARNING, etc.
120
+ - `LOG_FORMAT` – json or plain
121
+ - `CORS_ALLOW_ORIGINS` – Comma-separated CORS origins
122
+
123
+ No boilerplate needed! 🎉
124
+
125
+ ---
126
+
127
+ That's it! Your app now has:
128
+ - `POST /payments/customers` – Create/upsert customers
129
+ - `POST /payments/intents` – Create payment intents
130
+ - `POST /payments/methods/attach` – Attach payment methods
131
+ - `GET /payments/methods` – List customer payment methods
132
+ - `POST /payments/subscriptions` – Create subscriptions
133
+ - `POST /payments/invoices` – Create invoices
134
+ - `POST /payments/webhooks/{provider}` – Handle provider webhooks
135
+ - And many more endpoints for refunds, disputes, payouts, etc.
136
+
137
+ ### 3. Complete Example (app.py)
138
+
139
+ Here's a complete, production-ready service in ~20 lines:
140
+
141
+ ```python
142
+ # app.py
143
+ from svc_infra.api.fastapi.ease import easy_service_app
144
+ from svc_infra.api.fastapi.apf_payments.setup import add_payments
145
+
146
+ # Create service with auto-configured logging, metrics, CORS, middleware
147
+ app = easy_service_app(
148
+ name="Payment API",
149
+ release="1.0.0",
150
+ versions=[
151
+ ("v1", "myapp.routers.v1", None),
152
+ ],
153
+ )
154
+
155
+ # Add payments functionality
156
+ add_payments(app, prefix="/payments")
157
+
158
+ # Optional: Add custom routes
159
+ @app.get("/health")
160
+ async def health():
161
+ return {"status": "healthy"}
162
+
163
+ if __name__ == "__main__":
164
+ import uvicorn
165
+ uvicorn.run(app, host="0.0.0.0", port=8000)
166
+ ```
167
+
168
+ **Run it:**
169
+ ```bash
170
+ export STRIPE_SECRET="sk_test_..."
171
+ export STRIPE_WH_SECRET="whsec_..."
172
+ python app.py
173
+ ```
174
+
175
+ **Access:**
176
+ - Swagger docs: `http://localhost:8000/` (landing page with all doc links)
177
+ - Version v1: `http://localhost:8000/v1/docs`
178
+ - Payments: `http://localhost:8000/payments/`
179
+ - Metrics: `http://localhost:8000/metrics`
180
+
181
+ ### Why Use `easy_service_app`?
182
+
183
+ Without `easy_service_app`, you'd need to manually:
184
+ - Configure logging (JSON in prod, plain in dev)
185
+ - Set up CORS middleware
186
+ - Add request ID middleware
187
+ - Configure error handlers (catch-all exceptions)
188
+ - Add idempotency middleware
189
+ - Add rate limiting middleware
190
+ - Set up OpenAPI documentation
191
+ - Configure Prometheus metrics
192
+ - Add OpenTelemetry instrumentation
193
+ - Mount versioned routers
194
+ - Create a landing page for docs
195
+ - Handle environment-based config
196
+
197
+ **That's 50+ lines of boilerplate!** With `easy_service_app`, it's **3 lines**:
198
+
199
+ ```python
200
+ app = easy_service_app(name="My API", release="1.0.0", versions=[("v1", "myapp.routers.v1", None)])
201
+ add_payments(app, prefix="/payments")
202
+ # Done! Production-ready service with all features.
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Core Concepts
208
+
209
+ ### Providers
210
+ Payment providers are registered adapters that implement the `ProviderAdapter` protocol. Built-in:
211
+ - **Stripe** (`stripe`): Full-featured, production-ready.
212
+ - **Aiydan** (`aiydan`): Custom provider with similar capabilities.
213
+
214
+ The active provider is selected via `PAYMENTS_PROVIDER` env var.
215
+
216
+ ### Router Groups by Auth Posture
217
+ - **Public** (`/payments/webhooks/{provider}`): No auth, for webhooks.
218
+ - **User** (`/payments/customers`, `/payments/intents`, `/payments/methods/attach`): Requires user JWT.
219
+ - **Protected** (`/payments/methods`, `/payments/intents/{id}/confirm`, etc.): Requires auth (user or service).
220
+ - **Service** (`/payments/products`, `/payments/prices`): Service-to-service auth (API key).
221
+
222
+ ### Key Entities
223
+ - **Customers**: Link your app users to provider customer records.
224
+ - **Payment Intents**: Represent a payment attempt (amount, currency, status).
225
+ - **Payment Methods**: Saved cards/bank accounts attached to customers.
226
+ - **Subscriptions**: Recurring billing tied to prices and products.
227
+ - **Invoices**: Generated bills (automatic or manual).
228
+ - **Refunds / Disputes / Payouts**: Post-payment flows.
229
+
230
+ ---
231
+
232
+ ## Common Workflows
233
+
234
+ ### Create a Customer and Attach a Payment Method
235
+
236
+ ```python
237
+ import httpx
238
+
239
+ # 1. Create/upsert customer
240
+ resp = await client.post(
241
+ "/payments/customers",
242
+ json={"email": "user@example.com", "name": "Alice"},
243
+ headers={"Idempotency-Key": "customer-alice-1"}
244
+ )
245
+ customer = resp.json()
246
+ customer_id = customer["provider_customer_id"] # e.g., "cus_123"
247
+
248
+ # 2. Attach payment method (token from frontend)
249
+ resp = await client.post(
250
+ "/payments/methods/attach",
251
+ json={
252
+ "customer_provider_id": customer_id,
253
+ "payment_method_token": "pm_test_...", # from Stripe.js or similar
254
+ "make_default": True
255
+ },
256
+ headers={"Idempotency-Key": "attach-pm-1"}
257
+ )
258
+ method = resp.json()
259
+ ```
260
+
261
+ ### Create a Payment Intent
262
+
263
+ ```python
264
+ # Create an intent for $50.00 USD
265
+ resp = await client.post(
266
+ "/payments/intents",
267
+ json={
268
+ "amount": 5000, # minor units (cents)
269
+ "currency": "USD",
270
+ "description": "Order #12345",
271
+ "capture_method": "automatic"
272
+ },
273
+ headers={"Idempotency-Key": "intent-order-12345"}
274
+ )
275
+ intent = resp.json()
276
+ client_secret = intent["client_secret"] # pass to frontend for confirmation
277
+ ```
278
+
279
+ ### List Customer Payment Methods
280
+
281
+ ```python
282
+ resp = await client.get(
283
+ "/payments/methods",
284
+ params={"customer_provider_id": "cus_123"}
285
+ )
286
+ methods = resp.json()["items"]
287
+ ```
288
+
289
+ ### Create a Subscription
290
+
291
+ ```python
292
+ # 1. Create product & price (service-level)
293
+ product_resp = await client.post(
294
+ "/payments/products",
295
+ json={"name": "Pro Plan", "active": True},
296
+ headers={"Idempotency-Key": "product-pro", "Authorization": "Bearer <service-token>"}
297
+ )
298
+ product_id = product_resp.json()["provider_product_id"]
299
+
300
+ price_resp = await client.post(
301
+ "/payments/prices",
302
+ json={
303
+ "provider_product_id": product_id,
304
+ "currency": "USD",
305
+ "unit_amount": 2000,
306
+ "interval": "month",
307
+ "active": True
308
+ },
309
+ headers={"Idempotency-Key": "price-pro-monthly", "Authorization": "Bearer <service-token>"}
310
+ )
311
+ price_id = price_resp.json()["provider_price_id"]
312
+
313
+ # 2. Subscribe customer
314
+ sub_resp = await client.post(
315
+ "/payments/subscriptions",
316
+ json={
317
+ "customer_provider_id": "cus_123",
318
+ "price_provider_id": price_id,
319
+ "quantity": 1
320
+ },
321
+ headers={"Idempotency-Key": "sub-alice-pro"}
322
+ )
323
+ subscription = sub_resp.json()
324
+ ```
325
+
326
+ ### Handle Webhooks
327
+
328
+ ```python
329
+ # Provider (e.g., Stripe) POSTs to /payments/webhooks/stripe
330
+ # The adapter verifies signature and parses event
331
+ # Events trigger internal actions (e.g., mark payment succeeded, post to ledger)
332
+
333
+ # In your app:
334
+ from svc_infra.apf_payments.service import PaymentsService
335
+
336
+ async def on_payment_succeeded(event_data: dict):
337
+ # Custom business logic after payment
338
+ pass
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Advanced Usage
344
+
345
+ ### Fine-Tuned App Configuration
346
+
347
+ Override env-based defaults with `EasyAppOptions`:
348
+
349
+ ```python
350
+ from svc_infra.api.fastapi.ease import easy_service_app, EasyAppOptions, LoggingOptions, ObservabilityOptions
351
+
352
+ app = easy_service_app(
353
+ name="Payment API",
354
+ release="1.0.0",
355
+ versions=[("v1", "myapp.routers.v1", None)],
356
+ options=EasyAppOptions(
357
+ logging=LoggingOptions(
358
+ enable=True,
359
+ level="DEBUG",
360
+ fmt="json"
361
+ ),
362
+ observability=ObservabilityOptions(
363
+ enable=True,
364
+ db_engines=[engine], # pass SQLAlchemy engines for connection pool metrics
365
+ metrics_path="/metrics",
366
+ skip_metric_paths=["/health", "/metrics"]
367
+ )
368
+ )
369
+ )
370
+ ```
371
+
372
+ Or use flags for quick toggles:
373
+
374
+ ```python
375
+ app = easy_service_app(
376
+ name="Payment API",
377
+ release="1.0.0",
378
+ versions=[("v1", "myapp.routers.v1", None)],
379
+ enable_logging=True,
380
+ enable_observability=False # disable metrics
381
+ )
382
+ ```
383
+
384
+ **Configuration Precedence** (strongest → weakest):
385
+ 1. Function arguments (`enable_logging`, `enable_observability`)
386
+ 2. `options=` parameter
387
+ 3. Environment variables (`ENABLE_LOGGING`, `ENABLE_OBS`, etc.)
388
+
389
+ ### Custom Provider Adapter
390
+
391
+ If you need a provider not included by default:
392
+
393
+ ```python
394
+ from svc_infra.apf_payments.provider.base import ProviderAdapter
395
+ from svc_infra.api.fastapi.apf_payments.setup import add_payments
396
+
397
+ class MyCustomAdapter(ProviderAdapter):
398
+ name = "mycustom"
399
+
400
+ async def ensure_customer(self, data):
401
+ # Your implementation
402
+ ...
403
+
404
+ async def create_intent(self, data, *, user_id):
405
+ # Your implementation
406
+ ...
407
+
408
+ # Implement all required methods...
409
+
410
+ # Register it
411
+ add_payments(
412
+ app,
413
+ register_default_providers=False, # skip Stripe/Aiydan auto-registration
414
+ adapters=[MyCustomAdapter()]
415
+ )
416
+ ```
417
+
418
+ ### Multiple Providers in One App
419
+
420
+ ```python
421
+ from svc_infra.apf_payments.provider.stripe import StripeAdapter
422
+ from svc_infra.apf_payments.provider.aiydan import AiydanAdapter
423
+ from svc_infra.apf_payments.provider.registry import get_provider_registry
424
+
425
+ # Register both
426
+ reg = get_provider_registry()
427
+ reg.register(StripeAdapter())
428
+ reg.register(AiydanAdapter())
429
+
430
+ # Select at runtime via settings or per-request logic
431
+ # (Default provider is controlled by PAYMENTS_PROVIDER env)
432
+ ```
433
+
434
+ ### Database Models
435
+
436
+ The payments module creates these tables (via Alembic migrations):
437
+ - `pay_customers`: Maps app users to provider customer IDs
438
+ - `pay_intents`: Stores payment intent records
439
+ - `pay_payment_methods`: Cached payment methods
440
+ - `pay_products`, `pay_prices`: Product/price catalog
441
+ - `pay_subscriptions`: Active subscriptions
442
+ - `pay_invoices`: Invoice records
443
+ - `pay_events`: Webhook event log
444
+ - `ledger_entries`: Financial ledger (debits/credits for payments, refunds, fees, payouts)
445
+
446
+ To generate migrations:
447
+ ```bash
448
+ # From your project root
449
+ svc-infra db revision -m "Add payments tables"
450
+ svc-infra db upgrade head
451
+ ```
452
+
453
+ ### Observability
454
+
455
+ Payments endpoints are instrumented with OpenTelemetry spans. Webhook processing is logged.
456
+
457
+ ```python
458
+ from svc_infra.obs import add_observability
459
+
460
+ shutdown = add_observability(app, db_engines=[engine])
461
+ # Metrics available at /metrics
462
+ ```
463
+
464
+ ### Idempotency
465
+
466
+ All mutating endpoints (`POST`, `PUT`, `DELETE`) require an `Idempotency-Key` header to prevent duplicate operations:
467
+
468
+ ```python
469
+ headers = {"Idempotency-Key": "unique-key-per-request"}
470
+ ```
471
+
472
+ Implemented via `IdempotencyMiddleware` (uses in-memory store by default; plug in Redis for production).
473
+
474
+ ---
475
+
476
+ ## Configuration Reference
477
+
478
+ ### Environment Variables
479
+
480
+ **FastAPI Service Setup** (used by `easy_service_app`):
481
+
482
+ | Variable | Description | Default |
483
+ |----------|-------------|---------|
484
+ | `ENABLE_LOGGING` | Enable automatic logging setup | `true` |
485
+ | `ENABLE_OBS` | Enable observability (metrics/tracing) | `true` |
486
+ | `LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | INFO (prod/test), DEBUG (dev/local) |
487
+ | `LOG_FORMAT` | Log format (`json` or `plain`) | json (prod/test), plain (dev/local) |
488
+ | `LOG_DROP_PATHS` | Comma-separated paths to exclude from logs | `/metrics` (prod/test) |
489
+ | `CORS_ALLOW_ORIGINS` | Comma-separated CORS origins | `http://localhost:3000` |
490
+ | `METRICS_PATH` | Path for Prometheus metrics endpoint | `/metrics` |
491
+ | `OBS_SKIP_PATHS` | Comma/space-separated paths to skip in metrics | `/metrics,/health` |
492
+ | `APP_ENV` | Environment name (prod, test, dev, local) | Auto-detected from `RAILWAY_ENVIRONMENT_NAME` or defaults to `local` |
493
+
494
+ **Payment Provider Configuration**:
495
+
496
+ | Variable | Description | Default |
497
+ |----------|-------------|---------|
498
+ | `PAYMENTS_PROVIDER` | Active provider name (`stripe`, `aiydan`, etc.) | `stripe` |
499
+ | `STRIPE_SECRET` or `STRIPE_API_KEY` | Stripe secret key | - |
500
+ | `STRIPE_WH_SECRET` | Stripe webhook signing secret | - |
501
+ | `AIYDAN_API_KEY` | Aiydan API key | - |
502
+ | `AIYDAN_CLIENT_KEY` | Aiydan client key (optional) | - |
503
+ | `AIYDAN_MERCHANT_ACCOUNT` | Aiydan merchant account (optional) | - |
504
+ | `AIYDAN_HMAC_KEY` | Aiydan HMAC key for signatures (optional) | - |
505
+ | `AIYDAN_BASE_URL` | Aiydan API base URL (optional) | - |
506
+ | `AIYDAN_WH_SECRET` | Aiydan webhook secret (optional) | - |
507
+
508
+ ### Settings Object
509
+
510
+ ```python
511
+ from svc_infra.apf_payments.settings import get_payments_settings
512
+
513
+ settings = get_payments_settings()
514
+ print(settings.default_provider) # "stripe"
515
+ print(settings.stripe.secret_key.get_secret_value()) # "sk_test_..."
516
+ ```
517
+
518
+ ---
519
+
520
+ ## API Endpoints Summary
521
+
522
+ ### Customers
523
+ - `POST /payments/customers` – Create/upsert customer (user auth)
524
+
525
+ ### Payment Intents
526
+ - `POST /payments/intents` – Create intent (user auth)
527
+ - `POST /payments/intents/{id}/confirm` – Confirm intent (protected)
528
+ - `POST /payments/intents/{id}/cancel` – Cancel intent (protected)
529
+ - `POST /payments/intents/{id}/refund` – Refund intent (protected)
530
+ - `POST /payments/intents/{id}/capture` – Capture manual intent (protected)
531
+ - `GET /payments/intents/{id}` – Retrieve intent (protected)
532
+ - `GET /payments/intents` – List intents (protected)
533
+
534
+ ### Payment Methods
535
+ - `POST /payments/methods/attach` – Attach method to customer (user auth)
536
+ - `GET /payments/methods` – List customer methods (protected, requires `customer_provider_id` query param)
537
+ - `POST /payments/methods/{id}/detach` – Detach method (protected)
538
+ - `POST /payments/methods/{id}/default` – Set as default (protected, requires `customer_provider_id` query param)
539
+ - `GET /payments/methods/{id}` – Retrieve method (protected)
540
+ - `PUT /payments/methods/{id}` – Update method (protected)
541
+
542
+ ### Products & Prices (Service Auth)
543
+ - `POST /payments/products` – Create product
544
+ - `GET /payments/products` – List products
545
+ - `GET /payments/products/{id}` – Retrieve product
546
+ - `PUT /payments/products/{id}` – Update product
547
+ - `POST /payments/prices` – Create price
548
+ - `GET /payments/prices` – List prices
549
+ - `GET /payments/prices/{id}` – Retrieve price
550
+ - `PUT /payments/prices/{id}` – Update price
551
+
552
+ ### Subscriptions
553
+ - `POST /payments/subscriptions` – Create subscription (protected)
554
+ - `POST /payments/subscriptions/{id}` – Update subscription (protected)
555
+ - `POST /payments/subscriptions/{id}/cancel` – Cancel subscription (protected)
556
+ - `GET /payments/subscriptions/{id}` – Retrieve subscription (protected)
557
+ - `GET /payments/subscriptions` – List subscriptions (protected)
558
+
559
+ ### Invoices
560
+ - `POST /payments/invoices` – Create invoice (protected)
561
+ - `POST /payments/invoices/{id}/finalize` – Finalize invoice (protected)
562
+ - `POST /payments/invoices/{id}/void` – Void invoice (protected)
563
+ - `POST /payments/invoices/{id}/pay` – Pay invoice (protected)
564
+ - `POST /payments/invoices/{id}/line-items` – Add line item (protected)
565
+ - `GET /payments/invoices/{id}` – Retrieve invoice (protected)
566
+ - `GET /payments/invoices` – List invoices (protected)
567
+ - `GET /payments/invoices/{id}/line-items` – List line items (protected)
568
+ - `GET /payments/invoices/preview` – Preview upcoming invoice (protected)
569
+
570
+ ### Disputes
571
+ - `GET /payments/disputes` – List disputes (protected)
572
+ - `GET /payments/disputes/{id}` – Retrieve dispute (protected)
573
+ - `POST /payments/disputes/{id}/evidence` – Submit evidence (protected)
574
+
575
+ ### Balance & Payouts
576
+ - `GET /payments/balance` – Get balance snapshot (protected)
577
+ - `GET /payments/payouts` – List payouts (protected)
578
+ - `GET /payments/payouts/{id}` – Retrieve payout (protected)
579
+
580
+ ### Refunds
581
+ - `GET /payments/refunds` – List refunds (protected)
582
+ - `GET /payments/refunds/{id}` – Retrieve refund (protected)
583
+
584
+ ### Transactions & Statements
585
+ - `GET /payments/transactions` – List all ledger transactions (protected)
586
+ - `GET /payments/statements/daily` – Daily rollup statements (protected)
587
+
588
+ ### Usage Records (Metered Billing)
589
+ - `POST /payments/usage-records` – Create usage record (protected)
590
+ - `GET /payments/usage-records` – List usage records (protected)
591
+ - `GET /payments/usage-records/{id}` – Retrieve usage record (protected)
592
+
593
+ ### Setup Intents (Off-Session)
594
+ - `POST /payments/setup-intents` – Create setup intent (user auth)
595
+ - `POST /payments/setup-intents/{id}/confirm` – Confirm setup intent (protected)
596
+ - `GET /payments/setup-intents/{id}` – Retrieve setup intent (protected)
597
+
598
+ ### Webhooks
599
+ - `POST /payments/webhooks/{provider}` – Receive provider webhooks (public, no auth)
600
+
601
+ ### Webhook Replay (Testing)
602
+ - `POST /payments/webhooks/{provider}/replay` – Replay webhook event (protected)
603
+
604
+ ---
605
+
606
+ ## Testing
607
+
608
+ ### Unit Tests
609
+
610
+ ```bash
611
+ pytest tests/payments/
612
+ ```
613
+
614
+ ### Integration Tests
615
+
616
+ Mock the provider adapter:
617
+
618
+ ```python
619
+ from svc_infra.apf_payments.provider.base import ProviderAdapter
620
+
621
+ class FakeAdapter(ProviderAdapter):
622
+ name = "fake"
623
+
624
+ async def ensure_customer(self, data):
625
+ return CustomerOut(
626
+ id="cus_fake",
627
+ provider="fake",
628
+ provider_customer_id="cus_fake",
629
+ email=data.email
630
+ )
631
+ # ... implement other methods
632
+
633
+ add_payments(app, register_default_providers=False, adapters=[FakeAdapter()])
634
+ ```
635
+
636
+ ### Local Webhook Testing
637
+
638
+ Use Stripe CLI or similar:
639
+
640
+ ```bash
641
+ stripe listen --forward-to http://localhost:8000/payments/webhooks/stripe
642
+ ```
643
+
644
+ ---
645
+
646
+ ## Troubleshooting
647
+
648
+ ### "No payments adapter registered for 'xyz'"
649
+ - Ensure you've set the correct `PAYMENTS_PROVIDER` env var.
650
+ - Verify the provider SDK is installed (`pip install stripe` or `pip install aiydan`).
651
+ - Check that the provider credentials are configured.
652
+
653
+ ### "Idempotency-Key required"
654
+ - All mutating operations need an `Idempotency-Key` header.
655
+ - Use a unique key per logical operation (e.g., `f"order-{order_id}-payment"`).
656
+
657
+ ### Webhook signature verification fails
658
+ - Ensure `STRIPE_WH_SECRET` (or equivalent) is set correctly.
659
+ - Check that the webhook endpoint URL in your provider dashboard matches your deployment.
660
+
661
+ ### Database migrations not applied
662
+ ```bash
663
+ svc-infra db upgrade head
664
+ ```
665
+
666
+ ---
667
+
668
+ ## Security Best Practices
669
+
670
+ 1. **Never log or expose secret keys** – Use `SecretStr` in settings.
671
+ 2. **Always verify webhook signatures** – Prevents spoofed events.
672
+ 3. **Use HTTPS in production** – Payment data is sensitive.
673
+ 4. **Implement rate limiting** – Protect webhook endpoints from abuse.
674
+ 5. **Store minimal PII** – Only cache what's needed; rely on provider for full records.
675
+ 6. **Rotate API keys periodically** – Follow provider recommendations.
676
+
677
+ ---
678
+
679
+ ## Performance Tips
680
+
681
+ - **Cache payment methods locally** – Reduce API calls to provider.
682
+ - **Use cursor-based pagination** – More efficient for large result sets.
683
+ - **Process webhooks asynchronously** – Respond quickly (< 5s) to avoid retries.
684
+ - **Monitor provider API usage** – Stay within rate limits.
685
+
686
+ ---
687
+
688
+ ## Roadmap
689
+
690
+ - [ ] Support for additional providers (PayPal, Square, etc.)
691
+ - [ ] Enhanced ledger reconciliation tools
692
+ - [ ] Multi-currency support improvements
693
+ - [ ] Automatic retry logic for failed provider calls
694
+ - [ ] Admin dashboard for payment operations
695
+
696
+ ---
697
+
698
+ ## Support
699
+
700
+ - GitHub Issues: [svc-infra/issues](https://github.com/your-org/svc-infra/issues)
701
+ - Documentation: [Full docs](https://github.com/your-org/svc-infra#readme)
702
+ - Provider docs: [Stripe](https://stripe.com/docs/api), [Aiydan](#) (when available)
703
+
704
+ ---
705
+
706
+ **Happy payments building!** 🎉
@@ -0,0 +1,4 @@
1
+ from .aiydan import AiydanAdapter # noqa: F401
2
+ from .stripe import StripeAdapter # noqa: F401
3
+
4
+ __all__ = ["AiydanAdapter", "StripeAdapter"]