Skip to content

Commit

Permalink
Merge pull request #17 from vivid-planet/add-brevo-api-for-target-groups
Browse files Browse the repository at this point in the history
add contact list brevo api functionality to target groups
  • Loading branch information
RainbowBunchie authored Jan 23, 2024
2 parents 55acbd7 + aef2ba9 commit 4007f91
Show file tree
Hide file tree
Showing 15 changed files with 410 additions and 69 deletions.
43 changes: 22 additions & 21 deletions demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ type DamFileLicense {
enum LicenseType {
ROYALTY_FREE
RIGHTS_MANAGED
SUBSCRIPTION
MICRO
}

"""
Expand Down Expand Up @@ -264,8 +266,6 @@ type DamFile {
license: DamFileLicense
createdAt: DateTime!
updatedAt: DateTime!
importSourceId: String
importSourceType: String
fileUrl: String!
duplicates: [DamFile!]!
damPath: String!
Expand Down Expand Up @@ -358,6 +358,24 @@ 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 @@ -385,24 +403,6 @@ 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 @@ -455,7 +455,7 @@ type Query {
products(offset: Int! = 0, limit: Int! = 25, search: String, filter: ProductFilter, sort: [ProductSort!]): PaginatedProducts!
mainMenu(scope: PageTreeNodeScopeInput!): [PageTreeNode!]!
brevoContact(id: Int!): BrevoContact!
brevoContacts(offset: Int! = 0, limit: Int! = 25, email: String): PaginatedBrevoContacts!
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!
targetGroup(id: ID!): TargetGroup!
Expand Down Expand Up @@ -796,6 +796,7 @@ enum SubscribeResponse {
input SubscribeInput {
email: String!
redirectionUrl: String!
scope: EmailCampaignContentScopeInput!
attributes: BrevoContactAttributesInput!
}

Expand Down
4 changes: 4 additions & 0 deletions demo/api/src/brevo-contact/dto/brevo-contact-attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export class BrevoContactAttributes {
@ObjectType()
@InputType("BrevoContactFilterAttributesInput")
export class BrevoContactFilterAttributes {
// index signature to match Array<any> | undefined in BrevoContactFilterAttributesInterface
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: Array<any> | undefined;

@Field(() => [BrevoContactSalutation], { nullable: true })
@IsEnum(BrevoContactSalutation, { each: true })
@Enum({ items: () => BrevoContactSalutation, array: true })
Expand Down
59 changes: 57 additions & 2 deletions packages/api/src/brevo-api/brevo-api-contact.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Inject, Injectable } from "@nestjs/common";
import * as SibApiV3Sdk from "@sendinblue/client";
import { BrevoContactInterface } from "src/brevo-contact/dto/brevo-contact.factory";
import { BrevoContactAttributesInterface } from "src/types";

import { BrevoContactInterface } from "../brevo-contact/dto/brevo-contact.factory";
import { BrevoContactUpdateInput } from "../brevo-contact/dto/brevo-contact.input";
import { BrevoModuleConfig } from "../config/brevo-module.config";
import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants";
import { TargetGroupInputInterface } from "../target-group/dto/target-group-input.factory";
import { isErrorFromBrevo } from "./brevo-api.utils";
import { BrevoApiContactList } from "./dto/brevo-api-contact-list";

export interface CreateDoubleOptInContactData {
email: string;
Expand All @@ -23,7 +25,7 @@ export class BrevoApiContactsService {
this.contactsApi.setApiKey(SibApiV3Sdk.ContactsApiApiKeys.apiKey, config.brevo.apiKey);
}

public async createDoubleOptInContact(
public async createDoubleOptInBrevoContact(
{ email, redirectionUrl, attributes }: CreateDoubleOptInContactData,
brevoIds: number[],
templateId: number,
Expand Down Expand Up @@ -92,4 +94,57 @@ export class BrevoApiContactsService {

await this.contactsApi.updateBatchContacts({ contacts: blacklistedContacts });
}

public async createBrevoContactList(input: TargetGroupInputInterface): Promise<number | undefined> {
const contactList = {
name: input.title,
folderId: 1, // folderId is required, folder #1 is created by default
};

const data = await this.contactsApi.createList(contactList);
return data.body.id;
}

public async updateBrevoContactList(id: number, input: TargetGroupInputInterface): Promise<boolean> {
const data = await this.contactsApi.updateList(id, { name: input.title });
return data.response.statusCode === 204;
}

public async deleteBrevoContactList(id: number): Promise<boolean> {
const data = await this.contactsApi.deleteList(id);
return data.response.statusCode === 204;
}

public async findBrevoContactListById(id: number): Promise<BrevoApiContactList> {
const data = await this.contactsApi.getList(id);
return data.body;
}

public async findBrevoContactListsByIds(ids: number[]): Promise<BrevoApiContactList[]> {
const lists: BrevoApiContactList[] = [];
for await (const list of await this.getBrevoContactListResponses()) {
if (ids.includes(list.id)) {
lists.push(list);
}
}

return lists;
}

async *getBrevoContactListResponses(): AsyncGenerator<BrevoApiContactList, void, undefined> {
const limit = 50;
let offset = 0;

while (true) {
const listsResponse = await this.contactsApi.getLists(limit, offset);
const lists = listsResponse.body.lists ?? [];

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

offset += limit;
}
}
}
28 changes: 28 additions & 0 deletions packages/api/src/brevo-api/dto/brevo-api-contact-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Field, ID, Int, ObjectType } from "@nestjs/graphql";

@ObjectType()
export class BrevoApiContactList {
@Field(() => ID)
id: number;

@Field(() => String)
createdAt: string;

@Field(() => String)
name: string;

@Field(() => Int)
totalBlacklisted: number;

@Field(() => Int)
totalSubscribers: number;

@Field(() => Int)
uniqueSubscribers: number;

@Field(() => Int)
folderId: number;

@Field(() => Boolean, { nullable: true })
dynamicList?: boolean;
}
13 changes: 8 additions & 5 deletions packages/api/src/brevo-contact/brevo-contact.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { DynamicModule, Module, Type } from "@nestjs/common";

import { BrevoApiModule } from "../brevo-api/brevo-api.module";
import { ConfigModule } from "../config/config.module";
import { BrevoContactAttributesInterface } from "../types";
import { TargetGroupModule } from "../target-group/target-group.module";
import { BrevoContactAttributesInterface, BrevoContactFilterAttributesInterface, EmailCampaignScopeInterface } from "../types";
import { createBrevoContactResolver } from "./brevo-contact.resolver";
import { BrevoContactsService } from "./brevo-contacts.service";
import { BrevoContactFactory } from "./dto/brevo-contact.factory";
Expand All @@ -12,18 +13,20 @@ import { IsValidRedirectURLConstraint } from "./validator/redirect-url.validator

interface BrevoContactModuleConfig {
BrevoContactAttributes?: Type<BrevoContactAttributesInterface>;
Scope: Type<EmailCampaignScopeInterface>;
BrevoFilterAttributes?: Type<BrevoContactFilterAttributesInterface>;
}

@Module({})
export class BrevoContactModule {
static register({ BrevoContactAttributes }: BrevoContactModuleConfig): DynamicModule {
static register({ BrevoContactAttributes, Scope, BrevoFilterAttributes }: BrevoContactModuleConfig): DynamicModule {
const BrevoContact = BrevoContactFactory.create({ BrevoContactAttributes });
const BrevoContactSubscribeInput = SubscribeInputFactory.create({ BrevoContactAttributes });
const BrevoContactResolver = createBrevoContactResolver({ BrevoContact, BrevoContactSubscribeInput });
const BrevoContactSubscribeInput = SubscribeInputFactory.create({ BrevoContactAttributes, Scope });
const BrevoContactResolver = createBrevoContactResolver({ BrevoContact, BrevoContactSubscribeInput, Scope });

return {
module: BrevoContactModule,
imports: [BrevoApiModule, ConfigModule],
imports: [BrevoApiModule, ConfigModule, TargetGroupModule.register({ Scope, BrevoFilterAttributes })],
providers: [BrevoContactsService, BrevoContactResolver, EcgRtrListService, IsValidRedirectURLConstraint],
};
}
Expand Down
41 changes: 34 additions & 7 deletions packages/api/src/brevo-contact/brevo-contact.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
import { PaginatedResponseFactory } from "@comet/cms-api";
import { FilterQuery } from "@mikro-orm/core";
import { Inject, Type } from "@nestjs/common";
import { Args, Int, Mutation, ObjectType, Query, Resolver } from "@nestjs/graphql";
import { Args, ArgsType, Int, Mutation, ObjectType, Query, Resolver } from "@nestjs/graphql";

import { BrevoApiContactsService } from "../brevo-api/brevo-api-contact.service";
import { BrevoModuleConfig } from "../config/brevo-module.config";
import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants";
import { TargetGroupInterface } from "../target-group/entity/target-group-entity.factory";
import { TargetGroupsService } from "../target-group/target-groups.service";
import { EmailCampaignScopeInterface } from "../types";
import { BrevoContactsService } from "./brevo-contacts.service";
import { BrevoContactInterface } from "./dto/brevo-contact.factory";
import { BrevoContactUpdateInput } from "./dto/brevo-contact.input";
import { BrevoContactsArgs } from "./dto/brevo-contacts.args";
import { BrevoContactsArgsFactory } from "./dto/brevo-contacts.args";
import { SubscribeInputInterface } from "./dto/subscribe-input.factory";
import { SubscribeResponse } from "./dto/subscribe-response.enum";
import { EcgRtrListService } from "./ecg-rtr-list/ecg-rtr-list.service";

export function createBrevoContactResolver({
BrevoContact,
BrevoContactSubscribeInput,
Scope,
}: {
BrevoContact: Type<BrevoContactInterface>;
BrevoContactSubscribeInput: Type<SubscribeInputInterface>;
Scope: Type<EmailCampaignScopeInterface>;
}): Type<unknown> {
@ObjectType()
class PaginatedBrevoContacts extends PaginatedResponseFactory.create(BrevoContact) {}

@ArgsType()
class BrevoContactsArgs extends BrevoContactsArgsFactory.create({ Scope }) {}

@Resolver(() => BrevoContact)
class BrevoContactResolver {
constructor(
@Inject(BREVO_MODULE_CONFIG) private readonly config: BrevoModuleConfig,
private readonly brevoContactsApiService: BrevoApiContactsService,
private readonly brevoContactsService: BrevoContactsService,
private readonly ecgRtrListService: EcgRtrListService,
private readonly targetGroupService: TargetGroupsService,
) {}

@Query(() => BrevoContact)
Expand All @@ -38,10 +48,26 @@ export function createBrevoContactResolver({
}

@Query(() => PaginatedBrevoContacts)
async brevoContacts(@Args() { offset, limit, email }: BrevoContactsArgs): Promise<PaginatedBrevoContacts> {
// TODO: add correct lists when brevo contact list is implemented
// 2 is the id of the first list in brevo that is created by default
const contactListId = 2;
async brevoContacts(@Args() { offset, limit, email, scope, targetGroupId }: BrevoContactsArgs): Promise<PaginatedBrevoContacts> {
const where: FilterQuery<TargetGroupInterface> = { scope, isMainList: true };

if (targetGroupId) {
where.id = targetGroupId;
where.isMainList = false;
}

let targetGroup = await this.targetGroupService.findOneTargetGroup(where);

if (!targetGroup) {
// filtering for a specific target group, but it does not exist
if (targetGroupId) {
return new PaginatedBrevoContacts([], 0, { offset, limit });
}

// when there is no main target group for the scope, create one
targetGroup = await this.targetGroupService.createIfNotExistMainTargetGroupForScope(scope);
}

if (email) {
const contact = await this.brevoContactsApiService.getContactInfoByEmail(email);
if (contact) {
Expand All @@ -50,7 +76,8 @@ export function createBrevoContactResolver({
return new PaginatedBrevoContacts([], 0, { offset, limit });
}

const [contacts, count] = await this.brevoContactsApiService.findContactsByListId(contactListId, limit, offset);
const [contacts, count] = await this.brevoContactsApiService.findContactsByListId(targetGroup?.brevoId, limit, offset);

return new PaginatedBrevoContacts(contacts, count, { offset, limit });
}

Expand Down
32 changes: 26 additions & 6 deletions packages/api/src/brevo-contact/brevo-contacts.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import { Injectable } from "@nestjs/common";

import { BrevoApiContactsService, CreateDoubleOptInContactData } from "../brevo-api/brevo-api-contact.service";
import { BrevoApiContactsService } from "../brevo-api/brevo-api-contact.service";
import { TargetGroupsService } from "../target-group/target-groups.service";
import { SubscribeInputInterface } from "./dto/subscribe-input.factory";
import { SubscribeResponse } from "./dto/subscribe-response.enum";

@Injectable()
export class BrevoContactsService {
constructor(private readonly brevoContactsApiService: BrevoApiContactsService) {}
constructor(private readonly brevoContactsApiService: BrevoApiContactsService, private readonly targetGroupService: TargetGroupsService) {}

public async createDoubleOptInContact(data: CreateDoubleOptInContactData, templateId: number): Promise<SubscribeResponse> {
// TODO: add correct lists when brevo contact list is implemented
const contactListId = 2;
public async createDoubleOptInContact(data: SubscribeInputInterface, templateId: number): Promise<SubscribeResponse> {
const mainTargetGroupForScope = await this.targetGroupService.createIfNotExistMainTargetGroupForScope(data.scope);

const created = await this.brevoContactsApiService.createDoubleOptInContact(data, [contactListId], templateId);
let offset = 0;
let totalCount = 0;
const targetGroupIds: number[] = [];
const limit = 50;

do {
const [targetGroups, totalContactLists] = await this.targetGroupService.findNonMainTargetGroups(data, offset, limit);
totalCount = totalContactLists;
offset += targetGroups.length;

for (const targetGroup of targetGroups) {
const contactIsInTargetGroup = this.targetGroupService.checkIfContactIsInTargetGroup(data, targetGroup.filters);

if (contactIsInTargetGroup) {
targetGroupIds.push(targetGroup.brevoId);
}
}
} while (offset < totalCount);

const created = await this.brevoContactsApiService.createDoubleOptInBrevoContact(data, [mainTargetGroupForScope.brevoId], templateId);
if (created) {
return SubscribeResponse.SUCCESSFUL;
}
Expand Down
Loading

0 comments on commit 4007f91

Please sign in to comment.