feature/IO-3182-Phone-Number-Consent - Checkpoint
This commit is contained in:
@@ -2805,7 +2805,6 @@ exports.GET_BODYSHOP_BY_ID = `
|
||||
intellipay_config
|
||||
state
|
||||
notification_followers
|
||||
enforce_sms_consent
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -2969,132 +2968,3 @@ exports.GET_JOB_WATCHERS_MINIMAL = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Query to get consent status for a single phone number
|
||||
exports.GET_PHONE_NUMBER_CONSENT = `
|
||||
query GET_PHONE_NUMBER_CONSENT($bodyshopid: uuid!, $phone_number: String!) {
|
||||
phone_number_consent(where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _eq: $phone_number } }) {
|
||||
id
|
||||
bodyshopid
|
||||
phone_number
|
||||
consent_status
|
||||
created_at
|
||||
updated_at
|
||||
consent_updated_at
|
||||
phone_number_consent_history(order_by: { changed_at: desc }) {
|
||||
id
|
||||
old_value
|
||||
new_value
|
||||
reason
|
||||
changed_at
|
||||
changed_by
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Query to get consent statuses for multiple phone numbers
|
||||
exports.GET_PHONE_NUMBER_CONSENTS = `
|
||||
query GET_PHONE_NUMBER_CONSENTS($bodyshopid: uuid!, $phone_numbers: [String!]) {
|
||||
phone_number_consent(where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _in: $phone_numbers } }) {
|
||||
id
|
||||
bodyshopid
|
||||
phone_number
|
||||
consent_status
|
||||
created_at
|
||||
updated_at
|
||||
consent_updated_at
|
||||
phone_number_consent_history(order_by: { changed_at: desc }) {
|
||||
id
|
||||
old_value
|
||||
new_value
|
||||
reason
|
||||
changed_at
|
||||
changed_by
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Mutation to update enforce_sms_consent
|
||||
exports.UPDATE_BODYSHOP_ENFORCE_CONSENT = `
|
||||
mutation UPDATE_BODYSHOP_ENFORCE_CONSENT($id: uuid!, $enforce_sms_consent: Boolean!) {
|
||||
update_bodyshops_by_pk(
|
||||
pk_columns: { id: $id }
|
||||
_set: { enforce_sms_consent: $enforce_sms_consent }
|
||||
) {
|
||||
id
|
||||
enforce_sms_consent
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Mutation to set consent status for a single phone number
|
||||
exports.SET_PHONE_NUMBER_CONSENT = `
|
||||
mutation SET_PHONE_NUMBER_CONSENT($bodyshopid: uuid!, $phone_number: String!, $consent_status: Boolean!) {
|
||||
insert_phone_number_consent_one(
|
||||
object: {
|
||||
bodyshopid: $bodyshopid
|
||||
phone_number: $phone_number
|
||||
consent_status: $consent_status
|
||||
consent_updated_at: "now()"
|
||||
}
|
||||
on_conflict: {
|
||||
constraint: phone_number_consent_bodyshopid_phone_number_key
|
||||
update_columns: [consent_status, consent_updated_at]
|
||||
}
|
||||
) {
|
||||
id
|
||||
bodyshopid
|
||||
phone_number
|
||||
consent_status
|
||||
created_at
|
||||
updated_at
|
||||
consent_updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Mutation to set consent status for multiple phone numbers
|
||||
exports.BULK_SET_PHONE_NUMBER_CONSENT = `
|
||||
mutation BULK_SET_PHONE_NUMBER_CONSENT($objects: [phone_number_consent_insert_input!]!) {
|
||||
insert_phone_number_consent(
|
||||
objects: $objects
|
||||
on_conflict: {
|
||||
constraint: phone_number_consent_bodyshopid_phone_number_key
|
||||
update_columns: [consent_status, consent_updated_at]
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
returning {
|
||||
id
|
||||
bodyshopid
|
||||
phone_number
|
||||
consent_status
|
||||
created_at
|
||||
updated_at
|
||||
consent_updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Mutation to insert multiple consent history records
|
||||
exports.INSERT_PHONE_NUMBER_CONSENT_HISTORY = `
|
||||
mutation INSERT_PHONE_NUMBER_CONSENT_HISTORY($objects: [phone_number_consent_history_insert_input!]!) {
|
||||
insert_phone_number_consent_history(
|
||||
objects: $objects
|
||||
) {
|
||||
affected_rows
|
||||
returning {
|
||||
id
|
||||
phone_number_consent_id
|
||||
old_value
|
||||
new_value
|
||||
reason
|
||||
changed_at
|
||||
changed_by
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -5,7 +5,6 @@ const { receive } = require("../sms/receive");
|
||||
const { send } = require("../sms/send");
|
||||
const { status, markConversationRead } = require("../sms/status");
|
||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||
const { setConsent, bulkSetConsent } = require("../sms/consent");
|
||||
|
||||
// Twilio Webhook Middleware for production
|
||||
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
|
||||
@@ -14,7 +13,5 @@ router.post("/receive", twilioWebhookMiddleware, receive);
|
||||
router.post("/send", validateFirebaseIdTokenMiddleware, send);
|
||||
router.post("/status", twilioWebhookMiddleware, status);
|
||||
router.post("/markConversationRead", validateFirebaseIdTokenMiddleware, markConversationRead);
|
||||
router.post("/setConsent", validateFirebaseIdTokenMiddleware, setConsent);
|
||||
router.post("/bulkSetConsent", validateFirebaseIdTokenMiddleware, bulkSetConsent);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
const {
|
||||
SET_PHONE_NUMBER_CONSENT,
|
||||
BULK_SET_PHONE_NUMBER_CONSENT,
|
||||
INSERT_PHONE_NUMBER_CONSENT_HISTORY
|
||||
} = require("../graphql-client/queries");
|
||||
const { phone } = require("phone");
|
||||
const gqlClient = require("../graphql-client/graphql-client").client;
|
||||
|
||||
/**
|
||||
* Set SMS consent for a phone number
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const setConsent = async (req, res) => {
|
||||
const { bodyshopid, phone_number, consent_status, reason, changed_by } = req.body;
|
||||
const {
|
||||
logger,
|
||||
ioRedis,
|
||||
ioHelpers: { getBodyshopRoom },
|
||||
sessionUtils: { getBodyshopFromRedis }
|
||||
} = req;
|
||||
|
||||
if (!bodyshopid || !phone_number || consent_status === undefined || !reason || !changed_by) {
|
||||
logger.log("set-consent-error", "ERROR", req.user.email, null, {
|
||||
type: "missing-parameters",
|
||||
bodyshopid,
|
||||
phone_number,
|
||||
consent_status,
|
||||
reason,
|
||||
changed_by
|
||||
});
|
||||
return res.status(400).json({ success: false, message: "Missing required parameter(s)." });
|
||||
}
|
||||
|
||||
try {
|
||||
// Check enforce_sms_consent
|
||||
const bodyShopData = await getBodyshopFromRedis(bodyshopid);
|
||||
const enforceConsent = bodyShopData?.enforce_sms_consent ?? false;
|
||||
|
||||
if (!enforceConsent) {
|
||||
logger.log("set-consent-error", "ERROR", req.user.email, null, {
|
||||
type: "consent-not-enforced",
|
||||
bodyshopid
|
||||
});
|
||||
return res.status(403).json({ success: false, message: "SMS consent enforcement is not enabled." });
|
||||
}
|
||||
|
||||
const normalizedPhone = phone(phone_number, "CA").phoneNumber.replace(/^\+1/, "");
|
||||
const consentResponse = await gqlClient.request(SET_PHONE_NUMBER_CONSENT, {
|
||||
bodyshopid,
|
||||
phone_number: normalizedPhone,
|
||||
consent_status
|
||||
});
|
||||
|
||||
const consent = consentResponse.insert_phone_number_consent_one;
|
||||
|
||||
// Log audit history
|
||||
const historyResponse = await gqlClient.request(INSERT_PHONE_NUMBER_CONSENT_HISTORY, {
|
||||
objects: [
|
||||
{
|
||||
phone_number_consent_id: consent.id,
|
||||
old_value: null, // Not tracking old value
|
||||
new_value: consent_status,
|
||||
reason,
|
||||
changed_by,
|
||||
changed_at: "now()"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const history = historyResponse.insert_phone_number_consent_history.returning[0];
|
||||
|
||||
// Emit WebSocket event
|
||||
const broadcastRoom = getBodyshopRoom(bodyshopid);
|
||||
ioRedis.to(broadcastRoom).emit("consent-changed", {
|
||||
bodyshopId: bodyshopid,
|
||||
phone_number: normalizedPhone,
|
||||
consent_status,
|
||||
reason
|
||||
});
|
||||
|
||||
logger.log("set-consent-success", "DEBUG", req.user.email, null, {
|
||||
bodyshopid,
|
||||
phone_number: normalizedPhone,
|
||||
consent_status
|
||||
});
|
||||
|
||||
// Return both consent and history
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
consent: {
|
||||
...consent,
|
||||
phone_number_consent_history: [history]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log("set-consent-error", "ERROR", req.user.email, null, {
|
||||
bodyshopid,
|
||||
phone_number,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.status(500).json({ success: false, message: "Failed to update consent status." });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Bulk set SMS consent for multiple phone numbers
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const bulkSetConsent = async (req, res) => {
|
||||
const { bodyshopid, consents } = req.body; // consents: [{ phone_number, consent_status }]
|
||||
const {
|
||||
logger,
|
||||
ioRedis,
|
||||
ioHelpers: { getBodyshopRoom },
|
||||
sessionUtils: { getBodyshopFromRedis }
|
||||
} = req;
|
||||
|
||||
if (!bodyshopid || !Array.isArray(consents) || consents.length === 0) {
|
||||
logger.log("bulk-set-consent-error", "ERROR", req.user.email, null, {
|
||||
type: "missing-parameters",
|
||||
bodyshopid,
|
||||
consents
|
||||
});
|
||||
return res.status(400).json({ success: false, message: "Missing or invalid parameters." });
|
||||
}
|
||||
|
||||
try {
|
||||
// Check enforce_sms_consent
|
||||
const bodyShopData = await getBodyshopFromRedis(bodyshopid);
|
||||
const enforceConsent = bodyShopData?.enforce_sms_consent ?? false;
|
||||
|
||||
if (!enforceConsent) {
|
||||
logger.log("bulk-set-consent-error", "ERROR", req.user.email, null, {
|
||||
type: "consent-not-enforced",
|
||||
bodyshopid
|
||||
});
|
||||
return res.status(403).json({ success: false, message: "SMS consent enforcement is not enabled." });
|
||||
}
|
||||
|
||||
const objects = consents.map(({ phone_number, consent_status }) => ({
|
||||
bodyshopid,
|
||||
phone_number: phone(phone_number, "CA").phoneNumber.replace(/^\+1/, ""),
|
||||
consent_status,
|
||||
consent_updated_at: "now()"
|
||||
}));
|
||||
|
||||
// Insert or update phone_number_consent records
|
||||
const consentResponse = await gqlClient.request(BULK_SET_PHONE_NUMBER_CONSENT, {
|
||||
objects
|
||||
});
|
||||
|
||||
const updatedConsents = consentResponse.insert_phone_number_consent.returning;
|
||||
|
||||
// Log audit history
|
||||
const historyObjects = updatedConsents.map((consent) => ({
|
||||
phone_number_consent_id: consent.id,
|
||||
old_value: null, // Not tracking old value for bulk updates
|
||||
new_value: consent.consent_status,
|
||||
reason: "System update via bulk upload",
|
||||
changed_by: "system",
|
||||
changed_at: "now()"
|
||||
}));
|
||||
|
||||
const historyResponse = await gqlClient.request(INSERT_PHONE_NUMBER_CONSENT_HISTORY, {
|
||||
objects: historyObjects
|
||||
});
|
||||
|
||||
const history = historyResponse.insert_phone_number_consent_history.returning;
|
||||
|
||||
// Combine consents with their history
|
||||
const consentsWithhistory = updatedConsents.map((consent, index) => ({
|
||||
...consent,
|
||||
phone_number_consent_history: [history[index]]
|
||||
}));
|
||||
|
||||
// Emit WebSocket events for each consent change
|
||||
const broadcastRoom = getBodyshopRoom(bodyshopid);
|
||||
updatedConsents.forEach((consent) => {
|
||||
ioRedis.to(broadcastRoom).emit("consent-changed", {
|
||||
bodyshopId: bodyshopid,
|
||||
phone_number: consent.phone_number,
|
||||
consent_status: consent.consent_status,
|
||||
reason: "System update via bulk upload"
|
||||
});
|
||||
});
|
||||
|
||||
logger.log("bulk-set-consent-success", "DEBUG", req.user.email, null, {
|
||||
bodyshopid,
|
||||
updatedCount: updatedConsents.length
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
updatedCount: updatedConsents.length,
|
||||
consents: consentsWithhistory
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log("bulk-set-consent-error", "ERROR", req.user.email, null, {
|
||||
bodyshopid,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.status(500).json({ success: false, message: "Failed to update consents." });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setConsent,
|
||||
bulkSetConsent
|
||||
};
|
||||
@@ -3,8 +3,7 @@ const {
|
||||
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID,
|
||||
UNARCHIVE_CONVERSATION,
|
||||
CREATE_CONVERSATION,
|
||||
INSERT_MESSAGE,
|
||||
SET_PHONE_NUMBER_CONSENT
|
||||
INSERT_MESSAGE
|
||||
} = require("../graphql-client/queries");
|
||||
const { phone } = require("phone");
|
||||
const { admin } = require("../firebase/firebase-handler");
|
||||
@@ -17,11 +16,11 @@ const InstanceManager = require("../utils/instanceMgr").default;
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const receive = async (req, res) => {
|
||||
console.dir(req.body);
|
||||
const {
|
||||
logger,
|
||||
ioRedis,
|
||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
||||
sessionUtils: { getBodyshopFromRedis }
|
||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||
} = req;
|
||||
|
||||
const loggerData = {
|
||||
@@ -54,35 +53,6 @@ const receive = async (req, res) => {
|
||||
|
||||
const bodyshop = response.bodyshops[0];
|
||||
|
||||
// Step 2: Check enforce_sms_consent
|
||||
const bodyShopData = await getBodyshopFromRedis(bodyshopid);
|
||||
const enforceConsent = bodyShopData?.enforce_sms_consent ?? false;
|
||||
|
||||
// Step 3: Handle consent only if enforce_sms_consent is true
|
||||
if (enforceConsent) {
|
||||
const normalizedPhone = phone(req.body.From, "CA").phoneNumber.replace(/^\+1/, "");
|
||||
const isStop = req.body.Body.toUpperCase().includes("STOP");
|
||||
const consentStatus = isStop ? false : true;
|
||||
const reason = isStop ? "Customer texted STOP" : "Inbound message received";
|
||||
|
||||
const consentResponse = await client.request(SET_PHONE_NUMBER_CONSENT, {
|
||||
bodyshopid: bodyshop.id,
|
||||
phone_number: normalizedPhone,
|
||||
consent_status: consentStatus,
|
||||
reason,
|
||||
changed_by: "system"
|
||||
});
|
||||
|
||||
// Emit WebSocket event for consent change
|
||||
const broadcastRoom = getBodyshopRoom(bodyshop.id);
|
||||
ioRedis.to(broadcastRoom).emit("consent-changed", {
|
||||
bodyshopId: bodyshop.id,
|
||||
phone_number: normalizedPhone,
|
||||
consent_status: consentStatus,
|
||||
reason
|
||||
});
|
||||
}
|
||||
|
||||
// Step 4: Process conversation
|
||||
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
const existingConversation = sortedConversations.length
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const twilio = require("twilio");
|
||||
const { phone } = require("phone");
|
||||
const { INSERT_MESSAGE, GET_PHONE_NUMBER_CONSENT } = require("../graphql-client/queries");
|
||||
const { INSERT_MESSAGE } = require("../graphql-client/queries");
|
||||
const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
|
||||
const gqlClient = require("../graphql-client/graphql-client").client;
|
||||
|
||||
@@ -42,30 +42,6 @@ const send = async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Check bodyshop's enforce_sms_consent setting
|
||||
const bodyShopData = await getBodyshopFromRedis(bodyshopid);
|
||||
const enforceConsent = bodyShopData?.enforce_sms_consent ?? false;
|
||||
|
||||
// Check consent only if enforcement is enabled
|
||||
if (enforceConsent) {
|
||||
const normalizedPhone = phone(to, "CA").phoneNumber.replace(/^\+1/, "");
|
||||
const consentResponse = await gqlClient.request(GET_PHONE_NUMBER_CONSENT, {
|
||||
bodyshopid,
|
||||
phone_number: normalizedPhone
|
||||
});
|
||||
if (!consentResponse.phone_number_consent?.length || !consentResponse.phone_number_consent[0].consent_status) {
|
||||
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
||||
type: "no-consent",
|
||||
phone_number: normalizedPhone,
|
||||
conversationid
|
||||
});
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "Phone number has not consented to messaging."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const message = await client.messages.create({
|
||||
body,
|
||||
messagingServiceSid,
|
||||
|
||||
@@ -9,6 +9,7 @@ const logger = require("../utils/logger");
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const status = async (req, res) => {
|
||||
console.dir(req.body);
|
||||
const { SmsSid, SmsStatus } = req.body;
|
||||
const {
|
||||
ioRedis,
|
||||
|
||||
Reference in New Issue
Block a user