Skip to content

Commit

Permalink
Merge pull request #16 from vivid-planet/add-brevo-campaigns-api-to-m…
Browse files Browse the repository at this point in the history
…ailings

Add brevo campaigns api to email campaign
  • Loading branch information
RainbowBunchie authored Jan 24, 2024
2 parents f165fc3 + f409ab9 commit 40843b1
Show file tree
Hide file tree
Showing 20 changed files with 577 additions and 53 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ The following variables must be set manually
- `BREVO_SENDER_NAME`
- `BREVO_SENDER_EMAIL`
- `ECG_RTR_LIST_API_KEY`
- `CAMPAIGNS_FRONTEND_URL`
- `CAMPAIGNS_FRONTEND_BASIC_AUTH_USERNAME`
- `CAMPAIGNS_FRONTEND_BASIC_AUTH_PASSWORD`

### Start development processes

Expand Down
87 changes: 67 additions & 20 deletions demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ type DamFileLicense {
enum LicenseType {
ROYALTY_FREE
RIGHTS_MANAGED
SUBSCRIPTION
MICRO
}

"""
Expand Down Expand Up @@ -120,6 +118,46 @@ type FilenameResponse {
isOccupied: Boolean!
}

type BrevoApiCampaignStatistics {
"""Number of unique clicks for the campaign"""
uniqueClicks: Int!

"""Number of total clicks for the campaign"""
clickers: Int!

"""Number of complaints (Spam reports) for the campaign"""
complaints: Int!

"""Number of delivered emails for the campaign"""
delivered: Int!

"""Number of sent emails for the campaign"""
sent: Int!

"""Number of softbounce for the campaign"""
softBounces: Int!

"""Number of hardbounces for the campaign"""
hardBounces: Int!

"""Number of unique openings for the campaign"""
uniqueViews: Int!

"""Number of unique openings for the campaign"""
trackableViews: Int!

"""
Rate of recipients without any privacy protection option enabled in their email client, applied to all delivered emails
"""
estimatedViews: Int!

"""Number of unsubscription for the campaign"""
unsubscriptions: Int!

"""Number of openings for the campaign"""
viewed: Int!
}

type Product {
id: ID!
creatorId: String!
Expand Down Expand Up @@ -266,6 +304,8 @@ type DamFile {
license: DamFileLicense
createdAt: DateTime!
updatedAt: DateTime!
importSourceId: String
importSourceType: String
fileUrl: String!
duplicates: [DamFile!]!
damPath: String!
Expand Down Expand Up @@ -358,24 +398,6 @@ type PaginatedBrevoContacts {
totalCount: Int!
}

type TargetGroup implements DocumentInterface {
id: ID!
updatedAt: DateTime!
createdAt: DateTime!
title: String!
isMainList: Boolean!
brevoId: Int!
totalSubscribers: Int!
totalContactsBlocked: Int!
scope: EmailCampaignContentScope!
filters: BrevoContactFilterAttributes
}

type PaginatedTargetGroups {
nodes: [TargetGroup!]!
totalCount: Int!
}

type EmailCampaign implements DocumentInterface {
id: ID!
updatedAt: DateTime!
Expand Down Expand Up @@ -403,6 +425,24 @@ type PaginatedEmailCampaigns {
totalCount: Int!
}

type TargetGroup implements DocumentInterface {
id: ID!
updatedAt: DateTime!
createdAt: DateTime!
title: String!
isMainList: Boolean!
brevoId: Int!
totalSubscribers: Int!
totalContactsBlocked: Int!
scope: EmailCampaignContentScope!
filters: BrevoContactFilterAttributes
}

type PaginatedTargetGroups {
nodes: [TargetGroup!]!
totalCount: Int!
}

input PageTreeNodeScopeInput {
domain: String!
language: String!
Expand Down Expand Up @@ -458,6 +498,7 @@ type Query {
brevoContacts(targetGroupId: ID, email: String, scope: EmailCampaignContentScopeInput!, offset: Int! = 0, limit: Int! = 25): PaginatedBrevoContacts!
emailCampaign(id: ID!): EmailCampaign!
emailCampaigns(scope: EmailCampaignContentScopeInput!, search: String, filter: EmailCampaignFilter, sort: [EmailCampaignSort!], offset: Int! = 0, limit: Int! = 25): PaginatedEmailCampaigns!
emailCampaignStatistics(id: ID!): BrevoApiCampaignStatistics
targetGroup(id: ID!): TargetGroup!
targetGroups(scope: EmailCampaignContentScopeInput!, search: String, filter: TargetGroupFilter, sort: [TargetGroupSort!], offset: Int! = 0, limit: Int! = 25): PaginatedTargetGroups!
}
Expand Down Expand Up @@ -667,6 +708,8 @@ type Mutation {
createEmailCampaign(scope: EmailCampaignContentScopeInput!, input: EmailCampaignInput!): EmailCampaign!
updateEmailCampaign(id: ID!, input: EmailCampaignInput!, lastUpdatedAt: DateTime): EmailCampaign!
deleteEmailCampaign(id: ID!): Boolean!
sendEmailCampaignNow(id: ID!): Boolean!
sendEmailCampaignToTestEmails(id: ID!, data: SendTestEmailCampaignArgs!): Boolean!
createTargetGroup(scope: EmailCampaignContentScopeInput!, input: TargetGroupInput!): TargetGroup!
updateTargetGroup(id: ID!, input: TargetGroupInput!, lastUpdatedAt: DateTime): TargetGroup!
deleteTargetGroup(id: ID!): Boolean!
Expand Down Expand Up @@ -810,6 +853,10 @@ input EmailCampaignInput {
"""EmailCampaignContent root block input"""
scalar EmailCampaignContentBlockInput @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf")

input SendTestEmailCampaignArgs {
emails: [String!]!
}

input TargetGroupInput {
title: String!
filters: BrevoContactFilterAttributesInput
Expand Down
13 changes: 11 additions & 2 deletions demo/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,17 @@ export class AppModule {
ecgRtrList: {
apiKey: config.ecgRtrList.apiKey,
},
EmailCampaignContentBlock,
Scope: EmailCampaignContentScope,
emailCampaigns: {
EmailCampaignContentBlock,
Scope: EmailCampaignContentScope,
frontend: {
url: config.campaignsFrontend.url,
basicAuth: {
username: config.campaignsFrontend.basicAuth.username,
password: config.campaignsFrontend.basicAuth.password,
},
},
},
}),
],
};
Expand Down
7 changes: 7 additions & 0 deletions demo/api/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ export function createConfig(processEnv: NodeJS.ProcessEnv) {
email: envVars.BREVO_SENDER_EMAIL,
},
},
campaignsFrontend: {
url: envVars.CAMPAIGNS_FRONTEND_URL,
basicAuth: {
username: envVars.CAMPAIGNS_FRONTEND_BASIC_AUTH_USERNAME,
password: envVars.CAMPAIGNS_FRONTEND_BASIC_AUTH_PASSWORD,
},
},
ecgRtrList: {
apiKey: envVars.ECG_RTR_LIST_API_KEY,
},
Expand Down
9 changes: 9 additions & 0 deletions demo/api/src/config/environment-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,13 @@ export class EnvironmentVariables {

@IsString()
ECG_RTR_LIST_API_KEY: string;

@IsString()
CAMPAIGNS_FRONTEND_URL: string;

@IsString()
CAMPAIGNS_FRONTEND_BASIC_AUTH_USERNAME: string;

@IsString()
CAMPAIGNS_FRONTEND_BASIC_AUTH_PASSWORD: string;
}
5 changes: 4 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@nestjs/axios": "^1.0.0",
"@sendinblue/client": "^3.3.1",
"node-fetch": "^2.6.1"
},
"devDependencies": {
Expand All @@ -40,11 +42,11 @@
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/graphql": "^10.0.0",
"@sendinblue/client": "^3.3.1",
"@types/jest": "^29.5.0",
"@types/node-fetch": "^2.5.12",
"@types/rimraf": "^3.0.0",
"@types/uuid": "^8.3.0",
"axios": "^0.21.0",
"class-transformer": "^0.5.0",
"class-validator": "^0.13.1",
"eslint": "^8.0.0",
Expand Down Expand Up @@ -72,6 +74,7 @@
"@mikro-orm/nestjs": "^5.0.0",
"@mikro-orm/postgresql": "^5.0.4",
"@nestjs/common": "^9.0.0",
"axios": "^0.21.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.0.0"
}
Expand Down
118 changes: 118 additions & 0 deletions packages/api/src/brevo-api/brevo-api-campaigns.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Inject, Injectable } from "@nestjs/common";
import * as SibApiV3Sdk from "@sendinblue/client";

import { BrevoModuleConfig } from "../config/brevo-module.config";
import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants";
import { EmailCampaignInterface } from "../email-campaign/entities/email-campaign-entity.factory";
import { SendingState } from "../email-campaign/sending-state.enum";
import { BrevoApiCampaign } from "./dto/brevo-api-campaign";
import { BrevoApiCampaignStatistics } from "./dto/brevo-api-campaign-statistics";

@Injectable()
export class BrevoApiCampaignsService {
private readonly campaignsApi: SibApiV3Sdk.EmailCampaignsApi;

constructor(@Inject(BREVO_MODULE_CONFIG) private readonly config: BrevoModuleConfig) {
this.campaignsApi = new SibApiV3Sdk.EmailCampaignsApi();
this.campaignsApi.setApiKey(SibApiV3Sdk.EmailCampaignsApiApiKeys.apiKey, config.brevo.apiKey);
}

public getSendingInformationFromBrevoCampaign(campaign: BrevoApiCampaign): SendingState {
if (campaign.status === "sent") {
return SendingState.SENT;
} else if (campaign.status === "queued" || campaign.status === "in_process") {
return SendingState.SCHEDULED;
}

return SendingState.DRAFT;
}

public async createBrevoCampaign(campaign: EmailCampaignInterface, htmlContent: string, scheduledAt?: Date): Promise<number> {
const emailCampaign = {
name: campaign.title,
subject: campaign.subject,
sender: { name: this.config.brevo.sender.name, email: this.config.brevo.sender.email },
// TODO: add correct list after contact list/target groups are implemented
recipients: { listIds: [2] },
htmlContent,
scheduledAt: scheduledAt?.toISOString(),
};

const data = await this.campaignsApi.createEmailCampaign(emailCampaign);
return data.body.id;
}

public async updateBrevoCampaign(id: number, campaign: EmailCampaignInterface, htmlContent: string, scheduledAt?: Date): Promise<boolean> {
const emailCampaign = {
name: campaign.title,
subject: campaign.subject,
sender: { name: this.config.brevo.sender.name, email: this.config.brevo.sender.email },
// TODO: add correct list after contact list/target groups are implemented
recipients: { listIds: [2] },
htmlContent,
scheduledAt: scheduledAt?.toISOString(),
};

const result = await this.campaignsApi.updateEmailCampaign(id, emailCampaign);
return result.response.statusCode === 204;
}

public async sendBrevoCampaign(id: number): Promise<boolean> {
const result = await this.campaignsApi.sendEmailCampaignNow(id);
return result.response.statusCode === 204;
}

public async updateBrevoCampaignStatus(id: number, updated_status: SibApiV3Sdk.UpdateCampaignStatus.StatusEnum): Promise<boolean> {
const status = new SibApiV3Sdk.UpdateCampaignStatus();
status.status = updated_status;
const result = await this.campaignsApi.updateCampaignStatus(id, status);
return result.response.statusCode === 204;
}

public async sendTestEmail(id: number, emails: string[]): Promise<boolean> {
const result = await this.campaignsApi.sendTestEmail(id, { emailTo: emails });
return result.response.statusCode === 204;
}

public async loadBrevoCampaignsByIds(ids: number[]): Promise<BrevoApiCampaign[]> {
const campaigns = [];
for await (const campaign of await this.getCampaignsResponse(ids)) {
campaigns.push(campaign);
}

return campaigns;
}

public async loadBrevoCampaignById(id: number): Promise<BrevoApiCampaign> {
const response = await this.campaignsApi.getEmailCampaign(id);

// wrong type in brevo library -> needs to be cast to unknown first
return response.body as unknown as BrevoApiCampaign;
}

public async loadBrevoCampaignStatisticsById(id: number): Promise<BrevoApiCampaignStatistics> {
const campaign = await this.campaignsApi.getEmailCampaign(id);

return campaign.body.statistics.campaignStats[0];
}

private async *getCampaignsResponse(
ids: number[],
status?: "suspended" | "archive" | "sent" | "queued" | "draft" | "inProcess",
): AsyncGenerator<BrevoApiCampaign, void, undefined> {
let offset = 0;
const limit = 100;

while (true) {
const campaignsResponse = await this.campaignsApi.getEmailCampaigns(undefined, status, undefined, undefined, undefined, limit, offset);
const campaignArray = (campaignsResponse.body.campaigns ?? []).filter((item) => ids.includes(item.id));

if (campaignArray.length === 0) {
break;
}
yield* campaignArray;

offset += limit;
}
}
}
5 changes: 3 additions & 2 deletions packages/api/src/brevo-api/brevo-api.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Module } from "@nestjs/common";

import { ConfigModule } from "../config/config.module";
import { BrevoApiCampaignsService } from "./brevo-api-campaigns.service";
import { BrevoApiContactsService } from "./brevo-api-contact.service";

@Module({
imports: [ConfigModule],
providers: [BrevoApiContactsService],
exports: [BrevoApiContactsService],
providers: [BrevoApiContactsService, BrevoApiCampaignsService],
exports: [BrevoApiContactsService, BrevoApiCampaignsService],
})
export class BrevoApiModule {}
Loading

0 comments on commit 40843b1

Please sign in to comment.