Skip to content

Commit

Permalink
Merge pull request #21 from vivid-planet/add-target-group-to-email-ca…
Browse files Browse the repository at this point in the history
…mpaign

COM-268: add target group to email campaign
  • Loading branch information
RainbowBunchie authored Jan 25, 2024
2 parents beaec7d + 7cc2c25 commit 58d334a
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 44 deletions.
28 changes: 15 additions & 13 deletions demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,19 @@ type PaginatedDamFolders {
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 BrevoContact {
id: Int!
createdAt: String!
Expand All @@ -407,6 +420,7 @@ type EmailCampaign implements DocumentInterface {
brevoId: Int
sendingState: SendingState!
scheduledAt: DateTime
targetGroup: TargetGroup
content: EmailCampaignContentBlockData!
scope: EmailCampaignContentScope!
}
Expand All @@ -425,19 +439,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!
Expand Down Expand Up @@ -847,6 +848,7 @@ input EmailCampaignInput {
title: String!
subject: String!
scheduledAt: DateTime
targetGroup: ID = null
content: EmailCampaignContentBlockInput!
}

Expand Down
13 changes: 13 additions & 0 deletions demo/api/src/db/migrations/Migration20240123145606.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Migration } from "@mikro-orm/migrations";

export class Migration20240123145606 extends Migration {
// TODO: move to package
async up(): Promise<void> {
this.addSql('alter table "EmailCampaign" add column "targetGroup" uuid null');
this.addSql('alter table "EmailCampaign" drop column "contactList";');

this.addSql(
'alter table "EmailCampaign" add constraint "EmailCampaign_targetGroup_foreign" foreign key ("targetGroup") references "TargetGroup" ("id") on update cascade on delete set null;',
);
}
}
10 changes: 6 additions & 4 deletions packages/api/src/brevo-api/brevo-api-campaigns.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ export class BrevoApiCampaignsService {
}

public async createBrevoCampaign(campaign: EmailCampaignInterface, htmlContent: string, scheduledAt?: Date): Promise<number> {
const targetGroup = await campaign.targetGroup?.load();

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] },
recipients: { listIds: targetGroup ? [targetGroup?.brevoId] : [] },
htmlContent,
scheduledAt: scheduledAt?.toISOString(),
};
Expand All @@ -43,12 +44,13 @@ export class BrevoApiCampaignsService {
}

public async updateBrevoCampaign(id: number, campaign: EmailCampaignInterface, htmlContent: string, scheduledAt?: Date): Promise<boolean> {
const targetGroup = await campaign.targetGroup?.load();

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] },
recipients: { listIds: targetGroup ? [targetGroup?.brevoId] : [] },
htmlContent,
scheduledAt: scheduledAt?.toISOString(),
};
Expand Down
18 changes: 16 additions & 2 deletions packages/api/src/brevo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import { BrevoContactModule } from "./brevo-contact/brevo-contact.module";
import { BrevoModuleConfig } from "./config/brevo-module.config";
import { ConfigModule } from "./config/config.module";
import { EmailCampaignModule } from "./email-campaign/email-campaign.module";
import { TargetGroupEntityFactory } from "./target-group/entity/target-group-entity.factory";
import { TargetGroupModule } from "./target-group/target-group.module";

