Skip to content

Commit

Permalink
Merge pull request #2112 from opengovsg/chinyao/feat/unsubscribe-from…
Browse files Browse the repository at this point in the history
…-phonebook

feat: allow auto unsubscribe to Phonebook list
  • Loading branch information
ganchinyao committed Jul 27, 2023
2 parents 675cbbd + e4a2b57 commit ceaecad
Show file tree
Hide file tree
Showing 28 changed files with 586 additions and 107 deletions.
2 changes: 1 addition & 1 deletion amplify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ frontend:
# IMPORTANT - Please verify your build output directory
baseDirectory: frontend/build
files:
- '**/*'
- '**/*'
31 changes: 30 additions & 1 deletion backend/src/core/middlewares/phonebook.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { loggerWithLabel } from '@core/logger'
import { Request, Response } from 'express'
import { NextFunction, Request, Response } from 'express'

import { PhonebookService } from '@core/services/phonebook.service'
import { ChannelType } from '@core/constants'
import { ApiValidationError } from '@core/errors/rest-api.errors'

const logger = loggerWithLabel(module)

Expand All @@ -29,6 +30,34 @@ const getListsByChannel = async (
}
}

const verifyListBelongsToUser =
(channel: ChannelType) =>
async (req: Request, _: Response, next: NextFunction) => {
const userId = req.session?.user?.id
const { list_id: listId } = req.body

try {
const lists = await PhonebookService.getPhonebookLists({
userId,
channel,
})

if (lists.some((list) => list.id === listId)) {
// listid belongs to the user. Ok to proceed
return next()
} else {
throw new Error('List does not belong to user')
}
} catch (err) {
logger.error({
action: 'verifyListBelongsToUser',
message: err,
})
throw new ApiValidationError('This listId does not belong to this user')
}
}

export const PhonebookMiddleware = {
getListsByChannel,
verifyListBelongsToUser,
}
1 change: 1 addition & 0 deletions backend/src/core/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './unsubscriber'
export * from './agency'
export * from './domain'
export * from './initialize-models'
export * from './managed-list-campaign'
3 changes: 3 additions & 0 deletions backend/src/core/models/initialize-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Credential,
Domain,
JobQueue,
ManagedListCampaign,
ProtectedMessage,
Statistic,
Unsubscriber,
Expand Down Expand Up @@ -90,12 +91,14 @@ export const initializeModels = (sequelize: Sequelize): void => {
GovsgTemplatesAccess,
CampaignGovsgTemplate,
]
const phonebookModels = [ManagedListCampaign]
sequelize.addModels([
...coreModels,
...emailModels,
...smsModels,
...telegramModels,
...govsgModels,
...phonebookModels,
])
}

Expand Down
33 changes: 33 additions & 0 deletions backend/src/core/models/managed-list-campaign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Table,
Column,
Model,
DataType,
BelongsTo,
ForeignKey,
} from 'sequelize-typescript'
import { Campaign } from '@core/models/campaign'

@Table({
tableName: 'managed_list_campaigns',
underscored: true,
timestamps: true,
})
export class ManagedListCampaign extends Model<ManagedListCampaign> {
@ForeignKey(() => Campaign)
@Column({
type: DataType.INTEGER,
allowNull: false,
primaryKey: true,
})
campaignId: number

@BelongsTo(() => Campaign)
campaign: Campaign

@Column({
type: DataType.INTEGER,
allowNull: false,
})
managedListId: number
}
35 changes: 34 additions & 1 deletion backend/src/core/services/phonebook.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { loggerWithLabel } from '@core/logger'
import { ChannelType } from '@core/constants'
import PhonebookClient from '@shared/clients/phonebook-client.class'
import config from '@core/config'
import { User } from '@core/models'
import { ManagedListCampaign, User } from '@core/models'

const logger = loggerWithLabel(module)

Expand Down Expand Up @@ -57,7 +57,40 @@ const getPhonebookListById = async ({
}
}

const setPhonebookListForCampaign = async ({
campaignId,
listId,
}: {
campaignId: number
listId: number
}) => {
return await ManagedListCampaign.upsert({
campaignId,
managedListId: listId,
} as ManagedListCampaign)
}

const deletePhonebookListForCampaign = async (campaignId: number) => {
return await ManagedListCampaign.destroy({
where: {
campaignId,
},
})
}

const getPhonebookListIdForCampaign = async (campaignId: number) => {
const managedListCampaign = await ManagedListCampaign.findOne({
where: {
campaignId,
},
})
return managedListCampaign?.managedListId
}

export const PhonebookService = {
getPhonebookLists,
getPhonebookListById,
setPhonebookListForCampaign,
deletePhonebookListForCampaign,
getPhonebookListIdForCampaign,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict'

module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('managed_list_campaigns', {
campaign_id: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
references: {
model: 'campaigns',
key: 'id',
},
onUpdate: 'CASCADE',
},
managed_list_id: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false,
},
created_at: {
type: Sequelize.DataTypes.DATE,
allowNull: false,
},
updated_at: {
type: Sequelize.DataTypes.DATE,
allowNull: false,
},
})
},

down: async (queryInterface, _) => {
await queryInterface.dropTable('managed_list_campaigns')
},
}
46 changes: 46 additions & 0 deletions backend/src/email/middlewares/email-template.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export interface EmailTemplateMiddleware {
deleteCsvErrorHandler: Handler
uploadProtectedCompleteHandler: Handler
selectPhonebookListHandler: Handler
setPhonebookListAssociationHandler: Handler
deletePhonebookListAssociationHandler: Handler
getPhonebookListIdForCampaignHandler: Handler
}

export const InitEmailTemplateMiddleware = (
Expand Down Expand Up @@ -225,6 +228,46 @@ export const InitEmailTemplateMiddleware = (
}
}

/**
* Associate a phonebook list to a campaign.
*/
const setPhonebookListAssociationHandler = async (
req: Request,
res: Response
): Promise<Response> => {
const { campaignId } = req.params
const { list_id: listId } = req.body
await PhonebookService.setPhonebookListForCampaign({
campaignId: +campaignId,
listId,
})
return res.sendStatus(204)
}

const deletePhonebookListAssociationHandler = async (
req: Request,
res: Response
): Promise<Response> => {
const { campaignId } = req.params
await PhonebookService.deletePhonebookListForCampaign(+campaignId)
return res.sendStatus(204)
}

const getPhonebookListIdForCampaignHandler = async (
req: Request,
res: Response
): Promise<Response> => {
const { campaignId } = req.params
const phonebookListId =
await PhonebookService.getPhonebookListIdForCampaign(+campaignId)
if (phonebookListId) {
return res.json({ list_id: phonebookListId })
}
return res.json({
message: 'No managed_list_id associated with this campaign',
})
}

/*
* Returns status of csv processing
*/
Expand Down Expand Up @@ -364,5 +407,8 @@ export const InitEmailTemplateMiddleware = (
deleteCsvErrorHandler,
uploadProtectedCompleteHandler,
selectPhonebookListHandler,
setPhonebookListAssociationHandler,
deletePhonebookListAssociationHandler,
getPhonebookListIdForCampaignHandler,
}
}
25 changes: 25 additions & 0 deletions backend/src/email/routes/email-campaign.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { celebrate, Joi, Segments } from 'celebrate'
import {
CampaignMiddleware,
JobMiddleware,
PhonebookMiddleware,
ProtectedMiddleware,
UploadMiddleware,
} from '@core/middlewares'
Expand All @@ -12,6 +13,7 @@ import {
EmailTemplateMiddleware,
} from '@email/middlewares'
import { fromAddressValidator } from '@core/utils/from-address'
import { ChannelType } from '@core/constants'

export const InitEmailCampaignRoute = (
emailTemplateMiddleware: EmailTemplateMiddleware,
Expand Down Expand Up @@ -89,6 +91,12 @@ export const InitEmailCampaignRoute = (
}),
}

const associatePhonebookListValidator = {
[Segments.BODY]: Joi.object({
list_id: Joi.number().required(),
}),
}

// Routes

// Check if campaign belongs to user for this router
Expand Down Expand Up @@ -191,5 +199,22 @@ export const InitEmailCampaignRoute = (
emailTemplateMiddleware.selectPhonebookListHandler
)

router.put(
'/phonebook-associations',
celebrate(associatePhonebookListValidator),
PhonebookMiddleware.verifyListBelongsToUser(ChannelType.Email),
emailTemplateMiddleware.setPhonebookListAssociationHandler
)

router.delete(
'/phonebook-associations',
emailTemplateMiddleware.deletePhonebookListAssociationHandler
)

router.get(
'/phonebook-listid',
emailTemplateMiddleware.getPhonebookListIdForCampaignHandler
)

return router
}
41 changes: 41 additions & 0 deletions backend/src/sms/middlewares/sms-template.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,51 @@ const deleteCsvErrorHandler = async (
return res.status(200).json({ id: campaignId })
}

const setPhonebookListAssociationHandler = async (
req: Request,
res: Response
): Promise<Response> => {
const { campaignId } = req.params
const { list_id: listId } = req.body
await PhonebookService.setPhonebookListForCampaign({
campaignId: +campaignId,
listId,
})
return res.sendStatus(204)
}

const deletePhonebookListAssociationHandler = async (
req: Request,
res: Response
): Promise<Response> => {
const { campaignId } = req.params
await PhonebookService.deletePhonebookListForCampaign(+campaignId)
return res.sendStatus(204)
}

const getPhonebookListIdForCampaignHandler = async (
req: Request,
res: Response
): Promise<Response> => {
const { campaignId } = req.params
const phonebookListId = await PhonebookService.getPhonebookListIdForCampaign(
+campaignId
)
if (phonebookListId) {
return res.json({ list_id: phonebookListId })
}
return res.json({
message: 'No managed_list_id associated with this campaign',
})
}

export const SmsTemplateMiddleware = {
storeTemplate,
uploadCompleteHandler,
pollCsvStatusHandler,
deleteCsvErrorHandler,
selectPhonebookListHandler,
setPhonebookListAssociationHandler,
deletePhonebookListAssociationHandler,
getPhonebookListIdForCampaignHandler,
}
Loading

0 comments on commit ceaecad

Please sign in to comment.