Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DEV-1980] Add user to active campaign #1220

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/great-rockets-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"active-campaign-client": patch
---

Create package and add function signatures
476 changes: 317 additions & 159 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/active-campaign-client/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"root": true,
"extends": [
"custom"
]
}
6 changes: 6 additions & 0 deletions packages/active-campaign-client/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-disable functional/immutable-data */
/* eslint-disable functional/no-expression-statements */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
25 changes: 25 additions & 0 deletions packages/active-campaign-client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "active-campaign-client",
"version": "0.1.0",
"description": "Implements ActiveCampaign API to add, update and delete Accounts and to add and update lists",
"scripts": {
"lint": "eslint src",
"test": "jest"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.12.0",
"eslint": "^8.40.0",
"eslint-config-custom": "*"
},
"dependencies": {
"@types/aws-lambda": "^8.10.145",
"@types/jest": "^29.5.14",
"aws-lambda": "^1.0.7",
"axios": "^1.7.7",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.6.3",
"dotenv": "16.4.5"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { handler } from '../../handlers/addContact';

describe('addContact handler', () => {
it('should create a contact successfully', async () => {
const event = {
headers: {
Authorization: 'test-token',
},
body: JSON.stringify({
username: `test@example${new Date().getTime()}e.com`,
firstName: 'John',
lastName: 'Doe',
company: 'Test Co',
role: 'Developer',
mailinglistAccepted: true,
}),
};

const response = await handler(event as any);
expect(response.statusCode).toBe(200);
});

it('should return 401 for missing authorization', async () => {
const event = {
headers: {},
body: JSON.stringify({}),
};

const response = await handler(event as any);
expect(response.statusCode).toBe(401);
});
});
31 changes: 31 additions & 0 deletions packages/active-campaign-client/src/activeCampaign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export type ACContactPayload = {
readonly contact: {
readonly email: string;
readonly firstName: string;
readonly lastName: string;
readonly phone?: string;
readonly fieldValues: readonly {
readonly field: string;
readonly value: string;
}[];
};
};

export type ACListPayload = {
readonly list: {
readonly name: string;
readonly stringid: string;
readonly sender_url: string;
readonly sender_reminder: string;
readonly subscription_notify?: string;
readonly unsubscription_notify?: string;
};
};

export type ACListStatusPayload = {
readonly contactList: {
readonly list: string;
readonly contact: string;
readonly status: string;
};
};
75 changes: 75 additions & 0 deletions packages/active-campaign-client/src/activeCampaignClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* eslint-disable functional/no-expression-statements */
/* eslint-disable functional/no-this-expressions */
/* eslint-disable functional/no-classes */
import axios from 'axios';
import {
ACContactPayload,
ACListPayload,
ACListStatusPayload,
} from './activeCampaign';
import * as dotenv from 'dotenv';

// eslint-disable-next-line functional/no-expression-statements
dotenv.config({ path: '.env' });

export class ActiveCampaignClient {
private readonly baseUrl: string;
private readonly apiKey: string;

constructor(baseUrl: string, apiKey: string) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
}

private getHeaders() {
return {
'Api-Token': this.apiKey,
'Content-Type': 'application/json',
};
}

async createContact(data: ACContactPayload) {
const response = await axios.post(`${this.baseUrl}/api/3/contacts`, data, {
headers: this.getHeaders(),
});
return response.data;
}

async updateContact(contactId: string, data: ACContactPayload) {
const response = await axios.put(
`${this.baseUrl}/api/3/contacts/${contactId}`,
data,
{ headers: this.getHeaders() }
);
return response.data;
}

async deleteContact(contactId: string) {
const response = await axios.delete(
`${this.baseUrl}/api/3/contacts/${contactId}`,
{ headers: this.getHeaders() }
);
return response.data;
}

async createList(data: ACListPayload) {
const response = await axios.post(`${this.baseUrl}/api/3/lists`, data, {
headers: this.getHeaders(),
});
return response.data;
}

async updateListStatus(data: ACListStatusPayload) {
const response = await axios.post(
`${this.baseUrl}/api/3/contactLists`,
data,
{ headers: this.getHeaders() }
);
return response.data;
}
}

export const acClient = new ActiveCampaignClient(
process.env.AC_BASE_URL!,
process.env.AC_API_KEY!
);
7 changes: 7 additions & 0 deletions packages/active-campaign-client/src/cognito.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CognitoIdentityServiceProvider } from 'aws-sdk';

export async function validateCognitoToken(token: string): Promise<boolean> {
// TODO: Implement actual token validation when AWS credentials are available
// For local testing, return true if token exists
return !!token;
}
57 changes: 57 additions & 0 deletions packages/active-campaign-client/src/handlers/addContact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable functional/no-try-statements */
/* eslint-disable functional/no-expression-statements */
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { acClient } from '../activeCampaignClient';
import { validateCognitoToken } from '../cognito';
import { SignUpUserData } from 'nextjs-website/src/lib/types/sign-up';
import { ACContactPayload } from '../activeCampaign';

export async function handler(
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
try {
// Validate authorization
const token = event.headers.Authorization;
if (!token || !(await validateCognitoToken(token))) {
return {
statusCode: 401,
body: JSON.stringify({ message: 'Unauthorized' }),
};
}

// Parse request body
const userData: SignUpUserData = JSON.parse(event.body || '{}');

// Transform to AC payload
const acPayload: ACContactPayload = {
contact: {
email: userData.username,
firstName: userData.firstName,
lastName: userData.lastName,
fieldValues: [
{
field: 'company',
value: userData.company,
},
{
field: 'role',
value: userData.role,
},
],
},
};

const response = await acClient.createContact(acPayload);

return {
statusCode: 200,
body: JSON.stringify(response),
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ message: 'Internal server error' }),
};
}
}
29 changes: 29 additions & 0 deletions packages/active-campaign-client/src/manageContacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//check https://developers.activecampaign.com/reference/contact for informations on the contact api for Active Campaign

//Signup user data is used as a reference for now, if needed, update it with a type we can easily retrieve
import { SignUpUserData } from 'nextjs-website/src/lib/types/sign-up';

//https://developers.activecampaign.com/reference/create-a-new-contact
export type AddContact = (contactInfo: SignUpUserData) => unknown; //torna la risposta

//the return value could be the contact id
//https://developers.activecampaign.com/reference/list-all-contacts
//export type FetchContact = (searchQuery: string) => number;

//the contactId is the id used on Active Campaign, which must be retrieved
//https://developers.activecampaign.com/reference/delete-contact
export type DeleteContact = (contactId: string) => unknown;

//https://developers.activecampaign.com/reference/update-a-contact-new
export type UpdateContact = (
contactId: string,
contactInfo: SignUpUserData
) => unknown;

//https://developers.activecampaign.com/reference/update-list-status-for-contact
//status can have 2 values: "1" -> subscribe, "2" -> unsubscribe
export type UpdateListStatus = (
listId: string,
contactId: string,
status: string
) => unknown;
6 changes: 6 additions & 0 deletions packages/active-campaign-client/src/manageLists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Webinar } from 'nextjs-website/src/lib/types/webinar';

//https://developers.activecampaign.com/reference/create-new-list
//listInfo should include all the needed info for the api
//Important: the api returns the id of the list on Active Campaign
export type CreateList = (listInfo: Webinar) => unknown; //torna la risposta
8 changes: 8 additions & 0 deletions packages/active-campaign-client/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}
Loading