@Global()
@Module({})
export class BrevoModule {
static register(config: BrevoModuleConfig): DynamicModule {
const TargetGroup = TargetGroupEntityFactory.create({
Scope: config.emailCampaigns.Scope,
BrevoFilterAttributes: config.brevo.BrevoContactFilterAttributes,
});

return {
module: BrevoModule,
imports: [
Expand All @@ -20,8 +26,16 @@ export class BrevoModule {
BrevoFilterAttributes: config.brevo.BrevoContactFilterAttributes,
Scope: config.emailCampaigns.Scope,
}),
EmailCampaignModule.register(config),
TargetGroupModule.register({ Scope: config.emailCampaigns.Scope, BrevoFilterAttributes: config.brevo.BrevoContactFilterAttributes }),
EmailCampaignModule.register({
EmailCampaignContentBlock: config.emailCampaigns.EmailCampaignContentBlock,
Scope: config.emailCampaigns.Scope,
TargetGroup,
}),
TargetGroupModule.register({
Scope: config.emailCampaigns.Scope,
BrevoFilterAttributes: config.brevo.BrevoContactFilterAttributes,
TargetGroup: TargetGroup,
}),
ConfigModule.forRoot(config),
],
exports: [TargetGroupModule],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Block, BlockInputInterface, isBlockInputInterface } from "@comet/blocks-api";
import { IsUndefinable, RootBlockInputScalar } from "@comet/cms-api";
import { Type } from "@nestjs/common";
import { Field, InputType } from "@nestjs/graphql";
import { Field, ID, InputType } from "@nestjs/graphql";
import { Transform } from "class-transformer";
import { IsDate, IsNotEmpty, IsString, MinDate, ValidateNested } from "class-validator";
import { IsDate, IsNotEmpty, IsString, IsUUID, MinDate, ValidateNested } from "class-validator";

export interface EmailCampaignInputInterface {
title: string;
subject: string;
scheduledAt?: Date;
content: BlockInputInterface;
targetGroup?: string;
}

export class EmailCampaignInputFactory {
Expand All @@ -32,6 +33,11 @@ export class EmailCampaignInputFactory {
@Field(() => Date, { nullable: true })
scheduledAt?: Date;

@IsUndefinable()
@Field(() => ID, { nullable: true })
@IsUUID()
targetGroup?: string;

@Field(() => RootBlockInputScalar(EmailCampaignContentBlock))
@Transform(({ value }) => (isBlockInputInterface(value) ? value : EmailCampaignContentBlock.blockInputFactory(value)), {
toClassOnly: true,
Expand Down
25 changes: 17 additions & 8 deletions packages/api/src/email-campaign/email-campaign.module.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { Block } from "@comet/blocks-api";
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { HttpModule } from "@nestjs/axios";
import { DynamicModule, Module } from "@nestjs/common";
import { DynamicModule, Module, Type } from "@nestjs/common";
import { TargetGroupInterface } from "src/target-group/entity/target-group-entity.factory";

import { BrevoApiModule } from "../brevo-api/brevo-api.module";
import { EcgRtrListService } from "../brevo-contact/ecg-rtr-list/ecg-rtr-list.service";
import { BrevoModuleConfig } from "../config/brevo-module.config";
import { ConfigModule } from "../config/config.module";
import { EmailCampaignScopeInterface } from "../types";
import { EmailCampaignInputFactory } from "./dto/email-campaign-input.factory";
import { createEmailCampaignsResolver } from "./email-campaign.resolver";
import { EmailCampaignsService } from "./email-campaigns.service";
import { EmailCampaignEntityFactory } from "./entities/email-campaign-entity.factory";

interface EmailCampaignModuleConfig {
Scope: Type<EmailCampaignScopeInterface>;
EmailCampaignContentBlock: Block;
TargetGroup: Type<TargetGroupInterface>;
}

@Module({})
export class EmailCampaignModule {
static register(config: BrevoModuleConfig): DynamicModule {
static register({ Scope, EmailCampaignContentBlock, TargetGroup }: EmailCampaignModuleConfig): DynamicModule {
const EmailCampaign = EmailCampaignEntityFactory.create({
Scope: config.emailCampaigns.Scope,
EmailCampaignContentBlock: config.emailCampaigns.EmailCampaignContentBlock,
Scope,
EmailCampaignContentBlock,
TargetGroup,
});
const EmailCampaignInput = EmailCampaignInputFactory.create({ EmailCampaignContentBlock: config.emailCampaigns.EmailCampaignContentBlock });
const EmailCampaignsResolver = createEmailCampaignsResolver({ EmailCampaign, EmailCampaignInput, Scope: config.emailCampaigns.Scope });
const EmailCampaignInput = EmailCampaignInputFactory.create({ EmailCampaignContentBlock });
const EmailCampaignsResolver = createEmailCampaignsResolver({ EmailCampaign, EmailCampaignInput, Scope, TargetGroup });

return {
module: EmailCampaignModule,
Expand All @@ -29,7 +38,7 @@ export class EmailCampaignModule {
HttpModule.register({
timeout: 5000,
}),
MikroOrmModule.forFeature([EmailCampaign]),
MikroOrmModule.forFeature([EmailCampaign, TargetGroup]),
],
providers: [EmailCampaignsResolver, EmailCampaignsService, EcgRtrListService],
};
Expand Down
33 changes: 28 additions & 5 deletions packages/api/src/email-campaign/email-campaign.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PaginatedResponseFactory, SubjectEntity, validateNotModified } from "@comet/cms-api";
import { EntityManager, EntityRepository, FindOptions, wrap } from "@mikro-orm/core";
import { extractGraphqlFields, PaginatedResponseFactory, SubjectEntity, validateNotModified } from "@comet/cms-api";
import { EntityManager, EntityRepository, FindOptions, Reference, wrap } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { Type } from "@nestjs/common";
import { Args, ArgsType, ID, Mutation, ObjectType, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql";
import { Args, ArgsType, ID, Info, Mutation, ObjectType, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql";
import { GraphQLResolveInfo } from "graphql";
import { TargetGroupInterface } from "src/target-group/entity/target-group-entity.factory";

import { BrevoApiCampaignsService } from "../brevo-api/brevo-api-campaigns.service";
import { BrevoApiCampaignStatistics } from "../brevo-api/dto/brevo-api-campaign-statistics";
Expand All @@ -20,10 +22,12 @@ export function createEmailCampaignsResolver({
EmailCampaign,
EmailCampaignInput,
Scope,
TargetGroup,
}: {
EmailCampaign: Type<EmailCampaignInterface>;
EmailCampaignInput: Type<EmailCampaignInputInterface>;
Scope: Type<EmailCampaignScopeInterface>;
TargetGroup: Type<TargetGroupInterface>;
}): Type<unknown> {
@ObjectType()
class PaginatedEmailCampaigns extends PaginatedResponseFactory.create(EmailCampaign) {}
Expand All @@ -39,6 +43,7 @@ export function createEmailCampaignsResolver({
private readonly ecgRtrListService: EcgRtrListService,
private readonly entityManager: EntityManager,
@InjectRepository("EmailCampaign") private readonly repository: EntityRepository<EmailCampaignInterface>,
@InjectRepository("TargetGroup") private readonly targetGroupRepository: EntityRepository<TargetGroupInterface>,
) {}

@Query(() => EmailCampaign)
Expand All @@ -49,11 +54,21 @@ export function createEmailCampaignsResolver({
}

@Query(() => PaginatedEmailCampaigns)
async emailCampaigns(@Args() { search, filter, sort, offset, limit, scope }: EmailCampaignsArgs): Promise<PaginatedEmailCampaigns> {
async emailCampaigns(
@Args() { search, filter, sort, offset, limit, scope }: EmailCampaignsArgs,
@Info() info: GraphQLResolveInfo,
): Promise<PaginatedEmailCampaigns> {
const where = this.campaignsService.getFindCondition({ search, filter });
where.scope = scope;

const options: FindOptions<EmailCampaignInterface> = { offset, limit };
const fields = extractGraphqlFields(info, { root: "nodes" });
const populate: string[] = [];
if (fields.includes("targetGroup")) {
populate.push("targetGroup");
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: FindOptions<EmailCampaignInterface, any> = { offset, limit, populate };

if (sort) {
options.orderBy = sort.map((sortItem) => {
Expand All @@ -76,9 +91,12 @@ export function createEmailCampaignsResolver({
scope: typeof Scope,
@Args("input", { type: () => EmailCampaignInput }, new DynamicDtoValidationPipe(EmailCampaignInput)) input: EmailCampaignInputInterface,
): Promise<EmailCampaignInterface> {
const { targetGroup: targetGroupInput } = input;

const campaign = this.repository.create({
...input,
scope,
targetGroup: targetGroupInput ? Reference.create(await this.targetGroupRepository.findOneOrFail(targetGroupInput)) : undefined,
content: input.content.transformToBlockData(),
});

Expand Down Expand Up @@ -192,6 +210,11 @@ export function createEmailCampaignsResolver({

return SendingState.DRAFT;
}

@ResolveField(() => TargetGroup, { nullable: true })
async targetGroup(@Parent() emailCampaign: EmailCampaignInterface): Promise<TargetGroupInterface | undefined> {
return emailCampaign.targetGroup?.load();
}
}

return EmailCampaignsResolver;
Expand Down
9 changes: 3 additions & 6 deletions packages/api/src/email-campaign/email-campaigns.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,14 @@ export class EmailCampaignsService {
public async sendEmailCampaignNow(id: string): Promise<boolean> {
const campaign = await this.saveEmailCampaignInBrevo(id);

// TODO: add correct list of contact list / target groups after they are implemented
const contactList = {
brevoId: 2,
};
const targetGroup = await campaign.targetGroup?.load();

if (contactList?.brevoId) {
if (targetGroup?.brevoId) {
let currentOffset = 0;
let totalContacts = 0;
const limit = 50;
do {
const [contacts, total] = await this.brevoApiContactsService.findContactsByListId(contactList.brevoId, limit, currentOffset);
const [contacts, total] = await this.brevoApiContactsService.findContactsByListId(targetGroup.brevoId, limit, currentOffset);
const emails = contacts.map((contact) => contact.email);
const containedEmails = await this.ecgRtrListService.getContainedEcgRtrListEmails(emails);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Block, BlockDataInterface, RootBlock } from "@comet/blocks-api";
import { DocumentInterface, RootBlockDataScalar, RootBlockType } from "@comet/cms-api";
import { Embedded, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { Embedded, Entity, ManyToOne, OptionalProps, PrimaryKey, Property, Ref } from "@mikro-orm/core";
import { Type } from "@nestjs/common";
import { Field, ID, Int, ObjectType } from "@nestjs/graphql";
import { v4 } from "uuid";

import { TargetGroupInterface } from "../../target-group/entity/target-group-entity.factory";
import { EmailCampaignScopeInterface } from "../../types";
import { SendingState } from "../sending-state.enum";

Expand All @@ -20,15 +21,18 @@ export interface EmailCampaignInterface {
content: BlockDataInterface;
scope: EmailCampaignScopeInterface;
sendingState: SendingState;
targetGroup?: Ref<TargetGroupInterface>;
}

export class EmailCampaignEntityFactory {
static create({
EmailCampaignContentBlock,
Scope,
TargetGroup,
}: {
EmailCampaignContentBlock: Block;
Scope: EmailCampaignScopeInterface;
TargetGroup: Type<TargetGroupInterface>;
}): Type<EmailCampaignInterface> {
@Entity()
@ObjectType({
Expand Down Expand Up @@ -72,6 +76,10 @@ export class EmailCampaignEntityFactory {
@Field(() => Date, { nullable: true })
scheduledAt?: Date;

@ManyToOne(() => TargetGroup, { nullable: true, ref: true })
@Field(() => TargetGroup, { nullable: true })
targetGroup?: Ref<TargetGroupInterface> = undefined;

@RootBlock(EmailCampaignContentBlock)
@Property({ customType: new RootBlockType(EmailCampaignContentBlock) })
@Field(() => RootBlockDataScalar(EmailCampaignContentBlock))
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/target-group/target-group.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import { BrevoApiModule } from "../brevo-api/brevo-api.module";
import { ConfigModule } from "../config/config.module";
import { BrevoContactFilterAttributesInterface, EmailCampaignScopeInterface } from "../types";
import { TargetGroupInputFactory } from "./dto/target-group-input.factory";
import { TargetGroupEntityFactory } from "./entity/target-group-entity.factory";
import { TargetGroupInterface } from "./entity/target-group-entity.factory";
import { createTargetGroupsResolver } from "./target-group.resolver";
import { TargetGroupsService } from "./target-groups.service";

interface TargetGroupModuleConfig {
Scope: Type<EmailCampaignScopeInterface>;
BrevoFilterAttributes?: Type<BrevoContactFilterAttributesInterface>;
TargetGroup: Type<TargetGroupInterface>;
}

@Module({})
export class TargetGroupModule {
static register({ Scope, BrevoFilterAttributes }: TargetGroupModuleConfig): DynamicModule {
const TargetGroup = TargetGroupEntityFactory.create({ Scope, BrevoFilterAttributes });
static register({ Scope, BrevoFilterAttributes, TargetGroup }: TargetGroupModuleConfig): DynamicModule {
const TargetGroupInput = TargetGroupInputFactory.create({ BrevoFilterAttributes });
const TargetGroupResolver = createTargetGroupsResolver({ TargetGroup, TargetGroupInput, Scope });

Expand Down

0 comments on commit 58d334a

Please sign in to comment.