Skip to content

Commit

Permalink
feat: implement gift-exchange emails
Browse files Browse the repository at this point in the history
Merge PR #16
bjardon authored Nov 28, 2024
2 parents 8d02092 + ca026e3 commit 6a1ece1
Showing 12 changed files with 522 additions and 121 deletions.
499 changes: 378 additions & 121 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@getbrevo/brevo": "^2.2.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import {
import { AuthModule } from '@app/auth';
import { UsersModule } from '@app/users';
import { GiftExchangesModule } from '@app/gift-exchanges';
import { EmailsModule } from '@app/emails';

@Module({
imports: [
@@ -36,6 +37,7 @@ import { GiftExchangesModule } from '@app/gift-exchanges';
}),
AuthModule,
UsersModule,
EmailsModule,
GiftExchangesModule,
],
controllers: [],
11 changes: 11 additions & 0 deletions src/app/emails/emails.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Global, Module } from '@nestjs/common';
import { EmailsService } from './services';

@Global()
@Module({
imports: [],
providers: [EmailsService],
controllers: [],
exports: [EmailsService],
})
export class EmailsModule {}
1 change: 1 addition & 0 deletions src/app/emails/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './emails.module';
32 changes: 32 additions & 0 deletions src/app/emails/services/emails.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
TransactionalEmailsApi,
TransactionalEmailsApiApiKeys,
} from '@getbrevo/brevo';
import { EnvironmentConfig } from '@config/environment';

@Injectable()
export class EmailsService {
private client: TransactionalEmailsApi;

constructor(private readonly config: ConfigService<EnvironmentConfig>) {
this.client = new TransactionalEmailsApi();
this.client.setApiKey(
TransactionalEmailsApiApiKeys.apiKey,
this.config.get('brevo.apiKey', { infer: true }),
);
}

async sendEmail(email: {
subject?: string;
htmlContent?: string;
sender?: { name: string; email: string };
to: { email: string; name: string }[];
replyTo?: { email: string; name: string };
params?: object;
templateId?: number;
}) {
return this.client.sendTransacEmail({ ...email });
}
}
1 change: 1 addition & 0 deletions src/app/emails/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './emails.service';
4 changes: 4 additions & 0 deletions src/app/gift-exchanges/constants/email-templates.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const EMAIL_TEMPLATES = {
WELCOME_EMAIL: 1,
DRAW_EMAIL: 3,
};
1 change: 1 addition & 0 deletions src/app/gift-exchanges/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './collections.constant';
export * from './email-templates.constant';
85 changes: 85 additions & 0 deletions src/app/gift-exchanges/controllers/gift-exchanges.controller.ts
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import {
import { AuthGuard } from '@app/auth/guards';
import { User } from '@app/users/decorators';
import { UserDocument } from '@app/users/schemas';
import { EmailsService } from '@app/emails/services';
import {
CreateGiftExchangeDto,
GiftExchangeEntityDto,
@@ -30,13 +31,15 @@ import {
GiftExchangesService,
} from '@app/gift-exchanges/services';
import { first, isEmpty, shuffle } from 'lodash';
import { EMAIL_TEMPLATES } from '../constants';

@ApiTags('GiftExchanges')
@Controller('gift-exchanges')
export class GiftExchangesController {
constructor(
private readonly giftExchanges: GiftExchangesService,
private readonly participants: ParticipantsService,
private readonly emails: EmailsService,
) {}

@Get()
@@ -109,6 +112,43 @@ export class GiftExchangesController {
return exchange;
}

@Post(':exchangeId/resend-welcome')
@UseGuards(AuthGuard)
@ApiOperation({
summary: 'Resend welcome email',
description:
'Resends the welcome email to all participants in the gift exchange',
})
@ApiBearerAuth()
async resendWelcomeEmailById(
@User() user: UserDocument,
@Param('exchangeId') exchangeId: string,
) {
const exchange = await this.giftExchanges.findById(exchangeId);

if (!user._id.equals(exchange._organizer))
throw new UnauthorizedException('giftexchanges.organizer.match');

const participants = await this.participants.find({
_exchange: exchange._id,
});

const to = participants.map(({ user }) => ({
email: user.email,
name: user.name,
}));
const params = {
giftExchange: exchange.toObject({ virtuals: true }),
organizer: user.toObject({ virtuals: true }),
};

await this.emails.sendEmail({
templateId: EMAIL_TEMPLATES.WELCOME_EMAIL,
to,
params,
});
}

@Patch(':exchangeId/draw-names')
@UseGuards(AuthGuard)
@ApiOperation({
@@ -157,6 +197,51 @@ export class GiftExchangesController {
});
}

@Post(':exchangeId/resend-draw')
@UseGuards(AuthGuard)
@ApiOperation({
summary: 'Resend draw email',
description:
'Resends the draw result email to all participants in the gift exchange',
})
@ApiBearerAuth()
async resendDrawById(
@User() user: UserDocument,
@Param('exchangeId') exchangeId: string,
) {
const exchange = await this.giftExchanges.findById(exchangeId);

if (!user._id.equals(exchange._organizer))
throw new UnauthorizedException('giftexchanges.organizer.match');

if (!exchange.drawnOn)
throw new ConflictException('giftexchanges.draw.required');

const participants = await this.participants.find({
_exchange: exchange._id,
acknowledgedOn: { $exists: true, $ne: null },
});

await Promise.all(
participants.map(async (participant) => {
const { user, giftee } = participant;

const to = [{ email: user.email, name: user.name }];
const params = {
giftExchange: exchange.toObject({ virtuals: true }),
organizer: user.toObject({ virtuals: true }),
giftee: giftee.toObject({ virtuals: true }),
};

await this.emails.sendEmail({
templateId: EMAIL_TEMPLATES.DRAW_EMAIL,
to,
params,
});
}),
);
}

@Delete(':exchangeId')
@UseGuards(AuthGuard)
@ApiOperation({
3 changes: 3 additions & 0 deletions src/config/environment/environment-config.factory.ts
Original file line number Diff line number Diff line change
@@ -13,5 +13,8 @@ export const ENVIRONMENT_CONFIGURATION_FACTORY = (): EnvironmentConfig => {
user: process.env.MONGO_USER ?? '',
dbName: process.env.MONGO_DB ?? '',
},
brevo: {
apiKey: process.env.BREVO_API_KEY ?? '',
},
};
};
3 changes: 3 additions & 0 deletions src/config/environment/environment-config.interface.ts
Original file line number Diff line number Diff line change
@@ -9,4 +9,7 @@ export interface EnvironmentConfig {
user: string;
dbName: string;
};
brevo: {
apiKey: string;
};
}

0 comments on commit 6a1ece1

Please sign in to comment.