From c5c3a1d678b88e23281a81b621770f7d7c979582 Mon Sep 17 00:00:00 2001 From: Kishen Kumar Date: Thu, 23 Nov 2023 08:33:30 +0800 Subject: [PATCH] Refactor/remove phonebook (#2235) * refactor: remove phonebook * chore: add unsubscriber count back to FE --------- Co-authored-by: KishenKumarrrrr --- .secrets.baseline | 4 +- backend/src/core/config.ts | 18 --- backend/src/core/middlewares/index.ts | 1 - .../core/middlewares/phonebook.middleware.ts | 63 --------- backend/src/core/models/index.ts | 1 - backend/src/core/models/initialize-models.ts | 3 - .../src/core/models/managed-list-campaign.ts | 33 ----- backend/src/core/routes/index.ts | 7 - backend/src/core/routes/phonebook.routes.ts | 22 --- .../src/core/services/phonebook.service.ts | 96 ------------- .../middlewares/email-template.middleware.ts | 99 ------------- .../src/email/routes/email-campaign.routes.ts | 37 ----- .../middlewares/sms-template.middleware.ts | 91 ------------ backend/src/sms/routes/sms-campaign.routes.ts | 37 ----- frontend/src/classes/Phonebook.ts | 4 - frontend/src/classes/index.ts | 1 - .../export-recipients/ExportRecipients.tsx | 21 +-- .../progress-details/ProgressDetails.tsx | 62 ++++---- .../dashboard/create/email/EmailDetail.tsx | 16 +-- .../create/email/EmailRecipients.tsx | 98 +------------ .../dashboard/create/sms/SMSRecipients.tsx | 97 +------------ .../phonebook-list/PhonebookListSection.tsx | 61 -------- .../src/components/phonebook-list/index.ts | 1 - frontend/src/config.ts | 5 - frontend/src/services/phonebook.service.ts | 99 ------------- frontend/src/test-utils/api/index.ts | 33 ----- .../clients/phonebook-client.class/index.ts | 93 ------------ .../phonebook-client.class/interfaces.ts | 16 --- worker/src/core/config.ts | 55 -------- .../loaders/message-worker/email.class.ts | 16 --- .../core/loaders/message-worker/interface.ts | 6 - .../message-worker/util/contact-preference.ts | 133 ------------------ 32 files changed, 40 insertions(+), 1289 deletions(-) delete mode 100644 backend/src/core/middlewares/phonebook.middleware.ts delete mode 100644 backend/src/core/models/managed-list-campaign.ts delete mode 100644 backend/src/core/routes/phonebook.routes.ts delete mode 100644 backend/src/core/services/phonebook.service.ts delete mode 100644 frontend/src/classes/Phonebook.ts delete mode 100644 frontend/src/components/phonebook-list/PhonebookListSection.tsx delete mode 100644 frontend/src/components/phonebook-list/index.ts delete mode 100644 frontend/src/services/phonebook.service.ts delete mode 100644 shared/src/clients/phonebook-client.class/index.ts delete mode 100644 shared/src/clients/phonebook-client.class/interfaces.ts delete mode 100644 worker/src/core/loaders/message-worker/util/contact-preference.ts diff --git a/.secrets.baseline b/.secrets.baseline index ce4fc604c..80b284740 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -345,7 +345,7 @@ "filename": "frontend/src/test-utils/api/index.ts", "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", "is_verified": false, - "line_number": 46 + "line_number": 44 } ], "worker/.env-example": [ @@ -365,5 +365,5 @@ } ] }, - "generated_at": "2023-09-25T09:48:43Z" + "generated_at": "2023-11-16T03:18:03Z" } diff --git a/backend/src/core/config.ts b/backend/src/core/config.ts index a8ce42fc7..832d0b0eb 100644 --- a/backend/src/core/config.ts +++ b/backend/src/core/config.ts @@ -174,10 +174,6 @@ interface ConfigSchema { flamingo: { dbUri: string } - phonebook: { - endpointUrl: string - apiKey: string - } sgid: { clientId: string clientSecret: string @@ -811,20 +807,6 @@ const config: Config = convict({ env: 'FLAMINGO_DB_URI', }, }, - phonebook: { - endpointUrl: { - doc: 'Endpoint url of phonebook server', - default: 'http://localhost:8080', - env: 'PHONEBOOK_URL', - format: 'required-string', - }, - apiKey: { - doc: 'API key to make requests to Phonebook', - default: 'API_KEY', - env: 'PHONEBOOK_API_KEY', - format: 'required-string', - }, - }, sgid: { clientId: { doc: 'Client ID of application registered with sgID', diff --git a/backend/src/core/middlewares/index.ts b/backend/src/core/middlewares/index.ts index 5a5febfad..9595c7386 100644 --- a/backend/src/core/middlewares/index.ts +++ b/backend/src/core/middlewares/index.ts @@ -8,4 +8,3 @@ export * from './protected.middleware' export * from './unsubscriber.middleware' export * from './file-attachment.middleware' export * as ExperimentMiddleware from './experiment.middleware' -export * from './phonebook.middleware' diff --git a/backend/src/core/middlewares/phonebook.middleware.ts b/backend/src/core/middlewares/phonebook.middleware.ts deleted file mode 100644 index 5d1291980..000000000 --- a/backend/src/core/middlewares/phonebook.middleware.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { loggerWithLabel } from '@core/logger' -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) - -const getListsByChannel = async ( - req: Request, - res: Response -): Promise => { - logger.info({ - message: 'Get phonebook lists by channel', - action: 'getListsByChannel', - }) - const userId = req.session?.user?.id - const { channel } = req.params - - try { - const lists = await PhonebookService.getPhonebookLists({ - userId, - channel: channel as ChannelType, - }) - return res.status(200).json({ lists }) - } catch (e) { - // explicitly return 200 if something goes wrong with phonebook API and not an error - return res.status(200).json({ message: 'Could not retrieve lists.' }) - } -} - -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, -} diff --git a/backend/src/core/models/index.ts b/backend/src/core/models/index.ts index b2d1bd2f1..f05311331 100644 --- a/backend/src/core/models/index.ts +++ b/backend/src/core/models/index.ts @@ -13,4 +13,3 @@ export * from './unsubscriber' export * from './agency' export * from './domain' export * from './initialize-models' -export * from './managed-list-campaign' diff --git a/backend/src/core/models/initialize-models.ts b/backend/src/core/models/initialize-models.ts index 9bfbc3b82..575eb4c06 100644 --- a/backend/src/core/models/initialize-models.ts +++ b/backend/src/core/models/initialize-models.ts @@ -6,7 +6,6 @@ import { Credential, Domain, JobQueue, - ManagedListCampaign, ProtectedMessage, Statistic, Unsubscriber, @@ -95,14 +94,12 @@ export const initializeModels = (sequelize: Sequelize): void => { CampaignGovsgTemplate, GovsgVerification, ] - const phonebookModels = [ManagedListCampaign] sequelize.addModels([ ...coreModels, ...emailModels, ...smsModels, ...telegramModels, ...govsgModels, - ...phonebookModels, ]) } diff --git a/backend/src/core/models/managed-list-campaign.ts b/backend/src/core/models/managed-list-campaign.ts deleted file mode 100644 index a63597e32..000000000 --- a/backend/src/core/models/managed-list-campaign.ts +++ /dev/null @@ -1,33 +0,0 @@ -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 { - @ForeignKey(() => Campaign) - @Column({ - type: DataType.INTEGER, - allowNull: false, - primaryKey: true, - }) - campaignId: number - - @BelongsTo(() => Campaign) - campaign: Campaign - - @Column({ - type: DataType.INTEGER, - allowNull: false, - }) - managedListId: number -} diff --git a/backend/src/core/routes/index.ts b/backend/src/core/routes/index.ts index f0925291d..662078d0f 100644 --- a/backend/src/core/routes/index.ts +++ b/backend/src/core/routes/index.ts @@ -53,7 +53,6 @@ import { InitGovsgMessageTransactionalRoute, } from '@govsg/routes' import { InitGovsgTransactionalMiddleware } from '@govsg/middlewares/govsg-transactional.middleware' -import phonebookRoutes from '@core/routes/phonebook.routes' export const InitV1Route = (app: Application): Router => { const logger = loggerWithLabel(module) @@ -286,12 +285,6 @@ export const InitV1Route = (app: Application): Router => { router.use('/callback/govsg', govsgCallbackRoutes) - router.use( - '/phonebook', - authMiddleware.getAuthMiddleware([AuthType.Cookie]), - phonebookRoutes - ) - router.use( '/api-key', authMiddleware.getAuthMiddleware([AuthType.Cookie]), diff --git a/backend/src/core/routes/phonebook.routes.ts b/backend/src/core/routes/phonebook.routes.ts deleted file mode 100644 index a784934ed..000000000 --- a/backend/src/core/routes/phonebook.routes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Router } from 'express' -import { celebrate, Joi, Segments } from 'celebrate' -import { ChannelType } from '@core/constants' -import { PhonebookMiddleware } from '@core/middlewares/phonebook.middleware' - -const router = Router() - -const listByChannelValidator = { - [Segments.PARAMS]: Joi.object({ - channel: Joi.string() - .required() - .valid(...Object.values(ChannelType)), - }), -} - -router.get( - '/lists/:channel', - celebrate(listByChannelValidator), - PhonebookMiddleware.getListsByChannel -) - -export default router diff --git a/backend/src/core/services/phonebook.service.ts b/backend/src/core/services/phonebook.service.ts deleted file mode 100644 index 2e805ee65..000000000 --- a/backend/src/core/services/phonebook.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { loggerWithLabel } from '@core/logger' -import { ChannelType } from '@core/constants' -import PhonebookClient from '@shared/clients/phonebook-client.class' -import config from '@core/config' -import { ManagedListCampaign, User } from '@core/models' - -const logger = loggerWithLabel(module) - -const phonebookClient: PhonebookClient = new PhonebookClient( - config.get('phonebook.endpointUrl'), - config.get('phonebook.apiKey') -) - -const getPhonebookLists = async ({ - userId, - channel, -}: { - userId: number - channel: ChannelType -}): Promise<{ id: number; name: string }[]> => { - try { - const user = await User.findOne({ - where: { id: userId }, - attributes: ['email'], - }) - if (!user) { - logger.error('invalid user making request!') - return [] - } - const res = await phonebookClient.getManagedLists(user.email, channel) - - return res - } catch (err) { - logger.error({ - action: 'getPhonebookLists', - message: err, - }) - throw err - } -} - -const getPhonebookListById = async ({ - listId, - presignedUrl, -}: { - listId: number - presignedUrl: string -}): Promise<{ s3Key: string; etag: string; filename: string }> => { - try { - return await phonebookClient.getManagedListById(listId, presignedUrl) - } catch (err) { - logger.error({ - action: 'getPhonebookListById', - message: err, - }) - throw err - } -} - -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, -} diff --git a/backend/src/email/middlewares/email-template.middleware.ts b/backend/src/email/middlewares/email-template.middleware.ts index 34833fa13..a1918fd50 100644 --- a/backend/src/email/middlewares/email-template.middleware.ts +++ b/backend/src/email/middlewares/email-template.middleware.ts @@ -18,7 +18,6 @@ import { StoreTemplateOutput } from '@email/interfaces' import { loggerWithLabel } from '@core/logger' import { ThemeClient } from '@shared/theme' import { ApiInvalidTemplateError } from '@core/errors/rest-api.errors' -import { PhonebookService } from '@core/services/phonebook.service' export interface EmailTemplateMiddleware { storeTemplate: Handler @@ -26,10 +25,6 @@ export interface EmailTemplateMiddleware { pollCsvStatusHandler: Handler deleteCsvErrorHandler: Handler uploadProtectedCompleteHandler: Handler - selectPhonebookListHandler: Handler - setPhonebookListAssociationHandler: Handler - deletePhonebookListAssociationHandler: Handler - getPhonebookListIdForCampaignHandler: Handler } export const InitEmailTemplateMiddleware = ( @@ -178,96 +173,6 @@ export const InitEmailTemplateMiddleware = ( return next(err) } } - - const selectPhonebookListHandler = async ( - req: Request, - res: Response - ): Promise => { - try { - const { campaignId } = req.params - - const { list_id: listId } = req.body - - // check if template exists - const template = await EmailTemplateService.getFilledTemplate(+campaignId) - if (template === null) { - throw new Error( - 'Error: No message template found. Please create a message template before uploading a recipient file.' - ) - } - - const { s3Key, presignedUrl } = await UploadService.getPresignedUrl() - - const list = await PhonebookService.getPhonebookListById({ - listId, - presignedUrl, - }) - if (!list) throw new Error('Error: List not found') - - const { etag, filename } = list - - // Store temp filename - await UploadService.storeS3TempFilename(+campaignId, filename) - - // Enqueue upload job to be processed - await EmailTemplateService.enqueueUpload({ - campaignId: +campaignId, - template, - s3Key, - etag, - filename, - }) - - return res.status(202).json({ list_id: listId }) - } catch (e) { - // explicitly return a 500 to not block user flow but prompt them to upload an alternative csv - return res.status(500).json({ - message: - 'Error selecting phonebook list. Please try uploading the list directly.', - }) - } - } - - /** - * Associate a phonebook list to a campaign. - */ - const setPhonebookListAssociationHandler = async ( - req: Request, - res: Response - ): Promise => { - 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 => { - const { campaignId } = req.params - await PhonebookService.deletePhonebookListForCampaign(+campaignId) - return res.sendStatus(204) - } - - const getPhonebookListIdForCampaignHandler = async ( - req: Request, - res: Response - ): Promise => { - 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 */ @@ -406,9 +311,5 @@ export const InitEmailTemplateMiddleware = ( pollCsvStatusHandler, deleteCsvErrorHandler, uploadProtectedCompleteHandler, - selectPhonebookListHandler, - setPhonebookListAssociationHandler, - deletePhonebookListAssociationHandler, - getPhonebookListIdForCampaignHandler, } } diff --git a/backend/src/email/routes/email-campaign.routes.ts b/backend/src/email/routes/email-campaign.routes.ts index 13be4e6c2..f31fede6b 100644 --- a/backend/src/email/routes/email-campaign.routes.ts +++ b/backend/src/email/routes/email-campaign.routes.ts @@ -3,7 +3,6 @@ import { celebrate, Joi, Segments } from 'celebrate' import { CampaignMiddleware, JobMiddleware, - PhonebookMiddleware, ProtectedMiddleware, UploadMiddleware, } from '@core/middlewares' @@ -13,7 +12,6 @@ import { EmailTemplateMiddleware, } from '@email/middlewares' import { fromAddressValidator } from '@core/utils/from-address' -import { ChannelType } from '@core/constants' export const InitEmailCampaignRoute = ( emailTemplateMiddleware: EmailTemplateMiddleware, @@ -85,18 +83,6 @@ export const InitEmailCampaignRoute = ( }), } - const selectPhonebookListValidator = { - [Segments.BODY]: Joi.object({ - list_id: Joi.number().required(), - }), - } - - const associatePhonebookListValidator = { - [Segments.BODY]: Joi.object({ - list_id: Joi.number().required(), - }), - } - // Routes // Check if campaign belongs to user for this router @@ -193,28 +179,5 @@ export const InitEmailCampaignRoute = ( emailMiddleware.duplicateCampaign ) - router.post( - '/phonebook-list', - celebrate(selectPhonebookListValidator), - 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 } diff --git a/backend/src/sms/middlewares/sms-template.middleware.ts b/backend/src/sms/middlewares/sms-template.middleware.ts index f35bc2e2b..c45998a94 100644 --- a/backend/src/sms/middlewares/sms-template.middleware.ts +++ b/backend/src/sms/middlewares/sms-template.middleware.ts @@ -12,7 +12,6 @@ import { SmsService, SmsTemplateService } from '@sms/services' import { StoreTemplateOutput } from '@sms/interfaces' import { loggerWithLabel } from '@core/logger' import { ApiInvalidTemplateError } from '@core/errors/rest-api.errors' -import { PhonebookService } from '@core/services/phonebook.service' const logger = loggerWithLabel(module) /** @@ -147,54 +146,6 @@ const uploadCompleteHandler = async ( } } -const selectPhonebookListHandler = async ( - req: Request, - res: Response -): Promise => { - try { - const { campaignId } = req.params - - const { list_id: listId } = req.body - - // check if template exists - const template = await SmsTemplateService.getFilledTemplate(+campaignId) - if (template === null) { - throw new Error( - 'Error: No message template found. Please create a message template before uploading a recipient file.' - ) - } - const { s3Key, presignedUrl } = await UploadService.getPresignedUrl() - - const list = await PhonebookService.getPhonebookListById({ - listId, - presignedUrl, - }) - if (!list) throw new Error('Error: List not found') - - const { etag, filename } = list - - // Store temp filename - await UploadService.storeS3TempFilename(+campaignId, filename) - - // Enqueue upload job to be processed - await SmsTemplateService.enqueueUpload({ - campaignId: +campaignId, - template, - s3Key, - etag, - filename, - }) - - return res.status(202).json({ list_id: listId }) - } catch (e) { - // explicitly return a 500 to not block user flow but prompt them to upload an alternative csv - return res.status(500).json({ - message: - 'Error selecting phonebook list. Please try uploading the list directly.', - }) - } -} - /* * Returns status of csv processing */ @@ -242,51 +193,9 @@ const deleteCsvErrorHandler = async ( return res.status(200).json({ id: campaignId }) } -const setPhonebookListAssociationHandler = async ( - req: Request, - res: Response -): Promise => { - 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 => { - const { campaignId } = req.params - await PhonebookService.deletePhonebookListForCampaign(+campaignId) - return res.sendStatus(204) -} - -const getPhonebookListIdForCampaignHandler = async ( - req: Request, - res: Response -): Promise => { - 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, } diff --git a/backend/src/sms/routes/sms-campaign.routes.ts b/backend/src/sms/routes/sms-campaign.routes.ts index 3f173e8df..6fb81ef22 100644 --- a/backend/src/sms/routes/sms-campaign.routes.ts +++ b/backend/src/sms/routes/sms-campaign.routes.ts @@ -3,7 +3,6 @@ import { celebrate, Joi, Segments } from 'celebrate' import { CampaignMiddleware, JobMiddleware, - PhonebookMiddleware, SettingsMiddleware, UploadMiddleware, } from '@core/middlewares' @@ -12,7 +11,6 @@ import { SmsStatsMiddleware, SmsTemplateMiddleware, } from '@sms/middlewares' -import { ChannelType } from '@core/constants' export const InitSmsCampaignRoute = ( smsMiddleware: SmsMiddleware, @@ -77,18 +75,6 @@ export const InitSmsCampaignRoute = ( }), } - const selectPhonebookListValidator = { - [Segments.BODY]: Joi.object({ - list_id: Joi.number().required(), - }), - } - - const associatePhonebookListValidator = { - [Segments.BODY]: Joi.object({ - list_id: Joi.number().required(), - }), - } - // Routes // Check if campaign belongs to user for this router @@ -178,28 +164,5 @@ export const InitSmsCampaignRoute = ( smsMiddleware.duplicateCampaign ) - router.post( - '/phonebook-list', - celebrate(selectPhonebookListValidator), - SmsTemplateMiddleware.selectPhonebookListHandler - ) - - router.put( - '/phonebook-associations', - celebrate(associatePhonebookListValidator), - PhonebookMiddleware.verifyListBelongsToUser(ChannelType.SMS), - SmsTemplateMiddleware.setPhonebookListAssociationHandler - ) - - router.delete( - '/phonebook-associations', - SmsTemplateMiddleware.deletePhonebookListAssociationHandler - ) - - router.get( - '/phonebook-listid', - SmsTemplateMiddleware.getPhonebookListIdForCampaignHandler - ) - return router } diff --git a/frontend/src/classes/Phonebook.ts b/frontend/src/classes/Phonebook.ts deleted file mode 100644 index 1c01b0a60..000000000 --- a/frontend/src/classes/Phonebook.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface AgencyList { - id: number - name: string -} diff --git a/frontend/src/classes/index.ts b/frontend/src/classes/index.ts index 54b1c745a..114c1ac02 100644 --- a/frontend/src/classes/index.ts +++ b/frontend/src/classes/index.ts @@ -4,4 +4,3 @@ export * from './EmailCampaign' export * from './TelegramCampaign' export * from './List' export * from './GovsgCampaign' -export * from './Phonebook' diff --git a/frontend/src/components/common/export-recipients/ExportRecipients.tsx b/frontend/src/components/common/export-recipients/ExportRecipients.tsx index a8b31662f..622214278 100644 --- a/frontend/src/components/common/export-recipients/ExportRecipients.tsx +++ b/frontend/src/components/common/export-recipients/ExportRecipients.tsx @@ -13,7 +13,6 @@ import { Status } from 'classes/Campaign' import { ActionButton } from 'components/common' import { exportCampaignStats } from 'services/campaign.service' import { GA_USER_EVENTS, sendUserEvent } from 'services/ga.service' -import { isPhonebookAutoUnsubscribeEnabled } from 'services/phonebook.service' export enum CampaignExportStatus { Unavailable = 'Unavailable', @@ -33,7 +32,6 @@ const ExportRecipients = ({ statusUpdatedAt, iconPosition, isButton = false, - isUsingPhonebook = false, }: { campaignId: number campaignName: string @@ -43,7 +41,6 @@ const ExportRecipients = ({ statusUpdatedAt?: string iconPosition: 'left' | 'right' isButton?: boolean - isUsingPhonebook?: boolean }) => { const [exportStatus, setExportStatus] = useState( CampaignExportStatus.Unavailable @@ -97,17 +94,13 @@ const ExportRecipients = ({ if (list.length > 0) { let keys = Object.keys(list[0]).filter((k) => k !== 'unsubscriber') // this field is only used to detect whether the person has unsub-ed if (campaignType === ChannelType.Email) { - if (isUsingPhonebook && isPhonebookAutoUnsubscribeEnabled()) { - keys = ['recipient', 'status', 'errorCode', 'updatedAt'] - } else { - keys = [ - 'recipient', - 'status', - 'unsubscribeReason', - 'errorCode', - 'updatedAt', - ] - } + keys = [ + 'recipient', + 'status', + 'unsubscribeReason', + 'errorCode', + 'updatedAt', + ] } const headers = keys .map((key) => `"${key}"`) diff --git a/frontend/src/components/common/progress-details/ProgressDetails.tsx b/frontend/src/components/common/progress-details/ProgressDetails.tsx index 2c7f18854..ece95bcf7 100644 --- a/frontend/src/components/common/progress-details/ProgressDetails.tsx +++ b/frontend/src/components/common/progress-details/ProgressDetails.tsx @@ -17,18 +17,15 @@ import { } from 'components/common' import { LINKS } from 'config' import { CampaignContext } from 'contexts/campaign.context' -import { isPhonebookAutoUnsubscribeEnabled } from 'services/phonebook.service' const ProgressDetails = ({ stats, redacted, handleRetry, - isUsingPhonebook = false, }: { stats: CampaignStats redacted: boolean handleRetry: () => Promise - isUsingPhonebook?: boolean }) => { const { campaign } = useContext(CampaignContext) const { id, name, type, sentAt, numRecipients } = campaign @@ -149,7 +146,6 @@ const ProgressDetails = ({ status={status} statusUpdatedAt={statusUpdatedAt} isButton - isUsingPhonebook={isUsingPhonebook} /> ) : ( @@ -218,40 +214,34 @@ const ProgressDetails = ({ Recipient does not exist {invalid} - {type === ChannelType.Email && - !(isUsingPhonebook && isPhonebookAutoUnsubscribeEnabled()) && ( - - - - Unsubscribers - - Recipient indicated to unsubscribe - {unsubscribed} - - )} + {type === ChannelType.Email && ( + + + + Unsubscribers + + Recipient indicated to unsubscribe + {unsubscribed} + + )} - {type === ChannelType.Email && - !(isUsingPhonebook && isPhonebookAutoUnsubscribeEnabled()) && ( - - Remove unsubscribers from your recipient list, to - avoid campaigns being marked as spam and affecting the reputation of - your agency.{' '} - - Learn more - - - )} + {type === ChannelType.Email && ( + + Remove unsubscribers from your recipient list, to + avoid campaigns being marked as spam and affecting the reputation of + your agency.{' '} + + Learn more + + + )} {renderUpdateStats()} ) diff --git a/frontend/src/components/dashboard/create/email/EmailDetail.tsx b/frontend/src/components/dashboard/create/email/EmailDetail.tsx index ebd121c4c..d7107206d 100644 --- a/frontend/src/components/dashboard/create/email/EmailDetail.tsx +++ b/frontend/src/components/dashboard/create/email/EmailDetail.tsx @@ -1,5 +1,5 @@ // eslint-disable-next-line import/order -import { useContext, useEffect, useState } from 'react' +import { useContext } from 'react' import { EmailCampaign } from 'classes' @@ -16,27 +16,14 @@ import { CampaignContext } from 'contexts/campaign.context' import { retryCampaign } from 'services/campaign.service' import { GA_USER_EVENTS, sendUserEvent } from 'services/ga.service' -import { getPhonebookListIdForCampaign } from 'services/phonebook.service' const EmailDetail = () => { const { campaign, updateCampaign } = useContext(CampaignContext) const { id } = campaign const { stats, refreshCampaignStats } = usePollCampaignStats() - const [isUsingPhonebook, setIsUsingPhonebook] = useState(false) const emailCampaign = campaign as EmailCampaign - useEffect(() => { - const checkIfUsingPhonebook = async () => { - const phonebookListId = await getPhonebookListIdForCampaign(id) - if (phonebookListId) { - setIsUsingPhonebook(true) - } - } - - void checkIfUsingPhonebook() - }, [id]) - async function handleRetry() { try { sendUserEvent(GA_USER_EVENTS.RETRY_RESUME_SENDING, ChannelType.Email) @@ -111,7 +98,6 @@ const EmailDetail = () => { stats={stats} redacted={campaign.redacted} handleRetry={handleRetry} - isUsingPhonebook={isUsingPhonebook} /> )} {campaign.status === Status.Scheduled && ( diff --git a/frontend/src/components/dashboard/create/email/EmailRecipients.tsx b/frontend/src/components/dashboard/create/email/EmailRecipients.tsx index 37d6c6728..2fb9ffbff 100644 --- a/frontend/src/components/dashboard/create/email/EmailRecipients.tsx +++ b/frontend/src/components/dashboard/create/email/EmailRecipients.tsx @@ -3,7 +3,6 @@ import { i18n } from '@lingui/core' import { Dispatch, SetStateAction, - useCallback, useContext, useEffect, useState, @@ -15,7 +14,7 @@ import { useParams } from 'react-router-dom' import styles from '../Create.module.scss' -import { AgencyList, EmailPreview, EmailProgress } from 'classes' +import { EmailPreview, EmailProgress } from 'classes' import { ButtonGroup, CsvUpload, @@ -30,17 +29,9 @@ import { WarningBlock, } from 'components/common' import useIsMounted from 'components/custom-hooks/use-is-mounted' -import { PhonebookListSection } from 'components/phonebook-list' -import { LINKS, PHONEBOOK_FEATURE_ENABLE } from 'config' +import { LINKS } from 'config' import { CampaignContext } from 'contexts/campaign.context' import { sendTiming } from 'services/ga.service' -import { - deletePhonebookListForCampaign, - getPhonebookListIdForCampaign, - getPhonebookListsByChannel, - selectPhonebookList, - setPhonebookListForCampaign, -} from 'services/phonebook.service' import { CsvStatusResponse, deleteCsvStatus, @@ -81,12 +72,6 @@ const EmailRecipients = ({ const { csvFilename, numRecipients = 0 } = csvInfo const isMounted = useIsMounted() - const [phonebookLists, setPhonebookLists] = useState< - { label: string; value: string }[] - >([]) - const [selectedPhonebookListId, setSelectedPhonebookListId] = - useState() - // Poll csv status useEffect(() => { if (!campaignId) return @@ -125,28 +110,6 @@ const EmailRecipients = ({ return () => clearTimeout(timeoutId) }, [campaignId, csvFilename, forceReset, isCsvProcessing, isMounted]) - const onPhonebookListSelected = useCallback( - async (phonebookListId: number) => { - try { - // Upload phonebook list to s3 - await selectPhonebookList({ - campaignId: +(campaignId as string), - listId: phonebookListId, - }) - // Associate current campaign with phonebook list - await setPhonebookListForCampaign({ - campaignId: +(campaignId as string), - listId: phonebookListId, - }) - setIsCsvProcessing(true) - setSelectedPhonebookListId(phonebookListId) - } catch (e) { - setErrorMessage((e as Error).message) - } - }, - [campaignId] - ) - // If campaign properties change, bubble up to root campaign object useEffect(() => { updateCampaign({ @@ -156,38 +119,6 @@ const EmailRecipients = ({ }) }, [isCsvProcessing, csvFilename, numRecipients, updateCampaign]) - const retrieveAndPopulatePhonebookLists = useCallback(async () => { - const lists = await getPhonebookListsByChannel({ channel: campaign.type }) - if (lists) { - setPhonebookLists( - lists.map((l: AgencyList) => { - return { label: l.name, value: l.id.toString() } - }) - ) - } - }, [campaign.type]) - // On load, retrieve the list of phonebook lists - useEffect(() => { - void retrieveAndPopulatePhonebookLists() - }, [campaignId, retrieveAndPopulatePhonebookLists]) - - // On load, check if user was already using Phonebook. - useEffect(() => { - if (!campaignId) { - return - } - const checkIfUsingPhonebook = async () => { - const defaultPhonebookListId = await getPhonebookListIdForCampaign( - +campaignId - ) - if (defaultPhonebookListId) { - setSelectedPhonebookListId(defaultPhonebookListId) - } - } - - void checkIfUsingPhonebook() - }, [campaignId]) - // Handle file upload async function uploadFile(files: FileList) { setIsUploading(true) @@ -212,12 +143,6 @@ const EmailRecipients = ({ setIsCsvProcessing(true) setCsvInfo((info) => ({ ...info, tempCsvFilename: files[0].name })) - if (selectedPhonebookListId) { - // dissociate current campaign with phonebook list - await deletePhonebookListForCampaign(+campaignId) - // clear phonebook selector - setSelectedPhonebookListId(undefined) - } } catch (err) { setErrorMessage((err as Error).message) } @@ -234,25 +159,6 @@ const EmailRecipients = ({ return ( <> - - {!protect && PHONEBOOK_FEATURE_ENABLE === 'true' && ( - +l.value === selectedPhonebookListId - )[0]?.label - : 'Select an option' - } - /> - )}

diff --git a/frontend/src/components/dashboard/create/sms/SMSRecipients.tsx b/frontend/src/components/dashboard/create/sms/SMSRecipients.tsx index 7bd08285f..8954994c9 100644 --- a/frontend/src/components/dashboard/create/sms/SMSRecipients.tsx +++ b/frontend/src/components/dashboard/create/sms/SMSRecipients.tsx @@ -1,7 +1,7 @@ import { i18n } from '@lingui/core' import type { Dispatch, SetStateAction } from 'react' -import { useCallback, useContext, useEffect, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { OutboundLink } from 'react-ga' @@ -10,7 +10,6 @@ import { useParams } from 'react-router-dom' import styles from '../Create.module.scss' import type { SMSCampaign, SMSPreview, SMSProgress } from 'classes' -import { AgencyList } from 'classes' import { ButtonGroup, CsvUpload, @@ -26,17 +25,9 @@ import { WarningBlock, } from 'components/common' import useIsMounted from 'components/custom-hooks/use-is-mounted' -import { PhonebookListSection } from 'components/phonebook-list' -import { LINKS, PHONEBOOK_FEATURE_ENABLE } from 'config' +import { LINKS } from 'config' import { CampaignContext } from 'contexts/campaign.context' import { sendTiming } from 'services/ga.service' -import { - deletePhonebookListForCampaign, - getPhonebookListIdForCampaign, - getPhonebookListsByChannel, - selectPhonebookList, - setPhonebookListForCampaign, -} from 'services/phonebook.service' import type { CsvStatusResponse } from 'services/upload.service' import { deleteCsvStatus, @@ -62,11 +53,6 @@ const SMSRecipients = ({ const [errorMessage, setErrorMessage] = useState(null) const [isCsvProcessing, setIsCsvProcessing] = useState(initialIsProcessing) const [isUploading, setIsUploading] = useState(false) - const [phonebookLists, setPhonebookLists] = useState< - { label: string; value: string }[] - >([]) - const [selectedPhonebookListId, setSelectedPhonebookListId] = - useState() const [csvInfo, setCsvInfo] = useState< Omit >({ @@ -79,28 +65,6 @@ const SMSRecipients = ({ const { csvFilename, numRecipients = 0 } = csvInfo const isMounted = useIsMounted() - const onPhonebookListSelected = useCallback( - async (phonebookListId: number) => { - try { - // Upload phonebook list to s3 - await selectPhonebookList({ - campaignId: +(campaignId as string), - listId: phonebookListId, - }) - // Associate current campaign with phonebook list - await setPhonebookListForCampaign({ - campaignId: +(campaignId as string), - listId: phonebookListId, - }) - setIsCsvProcessing(true) - setSelectedPhonebookListId(phonebookListId) - } catch (e) { - setErrorMessage((e as Error).message) - } - }, - [campaignId] - ) - // Poll csv status useEffect(() => { if (!campaignId) return @@ -143,38 +107,6 @@ const SMSRecipients = ({ }) }, [isCsvProcessing, csvFilename, numRecipients, updateCampaign]) - const retrieveAndPopulatePhonebookLists = useCallback(async () => { - const lists = await getPhonebookListsByChannel({ channel: campaign.type }) - if (lists) { - setPhonebookLists( - lists.map((l: AgencyList) => { - return { label: l.name, value: l.id.toString() } - }) - ) - } - }, [campaign.type]) - // On load, retrieve the list of phonebook lists - useEffect(() => { - void retrieveAndPopulatePhonebookLists() - }, [campaignId, retrieveAndPopulatePhonebookLists]) - - // On load, check if user was already using Phonebook. - useEffect(() => { - if (!campaignId) { - return - } - const checkIfUsingPhonebook = async () => { - const defaultPhonebookListId = await getPhonebookListIdForCampaign( - +campaignId - ) - if (defaultPhonebookListId) { - setSelectedPhonebookListId(defaultPhonebookListId) - } - } - - void checkIfUsingPhonebook() - }, [campaignId]) - // Handle file upload async function uploadFile(files: FileList) { setIsUploading(true) @@ -199,12 +131,6 @@ const SMSRecipients = ({ setIsCsvProcessing(true) setCsvInfo((info) => ({ ...info, tempCsvFilename })) - if (selectedPhonebookListId) { - // dissociate current campaign with phonebook list - await deletePhonebookListForCampaign(+campaignId) - // clear phonebook selector - setSelectedPhonebookListId(undefined) - } } catch (err) { setErrorMessage((err as Error).message) } @@ -221,25 +147,6 @@ const SMSRecipients = ({ return ( <> - - {PHONEBOOK_FEATURE_ENABLE === 'true' && ( - +l.value === selectedPhonebookListId - )[0]?.label - : 'Select an option' - } - /> - )}

diff --git a/frontend/src/components/phonebook-list/PhonebookListSection.tsx b/frontend/src/components/phonebook-list/PhonebookListSection.tsx deleted file mode 100644 index f51e78308..000000000 --- a/frontend/src/components/phonebook-list/PhonebookListSection.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { OutboundLink } from 'react-ga' - -import { Dropdown, InfoBlock, StepSection, TextButton } from 'components/common' -import { isPhonebookAutoUnsubscribeEnabled } from 'services/phonebook.service' - -export const PhonebookListSection = ({ - phonebookLists, - onPhonebookListSelected, - retrieveAndPopulatePhonebookLists, - isProcessing, - defaultLabel, -}: { - phonebookLists: { label: string; value: string }[] - onPhonebookListSelected: (listId: number) => void - retrieveAndPopulatePhonebookLists: () => void - isProcessing: boolean - defaultLabel: string -}) => { - return ( - -

Phonebook Contact List

-

- Phonebook allows you to manage your contact lists and send messages via - Postman.   - New to Phonebook?   Log in   - - here - -   to try. -

- onPhonebookListSelected(+selected)} - disabled={!phonebookLists.length || isProcessing} - options={phonebookLists} - aria-label="Phonebook list selector" - defaultLabel={defaultLabel} - skipOnSelectForDefaultLabel={true} - > - -

- All your contact lists on Phonebook should be listed.   - - Click here to refresh - -   if it does not appear above. Manual uploading of csv will - override the Phonebook contact list above. -

- {isPhonebookAutoUnsubscribeEnabled() && ( -

- Note: If your recipient unsubscribe from your - Phonebook list, they will automatically be removed from your list. -

- )} -
-
- ) -} diff --git a/frontend/src/components/phonebook-list/index.ts b/frontend/src/components/phonebook-list/index.ts deleted file mode 100644 index b3574b745..000000000 --- a/frontend/src/components/phonebook-list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './PhonebookListSection' diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 5289c34e0..805306744 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -99,11 +99,6 @@ export const INFO_BANNER = process.env.REACT_APP_INFO_BANNER as string export const INFO_BANNER_COLOR = process.env .REACT_APP_INFO_BANNER_COLOR as string -export const PHONEBOOK_FEATURE_ENABLE = process.env - .REACT_APP_PHONEBOOK_FEATURE_ENABLE as string -export const REACT_APP_PHONEBOOK_ENABLE_AUTO_UNSUBSCRIBE = process.env - .REACT_APP_PHONEBOOK_ENABLE_AUTO_UNSUBSCRIBE as string - // Feature Launch Announcements // If `isActive` is false, the modal will not proc for ANY user export const ANNOUNCEMENT: Record = { diff --git a/frontend/src/services/phonebook.service.ts b/frontend/src/services/phonebook.service.ts deleted file mode 100644 index 50bb309bd..000000000 --- a/frontend/src/services/phonebook.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import axios, { AxiosError } from 'axios' - -import { AgencyList, ChannelType } from 'classes' -import { REACT_APP_PHONEBOOK_ENABLE_AUTO_UNSUBSCRIBE } from 'config' - -export function isPhonebookAutoUnsubscribeEnabled(): boolean { - return REACT_APP_PHONEBOOK_ENABLE_AUTO_UNSUBSCRIBE === 'true' -} - -export async function getPhonebookListsByChannel({ - channel, -}: { - channel: ChannelType -}): Promise { - try { - return (await axios.get(`/phonebook/lists/${channel}`)).data - .lists as AgencyList[] - } catch (e) { - errorHandler(e, 'Error getting phonebook lists') - } -} - -export async function selectPhonebookList({ - campaignId, - listId, -}: { - campaignId: number - listId: number -}): Promise<{ - list_id: number -}> { - try { - return axios.post(`/campaign/${campaignId}/phonebook-list`, { - list_id: listId, - }) - } catch (e) { - errorHandler(e, 'Error selecting list') - } -} - -/** - * Associate a phonebook list with a campaign. - * Calling this API would mean that a particular campaign is using a particular phonebook list. - */ -export async function setPhonebookListForCampaign({ - campaignId, - listId, -}: { - campaignId: number - listId: number -}) { - try { - return axios.put(`/campaign/${campaignId}/phonebook-associations`, { - list_id: listId, - }) - } catch (e) { - errorHandler(e, 'Error setting association between campaign and list') - } -} - -/** - * Delete the association between a phonebook list and a campaign. - * Calling this API would mean the campaign is no longer using a phonebook list. - */ -export async function deletePhonebookListForCampaign(campaignId: number) { - try { - return axios.delete(`/campaign/${campaignId}/phonebook-associations`) - } catch (e) { - errorHandler(e, 'Error deleting the association between campaign and list') - } -} - -/** - * Determines if the current campaign is using a Phonebook list. - * If it is, return the list Id. Otherwise, return undefined. - */ -export async function getPhonebookListIdForCampaign( - campaignId: number -): Promise { - try { - const response = await axios.get(`/campaign/${campaignId}/phonebook-listid`) - return response.data?.list_id - } catch (e) { - errorHandler(e, 'Error getting managed list of campaign') - } -} - -function errorHandler(e: unknown, defaultMsg?: string): never { - console.error(e) - if ( - axios.isAxiosError(e) && - e.response && - e.response.data && - e.response.data.message - ) { - throw new Error(e.response.data.message) - } - throw new Error(defaultMsg || (e as AxiosError).response?.statusText) -} diff --git a/frontend/src/test-utils/api/index.ts b/frontend/src/test-utils/api/index.ts index 0d7dfc67d..e44390c11 100644 --- a/frontend/src/test-utils/api/index.ts +++ b/frontend/src/test-utils/api/index.ts @@ -26,8 +26,6 @@ import type { TelegramTemplate, } from './interfaces' -import { ChannelType } from 'classes' - const smsTemplateClient = new TemplateClient({ xssOptions: XSS_SMS_OPTION }) const emailTemplateClient = new TemplateClient({ xssOptions: XSS_EMAIL_OPTION }) const telegramTemplateClient = new TemplateClient({ @@ -89,7 +87,6 @@ function mockCommonApis(initialState?: Partial) { ...mockCampaignUploadApis(state), ...mockUnsubscribeApis(state), ...mockProtectApis(state), - ...mockPhonebookApis(state), ], } } @@ -778,36 +775,6 @@ function mockProtectApis(state: State) { ] } -function mockPhonebookApis(state: State) { - return [ - rest.get('/phonebook/lists/:channel', (req, res, ctx) => { - const { channel } = req.params - if ( - !Object.values(ChannelType).includes(channel as unknown as ChannelType) - ) { - return res(ctx.status(400)) - } - - return res(ctx.status(200), ctx.json({ lists: state.lists })) - }), - rest.put( - '/campaign/:campaignId/phonebook-associations', - (req, res, ctx) => { - return res(ctx.status(200)) - } - ), - rest.delete( - '/campaign/:campaignId/phonebook-associations', - (req, res, ctx) => { - return res(ctx.status(200)) - } - ), - rest.get('/campaign/:campaignId/phonebook-listid', (req, res, ctx) => { - return res(ctx.status(200), ctx.json({})) - }), - ] -} - export { mockCommonApis } export * from './constants' export * from './interfaces' diff --git a/shared/src/clients/phonebook-client.class/index.ts b/shared/src/clients/phonebook-client.class/index.ts deleted file mode 100644 index 9e0116960..000000000 --- a/shared/src/clients/phonebook-client.class/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' -import * as http from 'http' -import { - GetUniqueLinksRequestDto, - GetUniqueLinksResponseDto, -} from './interfaces' - -export default class PhonebookClient { - private client: AxiosInstance - private baseUrl: string - private apiKey: string - - constructor(baseUrl: string, apiKey: string) { - this.baseUrl = baseUrl - this.apiKey = apiKey - this.client = axios.create({ - baseURL: this.baseUrl, - timeout: 5000, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': this.apiKey, - }, - httpAgent: new http.Agent({ keepAlive: true }), - }) - } - - request( - options: AxiosRequestConfig, - body?: TBody - ) { - const defaultOptions: AxiosRequestConfig = { - method: 'post', // default method will be post - } - const requestConfig = body - ? { ...defaultOptions, ...options, data: { ...body } } - : { ...defaultOptions, ...options } - return this.client.request, TBody>( - requestConfig - ) - } - - public async getManagedLists(email: string, channel: string) { - try { - const res = await this.request<{ id: number; name: string }[]>({ - method: 'get', - url: `/managed-list`, - params: { - owner: email, - channel, - }, - }) - return res.data - } catch (err) { - throw new Error('Could not get managed lists') - } - } - - public async getManagedListById(listId: number, presignedUrl: string) { - try { - const res = await this.request<{ - s3Key: string - etag: string - filename: string - }>( - { - method: 'post', - url: `/managed-list/${listId}/members/s3`, - }, - { - presignedUrl, - } - ) - return res.data - } catch (err) { - throw new Error('Could not get managed list by id') - } - } - - public async getUniqueLinksForUsers(body: GetUniqueLinksRequestDto) { - try { - const res = await this.request( - { - method: 'post', - url: '/public-user/unique-links', - }, - body - ) - return res.data - } catch (err) { - throw new Error('Could not get unique links for users') - } - } -} diff --git a/shared/src/clients/phonebook-client.class/interfaces.ts b/shared/src/clients/phonebook-client.class/interfaces.ts deleted file mode 100644 index a75c93c31..000000000 --- a/shared/src/clients/phonebook-client.class/interfaces.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type UserChannel = { - channel: string - channelId: string -} - -export type GetUniqueLinksRequestDto = { - userChannels: UserChannel[] - managedListId?: number -} - -export type GetUniqueLinksResponseDto = { - channel: string - channelId: string - userUniqueLink?: string - userUnsubscribeLink?: string -} diff --git a/worker/src/core/config.ts b/worker/src/core/config.ts index 4ea1c73e5..8c582f8af 100644 --- a/worker/src/core/config.ts +++ b/worker/src/core/config.ts @@ -75,11 +75,6 @@ export interface ConfigSchema { smsFallback: { activate: boolean; senderId: string } emailFallback: { activate: boolean } showMastheadDomain: string - phonebookContactPref: { - enabled: boolean - url: string - apiKey: string - } whatsapp: { namespace: string adminCredentialsOne: string @@ -94,13 +89,6 @@ export interface ConfigSchema { authTokenTwoExpiry: string } sgcCampaignAlertChannelWebhookUrl: string - - phonebook: { - enabled: boolean - endpointUrl: string - apiKey: string - enableAutoUnsubscribe: boolean - } } const config: Config = convict({ @@ -339,25 +327,6 @@ const config: Config = convict({ default: '.gov.sg', env: 'SHOW_MASTHEAD_DOMAIN', }, - phonebookContactPref: { - enabled: { - doc: 'Enable display of phonebook contact preferences', - default: false, - env: 'SHOW_PHONEBOOK_CONTACT_PREF', - }, - url: { - doc: 'Phonebook uri to fetch contact preferences', - default: 'phonebook.postman.gov.sg', - env: 'PHONEBOOK_URL', - format: 'required-string', - }, - apiKey: { - doc: 'API key for Phonebook contact preferences api', - default: 'somekey', - env: 'PHONEBOOK_API_KEY', - format: 'required-string', - }, - }, whatsapp: { adminCredentialsOne: { doc: 'Admin credentials for retrieving WhatsApp tokens for client 1', @@ -425,30 +394,6 @@ const config: Config = convict({ env: 'SGC_CAMPAIGN_ALERT_WEBHOOK', default: '', }, - phonebook: { - enabled: { - doc: 'Kill switch of phonebook related features', - default: false, - env: 'PHONEBOOK_FEATURE_ENABLE', - }, - endpointUrl: { - doc: 'Endpoint url of phonebook server', - default: 'http://localhost:8080', - env: 'PHONEBOOK_URL', - format: 'required-string', - }, - apiKey: { - doc: 'API key to make requests to Phonebook', - default: 'API_KEY', - env: 'PHONEBOOK_API_KEY', - format: 'required-string', - }, - enableAutoUnsubscribe: { - doc: 'Use Phonebook unsubscribe URL to automatically remove unsubscribed users from the ManagedList', - default: true, - env: 'PHONEBOOK_ENABLE_AUTO_UNSUBSCRIBE', - }, - }, }) // If mailFrom was not set in an env var, set it using the app_name diff --git a/worker/src/core/loaders/message-worker/email.class.ts b/worker/src/core/loaders/message-worker/email.class.ts index 8a1d63907..6d11834b3 100644 --- a/worker/src/core/loaders/message-worker/email.class.ts +++ b/worker/src/core/loaders/message-worker/email.class.ts @@ -58,22 +58,6 @@ class Email { }) } - async fetchManagedListIdOfCampaign(campaignId: number) { - const result = await this.connection.query<{ managed_list_id: number }>( - 'SELECT managed_list_id FROM managed_list_campaigns WHERE campaign_id = :campaign_id', - { - replacements: { campaign_id: campaignId }, - type: QueryTypes.SELECT, - } - ) - - if (result.length > 0) { - return result[0].managed_list_id - } - // Current campaign is not using Phonebook - return undefined - } - async getMessages( jobId: number, rate: number, diff --git a/worker/src/core/loaders/message-worker/interface.ts b/worker/src/core/loaders/message-worker/interface.ts index 93b489825..fdedf6c8a 100644 --- a/worker/src/core/loaders/message-worker/interface.ts +++ b/worker/src/core/loaders/message-worker/interface.ts @@ -23,12 +23,6 @@ export interface Message { unsubLink?: string // custom unsubscribe link } -export interface ContactChannel { - channel: string - channelId: string - contactPrefLink?: string -} - export interface EmailResultRow { message: Message & { senderEmail: string } } diff --git a/worker/src/core/loaders/message-worker/util/contact-preference.ts b/worker/src/core/loaders/message-worker/util/contact-preference.ts deleted file mode 100644 index f5027b6f9..000000000 --- a/worker/src/core/loaders/message-worker/util/contact-preference.ts +++ /dev/null @@ -1,133 +0,0 @@ -import axios from 'axios' -import { ContactChannel, EmailResultRow } from '../interface' -import config from '@core/config' -import { map } from 'lodash' -import CircuitBreaker from 'opossum' - -const options = { - timeout: 5000, // Trigger failure if phonebook takes longer than 5sec to respond - errorThresholdPercentage: 20, // When 20% of requests fail, open the circuit - resetTimeout: 30000, // After 30 seconds, half open the circuit and try again. -} - -type ContactPreferenceRequest = { - userChannels: ContactChannel[] -} - -async function getContactPrefLinks(request: ContactPreferenceRequest) { - const url = `${config.get( - 'phonebookContactPref.url' - )}/api/v1/generate_pref_links` - return axios - .post(url, JSON.stringify(request), { - headers: { - 'Content-Type': 'application/json', - 'x-api-key': config.get('phonebookContactPref.apiKey'), - }, - }) - .then((resp) => resp.data) - .catch((err) => { - throw err - }) -} - -const breaker = new CircuitBreaker(getContactPrefLinks, options) - -export const getContactPrefLinksForEmail = async ( - result: EmailResultRow[], - campaignId: number, - campaignOwnerEmail: string, - channel = 'Email' -) => { - const showMastheadDomain = config.get('showMastheadDomain') - const userChannelsRequest: ContactChannel[] = result.map((row) => { - return { - channel, - channelId: row.message.recipient, - } - }) - const request = { - userChannels: userChannelsRequest, - postmanCampaignId: campaignId, - postmanCampaignOwner: campaignOwnerEmail, - } - - return breaker - .fire(request) - .then((resp) => { - const userChannelsResp = resp as ContactChannel[] - const userChannelMap = userChannelsResp.reduce( - (map: Map, contactChannel: ContactChannel) => { - map.set(contactChannel.channelId, contactChannel) - return map - }, - new Map() - ) - return map(result, (row) => { - const { senderEmail } = row.message - const showMasthead = senderEmail.endsWith(showMastheadDomain) - const contactPreference = userChannelMap.get(row.message.recipient) - return { - ...row.message, - showMasthead, - contactPrefLink: contactPreference?.contactPrefLink || '', - } - }) - }) - .catch((error) => { - throw error - }) -} - -export const getMessagesWithContactPrefLinks = async ( - result: { - id: number - recipient: string - params: { [key: string]: string } - body: string - campaignId: number - }[], - campaignId: number, - campaignOwnerEmail: string, - channel = 'Sms' -) => { - const userChannelsRequest: ContactChannel[] = result.map((message) => { - return { - channel, - channelId: message.recipient, - } - }) - const request = { - userChannels: userChannelsRequest, - postmanCampaignId: campaignId, - postmanCampaignOwner: campaignOwnerEmail, - } - return breaker - .fire(request) - .then((resp) => { - const userChannelsResp = resp as ContactChannel[] - const userChannelMap = userChannelsResp.reduce( - (map: Map, contactChannel: ContactChannel) => { - map.set(contactChannel.channelId, contactChannel) - return map - }, - new Map() - ) - return map(result, (message) => { - const contactPreference = userChannelMap.get(message.recipient) - let body: string - if (contactPreference?.contactPrefLink) { - body = `${message.body}\n\nPrefer hearing from this agency a different way? Set your preference at: ${contactPreference?.contactPrefLink}` - } else { - body = message.body - } - return { - ...message, - body, - } - }) - }) - .catch((error) => { - throw error - }) -}