Improve backend
This commit is contained in:
308
services/tenant/app/api/whatsapp_admin.py
Normal file
308
services/tenant/app/api/whatsapp_admin.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# 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
|
||||
)
|
||||
Reference in New Issue
Block a user