diff --git a/backend/app/whatsapp_onboarding.py b/backend/app/whatsapp_onboarding.py index fdae147..d1cc396 100644 --- a/backend/app/whatsapp_onboarding.py +++ b/backend/app/whatsapp_onboarding.py @@ -169,24 +169,31 @@ async def _debug_token(db: Session, token: str) -> dict[str, Any]: return payload if isinstance(payload, dict) else {} -def _waba_ids_from_debug(debug: dict[str, Any]) -> list[str]: +def _target_ids(debug: dict[str, Any], scope: str) -> list[str]: found: list[str] = [] scopes = debug.get("granular_scopes") if not isinstance(scopes, list): return found - preferred = {"whatsapp_business_management", "whatsapp_business_messaging"} for item in scopes: - if not isinstance(item, dict): - continue - if item.get("scope") not in preferred: + if not isinstance(item, dict) or item.get("scope") != scope: continue for target in item.get("target_ids") or []: - value = str(target) + value = str(target).strip() if value and value not in found: found.append(value) return found +def _waba_ids_from_debug(debug: dict[str, Any]) -> list[str]: + """whatsapp_business_management targets are WABA IDs.""" + return _target_ids(debug, "whatsapp_business_management") + + +def _phone_ids_from_debug(debug: dict[str, Any]) -> list[str]: + """whatsapp_business_messaging targets are phone number IDs, not WABA IDs.""" + return _target_ids(debug, "whatsapp_business_messaging") + + def _as_uuid(account_id: Any) -> uuid.UUID: try: return account_id if isinstance(account_id, uuid.UUID) else uuid.UUID(str(account_id)) @@ -599,7 +606,15 @@ async def complete_signup(db: Session, user: UserAccount, payload: dict[str, Any } session_waba = str(payload.get("waba_id") or "").strip() - waba_ids = [session_waba] if session_waba else _waba_ids_from_debug(debug) + session_phone = str(payload.get("phone_number_id") or "").strip() or None + management_ids = _waba_ids_from_debug(debug) + phone_ids = _phone_ids_from_debug(debug) + if session_waba and session_waba in phone_ids and session_waba not in management_ids: + session_phone = session_phone or session_waba + session_waba = "" + waba_ids = [session_waba] if session_waba else management_ids + if not session_phone and len(phone_ids) == 1: + session_phone = phone_ids[0] if not waba_ids: _audit(db, user, "provision_failed", "Embedded Signup did not return a WhatsApp Business Account") db.commit() @@ -622,7 +637,6 @@ async def complete_signup(db: Session, user: UserAccount, payload: dict[str, Any db.flush() _audit(db, user, "authorization_received", "Embedded Signup returned an authorization code", connection_id=connection.id) - session_phone = str(payload.get("phone_number_id") or "").strip() or None accounts: list[WhatsAppBusinessAccount] = [] for waba_id in waba_ids: accounts.append(await _provision_waba(db, user, connection, token, waba_id, session_phone)) diff --git a/backend/app/whatsapp_template_service.py b/backend/app/whatsapp_template_service.py index c233fdc..5fda231 100644 --- a/backend/app/whatsapp_template_service.py +++ b/backend/app/whatsapp_template_service.py @@ -12,7 +12,19 @@ from sqlalchemy.orm import Session, selectinload from .config import settings from .models import UserAccount, WhatsAppBusinessAccount, WhatsAppMessage, WhatsAppMessageTemplate, WhatsAppMetaConnection -from .whatsapp_onboarding import _graph, _graph_error_message, _owned_account, _quality_label, _token_app_id +from .whatsapp_onboarding import ( + _apply_phones, + _debug_token, + _graph, + _graph_error_message, + _load_phones, + _load_waba, + _owned_account, + _phone_ids_from_debug, + _quality_label, + _token_app_id, + _waba_ids_from_debug, +) _NAME = re.compile(r"^[a-z0-9_]{1,512}$") _LANG = re.compile(r"^[a-z]{2}(_[A-Z]{2})?$") @@ -510,6 +522,62 @@ async def _pull_templates(db: Session, account: WhatsAppBusinessAccount, token: return seen +def _wrong_template_node(exc: HTTPException) -> bool: + detail = str(exc.detail).lower() + return "message_templates" in detail and "nonexisting field" in detail + + +async def _correct_waba_id(db: Session, account: WhatsAppBusinessAccount, token: str) -> bool: + """Replace a stored phone-number ID with the WABA ID from the embedded-signup token.""" + try: + debug = await _debug_token(db, token) + except HTTPException: + return False + wabas = _waba_ids_from_debug(debug) + phone_ids = set(_phone_ids_from_debug(debug)) + stored = str(account.waba_id) + if not wabas or stored in wabas: + return False + real = wabas[0] if len(wabas) == 1 else None + if real is None: + for waba in wabas: + try: + rows = await _load_phones(token, waba) + except HTTPException: + continue + ids = {str(row.get("id")) for row in rows if row.get("id")} + if stored in ids or phone_ids.intersection(ids): + real = waba + break + if not real or real == stored: + return False + taken = db.scalar( + select(WhatsAppBusinessAccount.id).where( + WhatsAppBusinessAccount.waba_id == real, + WhatsAppBusinessAccount.id != account.id, + ) + ) + if taken is not None: + return False + account.waba_id = real + try: + info = await _load_waba(token, real) + name = str(info.get("name") or "").strip() + if name: + account.name = name[:200] + except HTTPException: + pass + user = db.get(UserAccount, account.user_id) + if user is not None: + try: + rows = await _load_phones(token, real) + _apply_phones(db, user, account, rows, None) + except HTTPException: + pass + db.flush() + return True + + async def sync_account_templates(db: Session, account: WhatsAppBusinessAccount) -> str | None: tokens = _candidate_tokens(db, account) if not tokens: @@ -519,7 +587,13 @@ async def sync_account_templates(db: Session, account: WhatsAppBusinessAccount) last_error: str | None = None for index, token in enumerate(tokens): try: - seen = await _pull_templates(db, account, token) + try: + seen = await _pull_templates(db, account, token) + except HTTPException as exc: + db.rollback() + if not _wrong_template_node(exc) or not await _correct_waba_id(db, account, token): + raise + seen = await _pull_templates(db, account, token) except HTTPException as exc: db.rollback() last_error = str(exc.detail) diff --git a/frontend/src/features/whatsapp/WhatsAppSetupPage.tsx b/frontend/src/features/whatsapp/WhatsAppSetupPage.tsx index c3a9661..57e2350 100644 --- a/frontend/src/features/whatsapp/WhatsAppSetupPage.tsx +++ b/frontend/src/features/whatsapp/WhatsAppSetupPage.tsx @@ -112,11 +112,16 @@ function parseSignupMessage(raw: unknown): SignupSession | null { const message = data as { type?: string; event?: string; data?: Record } if (message.type !== 'WA_EMBEDDED_SIGNUP' || !message.event) return null const body = message.data || {} + const metaId = (value: unknown) => { + if (typeof value === 'string' && value.trim()) return value.trim() + if (typeof value === 'number' && Number.isFinite(value)) return String(Math.trunc(value)) + return undefined + } return { event: message.event, - wabaId: typeof body.waba_id === 'string' ? body.waba_id : undefined, - phoneNumberId: typeof body.phone_number_id === 'string' ? body.phone_number_id : undefined, - businessId: typeof body.business_id === 'string' ? body.business_id : undefined, + wabaId: metaId(body.waba_id), + phoneNumberId: metaId(body.phone_number_id), + businessId: metaId(body.business_id), errorMessage: typeof body.error_message === 'string' ? body.error_message : undefined, currentStep: typeof body.current_step === 'string' ? body.current_step : undefined, } @@ -311,7 +316,7 @@ export function WhatsAppSetupPage() { config_id: config.config_id, response_type: 'code', override_default_response_type: true, - extras: {}, + extras: { sessionInfoVersion: 3 }, }, ) } catch (reason) {