From 8ee52598e80a1ca68b2961e6bf622fe30bb6bd7d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 21 May 2025 14:32:35 -0400 Subject: [PATCH] feature/IO-3182-Phone-Number-Consent - Checkpoint --- client/package-lock.json | 104 ++++---- client/package.json | 6 +- .../chat-affix/chat-affix.container.jsx | 57 +---- .../chat-conversation-list.component.jsx | 81 ++---- .../chat-send-message.component.jsx | 15 +- .../phone-number-consent.component.jsx | 231 +----------------- .../shop-info/shop-info.consent.component.jsx | 50 +--- client/src/graphql/bodyshop.queries.js | 10 - client/src/redux/user/user.actions.js | 5 - client/src/redux/user/user.reducer.js | 9 +- client/src/redux/user/user.types.js | 3 +- client/src/translations/en_us/common.json | 15 +- hasura/metadata/tables.yaml | 96 +------- .../down.sql | 3 + .../up.sql | 1 + .../down.sql | 2 + .../up.sql | 1 + .../down.sql | 3 + .../up.sql | 1 + .../down.sql | 1 + .../up.sql | 1 + .../down.sql | 2 + .../up.sql | 1 + package-lock.json | 8 +- package.json | 2 +- server/graphql-client/queries.js | 130 ---------- server/routes/smsRoutes.js | 3 - server/sms/consent.js | 215 ---------------- server/sms/receive.js | 36 +-- server/sms/send.js | 26 +- server/sms/status.js | 1 + 31 files changed, 128 insertions(+), 991 deletions(-) create mode 100644 hasura/migrations/1747850002182_drop_table_public_phone_number_consent_history/down.sql create mode 100644 hasura/migrations/1747850002182_drop_table_public_phone_number_consent_history/up.sql create mode 100644 hasura/migrations/1747850205206_alter_table_public_phone_number_consent_drop_column_consent_status/down.sql create mode 100644 hasura/migrations/1747850205206_alter_table_public_phone_number_consent_drop_column_consent_status/up.sql create mode 100644 hasura/migrations/1747850221535_alter_table_public_phone_number_consent_drop_column_consent_updated_at/down.sql create mode 100644 hasura/migrations/1747850221535_alter_table_public_phone_number_consent_drop_column_consent_updated_at/up.sql create mode 100644 hasura/migrations/1747850386584_rename_table_public_phone_number_consent/down.sql create mode 100644 hasura/migrations/1747850386584_rename_table_public_phone_number_consent/up.sql create mode 100644 hasura/migrations/1747850458206_alter_table_public_bodyshops_drop_column_enforce_sms_consent/down.sql create mode 100644 hasura/migrations/1747850458206_alter_table_public_bodyshops_drop_column_enforce_sms_consent/up.sql delete mode 100644 server/sms/consent.js diff --git a/client/package-lock.json b/client/package-lock.json index 2939dbd7c..12ba5dfb3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -21,8 +21,8 @@ "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.21.0", - "@sentry/vite-plugin": "^3.4.0", + "@sentry/react": "^9.22.0", + "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", "antd": "^5.25.2", @@ -95,7 +95,7 @@ "@emotion/react": "^11.14.0", "@eslint/js": "^9.27.0", "@playwright/test": "^1.51.1", - "@sentry/webpack-plugin": "^3.4.0", + "@sentry/webpack-plugin": "^3.5.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", @@ -4461,88 +4461,88 @@ "license": "MIT" }, "node_modules/@sentry-internal/browser-utils": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.21.0.tgz", - "integrity": "sha512-/lJ5EVUDbsVsPH/sSXwWBERVtzi4kWYeFLc+u+1zr4NrfDrGnPJ5mVS1VlHwtBmYIIWv8harLP+CReg3nDcXdw==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.22.0.tgz", + "integrity": "sha512-Ou1tBnVxFAIn8i9gvrWzRotNJQYiu3awNXpsFCw6qFwmiKAVPa6b13vCdolhXnrIiuR77jY1LQnKh9hXpoRzsg==", "license": "MIT", "dependencies": { - "@sentry/core": "9.21.0" + "@sentry/core": "9.22.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.21.0.tgz", - "integrity": "sha512-Z234NgcWolFpmztCh+9smC6WlO8By5t4KucHNfYSQ0xQYQCxPL5iChj3JpF4dwv+qCYXhDFLQFQbK0U3Px056g==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.22.0.tgz", + "integrity": "sha512-zgMVkoC61fgi41zLcSZA59vOtKxcLrKBo1ECYhPD1hxEaneNqY5fhXDwlQBw96P5l2yqkgfX6YZtSdU4ejI9yA==", "license": "MIT", "dependencies": { - "@sentry/core": "9.21.0" + "@sentry/core": "9.22.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.21.0.tgz", - "integrity": "sha512-7mq3Bsp8EJa3YTIYgmWfNgJdvbeaAJ6VYsqi0yxR/vNGxY3qH+PLlv+ZOEXI2U0CL6vhqFPbqmxiUOCuAjnpGg==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.22.0.tgz", + "integrity": "sha512-9GOycoKbrclcRXfcbNV8svbmAsOS5R4wXBQmKF4pFLkmFA/lJv9kdZSNYkRvkrxdNfbMIJXP+DV9EqTZcryXig==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.21.0", - "@sentry/core": "9.21.0" + "@sentry-internal/browser-utils": "9.22.0", + "@sentry/core": "9.22.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.21.0.tgz", - "integrity": "sha512-4tHiNil8qXphaql2YXLGA/wlm0hxaadrh7x8/KErn1iy3vJpn7t/Kka5uug7c2UWhtveS6dgGmqjSkDxM5h9bA==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.22.0.tgz", + "integrity": "sha512-EcG9IMSEalFe49kowBTJObWjof/iHteDwpyuAszsFDdQUYATrVUtwpwN7o52vDYWJud4arhjrQnMamIGxa79eQ==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "9.21.0", - "@sentry/core": "9.21.0" + "@sentry-internal/replay": "9.22.0", + "@sentry/core": "9.22.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz", - "integrity": "sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz", + "integrity": "sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==", "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/@sentry/browser": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.21.0.tgz", - "integrity": "sha512-NF0G104JRP2TZ2hpMHElO4bEEUdBWknKSh2d0SRyGpJFVfOQG3oRHczXWH08A5InA/lNrS9LEdodUhiFue+F3A==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.22.0.tgz", + "integrity": "sha512-3TeRm74dvX0JdjX0AgkQa+22iUHwHnY+Q6M05NZ+tDeCNHGK/mEBTeqquS1oQX67jWyuvYmG3VV6RJUxtG9Paw==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.21.0", - "@sentry-internal/feedback": "9.21.0", - "@sentry-internal/replay": "9.21.0", - "@sentry-internal/replay-canvas": "9.21.0", - "@sentry/core": "9.21.0" + "@sentry-internal/browser-utils": "9.22.0", + "@sentry-internal/feedback": "9.22.0", + "@sentry-internal/replay": "9.22.0", + "@sentry-internal/replay-canvas": "9.22.0", + "@sentry/core": "9.22.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/bundler-plugin-core": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz", - "integrity": "sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz", + "integrity": "sha512-zDzPrhJqAAy2VzV4g540qAZH4qxzisstK2+NIJPZUUKztWRWUV2cMHsyUtdctYgloGkLyGpZJBE3RE6dmP/xqQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.18.5", - "@sentry/babel-plugin-component-annotate": "3.4.0", + "@sentry/babel-plugin-component-annotate": "3.5.0", "@sentry/cli": "2.42.2", "dotenv": "^16.3.1", "find-up": "^5.0.0", @@ -4902,22 +4902,22 @@ } }, "node_modules/@sentry/core": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.21.0.tgz", - "integrity": "sha512-K0a72Evg0fzc52Oe8R8Op5TyUMzORkk4ytt3G24lSnF4hh8NPf0m6VGkEUgQRPj27g2bF6tq9fCNsJILsf1PDA==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.22.0.tgz", + "integrity": "sha512-ixvtKmPF42Y6ckGUbFlB54OWI75H2gO5UYHojO6eXFpS7xO3ZGgV/QH6wb40mWK+0w5XZ0233FuU9VpsuE6mKA==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.21.0.tgz", - "integrity": "sha512-RGbyVo4fS7SX2AjEpdRXDo4C4IYIx0zQcI5bSTgySuhxL0JAxohcuSsNWpx48QkJwK/avtmlmCIPKgbvhF16TQ==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.22.0.tgz", + "integrity": "sha512-mI43NnioBYdG5TiXqRlhV1feZs9bnrrl+k5HOHBK7VQtymaXO0fkcsRLZTkdSgLRLMJGasZuvVhq2xK+18QyWQ==", "license": "MIT", "dependencies": { - "@sentry/browser": "9.21.0", - "@sentry/core": "9.21.0", + "@sentry/browser": "9.22.0", + "@sentry/core": "9.22.0", "hoist-non-react-statics": "^3.3.2" }, "engines": { @@ -4928,12 +4928,12 @@ } }, "node_modules/@sentry/vite-plugin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.4.0.tgz", - "integrity": "sha512-pUFBGrKsHuc8K6A7B1wU2nx65n9aIzvTlcHX9yZ1qvjEO0cZFih0JCwu1Fcav/yrtT9RMN44L/ugu/kMBHQhjQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.5.0.tgz", + "integrity": "sha512-jUnpTdpicG8wefamw7eNo2uO+Q3KCbOAiF76xH4gfNHSW6TN2hBfOtmLu7J+ive4c0Al3+NEHz19bIPR0lkwWg==", "license": "MIT", "dependencies": { - "@sentry/bundler-plugin-core": "3.4.0", + "@sentry/bundler-plugin-core": "3.5.0", "unplugin": "1.0.1" }, "engines": { @@ -4941,13 +4941,13 @@ } }, "node_modules/@sentry/webpack-plugin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.4.0.tgz", - "integrity": "sha512-i+nAxxniJV5ovijojjTF5n+Yj08Xk8my+vm8+oo0C0I7xcnI2gOKft6B0sJOq01CNbo85X5m/3/edL0PKoWE9w==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.5.0.tgz", + "integrity": "sha512-xvclj0QY2HyU7uJLzOlHSrZQBDwfnGKJxp8mmlU4L7CwmK+8xMCqlO7tYZoqE4K/wU3c2xpXql70x8qmvNMxzQ==", "dev": true, "license": "MIT", "dependencies": { - "@sentry/bundler-plugin-core": "3.4.0", + "@sentry/bundler-plugin-core": "3.5.0", "unplugin": "1.0.1", "uuid": "^9.0.0" }, diff --git a/client/package.json b/client/package.json index c6bc4f515..40c52b409 100644 --- a/client/package.json +++ b/client/package.json @@ -20,8 +20,8 @@ "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.21.0", - "@sentry/vite-plugin": "^3.4.0", + "@sentry/react": "^9.22.0", + "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", "antd": "^5.25.2", @@ -135,7 +135,7 @@ "@emotion/react": "^11.14.0", "@eslint/js": "^9.27.0", "@playwright/test": "^1.51.1", - "@sentry/webpack-plugin": "^3.4.0", + "@sentry/webpack-plugin": "^3.5.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index 85b06f2e8..b370b221c 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -8,15 +8,12 @@ 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"; 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; @@ -41,63 +38,11 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) { if (socket && socket.connected) { registerMessagingHandlers({ socket, client }); - // 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 { - 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 } - }, - { - phone_number_consent: [updatedConsent] - } - ); - - console.log("Cache update in handleConsentChanged:", { phone_number, consent_status, updatedConsent }); - } catch (error) { - console.error("Error updating consent cache in handleConsentChanged:", error.message, error.stack); - } - }; - - socket.on("consent-changed", handleConsentChanged); - return () => { - socket.off("consent-changed", handleConsentChanged); unregisterMessagingHandlers({ socket }); }; } - }, [bodyshop, socket, t, client, enforceConsent]); + }, [bodyshop, socket, t, client]); 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 3bdc9cd28..16d4c0bf1 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 { useEffect, useState, useMemo } from "react"; +import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import { Virtuoso } from "react-virtuoso"; import { createStructuredSelector } from "reselect"; @@ -10,61 +10,35 @@ import PhoneFormatter from "../../utils/PhoneFormatter"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import _ from "lodash"; import "./chat-conversation-list.styles.scss"; -import { useQuery } from "@apollo/client"; -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, - bodyshop: selectBodyshop + selectedConversation: selectSelectedConversation }); const mapDispatchToProps = (dispatch) => ({ setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId)) }); -function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) { - const { t } = useTranslation(); +function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) { + // That comma is there for a reason, do not remove it const [, forceUpdate] = useState(false); - 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: !enforceConsent || !conversationList.length || !conversationList[0]?.bodyshopid, - fetchPolicy: "cache-and-network" - }); - - const consentMap = useMemo(() => { - const map = new Map(); - consentData?.phone_number_consent?.forEach((consent) => { - map.set(consent.phone_number, consent.consent_status); - }); - return map; - }, [consentData]); - + // Re-render every minute useEffect(() => { const interval = setInterval(() => { - forceUpdate((prev) => !prev); - }, 60000); - return () => clearInterval(interval); + forceUpdate((prev) => !prev); // Toggle state to trigger re-render + }, 60000); // 1 minute in milliseconds + + return () => clearInterval(interval); // Cleanup on unmount }, []); - const sortedConversationList = useMemo(() => { + // Memoize the sorted conversation list + const sortedConversationList = React.useMemo(() => { return _.orderBy(conversationList, ["updated_at"], ["desc"]); }, [conversationList]); - const renderConversation = (index, t) => { + const renderConversation = (index) => { const item = sortedConversationList[index]; - const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""); - const isConsented = enforceConsent ? (consentMap.get(normalizedPhone) ?? false) : true; - const cardContentRight = {item.updated_at}; const cardContentLeft = item.job_conversations.length > 0 @@ -86,12 +60,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation, ); - const cardExtra = ( - <> - - {enforceConsent && !isConsented && {t("messaging.labels.no_consent")}} - - ); + const cardExtra = ; const getCardStyle = () => item.id === selectedConversation @@ -104,25 +73,9 @@ function ChatConversationListComponent({ conversationList, selectedConversation, onClick={() => setSelectedConversation(item.id)} className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`} > - -
- {cardContentLeft} -
-
- {cardContentRight} -
+ +
{cardContentLeft}
+
{cardContentRight}
); @@ -132,7 +85,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
renderConversation(index, t)} + itemContent={(index) => renderConversation(index)} style={{ height: "100%", width: "100%" }} />
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 a42946113..aa5b3f035 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, Alert } 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"; @@ -12,7 +12,6 @@ import ChatMediaSelector from "../chat-media-selector/chat-media-selector.compon import ChatPresetsComponent from "../chat-presets/chat-presets.component"; import { useQuery } from "@apollo/client"; import { GET_PHONE_NUMBER_CONSENT } from "../../graphql/consent.queries"; -import AlertComponent from "../alert/alert.component"; import { phone } from "phone"; const mapStateToProps = createStructuredSelector({ @@ -31,15 +30,13 @@ 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", - skip: !enforceConsent + fetchPolicy: "cache-and-network" }); - const isConsented = enforceConsent ? (consentData?.phone_number_consent?.[0]?.consent_status ?? false) : true; + + const isConsented = consentData?.phone_number_consent?.[0]?.consent_status ?? false; useEffect(() => { inputArea.current.focus(); @@ -72,9 +69,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi return (
- {enforceConsent && !isConsented && ( - - )} + {!isConsented && } { - 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"), @@ -249,15 +37,6 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) { render: (text) => {text}, sorter: (a, b) => a.phone_number.localeCompare(b.phone_number) }, - { - title: t("consent.status"), - dataIndex: "consent_status", - render: (status, record) => ( - - handleSetConsent(record.phone_number, checked)} /> - - ) - }, { title: t("consent.updated_at"), dataIndex: "consent_updated_at", @@ -272,9 +51,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) { onSearch={(value) => setSearch(value)} style={{ marginBottom: 16 }} /> - - - + ({ - updateBodyshopEnforceConsent: (enforce_sms_consent) => dispatch(updateBodyshopEnforceConsent(enforce_sms_consent)) -}); +const mapDispatchToProps = (dispatch) => ({}); -function ShopInfoConsentComponent({ bodyshop, updateBodyshopEnforceConsent }) { +function ShopInfoConsentComponent({ bodyshop }) { const { t } = useTranslation(); - 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; - return (
{t("settings.title")} -
- {t("settings.enforce_sms_consent")} - - { - 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/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index af16899cf..7faff13a2 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -142,7 +142,6 @@ export const QUERY_BODYSHOP = gql` intellipay_config md_ro_guard notification_followers - enforce_sms_consent employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) { id name @@ -364,12 +363,3 @@ export const GET_ACTIVE_EMPLOYEES_IN_SHOP = gql` } } `; - -export const UPDATE_BODYSHOP_ENFORCE_CONSENT = gql` - 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 - } - } -`; diff --git a/client/src/redux/user/user.actions.js b/client/src/redux/user/user.actions.js index 125aab415..01ba22534 100644 --- a/client/src/redux/user/user.actions.js +++ b/client/src/redux/user/user.actions.js @@ -123,8 +123,3 @@ 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 a72d4f068..eebb32433 100644 --- a/client/src/redux/user/user.reducer.js +++ b/client/src/redux/user/user.reducer.js @@ -125,14 +125,7 @@ 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 ff21dbb5a..d9cd6fe62 100644 --- a/client/src/redux/user/user.types.js +++ b/client/src/redux/user/user.types.js @@ -33,7 +33,6 @@ 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", - UPDATE_BODYSHOP_ENFORCE_CONSENT: "UPDATE_BODYSHOP_ENFORCE_CONSENT" + SET_IMEX_SHOP_ID: "SET_IMEX_SHOP_ID" }; export default UserActionTypes; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index e753871c4..5c4e9cd85 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -656,6 +656,7 @@ } }, "labels": { + "consent_settings": "Consent Settings", "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 fd83ecbc4..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_history - 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/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/graphql-client/queries.js b/server/graphql-client/queries.js index 84e20db6f..d61d814b9 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -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 - } - } - } -`; diff --git a/server/routes/smsRoutes.js b/server/routes/smsRoutes.js index 917699aaf..bb23d24e8 100644 --- a/server/routes/smsRoutes.js +++ b/server/routes/smsRoutes.js @@ -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; diff --git a/server/sms/consent.js b/server/sms/consent.js deleted file mode 100644 index d7a997423..000000000 --- a/server/sms/consent.js +++ /dev/null @@ -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 -}; diff --git a/server/sms/receive.js b/server/sms/receive.js index 59c4b1b32..ad539dbe4 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -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 diff --git a/server/sms/send.js b/server/sms/send.js index 81b2d4d6d..aa3c5a84c 100644 --- a/server/sms/send.js +++ b/server/sms/send.js @@ -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, diff --git a/server/sms/status.js b/server/sms/status.js index 509c76d6b..385dbaa40 100644 --- a/server/sms/status.js +++ b/server/sms/status.js @@ -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,