vachaspathi commited on
Commit
aa3ff72
Β·
verified Β·
1 Parent(s): 48772c6

Update zoho_client.py

Browse files
Files changed (1) hide show
  1. zoho_client.py +120 -116
zoho_client.py CHANGED
@@ -5,7 +5,7 @@ from utils import normalize_date
5
  import time
6
 
7
  # =====================================================
8
- # 1. CORE AUTH
9
  # =====================================================
10
  def get_headers():
11
  try:
@@ -16,165 +16,169 @@ def get_headers():
16
  "grant_type": "refresh_token",
17
  "redirect_uri": "http://www.google.com"
18
  }
19
- # Short timeout to fail fast if auth is broken
20
- resp = requests.post(config.AUTH_URL, data=params, timeout=10)
21
  if resp.status_code == 200:
22
  return {"Authorization": f"Zoho-oauthtoken {resp.json().get('access_token')}"}
23
- else:
24
- print(f"Auth Failed: {resp.text}")
25
  except Exception as e:
26
- print(f"Auth Connection Error: {e}")
27
  return None
28
 
29
  # =====================================================
30
- # 2. CUSTOMER RESOLVER (With Hard Fallback)
31
  # =====================================================
32
  def get_customer_id(name, headers):
33
  """
34
- Guarantees a Customer ID is returned.
35
- 1. Search Specific Name
36
- 2. Create Specific Name
37
- 3. Fallback to 'MCP Generic Customer'
38
  """
39
  if not name or str(name).lower() in ["unknown", "none"]:
40
  name = "MCP Generic Customer"
41
 
42
- # --- ATTEMPT 1: SEARCH ---
43
- print(f" πŸ”Ž Searching for: '{name}'")
 
44
  search_url = f"{config.API_BASE}/contacts"
 
 
 
 
 
45
  try:
46
- res = requests.get(search_url, headers=headers, params={'organization_id': config.ORGANIZATION_ID, 'contact_name_contains': name})
 
 
47
  if res.status_code == 200:
48
  contacts = res.json().get('contacts', [])
49
- for c in contacts:
50
- # Loose match
51
- if name.lower() in c['contact_name'].lower():
52
- return c['contact_id'], "Found Existing"
 
 
 
 
53
  except Exception as e:
54
- print(f" ⚠️ Search Error: {e}")
55
 
56
- # --- ATTEMPT 2: CREATE ---
57
- print(f" πŸ†• Creating: '{name}'")
 
 
 
 
 
 
58
  try:
59
- payload = {"contact_name": name, "contact_type": "customer"}
60
- res = requests.post(f"{config.API_BASE}/contacts?organization_id={config.ORGANIZATION_ID}", headers=headers, json=payload)
61
 
62
  if res.status_code == 201:
63
- return res.json()['contact']['contact_id'], "Created New"
 
 
64
 
65
- # If Duplicate (Code 3062), we MUST find it via search now
66
  if res.json().get('code') == 3062:
67
- print(" ⚠️ Duplicate detected. Retrying Search...")
68
- return get_customer_id(name, headers) # Recursive retry (Safe because search logic handles it)
69
-
70
- except Exception as e:
71
- print(f" ⚠️ Create Error: {e}")
72
-
73
- # --- ATTEMPT 3: FALLBACK (The Safety Net) ---
74
- if name != "MCP Generic Customer":
75
- print(" ❌ Specific Customer Failed. Switching to Generic Fallback.")
76
- return get_customer_id("MCP Generic Customer", headers)
77
-
78
- return None, "Failed Completely"
79
 
80
- # =====================================================
81
- # 3. DATA CLEANING
82
- # =====================================================
83
- def sanitize_lines(raw_items):
84
- """Ensures valid line items for Zoho."""
85
- clean = []
86
- if not raw_items:
87
- # Default if extraction failed
88
- return [{"name": "Services Rendered", "rate": 0, "quantity": 1}]
89
-
90
- for item in raw_items:
91
- name = str(item.get("name") or item.get("description") or "Item")
92
- desc = str(item.get("description", ""))
93
-
94
- try: rate = float(str(item.get("rate", 0)).replace(",", "").replace("$", ""))
95
- except: rate = 0.0
96
 
97
- try: qty = float(str(item.get("quantity", 1)))
98
- except: qty = 1.0
99
-
100
- if len(name) > 100:
101
- desc = f"{name} \n {desc}"
102
- name = name[:95] + "..."
103
 
104
- clean.append({
105
- "name": name,
106
- "description": desc,
107
- "rate": rate,
108
- "quantity": qty
109
- })
110
- return clean
111
 
112
  # =====================================================
113
- # 4. MAIN PROCESSOR (Invoices Only)
114
  # =====================================================
115
- def process_invoice(ai_data):
116
- """
117
- Linear Script: Auth -> Customer -> Invoice -> Done.
118
- """
119
- yield "πŸ”„ Mode: INVOICE ONLY (Simplified)\n"
120
 
