396 lines
16 KiB
Markdown
396 lines
16 KiB
Markdown
|
|
# Subscription System Architecture Redesign
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
This document outlines a comprehensive redesign of the subscription system architecture to improve organization, maintainability, and separation of concerns while respecting the existing service-based client architecture.
|
||
|
|
|
||
|
|
## Current Issues
|
||
|
|
|
||
|
|
1. **Mixed Concerns**: Subscription endpoints are scattered across different API files
|
||
|
|
2. **Inconsistent URL Patterns**: Mixed use of path vs query parameters for tenant IDs
|
||
|
|
3. **Poor Organization**: Subscription operations mixed with tenant management operations
|
||
|
|
4. **Middleware Dependencies**: Hardcoded URLs that need updating
|
||
|
|
5. **Client Layer Confusion**: Tenant client contains many subscription-specific methods
|
||
|
|
|
||
|
|
## New Architecture Design
|
||
|
|
|
||
|
|
### API Structure & URL Patterns
|
||
|
|
|
||
|
|
#### Public Endpoints (No Authentication)
|
||
|
|
```
|
||
|
|
GET /api/v1/plans - Get all available plans
|
||
|
|
GET /api/v1/plans/{tier} - Get specific plan details
|
||
|
|
GET /api/v1/plans/{tier}/features - Get plan features
|
||
|
|
GET /api/v1/plans/{tier}/limits - Get plan limits
|
||
|
|
GET /api/v1/plans/compare - Compare all plans
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Registration Flow (No Tenant Context)
|
||
|
|
```
|
||
|
|
POST /api/v1/registration/payment-setup - Start registration with payment
|
||
|
|
POST /api/v1/registration/complete - Complete registration after 3DS
|
||
|
|
GET /api/v1/registration/state/{state_id} - Check registration state
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Tenant-Dependent Subscription Endpoints
|
||
|
|
```
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/status - Get subscription status
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/details - Get full subscription details
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/tier - Get subscription tier (cached)
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/limits - Get subscription limits
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/usage - Get usage summary
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/features/{feature} - Check feature access
|
||
|
|
|
||
|
|
# Subscription Management
|
||
|
|
POST /api/v1/tenants/{tenant_id}/subscription/cancel - Cancel subscription
|
||
|
|
POST /api/v1/tenants/{tenant_id}/subscription/reactivate - Reactivate subscription
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/validate-upgrade/{new_plan} - Validate upgrade
|
||
|
|
POST /api/v1/tenants/{tenant_id}/subscription/upgrade - Upgrade subscription
|
||
|
|
|
||
|
|
# Quota & Limit Checks
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/limits/locations - Check location limits
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/limits/products - Check product limits
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/limits/users - Check user limits
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/limits/recipes - Check recipe limits
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/limits/suppliers - Check supplier limits
|
||
|
|
|
||
|
|
# Payment Management
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/payment-method - Get payment method
|
||
|
|
POST /api/v1/tenants/{tenant_id}/subscription/payment-method - Update payment method
|
||
|
|
GET /api/v1/tenants/{tenant_id}/subscription/invoices - Get invoices
|
||
|
|
```
|
||
|
|
|
||
|
|
### Service Layer Organization
|
||
|
|
|
||
|
|
**`subscription.py`** - All subscription-related endpoints
|
||
|
|
- Registration flow endpoints
|
||
|
|
- Tenant-dependent subscription endpoints
|
||
|
|
- Subscription management endpoints
|
||
|
|
- Quota and limit checks
|
||
|
|
- Payment management endpoints
|
||
|
|
|
||
|
|
**`tenant_operations.py`** - Only tenant-centric operations
|
||
|
|
- Tenant creation/management
|
||
|
|
- Tenant hierarchy operations
|
||
|
|
- Tenant settings management
|
||
|
|
- Tenant location management
|
||
|
|
|
||
|
|
**`plans.py`** - Public plan information (unchanged)
|
||
|
|
|
||
|
|
## Implementation Plan
|
||
|
|
|
||
|
|
### Phase 1: Backend API Reorganization
|
||
|
|
|
||
|
|
#### Step 1: Move Subscription Endpoints
|
||
|
|
- Move all subscription endpoints from `tenant_operations.py` to `subscription.py`
|
||
|
|
- Implement new URL patterns with consistent structure
|
||
|
|
- Update all endpoint implementations to use new paths
|
||
|
|
|
||
|
|
**Files to modify:**
|
||
|
|
- `services/tenant/app/api/subscription.py` - Add all subscription endpoints
|
||
|
|
- `services/tenant/app/api/tenant_operations.py` - Remove subscription endpoints
|
||
|
|
|
||
|
|
#### Step 2: Update Tenant Service Main.py
|
||
|
|
- Ensure proper router inclusion for new subscription endpoints
|
||
|
|
- Remove old subscription endpoints from tenant operations router
|
||
|
|
|
||
|
|
**Files to modify:**
|
||
|
|
- `services/tenant/app/main.py` - Update router inclusion
|
||
|
|
|
||
|
|
### Phase 2: Update Tenant Client
|
||
|
|
|
||
|
|
#### Step 3: Update Tenant Client URL Patterns
|
||
|
|
|
||
|
|
Update all subscription-related methods in `shared/clients/tenant_client.py`:
|
||
|
|
|
||
|
|
```python
|
||
|
|
async def get_subscription_status(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||
|
|
# Updated URL pattern
|
||
|
|
result = await self.get(f"tenants/{tenant_id}/subscription/status")
|
||
|
|
return result
|
||
|
|
|
||
|
|
async def get_subscription_details(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||
|
|
# Updated URL pattern
|
||
|
|
result = await self.get(f"tenants/{tenant_id}/subscription/details")
|
||
|
|
return result
|
||
|
|
|
||
|
|
# Update all other subscription methods similarly
|
||
|
|
async def get_subscription_tier(self, tenant_id: str) -> Optional[str]:
|
||
|
|
result = await self.get(f"tenants/{tenant_id}/subscription/tier")
|
||
|
|
return result.get('tier') if result else None
|
||
|
|
|
||
|
|
async def get_subscription_limits(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/limits")
|
||
|
|
|
||
|
|
async def get_usage_summary(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/usage")
|
||
|
|
|
||
|
|
async def has_feature(self, tenant_id: str, feature: str) -> bool:
|
||
|
|
result = await self.get(f"tenants/{tenant_id}/subscription/features/{feature}")
|
||
|
|
return result.get('has_feature', False) if result else False
|
||
|
|
|
||
|
|
# Registration flow methods
|
||
|
|
async def start_registration_payment_setup(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
result = await self.post("registration/payment-setup", user_data)
|
||
|
|
return result
|
||
|
|
|
||
|
|
async def complete_registration(self, setup_intent_id: str, user_data: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
result = await self.post("registration/complete", {
|
||
|
|
"setup_intent_id": setup_intent_id,
|
||
|
|
"user_data": user_data
|
||
|
|
})
|
||
|
|
return result
|
||
|
|
|
||
|
|
# Update quota check methods
|
||
|
|
async def can_add_location(self, tenant_id: str) -> Dict[str, Any]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/limits/locations")
|
||
|
|
|
||
|
|
async def can_add_product(self, tenant_id: str) -> Dict[str, Any]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/limits/products")
|
||
|
|
|
||
|
|
async def can_add_user(self, tenant_id: str) -> Dict[str, Any]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/limits/users")
|
||
|
|
|
||
|
|
async def can_add_recipe(self, tenant_id: str) -> Dict[str, Any]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/limits/recipes")
|
||
|
|
|
||
|
|
async def can_add_supplier(self, tenant_id: str) -> Dict[str, Any]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/limits/suppliers")
|
||
|
|
|
||
|
|
# Update subscription management methods
|
||
|
|
async def cancel_subscription(self, tenant_id: str, reason: str = "") -> Dict[str, Any]:
|
||
|
|
return await self.post(f"tenants/{tenant_id}/subscription/cancel", {"reason": reason})
|
||
|
|
|
||
|
|
async def reactivate_subscription(self, tenant_id: str, plan: str = "starter") -> Dict[str, Any]:
|
||
|
|
return await self.post(f"tenants/{tenant_id}/subscription/reactivate", {"plan": plan})
|
||
|
|
|
||
|
|
async def validate_plan_upgrade(self, tenant_id: str, new_plan: str) -> Dict[str, Any]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/validate-upgrade/{new_plan}")
|
||
|
|
|
||
|
|
async def upgrade_subscription_plan(self, tenant_id: str, new_plan: str) -> Dict[str, Any]:
|
||
|
|
return await self.post(f"tenants/{tenant_id}/subscription/upgrade", {"new_plan": new_plan})
|
||
|
|
|
||
|
|
# Update payment management methods
|
||
|
|
async def get_payment_method(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/payment-method")
|
||
|
|
|
||
|
|
async def update_payment_method(self, tenant_id: str, payment_method_id: str) -> Dict[str, Any]:
|
||
|
|
return await self.post(f"tenants/{tenant_id}/subscription/payment-method", {
|
||
|
|
"payment_method_id": payment_method_id
|
||
|
|
})
|
||
|
|
|
||
|
|
async def get_invoices(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||
|
|
return await self.get(f"tenants/{tenant_id}/subscription/invoices")
|
||
|
|
```
|
||
|
|
|
||
|
|
**Files to modify:**
|
||
|
|
- `shared/clients/tenant_client.py` - Update all subscription methods with new URL patterns
|
||
|
|
|
||
|
|
### Phase 3: Gateway Updates
|
||
|
|
|
||
|
|
#### Step 4: Update Gateway Routes
|
||
|
|
|
||
|
|
```python
|
||
|
|
# In gateway/app/routes/subscription.py
|
||
|
|
|
||
|
|
# Public endpoints
|
||
|
|
@router.get("/plans")
|
||
|
|
@router.get("/plans/{tier}")
|
||
|
|
@router.get("/plans/{tier}/features")
|
||
|
|
@router.get("/plans/{tier}/limits")
|
||
|
|
@router.get("/plans/compare")
|
||
|
|
|
||
|
|
# Registration flow
|
||
|
|
@router.post("/registration/payment-setup")
|
||
|
|
@router.post("/registration/complete")
|
||
|
|
@router.get("/registration/state/{state_id}")
|
||
|
|
|
||
|
|
# Tenant subscription endpoints
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/status")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/details")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/tier")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/limits")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/usage")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/features/{feature}")
|
||
|
|
@router.post("/tenants/{tenant_id}/subscription/cancel")
|
||
|
|
@router.post("/tenants/{tenant_id}/subscription/reactivate")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/validate-upgrade/{new_plan}")
|
||
|
|
@router.post("/tenants/{tenant_id}/subscription/upgrade")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/limits/locations")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/limits/products")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/limits/users")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/limits/recipes")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/limits/suppliers")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/payment-method")
|
||
|
|
@router.post("/tenants/{tenant_id}/subscription/payment-method")
|
||
|
|
@router.get("/tenants/{tenant_id}/subscription/invoices")
|
||
|
|
```
|
||
|
|
|
||
|
|
**Files to modify:**
|
||
|
|
- `gateway/app/routes/subscription.py` - Update all gateway routes with new patterns
|
||
|
|
|
||
|
|
#### Step 5: Update Middleware URL Patterns
|
||
|
|
|
||
|
|
```python
|
||
|
|
# In gateway/app/middleware/auth.py
|
||
|
|
async def _get_tenant_subscription_tier(self, tenant_id: str, request: Request) -> Optional[str]:
|
||
|
|
try:
|
||
|
|
# Updated URL pattern
|
||
|
|
response = await self._make_request(
|
||
|
|
"GET",
|
||
|
|
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/{tenant_id}/subscription/tier"
|
||
|
|
)
|
||
|
|
# ... rest of method remains the same
|
||
|
|
```
|
||
|
|
|
||
|
|
```python
|
||
|
|
# In gateway/app/middleware/read_only_mode.py
|
||
|
|
async def check_subscription_status(self, tenant_id: str, authorization: str) -> dict:
|
||
|
|
try:
|
||
|
|
# Updated URL pattern
|
||
|
|
response = await self._make_request(
|
||
|
|
"GET",
|
||
|
|
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/{tenant_id}/subscription/status"
|
||
|
|
)
|
||
|
|
# ... rest of method remains the same
|
||
|
|
```
|
||
|
|
|
||
|
|
**Files to modify:**
|
||
|
|
- `gateway/app/middleware/subscription.py` - Update URL patterns
|
||
|
|
- `gateway/app/middleware/auth.py` - Update subscription tier lookup URL
|
||
|
|
- `gateway/app/middleware/read_only_mode.py` - Update subscription status check URL
|
||
|
|
|
||
|
|
### Phase 4: Frontend Updates
|
||
|
|
|
||
|
|
#### Step 6: Update Frontend Subscription Service
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// In frontend/src/api/services/subscription.ts
|
||
|
|
export class SubscriptionService {
|
||
|
|
// Update all methods to use new URL patterns
|
||
|
|
|
||
|
|
async getSubscriptionStatus(tenantId: string): Promise<UsageSummary> {
|
||
|
|
return apiClient.get<UsageSummary>(`/tenants/${tenantId}/subscription/status`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getSubscriptionDetails(tenantId: string): Promise<any> {
|
||
|
|
return apiClient.get(`/tenants/${tenantId}/subscription/details`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getSubscriptionTier(tenantId: string): Promise<string> {
|
||
|
|
return apiClient.get(`/tenants/${tenantId}/subscription/tier`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getUsageSummary(tenantId: string): Promise<UsageSummary> {
|
||
|
|
return apiClient.get<UsageSummary>(`/tenants/${tenantId}/subscription/usage`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async checkFeatureAccess(tenantId: string, featureName: string): Promise<FeatureCheckResponse> {
|
||
|
|
return apiClient.get<FeatureCheckResponse>(`/tenants/${tenantId}/subscription/features/${featureName}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async cancelSubscription(tenantId: string, reason?: string): Promise<any> {
|
||
|
|
return apiClient.post(`/tenants/${tenantId}/subscription/cancel`, { reason });
|
||
|
|
}
|
||
|
|
|
||
|
|
async reactivateSubscription(tenantId: string, plan: string = 'starter'): Promise<any> {
|
||
|
|
return apiClient.post(`/tenants/${tenantId}/subscription/reactivate`, { plan });
|
||
|
|
}
|
||
|
|
|
||
|
|
async validatePlanUpgrade(tenantId: string, newPlan: string): Promise<PlanUpgradeValidation> {
|
||
|
|
return apiClient.get<PlanUpgradeValidation>(`/tenants/${tenantId}/subscription/validate-upgrade/${newPlan}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async upgradePlan(tenantId: string, newPlan: string): Promise<PlanUpgradeResult> {
|
||
|
|
return apiClient.post<PlanUpgradeResult>(`/tenants/${tenantId}/subscription/upgrade`, { new_plan: newPlan });
|
||
|
|
}
|
||
|
|
|
||
|
|
async canAddLocation(tenantId: string): Promise<any> {
|
||
|
|
return apiClient.get(`/tenants/${tenantId}/subscription/limits/locations`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async canAddProduct(tenantId: string): Promise<any> {
|
||
|
|
return apiClient.get(`/tenants/${tenantId}/subscription/limits/products`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async canAddUser(tenantId: string): Promise<any> {
|
||
|
|
return apiClient.get(`/tenants/${tenantId}/subscription/limits/users`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async canAddRecipe(tenantId: string): Promise<any> {
|
||
|
|
return apiClient.get(`/tenants/${tenantId}/subscription/limits/recipes`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async canAddSupplier(tenantId: string): Promise<any> {
|
||
|
|
return apiClient.get(`/tenants/${tenantId}/subscription/limits/suppliers`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getPaymentMethod(tenantId: string): Promise<any> {
|
||
|
|
return apiClient.get(`/tenants/${tenantId}/subscription/payment-method`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async updatePaymentMethod(tenantId: string, paymentMethodId: string): Promise<any> {
|
||
|
|
return apiClient.post(`/tenants/${tenantId}/subscription/payment-method`, { payment_method_id: paymentMethodId });
|
||
|
|
}
|
||
|
|
|
||
|
|
async getInvoices(tenantId: string): Promise<any[]> {
|
||
|
|
return apiClient.get(`/tenants/${tenantId}/subscription/invoices`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Registration flow methods
|
||
|
|
async startRegistrationPaymentSetup(userData: any): Promise<any> {
|
||
|
|
return apiClient.post('/registration/payment-setup', userData);
|
||
|
|
}
|
||
|
|
|
||
|
|
async completeRegistration(setupIntentId: string, userData: any): Promise<any> {
|
||
|
|
return apiClient.post('/registration/complete', {
|
||
|
|
setup_intent_id: setupIntentId,
|
||
|
|
user_data: userData
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Files to modify:**
|
||
|
|
- `frontend/src/api/services/subscription.ts` - Update all URL patterns
|
||
|
|
|
||
|
|
## Benefits of New Architecture
|
||
|
|
|
||
|
|
1. **Clear Separation of Concerns**: Subscription operations are properly separated from tenant management operations
|
||
|
|
2. **Consistent URL Patterns**: All subscription endpoints follow a logical, standardized structure
|
||
|
|
3. **Better Organization**: Easier to find and maintain subscription-related code
|
||
|
|
4. **Service-Based Architecture**: Maintains the constraint of using tenant client only
|
||
|
|
5. **Improved Maintainability**: Changes to subscription logic are localized to one API file
|
||
|
|
6. **Better Performance**: Clear caching strategies for subscription data
|
||
|
|
7. **Easier Scaling**: Subscription endpoints can be scaled independently if needed
|
||
|
|
8. **Cleaner Codebase**: No mixed concerns between tenant and subscription operations
|
||
|
|
|
||
|
|
## Implementation Summary
|
||
|
|
|
||
|
|
### Files to Modify
|
||
|
|
|
||
|
|
**Backend Services:**
|
||
|
|
- `services/tenant/app/api/subscription.py` - Add all subscription endpoints
|
||
|
|
- `services/tenant/app/api/tenant_operations.py` - Remove subscription endpoints
|
||
|
|
- `services/tenant/app/main.py` - Update router inclusion
|
||
|
|
|
||
|
|
**Shared Client Layer:**
|
||
|
|
- `shared/clients/tenant_client.py` - Update all subscription methods with new URL patterns
|
||
|
|
|
||
|
|
**Gateway Layer:**
|
||
|
|
- `gateway/app/routes/subscription.py` - Update all gateway routes with new patterns
|
||
|
|
- `gateway/app/middleware/subscription.py` - Update URL patterns
|
||
|
|
- `gateway/app/middleware/auth.py` - Update subscription tier lookup URL
|
||
|
|
- `gateway/app/middleware/read_only_mode.py` - Update subscription status check URL
|
||
|
|
|
||
|
|
**Frontend Layer:**
|
||
|
|
- `frontend/src/api/services/subscription.ts` - Update all URL patterns
|
||
|
|
|
||
|
|
### Implementation Order
|
||
|
|
|
||
|
|
1. **Backend API Reorganization** (Phase 1)
|
||
|
|
2. **Update Tenant Client** (Phase 2)
|
||
|
|
3. **Gateway Updates** (Phase 3)
|
||
|
|
4. **Frontend Updates** (Phase 4)
|
||
|
|
|
||
|
|
This comprehensive redesign creates a clean, modern subscription system with proper separation of concerns while respecting the existing service-based client architecture constraint.
|