feat: configure page and form from CRM UI
This commit is contained in:
parent
6e1dd04819
commit
cd366839fc
143
crm/lead_syncing/doctype/lead_sync_source/facebook.py
Normal file
143
crm/lead_syncing/doctype/lead_sync_source/facebook.py
Normal file
@ -0,0 +1,143 @@
|
||||
import frappe
|
||||
from frappe.integrations.utils import make_get_request
|
||||
|
||||
FB_GRAPH_API_BASE = "https://graph.facebook.com"
|
||||
FB_GRAPH_API_VERSION = "v23.0"
|
||||
|
||||
|
||||
def get_fb_graph_api_url(endpoint: str) -> str:
|
||||
if endpoint.startswith("/"):
|
||||
endpoint = endpoint[1:]
|
||||
|
||||
return f"{FB_GRAPH_API_BASE}/{FB_GRAPH_API_VERSION}/{endpoint}"
|
||||
|
||||
|
||||
def sync_leads_from_facebook(access_token: str, lead_form_id: str) -> None:
|
||||
url = get_fb_graph_api_url(f"/{lead_form_id}/leads")
|
||||
last_synced_at = frappe.db.get_value(
|
||||
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at"
|
||||
)
|
||||
timestamp = frappe.utils.data.get_timestamp(last_synced_at)
|
||||
filtering = f"filtering=[{{'field':'time_created','operator':'GREATER_THAN','value':{timestamp}}}]"
|
||||
if last_synced_at:
|
||||
url = f"{url}?{filtering}"
|
||||
|
||||
leads = make_get_request(
|
||||
url,
|
||||
params={
|
||||
"access_token": access_token,
|
||||
"fields": "id,created_time,field_data",
|
||||
"limit": 15000,
|
||||
},
|
||||
).get("data", [])
|
||||
|
||||
form_questions = frappe.db.get_all(
|
||||
"Facebook Lead Form Question", filters={"parent": lead_form_id}, fields=["key", "mapped_to_crm_field"]
|
||||
)
|
||||
|
||||
# Map form questions to CRM Lead fields
|
||||
question_to_field_map = {
|
||||
q["key"]: q["mapped_to_crm_field"] for q in form_questions if q["mapped_to_crm_field"]
|
||||
}
|
||||
|
||||
for lead in leads:
|
||||
lead_data = {item["name"]: item["values"][0] for item in lead["field_data"]}
|
||||
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"]
|
||||
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "CRM Lead",
|
||||
**crm_lead_data,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
except frappe.UniqueValidationError:
|
||||
# Skip duplicate leads based on facebook_lead_id
|
||||
frappe.log_error("Duplicate lead skipped")
|
||||
continue
|
||||
|
||||
frappe.db.set_value(
|
||||
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at", frappe.utils.now()
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def fetch_and_store_pages_from_facebook(access_token: str) -> list[dict]:
|
||||
if not access_token:
|
||||
frappe.throw(frappe._("Access token is required"))
|
||||
|
||||
account_details = get_fb_account_details(access_token)
|
||||
if not account_details.get("id"):
|
||||
frappe.throw(frappe._("Invalid access token provided for Facebook."))
|
||||
|
||||
url = get_fb_graph_api_url("/me/accounts")
|
||||
pages = make_get_request(url, params={"access_token": access_token}).get("data", [])
|
||||
for page in pages:
|
||||
page_id = page["id"]
|
||||
already_synced = frappe.db.exists("Facebook Page", page_id)
|
||||
if not already_synced:
|
||||
create_facebook_page_in_db(page, account_details)
|
||||
forms = fetch_and_store_leadgen_forms_from_facebook(page_id, page["access_token"])
|
||||
page["forms"] = forms
|
||||
|
||||
return pages
|
||||
|
||||
def get_fb_account_details(access_token: str) -> dict:
|
||||
url = get_fb_graph_api_url("me")
|
||||
try:
|
||||
response = make_get_request(url, params={"access_token": access_token})
|
||||
except Exception as _:
|
||||
frappe.throw(frappe._("Please check your access token"))
|
||||
return response
|
||||
|
||||
|
||||
def create_facebook_page_in_db(page: dict, account_details: dict) -> None:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Facebook Page",
|
||||
"page_name": page["name"],
|
||||
"id": page["id"],
|
||||
"category": page["category"],
|
||||
"access_token": page["access_token"],
|
||||
"account_id": account_details["id"],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def fetch_and_store_leadgen_forms_from_facebook(page_id: str, page_access_token: str) -> list[dict]:
|
||||
fields = "id,name,questions"
|
||||
url = get_fb_graph_api_url(f"/{page_id}/leadgen_forms")
|
||||
forms = make_get_request(
|
||||
url,
|
||||
params={
|
||||
"access_token": page_access_token,
|
||||
"fields": fields,
|
||||
"limit": 15000,
|
||||
},
|
||||
).get("data", [])
|
||||
for form in forms:
|
||||
form_id = form["id"]
|
||||
already_synced = frappe.db.exists("Facebook Lead Form", form_id)
|
||||
if already_synced:
|
||||
continue
|
||||
create_facebook_lead_form_in_db(form, page_id)
|
||||
|
||||
return forms
|
||||
|
||||
|
||||
def create_facebook_lead_form_in_db(form: dict, page_id: str) -> None:
|
||||
form_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Facebook Lead Form",
|
||||
"form_name": form["name"],
|
||||
"id": form["id"],
|
||||
"page": page_id,
|
||||
"questions": form["questions"],
|
||||
}
|
||||
)
|
||||
|
||||
frappe.errprint(form_doc.as_dict())
|
||||
form_doc.insert(ignore_permissions=True)
|
||||
@ -2,18 +2,9 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.integrations.utils import make_get_request
|
||||
from frappe.model.document import Document
|
||||
|
||||
FB_GRAPH_API_BASE = "https://graph.facebook.com"
|
||||
FB_GRAPH_API_VERSION = "v23.0"
|
||||
|
||||
|
||||
def get_fb_graph_api_url(endpoint: str) -> str:
|
||||
if endpoint.startswith("/"):
|
||||
endpoint = endpoint[1:]
|
||||
|
||||
return f"{FB_GRAPH_API_BASE}/{FB_GRAPH_API_VERSION}/{endpoint}"
|
||||
from crm.lead_syncing.doctype.lead_sync_source.facebook import sync_leads_from_facebook
|
||||
|
||||
|
||||
class LeadSyncSource(Document):
|
||||
@ -41,125 +32,6 @@ class LeadSyncSource(Document):
|
||||
@frappe.whitelist()
|
||||
def sync_leads(self):
|
||||
if self.type == "Facebook" and self.access_token:
|
||||
if not self.facebook_lead_form:
|
||||
frappe.throw(frappe._("Please select a lead gen form before syncing!"))
|
||||
sync_leads_from_facebook(self.get_password("access_token"), self.facebook_lead_form)
|
||||
|
||||
|
||||
def sync_leads_from_facebook(access_token: str, lead_form_id: str) -> None:
|
||||
url = get_fb_graph_api_url(f"/{lead_form_id}/leads")
|
||||
last_synced_at = frappe.db.get_value(
|
||||
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at"
|
||||
)
|
||||
timestamp = frappe.utils.data.get_timestamp(last_synced_at)
|
||||
filtering = f"filtering=[{{'field':'time_created','operator':'GREATER_THAN','value':{timestamp}}}]"
|
||||
if last_synced_at:
|
||||
url = f"{url}?{filtering}"
|
||||
|
||||
leads = make_get_request(
|
||||
url,
|
||||
params={
|
||||
"access_token": access_token,
|
||||
"fields": "id,created_time,field_data",
|
||||
"limit": 15000,
|
||||
},
|
||||
).get("data", [])
|
||||
|
||||
form_questions = frappe.db.get_all(
|
||||
"Facebook Lead Form Question", filters={"parent": lead_form_id}, fields=["key", "mapped_to_crm_field"]
|
||||
)
|
||||
|
||||
# Map form questions to CRM Lead fields
|
||||
question_to_field_map = {
|
||||
q["key"]: q["mapped_to_crm_field"] for q in form_questions if q["mapped_to_crm_field"]
|
||||
}
|
||||
|
||||
for lead in leads:
|
||||
lead_data = {item["name"]: item["values"][0] for item in lead["field_data"]}
|
||||
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"]
|
||||
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "CRM Lead",
|
||||
**crm_lead_data,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
except frappe.UniqueValidationError:
|
||||
# Skip duplicate leads based on facebook_lead_id
|
||||
frappe.log_error("Duplicate lead skipped")
|
||||
continue
|
||||
|
||||
frappe.db.set_value(
|
||||
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at", frappe.utils.now()
|
||||
)
|
||||
|
||||
|
||||
def fetch_and_store_pages_from_facebook(access_token: str) -> None:
|
||||
account_details = get_fb_account_details(access_token)
|
||||
if not account_details.get("id"):
|
||||
frappe.log_error("Invalid access token provided for Facebook.", "Lead Sync Source")
|
||||
return
|
||||
|
||||
url = get_fb_graph_api_url("/me/accounts")
|
||||
pages = make_get_request(url, params={"access_token": access_token}).get("data", [])
|
||||
for page in pages:
|
||||
page_id = page["id"]
|
||||
already_synced = frappe.db.exists("Facebook Page", page_id)
|
||||
if not already_synced:
|
||||
create_facebook_page_in_db(page, account_details)
|
||||
fetch_and_store_leadgen_forms_from_facebook(page_id, page["access_token"])
|
||||
|
||||
|
||||
def get_fb_account_details(access_token: str) -> dict:
|
||||
url = get_fb_graph_api_url("me")
|
||||
return make_get_request(url, params={"access_token": access_token})
|
||||
|
||||
|
||||
def create_facebook_page_in_db(page: dict, account_details: dict) -> None:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Facebook Page",
|
||||
"page_name": page["name"],
|
||||
"id": page["id"],
|
||||
"category": page["category"],
|
||||
"access_token": page["access_token"],
|
||||
"account_id": account_details["id"],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def fetch_and_store_leadgen_forms_from_facebook(page_id: str, page_access_token: str) -> None:
|
||||
fields = "id,name,questions"
|
||||
url = get_fb_graph_api_url(f"/{page_id}/leadgen_forms")
|
||||
forms = make_get_request(
|
||||
url,
|
||||
params={
|
||||
"access_token": page_access_token,
|
||||
"fields": fields,
|
||||
"limit": 15000,
|
||||
},
|
||||
).get("data", [])
|
||||
for form in forms:
|
||||
form_id = form["id"]
|
||||
already_synced = frappe.db.exists("Facebook Lead Form", form_id)
|
||||
if already_synced:
|
||||
continue
|
||||
create_facebook_lead_form_in_db(form, page_id)
|
||||
|
||||
|
||||
def create_facebook_lead_form_in_db(form: dict, page_id: str) -> None:
|
||||
form_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Facebook Lead Form",
|
||||
"form_name": form["name"],
|
||||
"id": form["id"],
|
||||
"page": page_id,
|
||||
"questions": form["questions"],
|
||||
}
|
||||
)
|
||||
|
||||
frappe.errprint(form_doc.as_dict())
|
||||
form_doc.insert(ignore_permissions=True)
|
||||
|
||||
@ -37,6 +37,18 @@
|
||||
:type="field.type" :placeholder="field.placeholder" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="state.fbAccountPages.length">
|
||||
<FormControl type="autocomplete" :placeholder="__('Select an account page')"
|
||||
:options="state.fbAccountPages" :label="__('Facebook Page')"
|
||||
v-model="syncSource.facebook_page" />
|
||||
</div>
|
||||
|
||||
<div v-if="syncSource.facebook_page">
|
||||
<FormControl type="autocomplete" :placeholder="__('Select a lead gen form')"
|
||||
:options="leadFormsForSelectedPage" :label="__('Lead Form')"
|
||||
v-model="syncSource.facebook_lead_form" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -44,79 +56,131 @@
|
||||
<div v-if="selectedSourceType" class="flex justify-between mt-auto">
|
||||
<Button :label="__('Back')" variant="outline" :disabled="sources.insert.loading"
|
||||
@click="emit('updateStep', 'source-list')" />
|
||||
<Button :label="__('Create')" variant="solid" :loading="sources.insert.loading"
|
||||
|
||||
<Button v-if="state.fbPagesFetched" :label="__('Create')" variant="solid" :loading="sources.insert.loading"
|
||||
@click="createLeadSyncSource" />
|
||||
|
||||
<Button v-else="state.fbPagesFetched" :label="__('Fetch Account Pages')" variant="solid"
|
||||
:loading="state.fbPagesFetching" @click="getAccountPages(syncSource.access_token)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted } from "vue";
|
||||
import { FormControl, toast } from "frappe-ui";
|
||||
import { ref, inject, onMounted, reactive, watch, computed } from "vue";
|
||||
import { FormControl, toast, call } from "frappe-ui";
|
||||
import CircleAlert from "~icons/lucide/circle-alert";
|
||||
import { supportedSourceTypes } from "./leadSyncSourceConfig";
|
||||
import EmailProviderIcon from "../EmailProviderIcon.vue";
|
||||
|
||||
const syncSource = ref({
|
||||
name: "",
|
||||
type: "",
|
||||
access_token: "",
|
||||
name: "",
|
||||
type: "",
|
||||
access_token: "",
|
||||
facebook_page: "",
|
||||
facebook_lead_form: "",
|
||||
});
|
||||
|
||||
const emit = defineEmits()
|
||||
const state = reactive({
|
||||
fbPagesFetched: false,
|
||||
fbPagesFetching: false,
|
||||
fbAccountPages: [],
|
||||
});
|
||||
|
||||
const emit = defineEmits();
|
||||
|
||||
const props = defineProps({
|
||||
sourceData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
sourceData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const selectedSourceType = ref(supportedSourceTypes[0]);
|
||||
syncSource.value.type = selectedSourceType.value.name;
|
||||
|
||||
const sources = inject("sources");
|
||||
const fbSourceFields = [
|
||||
{
|
||||
name: "name",
|
||||
label: __("Name"),
|
||||
type: "text",
|
||||
placeholder: __("Add a name for your source"),
|
||||
},
|
||||
{
|
||||
name: "access_token",
|
||||
label: __("Access Token"),
|
||||
type: "password",
|
||||
placeholder: __("Enter your Facebook Access Token"),
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
label: __("Name"),
|
||||
type: "text",
|
||||
placeholder: __("Add a name for your source"),
|
||||
},
|
||||
{
|
||||
name: "access_token",
|
||||
label: __("Access Token"),
|
||||
type: "password",
|
||||
placeholder: __("Enter your Facebook Access Token"),
|
||||
},
|
||||
];
|
||||
|
||||
function handleSelect(sourceType) {
|
||||
selectedSourceType.value = sourceType;
|
||||
syncSource.value.type = sourceType.name;
|
||||
selectedSourceType.value = sourceType;
|
||||
syncSource.value.type = sourceType.name;
|
||||
}
|
||||
|
||||
function createLeadSyncSource() {
|
||||
sources.insert.submit({
|
||||
...syncSource.value
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success(__('New Lead Syncing Source created successfully'))
|
||||
emit('updateStep', 'edit-source', { ...syncSource.value })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.messages[0] || __('Failed to create source'))
|
||||
},
|
||||
})
|
||||
sources.insert.submit(
|
||||
{
|
||||
...syncSource.value,
|
||||
facebook_page: syncSource.value.facebook_page.id,
|
||||
facebook_lead_form: syncSource.value.facebook_lead_form.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(__("New Lead Syncing Source created successfully"));
|
||||
emit("updateStep", "edit-source", { ...syncSource.value });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.messages[0] || __("Failed to create source"));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const getAccountPages = (access_token) => {
|
||||
state.fbPagesFetching = true;
|
||||
call(
|
||||
"crm.lead_syncing.doctype.lead_sync_source.facebook.fetch_and_store_pages_from_facebook",
|
||||
{ access_token },
|
||||
)
|
||||
.then((data) => {
|
||||
state.fbPagesFetched = true;
|
||||
state.fbAccountPages = data.map((page) => ({
|
||||
label: page.name,
|
||||
value: page.id,
|
||||
...page,
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.messages[0] || __("Failed to fetch pages"));
|
||||
})
|
||||
.finally(() => {
|
||||
state.fbPagesFetching = false;
|
||||
});
|
||||
};
|
||||
|
||||
const leadFormsForSelectedPage = computed(() => {
|
||||
if (!state.fbAccountPages || !syncSource.value.facebook_page) {
|
||||
return [];
|
||||
}
|
||||
const selectedPage = state.fbAccountPages.find(
|
||||
(page) => page.id === syncSource.value.facebook_page.id,
|
||||
);
|
||||
return selectedPage.forms.map((form) => ({
|
||||
label: form.name,
|
||||
value: form.id,
|
||||
...form,
|
||||
}));
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.sourceData?.name) {
|
||||
Object.assign(syncSource.value, props.sourceData)
|
||||
syncSource.value.name = `${syncSource.value.name} - Copy`
|
||||
syncSource.value.enabled = true // Default to enabled
|
||||
}
|
||||
})
|
||||
if (props.sourceData?.name) {
|
||||
Object.assign(syncSource.value, props.sourceData);
|
||||
syncSource.value.name = `${syncSource.value.name} - Copy`;
|
||||
syncSource.value.enabled = true; // Default to enabled
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Loading…
x
Reference in New Issue
Block a user