121
- # 1. Auth
122
- headers = get_headers()
123
- if not headers:
124
- yield "❌ Critical: Authentication Failed."
125
- return
126
-
127
- # 2. Customer
128
- cust_name = ai_data.get('data', {}).get('vendor_name', 'Unknown')
129
- yield f"βŒ› Resolving Customer: {cust_name}..."
130
 
131
- cid, status = get_customer_id(cust_name, headers)
 
 
 
 
 
 
132
 
133
- if not cid:
134
- yield "πŸ›‘ Critical: Could not find or create ANY customer (even fallback)."
135
- return
136
- yield f" -> {status} (ID: {cid})\n"
 
 
 
 
 
137
 
138
- # 3. Prepare Payload
139
- raw_data = ai_data.get('data', {})
140
  payload = {
141
  "customer_id": cid,
142
- "date": normalize_date(raw_data.get('date')),
143
- "line_items": sanitize_lines(raw_data.get('line_items')),
 
144
  "status": "draft"
145
  }
146
-
147
- # Invoice Number
148
- ref_num = raw_data.get("reference_number") or f"INV-{int(time.time())}"
149
- payload["invoice_number"] = ref_num
150
 
151
- # 4. Send to Zoho
152
- yield "πŸš€ Sending Invoice..."
153
  url = f"{config.API_BASE}/invoices?organization_id={config.ORGANIZATION_ID}"
154
-
155
  try:
156
- resp = requests.post(url, headers=headers, json=payload)
 
157
 
158
- # Retry if Auto-Numbering is On (Code 4097)
159
- if resp.status_code != 201 and resp.json().get('code') == 4097:
160
- yield "\n⚠️ Auto-Numbering is ON. Retrying without Invoice Number..."
161
- del payload["invoice_number"]
162
- resp = requests.post(url, headers=headers, json=payload)
163
-
164
- # Final Result
165
- if resp.status_code == 201:
166
- inv = resp.json().get('invoice', {})
167
- yield f"\nβœ… SUCCESS! Invoice Created.\nNumber: {inv.get('invoice_number')}\nID: {inv.get('invoice_id')}"
168
- else:
169
- yield f"\n❌ Zoho Error: {resp.text}"
170
-
171
  except Exception as e:
172
- yield f"\n❌ System Error: {e}"
173
 
174
  # =====================================================
175
- # 5. APP BRIDGE
176
  # =====================================================
177
  def route_and_execute(ai_output):
178
- # Ignore doc_type, force Invoice logic
179
- for log in process_invoice(ai_output):
180
- yield log
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import time
6
 
7
  # =====================================================
8
+ # 1. AUTHENTICATION
9
  # =====================================================
10
  def get_headers():
11
  try:
 
16
  "grant_type": "refresh_token",
17
  "redirect_uri": "http://www.google.com"
18
  }
19
+ # Added 10s timeout to prevent hanging
20
+ resp = requests.post(config.AUTH_URL, data=params, timeout=10)
21
  if resp.status_code == 200:
22
  return {"Authorization": f"Zoho-oauthtoken {resp.json().get('access_token')}"}
 
 
23
  except Exception as e:
24
+ print(f"Auth Error: {e}")
25
  return None
26
 
27
  # =====================================================
28
+ # 2. DIRECT CUSTOMER RESOLVER
29
  # =====================================================
30
  def get_customer_id(name, headers):
31
  """
32
+ Strict Logic: Search -> Found? -> Return ID.
33
+ Else -> Create -> Return ID.
 
 
34
  """
35
  if not name or str(name).lower() in ["unknown", "none"]:
36
  name = "MCP Generic Customer"
37
 
38
+ print(f" πŸ”Ž Searching Zoho for: '{name}'...")
39
+
40
+ # --- STEP 1: SEARCH ---
41
  search_url = f"{config.API_BASE}/contacts"
42
+ search_params = {
43
+ 'organization_id': config.ORGANIZATION_ID,
44
+ 'contact_name_contains': name
45
+ }
46
+
47
  try:
48
+ # 10s Timeout
49
+ res = requests.get(search_url, headers=headers, params=search_params, timeout=10)
50
+
51
  if res.status_code == 200:
52
  contacts = res.json().get('contacts', [])
53
+ if len(contacts) > 0:
54
+ # MATCH FOUND!
55
+ found_id = contacts[0]['contact_id']
56
+ print(f" βœ… Found Existing Customer: {contacts[0]['contact_name']} ({found_id})")
57
+ return found_id
58
+ else:
59
+ print(f" ⚠️ Search API Status: {res.status_code} (Not 200)")
60
+
61
  except Exception as e:
62
+ print(f" ❌ Search Exception: {e}")
63
 
64
+ # --- STEP 2: CREATE (Only if Search failed) ---
65
+ print(f" πŸ†• Not found. Creating new customer: '{name}'...")
66
+
67
+ create_payload = {
68
+ "contact_name": name,
69
+ "contact_type": "customer"
70
+ }
71
+
72
  try:
