Seth commited on
Commit
4312115
·
1 Parent(s): c94d5d9
backend/app/auth_routes.py CHANGED
@@ -81,11 +81,10 @@ async def firebase_login(
81
  db.refresh(user)
82
  print(f"[INFO] New user created via Firebase: {email}")
83
 
84
- # Enrich contact data from Apollo.io and update Brevo + Monday.com
85
  try:
86
  from .apollo_service import enrich_contact_by_email
87
  from .brevo_service import create_brevo_contact, BREVO_TRIAL_LIST_ID
88
- from .monday_service import create_monday_lead
89
 
90
  # Enrich contact data from Apollo.io
91
  enriched_data = await enrich_contact_by_email(email)
@@ -121,20 +120,6 @@ async def firebase_login(
121
  organization_address=enriched_data.get("organization_address") if enriched_data else None,
122
  list_id=BREVO_TRIAL_LIST_ID
123
  )
124
-
125
- # Create lead in Monday.com
126
- await create_monday_lead(
127
- email=email,
128
- first_name=first_name,
129
- last_name=last_name,
130
- phone_number=enriched_data.get("phone_number") if enriched_data else None,
131
- linkedin_url=enriched_data.get("linkedin_url") if enriched_data else None,
132
- title=enriched_data.get("title") if enriched_data else None,
133
- headline=enriched_data.get("headline") if enriched_data else None,
134
- organization_name=org_name or (enriched_data.get("organization_name") if enriched_data else None),
135
- organization_website=enriched_data.get("organization_website") if enriched_data else None,
136
- organization_address=enriched_data.get("organization_address") if enriched_data else None,
137
- )
138
  except Exception as e:
139
  # Don't fail user creation if integrations fail
140
  print(f"[WARNING] Failed to enrich/update contact for {email}: {str(e)}")
 
81
  db.refresh(user)
82
  print(f"[INFO] New user created via Firebase: {email}")
83
 
84
+ # Enrich contact data from Apollo.io and update Brevo
85
  try:
86
  from .apollo_service import enrich_contact_by_email
87
  from .brevo_service import create_brevo_contact, BREVO_TRIAL_LIST_ID
 
88
 
89
  # Enrich contact data from Apollo.io
90
  enriched_data = await enrich_contact_by_email(email)
 
120
  organization_address=enriched_data.get("organization_address") if enriched_data else None,
121
  list_id=BREVO_TRIAL_LIST_ID
122
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  except Exception as e:
124
  # Don't fail user creation if integrations fail
125
  print(f"[WARNING] Failed to enrich/update contact for {email}: {str(e)}")
backend/app/monday_service.py DELETED
@@ -1,390 +0,0 @@
1
- """
2
- Monday.com API service for creating leads with automatic field matching.
3
- Reference: https://developer.monday.com/api-reference/docs
4
- """
5
- import os
6
- import httpx
7
- import json
8
- from typing import Optional, Dict, Any, List, Tuple
9
- from difflib import SequenceMatcher
10
-
11
- MONDAY_API_KEY = os.environ.get("MONDAY_API_KEY", "")
12
- MONDAY_API_URL = "https://api.monday.com/v2"
13
- MONDAY_BOARD_ID = os.environ.get("MONDAY_BOARD_ID", None) # Your "New Leads" board ID
14
-
15
- # Cache for board columns to avoid repeated API calls
16
- _board_columns_cache: Dict[str, List[Dict[str, Any]]] = {}
17
-
18
-
19
- def _calculate_similarity(str1: str, str2: str) -> float:
20
- """
21
- Calculate similarity between two strings using SequenceMatcher.
22
- Returns a value between 0.0 and 1.0.
23
- """
24
- return SequenceMatcher(None, str1.lower(), str2.lower()).ratio()
25
-
26
-
27
- def _find_best_column_match(
28
- field_name: str,
29
- available_columns: List[Dict[str, Any]],
30
- min_similarity: float = 0.3
31
- ) -> Optional[Tuple[str, str, float]]:
32
- """
33
- Find the best matching column for a field name using semantic similarity.
34
-
35
- Args:
36
- field_name: The field name to match (e.g., "first_name", "email")
37
- available_columns: List of column dicts with 'id' and 'title' keys
38
- min_similarity: Minimum similarity threshold (0.0 to 1.0)
39
-
40
- Returns:
41
- Tuple of (column_id, column_title, similarity_score) or None if no match found
42
- """
43
- best_match = None
44
- best_score = 0.0
45
-
46
- # Normalize field name for matching
47
- normalized_field = field_name.lower().replace("_", " ").replace("-", " ")
48
-
49
- # Common field name variations
50
- field_variations = [
51
- normalized_field,
52
- field_name.lower(),
53
- field_name.replace("_", ""),
54
- ]
55
-
56
- # Add common synonyms
57
- synonyms = {
58
- "first_name": ["first name", "firstname", "fname", "given name"],
59
- "last_name": ["last name", "lastname", "lname", "surname", "family name"],
60
- "email": ["email address", "email", "e-mail", "mail"],
61
- "phone_number": ["phone", "phone number", "telephone", "mobile", "cell"],
62
- "linkedin_url": ["linkedin", "linkedin profile", "linkedin url", "linkedin link"],
63
- "title": ["job title", "position", "role", "job"],
64
- "headline": ["headline", "tagline", "bio"],
65
- "organization_name": ["company", "organization", "org", "company name", "employer"],
66
- "organization_website": ["website", "company website", "url", "web"],
67
- "organization_address": ["address", "company address", "location"],
68
- }
69
-
70
- if field_name in synonyms:
71
- field_variations.extend(synonyms[field_name])
72
-
73
- for column in available_columns:
74
- column_title = column.get("title", "").lower()
75
- column_id = column.get("id", "")
76
-
77
- if not column_title or not column_id:
78
- continue
79
-
80
- # Calculate similarity for each variation
81
- for variation in field_variations:
82
- score = _calculate_similarity(variation, column_title)
83
- if score > best_score:
84
- best_score = score
85
- best_match = (column_id, column.get("title", ""), score)
86
-
87
- if best_match and best_score >= min_similarity:
88
- return best_match
89
- return None
90
-
91
-
92
- async def _get_board_columns(board_id: str) -> List[Dict[str, Any]]:
93
- """
94
- Fetch board columns from Monday.com API.
95
-
96
- Args:
97
- board_id: Monday.com board ID
98
-
99
- Returns:
100
- List of column dictionaries with 'id', 'title', and 'type' keys
101
- """
102
- # Check cache first
103
- if board_id in _board_columns_cache:
104
- return _board_columns_cache[board_id]
105
-
106
- if not MONDAY_API_KEY:
107
- print("[WARNING] MONDAY_API_KEY not set, cannot fetch board columns")
108
- return []
109
-
110
- query = """
111
- query ($boardId: ID!) {
112
- boards(ids: [$boardId]) {
113
- columns {
114
- id
115
- title
116
- type
117
- }
118
- }
119
- }
120
- """
121
-
122
- headers = {
123
- "Authorization": MONDAY_API_KEY,
124
- "Content-Type": "application/json"
125
- }
126
-
127
- try:
128
- async with httpx.AsyncClient(timeout=30.0) as client:
129
- response = await client.post(
130
- MONDAY_API_URL,
131
- json={
132
- "query": query,
133
- "variables": {"boardId": board_id}
134
- },
135
- headers=headers
136
- )
137
-
138
- if response.status_code == 200:
139
- result = response.json()
140
- if result.get("data") and result["data"].get("boards"):
141
- boards = result["data"]["boards"]
142
- if boards and boards[0].get("columns"):
143
- columns = boards[0]["columns"]
144
- # Cache the result
145
- _board_columns_cache[board_id] = columns
146
- print(f"[INFO] Fetched {len(columns)} columns from Monday.com board {board_id}")
147
- return columns
148
- elif result.get("errors"):
149
- print(f"[ERROR] Failed to fetch board columns: {result['errors']}")
150
- else:
151
- print(f"[ERROR] Failed to fetch board columns: {response.status_code} - {response.text}")
152
- except Exception as e:
153
- print(f"[ERROR] Exception while fetching board columns: {str(e)}")
154
-
155
- return []
156
-
157
-
158
- def _format_column_value(value: Any, column_type: str, column_id: Optional[str] = None) -> Any:
159
- """
160
- Format a value according to Monday.com column type.
161
-
162
- Args:
163
- value: The value to format
164
- column_type: Monday.com column type (email, phone, link, text, etc.)
165
- column_id: Column ID (for special handling)
166
-
167
- Returns:
168
- For email/phone/link: Python dict object
169
- For text/other types: Plain string
170
- """
171
- if value is None:
172
- return ""
173
-
174
- value_str = str(value)
175
-
176
- if column_type == "email":
177
- # Monday.com email format requires dict object (will be JSON encoded later)
178
- return {"email": value_str, "text": value_str}
179
- elif column_type == "phone":
180
- return {"phone": value_str, "countryShortName": "US"}
181
- elif column_type == "link":
182
- # If it's already a URL, use it; otherwise create a link
183
- if value_str.startswith("http://") or value_str.startswith("https://"):
184
- return {"url": value_str, "text": value_str}
185
- else:
186
- return {"url": f"https://{value_str}", "text": value_str}
187
- else:
188
- # Text, status, and other types - just return the string
189
- return value_str
190
-
191
-
192
- async def create_monday_lead(
193
- email: str,
194
- first_name: Optional[str] = None,
195
- last_name: Optional[str] = None,
196
- phone_number: Optional[str] = None,
197
- linkedin_url: Optional[str] = None,
198
- title: Optional[str] = None,
199
- headline: Optional[str] = None,
200
- organization_name: Optional[str] = None,
201
- organization_website: Optional[str] = None,
202
- organization_address: Optional[str] = None,
203
- board_id: Optional[str] = None
204
- ) -> bool:
205
- """
206
- Create a new lead item in Monday.com board.
207
-
208
- Args:
209
- email: Contact email address (required)
210
- first_name: Contact first name
211
- last_name: Contact last name
212
- phone_number: Phone number
213
- linkedin_url: LinkedIn profile URL
214
- title: Job title
215
- headline: Professional headline
216
- organization_name: Company name
217
- organization_website: Company website
218
- organization_address: Company address
219
- board_id: Monday.com board ID as string (defaults to MONDAY_BOARD_ID env var)
220
-
221
- Returns:
222
- True if lead created successfully, False otherwise
223
- """
224
- if not MONDAY_API_KEY:
225
- print("[WARNING] MONDAY_API_KEY not set, skipping Monday.com lead creation")
226
- return False
227
-
228
- target_board_id = board_id or MONDAY_BOARD_ID
229
- if not target_board_id:
230
- print("[WARNING] MONDAY_BOARD_ID not set, skipping Monday.com lead creation")
231
- return False
232
-
233
- # Prepare item name (use full name or email)
234
- item_name = email
235
- if first_name and last_name:
236
- item_name = f"{first_name} {last_name}"
237
- elif first_name:
238
- item_name = first_name
239
- elif last_name:
240
- item_name = last_name
241
-
242
- # Fetch board columns to automatically match fields
243
- print(f"[INFO] Fetching Monday.com board columns for automatic field matching...")
244
- board_columns = await _get_board_columns(str(target_board_id))
245
-
246
- if not board_columns:
247
- print("[WARNING] Could not fetch board columns, skipping Monday.com lead creation")
248
- return False
249
-
250
- # Create a mapping of column IDs to column types for formatting
251
- column_types = {col["id"]: col.get("type", "text") for col in board_columns}
252
-
253
- # Prepare data fields to map
254
- data_fields = {
255
- "email": email,
256
- "first_name": first_name,
257
- "last_name": last_name,
258
- "phone_number": phone_number,
259
- "linkedin_url": linkedin_url,
260
- "title": title,
261
- "headline": headline,
262
- "organization_name": organization_name,
263
- "organization_website": organization_website,
264
- "organization_address": organization_address,
265
- }
266
-
267
- # Automatically match fields to columns using semantic similarity
268
- column_values = {}
269
- matched_fields = []
270
- # Track which columns have been matched to handle duplicates (e.g., first_name and last_name -> Name)
271
- column_matches = {} # column_id -> (field_name, value)
272
-
273
- for field_name, field_value in data_fields.items():
274
- if not field_value:
275
- continue
276
-
277
- match = _find_best_column_match(field_name, board_columns)
278
- if match:
279
- column_id, column_title, similarity = match
280
- column_type = column_types.get(column_id, "text")
281
-
282
- # Handle special case: if first_name and last_name both match to the same "Name" column
283
- if column_id in column_matches:
284
- existing_field, existing_value = column_matches[column_id]
285
- # If both first_name and last_name match to the same column, combine them
286
- if (field_name in ["first_name", "last_name"] and
287
- existing_field in ["first_name", "last_name"] and
288
- field_name != existing_field):
289
- # Combine first and last name
290
- if field_name == "first_name":
291
- combined_value = f"{field_value} {existing_value}"
292
- else:
293
- combined_value = f"{existing_value} {field_value}"
294
- formatted_value = _format_column_value(combined_value, column_type, column_id)
295
- column_values[column_id] = formatted_value
296
- matched_fields.append(f"{existing_field}+{field_name} -> {column_title} (combined)")
297
- print(f"[INFO] Combined '{existing_field}' and '{field_name}' to column '{column_title}' (ID: {column_id})")
298
- continue
299
- else:
300
- # Different fields matching to same column - use the one with higher similarity
301
- print(f"[DEBUG] Column '{column_title}' already matched to '{existing_field}', skipping '{field_name}'")
302
- continue
303
-
304
- formatted_value = _format_column_value(field_value, column_type, column_id)
305
- column_values[column_id] = formatted_value
306
- column_matches[column_id] = (field_name, field_value)
307
- matched_fields.append(f"{field_name} -> {column_title} (similarity: {similarity:.2f})")
308
- print(f"[INFO] Matched '{field_name}' to column '{column_title}' (ID: {column_id}, type: {column_type}, value: {formatted_value[:100] if len(str(formatted_value)) > 100 else formatted_value})")
309
- else:
310
- print(f"[DEBUG] No suitable column match found for '{field_name}' (skipping)")
311
-
312
- if not column_values:
313
- print("[WARNING] No fields could be matched to board columns")
314
- return False
315
-
316
- print(f"[INFO] Successfully matched {len(matched_fields)} fields to Monday.com columns")
317
-
318
- # Convert column_values to JSON string for GraphQL mutation
319
- # Monday.com expects column values as a JSON string where:
320
- # - Text columns: plain string values
321
- # - Email/Phone/Link columns: dict objects (properly JSON encoded)
322
- column_values_json = json.dumps(column_values)
323
- print(f"[DEBUG] Monday.com column_values JSON: {column_values_json[:500]}")
324
-
325
- # GraphQL mutation
326
- # Note: Monday.com uses ID! (string) type for board_id, not Int!
327
- mutation = """
328
- mutation ($boardId: ID!, $itemName: String!, $columnValues: JSON!) {
329
- create_item (board_id: $boardId, item_name: $itemName, column_values: $columnValues) {
330
- id
331
- }
332
- }
333
- """
334
-
335
- # Convert board_id to string (Monday.com expects ID! which is a string)
336
- board_id_str = str(target_board_id)
337
-
338
- variables = {
339
- "boardId": board_id_str,
340
- "itemName": item_name,
341
- "columnValues": column_values_json
342
- }
343
-
344
- headers = {
345
- "Authorization": MONDAY_API_KEY,
346
- "Content-Type": "application/json"
347
- }
348
-
349
- try:
350
- async with httpx.AsyncClient(timeout=30.0) as client:
351
- response = await client.post(
352
- MONDAY_API_URL,
353
- json={
354
- "query": mutation,
355
- "variables": variables
356
- },
357
- headers=headers
358
- )
359
-
360
- if response.status_code == 200:
361
- result = response.json()
362
- if result.get("data") and result["data"].get("create_item"):
363
- item_id = result["data"]["create_item"].get("id")
364
- print(f"[INFO] Successfully created Monday.com lead: {item_name} (ID: {item_id})")
365
- return True
366
- elif result.get("errors"):
367
- errors = result.get("errors", [])
368
- for error in errors:
369
- error_msg = error.get("message", "Unknown error")
370
- error_path = error.get("path", [])
371
- print(f"[ERROR] Monday.com API error: {error_msg}")
372
- if error_path:
373
- print(f"[ERROR] Error path: {error_path}")
374
- # Log full error for debugging
375
- print(f"[DEBUG] Full Monday.com error response: {json.dumps(errors, indent=2)}")
376
- return False
377
- else:
378
- print(f"[ERROR] Unexpected Monday.com API response: {result}")
379
- return False
380
- else:
381
- error_data = response.text
382
- print(f"[ERROR] Failed to create Monday.com lead: {response.status_code} - {error_data}")
383
- return False
384
-
385
- except httpx.HTTPStatusError as e:
386
- print(f"[ERROR] Monday.com API HTTP error: {e.response.status_code} - {e.response.text}")
387
- return False
388
- except Exception as e:
389
- print(f"[ERROR] Failed to create Monday.com lead: {str(e)}")
390
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/otp_service.py CHANGED
@@ -133,11 +133,10 @@ async def verify_otp(email: str, otp: str, db: Session) -> User:
133
  db.refresh(user)
134
  print(f"[INFO] New user created via OTP: {email_lower}")
135
 
136
- # Enrich contact data from Apollo.io and update Brevo + Monday.com
137
  try:
138
  from .apollo_service import enrich_contact_by_email
139
  from .brevo_service import create_brevo_contact, BREVO_TRIAL_LIST_ID
140
- from .monday_service import create_monday_lead
141
 
142
  # Enrich contact data from Apollo.io
143
  enriched_data = await enrich_contact_by_email(email_lower)
@@ -166,20 +165,6 @@ async def verify_otp(email: str, otp: str, db: Session) -> User:
166
  organization_address=enriched_data.get("organization_address") if enriched_data else None,
167
  list_id=BREVO_TRIAL_LIST_ID
168
  )
