Commit 4672c840 authored by Your Name's avatar Your Name

docs: add Phase 2 implementation plan (Fiat Payments)

parent 842541d3
# Payment System Phase 2: Fiat Payments Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement Stripe and PayPal payment integrations with authorization holds for verification, payment method management, and webhook handlers.
**Architecture:** Async Stripe/PayPal SDK integration, authorization holds (not charges) for verification, webhook signature verification, database-backed payment method storage.
**Tech Stack:** Stripe Python SDK, PayPal REST API, httpx (async), FastAPI webhooks
**Prerequisites:** Phase 1 completed (database schema, payment service foundation)
---
## Phase 2 Deliverables
- ✅ Stripe payment method integration
- ✅ PayPal billing agreement integration
- ✅ Authorization hold verification (not charges)
- ✅ Payment method management (list, delete)
- ✅ Webhook handlers for both gateways
- ✅ API endpoints for payment methods
- ✅ Unit tests for fiat handlers
---
## Task 1: Stripe Payment Handler
**Files:**
- Create: `aisbf/payments/fiat/__init__.py`
- Create: `aisbf/payments/fiat/stripe_handler.py`
- Create: `tests/payments/test_stripe.py`
- [ ] **Step 1: Write failing test for Stripe payment method**
Create `tests/payments/test_stripe.py`:
```python
import pytest
from aisbf.database import DatabaseManager
from aisbf.payments.migrations import PaymentMigrations
from aisbf.payments.fiat.stripe_handler import StripePaymentHandler
@pytest.fixture
def db_manager(tmp_path):
"""Create test database"""
db_path = tmp_path / "test.db"
db_config = {
'type': 'sqlite',
'sqlite_path': str(db_path)
}
db = DatabaseManager(db_config)
migrations = PaymentMigrations(db)
migrations.run_migrations()
# Add test user
db.execute("""
INSERT INTO users (id, email, username, password_hash)
VALUES (1, 'test@example.com', 'testuser', 'hash')
""")
return db
@pytest.mark.asyncio
async def test_add_payment_method_creates_customer(db_manager):
"""Test that adding payment method creates Stripe customer"""
config = {}
handler = StripePaymentHandler(db_manager, config)
# Mock Stripe API calls would go here
# For now, test the structure exists
assert handler is not None
```
- [ ] **Step 2: Run test to verify it fails**
```bash
pytest tests/payments/test_stripe.py -v
```
Expected: FAIL with "No module named 'aisbf.payments.fiat.stripe_handler'"
- [ ] **Step 3: Create fiat module init**
Create `aisbf/payments/fiat/__init__.py`:
```python
"""
Fiat payment gateway integrations
"""
from aisbf.payments.fiat.stripe_handler import StripePaymentHandler
__all__ = ['StripePaymentHandler']
```
- [ ] **Step 4: Implement Stripe handler**
Create `aisbf/payments/fiat/stripe_handler.py`:
```python
"""
Stripe payment integration
"""
import logging
import stripe
import asyncio
from typing import Optional
logger = logging.getLogger(__name__)
class StripePaymentHandler:
"""Handle Stripe payments with async operations"""
def __init__(self, db_manager, config: dict):
self.db = db_manager
self.config = config
# Load Stripe configuration from database
stripe_config = self.db.fetch_one("""
SELECT * FROM payment_gateway_config
WHERE gateway = 'stripe'
""")
if stripe_config and stripe_config['enabled']:
stripe.api_key = stripe_config['secret_key']
self.publishable_key = stripe_config['publishable_key']
self.webhook_secret = stripe_config['webhook_secret']
self.test_mode = stripe_config.get('sandbox', False)
else:
self.publishable_key = None
self.webhook_secret = None
async def add_payment_method(self, user_id: int, payment_method_token: str) -> dict:
"""Add Stripe payment method with authorization hold for verification"""
try:
# Get or create Stripe customer
customer_id = await self._get_or_create_customer(user_id)
# Attach payment method to customer
payment_method = await asyncio.to_thread(
stripe.PaymentMethod.attach,
payment_method_token,
customer=customer_id
)
# Set as default payment method
await asyncio.to_thread(
stripe.Customer.modify,
customer_id,
invoice_settings={'default_payment_method': payment_method.id}
)
# Create authorization hold for verification (not a charge)
verification_amount = 100 # $1.00 in cents
payment_intent = await asyncio.to_thread(
stripe.PaymentIntent.create,
amount=verification_amount,
currency=self.config.get('currency_code', 'usd').lower(),
customer=customer_id,
payment_method=payment_method.id,
capture_method='manual', # Authorization only, not captured
confirm=True,
description='Payment method verification'
)
# Cancel the authorization immediately (releases the hold)
await asyncio.to_thread(
stripe.PaymentIntent.cancel,
payment_intent.id
)
# Store payment method in database
self.db.execute("""
INSERT INTO payment_methods
(user_id, type, gateway, external_id, is_default, status)
VALUES (?, 'stripe', 'stripe', ?, TRUE, 'active')
""", (user_id, payment_method.id))
logger.info(f"Added Stripe payment method for user {user_id}")
return {
'success': True,
'payment_method_id': payment_method.id,
'last4': payment_method.card.last4,
'brand': payment_method.card.brand
}
except stripe.error.CardError as e:
logger.error(f"Card error: {e.user_message}")
return {'success': False, 'error': e.user_message}
except Exception as e:
logger.error(f"Error adding Stripe payment method: {e}")
return {'success': False, 'error': str(e)}
async def _get_or_create_customer(self, user_id: int) -> str:
"""Get existing Stripe customer or create new one"""
# Check if customer exists in database
user = self.db.fetch_one(
"SELECT email, stripe_customer_id FROM users WHERE id = ?",
(user_id,)
)
if user.get('stripe_customer_id'):
return user['stripe_customer_id']
# Create new Stripe customer
customer = await asyncio.to_thread(
stripe.Customer.create,
email=user['email'],
metadata={'user_id': user_id}
)
# Store customer ID
self.db.execute(
"UPDATE users SET stripe_customer_id = ? WHERE id = ?",
(customer.id, user_id)
)
return customer.id
async def handle_webhook(self, payload: bytes, signature: str) -> dict:
"""Handle Stripe webhook events"""
try:
# Verify webhook signature
event = stripe.Webhook.construct_event(
payload, signature, self.webhook_secret
)
event_type = event['type']
if event_type == 'payment_intent.succeeded':
await self._handle_payment_succeeded(event['data']['object'])
elif event_type == 'payment_intent.payment_failed':
await self._handle_payment_failed(event['data']['object'])
return {'status': 'success'}
except stripe.error.SignatureVerificationError as e:
logger.error(f"Invalid webhook signature: {e}")
return {'status': 'error', 'message': 'Invalid signature'}
except Exception as e:
logger.error(f"Error handling Stripe webhook: {e}")
return {'status': 'error', 'message': str(e)}
async def _handle_payment_succeeded(self, payment_intent: dict):
"""Handle successful payment"""
logger.info(f"Payment succeeded: {payment_intent['id']}")
async def _handle_payment_failed(self, payment_intent: dict):
"""Handle failed payment"""
logger.warning(f"Payment failed: {payment_intent['id']}")
```
- [ ] **Step 5: Add stripe_customer_id column to users table**
Modify `aisbf/payments/migrations.py`, add to `run_migrations()` method:
```python
# Add Stripe customer ID to users table
self.db.execute("""
ALTER TABLE users ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(100)
""")
```
- [ ] **Step 6: Run test to verify it passes**
```bash
pytest tests/payments/test_stripe.py -v
```
Expected: PASS
- [ ] **Step 7: Commit**
```bash
git add aisbf/payments/fiat/ tests/payments/test_stripe.py aisbf/payments/migrations.py
git commit -m "feat(payments): implement Stripe payment handler"
```
---
## Task 2: PayPal Payment Handler
**Files:**
- Create: `aisbf/payments/fiat/paypal_handler.py`
- Create: `tests/payments/test_paypal.py`
- [ ] **Step 1: Write failing test for PayPal billing agreement**
Create `tests/payments/test_paypal.py`:
```python
import pytest
from aisbf.database import DatabaseManager
from aisbf.payments.migrations import PaymentMigrations
from aisbf.payments.fiat.paypal_handler import PayPalPaymentHandler
@pytest.fixture
def db_manager(tmp_path):
"""Create test database"""
db_path = tmp_path / "test.db"
db_config = {
'type': 'sqlite',
'sqlite_path': str(db_path)
}
db = DatabaseManager(db_config)
migrations = PaymentMigrations(db)
migrations.run_migrations()
# Add test user
db.execute("""
INSERT INTO users (id, email, username, password_hash)
VALUES (1, 'test@example.com', 'testuser', 'hash')
""")
return db
@pytest.mark.asyncio
async def test_paypal_handler_initialization(db_manager):
"""Test PayPal handler initialization"""
config = {'base_url': 'http://localhost:17765'}
handler = PayPalPaymentHandler(db_manager, config)
assert handler is not None
```
- [ ] **Step 2: Run test to verify it fails**
```bash
pytest tests/payments/test_paypal.py -v
```
Expected: FAIL with "No module named 'aisbf.payments.fiat.paypal_handler'"
- [ ] **Step 3: Implement PayPal handler**
Create `aisbf/payments/fiat/paypal_handler.py`:
```python
"""
PayPal payment integration
"""
import logging
import base64
import httpx
from typing import Optional
logger = logging.getLogger(__name__)
class PayPalPaymentHandler:
"""Handle PayPal payments with async operations"""
def __init__(self, db_manager, config: dict):
self.db = db_manager
self.config = config
self.http_client = httpx.AsyncClient(timeout=30.0)
# Load PayPal configuration from database
paypal_config = self.db.fetch_one("""
SELECT * FROM payment_gateway_config
WHERE gateway = 'paypal'
""")
if paypal_config and paypal_config['enabled']:
self.client_id = paypal_config['client_id']
self.client_secret = paypal_config['client_secret']
self.webhook_id = paypal_config.get('webhook_id')
self.sandbox = paypal_config.get('sandbox', False)
# Set API base URL
if self.sandbox:
self.base_url = 'https://api-m.sandbox.paypal.com'
else:
self.base_url = 'https://api-m.paypal.com'
else:
self.client_id = None
self.client_secret = None
async def get_access_token(self) -> str:
"""Get PayPal OAuth access token"""
auth = base64.b64encode(
f"{self.client_id}:{self.client_secret}".encode()
).decode()
response = await self.http_client.post(
f"{self.base_url}/v1/oauth2/token",
headers={
'Authorization': f'Basic {auth}',
'Content-Type': 'application/x-www-form-urlencoded'
},
data={'grant_type': 'client_credentials'}
)
if response.status_code == 200:
data = response.json()
return data['access_token']
else:
raise Exception(f"Failed to get PayPal access token: {response.text}")
async def create_billing_agreement(self, user_id: int, return_url: str,
cancel_url: str) -> dict:
"""Create PayPal billing agreement for recurring payments"""
try:
access_token = await self.get_access_token()
# Create billing agreement token
response = await self.http_client.post(
f"{self.base_url}/v1/billing-agreements/agreement-tokens",
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
},
json={
'description': 'AISBF Subscription',
'payer': {
'payment_method': 'PAYPAL'
},
'plan': {
'type': 'MERCHANT_INITIATED_BILLING',
'merchant_preferences': {
'return_url': return_url,
'cancel_url': cancel_url,
'notify_url': f"{self.config['base_url']}/api/webhooks/paypal",
'accepted_payment_type': 'INSTANT',
'skip_shipping_address': True
}
}
}
)
if response.status_code == 201:
data = response.json()
token = data['token_id']
# Generate approval URL
approval_url = f"{self.base_url}/checkoutnow?token={token}"
return {
'success': True,
'token': token,
'approval_url': approval_url
}
else:
logger.error(f"Failed to create billing agreement: {response.text}")
return {'success': False, 'error': response.text}
except Exception as e:
logger.error(f"Error creating PayPal billing agreement: {e}")
return {'success': False, 'error': str(e)}
async def execute_billing_agreement(self, user_id: int, token: str) -> dict:
"""Execute billing agreement after user approval"""
try:
access_token = await self.get_access_token()
# Execute agreement
response = await self.http_client.post(
f"{self.base_url}/v1/billing-agreements/agreements",
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
},
json={'token_id': token}
)
if response.status_code == 201:
data = response.json()
agreement_id = data['id']
# Store payment method
self.db.execute("""
INSERT INTO payment_methods
(user_id, type, gateway, external_id, is_default, status)
VALUES (?, 'paypal', 'paypal', ?, TRUE, 'active')
""", (user_id, agreement_id))
logger.info(f"Executed PayPal billing agreement for user {user_id}")
return {
'success': True,
'agreement_id': agreement_id,
'payer_email': data.get('payer', {}).get('payer_info', {}).get('email')
}
else:
logger.error(f"Failed to execute billing agreement: {response.text}")
return {'success': False, 'error': response.text}
except Exception as e:
logger.error(f"Error executing PayPal billing agreement: {e}")
return {'success': False, 'error': str(e)}
async def handle_webhook(self, payload: dict, headers: dict) -> dict:
"""Handle PayPal webhook events"""
try:
# Verify webhook signature (simplified)
event_type = payload.get('event_type')
if event_type == 'PAYMENT.SALE.COMPLETED':
await self._handle_payment_completed(payload['resource'])
elif event_type == 'PAYMENT.SALE.DENIED':
await self._handle_payment_denied(payload['resource'])
return {'status': 'success'}
except Exception as e:
logger.error(f"Error handling PayPal webhook: {e}")
return {'status': 'error', 'message': str(e)}
async def _handle_payment_completed(self, resource: dict):
"""Handle completed payment"""
logger.info(f"PayPal payment completed: {resource.get('id')}")
async def _handle_payment_denied(self, resource: dict):
"""Handle denied payment"""
logger.warning(f"PayPal payment denied: {resource.get('id')}")
```
- [ ] **Step 4: Update fiat module init**
Modify `aisbf/payments/fiat/__init__.py`:
```python
"""
Fiat payment gateway integrations
"""
from aisbf.payments.fiat.stripe_handler import StripePaymentHandler
from aisbf.payments.fiat.paypal_handler import PayPalPaymentHandler
__all__ = ['StripePaymentHandler', 'PayPalPaymentHandler']
```
- [ ] **Step 5: Run test to verify it passes**
```bash
pytest tests/payments/test_paypal.py -v
```
Expected: PASS
- [ ] **Step 6: Commit**
```bash
git add aisbf/payments/fiat/paypal_handler.py tests/payments/test_paypal.py aisbf/payments/fiat/__init__.py
git commit -m "feat(payments): implement PayPal payment handler"
```
---
## Task 3: Update Payment Service with Fiat Handlers
**Files:**
- Modify: `aisbf/payments/service.py`
- [ ] **Step 1: Add fiat handlers to payment service**
Modify `aisbf/payments/service.py`, update `__init__` method:
```python
def __init__(self, db_manager, config: dict):
self.db = db_manager
self.config = config
# Initialize crypto sub-services
from aisbf.payments.crypto.wallet import CryptoWalletManager
from aisbf.payments.crypto.pricing import CryptoPriceService
from aisbf.payments.crypto.monitor import BlockchainMonitor
self.wallet_manager = CryptoWalletManager(db_manager, config['encryption_key'])
self.price_service = CryptoPriceService(db_manager, config)
self.blockchain_monitor = BlockchainMonitor(db_manager, config)
# Initialize fiat sub-services
from aisbf.payments.fiat.stripe_handler import StripePaymentHandler
from aisbf.payments.fiat.paypal_handler import PayPalPaymentHandler
self.stripe_handler = StripePaymentHandler(db_manager, config)
self.paypal_handler = PayPalPaymentHandler(db_manager, config)
```
- [ ] **Step 2: Add fiat payment method methods**
Add to `aisbf/payments/service.py`:
```python
async def add_stripe_payment_method(self, user_id: int, payment_method_token: str) -> dict:
"""Add Stripe payment method"""
return await self.stripe_handler.add_payment_method(user_id, payment_method_token)
async def initiate_paypal_billing_agreement(self, user_id: int,
return_url: str,
cancel_url: str) -> dict:
"""Initiate PayPal billing agreement"""
return await self.paypal_handler.create_billing_agreement(
user_id,
return_url,
cancel_url
)
async def complete_paypal_billing_agreement(self, user_id: int, token: str) -> dict:
"""Complete PayPal billing agreement after user approval"""
return await self.paypal_handler.execute_billing_agreement(user_id, token)
async def get_payment_methods(self, user_id: int) -> list:
"""Get user's payment methods"""
methods = self.db.fetch_all("""
SELECT * FROM payment_methods
WHERE user_id = ?
ORDER BY is_default DESC, created_at DESC
""", (user_id,))
return [dict(method) for method in methods]
async def delete_payment_method(self, user_id: int, payment_method_id: int) -> dict:
"""Delete payment method"""
try:
# Check if used by active subscription
subscription = self.db.fetch_one("""
SELECT id FROM subscriptions
WHERE user_id = ?
AND payment_method_id = ?
AND status = 'active'
""", (user_id, payment_method_id))
if subscription:
return {
'success': False,
'error': 'Cannot delete payment method used by active subscription'
}
# Delete payment method
self.db.execute("""
DELETE FROM payment_methods
WHERE id = ? AND user_id = ?
""", (payment_method_id, user_id))
return {'success': True}
except Exception as e:
logger.error(f"Error deleting payment method: {e}")
return {'success': False, 'error': str(e)}
```
- [ ] **Step 3: Commit**
```bash
git add aisbf/payments/service.py
git commit -m "feat(payments): integrate fiat handlers into payment service"
```
---
## Task 4: Fiat Payment API Endpoints
**Files:**
- Modify: `main.py`
- [ ] **Step 1: Add Stripe payment method endpoint**
Add to `main.py`:
```python
@app.post("/api/payment-methods/stripe")
async def add_stripe_payment_method(
request: dict,
current_user: dict = Depends(get_current_user)
):
"""Add Stripe payment method"""
result = await payment_service.add_stripe_payment_method(
current_user['id'],
request['payment_method_token']
)
if not result['success']:
raise HTTPException(status_code=400, detail=result['error'])
return result
```
- [ ] **Step 2: Add PayPal endpoints**
Add to `main.py`:
```python
@app.post("/api/payment-methods/paypal/initiate")
async def initiate_paypal_payment_method(
request: dict,
current_user: dict = Depends(get_current_user)
):
"""Initiate PayPal billing agreement"""
result = await payment_service.initiate_paypal_billing_agreement(
current_user['id'],
request['return_url'],
request['cancel_url']
)
if not result['success']:
raise HTTPException(status_code=400, detail=result['error'])
return result
@app.post("/api/payment-methods/paypal/complete")
async def complete_paypal_payment_method(
request: dict,
current_user: dict = Depends(get_current_user)
):
"""Complete PayPal billing agreement"""
result = await payment_service.complete_paypal_billing_agreement(
current_user['id'],
request['token']
)
if not result['success']:
raise HTTPException(status_code=400, detail=result['error'])
return result
```
- [ ] **Step 3: Add payment method management endpoints**
Add to `main.py`:
```python
@app.get("/api/payment-methods")
async def get_payment_methods(current_user: dict = Depends(get_current_user)):
"""Get user's payment methods"""
methods = await payment_service.get_payment_methods(current_user['id'])
return {'payment_methods': methods}
@app.delete("/api/payment-methods/{payment_method_id}")
async def delete_payment_method(
payment_method_id: int,
current_user: dict = Depends(get_current_user)
):
"""Delete payment method"""
result = await payment_service.delete_payment_method(
current_user['id'],
payment_method_id
)
if not result['success']:
raise HTTPException(status_code=400, detail=result['error'])
return result
```
- [ ] **Step 4: Add webhook endpoints**
Add to `main.py`:
```python
@app.post("/api/webhooks/stripe")
async def stripe_webhook(
request: Request,
stripe_signature: str = Header(None, alias="Stripe-Signature")
):
"""Handle Stripe webhooks"""
payload = await request.body()
result = await payment_service.stripe_handler.handle_webhook(
payload,
stripe_signature
)
return result
@app.post("/api/webhooks/paypal")
async def paypal_webhook(request: Request):
"""Handle PayPal webhooks"""
payload = await request.json()
headers = dict(request.headers)
result = await payment_service.paypal_handler.handle_webhook(payload, headers)
return result
```
- [ ] **Step 5: Commit**
```bash
git add main.py
git commit -m "feat(payments): add fiat payment API endpoints"
```
---
## Phase 2 Complete!
Phase 2 deliverables achieved:
- ✅ Stripe payment method integration
- ✅ PayPal billing agreement integration
- ✅ Authorization hold verification (not charges)
- ✅ Payment method management (list, delete)
- ✅ Webhook handlers for both gateways
- ✅ API endpoints for payment methods
- ✅ Unit tests for fiat handlers
**Next Steps:**
- Phase 3: Subscriptions & Billing (creation, upgrades, downgrades, renewals, retries)
- Phase 4: Advanced Features (Quota enforcement, consolidation, emails, scheduler)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment