Hacktober apps fix (#15733)

- update dependencies in mailchimp, stripe and last email interaction
apps
- fix logic in mailchimp integration, now it's triggered by update of
People records and allows for update Mailchimp records
- fix logic in stripe integration, now properly reads data from webhook
- update READMEs to make it more understandable to non-technical users
This commit is contained in:
BOHEUS
2025-11-10 08:37:09 +00:00
committed by GitHub
parent 912c668eb5
commit ae2d399d6e
21 changed files with 751 additions and 318 deletions
@@ -17,5 +17,5 @@ twenty app sync
## Flow
- Checks if fields are created, if not, creates them on fly
- Extracts the timedate of message and calculates the last interaction status
- Extracts the datetime of message and calculates the last interaction status
- Fetches all users and companies connected to them and updates their Last interaction and Interaction status fields
@@ -9,13 +9,11 @@ const config: ApplicationConfig = {
TWENTY_API_KEY: {
universalIdentifier: 'aae3f523-4c1f-4805-b3ee-afeb676c381e',
isSecret: true,
value: '',
description: 'Required to send requests to Twenty',
},
TWENTY_API_URL: {
universalIdentifier: '6d19bb04-45bb-46aa-a4e5-4a2682c7b19d',
isSecret: false,
value: '',
description: 'Optional, defaults to cloud API URL',
},
},
@@ -10,7 +10,7 @@
"packageManager": "yarn@4.9.2",
"dependencies": {
"axios": "^1.12.2",
"twenty-sdk": "^0.0.3"
"twenty-sdk": "^0.0.4"
},
"devDependencies": {
"@types/node": "^24.7.2"
@@ -1,5 +1,4 @@
import axios from 'axios';
import { setTimeout } from 'timers/promises';
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '';
@@ -7,7 +6,6 @@ const TWENTY_URL =
process.env.TWENTY_API_URL !== '' && process.env.TWENTY_API_URL !== undefined
? `${process.env.TWENTY_API_URL}/rest`
: 'https://api.twenty.com/rest';
const DELAY = 500;
const create_last_interaction = (id: string) => {
return {
@@ -100,6 +98,53 @@ const interactionData = (date: string, status: string) => {
};
};
const updateInteractionStatus = async (objectName: string, id: string, messageDate: string, status: string) => {
const options = {
method: 'PATCH',
url: `${TWENTY_URL}/${objectName}/${id}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
data: {
lastInteraction: messageDate,
interactionStatus: status
}
};
try {
const response = await axios.request(options);
if (response.status === 200) {
console.log('Successfully updated company last interaction field');
}
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
}
const fetchRelatedCompanyId = async (id: string) => {
const options = {
method: 'GET',
url: `${TWENTY_URL}/people/${id}`,
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
};
try {
const req = await axios.request(options);
if (req.status === 200 && req.data.person.companyId !== null && req.data.person.companyId !== undefined) {
return req.data.person.companyId;
}
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
}
export const main = async (params: {
properties: Record<string, any>;
recordId: string;
@@ -147,7 +192,6 @@ export const main = async (params: {
if (response2.status === 201) {
console.log('Successfully created company last interaction field');
}
await setTimeout(DELAY);
}
if (company_interaction_status === undefined) {
const response2 = await axios.request(
@@ -156,7 +200,6 @@ export const main = async (params: {
if (response2.status === 201) {
console.log('Successfully created company interaction status field');
}
await setTimeout(DELAY);
}
if (person_last_interaction === undefined) {
const response2 = await axios.request(
@@ -165,7 +208,6 @@ export const main = async (params: {
if (response2.status === 201) {
console.log('Successfully created person last interaction field');
}
await setTimeout(DELAY);
}
if (person_interaction_status === undefined) {
const response2 = await axios.request(
@@ -174,11 +216,10 @@ export const main = async (params: {
if (response2.status === 201) {
console.log('Successfully created person interaction status field');
}
await setTimeout(DELAY);
}
// Extract the timestamp of message
const messageDate = properties.receivedAt;
const messageDate = properties.after.receivedAt;
const interactionStatus = calculateStatus(messageDate);
// Get the details of person and related company
@@ -190,8 +231,7 @@ export const main = async (params: {
},
};
const messageDetails = await axios.request(messageOptions);
await setTimeout(DELAY);
const peopleIds = [];
const peopleIds: string[] = [];
for (const participant of messageDetails.data.messages
.messageParticipants) {
peopleIds.push(participant.personId);
@@ -199,78 +239,22 @@ export const main = async (params: {
const companiesIds = [];
for (const id of peopleIds) {
const options = {
method: 'GET',
url: `${TWENTY_URL}/people/${id}`,
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
};
try {
const req = await axios.request(options);
companiesIds.push(req.data.person.companyId);
await setTimeout(DELAY);
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
companiesIds.push(await fetchRelatedCompanyId(id));
}
// Update the field value depending on the timestamp
for (const id of peopleIds) {
const peopleOptions = {
method: 'PATCH',
url: `${TWENTY_URL}/people/${id}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
data: interactionData(messageDate, interactionStatus),
};
try {
const response = await axios.request(options);
if (response.status === 200) {
console.log('Successfully updated company last interaction field');
await setTimeout(DELAY);
}
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
await updateInteractionStatus("people", id, messageDate, interactionStatus);
}
for (const id of companiesIds) {
const companiesOptions = {
method: 'PATCH',
url: `${TWENTY_URL}/companies/${id}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
data: interactionData(messageDate, interactionStatus),
};
try {
const req = await axios.request(companiesOptions);
if (req.status === 200) {
console.log(`Successfully updated company with ID ${id}`);
await setTimeout(DELAY);
}
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
await updateInteractionStatus("companies", id, messageDate, interactionStatus);
}
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error);
console.error(error.message);
return {};
}
console.log(error);
console.error(error);
return {};
}
};
@@ -5,13 +5,14 @@ __metadata:
version: 8
cacheKey: 10c0
"Last email interaction@workspace:.":
version: 0.0.0-use.local
resolution: "Last email interaction@workspace:."
"@types/node@npm:^24.7.2":
version: 24.10.0
resolution: "@types/node@npm:24.10.0"
dependencies:
axios: "npm:^1.12.2"
languageName: unknown
linkType: soft
undici-types: "npm:~7.16.0"
checksum: 10c0/f82ed7194e16f5590ef7afdc20c6d09068c76d50278b485ede8f0c5749683536e3064ffa8def8db76915196afb3724b854aa5723c64d6571b890b14492943b46
languageName: node
linkType: hard
"async-function@npm:^1.0.0":
version: 1.0.0
@@ -217,6 +218,16 @@ __metadata:
languageName: node
linkType: hard
"last-email-interaction@workspace:.":
version: 0.0.0-use.local
resolution: "last-email-interaction@workspace:."
dependencies:
"@types/node": "npm:^24.7.2"
axios: "npm:^1.12.2"
twenty-sdk: "npm:^0.0.4"
languageName: unknown
linkType: soft
"math-intrinsics@npm:^1.1.0":
version: 1.1.0
resolution: "math-intrinsics@npm:1.1.0"
@@ -246,3 +257,17 @@ __metadata:
checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b
languageName: node
linkType: hard
"twenty-sdk@npm:^0.0.4":
version: 0.0.4
resolution: "twenty-sdk@npm:0.0.4"
checksum: 10c0/550f1d85bf0701396c9dd2d4c6bc55ba1b067fce13636f8540eec60ab6a4257c6d7cd86cb3f62e0974bf99467bc31270d92b17b9681a1b7a6281b7ef97224080
languageName: node
linkType: hard
"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
languageName: node
linkType: hard
@@ -5,6 +5,7 @@ Synchronizing contacts between Twenty and Mailchimp
## Requirements
- twenty-cli `npm install -g twenty-cli`
- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one
- Mailchimp API key - Mailchimp > avatar in top right corner > Profile > Extras > API keys
## Setup
1. Add app to your workspace
@@ -13,13 +14,20 @@ twenty auth login
cd mailchimp-synchronizer
twenty app sync
```
2. Go to Settings > Integrations > Mailchimp synchronizer > Settings and add required variables
## Flow
- Check if required variables are set, if not, exit
- Validate data based on set constraints
- If all constraints are checked, send a request to Mailchimp with new contact
- Validate data based on set constraints, if data doesn't match constraints, exit
- Check if person already exists in Mailchimp:
- if yes, check if UPDATE_PERSON is set to true
- if UPDATE_PERSON is true, check if Twenty record is the same as Mailchimp record
- if they're the same, exit
- if not, update
- if UPDATE_PERSON is false, exit
- if person doesn't exist in Mailchimp, send a request to Mailchimp with new contact
## Note
- SMS support is experimental and may cause errors
- SMS support is experimental and may cause errors
- constraints are directly responsible for sent data so if e.g. you want to have a company name in
Mailchimp, you have to set IS_COMPANY_CONSTRAINT to true
@@ -3,63 +3,67 @@ import { type ApplicationConfig } from 'twenty-sdk/application';
const config: ApplicationConfig = {
universalIdentifier: '1eadac4e-db9f-4cce-b20b-de75f41e34dc',
displayName: 'Mailchimp synchronizer',
description: '',
description: 'Synchronizes Twenty contacts in Mailchimp',
icon: "IconMailFast",
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: '0af17af3-66b8-40cf-b6e2-6a29a1da5464',
isSecret: true,
value: '',
description: 'Required to send requests to Twenty',
},
TWENTY_API_URL: {
universalIdentifier: '12949c1c-aed7-4a9f-bd06-9fd15f0bfa63',
value: '',
isSecret: false,
description: 'Optional, defaults to cloud API URL',
},
MAILCHIMP_API_KEY: {
universalIdentifier: 'f10d4e8a-8055-4eb2-b9ad-efd69d43b1f0',
isSecret: true,
value: '',
description: 'Required to send requests to Mailchimp',
},
MAILCHIMP_URL: {
MAILCHIMP_SERVER_PREFIX: {
universalIdentifier: '6c8b6ac9-dd45-4f0b-a397-c4a38edccfd9',
value: '',
isSecret: false,
description: 'Required to send requests to Mailchimp (it\'s found in url, e.g. https://us9.admin.mailchimp.com > us9 is prefix)',
},
MAILCHIMP_AUDIENCE_ID: {
universalIdentifier: '5492f06f-bb29-4c93-9436-b4736a396376',
isSecret: false,
description: 'Required to send requests to Mailchimp',
},
IS_EMAIL_CONSTRAINT: {
universalIdentifier: '62626c57-470f-4866-be1e-5b4d7ec09f9f',
isSecret: false,
value: 'false',
description:
'Set to true if you want to add additional constraint (default is false)',
},
IS_PHONE_CONSTRAINT: {
universalIdentifier: 'fac8ec5b-dade-46bf-b938-3dfdef0aa298',
isSecret: false,
value: 'false',
description:
'Set to true if you want to add additional constraint (default is false)',
},
IS_COMPANY_CONSTRAINT: {
universalIdentifier: '9ffd8e76-4ab2-42f9-8549-3622a5ae2343',
isSecret: false,
value: 'false',
description:
'Set to true if you want to add additional constraint (default is false)',
},
IS_ADDRESS_CONSTRAINT: {
universalIdentifier: '4b899eb6-517e-4afd-bbf8-88097900ea42',
isSecret: false,
value: 'false',
description:
'Set to true if you want to add additional constraint (default is false)',
},
UPDATE_PERSON: {
universalIdentifier: '9d753e1e-4408-40ca-b0f0-5c7e8625c2aa',
isSecret: true,
value: 'false',
description: 'Set to true if you want to update record if it exists',
},
MAILCHIMP_AUDIENCE_ID: {
universalIdentifier: '5492f06f-bb29-4c93-9436-b4736a396376',
value: '',
description: 'Required to send requests to Mailchimp',
description: 'Set to true if you want to update record in Mailchimp if it exists',
},
},
};
@@ -9,8 +9,8 @@
},
"packageManager": "yarn@4.9.2",
"dependencies": {
"axios": "^1.13.1",
"twenty-sdk": "^0.0.3"
"axios": "^1.13.2",
"twenty-sdk": "^0.0.4"
},
"devDependencies": {
"@types/node": "^24.7.2"
@@ -7,16 +7,19 @@ const TWENTY_API_URL: string =
: 'https://api.twenty.com/rest';
const TWENTY_API_KEY: string = process.env.TWENTY_API_KEY ?? '';
const MAILCHIMP_API_URL: string =
process.env.MAILCHIMP_URL !== '' && process.env.MAILCHIMP_URL !== undefined
? `https://${process.env.MAILCHIMP_URL}.api.mailchimp.com/3.0/`
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_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';
process.env.IS_ADDRESS_CONSTRAINT === 'true';
const UPDATE_PERSON: boolean = process.env.UPDATE_PERSON === 'true';
type mailchimpAddress = {
street1: string;
@@ -27,6 +30,29 @@ type mailchimpAddress = {
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;
@@ -36,22 +62,27 @@ type twentyAddress = {
addressCountry: string;
};
type twentyCompany = {
name: string;
address: twentyAddress;
};
type twentyPerson = {
name: {
firstName: string;
lastName: string;
};
email: {
emails: {
primaryEmail: string;
};
phones: {
primaryPhoneNumber: string;
primaryPhoneCallingCode: string;
};
companyId: string;
companyId: string | null;
};
const fetchCompanyData = async (companyId: string): Promise<object> => {
const fetchCompanyData = async (companyId: string): Promise<twentyCompany> => {
const options = {
method: 'GET',
headers: {
@@ -60,11 +91,13 @@ const fetchCompanyData = async (companyId: string): Promise<object> => {
url: `${TWENTY_API_URL}/company/${companyId}`,
};
try {
const temp = await axios.request(options);
return {
name: temp.data.name as string,
address: temp.data.address as twentyAddress,
};
const response = await axios.request(options);
return response.status === 200
? ({
name: response.data.name as string,
address: response.data.address as twentyAddress,
} as twentyCompany)
: ({} as twentyCompany);
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
@@ -94,7 +127,7 @@ const checkAddress = (address: twentyAddress): mailchimpAddress => {
const checkAudiencePermissions = async (
audienceId: string,
): Promise<string[]> => {
): Promise<string[] | undefined> => {
const options = {
method: 'GET',
headers: {
@@ -109,7 +142,6 @@ const checkAudiencePermissions = async (
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
};
@@ -120,23 +152,23 @@ const prepareData = (
phoneNumber: string,
phoneCallingCode: string,
companyName: string,
address: mailchimpAddress | null,
): object => {
const data = {} as any;
address: mailchimpAddress | string,
): mailchimpRecord => {
let data = {
mergeFields: {},
} as mailchimpRecord;
data.mergeFields.FNAME = firstName;
data.mergeFields.LNAME = lastName;
if (IS_EMAIL_CONSTRAINT) {
data['email_channel'] = {
data.email_channel = {
email: email,
marketing_consent: {
status: 'unknown',
},
};
}
data['mergeFields'] = {
FNAME: firstName,
LNAME: lastName,
};
if (IS_ADDRESS_CONSTRAINT) {
data['mergeFields']['ADDRESS'] = address;
data.mergeFields.ADDRESS = address;
}
if (IS_PHONE_CONSTRAINT) {
const mergedPhoneNumber: string = phoneCallingCode.startsWith('+')
@@ -156,110 +188,247 @@ const prepareData = (
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: {
properties: Record<string, any>;
recordId: string;
userId: string;
}): Promise<object> => {
}): Promise<object | undefined> => {
if (!IS_EMAIL_CONSTRAINT && !IS_PHONE_CONSTRAINT) {
console.log(
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.log('Missing Mailchimp required parameters');
console.warn('Missing Mailchimp required parameters');
return {};
}
if (IS_COMPANY_CONSTRAINT && TWENTY_API_KEY === '') {
console.log('Missing Twenty related parameters');
console.warn('Missing Twenty related parameters');
return {};
}
try {
const { properties, recordId } = params;
console.log(properties);
console.log(recordId);
const twentyRecord: twentyPerson = properties as twentyPerson;
const email: boolean =
IS_EMAIL_CONSTRAINT && twentyRecord.email.primaryEmail !== '';
const phoneNumber: boolean =
IS_PHONE_CONSTRAINT &&
twentyRecord.phones.primaryPhoneNumber !== '' &&
twentyRecord.phones.primaryPhoneCallingCode !== '';
const company: any = IS_COMPANY_CONSTRAINT
? await fetchCompanyData(properties.after.companyId)
: null;
const companyName: string = company['name'] !== '' ? company['name'] : null;
const address: mailchimpAddress | null = IS_ADDRESS_CONSTRAINT
? checkAddress(company['address'])
: null;
const { properties } = params;
const twentyRecord: twentyPerson = properties.after as twentyPerson;
if (
twentyRecord.name.firstName === '' ||
twentyRecord.name.lastName === ''
) {
console.error('First or last name is empty');
return {};
throw new Error('First or last name is empty');
}
const audiencePermissions: string[] = await checkAudiencePermissions(
MAILCHIMP_AUDIENCE_ID,
);
const audiencePermissions: string[] | undefined =
await checkAudiencePermissions(MAILCHIMP_AUDIENCE_ID);
if (
IS_EMAIL_CONSTRAINT &&
audiencePermissions.includes('Email') &&
!email
audiencePermissions?.includes('Email') &&
twentyRecord.emails.primaryEmail === ''
) {
console.error('Email is empty');
return {};
throw new Error('Email is empty');
}
if (
IS_PHONE_CONSTRAINT &&
audiencePermissions.includes('SMS') &&
!phoneNumber
audiencePermissions?.includes('SMS') &&
(twentyRecord.phones.primaryPhoneNumber === '' ||
twentyRecord.phones.primaryPhoneCallingCode === '')
) {
console.error('Phone number is empty');
return {};
throw new Error('Phone number is empty');
}
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${MAILCHIMP_API_KEY}`,
},
url: `${MAILCHIMP_API_URL}/audiences/${MAILCHIMP_AUDIENCE_ID}/contacts`,
data: prepareData(
twentyRecord.name.firstName,
twentyRecord.name.lastName,
twentyRecord.email.primaryEmail,
twentyRecord.phones.primaryPhoneNumber,
twentyRecord.phones.primaryPhoneCallingCode,
companyName,
address,
),
};
const temp = await axios.request(options);
if (temp.status === 200) {
console.log('Person has been successfully added');
return {};
} else {
throw temp;
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.message);
console.error(error.response);
return {};
}
console.error(error);
@@ -276,5 +445,10 @@ export const config: ServerlessFunctionConfig = {
type: 'databaseEvent',
eventName: 'person.created',
},
{
universalIdentifier: '657ece26-4478-4408-a257-4e9e16cce279',
type: 'databaseEvent',
eventName: 'person.updated',
},
],
};
@@ -5,6 +5,15 @@ __metadata:
version: 8
cacheKey: 10c0
"@types/node@npm:^24.7.2":
version: 24.10.0
resolution: "@types/node@npm:24.10.0"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/f82ed7194e16f5590ef7afdc20c6d09068c76d50278b485ede8f0c5749683536e3064ffa8def8db76915196afb3724b854aa5723c64d6571b890b14492943b46
languageName: node
linkType: hard
"async-function@npm:^1.0.0":
version: 1.0.0
resolution: "async-function@npm:1.0.0"
@@ -26,14 +35,14 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.13.1":
version: 1.13.1
resolution: "axios@npm:1.13.1"
"axios@npm:^1.13.2":
version: 1.13.2
resolution: "axios@npm:1.13.2"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.4"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/de9c3c6de43d3ee1146d3afe78645f19450cac6a5d7235bef8b8e8eeb705c2e47e2d231dea99cecaec4dae1897c521118ca9413b9d474063c719c4d94c5b9adc
checksum: 10c0/e8a42e37e5568ae9c7a28c348db0e8cf3e43d06fcbef73f0048669edfe4f71219664da7b6cc991b0c0f01c28a48f037c515263cb79be1f1ae8ff034cd813867b
languageName: node
linkType: hard
@@ -213,7 +222,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "mailchimp-synchronizer@workspace:."
dependencies:
axios: "npm:^1.13.1"
"@types/node": "npm:^24.7.2"
axios: "npm:^1.13.2"
twenty-sdk: "npm:^0.0.4"
languageName: unknown
linkType: soft
@@ -246,3 +257,17 @@ __metadata:
checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b
languageName: node
linkType: hard
"twenty-sdk@npm:^0.0.4":
version: 0.0.4
resolution: "twenty-sdk@npm:0.0.4"
checksum: 10c0/550f1d85bf0701396c9dd2d4c6bc55ba1b067fce13636f8540eec60ab6a4257c6d7cd86cb3f62e0974bf99467bc31270d92b17b9681a1b7a6281b7ef97224080
languageName: node
linkType: hard
"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
languageName: node
linkType: hard
@@ -5,7 +5,7 @@ Synchronizes customers from Stripe to Twenty
## Requirements
- twenty-cli `npm install -g twenty-cli`
- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one
-
- Stripe secret API key - available in Stripe workbench
## Setup
1. Synchronize app
@@ -14,15 +14,24 @@ twenty auth login
cd stripe-synchronizer
twenty app sync
```
2. Go to Settings > Integrations > Stripe synchronizer > Settings and add values
2. Go to Stripe > Workbench > Webhooks and add webhook:
- events: customer.subscription.created and customer.subscription.updated
- webhook endpoint
- destination: `{TWENTY_URL}/s/webhook/stripe`, e.g. https://workspace.twenty.com/s/webhook/stripe
3. Go to Twenty > Settings > Integrations > Stripe synchronizer > Settings and add values
## Flow
1. Retrieve webhook
2. Check if it's either subscription created or updated
3. Read customer ID, sub status and quantity
4. Read customer data
1. Retrieve Stripe webhook
2. Check if it's either subscription created or updated, if not, exit
3. Read customer ID, sub status and quantity from webhook
4. Read customer data from Stripe API, if business name is empty, exit
5. Check if customer company exists in Twenty, if not, create it
6. Check if related person exists in Twenty, if not, create it and link to company
## Notes
- app synchronizes only new customers, those created before start of app won't be synchronized unless they're updated
- customers will be added to Twenty People object only if their name and email are filled with data, otherwise app will throw an error
## Todo
- add validation of signature key from Stripe to ensure that incoming request is valid
- add validation of signature key from Stripe to ensure that incoming request is valid
(possible once request headers are exposed to serverless functions)
@@ -4,23 +4,21 @@ const config: ApplicationConfig = {
universalIdentifier: '0ed2bcb8-64ab-4ca1-b875-eeabf41b5f95',
displayName: 'Stripe synchronizer',
description: 'Plugin synchronizing data from Stripe to Twenty',
icon: "IconMoneyBag",
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: 'b0d9569b-da3e-4dad-b7b1-36c96f0598b9',
isSecret: true,
value: '',
description: 'Required to send requests to Twenty',
},
TWENTY_API_URL: {
universalIdentifier: 'fa50e016-e045-497a-9cdf-0949e7ef9f7a',
isSecret: false,
value: '',
description: 'Optional, defaults to cloud API URL',
},
STRIPE_API_KEY: {
universalIdentifier: '807d67d6-f720-49c4-a93e-ef16cf4fe919',
isSecret: true,
value: '',
description: 'Required to send request to Stripe',
},
},
@@ -9,8 +9,8 @@
},
"packageManager": "yarn@4.9.2",
"dependencies": {
"axios": "^1.13.1",
"twenty-sdk": "^0.0.3"
"axios": "^1.13.2",
"twenty-sdk": "^0.0.4"
},
"devDependencies": {
"@types/node": "^24.7.2"
@@ -1,5 +1,6 @@
import axios from 'axios';
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
import { type stripeCustomer, type stripeEvent, type stripeStatus, type twentyObject } from './types';
const TWENTY_API_KEY: string = process.env.TWENTY_API_KEY ?? '';
const TWENTY_API_URL: string =
@@ -9,43 +10,9 @@ const TWENTY_API_URL: string =
const STRIPE_API_KEY: string = process.env.STRIPE_API_KEY ?? '';
const STRIPE_API_URL: string = 'https://api.stripe.com/v1/customers';
enum stripeStatus {
Incomplete = 'INCOMPLETE',
IncompleteExpired = 'INCOMPLETE_EXPIRED',
Trialing = 'TRIALING',
Active = 'ACTIVE',
PastDue = 'PAST_DUE',
Canceled = 'CANCELED',
Unpaid = 'UNPAID',
Paused = 'PAUSED',
}
type stripeData = {
quantity: number;
};
type stripeItems = {
data: stripeData[];
};
type stripeResponse = {
customer: string;
items: stripeItems;
status: stripeStatus;
type: string;
};
type stripeCustomer = {
businessName: string;
};
type twentyObject = {
id: string;
nameSingular: string;
fields: Record<string, any>[];
};
const getCompaniesObject = async (): Promise<twentyObject | undefined> => {
const getTwentyObjectData = async (
objectSingularName: string,
): Promise<twentyObject | undefined> => {
const options = {
method: 'GET',
headers: {
@@ -57,7 +24,7 @@ const getCompaniesObject = async (): Promise<twentyObject | undefined> => {
const response = await axios.request(options);
if (response.status === 200) {
const companyObject = response.data.data.objects.find(
(object: twentyObject) => object.nameSingular === 'company',
(object: twentyObject) => object.nameSingular === objectSingularName,
);
return (companyObject as twentyObject) ?? ({} as twentyObject);
}
@@ -76,59 +43,61 @@ const createFields = async (objectId: string, fieldName: string) => {
objectMetadataId: objectId,
name: 'seats',
label: 'Seats',
icon: 'IconMan',
}
: {
type: 'SELECT',
objectMetadataId: objectId,
name: 'subStatus',
label: 'Sub Status',
icon: 'IconStatusChange',
options: [
{
color: 'iris',
label: 'Incomplete',
value: stripeStatus.Incomplete,
value: 'INCOMPLETE',
position: 1,
},
{
color: 'sky',
label: 'Incomplete (expired)',
value: stripeStatus.IncompleteExpired,
value: 'INCOMPLETE_EXPIRED',
position: 2,
},
{
color: 'amber',
label: 'Trialing',
value: stripeStatus.Trialing,
value: 'TRIALING',
position: 3,
},
{
color: 'green',
label: 'Active',
value: stripeStatus.Active,
value: 'ACTIVE',
position: 4,
},
{
color: 'orange',
label: 'Past due',
value: stripeStatus.PastDue,
value: 'PAST_DUE',
position: 5,
},
{
color: 'brown',
label: 'Canceled',
value: stripeStatus.Canceled,
value: 'CANCELED',
position: 6,
},
{
color: 'red',
label: 'Unpaid',
value: stripeStatus.Unpaid,
value: 'UNPAID',
position: 7,
},
{
color: 'gray',
label: 'Paused',
value: stripeStatus.Paused,
value: 'PAUSED',
position: 8,
},
],
@@ -167,7 +136,11 @@ const getStripeCustomerData = async (
try {
const response = await axios(options);
return response.status === 200
? (response.data as stripeCustomer)
? ({
name: response.data.name,
businessName: response.data.business_name,
email: response.data.email,
} as stripeCustomer)
: ({} as stripeCustomer);
} catch (error) {
if (axios.isAxiosError(error)) {
@@ -176,10 +149,9 @@ const getStripeCustomerData = async (
}
};
const checkIfCompanyExistsInTwenty = async (name: string | undefined) => {
if (!name) {
return {};
}
const checkIfCompanyExistsInTwenty = async (
name: string | undefined,
): Promise<string | undefined> => {
const options = {
method: 'GET',
headers: {
@@ -189,9 +161,10 @@ const checkIfCompanyExistsInTwenty = async (name: string | undefined) => {
};
try {
const response = await axios(options);
return response.status === 200 && response.data.data.companies.length > 0
? response.data.data.companies[0]
: {};
return response.status === 200 &&
response.data.data.companies[0].id !== undefined
? (response.data.data.companies[0].id as string)
: '';
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
@@ -200,8 +173,8 @@ const checkIfCompanyExistsInTwenty = async (name: string | undefined) => {
};
const updateTwentyCompany = async (
companyId: string,
seats: number,
companyId: string | undefined,
seats: number | null,
subStatus: stripeStatus,
): Promise<boolean | undefined> => {
const options = {
@@ -226,11 +199,11 @@ const updateTwentyCompany = async (
}
};
const createTwentyCustomer = async (
const createTwentyCompany = async (
customerName: string | undefined,
seats: number,
seats: number | null,
subStatus: string,
) => {
): Promise<string | undefined> => {
const options = {
method: 'POST',
headers: {
@@ -246,7 +219,7 @@ const createTwentyCustomer = async (
};
try {
const response = await axios(options);
return response.status === 201 ? response.data : {};
return response.status === 201 ? (response.data.data.id as string) : '';
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
@@ -254,85 +227,258 @@ const createTwentyCustomer = async (
}
};
export const main = async (params: {
properties: unknown;
}): Promise<object | undefined> => {
const checkIfStripePersonExistsInTwenty = async (email: string | null) => {
const options = {
method: 'GET',
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
'Content-Type': 'application/json',
},
url: `${TWENTY_API_URL}/people?filter=emails.primaryEmail%5Beq%5D%3A%22${email}%22`, // mail is unique by default so there can be only 1 person with given mail
};
try {
const response = await axios.request(options);
return response.status === 200 &&
response.data.data.people[0].id !== undefined
? (response.data.data.people[0].id as string)
: '';
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
}
};
const addTwentyPerson = async (
firstName: string,
lastName: string,
email: string,
companyId: string,
seats: number,
subStatus: stripeStatus,
): Promise<boolean | undefined> => {
const options = {
method: 'POST',
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
'Content-Type': 'application/json',
},
url: `${TWENTY_API_URL}/people`,
data: {
firstName: firstName,
lastName: lastName,
emails: { primaryEmail: email },
companyId: companyId,
seats: seats,
subStatus: subStatus,
},
};
try {
const response = await axios.request(options);
return response.status === 201;
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
}
};
const updateTwentyPerson = async (
id: string,
seats: number,
subStatus: stripeStatus,
): Promise<boolean | undefined> => {
const options = {
method: 'PATCH',
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
'Content-Type': 'application/json',
},
url: `${TWENTY_API_URL}/people/${id}`,
data: {
seats: seats,
subStatus: subStatus,
},
};
try {
const response = await axios.request(options);
return response.status === 200;
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
}
};
export const main = async (
params: Record<string, any>,
): Promise<object | undefined> => {
if (TWENTY_API_KEY === '' || STRIPE_API_KEY === '') {
console.warn('Missing variables');
return {};
throw new Error('Missing variables');
}
try {
// TODO: add validation of signature key from Stripe
const { properties } = params;
const stripe = properties as stripeResponse;
const allowed_types = [
// TODO: add validation of signature key from Stripe (not possible at the moment as headers aren't accessible in serverless functions)
const stripe = params as stripeEvent;
const allowed_types: string[] = [
'customer.subscription.created',
'customer.subscription.updated',
];
if (!allowed_types.includes(stripe.type)) {
console.error('Wrong webhook');
throw new Error('Wrong type of webhook');
}
const stripeCustomer: stripeCustomer | undefined =
await getStripeCustomerData(stripe.data.object.customer);
if (
stripeCustomer?.businessName === undefined ||
stripeCustomer?.businessName === ''
) {
console.warn('Set customer business name in Stripe');
return {};
}
const companyObject = await getCompaniesObject();
const companyObject = await getTwentyObjectData('company');
if (
companyObject?.fields.find((field) => field.name === 'seats') ===
undefined
) {
const t: boolean | undefined = companyObject?.id
const seatsFieldCreated: boolean | undefined = companyObject?.id
? await createFields(companyObject?.id, 'seats')
: false;
if (t === false) {
console.error('Seats field creation failed');
return {};
if (!seatsFieldCreated) {
throw new Error('Seats field creation in Company object failed');
} else {
console.info('Seats field creation in Company object succeeded');
}
}
if (
companyObject?.fields.find((field) => field.name === 'subStatus') ===
undefined
) {
const t: boolean | undefined = companyObject?.id
const subStatusFieldCreated: boolean | undefined = companyObject?.id
? await createFields(companyObject?.id, 'subStatus')
: false;
if (t === false) {
console.error('Sub status field creation failed');
return {};
if (!subStatusFieldCreated) {
throw new Error('Sub status field creation in Company object failed');
} else {
console.info('Sub status field creation in Company object succeeded');
}
}
const stripeCustomer = await getStripeCustomerData(stripe.customer);
if (stripeCustomer?.businessName) {
console.warn('Set customer business name in Stripe');
return {};
const personObject = await getTwentyObjectData('person');
if (
personObject?.fields.find((field) => field.name === 'seats') === undefined
) {
const seatsFieldCreated: boolean | undefined = personObject?.id
? await createFields(personObject?.id, 'seats')
: false;
if (!seatsFieldCreated) {
throw new Error('Seats field creation in People object failed');
} else {
console.info('Seats field creation in People object succeeded');
}
}
const twentyCustomer = await checkIfCompanyExistsInTwenty(
stripeCustomer?.businessName,
);
if (Object.keys(twentyCustomer).length === 0) {
const a = await createTwentyCustomer(
stripeCustomer?.businessName,
stripe.items.data[0].quantity,
stripe.status.toUpperCase(),
);
if (Object.keys(a).length === 0) {
console.error('Creation of Stripe customer in Twenty failed');
return {};
if (
personObject?.fields.find((field) => field.name === 'subStatus') ===
undefined
) {
const subStatusFieldCreated: boolean | undefined = personObject?.id
? await createFields(personObject?.id, 'subStatus')
: false;
if (!subStatusFieldCreated) {
throw new Error('Sub status field creation in People object failed');
} else {
console.info('Sub status field creation in People object succeeded');
}
}
const twentyCompanyId: string | undefined =
await checkIfCompanyExistsInTwenty(stripeCustomer?.businessName);
const seats: number =
stripe.data.object.quantity ??
stripe.data.object.items.data.reduce(
(acc, item) => acc + item.quantity,
0,
); // we don't know if subscription has only 1 item (product) or more
let updatedTwentyCompanyId: string | undefined;
if (twentyCompanyId === '') {
const twentyCompanyCreated: string | undefined =
await createTwentyCompany(
stripeCustomer?.businessName,
seats,
stripe.data.object.status.toUpperCase(),
);
if (twentyCompanyCreated === '') {
throw new Error('Creation of Stripe customer in Twenty failed');
} else {
console.log('Creation of Stripe customer in Twenty succeeded');
updatedTwentyCompanyId = twentyCompanyCreated;
}
} else {
const a = await updateTwentyCompany(
twentyCustomer.id,
stripe.items.data[0].quantity,
stripe.status.toUpperCase() as stripeStatus,
);
if (!a) {
console.error('Update of Stripe customer in Twenty failed');
return {};
const twentyCompanyUpdated: boolean | undefined =
await updateTwentyCompany(
twentyCompanyId,
seats,
stripe.data.object.status.toUpperCase() as stripeStatus,
);
if (!twentyCompanyUpdated) {
throw new Error('Update of Stripe customer in Twenty failed');
} else {
console.log('Update of Stripe customer in Twenty succeeded');
updatedTwentyCompanyId = twentyCompanyId;
}
}
if (updatedTwentyCompanyId === undefined || updatedTwentyCompanyId === '') {
throw new Error('TwentyCompanyId not found');
} else {
const stripeCustomerInTwenty: string | undefined =
await checkIfStripePersonExistsInTwenty(stripeCustomer.email);
if (stripeCustomerInTwenty === '') {
if (!stripeCustomer.name) {
throw new Error('Missing Stripe customer first or last name');
}
if (!stripeCustomer.email) {
throw new Error('Missing Stripe customer email');
}
const firstName: string = stripeCustomer.name?.split(' ')[0];
const lastName: string = stripeCustomer.name?.split(' ')[1];
const addedStripePersonToTwenty: boolean | undefined =
await addTwentyPerson(
firstName,
lastName,
stripeCustomer.email,
updatedTwentyCompanyId,
seats,
stripe.data.object.status.toUpperCase() as stripeStatus,
);
if (!addedStripePersonToTwenty) {
throw new Error('Adding Stripe person to Twenty failed');
} else {
console.log('Stripe person was added to Twenty');
}
} else if (stripeCustomerInTwenty !== undefined) {
const updatedStripePersonInTwenty: boolean | undefined =
await updateTwentyPerson(
stripeCustomerInTwenty,
seats,
stripe.data.object.status.toUpperCase() as stripeStatus,
);
if (!updatedStripePersonInTwenty) {
throw new Error('Update of Stripe person in Twenty failed');
} else {
console.log('Update of Stripe person in Twenty succeeded');
}
} else {
throw new Error('Twenty not found');
}
}
return {};
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error.message);
return {};
}
console.error(error);
return {};
@@ -346,7 +492,7 @@ export const config: ServerlessFunctionConfig = {
{
universalIdentifier: '55f58e19-d832-43c4-9f8b-3f29fc05c162',
type: 'route',
path: '/stripe',
path: '/webhook/stripe',
httpMethod: 'POST',
isAuthRequired: false,
},
@@ -0,0 +1,37 @@
export type stripeStatus = 'INCOMPLETE' | 'INCOMPLETE_EXPIRED' | 'TRIALING' | 'ACTIVE' | 'PAST_DUE' | 'CANCELED' | 'UNPAID' | 'PAUSED';
type stripeItem = {
quantity: number;
};
type stripeItemsData = {
data: stripeItem[];
};
type stripeEventObject = {
customer: string;
items: stripeItemsData;
status: stripeStatus;
quantity: number | null;
};
type stripeEventData = {
object: stripeEventObject;
};
export type stripeEvent = {
data: stripeEventData;
type: string;
};
export type stripeCustomer = {
businessName?: string;
name: string | null;
email: string | null;
};
export type twentyObject = {
id: string;
nameSingular: string;
fields: Record<string, any>[];
};
@@ -5,6 +5,15 @@ __metadata:
version: 8
cacheKey: 10c0
"@types/node@npm:^24.7.2":
version: 24.10.0
resolution: "@types/node@npm:24.10.0"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/f82ed7194e16f5590ef7afdc20c6d09068c76d50278b485ede8f0c5749683536e3064ffa8def8db76915196afb3724b854aa5723c64d6571b890b14492943b46
languageName: node
linkType: hard
"async-function@npm:^1.0.0":
version: 1.0.0
resolution: "async-function@npm:1.0.0"
@@ -26,14 +35,14 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.13.1":
version: 1.13.1
resolution: "axios@npm:1.13.1"
"axios@npm:^1.13.2":
version: 1.13.2
resolution: "axios@npm:1.13.2"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.4"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/de9c3c6de43d3ee1146d3afe78645f19450cac6a5d7235bef8b8e8eeb705c2e47e2d231dea99cecaec4dae1897c521118ca9413b9d474063c719c4d94c5b9adc
checksum: 10c0/e8a42e37e5568ae9c7a28c348db0e8cf3e43d06fcbef73f0048669edfe4f71219664da7b6cc991b0c0f01c28a48f037c515263cb79be1f1ae8ff034cd813867b
languageName: node
linkType: hard
@@ -243,6 +252,22 @@ __metadata:
version: 0.0.0-use.local
resolution: "stripe-synchronizer@workspace:."
dependencies:
axios: "npm:^1.13.1"
"@types/node": "npm:^24.7.2"
axios: "npm:^1.13.2"
twenty-sdk: "npm:^0.0.4"
languageName: unknown
linkType: soft
"twenty-sdk@npm:^0.0.4":
version: 0.0.4
resolution: "twenty-sdk@npm:0.0.4"
checksum: 10c0/550f1d85bf0701396c9dd2d4c6bc55ba1b067fce13636f8540eec60ab6a4257c6d7cd86cb3f62e0974bf99467bc31270d92b17b9681a1b7a6281b7ef97224080
languageName: node
linkType: hard
"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
languageName: node
linkType: hard
@@ -10,7 +10,7 @@
"packageManager": "yarn@4.9.2",
"dependencies": {
"axios": "^1.12.2",
"twenty-sdk": "^0.0.3"
"twenty-sdk": "^0.0.4"
},
"devDependencies": {
"@types/node": "^24.7.2"
+5 -5
View File
@@ -224,7 +224,7 @@ __metadata:
dependencies:
"@types/node": "npm:^24.7.2"
axios: "npm:^1.12.2"
twenty-sdk: "npm:0.0.3-alpha"
twenty-sdk: "npm:^0.0.4"
languageName: unknown
linkType: soft
@@ -258,10 +258,10 @@ __metadata:
languageName: node
linkType: hard
"twenty-sdk@npm:0.0.3-alpha":
version: 0.0.3-alpha
resolution: "twenty-sdk@npm:0.0.3-alpha"
checksum: 10c0/e8028f47767e3fa6318100f26542e0477b68e36d363df5f3a6c20d8442951fecba60089d07e97224b0b3533a729bbd3526cca08728079577cfa204dbffaecb03
"twenty-sdk@npm:^0.0.4":
version: 0.0.4
resolution: "twenty-sdk@npm:0.0.4"
checksum: 10c0/550f1d85bf0701396c9dd2d4c6bc55ba1b067fce13636f8540eec60ab6a4257c6d7cd86cb3f62e0974bf99467bc31270d92b17b9681a1b7a6281b7ef97224080
languageName: node
linkType: hard