Support subcription payments
This commit is contained in:
@@ -8,6 +8,7 @@ from typing import List, Dict, Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from app.services.subscription_limit_service import SubscriptionLimitService
|
||||
from app.services.payment_service import PaymentService
|
||||
from app.repositories import SubscriptionRepository
|
||||
from app.models.tenants import Subscription
|
||||
from shared.auth.decorators import get_current_user_dep, require_admin_role_dep
|
||||
@@ -27,6 +28,13 @@ def get_subscription_limit_service():
|
||||
logger.error("Failed to create subscription limit service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Service initialization failed")
|
||||
|
||||
def get_payment_service():
|
||||
try:
|
||||
return PaymentService()
|
||||
except Exception as e:
|
||||
logger.error("Failed to create payment service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Payment service initialization failed")
|
||||
|
||||
def get_subscription_repository():
|
||||
try:
|
||||
from app.core.config import settings
|
||||
@@ -182,7 +190,7 @@ async def validate_plan_upgrade(
|
||||
"""Validate if tenant can upgrade to a new plan"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has admin access to tenant
|
||||
# TODO: Add access control - verify user is owner/admin of tenant
|
||||
result = await limit_service.validate_plan_upgrade(str(tenant_id), new_plan)
|
||||
return result
|
||||
|
||||
@@ -241,9 +249,9 @@ async def upgrade_subscription_plan(
|
||||
detail="Failed to upgrade subscription plan"
|
||||
)
|
||||
|
||||
@router.get("/plans/available")
|
||||
@router.get("/plans")
|
||||
async def get_available_plans():
|
||||
"""Get all available subscription plans with features and pricing"""
|
||||
"""Get all available subscription plans with features and pricing - Public endpoint"""
|
||||
|
||||
try:
|
||||
# This could be moved to a config service or database
|
||||
@@ -294,7 +302,7 @@ async def get_available_plans():
|
||||
"description": "Ideal para cadenas con obradores centrales",
|
||||
"monthly_price": 399.0,
|
||||
"max_users": -1, # Unlimited
|
||||
"max_locations": -1, # Unlimited
|
||||
"max_locations": -1, # Unlimited
|
||||
"max_products": -1, # Unlimited
|
||||
"features": {
|
||||
"inventory_management": "multi_location",
|
||||
@@ -321,4 +329,93 @@ async def get_available_plans():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get available plans"
|
||||
)
|
||||
)
|
||||
|
||||
# New endpoints for payment processing during registration
|
||||
@router.post("/subscriptions/register-with-subscription")
|
||||
async def register_with_subscription(
|
||||
user_data: Dict[str, Any],
|
||||
plan_id: str = Query(..., description="Plan ID to subscribe to"),
|
||||
payment_method_id: str = Query(..., description="Payment method ID from frontend"),
|
||||
use_trial: bool = Query(False, description="Whether to use trial period for pilot users"),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Process user registration with subscription creation"""
|
||||
|
||||
try:
|
||||
result = await payment_service.process_registration_with_subscription(
|
||||
user_data,
|
||||
plan_id,
|
||||
payment_method_id,
|
||||
use_trial
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Registration and subscription created successfully",
|
||||
"data": result
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to register with subscription", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to register with subscription"
|
||||
)
|
||||
|
||||
@router.post("/subscriptions/{tenant_id}/cancel")
|
||||
async def cancel_subscription(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Cancel subscription for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user is owner/admin of tenant
|
||||
# In a real implementation, you would need to retrieve the subscription ID from the database
|
||||
# For now, this is a placeholder
|
||||
subscription_id = "sub_test" # This would come from the database
|
||||
|
||||
result = await payment_service.cancel_subscription(subscription_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Subscription cancelled successfully",
|
||||
"data": {
|
||||
"subscription_id": result.id,
|
||||
"status": result.status
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to cancel subscription", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to cancel subscription"
|
||||
)
|
||||
|
||||
@router.get("/subscriptions/{tenant_id}/invoices")
|
||||
async def get_invoices(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Get invoices for a tenant"""
|
||||
|
||||
try:
|
||||
# TODO: Add access control - verify user has access to tenant
|
||||
# In a real implementation, you would need to retrieve the customer ID from the database
|
||||
# For now, this is a placeholder
|
||||
customer_id = "cus_test" # This would come from the database
|
||||
|
||||
invoices = await payment_service.get_invoices(customer_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": invoices
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get invoices", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get invoices"
|
||||
)
|
||||
|
||||
133
services/tenant/app/api/webhooks.py
Normal file
133
services/tenant/app/api/webhooks.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Webhook endpoints for handling payment provider events
|
||||
These endpoints receive events from payment providers like Stripe
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.services.payment_service import PaymentService
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.monitoring.metrics import track_endpoint_metrics
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
|
||||
def get_payment_service():
|
||||
try:
|
||||
return PaymentService()
|
||||
except Exception as e:
|
||||
logger.error("Failed to create payment service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Payment service initialization failed")
|
||||
|
||||
@router.post("/webhooks/stripe")
|
||||
async def stripe_webhook(
|
||||
request: Request,
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""
|
||||
Stripe webhook endpoint to handle payment events
|
||||
"""
|
||||
try:
|
||||
# Get the payload
|
||||
payload = await request.body()
|
||||
sig_header = request.headers.get('stripe-signature')
|
||||
|
||||
# In a real implementation, you would verify the signature
|
||||
# using the webhook signing secret
|
||||
# event = stripe.Webhook.construct_event(
|
||||
# payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
|
||||
# )
|
||||
|
||||
# For now, we'll just log the event
|
||||
logger.info("Received Stripe webhook", payload=payload.decode('utf-8'))
|
||||
|
||||
# Process different types of events
|
||||
# event_type = event['type']
|
||||
# event_data = event['data']['object']
|
||||
|
||||
# Example processing for different event types:
|
||||
# if event_type == 'checkout.session.completed':
|
||||
# # Handle successful checkout
|
||||
# pass
|
||||
# elif event_type == 'customer.subscription.created':
|
||||
# # Handle new subscription
|
||||
# pass
|
||||
# elif event_type == 'customer.subscription.updated':
|
||||
# # Handle subscription update
|
||||
# pass
|
||||
# elif event_type == 'customer.subscription.deleted':
|
||||
# # Handle subscription cancellation
|
||||
# pass
|
||||
# elif event_type == 'invoice.payment_succeeded':
|
||||
# # Handle successful payment
|
||||
# pass
|
||||
# elif event_type == 'invoice.payment_failed':
|
||||
# # Handle failed payment
|
||||
# pass
|
||||
|
||||
return {"success": True}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error processing Stripe webhook", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Webhook error"
|
||||
)
|
||||
|
||||
@router.post("/webhooks/generic")
|
||||
async def generic_webhook(
|
||||
request: Request,
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""
|
||||
Generic webhook endpoint that can handle events from any payment provider
|
||||
"""
|
||||
try:
|
||||
# Get the payload
|
||||
payload = await request.json()
|
||||
|
||||
# Log the event for debugging
|
||||
logger.info("Received generic webhook", payload=payload)
|
||||
|
||||
# Process the event based on its type
|
||||
event_type = payload.get('type', 'unknown')
|
||||
event_data = payload.get('data', {})
|
||||
|
||||
# Process different types of events
|
||||
if event_type == 'subscription.created':
|
||||
# Handle new subscription
|
||||
logger.info("Processing new subscription event", subscription_id=event_data.get('id'))
|
||||
# Update database with new subscription
|
||||
elif event_type == 'subscription.updated':
|
||||
# Handle subscription update
|
||||
logger.info("Processing subscription update event", subscription_id=event_data.get('id'))
|
||||
# Update database with subscription changes
|
||||
elif event_type == 'subscription.deleted':
|
||||
# Handle subscription cancellation
|
||||
logger.info("Processing subscription cancellation event", subscription_id=event_data.get('id'))
|
||||
# Update database with cancellation
|
||||
elif event_type == 'payment.succeeded':
|
||||
# Handle successful payment
|
||||
logger.info("Processing successful payment event", payment_id=event_data.get('id'))
|
||||
# Update payment status in database
|
||||
elif event_type == 'payment.failed':
|
||||
# Handle failed payment
|
||||
logger.info("Processing failed payment event", payment_id=event_data.get('id'))
|
||||
# Update payment status and notify user
|
||||
elif event_type == 'invoice.created':
|
||||
# Handle new invoice
|
||||
logger.info("Processing new invoice event", invoice_id=event_data.get('id'))
|
||||
# Store invoice information
|
||||
else:
|
||||
logger.warning("Unknown event type received", event_type=event_type)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error processing generic webhook", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Webhook error"
|
||||
)
|
||||
@@ -66,5 +66,10 @@ class TenantSettings(BaseServiceSettings):
|
||||
GDPR_COMPLIANCE_ENABLED: bool = True
|
||||
DATA_EXPORT_ENABLED: bool = True
|
||||
DATA_DELETION_ENABLED: bool = True
|
||||
|
||||
# Stripe Payment Configuration
|
||||
STRIPE_PUBLISHABLE_KEY: str = os.getenv("STRIPE_PUBLISHABLE_KEY", "")
|
||||
STRIPE_SECRET_KEY: str = os.getenv("STRIPE_SECRET_KEY", "")
|
||||
STRIPE_WEBHOOK_SECRET: str = os.getenv("STRIPE_WEBHOOK_SECRET", "")
|
||||
|
||||
settings = TenantSettings()
|
||||
|
||||
@@ -9,7 +9,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import database_manager
|
||||
from app.api import tenants, subscriptions
|
||||
from app.api import tenants, subscriptions, webhooks
|
||||
from shared.monitoring.logging import setup_logging
|
||||
from shared.monitoring.metrics import MetricsCollector
|
||||
|
||||
@@ -42,6 +42,7 @@ app.add_middleware(
|
||||
# Include routers
|
||||
app.include_router(tenants.router, prefix="/api/v1", tags=["tenants"])
|
||||
app.include_router(subscriptions.router, prefix="/api/v1", tags=["subscriptions"])
|
||||
app.include_router(webhooks.router, prefix="/api/v1", tags=["webhooks"])
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
@@ -94,4 +95,4 @@ async def metrics():
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
152
services/tenant/app/services/payment_service.py
Normal file
152
services/tenant/app/services/payment_service.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Payment Service for handling subscription payments
|
||||
This service abstracts payment provider interactions and makes the system payment-agnostic
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from typing import Dict, Any, Optional
|
||||
import uuid
|
||||
|
||||
from app.core.config import settings
|
||||
from shared.clients.payment_client import PaymentProvider, PaymentCustomer, Subscription, PaymentMethod
|
||||
from shared.clients.stripe_client import StripeProvider
|
||||
from shared.database.base import create_database_manager
|
||||
from app.repositories.subscription_repository import SubscriptionRepository
|
||||
from app.models.tenants import Subscription as SubscriptionModel
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class PaymentService:
|
||||
"""Service for handling payment provider interactions"""
|
||||
|
||||
def __init__(self):
|
||||
# Initialize payment provider based on configuration
|
||||
# For now, we'll use Stripe, but this can be swapped for other providers
|
||||
self.payment_provider: PaymentProvider = StripeProvider(
|
||||
api_key=settings.STRIPE_SECRET_KEY,
|
||||
webhook_secret=settings.STRIPE_WEBHOOK_SECRET
|
||||
)
|
||||
|
||||
# Initialize database components
|
||||
self.database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
self.subscription_repo = SubscriptionRepository(SubscriptionModel, None) # Will be set in methods
|
||||
|
||||
async def create_customer(self, user_data: Dict[str, Any]) -> PaymentCustomer:
|
||||
"""Create a customer in the payment provider system"""
|
||||
try:
|
||||
customer_data = {
|
||||
'email': user_data.get('email'),
|
||||
'name': user_data.get('full_name'),
|
||||
'metadata': {
|
||||
'user_id': user_data.get('user_id'),
|
||||
'tenant_id': user_data.get('tenant_id')
|
||||
}
|
||||
}
|
||||
|
||||
return await self.payment_provider.create_customer(customer_data)
|
||||
except Exception as e:
|
||||
logger.error("Failed to create customer in payment provider", error=str(e))
|
||||
raise e
|
||||
|
||||
async def create_subscription(
|
||||
self,
|
||||
customer_id: str,
|
||||
plan_id: str,
|
||||
payment_method_id: str,
|
||||
trial_period_days: Optional[int] = None
|
||||
) -> Subscription:
|
||||
"""Create a subscription for a customer"""
|
||||
try:
|
||||
return await self.payment_provider.create_subscription(
|
||||
customer_id,
|
||||
plan_id,
|
||||
payment_method_id,
|
||||
trial_period_days
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Failed to create subscription in payment provider", error=str(e))
|
||||
raise e
|
||||
|
||||
async def process_registration_with_subscription(
|
||||
self,
|
||||
user_data: Dict[str, Any],
|
||||
plan_id: str,
|
||||
payment_method_id: str,
|
||||
use_trial: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Process user registration with subscription creation"""
|
||||
try:
|
||||
# Create customer in payment provider
|
||||
customer = await self.create_customer(user_data)
|
||||
|
||||
# Determine trial period
|
||||
trial_period_days = None
|
||||
if use_trial:
|
||||
trial_period_days = 90 # 3 months trial for pilot users
|
||||
|
||||
# Create subscription
|
||||
subscription = await self.create_subscription(
|
||||
customer.id,
|
||||
plan_id,
|
||||
payment_method_id,
|
||||
trial_period_days
|
||||
)
|
||||
|
||||
# Save subscription to database
|
||||
async with self.database_manager.get_session() as session:
|
||||
self.subscription_repo.session = session
|
||||
subscription_record = await self.subscription_repo.create({
|
||||
'id': str(uuid.uuid4()),
|
||||
'tenant_id': user_data.get('tenant_id'),
|
||||
'customer_id': customer.id,
|
||||
'subscription_id': subscription.id,
|
||||
'plan_id': plan_id,
|
||||
'status': subscription.status,
|
||||
'current_period_start': subscription.current_period_start,
|
||||
'current_period_end': subscription.current_period_end,
|
||||
'created_at': subscription.created_at,
|
||||
'trial_period_days': trial_period_days
|
||||
})
|
||||
|
||||
return {
|
||||
'customer_id': customer.id,
|
||||
'subscription_id': subscription.id,
|
||||
'status': subscription.status,
|
||||
'trial_period_days': trial_period_days
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to process registration with subscription", error=str(e))
|
||||
raise e
|
||||
|
||||
async def cancel_subscription(self, subscription_id: str) -> Subscription:
|
||||
"""Cancel a subscription in the payment provider"""
|
||||
try:
|
||||
return await self.payment_provider.cancel_subscription(subscription_id)
|
||||
except Exception as e:
|
||||
logger.error("Failed to cancel subscription in payment provider", error=str(e))
|
||||
raise e
|
||||
|
||||
async def update_payment_method(self, customer_id: str, payment_method_id: str) -> PaymentMethod:
|
||||
"""Update the payment method for a customer"""
|
||||
try:
|
||||
return await self.payment_provider.update_payment_method(customer_id, payment_method_id)
|
||||
except Exception as e:
|
||||
logger.error("Failed to update payment method in payment provider", error=str(e))
|
||||
raise e
|
||||
|
||||
async def get_invoices(self, customer_id: str) -> list:
|
||||
"""Get invoices for a customer from the payment provider"""
|
||||
try:
|
||||
return await self.payment_provider.get_invoices(customer_id)
|
||||
except Exception as e:
|
||||
logger.error("Failed to get invoices from payment provider", error=str(e))
|
||||
raise e
|
||||
|
||||
async def get_subscription(self, subscription_id: str) -> Subscription:
|
||||
"""Get subscription details from the payment provider"""
|
||||
try:
|
||||
return await self.payment_provider.get_subscription(subscription_id)
|
||||
except Exception as e:
|
||||
logger.error("Failed to get subscription from payment provider", error=str(e))
|
||||
raise e
|
||||
@@ -13,4 +13,5 @@ python-json-logger==2.0.4
|
||||
pytz==2023.3
|
||||
python-logstash==0.4.8
|
||||
structlog==23.2.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
stripe==7.4.0
|
||||
|
||||
Reference in New Issue
Block a user