refactor: class based Sync source
This commit is contained in:
parent
91b8a48c0e
commit
176c7c73bb
@ -6,6 +6,9 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"type",
|
"type",
|
||||||
|
"column_break_dhay",
|
||||||
|
"source",
|
||||||
|
"section_break_fhot",
|
||||||
"lead_data"
|
"lead_data"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -25,13 +28,28 @@
|
|||||||
"label": "Lead Data",
|
"label": "Lead Data",
|
||||||
"options": "JSON",
|
"options": "JSON",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_dhay",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "source",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Source",
|
||||||
|
"options": "Lead Sync Source",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_fhot",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-19 17:41:28.640446",
|
"modified": "2025-10-19 18:30:15.381205",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Lead Syncing",
|
"module": "Lead Syncing",
|
||||||
"name": "Failed Lead Sync Log",
|
"name": "Failed Lead Sync Log",
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class FailedLeadSyncLog(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
lead_data: DF.Code | None
|
lead_data: DF.Code | None
|
||||||
|
source: DF.Link | None
|
||||||
type: DF.Literal["Duplicate", "Failure"]
|
type: DF.Literal["Duplicate", "Failure"]
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
|
|||||||
@ -12,63 +12,105 @@ def get_fb_graph_api_url(endpoint: str) -> str:
|
|||||||
return f"{FB_GRAPH_API_BASE}/{FB_GRAPH_API_VERSION}/{endpoint}"
|
return f"{FB_GRAPH_API_BASE}/{FB_GRAPH_API_VERSION}/{endpoint}"
|
||||||
|
|
||||||
|
|
||||||
def sync_leads_from_facebook(access_token: str, lead_form_id: str) -> None:
|
class FacebookSyncSource:
|
||||||
url = get_fb_graph_api_url(f"/{lead_form_id}/leads")
|
def __init__(
|
||||||
last_synced_at = frappe.db.get_value(
|
self,
|
||||||
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at"
|
access_token: str,
|
||||||
)
|
form_id: str,
|
||||||
if last_synced_at:
|
source_name: str | None = None,
|
||||||
timestamp = frappe.utils.data.get_timestamp(last_synced_at)
|
):
|
||||||
filtering = f"filtering=[{{'field':'time_created','operator':'GREATER_THAN','value':{timestamp}}}]"
|
self.access_token = access_token
|
||||||
url = f"{url}?{filtering}"
|
self.form_id = form_id
|
||||||
|
self.source_name = source_name
|
||||||
|
|
||||||
leads = make_get_request(
|
def get_api_url(self, endpoint: str) -> str:
|
||||||
url,
|
return get_fb_graph_api_url(endpoint)
|
||||||
params={
|
|
||||||
"access_token": access_token,
|
|
||||||
"fields": "id,created_time,field_data",
|
|
||||||
"limit": 15000,
|
|
||||||
},
|
|
||||||
).get("data", [])
|
|
||||||
|
|
||||||
form_questions = frappe.db.get_all(
|
def sync(self):
|
||||||
"Facebook Lead Form Question", filters={"parent": lead_form_id}, fields=["key", "mapped_to_crm_field"]
|
leads = self.fetch_leads()
|
||||||
)
|
question_to_field_map = self.get_form_questions_mapping()
|
||||||
|
|
||||||
# Map form questions to CRM Lead fields
|
for lead in leads:
|
||||||
question_to_field_map = {
|
lead_data = {item["name"]: item["values"][0] for item in lead["field_data"]}
|
||||||
q["key"]: q["mapped_to_crm_field"] for q in form_questions if q["mapped_to_crm_field"]
|
crm_lead_data = {
|
||||||
}
|
question_to_field_map.get(k): v for k, v in lead_data.items() if k in question_to_field_map
|
||||||
|
}
|
||||||
|
crm_lead_data["source"] = "Facebook"
|
||||||
|
crm_lead_data["facebook_lead_id"] = lead["id"]
|
||||||
|
|
||||||
for lead in leads:
|
try:
|
||||||
lead_data = {item["name"]: item["values"][0] for item in lead["field_data"]}
|
frappe.get_doc(
|
||||||
crm_lead_data = {
|
{
|
||||||
question_to_field_map.get(k): v for k, v in lead_data.items() if k in question_to_field_map
|
"doctype": "CRM Lead",
|
||||||
}
|
**crm_lead_data,
|
||||||
crm_lead_data["source"] = "Facebook"
|
}
|
||||||
crm_lead_data["facebook_lead_id"] = lead["id"]
|
).insert(ignore_permissions=True)
|
||||||
|
except frappe.UniqueValidationError:
|
||||||
|
# Skip duplicate leads based on facebook_lead_id
|
||||||
|
# TODO: de-duplication based on field values
|
||||||
|
self.create_failure_log(lead, "Duplicate")
|
||||||
|
except Exception:
|
||||||
|
self.create_failure_log(lead)
|
||||||
|
|
||||||
try:
|
self.update_last_synced_at()
|
||||||
frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "CRM Lead",
|
|
||||||
**crm_lead_data,
|
|
||||||
}
|
|
||||||
).insert(ignore_permissions=True)
|
|
||||||
except frappe.UniqueValidationError:
|
|
||||||
# Skip duplicate leads based on facebook_lead_id
|
|
||||||
# TODO: de-duplication based on field values
|
|
||||||
frappe.get_doc(
|
|
||||||
{"doctype": "Failed Lead Sync Log", "type": "Duplicate", "lead_data": frappe.as_json(lead)}
|
|
||||||
).insert(ignore_permissions=True)
|
|
||||||
except Exception:
|
|
||||||
frappe.get_doc(
|
|
||||||
{"doctype": "Failed Lead Sync Log", "type": "Failure", "lead_data": frappe.as_json(lead)}
|
|
||||||
).insert(ignore_permissions=True)
|
|
||||||
|
|
||||||
frappe.db.set_value(
|
def fetch_leads(self):
|
||||||
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at", frappe.utils.now()
|
url = self.get_api_url(f"/{self.form_id}/leads")
|
||||||
)
|
|
||||||
|
if self.last_synced_at:
|
||||||
|
timestamp = frappe.utils.data.get_timestamp(self.last_synced_at)
|
||||||
|
filtering = (
|
||||||
|
f"filtering=[{{'field':'time_created','operator':'GREATER_THAN','value':{timestamp}}}]"
|
||||||
|
)
|
||||||
|
url = f"{url}?{filtering}"
|
||||||
|
|
||||||
|
return make_get_request(
|
||||||
|
url,
|
||||||
|
params={
|
||||||
|
"access_token": self.access_token,
|
||||||
|
"fields": "id,created_time,field_data",
|
||||||
|
"limit": 100000, # TODO: pagination
|
||||||
|
},
|
||||||
|
).get("data", [])
|
||||||
|
|
||||||
|
def get_form_questions_mapping(self):
|
||||||
|
form_questions = frappe.db.get_all(
|
||||||
|
"Facebook Lead Form Question",
|
||||||
|
filters={"parent": self.form_id},
|
||||||
|
fields=["key", "mapped_to_crm_field"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {q["key"]: q["mapped_to_crm_field"] for q in form_questions if q["mapped_to_crm_field"]}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_synced_at(self):
|
||||||
|
return frappe.db.get_value(
|
||||||
|
"Lead Sync Source", self.source_name or {"facebook_lead_form": self.form_id}, "last_synced_at"
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_failure_log(self, lead_data: dict | None = None, type: str = "Failure"):
|
||||||
|
return frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Failed Lead Sync Log",
|
||||||
|
"type": type,
|
||||||
|
"lead_data": frappe.as_json(lead_data),
|
||||||
|
"source": self.get_source_name()
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
def update_last_synced_at(self):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Lead Sync Source",
|
||||||
|
self.source_name or {"facebook_lead_form": self.form_id},
|
||||||
|
"last_synced_at",
|
||||||
|
frappe.utils.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_source_name(self):
|
||||||
|
if self.source_name:
|
||||||
|
return self.source_name
|
||||||
|
|
||||||
|
return frappe.db.get_value("Lead Sync Source", {"facebook_lead_form": self.form_id}, "name")
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -156,3 +198,8 @@ def get_pages_with_forms() -> list[dict]:
|
|||||||
forms = frappe.db.get_all("Facebook Lead Form", filters={"page": page["id"]}, fields=["id", "name"])
|
forms = frappe.db.get_all("Facebook Lead Form", filters={"page": page["id"]}, fields=["id", "name"])
|
||||||
page["forms"] = forms
|
page["forms"] = forms
|
||||||
return pages
|
return pages
|
||||||
|
|
||||||
|
|
||||||
|
def validate_duplicate(lead: dict, field_mapping: dict):
|
||||||
|
# if a lead exists with
|
||||||
|
pass
|
||||||
|
|||||||
@ -54,7 +54,8 @@
|
|||||||
"fieldname": "facebook_lead_form",
|
"fieldname": "facebook_lead_form",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Facebook Lead Form",
|
"label": "Facebook Lead Form",
|
||||||
"options": "Facebook Lead Form"
|
"options": "Facebook Lead Form",
|
||||||
|
"unique": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
@ -84,7 +85,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-19 15:07:26.256720",
|
"modified": "2025-10-19 18:23:24.725350",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Lead Syncing",
|
"module": "Lead Syncing",
|
||||||
"name": "Lead Sync Source",
|
"name": "Lead Sync Source",
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import frappe
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from crm.lead_syncing.doctype.lead_sync_source.facebook import (
|
from crm.lead_syncing.doctype.lead_sync_source.facebook import (
|
||||||
|
FacebookSyncSource,
|
||||||
fetch_and_store_pages_from_facebook,
|
fetch_and_store_pages_from_facebook,
|
||||||
sync_leads_from_facebook,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -60,4 +60,8 @@ class LeadSyncSource(Document):
|
|||||||
if self.type == "Facebook" and self.access_token:
|
if self.type == "Facebook" and self.access_token:
|
||||||
if not self.facebook_lead_form:
|
if not self.facebook_lead_form:
|
||||||
frappe.throw(frappe._("Please select a lead gen form before syncing!"))
|
frappe.throw(frappe._("Please select a lead gen form before syncing!"))
|
||||||
sync_leads_from_facebook(self.get_password("access_token"), self.facebook_lead_form)
|
|
||||||
|
FacebookSyncSource(
|
||||||
|
self.get_password("access_token"),
|
||||||
|
self.facebook_lead_form
|
||||||
|
).sync()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user