feature/IO-3182-Phone-Number-Consent - Checkpoint

This commit is contained in:
Dave Richer
2025-05-28 12:17:43 -04:00
parent f6c63bbd74
commit 2e95fa25af
13 changed files with 303 additions and 157 deletions

View File

@@ -15,7 +15,13 @@ const InstanceManager = require("../utils/instanceMgr").default;
// Note: When we handle different languages, we might need to adjust these keywords accordingly.
const optInKeywords = ["START", "YES", "UNSTOP"];
const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT", "REVOKE", "OPTOUT"];
// System Message text, will also need to be localized if we support multiple languages
const systemMessageOptions = {
optIn: "Customer has Opted-in",
optOut: "Customer has Opted-out"
};
/**
* Receive SMS messages from Twilio and process them
* @param req
@@ -61,7 +67,38 @@ const receive = async (req, res) => {
const normalizedPhone = phone(req.body.From).phoneNumber.replace(/^\+1/, ""); // Normalize phone number (remove +1 for CA numbers)
const messageText = (req.body.Body || "").trim().toUpperCase();
// Step 2: Check for opt-in or opt-out keywords
// Step 2: Process conversation
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
const existingConversation = sortedConversations.length
? sortedConversations[sortedConversations.length - 1]
: null;
let conversationid;
if (existingConversation) {
conversationid = existingConversation.id;
if (existingConversation.archived) {
await client.request(UNARCHIVE_CONVERSATION, {
id: conversationid,
archived: false
});
}
} else {
const newConversationResponse = await client.request(CREATE_CONVERSATION, {
conversation: {
bodyshopid: bodyshop.id,
phone_num: phone(req.body.From).phoneNumber,
archived: false
}
});
const createdConversation = newConversationResponse.insert_conversations.returning[0];
conversationid = createdConversation.id;
}
// Step 3: Handle opt-in or opt-out keywords
let systemMessageText = "";
let socketEventType = "";
if (optInKeywords.includes(messageText) || optOutKeywords.includes(messageText)) {
// Check if the phone number is in phone_number_opt_out
const optOutCheck = await client.request(CHECK_PHONE_NUMBER_OPT_OUT, {
@@ -69,6 +106,7 @@ const receive = async (req, res) => {
phone_number: normalizedPhone
});
// Opt In
if (optInKeywords.includes(messageText)) {
// Handle opt-in
if (optOutCheck.phone_number_opt_out.length > 0) {
@@ -85,14 +123,12 @@ const receive = async (req, res) => {
affected_rows: deleteResponse.delete_phone_number_opt_out.affected_rows
});
// Emit WebSocket event to notify clients
const broadcastRoom = getBodyshopRoom(bodyshop.id);
ioRedis.to(broadcastRoom).emit("phone-number-opted-in", {
bodyshopid: bodyshop.id,
phone_number: normalizedPhone
});
systemMessageText = systemMessageOptions.optIn;
socketEventType = "phone-number-opted-in";
}
} else if (optOutKeywords.includes(messageText)) {
}
// Opt Out
else if (optOutKeywords.includes(messageText)) {
// Handle opt-out
if (optOutCheck.phone_number_opt_out.length === 0) {
// Phone number is not opted out; insert a new record
@@ -115,59 +151,78 @@ const receive = async (req, res) => {
affected_rows: insertResponse.insert_phone_number_opt_out.affected_rows
});
// Emit WebSocket event to notify clients
const broadcastRoom = getBodyshopRoom(bodyshop.id);
ioRedis.to(broadcastRoom).emit("phone-number-opted-out", {
bodyshopid: bodyshop.id,
phone_number: normalizedPhone
});
systemMessageText = systemMessageOptions.optOut;
socketEventType = "phone-number-opted-out";
}
}
// Respond immediately without processing as a regular message
res.status(200).send("");
return;
// Insert system message if an opt-in or opt-out action was taken
if (systemMessageText) {
const systemMessage = {
msid: `SYS_${req.body.SmsMessageSid}_${Date.now()}`, // Unique ID for system message
text: systemMessageText,
conversationid,
isoutbound: false,
userid: null,
image: false,
image_path: null,
is_system: true
};
const systemMessageResponse = await client.request(INSERT_MESSAGE, {
msg: systemMessage,
conversationid
});
const insertedSystemMessage = systemMessageResponse.insert_messages.returning[0];
// Emit WebSocket events for system message
const broadcastRoom = getBodyshopRoom(bodyshop.id);
const conversationRoom = getBodyshopConversationRoom({
bodyshopId: bodyshop.id,
conversationId: conversationid
});
const systemPayload = {
isoutbound: false,
conversationId: conversationid,
updated_at: insertedSystemMessage.updated_at,
msid: insertedSystemMessage.msid,
existingConversation: !!existingConversation,
newConversation: !existingConversation ? insertedSystemMessage.conversation : null
};
ioRedis.to(broadcastRoom).emit("new-message-summary", {
...systemPayload,
summary: true
});
ioRedis.to(conversationRoom).emit("new-message-detailed", {
newMessage: insertedSystemMessage,
...systemPayload,
summary: false
});
// Emit opt-in or opt-out event
ioRedis.to(broadcastRoom).emit(socketEventType, {
bodyshopid: bodyshop.id,
phone_number: normalizedPhone
});
}
}
// Step 3: Process conversation
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
const existingConversation = sortedConversations.length
? sortedConversations[sortedConversations.length - 1]
: null;
let conversationid;
let newMessage = {
// Step 4: Insert the original message
const newMessage = {
msid: req.body.SmsMessageSid,
text: req.body.Body,
image: !!req.body.MediaUrl0,
image_path: generateMediaArray(req.body, logger),
isoutbound: false,
userid: null
userid: null,
conversationid,
is_system: false
};
if (existingConversation) {
conversationid = existingConversation.id;
if (existingConversation.archived) {
await client.request(UNARCHIVE_CONVERSATION, {
id: conversationid,
archived: false
});
}
} else {
const newConversationResponse = await client.request(CREATE_CONVERSATION, {
conversation: {
bodyshopid: bodyshop.id,
phone_num: phone(req.body.From).phoneNumber,
archived: false
}
});
const createdConversation = newConversationResponse.insert_conversations.returning[0];
conversationid = createdConversation.id;
}
newMessage.conversationid = conversationid;
// Step 4: Insert the message
const insertresp = await client.request(INSERT_MESSAGE, {
msg: newMessage,
conversationid
@@ -180,7 +235,7 @@ const receive = async (req, res) => {
throw new Error("Conversation data is missing from the response.");
}
// Step 5: Notify clients
// Step 5: Notify clients for original message
const conversationRoom = getBodyshopConversationRoom({
bodyshopId: conversation.bodyshop.id,
conversationId: conversation.id
@@ -190,7 +245,7 @@ const receive = async (req, res) => {
isoutbound: false,
conversationId: conversation.id,
updated_at: message.updated_at,
msid: message.sid
msid: message.msid
};
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);