Add subcription feature 3
This commit is contained in:
160
shared/clients/payment_provider.py
Normal file
160
shared/clients/payment_provider.py
Normal 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
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -310,6 +310,7 @@ class BaseServiceSettings(BaseSettings):
|
||||
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", "")
|
||||
STRIPE_API_VERSION: str = os.getenv("STRIPE_API_VERSION", "") # Empty = use SDK default
|
||||
|
||||
# ================================================================
|
||||
# ML & AI CONFIGURATION
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
"cancelled_at": null,
|
||||
"cancellation_effective_date": null,
|
||||
"created_at": "BASE_TS-90d",
|
||||
"updated_at": "BASE_TS-1d"
|
||||
"updated_at": "BASE_TS-1d",
|
||||
"is_tenant_linked": true
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"cancelled_at": null,
|
||||
"cancellation_effective_date": null,
|
||||
"created_at": "BASE_TS-30d",
|
||||
"updated_at": "BASE_TS-30d"
|
||||
"updated_at": "BASE_TS-30d",
|
||||
"is_tenant_linked": true
|
||||
}
|
||||
}
|
||||
18
shared/demo/fixtures/professional/12-distribution.json
Normal file
18
shared/demo/fixtures/professional/12-distribution.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"delivery_routes": [],
|
||||
"vehicles": [],
|
||||
"drivers": [],
|
||||
"delivery_schedules": [],
|
||||
"route_optimization_config": {
|
||||
"enabled": false,
|
||||
"algorithm": "basic",
|
||||
"constraints": {
|
||||
"max_distance_km": 50,
|
||||
"max_duration_minutes": 180,
|
||||
"vehicle_capacity_kg": 500
|
||||
}
|
||||
},
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"is_active": false,
|
||||
"description": "Professional tier demo - basic distribution setup for single location bakery"
|
||||
}
|
||||
8
shared/exceptions/__init__.py
Normal file
8
shared/exceptions/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Exceptions package for the bakery platform
|
||||
"""
|
||||
|
||||
# Import all exceptions for easy access
|
||||
from .payment_exceptions import *
|
||||
from .auth_exceptions import *
|
||||
from .registration_exceptions import *
|
||||
43
shared/exceptions/auth_exceptions.py
Normal file
43
shared/exceptions/auth_exceptions.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Authentication-related exceptions for the auth system
|
||||
"""
|
||||
|
||||
class AuthServiceError(Exception):
|
||||
"""Base exception for authentication service errors"""
|
||||
pass
|
||||
|
||||
class UserCreationError(Exception):
|
||||
"""Exception for user creation failures"""
|
||||
pass
|
||||
|
||||
class RegistrationError(Exception):
|
||||
"""Exception for registration failures"""
|
||||
pass
|
||||
|
||||
class PaymentOrchestrationError(Exception):
|
||||
"""Exception for payment orchestration failures"""
|
||||
pass
|
||||
|
||||
class LoginError(Exception):
|
||||
"""Exception for login failures"""
|
||||
pass
|
||||
|
||||
class TokenError(Exception):
|
||||
"""Exception for token-related errors"""
|
||||
pass
|
||||
|
||||
class PermissionError(Exception):
|
||||
"""Exception for permission-related errors"""
|
||||
pass
|
||||
|
||||
class UserNotFoundError(Exception):
|
||||
"""Exception for user not found errors"""
|
||||
pass
|
||||
|
||||
class EmailVerificationError(Exception):
|
||||
"""Exception for email verification failures"""
|
||||
pass
|
||||
|
||||
class PasswordResetError(Exception):
|
||||
"""Exception for password reset failures"""
|
||||
pass
|
||||
70
shared/exceptions/payment_exceptions.py
Normal file
70
shared/exceptions/payment_exceptions.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Payment-related exceptions for the atomic payment architecture
|
||||
"""
|
||||
|
||||
class PaymentException(Exception):
|
||||
"""Base class for payment-related exceptions"""
|
||||
pass
|
||||
|
||||
class PaymentVerificationRequired(PaymentException):
|
||||
"""Exception raised when payment verification is required before proceeding"""
|
||||
def __init__(self, message: str = "Payment verification required", setup_intent_id: str = None, client_secret: str = None):
|
||||
self.message = message
|
||||
self.setup_intent_id = setup_intent_id
|
||||
self.client_secret = client_secret
|
||||
super().__init__(message)
|
||||
|
||||
class ThreeDSAuthenticationRequired(PaymentException):
|
||||
"""Exception raised when 3DS authentication is required"""
|
||||
def __init__(self, setup_intent_id: str, client_secret: str, action_type: str, **kwargs):
|
||||
self.setup_intent_id = setup_intent_id
|
||||
self.client_secret = client_secret
|
||||
self.action_type = action_type
|
||||
self.extra_data = kwargs
|
||||
message = f"3DS authentication required (action: {action_type})"
|
||||
super().__init__(message)
|
||||
|
||||
class PaymentMethodInvalid(PaymentException):
|
||||
"""Exception raised when payment method is invalid"""
|
||||
def __init__(self, message: str = "Payment method is invalid"):
|
||||
super().__init__(message)
|
||||
|
||||
class PaymentFailed(PaymentException):
|
||||
"""Exception raised when payment fails"""
|
||||
def __init__(self, message: str = "Payment failed"):
|
||||
super().__init__(message)
|
||||
|
||||
class SubscriptionCreationFailed(PaymentException):
|
||||
"""Exception raised when subscription creation fails"""
|
||||
def __init__(self, message: str = "Subscription creation failed"):
|
||||
super().__init__(message)
|
||||
|
||||
class PaymentProviderError(PaymentException):
|
||||
"""Exception raised when there's an error with the payment provider"""
|
||||
def __init__(self, message: str = "Payment provider error"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class PaymentVerificationError(PaymentException):
|
||||
"""Exception raised when payment verification fails"""
|
||||
def __init__(self, message: str = "Payment verification failed"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class SetupIntentError(PaymentException):
|
||||
"""Exception raised when SetupIntent operations fail"""
|
||||
def __init__(self, message: str = "SetupIntent operation failed"):
|
||||
super().__init__(message)
|
||||
|
||||
class SubscriptionUpdateFailed(PaymentException):
|
||||
"""Exception raised when subscription update fails"""
|
||||
def __init__(self, message: str = "Subscription update failed"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
|
||||
|
||||
class PaymentServiceError(PaymentException):
|
||||
"""General payment service error"""
|
||||
def __init__(self, message: str = "Payment service error"):
|
||||
super().__init__(message)
|
||||
55
shared/exceptions/registration_exceptions.py
Normal file
55
shared/exceptions/registration_exceptions.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Registration-related exceptions for the atomic registration architecture
|
||||
"""
|
||||
|
||||
class RegistrationException(Exception):
|
||||
"""Base class for registration-related exceptions"""
|
||||
pass
|
||||
|
||||
class RegistrationPaymentFailed(RegistrationException):
|
||||
"""Exception raised when registration payment fails"""
|
||||
def __init__(self, message: str = "Registration payment failed"):
|
||||
super().__init__(message)
|
||||
|
||||
class RegistrationValidationFailed(RegistrationException):
|
||||
"""Exception raised when registration validation fails"""
|
||||
def __init__(self, message: str = "Registration validation failed", errors: dict = None):
|
||||
self.errors = errors or {}
|
||||
super().__init__(message)
|
||||
|
||||
class UserCreationFailed(RegistrationException):
|
||||
"""Exception raised when user creation fails"""
|
||||
def __init__(self, message: str = "User creation failed"):
|
||||
super().__init__(message)
|
||||
|
||||
class TenantCreationFailed(RegistrationException):
|
||||
"""Exception raised when tenant creation fails"""
|
||||
def __init__(self, message: str = "Tenant creation failed"):
|
||||
super().__init__(message)
|
||||
|
||||
class RegistrationFlowInterrupted(RegistrationException):
|
||||
"""Exception raised when registration flow is interrupted"""
|
||||
def __init__(self, message: str = "Registration flow interrupted"):
|
||||
super().__init__(message)
|
||||
|
||||
class EmailAlreadyRegistered(RegistrationException):
|
||||
"""Exception raised when email is already registered"""
|
||||
def __init__(self, message: str = "Email already registered"):
|
||||
super().__init__(message)
|
||||
|
||||
class RegistrationStateInvalid(RegistrationException):
|
||||
"""Exception raised when registration state is invalid"""
|
||||
def __init__(self, message: str = "Registration state is invalid"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class RegistrationStateError(RegistrationException):
|
||||
"""Exception raised when registration state operations fail"""
|
||||
def __init__(self, message: str = "Registration state error"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class InvalidStateTransitionError(RegistrationException):
|
||||
"""Exception raised when an invalid state transition is attempted"""
|
||||
def __init__(self, message: str = "Invalid state transition"):
|
||||
super().__init__(message)
|
||||
22
shared/exceptions/subscription_exceptions.py
Normal file
22
shared/exceptions/subscription_exceptions.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Subscription-related exceptions
|
||||
"""
|
||||
|
||||
class SubscriptionException(Exception):
|
||||
"""Base class for subscription-related exceptions"""
|
||||
pass
|
||||
|
||||
class SubscriptionNotFound(SubscriptionException):
|
||||
"""Exception raised when subscription is not found"""
|
||||
def __init__(self, message: str = "Subscription not found"):
|
||||
super().__init__(message)
|
||||
|
||||
class SubscriptionAlreadyExists(SubscriptionException):
|
||||
"""Exception raised when subscription already exists"""
|
||||
def __init__(self, message: str = "Subscription already exists"):
|
||||
super().__init__(message)
|
||||
|
||||
class SubscriptionUpdateFailed(SubscriptionException):
|
||||
"""Exception raised when subscription update fails"""
|
||||
def __init__(self, message: str = "Subscription update failed"):
|
||||
super().__init__(message)
|
||||
@@ -320,18 +320,20 @@ class BaseFastAPIService:
|
||||
@self.app.exception_handler(Exception)
|
||||
async def general_exception_handler(request: Request, exc: Exception):
|
||||
"""Handle general exceptions"""
|
||||
self.logger.error(
|
||||
print(f"DEBUG_PRINT: Base service caught unhandled exception: {str(exc)} on path {request.url.path}")
|
||||
self.logger.critical(
|
||||
"Unhandled exception",
|
||||
error=str(exc),
|
||||
path=request.url.path,
|
||||
method=request.method
|
||||
method=request.method,
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"error": "Internal Server Error",
|
||||
"detail": "An unexpected error occurred",
|
||||
"detail": f"An unexpected error occurred: {str(exc)}",
|
||||
"type": "internal_error"
|
||||
}
|
||||
)
|
||||
|
||||
64
shared/utils/retry.py
Normal file
64
shared/utils/retry.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Retry utilities for shared use across services
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
from typing import Callable, Any, Tuple, Type
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def retry_with_backoff(
|
||||
func,
|
||||
max_retries: int = 3,
|
||||
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
||||
base_delay: float = 1.0,
|
||||
max_delay: float = 60.0,
|
||||
backoff_factor: float = 2.0
|
||||
):
|
||||
"""
|
||||
Retry a function with exponential backoff.
|
||||
|
||||
Args:
|
||||
func: The function to retry (can be sync or async)
|
||||
max_retries: Maximum number of retry attempts
|
||||
exceptions: Tuple of exception types to catch and retry
|
||||
base_delay: Initial delay in seconds
|
||||
max_delay: Maximum delay between retries
|
||||
backoff_factor: Factor by which delay increases after each retry
|
||||
|
||||
Returns:
|
||||
Result of the function call
|
||||
|
||||
Raises:
|
||||
The original exception if all retries are exhausted
|
||||
"""
|
||||
for attempt in range(max_retries + 1): # +1 because first attempt doesn't count as retry
|
||||
try:
|
||||
result = func()
|
||||
# Handle both async functions and lambdas that return coroutines
|
||||
if asyncio.iscoroutine(result):
|
||||
result = await result
|
||||
return result
|
||||
except exceptions as e:
|
||||
if attempt == max_retries:
|
||||
# Exhausted all retries, re-raise the exception
|
||||
raise e
|
||||
|
||||
# Calculate delay with exponential backoff and jitter
|
||||
delay = min(base_delay * (backoff_factor ** attempt), max_delay)
|
||||
# Add jitter to prevent thundering herd
|
||||
delay = delay * (0.5 + random.random() * 0.5)
|
||||
|
||||
logger.warning(
|
||||
f"Attempt {attempt + 1} failed, retrying in {delay:.2f}s: {str(e)}",
|
||||
extra={
|
||||
"attempt": attempt + 1,
|
||||
"max_retries": max_retries,
|
||||
"exception": str(e)
|
||||
}
|
||||
)
|
||||
|
||||
await asyncio.sleep(delay)
|
||||
Reference in New Issue
Block a user