Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ajoute l'envoi des sondages par SMS #4074

Merged
merged 17 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions backend/controllers/followups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import Request from "../types/express.d.js"
import { phoneNumberValidation } from "../../lib/phone-number.js"
import config from "../config/index.js"
import { sendSimulationResultsEmail } from "../lib/messaging/email/email-service.js"
import { sendSimulationResultsSms } from "../lib/messaging/sms/sms-service.js"
import dayjs from "dayjs"

export function followup(
req: Request,
Expand Down Expand Up @@ -68,7 +70,7 @@ export async function persist(req: Request, res: Response) {
config.smsService.internationalDiallingCodes
)
) {
await followup.sendSimulationResultsSms()
await sendSimulationResultsSms(followup)
} else {
return res.status(422).send("Unsupported phone number format")
}
Expand Down Expand Up @@ -200,25 +202,27 @@ async function updateSurveyInFollowup(req: Request) {
await followup.updateSurvey(SurveyType.TousABordNotification)
break
default:
throw new Error("Unknown survey type")
throw new Error(`Unknown survey type: ${surveyType}`)
}
}

async function getRedirectUrl(req: Request) {
Shamzic marked this conversation as resolved.
Show resolved Hide resolved
const { surveyType } = req.params
const { followup } = req

switch (surveyType) {
case SurveyType.TrackClickOnSimulationUsefulnessEmail:
case SurveyType.TrackClickOnBenefitActionEmail:
case SurveyType.TrackClickOnBenefitActionSms: {
await followup.addSurveyIfMissing(SurveyType.BenefitAction)
const surveyOpened = await followup.addSurveyIfMissing(surveyType)
surveyOpened.openedAt = dayjs().toDate()
await followup.save()

return followup.surveyPath
}
case SurveyType.TousABordNotification:
return "https://www.tadao.fr/713-Demandeur-d-emploi.html"
default:
throw new Error("Unknown survey type")
throw new Error(`Unknown survey type: ${surveyType}`)
}
}

Expand All @@ -233,3 +237,14 @@ export async function logSurveyLinkClick(req: Request, res: Response) {
return res.sendStatus(404)
}
}

export async function smsSurveyLinkClick(req: Request, res: Response) {
try {
req.params.surveyType = SurveyType.TrackClickOnBenefitActionSms
const redirectUrl = await getRedirectUrl(req)
res.redirect(redirectUrl)
} catch (error) {
console.error("error", error)
return res.sendStatus(404)
}
}
129 changes: 126 additions & 3 deletions backend/lib/messaging/sending.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import dayjs from "dayjs"

import { EmailType } from "../../../lib/enums/messaging.js"
import { EmailType, SmsType } from "../../../lib/enums/messaging.js"
import { SurveyType } from "../../../lib/enums/survey.js"
import Followups from "../../models/followup.js"
import { Followup } from "../../../lib/types/followup.js"
import {
sendSimulationResultsEmail,
sendSurveyEmail,
} from "../messaging/email/email-service.js"
import {
sendSimulationResultsSms,
sendSurveyBySms,
} from "../messaging/sms/sms-service.js"
import { Survey } from "../../../lib/types/survey.js"

const DaysBeforeInitialEmail = 6
const DaysBeforeInitialSurvey = 6
const DelayAfterInitialSurveyEmail = 3

async function sendMultipleEmails(emailType: EmailType, limit: number) {
switch (emailType) {
Expand Down Expand Up @@ -37,7 +43,7 @@ async function sendMultipleInitialEmails(limit: number) {
},
},
sentAt: {
$lt: dayjs().subtract(DaysBeforeInitialEmail, "day").toDate(),
$lt: dayjs().subtract(DaysBeforeInitialSurvey, "day").toDate(),
},
surveyOptin: true,
})
Expand Down Expand Up @@ -95,3 +101,120 @@ export async function processSendEmails(
throw new Error("Missing followup id or multiple")
}
}

function getEmailSurvey(followup: Followup): Survey | undefined {
return followup.surveys.find((survey) =>
[
SurveyType.TrackClickOnBenefitActionEmail,
SurveyType.TrackClickOnSimulationUsefulnessEmail,
].includes(survey.type)
)
}

export function shouldSendSurveyBySms(followup: Followup): boolean {
const hasPhone = !!followup.phone
const hasEmail = !!followup.email
const emailSurvey = getEmailSurvey(followup)

if (hasPhone && !hasEmail) {
return true
}

if (hasPhone && hasEmail && emailSurvey && emailSurvey.answers.length === 0) {
const surveyEmailCreatedAtWithDelay = dayjs(emailSurvey.createdAt).add(
DelayAfterInitialSurveyEmail,
"day"
)
return dayjs().isAfter(surveyEmailCreatedAtWithDelay)
}

return false
}

function initialSurveySmsMongooseCriteria(): any {
const getDaysBeforeInitialSurveyDate = (): Date =>
dayjs().subtract(DaysBeforeInitialSurvey, "day").toDate()
return {
surveys: {
$not: {
$elemMatch: {
type: {
$in: [
SurveyType.BenefitAction, // TODO:
// - remove this line 10 days after this comment added in production
// - add a condition on createdAt : { $gt: "this comment added in production date" }
SurveyType.TrackClickOnBenefitActionSms,
],
},
},
},
},
smsSentAt: {
$lt: getDaysBeforeInitialSurveyDate(),
},
surveyOptin: true,
}
}

