309 lines
11 KiB
Python
309 lines
11 KiB
Python
# 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
|
|
)
|