Add subcription feature 3

This commit is contained in:
Urtzi Alfaro
2026-01-15 20:45:49 +01:00
parent a4c3b7da3f
commit b674708a4c
83 changed files with 9451 additions and 6828 deletions

View File

@@ -0,0 +1,160 @@
"""
Payment Provider Interface
Abstract base class for payment provider implementations
Allows easy swapping of payment SDKs (Stripe, PayPal, etc.)
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
class PaymentProvider(ABC):
"""
Abstract Payment Provider Interface
Define all required methods for payment processing
"""
@abstractmethod
async def create_customer(
self,
email: str,
name: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create a customer in the payment provider"""
pass
@abstractmethod
async def attach_payment_method(
self,
payment_method_id: str,
customer_id: str
) -> Dict[str, Any]:
"""Attach a payment method to a customer"""
pass
@abstractmethod
async def set_default_payment_method(
self,
customer_id: str,
payment_method_id: str
) -> Dict[str, Any]:
"""Set the default payment method for a customer"""
pass
@abstractmethod
async def create_setup_intent_for_verification(
self,
customer_id: str,
payment_method_id: str,
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create a SetupIntent for payment method verification (3DS support)"""
pass
@abstractmethod
async def verify_setup_intent_status(
self,
setup_intent_id: str
) -> Dict[str, Any]:
"""Verify the status of a SetupIntent"""
pass
@abstractmethod
async def create_subscription_with_verified_payment(
self,
customer_id: str,
price_id: str,
payment_method_id: str,
trial_period_days: Optional[int] = None,
billing_cycle_anchor: Optional[Any] = None,
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create a subscription with a verified payment method
Args:
billing_cycle_anchor: Can be int (Unix timestamp), "now", or "unchanged"
"""
pass
@abstractmethod
async def create_setup_intent(self) -> Dict[str, Any]:
"""Create a basic SetupIntent"""
pass
@abstractmethod
async def get_setup_intent(
self,
setup_intent_id: str
) -> Any:
"""Get SetupIntent details"""
pass
@abstractmethod
async def create_payment_intent(
self,
amount: float,
currency: str,
customer_id: str,
payment_method_id: str
) -> Dict[str, Any]:
"""Create a PaymentIntent for one-time payments"""
pass
@abstractmethod
async def complete_subscription_after_setup_intent(
self,
setup_intent_id: str
) -> Dict[str, Any]:
"""Complete subscription creation after SetupIntent verification"""
pass
@abstractmethod
async def cancel_subscription(
self,
subscription_id: str
) -> Dict[str, Any]:
"""Cancel a subscription"""
pass
@abstractmethod
async def update_payment_method(
self,
customer_id: str,
payment_method_id: str
) -> Dict[str, Any]:
"""Update customer's payment method"""
pass
@abstractmethod
async def update_subscription(
self,
subscription_id: str,
new_price_id: str
) -> Dict[str, Any]:
"""Update subscription price"""
pass
@abstractmethod
async def get_subscription(
self,
subscription_id: str
) -> Dict[str, Any]:
"""Get subscription details"""
pass
@abstractmethod
async def get_customer_payment_method(
self,
customer_id: str
) -> Dict[str, Any]:
"""Get customer's payment method"""
pass
@abstractmethod
async def get_invoices(
self,
customer_id: str
) -> Dict[str, Any]:
"""Get customer invoices"""
pass

File diff suppressed because it is too large Load Diff

View File

@@ -57,7 +57,7 @@ class SubscriptionServiceClient:
'features': subscription.features or {}
}
except Exception as e:
logger.error("Failed to get subscription", tenant_id=tenant_id, error=str(e))
logger.error(f"Failed to get subscription, tenant_id={tenant_id}, error={str(e)}")
raise
async def update_subscription_plan(self, tenant_id: str, new_plan: str) -> Dict[str, Any]:
@@ -93,7 +93,7 @@ class SubscriptionServiceClient:
'status': updated_subscription.status
}
except Exception as e:
logger.error("Failed to update subscription plan", tenant_id=tenant_id, new_plan=new_plan, error=str(e))
logger.error(f"Failed to update subscription plan, tenant_id={tenant_id}, new_plan={new_plan}, error={str(e)}")
raise
async def create_child_subscription(self, child_tenant_id: str, parent_tenant_id: str) -> Dict[str, Any]:

View File

