From 95cd70af1f158e5938619a7fa2c262d43e56d1da Mon Sep 17 00:00:00 2001 From: Claude AI Date: Thu, 29 Jan 2026 22:26:18 +0000 Subject: [PATCH] feat(integrations): add contact sync service Add bidirectional contact synchronization between WhatsApp Central and Odoo, including sync endpoints and ContactSyncService. Co-Authored-By: Claude Opus 4.5 --- services/integrations/app/main.py | 3 +- services/integrations/app/routers/__init__.py | 3 +- services/integrations/app/routers/sync.py | 48 ++++++++++ .../integrations/app/services/__init__.py | 3 +- services/integrations/app/services/sync.py | 87 +++++++++++++++++++ 5 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 services/integrations/app/routers/sync.py create mode 100644 services/integrations/app/services/sync.py diff --git a/services/integrations/app/main.py b/services/integrations/app/main.py index 43913b6..225f981 100644 --- a/services/integrations/app/main.py +++ b/services/integrations/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.routers import odoo_router +from app.routers import odoo_router, sync_router app = FastAPI(title="WhatsApp Central - Integrations Service") @@ -13,6 +13,7 @@ app.add_middleware( ) app.include_router(odoo_router) +app.include_router(sync_router) @app.get("/health") diff --git a/services/integrations/app/routers/__init__.py b/services/integrations/app/routers/__init__.py index 3bda305..8d7a2d6 100644 --- a/services/integrations/app/routers/__init__.py +++ b/services/integrations/app/routers/__init__.py @@ -1,3 +1,4 @@ from app.routers.odoo import router as odoo_router +from app.routers.sync import router as sync_router -__all__ = ["odoo_router"] +__all__ = ["odoo_router", "sync_router"] diff --git a/services/integrations/app/routers/sync.py b/services/integrations/app/routers/sync.py new file mode 100644 index 0000000..8540537 --- /dev/null +++ b/services/integrations/app/routers/sync.py @@ -0,0 +1,48 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import Optional +from app.services.sync import ContactSyncService + +router = APIRouter(prefix="/api/sync", tags=["sync"]) + + +class SyncContactRequest(BaseModel): + contact_id: str + phone: str + name: Optional[str] = None + email: Optional[str] = None + + +@router.post("/contact-to-odoo") +async def sync_contact_to_odoo(request: SyncContactRequest): + """Sync WhatsApp contact to Odoo partner""" + try: + service = ContactSyncService() + partner_id = await service.sync_contact_to_odoo( + contact_id=request.contact_id, + phone=request.phone, + name=request.name, + email=request.email, + ) + + if partner_id: + return {"success": True, "odoo_partner_id": partner_id} + raise HTTPException(500, "Failed to sync contact") + except Exception as e: + raise HTTPException(500, str(e)) + + +@router.post("/partner-to-contact/{partner_id}") +async def sync_partner_to_contact(partner_id: int): + """Sync Odoo partner to WhatsApp contact""" + try: + service = ContactSyncService() + contact_id = await service.sync_partner_to_contact(partner_id) + + return { + "success": True, + "contact_id": contact_id, + "message": "Contact found" if contact_id else "No matching contact", + } + except Exception as e: + raise HTTPException(500, str(e)) diff --git a/services/integrations/app/services/__init__.py b/services/integrations/app/services/__init__.py index 4d06506..3612c9b 100644 --- a/services/integrations/app/services/__init__.py +++ b/services/integrations/app/services/__init__.py @@ -1,5 +1,6 @@ from app.services.partner import PartnerService from app.services.crm import CRMService from app.services.product import ProductService +from app.services.sync import ContactSyncService -__all__ = ["PartnerService", "CRMService", "ProductService"] +__all__ = ["PartnerService", "CRMService", "ProductService", "ContactSyncService"] diff --git a/services/integrations/app/services/sync.py b/services/integrations/app/services/sync.py new file mode 100644 index 0000000..7eebb15 --- /dev/null +++ b/services/integrations/app/services/sync.py @@ -0,0 +1,87 @@ +from typing import Optional +import httpx +from app.config import get_settings +from app.services.partner import PartnerService +from app.schemas.partner import PartnerCreate + +settings = get_settings() + + +class ContactSyncService: + """Sync contacts between WhatsApp Central and Odoo""" + + def __init__(self): + self.partner_service = PartnerService() + + async def sync_contact_to_odoo( + self, + contact_id: str, + phone: str, + name: str = None, + email: str = None, + ) -> Optional[int]: + """ + Sync a WhatsApp contact to Odoo. + Returns Odoo partner_id. + """ + # Check if partner exists + partner = self.partner_service.search_by_phone(phone) + + if partner: + return partner.id + + # Create new partner + data = PartnerCreate( + name=name or phone, + mobile=phone, + email=email, + ) + partner_id = self.partner_service.create(data) + + # Update contact in API Gateway with odoo_partner_id + await self._update_contact_odoo_id(contact_id, partner_id) + + return partner_id + + async def _update_contact_odoo_id(self, contact_id: str, odoo_id: int): + """Update contact's odoo_partner_id in API Gateway""" + try: + async with httpx.AsyncClient() as client: + await client.patch( + f"{settings.API_GATEWAY_URL}/api/internal/contacts/{contact_id}", + json={"odoo_partner_id": odoo_id}, + timeout=10, + ) + except Exception as e: + print(f"Failed to update contact odoo_id: {e}") + + async def sync_partner_to_contact(self, partner_id: int) -> Optional[str]: + """ + Sync Odoo partner to WhatsApp contact. + Returns contact_id if found. + """ + partner = self.partner_service.get_by_id(partner_id) + + if not partner.phone and not partner.mobile: + return None + + phone = partner.mobile or partner.phone + + # Search contact in API Gateway + try: + async with httpx.AsyncClient() as client: + response = await client.get( + f"{settings.API_GATEWAY_URL}/api/internal/contacts/search", + params={"phone": phone}, + timeout=10, + ) + + if response.status_code == 200: + contact = response.json() + if not contact.get("odoo_partner_id"): + await self._update_contact_odoo_id(contact["id"], partner_id) + return contact["id"] + except Exception as e: + print(f"Failed to search contact: {e}") + + return None