diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx
index cf8afce3b..85b06f2e8 100644
--- a/client/src/components/chat-affix/chat-affix.container.jsx
+++ b/client/src/components/chat-affix/chat-affix.container.jsx
@@ -8,13 +8,15 @@ import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss";
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
-import { GET_PHONE_NUMBER_CONSENT } from "../../graphql/consent.queries.js";
+import { GET_PHONE_NUMBER_CONSENT } from "../../graphql/consent.queries";
export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation();
const client = useApolloClient();
const { socket } = useSocket();
+ const enforceConsent = bodyshop?.enforce_sms_consent ?? false;
+
useEffect(() => {
if (!bodyshop || !bodyshop.messagingservicesid) return;
@@ -39,45 +41,52 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
if (socket && socket.connected) {
registerMessagingHandlers({ socket, client });
- // Handle consent-changed events
- const handleConsentChanged = ({ bodyshopId, phone_number, consent_status }) => {
+ // Handle consent-changed events only if enforce_sms_consent is true
+ const handleConsentChanged = ({ bodyshopId, phone_number, consent_status, reason }) => {
+ if (!enforceConsent || bodyshopId !== bodyshop.id) return;
+
try {
- client.cache.writeQuery(
+ const cacheData = client.readQuery({
+ query: GET_PHONE_NUMBER_CONSENT,
+ variables: { bodyshopid: bodyshopId, phone_number }
+ });
+
+ if (!cacheData?.phone_number_consent?.[0]) {
+ console.warn("No cached data for GET_PHONE_NUMBER_CONSENT:", { bodyshopId, phone_number });
+ return;
+ }
+
+ const updatedConsent = {
+ ...cacheData.phone_number_consent[0],
+ consent_status,
+ consent_updated_at: new Date().toISOString(),
+ phone_number_consent_history: [
+ {
+ __typename: "phone_number_consent_history",
+ id: `temp-${Date.now()}`,
+ reason,
+ changed_at: new Date().toISOString(),
+ old_value: cacheData.phone_number_consent[0].consent_status,
+ new_value: consent_status,
+ changed_by: "system"
+ },
+ ...(cacheData.phone_number_consent[0].phone_number_consent_history || [])
+ ]
+ };
+
+ client.writeQuery(
{
query: GET_PHONE_NUMBER_CONSENT,
variables: { bodyshopid: bodyshopId, phone_number }
},
- (data) => {
- if (!data?.phone_number_consent?.[0]) {
- return {
- phone_number_consent: [
- {
- __typename: "phone_number_consent",
- id: null,
- bodyshopid: bodyshopId,
- phone_number,
- consent_status,
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- consent_updated_at: new Date().toISOString(),
- history: []
- }
- ]
- };
- }
- return {
- phone_number_consent: [
- {
- ...data.phone_number_consent[0],
- consent_status,
- consent_updated_at: new Date().toISOString()
- }
- ]
- };
+ {
+ phone_number_consent: [updatedConsent]
}
);
+
+ console.log("Cache update in handleConsentChanged:", { phone_number, consent_status, updatedConsent });
} catch (error) {
- console.error("Error updating consent cache:", error);
+ console.error("Error updating consent cache in handleConsentChanged:", error.message, error.stack);
}
};
@@ -88,7 +97,7 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
unregisterMessagingHandlers({ socket });
};
}
- }, [bodyshop, socket, t, client]);
+ }, [bodyshop, socket, t, client, enforceConsent]);
if (!bodyshop || !bodyshop.messagingservicesid) return <>>;
diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx
index ede2a2570..3bdc9cd28 100644
--- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx
+++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx
@@ -1,5 +1,5 @@
import { Badge, Card, List, Space, Tag } from "antd";
-import React, { useEffect, useState } from "react";
+import { useEffect, useState, useMemo } from "react";
import { connect } from "react-redux";
import { Virtuoso } from "react-virtuoso";
import { createStructuredSelector } from "reselect";
@@ -11,35 +11,37 @@ import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-displ
import _ from "lodash";
import "./chat-conversation-list.styles.scss";
import { useQuery } from "@apollo/client";
-import { GET_PHONE_NUMBER_CONSENTS } from "../../graphql/consent.queries.js";
+import { GET_PHONE_NUMBER_CONSENTS } from "../../graphql/consent.queries";
import { phone } from "phone";
import { useTranslation } from "react-i18next";
+import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
- selectedConversation: selectSelectedConversation
+ selectedConversation: selectSelectedConversation,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
});
-function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
+function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
const { t } = useTranslation();
const [, forceUpdate] = useState(false);
- // Normalize phone numbers and fetch consent statuses
+ const enforceConsent = bodyshop?.enforce_sms_consent ?? false;
+
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
const { data: consentData, loading: consentLoading } = useQuery(GET_PHONE_NUMBER_CONSENTS, {
variables: {
bodyshopid: conversationList[0]?.bodyshopid,
phone_numbers: phoneNumbers
},
- skip: !conversationList.length || !conversationList[0]?.bodyshopid,
+ skip: !enforceConsent || !conversationList.length || !conversationList[0]?.bodyshopid,
fetchPolicy: "cache-and-network"
});
- // Create a map of phone number to consent status
- const consentMap = React.useMemo(() => {
+ const consentMap = useMemo(() => {
const map = new Map();
consentData?.phone_number_consent?.forEach((consent) => {
map.set(consent.phone_number, consent.consent_status);
@@ -54,14 +56,14 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
return () => clearInterval(interval);
}, []);
- const sortedConversationList = React.useMemo(() => {
+ const sortedConversationList = useMemo(() => {
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
}, [conversationList]);
const renderConversation = (index, t) => {
const item = sortedConversationList[index];
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
- const isConsented = consentMap.get(normalizedPhone) ?? false;
+ const isConsented = enforceConsent ? (consentMap.get(normalizedPhone) ?? false) : true;
const cardContentRight = {item.updated_at};
const cardContentLeft =
@@ -87,7 +89,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
const cardExtra = (
<>
- {!isConsented && {t("messaging.labels.no_consent")}}
+ {enforceConsent && !isConsented && {t("messaging.labels.no_consent")}}
>
);
@@ -103,8 +105,24 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
>
- {cardContentLeft}
- {cardContentRight}
+
+ {cardContentLeft}
+
+
+ {cardContentRight}
+
);
diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx
index 29e5bb8c6..a42946113 100644
--- a/client/src/components/chat-send-message/chat-send-message.component.jsx
+++ b/client/src/components/chat-send-message/chat-send-message.component.jsx
@@ -1,5 +1,5 @@
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
-import { Input, Spin } from "antd";
+import { Input, Spin, Alert } from "antd";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -31,12 +31,15 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
const [selectedMedia, setSelectedMedia] = useState([]);
const { t } = useTranslation();
+ const enforceConsent = bodyshop?.enforce_sms_consent ?? false;
+
const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
const { data: consentData } = useQuery(GET_PHONE_NUMBER_CONSENT, {
variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone },
- fetchPolicy: "cache-and-network"
+ fetchPolicy: "cache-and-network",
+ skip: !enforceConsent
});
- const isConsented = consentData?.phone_number_consent?.[0]?.consent_status ?? false;
+ const isConsented = enforceConsent ? (consentData?.phone_number_consent?.[0]?.consent_status ?? false) : true;
useEffect(() => {
inputArea.current.focus();
@@ -69,8 +72,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
return (
- {!isConsented && (
-
+ {enforceConsent && !isConsented && (
+
)}
({});
-function PhoneNumberConsentList({ bodyshop }) {
+function PhoneNumberConsentList({ bodyshop, currentUser }) {
const { t } = useTranslation();
const [search, setSearch] = useState("");
- const { loading, data } = useQuery(GET_PHONE_NUMBER_CONSENTS, {
- variables: { bodyshopid: bodyshop.id, search },
+ const notification = useNotification();
+ const { loading, data, refetch } = useQuery(GET_PHONE_NUMBER_CONSENTS, {
+ variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
fetchPolicy: "network-only"
});
- const [setConsent] = useMutation(SET_PHONE_NUMBER_CONSENT);
- const [bulkSetConsent] = useMutation(BULK_SET_PHONE_NUMBER_CONSENT);
+ const client = useApolloClient();
+ const { socket } = useSocket();
+
+ useEffect(() => {
+ if (!socket || !socket.connected) return;
+
+ const handleConsentChanged = ({ bodyshopId, phone_number, consent_status, reason }) => {
+ if (bodyshopId !== bodyshop.id) return;
+
+ try {
+ const cacheData = client.readQuery({
+ query: GET_PHONE_NUMBER_CONSENTS,
+ variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
+ });
+
+ if (!cacheData?.phone_number_consent) {
+ console.warn("No cached data for GET_PHONE_NUMBER_CONSENTS in WebSocket handler");
+ return;
+ }
+
+ const updatedConsents = cacheData.phone_number_consent.map((consent) =>
+ consent.phone_number === phone_number
+ ? {
+ ...consent,
+ consent_status,
+ consent_updated_at: new Date().toISOString(),
+ phone_number_consent_history: [
+ {
+ __typename: "phone_number_consent_history",
+ id: `temp-${Date.now()}`,
+ reason,
+ changed_at: new Date().toISOString(),
+ old_value: consent.consent_status,
+ new_value: consent_status,
+ changed_by: currentUser.email
+ },
+ ...(consent.phone_number_consent_history || [])
+ ]
+ }
+ : consent
+ );
+
+ client.writeQuery(
+ {
+ query: GET_PHONE_NUMBER_CONSENTS,
+ variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
+ },
+ {
+ phone_number_consent: updatedConsents
+ }
+ );
+
+ console.log("WebSocket cache update:", { phone_number, consent_status, updatedConsents });
+ } catch (error) {
+ console.error("Error updating consent cache (WebSocket):", error.message, error.stack);
+ }
+ };
+
+ socket.on("consent-changed", handleConsentChanged);
+
+ return () => {
+ socket.off("consent-changed", handleConsentChanged);
+ };
+ }, [socket, client, bodyshop.id, search, currentUser.email]);
+
+ const handleSetConsent = async (phone_number, consent_status) => {
+ try {
+ const response = await axios.post("/sms/setConsent", {
+ bodyshopid: bodyshop.id,
+ phone_number,
+ consent_status,
+ reason: "Manual override in app",
+ changed_by: currentUser.email
+ });
+
+ const updatedConsent = {
+ ...response.data.consent,
+ phone_number_consent_history: response.data.consent.phone_number_consent_history.map((history) => ({
+ ...history,
+ __typename: "phone_number_consent_history"
+ }))
+ };
+
+ // Update Apollo cache
+ const cacheData = client.readQuery({
+ query: GET_PHONE_NUMBER_CONSENTS,
+ variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
+ });
+
+ let cacheUpdated = false;
+ if (cacheData?.phone_number_consent) {
+ const isPhoneNumberInCache = cacheData.phone_number_consent.some(
+ (consent) => consent.phone_number === phone_number
+ );
+
+ const updatedConsents = isPhoneNumberInCache
+ ? cacheData.phone_number_consent.map((consent) =>
+ consent.phone_number === phone_number ? updatedConsent : consent
+ )
+ : [...cacheData.phone_number_consent, updatedConsent];
+
+ cacheUpdated = client.writeQuery(
+ {
+ query: GET_PHONE_NUMBER_CONSENTS,
+ variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
+ },
+ {
+ phone_number_consent: updatedConsents
+ }
+ );
+
+ console.log("Cache update in handleSetConsent:", {
+ phone_number,
+ consent_status,
+ updatedConsents,
+ search
+ });
+ } else {
+ console.warn("No cached data for GET_PHONE_NUMBER_CONSENTS in handleSetConsent");
+ }
+
+ // Always refetch to ensure UI updates
+ await refetch();
+
+ notification.success({
+ message: t("consent.update_success")
+ });
+ } catch (error) {
+ notification.error({
+ message: t("consent.update_failed")
+ });
+ console.error("Error updating consent:", error.message, error.stack);
+ }
+ };
+
+ const handleBulkUpload = async (file) => {
+ const reader = new FileReader();
+ reader.onload = async (e) => {
+ const text = e.target.result;
+ const lines = text.split("\n").slice(1); // Skip header
+ const consents = lines
+ .filter((line) => line.trim())
+ .map((line) => {
+ const [phone_number, consent_status] = line.split(",");
+ return {
+ phone_number: phone(phone_number, "CA").phoneNumber.replace(/^\+1/, ""),
+ consent_status: consent_status.trim().toLowerCase() === "true"
+ };
+ });
+
+ try {
+ const response = await axios.post("/sms/bulkSetConsent", {
+ bodyshopid: bodyshop.id,
+ consents
+ });
+
+ const updatedConsents = response.data.consents.map((consent) => ({
+ ...consent,
+ phone_number_consent_history: consent.phone_number_consent_history.map((history) => ({
+ ...history,
+ __typename: "phone_number_consent_history"
+ }))
+ }));
+
+ // Update Apollo cache
+ const cacheData = client.readQuery({
+ query: GET_PHONE_NUMBER_CONSENTS,
+ variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
+ });
+
+ if (cacheData?.phone_number_consent) {
+ const updatedConsentsMap = new Map(updatedConsents.map((consent) => [consent.phone_number, consent]));
+
+ const mergedConsents = cacheData.phone_number_consent.map((consent) =>
+ updatedConsentsMap.has(consent.phone_number) ? updatedConsentsMap.get(consent.phone_number) : consent
+ );
+
+ updatedConsents.forEach((consent) => {
+ if (!mergedConsents.some((c) => c.phone_number === consent.phone_number)) {
+ mergedConsents.push(consent);
+ }
+ });
+
+ client.writeQuery(
+ {
+ query: GET_PHONE_NUMBER_CONSENTS,
+ variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
+ },
+ {
+ phone_number_consent: mergedConsents
+ }
+ );
+
+ console.log("Cache update in handleBulkUpload:", { updatedConsents, mergedConsents });
+ } else {
+ console.warn("No cached data for GET_PHONE_NUMBER_CONSENTS in handleBulkUpload");
+ }
+
+ // Refetch to ensure UI updates
+ await refetch();
+ } catch (error) {
+ notification.error({
+ message: t("consent.bulk_update_failed")
+ });
+ console.error("Bulk upload failed:", error.message, error.stack);
+ }
+ };
+ reader.readAsText(file);
+ return false;
+ };
+
+ if (!bodyshop?.enforce_sms_consent) return null;
const columns = [
{
title: t("consent.phone_number"),
dataIndex: "phone_number",
- render: (text) => {text}
+ render: (text) => {text},
+ sorter: (a, b) => a.phone_number.localeCompare(b.phone_number)
},
{
title: t("consent.status"),
dataIndex: "consent_status",
render: (status, record) => (
-
-
- setConsent({
- variables: {
- bodyshopid: bodyshop.id,
- phone_number: record.phone_number,
- consent_status: checked,
- reason: "Manual override in app",
- changed_by: "user" // Replace with actual user email from context
- },
- optimisticResponse: {
- insert_phone_number_consent_one: {
- __typename: "phone_number_consent",
- id: record.id,
- bodyshopid: bodyshop.id,
- phone_number: record.phone_number,
- consent_status: checked,
- created_at: record.created_at,
- updated_at: new Date().toISOString(),
- consent_updated_at: new Date().toISOString()
- }
- }
- })
- }
- />
+
+ handleSetConsent(record.phone_number, checked)} />
)
},
@@ -78,35 +265,6 @@ function PhoneNumberConsentList({ bodyshop }) {
}
];
- const handleBulkUpload = async (file) => {
- const reader = new FileReader();
- reader.onload = async (e) => {
- const text = e.target.result;
- const lines = text.split("\n").slice(1); // Skip header
- const objects = lines
- .filter((line) => line.trim())
- .map((line) => {
- const [phone_number, consent_status] = line.split(",");
- return {
- bodyshopid: bodyshop.id,
- phone_number: phone(phone_number, "CA").phoneNumber.replace(/^\+1/, ""),
- consent_status: consent_status.trim().toLowerCase() === "true"
- };
- });
-
- try {
- await bulkSetConsent({
- variables: { objects },
- context: { headers: { "x-reason": "System update via bulk upload", "x-changed-by": "system" } }
- });
- } catch (error) {
- console.error("Bulk upload failed:", error);
- }
- };
- reader.readAsText(file);
- return false;
- };
-
return (
({});
+const mapDispatchToProps = (dispatch) => ({
+ updateBodyshopEnforceConsent: (enforce_sms_consent) => dispatch(updateBodyshopEnforceConsent(enforce_sms_consent))
+});
-function ShopInfoConsentComponent({ bodyshop }) {
+function ShopInfoConsentComponent({ bodyshop, updateBodyshopEnforceConsent }) {
const { t } = useTranslation();
- const [updateEnforceConsent] = useMutation(UPDATE_BODYSHOP_ENFORCE_CONSENT);
-
- console.dir(bodyshop);
+ const [updateEnforceConsent] = useMutation(UPDATE_BODYSHOP_ENFORCE_CONSENT, {
+ onError: (error) => {
+ message.error(t("settings.enforce_sms_consent_error"));
+ console.error("Error updating enforce_sms_consent:", error);
+ },
+ onCompleted: (data) => {
+ message.success(t("settings.enforce_sms_consent_success"));
+ updateBodyshopEnforceConsent(data.update_bodyshops_by_pk.enforce_sms_consent);
+ }
+ });
const enforceConsent = bodyshop?.enforce_sms_consent ?? false;
@@ -27,23 +37,29 @@ function ShopInfoConsentComponent({ bodyshop }) {
{t("settings.title")}
{t("settings.enforce_sms_consent")}
-
- updateEnforceConsent({
- variables: { id: bodyshop.id, enforce_sms_consent: checked },
- optimisticResponse: {
- update_bodyshops_by_pk: {
- __typename: "bodyshops",
- id: bodyshop.id,
- enforce_sms_consent: checked
+
+ {
+ if (!checked && enforceConsent) return; // Prevent disabling
+ updateEnforceConsent({
+ variables: { id: bodyshop.id, enforce_sms_consent: checked },
+ optimisticResponse: {
+ update_bodyshops_by_pk: {
+ __typename: "bodyshops",
+ id: bodyshop.id,
+ enforce_sms_consent: checked
+ }
}
- }
- })
- }
- />
+ });
+ }}
+ disabled={enforceConsent}
+ />
+
-
+ {enforceConsent && }
);
}
diff --git a/client/src/graphql/consent.queries.js b/client/src/graphql/consent.queries.js
index 8a3f78c8f..66b15f454 100644
--- a/client/src/graphql/consent.queries.js
+++ b/client/src/graphql/consent.queries.js
@@ -10,18 +10,23 @@ export const GET_PHONE_NUMBER_CONSENT = gql`
created_at
updated_at
consent_updated_at
- history(order_by: { changed_at: desc }, limit: 1) {
+ phone_number_consent_history(order_by: { changed_at: desc }, limit: 1) {
+ id
reason
+ changed_at
+ old_value
+ new_value
+ changed_by
}
}
}
`;
export const GET_PHONE_NUMBER_CONSENTS = gql`
- query GET_PHONE_NUMBER_CONSENTS($bodyshopid: uuid!, $phone_numbers: [String!]) {
+ query GET_PHONE_NUMBER_CONSENTS($bodyshopid: uuid!, $search: String) {
phone_number_consent(
- where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _in: $phone_numbers } }
- order_by: { consent_updated_at: desc }
+ where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _ilike: $search } }
+ order_by: [{ phone_number: asc }, { consent_updated_at: desc }]
) {
id
bodyshopid
@@ -30,60 +35,13 @@ export const GET_PHONE_NUMBER_CONSENTS = gql`
created_at
updated_at
consent_updated_at
- history(order_by: { changed_at: desc }, limit: 1) {
- reason
- }
- }
- }
-`;
-
-export const SET_PHONE_NUMBER_CONSENT = gql`
- mutation SET_PHONE_NUMBER_CONSENT(
- $bodyshopid: uuid!
- $phone_number: String!
- $consent_status: Boolean!
- $reason: String!
- $changed_by: String!
- ) {
- 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
- }
- }
-`;
-
-export const BULK_SET_PHONE_NUMBER_CONSENT = gql`
- 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 {
+ phone_number_consent_history(order_by: { changed_at: desc }, limit: 1) {
id
- bodyshopid
- phone_number
- consent_status
- consent_updated_at
+ reason
+ changed_at
+ old_value
+ new_value
+ changed_by
}
}
}
diff --git a/client/src/redux/user/user.actions.js b/client/src/redux/user/user.actions.js
index 01ba22534..125aab415 100644
--- a/client/src/redux/user/user.actions.js
+++ b/client/src/redux/user/user.actions.js
@@ -123,3 +123,8 @@ export const setImexShopId = (imexshopid) => ({
type: UserActionTypes.SET_IMEX_SHOP_ID,
payload: imexshopid
});
+
+export const updateBodyshopEnforceConsent = (enforce_sms_consent) => ({
+ type: UserActionTypes.UPDATE_BODYSHOP_ENFORCE_CONSENT,
+ payload: enforce_sms_consent
+});
diff --git a/client/src/redux/user/user.reducer.js b/client/src/redux/user/user.reducer.js
index 0042115ff..a72d4f068 100644
--- a/client/src/redux/user/user.reducer.js
+++ b/client/src/redux/user/user.reducer.js
@@ -105,7 +105,6 @@ const userReducer = (state = INITIAL_STATE, action) => {
...action.payload //Spread current user details in.
}
};
-
case UserActionTypes.SET_SHOP_DETAILS:
return {
...state,
@@ -126,6 +125,14 @@ const userReducer = (state = INITIAL_STATE, action) => {
...state,
imexshopid: action.payload
};
+ case UserActionTypes.UPDATE_BODYSHOP_ENFORCE_CONSENT:
+ return {
+ ...state,
+ bodyshop: {
+ ...state.bodyshop,
+ enforce_sms_consent: action.payload
+ }
+ };
default:
return state;
}
diff --git a/client/src/redux/user/user.types.js b/client/src/redux/user/user.types.js
index d9cd6fe62..ff21dbb5a 100644
--- a/client/src/redux/user/user.types.js
+++ b/client/src/redux/user/user.types.js
@@ -33,6 +33,7 @@ const UserActionTypes = {
CHECK_ACTION_CODE_FAILURE: "CHECK_ACTION_CODE_FAILURE",
SET_CURRENT_EULA: "SET_CURRENT_EULA",
EULA_ACCEPTED: "EULA_ACCEPTED",
- SET_IMEX_SHOP_ID: "SET_IMEX_SHOP_ID"
+ SET_IMEX_SHOP_ID: "SET_IMEX_SHOP_ID",
+ UPDATE_BODYSHOP_ENFORCE_CONSENT: "UPDATE_BODYSHOP_ENFORCE_CONSENT"
};
export default UserActionTypes;
diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml
index 17c162e07..fd83ecbc4 100644
--- a/hasura/metadata/tables.yaml
+++ b/hasura/metadata/tables.yaml
@@ -5871,7 +5871,7 @@
using:
foreign_key_constraint_on: bodyshopid
array_relationships:
- - name: phone_number_consent_histories
+ - name: phone_number_consent_history
using:
foreign_key_constraint_on:
column: phone_number_consent_id
diff --git a/hasura/migrations/1747775597734_alter_table_public_phone_number_consent_history_alter_column_old_value/down.sql b/hasura/migrations/1747775597734_alter_table_public_phone_number_consent_history_alter_column_old_value/down.sql
new file mode 100644
index 000000000..fb33566b8
--- /dev/null
+++ b/hasura/migrations/1747775597734_alter_table_public_phone_number_consent_history_alter_column_old_value/down.sql
@@ -0,0 +1 @@
+alter table "public"."phone_number_consent_history" alter column "old_value" set not null;
diff --git a/hasura/migrations/1747775597734_alter_table_public_phone_number_consent_history_alter_column_old_value/up.sql b/hasura/migrations/1747775597734_alter_table_public_phone_number_consent_history_alter_column_old_value/up.sql
new file mode 100644
index 000000000..406cf8efc
--- /dev/null
+++ b/hasura/migrations/1747775597734_alter_table_public_phone_number_consent_history_alter_column_old_value/up.sql
@@ -0,0 +1 @@
+alter table "public"."phone_number_consent_history" alter column "old_value" drop not null;
diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js
index 4ed9e5905..84e20db6f 100644
--- a/server/graphql-client/queries.js
+++ b/server/graphql-client/queries.js
@@ -2970,7 +2970,7 @@ exports.GET_JOB_WATCHERS_MINIMAL = `
}
`;
-// Query to get consent status for a phone number
+// 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 } }) {
@@ -2981,7 +2981,7 @@ exports.GET_PHONE_NUMBER_CONSENT = `
created_at
updated_at
consent_updated_at
- history(order_by: { changed_at: desc }) {
+ phone_number_consent_history(order_by: { changed_at: desc }) {
id
old_value
new_value
@@ -2993,24 +2993,45 @@ exports.GET_PHONE_NUMBER_CONSENT = `
}
`;
-// Query to get consent history
-exports.GET_PHONE_NUMBER_CONSENT_HISTORY = `
- query GET_PHONE_NUMBER_CONSENT_HISTORY($phone_number_consent_id: uuid!) {
- phone_number_consent_history(where: { phone_number_consent_id: { _eq: $phone_number_consent_id } }, order_by: { changed_at: desc }) {
+// 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
- phone_number_consent_id
- old_value
- new_value
- reason
- changed_at
- changed_by
+ 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 set consent status
+// 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!, $reason: String!) {
+ mutation SET_PHONE_NUMBER_CONSENT($bodyshopid: uuid!, $phone_number: String!, $consent_status: Boolean!) {
insert_phone_number_consent_one(
object: {
bodyshopid: $bodyshopid
@@ -3031,24 +3052,10 @@ exports.SET_PHONE_NUMBER_CONSENT = `
updated_at
consent_updated_at
}
- insert_phone_number_consent_history_one(
- object: {
- phone_number_consent_id: $id
- old_value: $old_value
- new_value: $consent_status
- reason: $reason
- changed_by: $changed_by
- }
- ) {
- id
- reason
- changed_at
- changed_by
- }
}
`;
-// Mutation for bulk consent updates
+// 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(
@@ -3064,8 +3071,30 @@ exports.BULK_SET_PHONE_NUMBER_CONSENT = `
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
+ }
+ }
+ }
+`;
diff --git a/server/routes/smsRoutes.js b/server/routes/smsRoutes.js
index 1b169747d..917699aaf 100644
--- a/server/routes/smsRoutes.js
+++ b/server/routes/smsRoutes.js
@@ -5,14 +5,16 @@ 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
-// TODO: Look into this because it technically is never validating anything
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
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;
diff --git a/server/sms/consent.js b/server/sms/consent.js
new file mode 100644
index 000000000..d7a997423
--- /dev/null
+++ b/server/sms/consent.js
@@ -0,0 +1,215 @@
+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
+};
diff --git a/server/sms/receive.js b/server/sms/receive.js
index 128fda2ff..59c4b1b32 100644
--- a/server/sms/receive.js
+++ b/server/sms/receive.js
@@ -8,65 +8,27 @@ const {
} = require("../graphql-client/queries");
const { phone } = require("phone");
const { admin } = require("../firebase/firebase-handler");
-const logger = require("../utils/logger");
const InstanceManager = require("../utils/instanceMgr").default;
/**
- * Generate an array of media URLs from the request body
- * @param body
- * @returns {null|*[]}
- */
-const generateMediaArray = (body) => {
- const { NumMedia } = body;
- if (parseInt(NumMedia) > 0) {
- const ret = [];
- for (let i = 0; i < parseInt(NumMedia); i++) {
- ret.push(body[`MediaUrl${i}`]);
- }
- return ret;
- } else {
- return null;
- }
-};
-
-/**
- * Handle errors during the message receiving process
- * @param req
- * @param error
- * @param res
- * @param context
- */
-const handleError = (req, error, res, context) => {
- logger.log("sms-inbound-error", "ERROR", "api", null, {
- msid: req.body.SmsMessageSid,
- text: req.body.Body,
- image: !!req.body.MediaUrl0,
- image_path: generateMediaArray(req.body),
- messagingServiceSid: req.body.MessagingServiceSid,
- context,
- error
- });
-
- res.status(500).json({ error: error.message || "Internal Server Error" });
-};
-
-/**
- * Receive an inbound SMS message
+ * Receive SMS messages from Twilio and process them
* @param req
* @param res
* @returns {Promise<*>}
*/
const receive = async (req, res) => {
const {
+ logger,
ioRedis,
- ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
+ ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
+ sessionUtils: { getBodyshopFromRedis }
} = req;
const loggerData = {
msid: req.body.SmsMessageSid,
text: req.body.Body,
image: !!req.body.MediaUrl0,
- image_path: generateMediaArray(req.body)
+ image_path: generateMediaArray(req.body, logger)
};
logger.log("sms-inbound", "DEBUG", "api", null, loggerData);
@@ -92,30 +54,36 @@ const receive = async (req, res) => {
const bodyshop = response.bodyshops[0];
- // Step 2: Handle consent
- 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";
+ // Step 2: Check enforce_sms_consent
+ const bodyShopData = await getBodyshopFromRedis(bodyshopid);
+ const enforceConsent = bodyShopData?.enforce_sms_consent ?? false;
- const consentResponse = await client.request(SET_PHONE_NUMBER_CONSENT, {
- bodyshopid: bodyshop.id,
- phone_number: normalizedPhone,
- consent_status: consentStatus,
- reason,
- changed_by: "system"
- });
+ // 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";
- // 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
- });
+ const consentResponse = await client.request(SET_PHONE_NUMBER_CONSENT, {
+ bodyshopid: bodyshop.id,
+ phone_number: normalizedPhone,
+ consent_status: consentStatus,
+ reason,
+ changed_by: "system"
+ });
- // Step 3: Process conversation
+ // 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
? sortedConversations[sortedConversations.length - 1]
@@ -126,7 +94,7 @@ const receive = async (req, res) => {
msid: req.body.SmsMessageSid,
text: req.body.Body,
image: !!req.body.MediaUrl0,
- image_path: generateMediaArray(req.body),
+ image_path: generateMediaArray(req.body, logger),
isoutbound: false,
userid: null
};
@@ -143,7 +111,7 @@ const receive = async (req, res) => {
const newConversationResponse = await client.request(CREATE_CONVERSATION, {
conversation: {
bodyshopid: bodyshop.id,
- phone_num: normalizedPhone,
+ phone_num: phone(req.body.From).phoneNumber,
archived: false
}
});
@@ -153,7 +121,7 @@ const receive = async (req, res) => {
newMessage.conversationid = conversationid;
- // Step 4: Insert the message
+ // Step 5: Insert the message
const insertresp = await client.request(INSERT_MESSAGE, {
msg: newMessage,
conversationid
@@ -166,7 +134,7 @@ const receive = async (req, res) => {
throw new Error("Conversation data is missing from the response.");
}
- // Step 5: Notify clients
+ // Step 6: Notify clients
const conversationRoom = getBodyshopConversationRoom({
bodyshopId: conversation.bodyshop.id,
conversationId: conversation.id
@@ -179,6 +147,7 @@ const receive = async (req, res) => {
msid: message.sid
};
+ const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
ioRedis.to(broadcastRoom).emit("new-message-summary", {
...commonPayload,
existingConversation: !!existingConversation,
@@ -194,7 +163,7 @@ const receive = async (req, res) => {
summary: false
});
- // Step 6: Send FCM notification
+ // Step 7: Send FCM notification
const fcmresp = await admin.messaging().send({
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
notification: {
@@ -220,10 +189,51 @@ const receive = async (req, res) => {
res.status(200).send("");
} catch (e) {
- handleError(req, e, res, "RECEIVE_MESSAGE");
+ handleError(req, e, res, "RECEIVE_MESSAGE", logger);
}
};
+/**
+ * Generate media array from the request body
+ * @param body
+ * @param logger
+ * @returns {null|*[]}
+ */
+const generateMediaArray = (body, logger) => {
+ const { NumMedia } = body;
+ if (parseInt(NumMedia) > 0) {
+ const ret = [];
+ for (let i = 0; i < parseInt(NumMedia); i++) {
+ ret.push(body[`MediaUrl${i}`]);
+ }
+ return ret;
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Handle error logging and response
+ * @param req
+ * @param error
+ * @param res
+ * @param context
+ * @param logger
+ */
+const handleError = (req, error, res, context, logger) => {
+ logger.log("sms-inbound-error", "ERROR", "api", null, {
+ msid: req.body.SmsMessageSid,
+ text: req.body.Body,
+ image: !!req.body.MediaUrl0,
+ image_path: generateMediaArray(req.body, logger),
+ messagingServiceSid: req.body.MessagingServiceSid,
+ context,
+ error
+ });
+
+ res.status(500).json({ error: error.message || "Internal Server Error" });
+};
+
module.exports = {
receive
};
diff --git a/server/sms/send.js b/server/sms/send.js
index bc0a95da9..81b2d4d6d 100644
--- a/server/sms/send.js
+++ b/server/sms/send.js
@@ -1,21 +1,16 @@
const twilio = require("twilio");
const { phone } = require("phone");
-const { INSERT_MESSAGE } = require("../graphql-client/queries");
-const logger = require("../utils/logger");
+const { INSERT_MESSAGE, GET_PHONE_NUMBER_CONSENT } = 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;
-/**
- * Send an outbound SMS message
- * @param req
- * @param res
- * @returns {Promise}
- */
const send = async (req, res) => {
- const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body;
+ const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid, bodyshopid } = req.body;
const {
ioRedis,
- ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
+ logger,
+ ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
+ sessionUtils: { getBodyshopFromRedis }
} = req;
logger.log("sms-outbound", "DEBUG", req.user.email, null, {
@@ -26,11 +21,11 @@ const send = async (req, res) => {
conversationid,
isoutbound: true,
userid: req.user.email,
- image: req.body.selectedMedia.length > 0,
- image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
+ image: selectedMedia.length > 0,
+ image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
});
- if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid) {
+ if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid || !bodyshopid) {
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
type: "missing-parameters",
messagingServiceSid,
@@ -39,14 +34,38 @@ const send = async (req, res) => {
conversationid,
isoutbound: true,
userid: req.user.email,
- image: req.body.selectedMedia.length > 0,
- image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
+ image: selectedMedia.length > 0,
+ image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
});
res.status(400).json({ success: false, message: "Missing required parameter(s)." });
return;
}
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,
@@ -60,8 +79,8 @@ const send = async (req, res) => {
conversationid,
isoutbound: true,
userid: req.user.email,
- image: req.body.selectedMedia.length > 0,
- image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
+ image: selectedMedia.length > 0,
+ image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
};
try {