Skip to content

Commit 3aa0bbb

Browse files
Merge pull request #12 from mihirpatel1510/536
TASK-2026-00536 & TASK-2026-00657
2 parents 29bff50 + 401ad97 commit 3aa0bbb

3 files changed

Lines changed: 123 additions & 12 deletions

File tree

nextiq/api.py

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,67 @@
2424
})
2525
_MAX_FIELD_LEN = 500 # max characters per Lead field value
2626

27+
# Map Frappe DocType names (as they appear in "Could not find X: Y" errors) to
28+
# the Lead field name, so _find_bad_field can strip the offending field.
29+
_LINK_DOCTYPE_TO_FIELD = {
30+
"country": "country",
31+
"salutation": "salutation",
32+
"Country": "country",
33+
"Salutation": "salutation",
34+
}
35+
36+
# Maps lowercase field labels (as Frappe uses them in error messages) to Lead field names.
37+
# Lets _find_bad_field identify any field from a ValidationError, not just link fields.
38+
_FIELD_LABEL_TO_NAME = {
39+
"salutation": "salutation",
40+
"first name": "first_name",
41+
"middle name": "middle_name",
42+
"last name": "last_name",
43+
"gender": "gender",
44+
"job title": "job_title",
45+
"email id": "email_id",
46+
"email": "email_id",
47+
"mobile no": "mobile_no",
48+
"mobile": "mobile_no",
49+
"whatsapp no": "whatsapp_no",
50+
"whatsapp": "whatsapp_no",
51+
"phone": "phone",
52+
"phone ext": "phone_ext",
53+
"company name": "company_name",
54+
"company": "company_name",
55+
"website": "website",
56+
"fax": "fax",
57+
"city": "city",
58+
"state": "state",
59+
"country": "country",
60+
}
61+
62+
63+
def _find_bad_field(error_msg, data):
64+
"""
65+
Parse a Frappe ValidationError message and return the Lead field name
66+
that caused it, or None if it cannot be determined.
67+
"""
68+
import re
69+
# "Could not find {DocType}: {value}" — Link field resolution failure
70+
m = re.search(r"Could not find ([\w ]+):", error_msg)
71+
if m:
72+
doctype = m.group(1).strip()
73+
field = _LINK_DOCTYPE_TO_FIELD.get(doctype) or _LINK_DOCTYPE_TO_FIELD.get(doctype.lower())
74+
if field and field in data:
75+
return field
76+
# Check if any field's current value appears verbatim in the error message
77+
for field, value in data.items():
78+
if value and str(value) in error_msg:
79+
return field
80+
# Check if any field label appears in the error message
81+
# (e.g. "Value for Gender must be one of …", "Invalid Email Id")
82+
err_lower = error_msg.lower()
83+
for label, field in _FIELD_LABEL_TO_NAME.items():
84+
if label in err_lower and field in data:
85+
return field
86+
return None
87+
2788

