14 KiB
Quick Start: Implementing Remaining Service Deletions
Overview
Time to complete per service: 30-45 minutes Remaining services: 3 (POS, External, Alert Processor) Pattern: Copy → Customize → Test
Step-by-Step Template
1. Create Deletion Service File
Location: services/{service}/app/services/tenant_deletion_service.py
Template:
"""
{Service} Service - Tenant Data Deletion
Handles deletion of all {service}-related data for a tenant
"""
from typing import Dict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete, func
import structlog
from shared.services.tenant_deletion import BaseTenantDataDeletionService, TenantDataDeletionResult
logger = structlog.get_logger()
class {Service}TenantDeletionService(BaseTenantDataDeletionService):
"""Service for deleting all {service}-related data for a tenant"""
def __init__(self, db_session: AsyncSession):
super().__init__("{service}-service")
self.db = db_session
async def get_tenant_data_preview(self, tenant_id: str) -> Dict[str, int]:
"""Get counts of what would be deleted"""
try:
preview = {}
# Import models here to avoid circular imports
from app.models.{model_file} import Model1, Model2
# Count each model type
count1 = await self.db.scalar(
select(func.count(Model1.id)).where(Model1.tenant_id == tenant_id)
)
preview["model1_plural"] = count1 or 0
# Repeat for each model...
return preview
except Exception as e:
logger.error("Error getting deletion preview",
tenant_id=tenant_id,
error=str(e))
return {}
async def delete_tenant_data(self, tenant_id: str) -> TenantDataDeletionResult:
"""Delete all data for a tenant"""
result = TenantDataDeletionResult(tenant_id, self.service_name)
try:
# Import models here
from app.models.{model_file} import Model1, Model2
# Delete in reverse dependency order (children first, then parents)
# Child models first
try:
child_delete = await self.db.execute(
delete(ChildModel).where(ChildModel.tenant_id == tenant_id)
)
result.add_deleted_items("child_models", child_delete.rowcount)
except Exception as e:
logger.error("Error deleting child models",
tenant_id=tenant_id,
error=str(e))
result.add_error(f"Child model deletion: {str(e)}")
# Parent models last
try:
parent_delete = await self.db.execute(
delete(ParentModel).where(ParentModel.tenant_id == tenant_id)
)
result.add_deleted_items("parent_models", parent_delete.rowcount)
logger.info("Deleted parent models for tenant",
tenant_id=tenant_id,
count=parent_delete.rowcount)
except Exception as e:
logger.error("Error deleting parent models",
tenant_id=tenant_id,
error=str(e))
result.add_error(f"Parent model deletion: {str(e)}")
# Commit all deletions
await self.db.commit()
logger.info("Tenant data deletion completed",
tenant_id=tenant_id,
deleted_counts=result.deleted_counts)
except Exception as e:
logger.error("Fatal error during tenant data deletion",
tenant_id=tenant_id,
error=str(e))
await self.db.rollback()
result.add_error(f"Fatal error: {str(e)}")
return result
2. Add API Endpoints
Location: services/{service}/app/api/{main_router}.py
Add at end of file:
# ===== Tenant Data Deletion Endpoints =====
@router.delete("/tenant/{tenant_id}")
async def delete_tenant_data(
tenant_id: str,
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Delete all {service}-related data for a tenant
Only accessible by internal services (called during tenant deletion)
"""
logger.info(f"Tenant data deletion request received for tenant: {tenant_id}")
# Only allow internal service calls
if current_user.get("type") != "service":
raise HTTPException(
status_code=403,
detail="This endpoint is only accessible to internal services"
)
try:
from app.services.tenant_deletion_service import {Service}TenantDeletionService
deletion_service = {Service}TenantDeletionService(db)
result = await deletion_service.safe_delete_tenant_data(tenant_id)
return {
"message": "Tenant data deletion completed in {service}-service",
"summary": result.to_dict()
}
except Exception as e:
logger.error(f"Tenant data deletion failed for {tenant_id}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to delete tenant data: {str(e)}"
)
@router.get("/tenant/{tenant_id}/deletion-preview")
async def preview_tenant_data_deletion(
tenant_id: str,
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Preview what data would be deleted for a tenant (dry-run)
Accessible by internal services and tenant admins
"""
# Allow internal services and admins
is_service = current_user.get("type") == "service"
is_admin = current_user.get("role") in ["owner", "admin"]
if not (is_service or is_admin):
raise HTTPException(
status_code=403,
detail="Insufficient permissions"
)
try:
from app.services.tenant_deletion_service import {Service}TenantDeletionService
deletion_service = {Service}TenantDeletionService(db)
preview = await deletion_service.get_tenant_data_preview(tenant_id)
return {
"tenant_id": tenant_id,
"service": "{service}-service",
"data_counts": preview,
"total_items": sum(preview.values())
}
except Exception as e:
logger.error(f"Deletion preview failed for {tenant_id}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to get deletion preview: {str(e)}"
)
Remaining Services
1. POS Service
Models to delete:
- POSConfiguration
- POSTransaction
- POSSession
- POSDevice (if exists)
Deletion order:
- POSTransaction (child)
- POSSession (child)
- POSDevice (if exists)
- POSConfiguration (parent)
Estimated time: 30 minutes
2. External Service
Models to delete:
- ExternalDataCache
- APIKeyUsage
- ExternalAPILog (if exists)
Deletion order:
- ExternalAPILog (if exists)
- APIKeyUsage
- ExternalDataCache
Estimated time: 30 minutes
3. Alert Processor Service
Models to delete:
- Alert
- AlertRule
- AlertHistory
- AlertNotification (if exists)
Deletion order:
- AlertNotification (if exists, child)
- AlertHistory (child)
- Alert (child of AlertRule)
- AlertRule (parent)
Estimated time: 30 minutes
Testing Checklist
Manual Testing (for each service):
# 1. Start the service
docker-compose up {service}-service
# 2. Test deletion preview (should return counts)
curl -X GET "http://localhost:8000/api/v1/{service}/tenant/{tenant_id}/deletion-preview" \
-H "Authorization: Bearer {token}" \
-H "X-Internal-Service: auth-service"
# 3. Test actual deletion
curl -X DELETE "http://localhost:8000/api/v1/{service}/tenant/{tenant_id}" \
-H "Authorization: Bearer {token}" \
-H "X-Internal-Service: auth-service"
# 4. Verify data is deleted
# Check database: SELECT COUNT(*) FROM {table} WHERE tenant_id = '{tenant_id}';
# Should return 0 for all tables
Integration Testing:
# Test via orchestrator
from services.auth.app.services.deletion_orchestrator import DeletionOrchestrator
orchestrator = DeletionOrchestrator()
job = await orchestrator.orchestrate_tenant_deletion(
tenant_id="test-tenant-123",
tenant_name="Test Bakery"
)
# Check results
print(job.to_dict())
# Should show:
# - services_completed: 12/12
# - services_failed: 0
# - total_items_deleted: > 0
Common Patterns
Pattern 1: Simple Service (1-2 models)
Example: Sales, External
# Just delete the main model(s)
sales_delete = await self.db.execute(
delete(SalesData).where(SalesData.tenant_id == tenant_id)
)
result.add_deleted_items("sales_records", sales_delete.rowcount)
Pattern 2: Parent-Child (CASCADE)
Example: Orders, Recipes
# Delete parent, CASCADE handles children
order_delete = await self.db.execute(
delete(Order).where(Order.tenant_id == tenant_id)
)
# order_items, order_status_history deleted via CASCADE
result.add_deleted_items("orders", order_delete.rowcount)
result.add_deleted_items("order_items", preview["order_items"]) # From preview
Pattern 3: Multiple Independent Models
Example: Inventory, Production
# Delete each independently
for Model in [InventoryItem, InventoryTransaction, StockAlert]:
try:
deleted = await self.db.execute(
delete(Model).where(Model.tenant_id == tenant_id)
)
result.add_deleted_items(model_name, deleted.rowcount)
except Exception as e:
result.add_error(f"{model_name}: {str(e)}")
Pattern 4: Complex Dependencies
Example: Suppliers
# Delete in specific order
# 1. Children first
poi_delete = await self.db.execute(
delete(PurchaseOrderItem)
.where(PurchaseOrderItem.purchase_order_id.in_(
select(PurchaseOrder.id).where(PurchaseOrder.tenant_id == tenant_id)
))
)
# 2. Then intermediate
po_delete = await self.db.execute(
delete(PurchaseOrder).where(PurchaseOrder.tenant_id == tenant_id)
)
# 3. Finally parent
supplier_delete = await self.db.execute(
delete(Supplier).where(Supplier.tenant_id == tenant_id)
)
Troubleshooting
Issue: "ModuleNotFoundError: No module named 'shared.services.tenant_deletion'"
Solution: Ensure shared module is in PYTHONPATH:
# Add to service's __init__.py or main.py
import sys
sys.path.insert(0, "/path/to/services/shared")
Issue: "Table doesn't exist"
Solution: Wrap in try-except:
try:
count = await self.db.scalar(select(func.count(Model.id))...)
preview["models"] = count or 0
except Exception:
preview["models"] = 0 # Table doesn't exist, ignore
Issue: "Foreign key constraint violation"
Solution: Delete in correct order (children before parents):
# Wrong order:
await delete(Parent).where(...) # Fails!
await delete(Child).where(...)
# Correct order:
await delete(Child).where(...)
await delete(Parent).where(...) # Success!
Issue: "Service timeout"
Solution: Increase timeout in orchestrator or implement chunked deletion:
# In deletion_orchestrator.py, change:
async with httpx.AsyncClient(timeout=60.0) as client:
# To:
async with httpx.AsyncClient(timeout=300.0) as client: # 5 minutes
Performance Tips
1. Batch Deletes for Large Datasets
# Instead of:
for item in items:
await self.db.delete(item)
# Use:
await self.db.execute(
delete(Model).where(Model.tenant_id == tenant_id)
)
2. Use Indexes
Ensure tenant_id has an index on all tables:
CREATE INDEX idx_{table}_tenant_id ON {table}(tenant_id);
3. Disable Triggers Temporarily (for very large deletes)
await self.db.execute(text("SET session_replication_role = replica"))
# ... do deletions ...
await self.db.execute(text("SET session_replication_role = DEFAULT"))
Completion Checklist
- POS Service deletion service created
- POS Service API endpoints added
- POS Service manually tested
- External Service deletion service created
- External Service API endpoints added
- External Service manually tested
- Alert Processor deletion service created
- Alert Processor API endpoints added
- Alert Processor manually tested
- All services tested via orchestrator
- Load testing completed
- Documentation updated
Next Steps After Completion
- Update DeletionOrchestrator - Verify all endpoint URLs are correct
- Integration Testing - Test complete tenant deletion end-to-end
- Performance Testing - Test with large datasets
- Monitoring Setup - Add Prometheus metrics
- Production Deployment - Deploy with feature flag
Total estimated time for all 3 services: 1.5-2 hours
Quick Reference: Completed Services
| Service | Status | Files | Lines |
|---|---|---|---|
| Tenant | ✅ | 2 API files + 1 service | 641 |
| Orders | ✅ | tenant_deletion_service.py + endpoints | 225 |
| Inventory | ✅ | tenant_deletion_service.py | 110 |
| Recipes | ✅ | tenant_deletion_service.py + endpoints | 217 |
| Sales | ✅ | tenant_deletion_service.py | 85 |
| Production | ✅ | tenant_deletion_service.py | 171 |
| Suppliers | ✅ | tenant_deletion_service.py | 195 |
| POS | ⏳ | - | - |
| External | ⏳ | - | - |
| Alert Processor | ⏳ | - | - |
| Forecasting | 🔄 | Needs refactor | - |
| Training | 🔄 | Needs refactor | - |
| Notification | 🔄 | Needs refactor | - |
Legend:
- ✅ Complete
- ⏳ Pending
- 🔄 Needs refactoring to standard pattern