- {cardContentLeft}
- {cardContentRight}
+
+
+ {cardContentLeft}
+
+
+ {cardContentRight}
+
);
@@ -85,7 +134,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
renderConversation(index)}
+ itemContent={(index) => renderConversation(index, t)}
style={{ height: "100%", width: "100%" }}
/>
diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss
index e6169777c..86cf06152 100644
--- a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss
+++ b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss
@@ -24,7 +24,7 @@
/* Add spacing and better alignment for items */
.chat-list-item {
- padding: 0.5rem 0; /* Add spacing between list items */
+ padding: 0.2rem 0; /* Add spacing between list items */
.ant-card {
border-radius: 8px; /* Slight rounding for card edges */
diff --git a/client/src/components/chat-media-selector/chat-media-selector.component.jsx b/client/src/components/chat-media-selector/chat-media-selector.component.jsx
index b7e7d64a3..162789fb6 100644
--- a/client/src/components/chat-media-selector/chat-media-selector.component.jsx
+++ b/client/src/components/chat-media-selector/chat-media-selector.component.jsx
@@ -37,7 +37,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
- jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
+ jobId: conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid
},
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
@@ -67,14 +67,14 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
<>
{!bodyshop.uselocalmediaserver && (
)}
{bodyshop.uselocalmediaserver && open && (
)}
>
@@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
{bodyshop.uselocalmediaserver && open && (
)}
>
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 824d5e591..798532a3e 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 { Alert, Input, Spin } from "antd";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -10,6 +10,9 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.
import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
+import { useQuery } from "@apollo/client";
+import { phone } from "phone";
+import { GET_PHONE_NUMBER_OPT_OUT } from "../../graphql/phone-number-opt-out.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -25,16 +28,24 @@ const mapDispatchToProps = (dispatch) => ({
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
const inputArea = useRef(null);
const [selectedMedia, setSelectedMedia] = useState([]);
+ const { t } = useTranslation();
+
+ const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
+ const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUT, {
+ variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone },
+ fetchPolicy: "cache-and-network"
+ });
+
+ const isOptedOut = !!optOutData?.phone_number_opt_out?.[0];
useEffect(() => {
inputArea.current.focus();
}, [isSending, setMessage]);
- const { t } = useTranslation();
-
const handleEnter = () => {
const selectedImages = selectedMedia.filter((i) => i.isSelected);
if ((message === "" || !message) && selectedImages.length === 0) return;
+ if (isOptedOut) return; // Prevent sending if phone number is opted out
logImEXEvent("messaging_send_message");
if (selectedImages.length < 11) {
@@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id,
selectedMedia: selectedImages,
- imexshopid: bodyshop.imexshopid
+ imexshopid: bodyshop.imexshopid,
+ bodyshopid: bodyshop.id
};
sendMessage(newMessage);
setSelectedMedia(
@@ -57,6 +69,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
return (
+ {isOptedOut &&
}
setMessage(e.target.value)}
onPressEnter={(event) => {
event.preventDefault();
- if (!!!event.shiftKey) handleEnter();
+ if (!event.shiftKey && !isOptedOut) handleEnter();
}}
/>
({});
+
+function PhoneNumberConsentList({ bodyshop, currentUser }) {
+ const { t } = useTranslation();
+ const [search, setSearch] = useState("");
+ const notification = useNotification();
+ const { loading, data, refetch } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
+ variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
+ fetchPolicy: "network-only"
+ });
+ const client = useApolloClient();
+ const { socket } = useSocket();
+
+ const columns = [
+ {
+ title: t("consent.phone_number"),
+ dataIndex: "phone_number",
+ render: (text) => {text},
+ sorter: (a, b) => a.phone_number.localeCompare(b.phone_number)
+ },
+ {
+ title: t("consent.updated_at"),
+ dataIndex: "consent_updated_at",
+ render: (text) => {text}
+ }
+ ];
+
+ return (
+
+
setSearch(value)}
+ style={{ marginBottom: 16 }}
+ />
+
+
+
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(PhoneNumberConsentList);
diff --git a/client/src/components/shop-info/shop-info.consent.component.jsx b/client/src/components/shop-info/shop-info.consent.component.jsx
new file mode 100644
index 000000000..15754c4e6
--- /dev/null
+++ b/client/src/components/shop-info/shop-info.consent.component.jsx
@@ -0,0 +1,25 @@
+import { Typography } from "antd";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import PhoneNumberConsentList from "../phone-number-consent/phone-number-consent.component";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop
+});
+
+const mapDispatchToProps = (dispatch) => ({});
+
+function ShopInfoConsentComponent({ bodyshop }) {
+ const { t } = useTranslation();
+
+ return (
+
+
{t("settings.title")}
+ {
}
+
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoConsentComponent);
diff --git a/client/src/graphql/phone-number-opt-out.queries.js b/client/src/graphql/phone-number-opt-out.queries.js
new file mode 100644
index 000000000..d77e0aa25
--- /dev/null
+++ b/client/src/graphql/phone-number-opt-out.queries.js
@@ -0,0 +1,28 @@
+import { gql } from "@apollo/client";
+
+export const GET_PHONE_NUMBER_OPT_OUT = gql`
+ query GET_PHONE_NUMBER_OPT_OUT($bodyshopid: uuid!, $phone_number: String!) {
+ phone_number_consent(where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _eq: $phone_number } }) {
+ id
+ bodyshopid
+ phone_number
+ created_at
+ updated_at
+ }
+ }
+`;
+
+export const GET_PHONE_NUMBER_OPT_OUTS = gql`
+ query GET_PHONE_NUMBER_OPT_OUTS($bodyshopid: uuid!, $search: String) {
+ phone_number_consent(
+ where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _ilike: $search } }
+ order_by: [{ phone_number: asc }, { consent_updated_at: desc }]
+ ) {
+ id
+ bodyshopid
+ phone_number
+ created_at
+ updated_at
+ }
+ }
+`;
diff --git a/client/src/pages/shop/shop.page.component.jsx b/client/src/pages/shop/shop.page.component.jsx
index b4b354b1d..b6ded16f6 100644
--- a/client/src/pages/shop/shop.page.component.jsx
+++ b/client/src/pages/shop/shop.page.component.jsx
@@ -10,10 +10,10 @@ import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.comp
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
+import ShopInfoConsentComponent from "../../components/shop-info/shop-info.consent.component";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
-
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
@@ -91,6 +91,14 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
children:
});
}
+
+ // Add Consent Settings tab
+ items.push({
+ key: "consent",
+ label: t("bodyshop.labels.consent_settings"),
+ children:
+ });
+
return (
history({ search: `?tab=${key}` })} items={items} />
diff --git a/client/src/redux/user/user.reducer.js b/client/src/redux/user/user.reducer.js
index 0042115ff..eebb32433 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,7 @@ const userReducer = (state = INITIAL_STATE, action) => {
...state,
imexshopid: action.payload
};
+
default:
return state;
}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index e753871c4..67ddcb72d 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -656,6 +656,7 @@
}
},
"labels": {
+ "consent_settings": "Phone Number Opt-Out List",
"2tiername": "Name => RO",
"2tiersetup": "2 Tier Setup",
"2tiersource": "Source => RO",
@@ -2377,7 +2378,8 @@
"errors": {
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
"noattachedjobs": "No Jobs have been associated to this conversation. ",
- "updatinglabel": "Error updating label. {{error}}"
+ "updatinglabel": "Error updating label. {{error}}",
+ "no_consent": "This phone number has not consented to receive messages."
},
"labels": {
"addlabel": "Add a label to this conversation.",
@@ -2393,7 +2395,8 @@
"selectmedia": "Select Media",
"sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...",
- "unarchive": "Unarchive"
+ "unarchive": "Unarchive",
+ "no_consent": "No Consent"
},
"render": {
"conversation_list": "Conversation List"
@@ -3862,6 +3865,14 @@
"validation": {
"unique_vendor_name": "You must enter a unique vendor name."
}
+ },
+ "consent": {
+ "phone_number": "Phone Number",
+ "status": "Consent Status",
+ "updated_at": "Last Updated"
+ },
+ "settings": {
+ "title": "Phone Number Opt-Out List"
}
}
}
diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml
index 17c162e07..025166032 100644
--- a/hasura/metadata/tables.yaml
+++ b/hasura/metadata/tables.yaml
@@ -957,7 +957,6 @@
- enforce_conversion_category
- enforce_conversion_csr
- enforce_referral
- - enforce_sms_consent
- entegral_configuration
- entegral_id
- features
@@ -1068,7 +1067,6 @@
- enforce_conversion_category
- enforce_conversion_csr
- enforce_referral
- - enforce_sms_consent
- federal_tax_id
- id
- inhousevendorid
@@ -5864,104 +5862,12 @@
url: '{{$base_url}}/opensearch'
version: 2
- table:
- name: phone_number_consent
+ name: phone_number_opt_out
schema: public
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
- array_relationships:
- - name: phone_number_consent_histories
- using:
- foreign_key_constraint_on:
- column: phone_number_consent_id
- table:
- name: phone_number_consent_history
- schema: public
- insert_permissions:
- - role: user
- permission:
- check:
- bodyshop:
- associations:
- _and:
- - user:
- authid:
- _eq: X-Hasura-User-Id
- - active:
- _eq: true
- columns:
- - consent_status
- - phone_number
- - consent_updated_at
- - created_at
- - updated_at
- - bodyshopid
- - id
- comment: ""
- select_permissions:
- - role: user
- permission:
- columns:
- - consent_status
- - phone_number
- - consent_updated_at
- - created_at
- - updated_at
- - bodyshopid
- - id
- filter:
- bodyshop:
- associations:
- _and:
- - user:
- authid:
- _eq: X-Hasura-User-Id
- - active:
- _eq: true
- comment: ""
- update_permissions:
- - role: user
- permission:
- columns:
- - bodyshopid
- - consent_status
- - consent_updated_at
- - created_at
- - phone_number
- - updated_at
- filter: {}
- check: null
- comment: ""
-- table:
- name: phone_number_consent_history
- schema: public
- object_relationships:
- - name: phone_number_consent
- using:
- foreign_key_constraint_on: phone_number_consent_id
- select_permissions:
- - role: user
- permission:
- columns:
- - new_value
- - old_value
- - changed_by
- - reason
- - changed_at
- - id
- - phone_number_consent_id
- filter:
- phone_number_consent:
- bodyshop:
- associations:
- _and:
- - user:
- authid:
- _eq: X-Hasura-User-Id
- - active:
- _eq: true
- comment: ""
- table:
name: phonebook
schema: public
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/hasura/migrations/1747850002182_drop_table_public_phone_number_consent_history/down.sql b/hasura/migrations/1747850002182_drop_table_public_phone_number_consent_history/down.sql
new file mode 100644
index 000000000..8b7230c77
--- /dev/null
+++ b/hasura/migrations/1747850002182_drop_table_public_phone_number_consent_history/down.sql
@@ -0,0 +1,3 @@
+-- Could not auto-generate a down migration.
+-- Please write an appropriate down migration for the SQL below:
+-- DROP table "public"."phone_number_consent_history";
diff --git a/hasura/migrations/1747850002182_drop_table_public_phone_number_consent_history/up.sql b/hasura/migrations/1747850002182_drop_table_public_phone_number_consent_history/up.sql
new file mode 100644
index 000000000..1093c487f
--- /dev/null
+++ b/hasura/migrations/1747850002182_drop_table_public_phone_number_consent_history/up.sql
@@ -0,0 +1 @@
+DROP table "public"."phone_number_consent_history";
diff --git a/hasura/migrations/1747850205206_alter_table_public_phone_number_consent_drop_column_consent_status/down.sql b/hasura/migrations/1747850205206_alter_table_public_phone_number_consent_drop_column_consent_status/down.sql
new file mode 100644
index 000000000..c7f137e6f
--- /dev/null
+++ b/hasura/migrations/1747850205206_alter_table_public_phone_number_consent_drop_column_consent_status/down.sql
@@ -0,0 +1,2 @@
+alter table "public"."phone_number_consent" alter column "consent_status" drop not null;
+alter table "public"."phone_number_consent" add column "consent_status" bool;
diff --git a/hasura/migrations/1747850205206_alter_table_public_phone_number_consent_drop_column_consent_status/up.sql b/hasura/migrations/1747850205206_alter_table_public_phone_number_consent_drop_column_consent_status/up.sql
new file mode 100644
index 000000000..8daf3ed0d
--- /dev/null
+++ b/hasura/migrations/1747850205206_alter_table_public_phone_number_consent_drop_column_consent_status/up.sql
@@ -0,0 +1 @@
+alter table "public"."phone_number_consent" drop column "consent_status" cascade;
diff --git a/hasura/migrations/1747850221535_alter_table_public_phone_number_consent_drop_column_consent_updated_at/down.sql b/hasura/migrations/1747850221535_alter_table_public_phone_number_consent_drop_column_consent_updated_at/down.sql
new file mode 100644
index 000000000..74f22f6fb
--- /dev/null
+++ b/hasura/migrations/1747850221535_alter_table_public_phone_number_consent_drop_column_consent_updated_at/down.sql
@@ -0,0 +1,3 @@
+alter table "public"."phone_number_consent" alter column "consent_updated_at" set default now();
+alter table "public"."phone_number_consent" alter column "consent_updated_at" drop not null;
+alter table "public"."phone_number_consent" add column "consent_updated_at" timestamptz;
diff --git a/hasura/migrations/1747850221535_alter_table_public_phone_number_consent_drop_column_consent_updated_at/up.sql b/hasura/migrations/1747850221535_alter_table_public_phone_number_consent_drop_column_consent_updated_at/up.sql
new file mode 100644
index 000000000..154b0a1e6
--- /dev/null
+++ b/hasura/migrations/1747850221535_alter_table_public_phone_number_consent_drop_column_consent_updated_at/up.sql
@@ -0,0 +1 @@
+alter table "public"."phone_number_consent" drop column "consent_updated_at" cascade;
diff --git a/hasura/migrations/1747850386584_rename_table_public_phone_number_consent/down.sql b/hasura/migrations/1747850386584_rename_table_public_phone_number_consent/down.sql
new file mode 100644
index 000000000..ede0fa6ca
--- /dev/null
+++ b/hasura/migrations/1747850386584_rename_table_public_phone_number_consent/down.sql
@@ -0,0 +1 @@
+alter table "public"."phone_number_opt_out" rename to "phone_number_consent";
diff --git a/hasura/migrations/1747850386584_rename_table_public_phone_number_consent/up.sql b/hasura/migrations/1747850386584_rename_table_public_phone_number_consent/up.sql
new file mode 100644
index 000000000..592a8c243
--- /dev/null
+++ b/hasura/migrations/1747850386584_rename_table_public_phone_number_consent/up.sql
@@ -0,0 +1 @@
+alter table "public"."phone_number_consent" rename to "phone_number_opt_out";
diff --git a/hasura/migrations/1747850458206_alter_table_public_bodyshops_drop_column_enforce_sms_consent/down.sql b/hasura/migrations/1747850458206_alter_table_public_bodyshops_drop_column_enforce_sms_consent/down.sql
new file mode 100644
index 000000000..f25ada416
--- /dev/null
+++ b/hasura/migrations/1747850458206_alter_table_public_bodyshops_drop_column_enforce_sms_consent/down.sql
@@ -0,0 +1,2 @@
+alter table "public"."bodyshops" alter column "enforce_sms_consent" drop not null;
+alter table "public"."bodyshops" add column "enforce_sms_consent" bool;
diff --git a/hasura/migrations/1747850458206_alter_table_public_bodyshops_drop_column_enforce_sms_consent/up.sql b/hasura/migrations/1747850458206_alter_table_public_bodyshops_drop_column_enforce_sms_consent/up.sql
new file mode 100644
index 000000000..57b8d6ebd
--- /dev/null
+++ b/hasura/migrations/1747850458206_alter_table_public_bodyshops_drop_column_enforce_sms_consent/up.sql
@@ -0,0 +1 @@
+alter table "public"."bodyshops" drop column "enforce_sms_consent" cascade;
diff --git a/package-lock.json b/package-lock.json
index 8e0440f9d..0ee5c9545 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,7 @@
"aws4": "^1.13.2",
"axios": "^1.8.4",
"better-queue": "^3.8.12",
- "bullmq": "^5.52.3",
+ "bullmq": "^5.53.0",
"chart.js": "^4.4.8",
"cloudinary": "^2.6.1",
"compression": "^1.8.0",
@@ -4634,9 +4634,9 @@
}
},
"node_modules/bullmq": {
- "version": "5.52.3",
- "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.52.3.tgz",
- "integrity": "sha512-UaVkg+uSgylYWjD6/d8TVm87SjDVZ5jKwDVh/pJACmStn71aIzOIpGazh2JrkGISgT10Q/lG2I40FhPg0KgNCQ==",
+ "version": "5.53.0",
+ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.53.0.tgz",
+ "integrity": "sha512-AbzcwR+9GdgrenolOC9kApF+TkUKZpUCMiFbXgRYw9ivWhOfLCqKeajIptM7NdwhY7cpXgv+QpbweUuQZUxkyA==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.9.0",
diff --git a/package.json b/package.json
index a2da483f7..5eec68f9b 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"aws4": "^1.13.2",
"axios": "^1.8.4",
"better-queue": "^3.8.12",
- "bullmq": "^5.52.3",
+ "bullmq": "^5.53.0",
"chart.js": "^4.4.8",
"cloudinary": "^2.6.1",
"compression": "^1.8.0",
diff --git a/server/routes/smsRoutes.js b/server/routes/smsRoutes.js
index 1b169747d..c09cc1632 100644
--- a/server/routes/smsRoutes.js
+++ b/server/routes/smsRoutes.js
@@ -7,7 +7,7 @@ const { status, markConversationRead } = require("../sms/status");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
// Twilio Webhook Middleware for production
-// TODO: Look into this because it technically is never validating anything
+// TODO: This is never actually doing anything, we should probably verify
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
router.post("/receive", twilioWebhookMiddleware, receive);
diff --git a/server/sms/receive.js b/server/sms/receive.js
index f880105e9..bf5262b25 100644
--- a/server/sms/receive.js
+++ b/server/sms/receive.js
@@ -7,56 +7,17 @@ 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 }
} = req;
@@ -65,7 +26,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)
};
logger.log("sms-inbound", "DEBUG", "api", null, loggerData);
@@ -91,7 +52,7 @@ const receive = async (req, res) => {
const bodyshop = response.bodyshops[0];
- // Sort conversations by `updated_at` (or `created_at`) and pick the last one
+ // 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]
@@ -102,16 +63,13 @@ 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 // Add additional fields as necessary
+ userid: null
};
if (existingConversation) {
- // Use the existing conversation
conversationid = existingConversation.id;
-
- // Unarchive the conversation if necessary
if (existingConversation.archived) {
await client.request(UNARCHIVE_CONVERSATION, {
id: conversationid,
@@ -119,7 +77,6 @@ const receive = async (req, res) => {
});
}
} else {
- // Create a new conversation
const newConversationResponse = await client.request(CREATE_CONVERSATION, {
conversation: {
bodyshopid: bodyshop.id,
@@ -131,13 +88,12 @@ const receive = async (req, res) => {
conversationid = createdConversation.id;
}
- // Ensure `conversationid` is added to the message
newMessage.conversationid = conversationid;
- // Step 3: Insert the message into the conversation
+ // Step 5: Insert the message
const insertresp = await client.request(INSERT_MESSAGE, {
msg: newMessage,
- conversationid: conversationid
+ conversationid
});
const message = insertresp?.insert_messages?.returning?.[0];
@@ -147,8 +103,7 @@ const receive = async (req, res) => {
throw new Error("Conversation data is missing from the response.");
}
- // Step 4: Notify clients through Redis
- const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
+ // Step 6: Notify clients
const conversationRoom = getBodyshopConversationRoom({
bodyshopId: conversation.bodyshop.id,
conversationId: conversation.id
@@ -161,6 +116,8 @@ const receive = async (req, res) => {
msid: message.sid
};
+ const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
+
ioRedis.to(broadcastRoom).emit("new-message-summary", {
...commonPayload,
existingConversation: !!existingConversation,
@@ -176,7 +133,7 @@ const receive = async (req, res) => {
summary: false
});
- // Step 5: Send FCM notification
+ // Step 7: Send FCM notification
const fcmresp = await admin.messaging().send({
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
notification: {
@@ -202,10 +159,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..fdd2c81ae 100644
--- a/server/sms/send.js
+++ b/server/sms/send.js
@@ -1,7 +1,6 @@
const twilio = require("twilio");
const { phone } = require("phone");
const { INSERT_MESSAGE } = require("../graphql-client/queries");
-const logger = require("../utils/logger");
const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
const gqlClient = require("../graphql-client/graphql-client").client;
@@ -15,6 +14,7 @@ const send = async (req, res) => {
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body;
const {
ioRedis,
+ logger,
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
} = req;
@@ -26,8 +26,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) : []
});
if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid) {
@@ -39,8 +39,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) : []
});
res.status(400).json({ success: false, message: "Missing required parameter(s)." });
return;
@@ -60,8 +60,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 {