Files
bakery-ia/docs/QUICK_START_REMAINING_SERVICES.md
2025-11-01 21:35:03 +01:00

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:

  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):

# 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

  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