2889
class _QuotaExceededError(Exception):
2990
pass
@@ -127,6 +188,7 @@ def submit_card_scan(merged_image_base64, filename="business_card.jpg"):
127188
"submitted_at": frappe.utils.now(),
128189
"job_id": job_id,
129190
"cb_secret": cb_secret,
191+
"scanned_by": frappe.session.user,
130192
})
131193
log.insert(ignore_permissions=True)
132194
frappe.db.commit()
@@ -186,9 +248,13 @@ def scan_callback(job_id, cb_secret, success, data=None, error=None,
186248
if not job_id or not cb_secret:
187249
return {"success": False, "error": "missing_params"}
188250

189-
log_name = frappe.db.get_value("Card Scan Log", {"job_id": job_id}, "name")
190-
if not log_name:
251+
log_data = frappe.db.get_value(
252+
"Card Scan Log", {"job_id": job_id}, ["name", "scanned_by"], as_dict=True
253+
)
254+
if not log_data:
191255
return {"success": False, "error": "invalid_job_id"}
256+
log_name = log_data.name
257+
scanned_by = log_data.scanned_by
192258

193259
# Constant-time secret comparison — prevents timing-based enumeration
194260
stored_secret = frappe.db.get_value("Card Scan Log", log_name, "cb_secret") or ""
@@ -218,11 +284,51 @@ def scan_callback(job_id, cb_secret, success, data=None, error=None,
218284
if k in _ALLOWED_LEAD_FIELDS and v not in (None, "")
219285
}
220286
if data:
287+
skipped_fields = {}
221288
try:
222-
lead = frappe.get_doc({"doctype": "Lead", **data})
223-
lead.insert(ignore_permissions=True)
224-
frappe.db.commit()
225-
lead_name = lead.name
289+
# Retry loop: on ValidationError, strip the offending field and try again.
290+
# This handles AI values that don't match ERPNext options (e.g. country="BHARAT").
291+
for _attempt in range(len(data) + 1):
292+
try:
293+
lead_doc_data = {"doctype": "Lead", **data}
294+
if scanned_by and scanned_by != "Guest":
295+
lead_doc_data["lead_owner"] = scanned_by
296+
lead = frappe.get_doc(lead_doc_data)
297+
lead.insert(ignore_permissions=True)
298+
frappe.db.commit()
299+
lead_name = lead.name
300+
break
301+
except frappe.exceptions.DuplicateEntryError:
302+
raise
303+
except frappe.ValidationError as e:
304+
frappe.db.rollback()
305+
bad_field = _find_bad_field(str(e), data)
306+
if bad_field:
307+
skipped_fields[bad_field] = data.pop(bad_field)
308+
else:
309+
raise # can't identify which field — propagate
310+
else:
311+
raise frappe.ValidationError("All fields were invalid; no lead could be created.")
312+
313+
# Add a comment listing any skipped fields so the sales rep knows what was dropped
314+
if skipped_fields:
315+
# Null out the skipped fields — without this, Frappe applies doctype
316+
# defaults (e.g. country defaults to "India") when the field is absent.
317+
frappe.db.set_value("Lead", lead_name,
318+
{f: None for f in skipped_fields})
319+
lines = ["<b>NextIQ: the following fields were skipped (invalid values):</b><ul>"]
320+
for f, v in skipped_fields.items():
321+
lines.append(f"<li><b>{f}</b>: {v}</li>")
322+
lines.append("</ul>")
323+
frappe.get_doc({
324+
"doctype": "Comment",
325+
"comment_type": "Info",
326+
"reference_doctype": "Lead",
327+
"reference_name": lead_name,
328+
"content": "".join(lines),
329+
}).insert(ignore_permissions=True)
330+
frappe.db.commit()
331+
226332
except frappe.exceptions.DuplicateEntryError as e:
227333
err_msg = str(e)[:500] or "A lead with this email address already exists."
228334
frappe.db.rollback()
@@ -236,11 +342,7 @@ def scan_callback(job_id, cb_secret, success, data=None, error=None,
236342
_send_scan_notification(log_name, "duplicate_lead", message=err_msg)
237343
return {"success": False, "error": "duplicate_lead"}
238344
except frappe.ValidationError as e:
239-
# AI processed successfully and returned data, but the data has
240-
# values that Frappe cannot accept (e.g. country="USA" instead of
241-
# "United States", invalid select option, bad link value, etc.).
242-
# Scan is already charged — this is a data quality issue, not a failure.
243-
err_msg = str(e)[:500] or "AI data could not be saved as a Lead — one or more field values were invalid."
345+
err_msg = str(e)[:500] or "AI data could not be saved as a Lead — all field values were invalid."
244346
frappe.db.rollback()
245347
frappe.db.set_value("Card Scan Log", log_name, {
246348
"status": "Invalid Data",

nextiq/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Copyright (c) 2026, krushang.patel@satat.tech and contributors
22
# For license information, please see license.txt
33

4-
SERVICE_URL = "https://nextiq.in"
4+
SERVICE_URL = "https://nextiq.in"

nextiq/nextiq/doctype/card_scan_log/card_scan_log.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"field_order": [
88
"status",
99
"submitted_at",
10+
"scanned_by",
1011
"column_break_pxss",
1112
"merged_image",
1213
"processed_at",
@@ -22,6 +23,14 @@
2223
"cb_secret"
2324
],
2425
"fields": [
26+
{
27+
"fieldname": "scanned_by",
28+
"fieldtype": "Link",
29+
"in_list_view": 1,
30+
"label": "Scanned By",
31+
"options": "User",
32+
"read_only": 1
33+
},
2534
{
2635
"fieldname": "status",
2736
"fieldtype": "Select",

0 commit comments

Comments
 (0)