73
+ res = requests.post(search_url, headers=headers, params={'organization_id': config.ORGANIZATION_ID}, json=create_payload, timeout=10)
 
74
 
75
  if res.status_code == 201:
76
+ new_id = res.json().get('contact', {}).get('contact_id')
77
+ print(f" βœ… Created New: {new_id}")
78
+ return new_id
79
 
80
+ # Handle Duplicate Error (Race Condition)
81
  if res.json().get('code') == 3062:
82
+ print(" ⚠️ Zoho says Duplicate. Forcing Search on exact name...")
83
+ # Logic: If we are here, the 'contains' search failed, but it exists.
84
+ # We assume it exists and we just failed to fetch it.
85
+ # We cannot proceed without an ID.
86
+ return None
 
 
 
 
 
 
 
87
 
88
+ print(f" ❌ Creation Failed: {res.text}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ except Exception as e:
91
+ print(f" ❌ Create Exception: {e}")
 
 
 
 
92
 
93
+ return None
 
 
 
 
 
 
94
 
95
  # =====================================================
96
+ # 3. INVOICE PUSHER
97
  # =====================================================
98
+ def create_invoice(cid, ai_data, headers):
99
+ print(" πŸ“¦ Preparing Invoice Payload...")
 
 
 
100
 
101
+ # 1. Process Line Items (Handle 0 items case)
102
+ raw_items = ai_data.get('line_items', [])
103
+ clean_items = []
 
 
 
 
 
 
104
 
105
+ if raw_items:
106
+ for item in raw_items:
107
+ clean_items.append({
108
+ "name": str(item.get("name", "Service"))[:100], # Limit 100 chars
109
+ "rate": float(item.get("rate", 0)),
110
+ "quantity": float(item.get("quantity", 1))
111
+ })
112
 
113
+ # Fallback if list is empty
114
+ if not clean_items:
115
+ print(" ⚠️ No items found. Using Default 'Services Rendered' Item.")
116
+ clean_items = [{
117
+ "name": "Services Rendered",
118
+ "description": "Auto-generated from document total",
119
+ "rate": float(ai_data.get("total", 0)),
120
+ "quantity": 1
121
+ }]
122
 
123
+ # 2. Build Payload
 
124
  payload = {
125
  "customer_id": cid,
126
+ "date": normalize_date(ai_data.get("date")),
127
+ "invoice_number": ai_data.get("reference_number") or f"INV-{int(time.time())}",
128
+ "line_items": clean_items,
129
  "status": "draft"
130
  }
 
 
 
 
131
 
132
+ # 3. Send
 
133
  url = f"{config.API_BASE}/invoices?organization_id={config.ORGANIZATION_ID}"
 
134
  try:
135
+ print(" πŸš€ Sending Invoice to Zoho...")
136
+ res = requests.post(url, headers=headers, json=payload, timeout=15)
137
 
138
+ # Auto-Number Retry
139
+ if res.status_code != 201 and res.json().get('code') == 4097:
140
+ print(" ⚠️ Auto-Numbering active. Removing ID and Retrying...")
141
+ del payload['invoice_number']
142
+ res = requests.post(url, headers=headers, json=payload, timeout=15)
143
+
144
+ if res.status_code == 201:
145
+ inv = res.json().get('invoice', {})
146
+ return f"βœ… SUCCESS! Invoice Created.\nNumber: {inv.get('invoice_number')}\nLink: https://books.zoho.in/app/{config.ORGANIZATION_ID}#/invoices/{inv.get('invoice_id')}"
147
+
148
+ return f"❌ Zoho Error: {res.text}"
149
+
 
150
  except Exception as e:
151
+ return f"❌ Connection Error: {e}"
152
 
153
  # =====================================================
154
+ # 4. MAIN EXECUTOR
155
  # =====================================================
156
  def route_and_execute(ai_output):
157
+ # 1. Get Auth
158
+ headers = get_headers()
159
+ if not headers:
160
+ yield "❌ Auth Failed. Check Credentials."
161
+ return
162
+
163
+ # 2. Get Data
164
+ data = ai_output.get('data', {})
165
+ vendor_name = data.get('vendor_name')
166
+
167
+ yield f"πŸ”„ Processing Invoice for: {vendor_name}\n"
168
+
169
+ # 3. Get Customer ID
170
+ cid = get_customer_id(vendor_name, headers)
171
+
172
+ if not cid:
173
+ # HARD FALLBACK: If name search fails, try generic
174
+ yield " ⚠️ Specific customer failed. Trying 'Generic' fallback...\n"
175
+ cid = get_customer_id("MCP Generic Customer", headers)
176
+ if not cid:
177
+ yield "πŸ›‘ Critical: Could not find/create any customer."
178
+ return
179
+
180
+ yield f" -> Customer ID: {cid}\n"
181
+
182
+ # 4. Create Invoice
183
+ result = create_invoice(cid, data, headers)
184
+ yield result