448 lines
12 KiB
TypeScript
448 lines
12 KiB
TypeScript
import axios from 'axios';
|
|
import {
|
|
type DatabaseEventPayload,
|
|
type FunctionConfig,
|
|
type ObjectRecordCreateEvent,
|
|
type ObjectRecordUpdateEvent,
|
|
} from 'twenty-sdk';
|
|
import Twenty, { type Person } from '../generated';
|
|
|
|
const MAILCHIMP_API_URL: string =
|
|
process.env.MAILCHIMP_SERVER_PREFIX !== '' &&
|
|
process.env.MAILCHIMP_SERVER_PREFIX !== undefined
|
|
? `https://${process.env.MAILCHIMP_SERVER_PREFIX}.api.mailchimp.com/3.0/`
|
|
: '';
|
|
const MAILCHIMP_API_KEY: string = process.env.MAILCHIMP_API_KEY ?? '';
|
|
const MAILCHIMP_AUDIENCE_ID: string = process.env.MAILCHIMP_AUDIENCE_ID ?? '';
|
|
const IS_EMAIL_CONSTRAINT: boolean = process.env.IS_EMAIL_CONSTRAINT === 'true';
|
|
const IS_COMPANY_CONSTRAINT: boolean =
|
|
process.env.COMPANY_CONSTRAINT === 'true';
|
|
const IS_PHONE_CONSTRAINT: boolean = process.env.IS_PHONE_CONSTRAINT === 'true';
|
|
const IS_ADDRESS_CONSTRAINT: boolean =
|
|
process.env.IS_ADDRESS_CONSTRAINT === 'true';
|
|
const UPDATE_PERSON: boolean = process.env.UPDATE_PERSON === 'true';
|
|
|
|
type mailchimpAddress = {
|
|
street1: string;
|
|
street2: string;
|
|
city: string;
|
|
state: string;
|
|
zipCode: string;
|
|
country: string;
|
|
};
|
|
|
|
type mailchimpRecord = {
|
|
id?: string;
|
|
email_channel?: {
|
|
email: string;
|
|
marketing_consent?: {
|
|
status: string;
|
|
};
|
|
};
|
|
sms_channel?: {
|
|
sms_phone: string;
|
|
marketing_consent?: {
|
|
status: string;
|
|
};
|
|
};
|
|
mergeFields: {
|
|
FNAME: string;
|
|
LNAME: string;
|
|
ADDRESS: string | mailchimpAddress;
|
|
COMPANY: string;
|
|
PHONE: string;
|
|
};
|
|
};
|
|
|
|
type twentyAddress = {
|
|
addressStreet1: string;
|
|
addressStreet2: string;
|
|
addressCity: string;
|
|
addressState: string;
|
|
addressPostCode: string;
|
|
addressCountry: string;
|
|
};
|
|
|
|
type twentyCompany = {
|
|
name: string;
|
|
address: twentyAddress;
|
|
};
|
|
|
|
type twentyPerson = {
|
|
name: {
|
|
firstName: string;
|
|
lastName: string;
|
|
};
|
|
emails: {
|
|
primaryEmail: string;
|
|
};
|
|
phones: {
|
|
primaryPhoneNumber: string;
|
|
primaryPhoneCallingCode: string;
|
|
};
|
|
companyId: string | null;
|
|
};
|
|
|
|
type DatabaseEvent =
|
|
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
|
|
| DatabaseEventPayload<ObjectRecordUpdateEvent<Person>>;
|
|
|
|
const fetchCompanyData = async (companyId: string): Promise<twentyCompany> => {
|
|
const { company } = await new Twenty().query({
|
|
company: {
|
|
__args: { filter: { id: { eq: companyId } } },
|
|
name: true,
|
|
address: {
|
|
addressStreet1: true,
|
|
addressStreet2: true,
|
|
addressState: true,
|
|
addressPostcode: true,
|
|
addressCity: true,
|
|
addressCountry: true,
|
|
},
|
|
},
|
|
});
|
|
return company as unknown as twentyCompany;
|
|
};
|
|
|
|
const checkAddress = (address: twentyAddress): mailchimpAddress => {
|
|
if (
|
|
address.addressStreet1 !== '' &&
|
|
address.addressCity !== '' &&
|
|
address.addressPostCode !== '' &&
|
|
address.addressState !== ''
|
|
) {
|
|
return {
|
|
street1: address.addressStreet1,
|
|
street2: address.addressStreet2,
|
|
city: address.addressCity,
|
|
state: address.addressState,
|
|
zipCode: address.addressPostCode,
|
|
country: address.addressCountry,
|
|
} as mailchimpAddress;
|
|
}
|
|
throw new Error('Invalid address');
|
|
};
|
|
|
|
const checkAudiencePermissions = async (
|
|
audienceId: string,
|
|
): Promise<string[] | undefined> => {
|
|
const options = {
|
|
method: 'GET',
|
|
headers: {
|
|
Authorization: `Bearer ${MAILCHIMP_API_KEY}`,
|
|
},
|
|
url: `${MAILCHIMP_API_URL}/audiences/${audienceId}`,
|
|
};
|
|
try {
|
|
const temp = await axios.request(options);
|
|
return temp.data.enabled_channels;
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
const prepareData = (
|
|
firstName: string,
|
|
lastName: string,
|
|
email: string,
|
|
phoneNumber: string,
|
|
phoneCallingCode: string,
|
|
companyName: string,
|
|
address: mailchimpAddress | string,
|
|
): mailchimpRecord => {
|
|
let data = {
|
|
mergeFields: {},
|
|
} as mailchimpRecord;
|
|
data.mergeFields.FNAME = firstName;
|
|
data.mergeFields.LNAME = lastName;
|
|
if (IS_EMAIL_CONSTRAINT) {
|
|
data.email_channel = {
|
|
email: email,
|
|
marketing_consent: {
|
|
status: 'unknown',
|
|
},
|
|
};
|
|
}
|
|
if (IS_ADDRESS_CONSTRAINT) {
|
|
data.mergeFields.ADDRESS = address;
|
|
}
|
|
if (IS_PHONE_CONSTRAINT) {
|
|
const mergedPhoneNumber: string = phoneCallingCode.startsWith('+')
|
|
? phoneCallingCode.concat(phoneNumber)
|
|
: '+'.concat(phoneCallingCode, phoneNumber);
|
|
data['sms_channel'] = {
|
|
sms_phone: mergedPhoneNumber,
|
|
marketing_consent: {
|
|
status: 'unknown',
|
|
},
|
|
};
|
|
data['mergeFields']['PHONE'] = mergedPhoneNumber;
|
|
}
|
|
if (IS_COMPANY_CONSTRAINT) {
|
|
data['mergeFields']['COMPANY'] = companyName;
|
|
}
|
|
return data;
|
|
};
|
|
|
|
const addTwentyPersonToMailchimp = async (
|
|
convertedRecord: mailchimpRecord,
|
|
): Promise<boolean | undefined> => {
|
|
const options = {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${MAILCHIMP_API_KEY}`,
|
|
},
|
|
url: `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts`,
|
|
data: convertedRecord,
|
|
};
|
|
try {
|
|
const response = await axios.request(options);
|
|
return response.status === 200;
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
const checkIfTwentyPersonExistsInMailchimp = async (
|
|
email?: string,
|
|
phoneNumber?: string,
|
|
cursor?: string,
|
|
) => {
|
|
const options = {
|
|
method: 'GET',
|
|
headers: {
|
|
Authorization: `Bearer ${MAILCHIMP_API_KEY}`,
|
|
},
|
|
url: cursor
|
|
? `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts?count%3D1000%26cursor%3D${cursor}`
|
|
: `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts?count%3D1000`,
|
|
};
|
|
try {
|
|
const response = await axios.request(options);
|
|
const doesPersonExist: mailchimpRecord | undefined =
|
|
(response.data.contacts.find(
|
|
(contact: any) => contact.email_channel.email === email,
|
|
) as mailchimpRecord) ||
|
|
(response.data.contacts.find(
|
|
(contact: any) => contact.sms_channel.sms_phone === phoneNumber,
|
|
) as mailchimpRecord);
|
|
if (doesPersonExist !== undefined) {
|
|
return doesPersonExist;
|
|
}
|
|
if (response.data.next_cursor === undefined) {
|
|
return undefined;
|
|
} else {
|
|
await checkIfTwentyPersonExistsInMailchimp(
|
|
email,
|
|
phoneNumber,
|
|
response.data.next_cursor,
|
|
);
|
|
}
|
|
return undefined;
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
const compareTwoRecords = (
|
|
object1: mailchimpRecord,
|
|
object2: mailchimpRecord,
|
|
) => {
|
|
return (
|
|
object1.email_channel?.email === object2.email_channel?.email &&
|
|
object1.sms_channel?.sms_phone === object2.sms_channel?.sms_phone &&
|
|
object1.mergeFields.FNAME === object2.mergeFields.FNAME &&
|
|
object1.mergeFields.LNAME === object2.mergeFields.LNAME &&
|
|
object1.mergeFields.ADDRESS === object2.mergeFields.ADDRESS &&
|
|
object1.mergeFields.COMPANY === object2.mergeFields.COMPANY
|
|
);
|
|
};
|
|
|
|
const updateTwentyPersonInMailchimp = async (
|
|
mailchimpRecordId: string,
|
|
twentyConvertedRecord: mailchimpRecord,
|
|
): Promise<boolean | undefined> => {
|
|
const options = {
|
|
method: 'PATCH',
|
|
headers: {
|
|
Authorization: `Bearer ${MAILCHIMP_API_KEY}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
url: `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts/${mailchimpRecordId}`,
|
|
data: twentyConvertedRecord,
|
|
};
|
|
try {
|
|
const response = await axios.request(options);
|
|
return response.status === 200;
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const main = async (
|
|
params: DatabaseEvent,
|
|
): Promise<object | undefined> => {
|
|
if (!IS_EMAIL_CONSTRAINT && !IS_PHONE_CONSTRAINT) {
|
|
console.warn(
|
|
'Function exited as there are no constraints to email nor phone number',
|
|
);
|
|
return {};
|
|
}
|
|
if (
|
|
MAILCHIMP_API_URL === '' ||
|
|
MAILCHIMP_API_KEY === '' ||
|
|
MAILCHIMP_AUDIENCE_ID === ''
|
|
) {
|
|
console.warn('Missing Mailchimp required parameters');
|
|
return {};
|
|
}
|
|
|
|
try {
|
|
const { properties } = params;
|
|
const twentyRecord: twentyPerson = properties.after as twentyPerson;
|
|
if (
|
|
twentyRecord.name.firstName === '' ||
|
|
twentyRecord.name.lastName === ''
|
|
) {
|
|
throw new Error('First or last name is empty');
|
|
}
|
|
|
|
const audiencePermissions: string[] | undefined =
|
|
await checkAudiencePermissions(MAILCHIMP_AUDIENCE_ID);
|
|
|
|
if (
|
|
IS_EMAIL_CONSTRAINT &&
|
|
audiencePermissions?.includes('Email') &&
|
|
twentyRecord.emails.primaryEmail === ''
|
|
) {
|
|
throw new Error('Email is empty');
|
|
}
|
|
|
|
if (
|
|
IS_PHONE_CONSTRAINT &&
|
|
audiencePermissions?.includes('SMS') &&
|
|
(twentyRecord.phones.primaryPhoneNumber === '' ||
|
|
twentyRecord.phones.primaryPhoneCallingCode === '')
|
|
) {
|
|
throw new Error('Phone number is empty');
|
|
}
|
|
|
|
let companyName: string = '';
|
|
let address: mailchimpAddress | string = '';
|
|
if (IS_COMPANY_CONSTRAINT) {
|
|
if (twentyRecord.companyId === null) {
|
|
throw new Error('Missing relation to company record');
|
|
}
|
|
const company: twentyCompany = await fetchCompanyData(
|
|
twentyRecord.companyId,
|
|
);
|
|
companyName = company.name ?? ''; // either "" or name
|
|
address = IS_ADDRESS_CONSTRAINT ? checkAddress(company.address) : '';
|
|
}
|
|
|
|
const twentyPersonToMailchimpRecord: mailchimpRecord = prepareData(
|
|
twentyRecord.name.firstName,
|
|
twentyRecord.name.lastName,
|
|
twentyRecord.emails.primaryEmail,
|
|
twentyRecord.phones.primaryPhoneNumber,
|
|
twentyRecord.phones.primaryPhoneCallingCode,
|
|
companyName,
|
|
address,
|
|
);
|
|
console.log(twentyPersonToMailchimpRecord);
|
|
|
|
const isTwentyPersonInMailchimp: mailchimpRecord | undefined =
|
|
await checkIfTwentyPersonExistsInMailchimp(
|
|
twentyRecord.emails.primaryEmail,
|
|
twentyPersonToMailchimpRecord.sms_channel?.sms_phone,
|
|
);
|
|
if (isTwentyPersonInMailchimp !== undefined) {
|
|
console.log(
|
|
`Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} found in Mailchimp`,
|
|
);
|
|
if (UPDATE_PERSON) {
|
|
if (
|
|
!compareTwoRecords(
|
|
isTwentyPersonInMailchimp,
|
|
twentyPersonToMailchimpRecord,
|
|
) &&
|
|
isTwentyPersonInMailchimp.id
|
|
) {
|
|
const isTwentyPersonUpdatedInMailchimp: boolean | undefined =
|
|
await updateTwentyPersonInMailchimp(
|
|
isTwentyPersonInMailchimp.id,
|
|
twentyPersonToMailchimpRecord,
|
|
);
|
|
if (!isTwentyPersonUpdatedInMailchimp) {
|
|
throw new Error(
|
|
`Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} update in Mailchimp failed`,
|
|
);
|
|
} else {
|
|
console.log(
|
|
`Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} update in Mailchimp succeeded`,
|
|
);
|
|
}
|
|
} else {
|
|
console.log(
|
|
`Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} wasn't updated in Mailchimp because they're the same`,
|
|
);
|
|
}
|
|
} else {
|
|
console.log(
|
|
`Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} wasn't updated in Mailchimp because UPDATE_PERSON is set to false`,
|
|
);
|
|
}
|
|
} else {
|
|
console.log(
|
|
`${twentyRecord.name.firstName} ${twentyRecord.name.lastName} doesn't exist in Mailchimp, adding`,
|
|
);
|
|
const isTwentyPersonAddedToMailchimp: boolean | undefined =
|
|
await addTwentyPersonToMailchimp(twentyPersonToMailchimpRecord);
|
|
if (!isTwentyPersonAddedToMailchimp) {
|
|
throw new Error(
|
|
`Adding ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} person to Mailchimp failed`,
|
|
);
|
|
} else {
|
|
console.log(
|
|
`Person ${twentyRecord.name.firstName} ${twentyRecord.name.lastName} has been successfully added`,
|
|
);
|
|
}
|
|
}
|
|
return {};
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
console.error(error.response);
|
|
return {};
|
|
}
|
|
console.error(error);
|
|
return {};
|
|
}
|
|
};
|
|
|
|
export const config: FunctionConfig = {
|
|
universalIdentifier: '83319670-775b-4862-b133-5c353e594151',
|
|
name: 'mailchimp-synchronizer',
|
|
triggers: [
|
|
{
|
|
universalIdentifier: 'e627ff6f-0a0c-48b2-bdbb-31967489ec96',
|
|
type: 'databaseEvent',
|
|
eventName: 'person.created',
|
|
},
|
|
{
|
|
universalIdentifier: '657ece26-4478-4408-a257-4e9e16cce279',
|
|
type: 'databaseEvent',
|
|
eventName: 'person.updated',
|
|
},
|
|
],
|
|
};
|