# 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:** ```python """ {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:** ```python # ===== 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:** 1. POSTransaction (child) 2. POSSession (child) 3. POSDevice (if exists) 4. POSConfiguration (parent) **Estimated time:** 30 minutes ### 2. External Service **Models to delete:** - ExternalDataCache - APIKeyUsage - ExternalAPILog (if exists) **Deletion order:** 1. ExternalAPILog (if exists) 2. APIKeyUsage 3. ExternalDataCache **Estimated time:** 30 minutes ### 3. Alert Processor Service **Models to delete:** - Alert - AlertRule - AlertHistory - AlertNotification (if exists) **Deletion order:** 1. AlertNotification (if exists, child) 2. AlertHistory (child) 3. Alert (child of AlertRule) 4. AlertRule (parent) **Estimated time:** 30 minutes --- ## Testing Checklist ### Manual Testing (for each service): ```bash # 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: ```python # 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 ```python # 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 ```python # 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 ```python # 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 ```python # 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: ```python # 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: ```python 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): ```python # 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: ```python # 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 ```python # 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: ```sql CREATE INDEX idx_{table}_tenant_id ON {table}(tenant_id); ``` ### 3. Disable Triggers Temporarily (for very large deletes) ```python 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 1. **Update DeletionOrchestrator** - Verify all endpoint URLs are correct 2. **Integration Testing** - Test complete tenant deletion end-to-end 3. **Performance Testing** - Test with large datasets 4. **Monitoring Setup** - Add Prometheus metrics 5. **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