# services/tenant/app/api/whatsapp_admin.py """ WhatsApp Admin API Endpoints Admin-only endpoints for managing WhatsApp phone number assignments """ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from uuid import UUID from typing import List, Optional from pydantic import BaseModel, Field import httpx import os from app.core.database import get_db from app.models.tenant_settings import TenantSettings from app.models.tenants import Tenant router = APIRouter() # ================================================================ # SCHEMAS # ================================================================ class WhatsAppPhoneNumberInfo(BaseModel): """Information about a WhatsApp phone number from Meta API""" id: str = Field(..., description="Phone Number ID") display_phone_number: str = Field(..., description="Display phone number (e.g., +34 612 345 678)") verified_name: str = Field(..., description="Verified business name") quality_rating: str = Field(..., description="Quality rating (GREEN, YELLOW, RED)") class TenantWhatsAppStatus(BaseModel): """WhatsApp status for a tenant""" tenant_id: UUID tenant_name: str whatsapp_enabled: bool phone_number_id: Optional[str] = None display_phone_number: Optional[str] = None class AssignPhoneNumberRequest(BaseModel): """Request to assign phone number to tenant""" phone_number_id: str = Field(..., description="Meta WhatsApp Phone Number ID") display_phone_number: str = Field(..., description="Display format (e.g., '+34 612 345 678')") class AssignPhoneNumberResponse(BaseModel): """Response after assigning phone number""" success: bool message: str tenant_id: UUID phone_number_id: str display_phone_number: str # ================================================================ # ENDPOINTS # ================================================================ @router.get( "/admin/whatsapp/phone-numbers", response_model=List[WhatsAppPhoneNumberInfo], summary="List available WhatsApp phone numbers", description="Get all phone numbers available in the master WhatsApp Business Account" ) async def list_available_phone_numbers(): """ List all phone numbers from the master WhatsApp Business Account Requires: - WHATSAPP_BUSINESS_ACCOUNT_ID environment variable - WHATSAPP_ACCESS_TOKEN environment variable Returns list of available phone numbers with their status """ business_account_id = os.getenv("WHATSAPP_BUSINESS_ACCOUNT_ID") access_token = os.getenv("WHATSAPP_ACCESS_TOKEN") api_version = os.getenv("WHATSAPP_API_VERSION", "v18.0") if not business_account_id or not access_token: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="WhatsApp master account not configured. Set WHATSAPP_BUSINESS_ACCOUNT_ID and WHATSAPP_ACCESS_TOKEN environment variables." ) try: # Fetch phone numbers from Meta Graph API async with httpx.AsyncClient(timeout=30.0) as client: response = await client.get( f"https://graph.facebook.com/{api_version}/{business_account_id}/phone_numbers", headers={"Authorization": f"Bearer {access_token}"}, params={ "fields": "id,display_phone_number,verified_name,quality_rating" } ) if response.status_code != 200: error_data = response.json() raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Meta API error: {error_data.get('error', {}).get('message', 'Unknown error')}" ) data = response.json() phone_numbers = data.get("data", []) return [ WhatsAppPhoneNumberInfo( id=phone.get("id"), display_phone_number=phone.get("display_phone_number"), verified_name=phone.get("verified_name", ""), quality_rating=phone.get("quality_rating", "UNKNOWN") ) for phone in phone_numbers ] except httpx.HTTPError as e: raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Failed to fetch phone numbers from Meta: {str(e)}" ) @router.get( "/admin/whatsapp/tenants", response_model=List[TenantWhatsAppStatus], summary="List all tenants with WhatsApp status", description="Get WhatsApp configuration status for all tenants" ) async def list_tenant_whatsapp_status( db: AsyncSession = Depends(get_db) ): """ List all tenants with their WhatsApp configuration status Returns: - tenant_id: Tenant UUID - tenant_name: Tenant name - whatsapp_enabled: Whether WhatsApp is enabled - phone_number_id: Assigned phone number ID (if any) - display_phone_number: Display format (if any) """ # Query all tenants with their settings query = select(Tenant, TenantSettings).outerjoin( TenantSettings, Tenant.id == TenantSettings.tenant_id ) result = await db.execute(query) rows = result.all() tenant_statuses = [] for tenant, settings in rows: notification_settings = settings.notification_settings if settings else {} tenant_statuses.append( TenantWhatsAppStatus( tenant_id=tenant.id, tenant_name=tenant.name, whatsapp_enabled=notification_settings.get("whatsapp_enabled", False), phone_number_id=notification_settings.get("whatsapp_phone_number_id", ""), display_phone_number=notification_settings.get("whatsapp_display_phone_number", "") ) ) return tenant_statuses @router.post( "/admin/whatsapp/tenants/{tenant_id}/assign-phone", response_model=AssignPhoneNumberResponse, summary="Assign phone number to tenant", description="Assign a WhatsApp phone number from the master account to a tenant" ) async def assign_phone_number_to_tenant( tenant_id: UUID, request: AssignPhoneNumberRequest, db: AsyncSession = Depends(get_db) ): """ Assign a WhatsApp phone number to a tenant - **tenant_id**: UUID of the tenant - **phone_number_id**: Meta Phone Number ID from master account - **display_phone_number**: Human-readable format (e.g., "+34 612 345 678") This will: 1. Validate the tenant exists 2. Check if phone number is already assigned to another tenant 3. Update tenant's notification settings 4. Enable WhatsApp for the tenant """ # Verify tenant exists tenant_query = select(Tenant).where(Tenant.id == tenant_id) tenant_result = await db.execute(tenant_query) tenant = tenant_result.scalar_one_or_none() if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Tenant {tenant_id} not found" ) # Check if phone number is already assigned to another tenant settings_query = select(TenantSettings).where(TenantSettings.tenant_id != tenant_id) settings_result = await db.execute(settings_query) all_settings = settings_result.scalars().all() for settings in all_settings: notification_settings = settings.notification_settings or {} if notification_settings.get("whatsapp_phone_number_id") == request.phone_number_id: # Get the other tenant's name other_tenant_query = select(Tenant).where(Tenant.id == settings.tenant_id) other_tenant_result = await db.execute(other_tenant_query) other_tenant = other_tenant_result.scalar_one_or_none() raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Phone number {request.display_phone_number} is already assigned to tenant '{other_tenant.name if other_tenant else 'Unknown'}'" ) # Get or create tenant settings settings_query = select(TenantSettings).where(TenantSettings.tenant_id == tenant_id) settings_result = await db.execute(settings_query) settings = settings_result.scalar_one_or_none() if not settings: # Create default settings settings = TenantSettings( tenant_id=tenant_id, **TenantSettings.get_default_settings() ) db.add(settings) # Update notification settings notification_settings = settings.notification_settings or {} notification_settings["whatsapp_enabled"] = True notification_settings["whatsapp_phone_number_id"] = request.phone_number_id notification_settings["whatsapp_display_phone_number"] = request.display_phone_number settings.notification_settings = notification_settings await db.commit() await db.refresh(settings) return AssignPhoneNumberResponse( success=True, message=f"Phone number {request.display_phone_number} assigned to tenant '{tenant.name}'", tenant_id=tenant_id, phone_number_id=request.phone_number_id, display_phone_number=request.display_phone_number ) @router.delete( "/admin/whatsapp/tenants/{tenant_id}/unassign-phone", response_model=AssignPhoneNumberResponse, summary="Unassign phone number from tenant", description="Remove WhatsApp phone number assignment from a tenant" ) async def unassign_phone_number_from_tenant( tenant_id: UUID, db: AsyncSession = Depends(get_db) ): """ Unassign WhatsApp phone number from a tenant - **tenant_id**: UUID of the tenant This will: 1. Clear the phone number assignment 2. Disable WhatsApp for the tenant """ # Get tenant settings settings_query = select(TenantSettings).where(TenantSettings.tenant_id == tenant_id) settings_result = await db.execute(settings_query) settings = settings_result.scalar_one_or_none() if not settings: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Settings not found for tenant {tenant_id}" ) # Get current values for response notification_settings = settings.notification_settings or {} old_phone_id = notification_settings.get("whatsapp_phone_number_id", "") old_display_phone = notification_settings.get("whatsapp_display_phone_number", "") # Update notification settings notification_settings["whatsapp_enabled"] = False notification_settings["whatsapp_phone_number_id"] = "" notification_settings["whatsapp_display_phone_number"] = "" settings.notification_settings = notification_settings await db.commit() return AssignPhoneNumberResponse( success=True, message=f"Phone number unassigned from tenant", tenant_id=tenant_id, phone_number_id=old_phone_id, display_phone_number=old_display_phone )