export async function filterInitialSurveySms(
followups: any[],
limit: number
): Promise<Followup[]> {
return followups
.sort((a, b) => a.createdAt - b.createdAt)
Shamzic marked this conversation as resolved.
Show resolved Hide resolved
.slice(0, limit)
.filter((followup) => shouldSendSurveyBySms(followup))
}

export async function sendMultipleInitialSms(limit: number) {
const mongooseFollowups = await Followups.find(
initialSurveySmsMongooseCriteria()
)
const followupsToSendSms: any[] = await filterInitialSurveySms(
mongooseFollowups,
limit
)

const results = await Promise.all(
followupsToSendSms.map(async (followup: Followup) => {
try {
const survey = await sendSurveyBySms(followup)
return { "Survey sent": survey._id }
} catch (error) {
return { ko: error }
}
})
)
console.log(results)
}

async function processSingleSms(smsType: SmsType, followupId: string) {
const followup: Followup | null = await Followups.findById(followupId)
if (!followup) {
throw new Error("Followup not found")
}
switch (smsType) {
case SmsType.SimulationResults:
await sendSimulationResultsSms(followup)
break
case SmsType.InitialSurvey:
await sendSurveyBySms(followup)
break
default:
throw new Error(`Unknown sms category: ${SmsType}`)
}
}

export async function processSendSms(
SmsType: SmsType,
followupId: string,
multiple: number | null
) {
if (followupId) {
await processSingleSms(SmsType, followupId)
} else if (multiple) {
await sendMultipleInitialSms(multiple)
} else {
throw new Error("Missing followup id or multiple")
}
}
120 changes: 120 additions & 0 deletions backend/lib/messaging/sms/sms-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import axios from "axios"
import config from "../../../config/index.js"
import { phoneNumberFormatting } from "./../../../../lib/phone-number.js"
import { SmsType } from "../../../../lib/enums/messaging.js"
import { Followup } from "../../../../lib/types/followup.js"
import { Survey } from "../../../../lib/types/survey.d.js"
import { SurveyType } from "../../../../lib/enums/survey.js"
import dayjs from "dayjs"

async function getSMSConfig() {
const { username, password } = config.smsService

if (!username || !password) {
throw new Error("Missing SMS service credentials")
}

return { username, password }
}

async function createAxiosInstance() {
const instance = axios.create({
timeout: 10000,
})
instance.interceptors.response.use(
(response) => {
const { data, status } = response
if (status !== 200 || data.responseCode !== 0) {
throw new Error(`SMS request failed. Body: ${JSON.stringify(data)}`)
}
return response
},
(error) => {
throw error
}
)

return instance
}

function buildSmsUrl({ accessToken, phone, username, password, smsType }) {
const { baseURL } = config
const { url } = config.smsService
const formattedPhone = phoneNumberFormatting(
phone,
config.smsService.internationalDiallingCodes
)

let text, surveyLink
switch (smsType) {
case SmsType.SimulationResults:
text = `Bonjour\nRetrouvez les résultats de votre simulation ici ${baseURL}/api/sms/${accessToken}\n1jeune1solution`
break
case SmsType.InitialSurvey:
surveyLink = `${baseURL}/api/r/${accessToken}`
text = `Bonjour\nVotre simulation sur 1jeune1solution.gouv.fr vous a-t-elle été utile?\nVoici un court sondage : ${surveyLink}\n1jeune1solution`
break
default:
throw new Error(`Unknown SMS type: ${smsType}`)
}

const encodedText = encodeURIComponent(text)
return `${url}?&originatorTON=1&originatingAddress=SIMUL 1J1S&destinationAddress=${formattedPhone}&messageText=${encodedText}&username=${username}&password=${password}`
}

export async function sendSimulationResultsSms(
followup: Followup
): Promise<Followup> {
try {
if (!followup.phone) {
throw new Error("Missing followup phone")
}

const { username, password } = await getSMSConfig()
const { phone, accessToken } = followup
const smsUrl = buildSmsUrl({
accessToken,
phone,
username,
password,
smsType: SmsType.SimulationResults,
})
const axiosInstance = await createAxiosInstance()
const { data } = await axiosInstance.get(smsUrl)
followup.smsError = undefined
if (!followup.surveyOptin) {
followup.phone = undefined
}
followup.smsSentAt = dayjs().toDate()
followup.smsMessageId = data.messageIds[0]
return await followup.save()
} catch (err) {
followup.smsError = JSON.stringify(err, null, 2)
throw err
}
}

export async function sendSurveyBySms(followup: Followup): Promise<Survey> {
if (!followup.phone) {
throw new Error("Missing followup phone")
}
const survey = await followup.addSurveyIfMissing(
SurveyType.TrackClickOnBenefitActionSms
)
const { username, password } = await getSMSConfig()
const { phone, accessToken } = followup
const smsUrl = buildSmsUrl({
accessToken,
phone,
username,
password,
smsType: SmsType.InitialSurvey,
})
const axiosInstance = await createAxiosInstance()
const { data } = await axiosInstance.get(smsUrl)
survey.messageId = data.messageIds[0]
survey.error = undefined
survey.smsSentAt = dayjs().toDate()
await followup.save()
return survey
}
3 changes: 1 addition & 2 deletions backend/models/followup-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ const FollowupSchema = new mongoose.Schema<Followup, FollowupModel>(
},
createdAt: { type: Date, default: Date.now },
sentAt: { type: Date },
smsSentAt: { type: Date },
messageId: { type: String },
smsMessageId: { type: String },
surveySentAt: { type: Date },
smsSentAt: { type: Date },
benefits: { type: Object },
surveyOptin: { type: Boolean, default: false },
surveys: {
Expand Down
Loading