169
-
170
- # Create lead in Monday.com
171
- await create_monday_lead(
172
- email=email_lower,
173
- first_name=first_name,
174
- last_name=last_name,
175
- phone_number=enriched_data.get("phone_number") if enriched_data else None,
176
- linkedin_url=enriched_data.get("linkedin_url") if enriched_data else None,
177
- title=enriched_data.get("title") if enriched_data else None,
178
- headline=enriched_data.get("headline") if enriched_data else None,
179
- organization_name=org_name or (enriched_data.get("organization_name") if enriched_data else None),
180
- organization_website=enriched_data.get("organization_website") if enriched_data else None,
181
- organization_address=enriched_data.get("organization_address") if enriched_data else None,
182
- )
183
  except Exception as e:
184
  # Don't fail user creation if integrations fail
185
  print(f"[WARNING] Failed to enrich/update contact for {email_lower}: {str(e)}")
 
133
  db.refresh(user)
134
  print(f"[INFO] New user created via OTP: {email_lower}")
135
 
136
+ # Enrich contact data from Apollo.io and update Brevo
137
  try:
138
  from .apollo_service import enrich_contact_by_email
139
  from .brevo_service import create_brevo_contact, BREVO_TRIAL_LIST_ID
 
140
 
141
  # Enrich contact data from Apollo.io
142
  enriched_data = await enrich_contact_by_email(email_lower)
 
165
  organization_address=enriched_data.get("organization_address") if enriched_data else None,
166
  list_id=BREVO_TRIAL_LIST_ID
167
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  except Exception as e:
169
  # Don't fail user creation if integrations fail
170
  print(f"[WARNING] Failed to enrich/update contact for {email_lower}: {str(e)}")