@@ -292,7 +292,7 @@ class TenantServiceClient(BaseServiceClient):
count=len(result) if isinstance(result, list) else 0)
return result if result else []
except Exception as e:
logger.error("Error getting active tenants", error=str(e))
logger.error(f"Error getting active tenants: {str(e)}")
return []
# ================================================================
@@ -417,72 +417,55 @@ class TenantServiceClient(BaseServiceClient):
result = await self.get("../health") # Health endpoint is not tenant-scoped
return result is not None
except Exception as e:
logger.error("Tenant service health check failed", error=str(e))
logger.error(f"Tenant service health check failed: {str(e)}")
return False
async def get_subscription_status(self, tenant_id: str) -> Optional[Dict[str, Any]]:
"""
Get subscription status for a tenant
Args:
tenant_id: Tenant ID
Returns:
Dictionary with subscription status information
"""
try:
result = await self.get(f"tenants/{tenant_id}/subscription/status")
if result:
logger.info("Retrieved subscription status from tenant service",
tenant_id=tenant_id, status=result.get('status'))
return result
except Exception as e:
logger.error("Error getting subscription status",
error=str(e), tenant_id=tenant_id)
return None
async def get_subscription_details(self, tenant_id: str) -> Optional[Dict[str, Any]]:
"""
Get detailed subscription information for a tenant
Args:
tenant_id: Tenant ID
Returns:
Dictionary with subscription details
"""
try:
result = await self.get(f"tenants/{tenant_id}/subscription")
if result:
logger.info("Retrieved subscription details from tenant service",
tenant_id=tenant_id, plan=result.get('plan'))
return result
except Exception as e:
logger.error("Error getting subscription details",
error=str(e), tenant_id=tenant_id)
return None
# ================================================================
# PAYMENT CUSTOMER MANAGEMENT
# ================================================================
async def create_payment_customer(
self,
user_data: Dict[str, Any],
payment_method_id: Optional[str] = None
) -> Optional[Dict[str, Any]]:
"""
Create a payment customer for a user
This method creates a payment customer record in the tenant service
during user registration or onboarding. It handles the integration
with payment providers and returns the payment customer details.
Args:
user_data: User data including:
- user_id: User ID (required)
- email: User email (required)
- full_name: User full name (required)
- name: User name (optional, defaults to full_name)
payment_method_id: Optional payment method ID to attach to the customer
Returns:
Dict with payment customer details including:
- success: boolean
- payment_customer_id: string
- payment_method: dict with payment method details
- customer: dict with customer details
Returns None if creation fails
"""
try:
logger.info("Creating payment customer via tenant service",
user_id=user_data.get('user_id'),
email=user_data.get('email'))
# Prepare data for tenant service
tenant_data = {
"user_data": user_data,
"payment_method_id": payment_method_id
}
# Call tenant service endpoint
result = await self.post("/payment-customers/create", tenant_data)
if result and result.get("success"):
logger.info("Payment customer created successfully via tenant service",
user_id=user_data.get('user_id'),
payment_customer_id=result.get('payment_customer_id'))
return result
else:
logger.error("Payment customer creation failed via tenant service",
user_id=user_data.get('user_id'),
error=result.get('detail') if result else 'No detail provided')
return None
except Exception as e:
logger.error("Failed to create payment customer via tenant service",
user_id=user_data.get('user_id'),
error=str(e))
return None
async def create_subscription_for_registration(
self,
user_data: Dict[str, Any],
@@ -537,7 +520,7 @@ class TenantServiceClient(BaseServiceClient):
}
# Call tenant service endpoint
result = await self.post("/subscriptions/create-for-registration", subscription_data)
result = await self.post("tenants/subscriptions/create-for-registration", subscription_data)
if result and result.get("success"):
data = result.get("data", {})
@@ -598,7 +581,7 @@ class TenantServiceClient(BaseServiceClient):
# Call tenant service endpoint
result = await self.post(
f"/tenants/{tenant_id}/link-subscription",
f"tenants/{tenant_id}/link-subscription",
linking_data
)
@@ -648,7 +631,7 @@ class TenantServiceClient(BaseServiceClient):
# Call tenant service endpoint
result = await self.post(
"/payment-customers/create",
"tenants/payment-customers/create",
{
"user_data": user_data,
"payment_method_id": payment_method_id
@@ -696,7 +679,7 @@ class TenantServiceClient(BaseServiceClient):
# Call tenant service orchestration endpoint
result = await self.post(
"/payment-customers/create",
"registration-payment-setup",
user_data
)
@@ -740,7 +723,7 @@ class TenantServiceClient(BaseServiceClient):
# Call tenant service orchestration endpoint
result = await self.get(
f"/setup-intents/{setup_intent_id}/verify"
f"setup-intents/{setup_intent_id}/verify"
)
if result:
@@ -779,7 +762,7 @@ class TenantServiceClient(BaseServiceClient):
# Call tenant service endpoint
result = await self.get(
f"/setup-intents/{setup_intent_id}/verify"
f"tenants/setup-intents/{setup_intent_id}/verify"
)
if result:
@@ -799,6 +782,67 @@ class TenantServiceClient(BaseServiceClient):
error=str(e))
raise
async def verify_and_complete_registration(
self,
setup_intent_id: str,
user_data: Dict[str, Any]
) -> Dict[str, Any]:
"""
Verify SetupIntent and complete registration via tenant service orchestration
This method calls the tenant service's orchestration endpoint to verify
SetupIntent status and complete the registration process by creating
the user record after successful payment verification.
Args:
setup_intent_id: SetupIntent ID to verify
user_data: User data for registration (email, full_name, etc.)
Returns:
Dictionary with registration completion result including user details
and subscription information
Raises:
Exception: If verification or registration completion fails
"""
try:
logger.info("Verifying SetupIntent and completing registration via tenant service orchestration",
setup_intent_id=setup_intent_id,
email=user_data.get('email'))
# Prepare data for tenant service orchestration
registration_data = {
"setup_intent_id": setup_intent_id,
"user_data": user_data
}
# Call tenant service orchestration endpoint
result = await self.post(
"verify-and-complete-registration",
registration_data
)
if result and result.get("success"):
logger.info("Registration completed successfully via tenant service orchestration",
setup_intent_id=setup_intent_id,
user_id=result.get('user_id'),
email=user_data.get('email'))
return result
else:
logger.error("Registration completion failed via tenant service orchestration",
setup_intent_id=setup_intent_id,
email=user_data.get('email'),
error=result.get('detail') if result else 'No detail provided')
raise Exception("Registration completion failed: " +
(result.get('detail') if result else 'Unknown error'))
except Exception as e:
logger.error("Failed to complete registration via tenant service orchestration",
setup_intent_id=setup_intent_id,
email=user_data.get('email'),
error=str(e))
raise
# Factory function for dependency injection
def create_tenant_client(config: BaseServiceSettings) -> TenantServiceClient: