From 96cba0aaab387c4add2d46b74da616a69ac5c276 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 15 May 2025 18:54:55 -0400 Subject: [PATCH 01/45] Clear Stage --- client/package-lock.json | 162 ++++++++++++------ client/package.json | 10 +- .../down.sql | 4 + .../up.sql | 2 + .../down.sql | 1 + .../up.sql | 2 + 6 files changed, 121 insertions(+), 60 deletions(-) create mode 100644 hasura/migrations/1747158530097_alter_table_public_bodyshops_add_column_enforce_sms_consent/down.sql create mode 100644 hasura/migrations/1747158530097_alter_table_public_bodyshops_add_column_enforce_sms_consent/up.sql create mode 100644 hasura/migrations/1747158981002_create_table_public_phone_number_consent/down.sql create mode 100644 hasura/migrations/1747158981002_create_table_public_phone_number_consent/up.sql diff --git a/client/package-lock.json b/client/package-lock.json index d0cd3d682..496885fb9 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,13 +15,13 @@ "@fingerprintjs/fingerprintjs": "^4.6.1", "@firebase/analytics": "^0.10.13", "@firebase/app": "^0.12.1", - "@firebase/auth": "^1.10.2", - "@firebase/firestore": "^4.7.12", + "@firebase/auth": "^1.10.4", + "@firebase/firestore": "^4.7.14", "@firebase/messaging": "^0.12.18", "@jsreport/browser-client": "^3.1.0", - "@reduxjs/toolkit": "^2.8.1", + "@reduxjs/toolkit": "^2.8.2", "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.18.0", + "@sentry/react": "^9.19.0", "@sentry/vite-plugin": "^3.4.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", @@ -108,7 +108,7 @@ "eslint-plugin-react": "^7.37.5", "globals": "^15.15.0", "jsdom": "^26.0.0", - "memfs": "^4.17.1", + "memfs": "^4.17.2", "os-browserify": "^0.3.0", "playwright": "^1.51.1", "react-error-overlay": "^6.1.0", @@ -2963,14 +2963,14 @@ } }, "node_modules/@firebase/auth": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.2.tgz", - "integrity": "sha512-HHudcj3CJyXpoMKslNOVHGSNJdAUjvy5xBA/G/uPb32QFqvx5F3EW9RDYvve2IHEN7Vpc1QTkk/28J32x83UGA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.4.tgz", + "integrity": "sha512-rZQZQkn5x7BcHenYJi9RYWoOMJHdM/CsF6DMclb/CKbntzjUaZj+R45Iyzf/BFUJ9L2sA4bNPhJK9x+l9VKvLQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", + "@firebase/component": "0.6.16", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.1", + "@firebase/util": "1.11.3", "tslib": "^2.1.0" }, "engines": { @@ -2986,6 +2986,32 @@ } } }, + "node_modules/@firebase/auth/node_modules/@firebase/component": { + "version": "0.6.16", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.16.tgz", + "integrity": "sha512-whx+e3pgC3J9O6t4LOB8jiLk3tpWtnXaQ+xt/ys/4IGUPRI+nnWooVdtWrEnMga/gT03ug9SdEAEJLl6I1BIlg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.11.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/auth/node_modules/@firebase/util": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.3.tgz", + "integrity": "sha512-4wYnOV9FpwdCq3rHQOCrdx4AQBUbfH1p2DhWGQxlQ+D3Xl/wSxc/HttcyPN4NNFiynxoNCFGWQH/zdhRfxP1Zg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@firebase/component": { "version": "0.6.14", "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.14.tgz", @@ -3000,14 +3026,14 @@ } }, "node_modules/@firebase/firestore": { - "version": "4.7.12", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.12.tgz", - "integrity": "sha512-50KRdSp8xA7+G0wfWxlnCoEN951mt8BVdLMxeP57Rehj2DqIb41q6Fc6JH0dfQ4TlMqWua1YfVY1jPEAaHVF9w==", + "version": "4.7.14", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.14.tgz", + "integrity": "sha512-YLz71p96ACfILNjnqh7H6ilsT3AZZyDpCCE+wpl8mJklAbdpyd2ahNIqS1eBCjseqls8vQO/XTaIcbpkSgQFIg==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", + "@firebase/component": "0.6.16", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.1", + "@firebase/util": "1.11.3", "@firebase/webchannel-wrapper": "1.0.3", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", @@ -3020,6 +3046,32 @@ "@firebase/app": "0.x" } }, + "node_modules/@firebase/firestore/node_modules/@firebase/component": { + "version": "0.6.16", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.16.tgz", + "integrity": "sha512-whx+e3pgC3J9O6t4LOB8jiLk3tpWtnXaQ+xt/ys/4IGUPRI+nnWooVdtWrEnMga/gT03ug9SdEAEJLl6I1BIlg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.11.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/firestore/node_modules/@firebase/util": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.3.tgz", + "integrity": "sha512-4wYnOV9FpwdCq3rHQOCrdx4AQBUbfH1p2DhWGQxlQ+D3Xl/wSxc/HttcyPN4NNFiynxoNCFGWQH/zdhRfxP1Zg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@firebase/installations": { "version": "0.6.14", "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.14.tgz", @@ -3836,9 +3888,9 @@ "license": "MIT" }, "node_modules/@reduxjs/toolkit": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.1.tgz", - "integrity": "sha512-GLjHS13LiBdiuxSJvfWs3+Cx5yt97mCbuVlDteTusS6VRksPhoWviO8L1e3Re1G94m6lkw/l4pjEEyyNaGf19g==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -4458,50 +4510,50 @@ "license": "MIT" }, "node_modules/@sentry-internal/browser-utils": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.18.0.tgz", - "integrity": "sha512-TwSlmgYpHhe55JpOcVApkM0XcXZh1/cYuEPKPFgeaaPD8BrQrLJJvwKxnonSWXOhdnkJxi4GgK7j7mw57PS4aA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.19.0.tgz", + "integrity": "sha512-DlEHX4eIHe5yIuh/cFu9OiaFuk1CTnFK95zj61I7Q2fxmN43dIwC3xAAGJ/Hy+GDQi7kU+BiS2sudSHSTq81BA==", "license": "MIT", "dependencies": { - "@sentry/core": "9.18.0" + "@sentry/core": "9.19.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.18.0.tgz", - "integrity": "sha512-QlrB8oQK+5bfhbgK6yHF6rLwLNJ9XuGblTc51yVkm4d4jn4W/HDyaNqMfQF+JXdTiFatl8oz2xdKR8kGK8kXyg==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.19.0.tgz", + "integrity": "sha512-yixRrv4NfpjhFW56AuUTjVwZlignB9FWAXXyrmRP3SsFeJCFrAsSD8HOxV9RXNr9ePYl7MEU0Agi43YWhJsiAw==", "license": "MIT", "dependencies": { - "@sentry/core": "9.18.0" + "@sentry/core": "9.19.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.18.0.tgz", - "integrity": "sha512-2A32FFwrlZtdpBruvpcLEfucu6BpyqOk3F4Bo5smM/5q7u0pa7q5d9FSY5l3nwKEAFAoLGv3hcCb+8wxMm50xA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.19.0.tgz", + "integrity": "sha512-i/X9brRchbAF25yjxLTI7E8eoESRPBgIyQOWoWRXXt2n51iBRTjLXSaEfGvjdN+qrMq/yd6nC1/UqJVxXHeIhA==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.18.0", - "@sentry/core": "9.18.0" + "@sentry-internal/browser-utils": "9.19.0", + "@sentry/core": "9.19.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.18.0.tgz", - "integrity": "sha512-3DEyQLmHcYgcwJ8n8eMhI6bhhawPuMc2xTT+Az8gXMqCO/X9ZACpipAmhXFjYP9Ptl+w0Vh3nllJw+gXc/DOsg==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.19.0.tgz", + "integrity": "sha512-YC8yrOjuKSfQgGniJnzkdbFsWEPTlNpzeeYPTxS4ouH1FwfGrSkPmcddjor2YHaLfiuHHqQ/Vvq70n+zruJH7A==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "9.18.0", - "@sentry/core": "9.18.0" + "@sentry-internal/replay": "9.19.0", + "@sentry/core": "9.19.0" }, "engines": { "node": ">=18" @@ -4517,16 +4569,16 @@ } }, "node_modules/@sentry/browser": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.18.0.tgz", - "integrity": "sha512-0SWfp4J2+mH4lZOcHfyIwt9VoGD7yCGQE1cm0BPcLwKnrVQeXHtUXNYNy8HTHSjTGyoFDhEAYelE/tdA3OLcWQ==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.19.0.tgz", + "integrity": "sha512-efKfPQ0yQkdIkC7qJ5TIHxnecLNENGUYl1YD/TC8yyzW2JRf/3OYo5yg1hY2rhsP5RwQShXlT7uA03ABVIkA4A==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.18.0", - "@sentry-internal/feedback": "9.18.0", - "@sentry-internal/replay": "9.18.0", - "@sentry-internal/replay-canvas": "9.18.0", - "@sentry/core": "9.18.0" + "@sentry-internal/browser-utils": "9.19.0", + "@sentry-internal/feedback": "9.19.0", + "@sentry-internal/replay": "9.19.0", + "@sentry-internal/replay-canvas": "9.19.0", + "@sentry/core": "9.19.0" }, "engines": { "node": ">=18" @@ -4899,22 +4951,22 @@ } }, "node_modules/@sentry/core": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.18.0.tgz", - "integrity": "sha512-kRVH8BqMiaU2FTHYa68zNlAloS43jl4XtIEHkLKVH/7gUtwRmM4Gqj8P7RTrZdO1Lo7ksYnGj+AG05Z09CRbOw==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.19.0.tgz", + "integrity": "sha512-I41rKpMJHHZb0z0Nja+Lxto6IkEEmX3uWjnECypF8Z1HIjeJB0+PXl8p/7TeaKYqw2J2GYcRTg7jQZDmvKle1w==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.18.0.tgz", - "integrity": "sha512-1cCLYZrZ2gu6H8eE83DC47kLf+pzD1Rim3dDoOEvwt1F5cD3K/DBeIhoHZaXqBeQxuVyHXOOLXSAC/CIuas5Aw==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.19.0.tgz", + "integrity": "sha512-tHuzPVbqKsONlFQsy7FqqGjBaujQoLRIDBLlPPMNoiGvP3rodBl6t1v5zoNAq4m47i3MhvpLEYf6C00j1w5UMQ==", "license": "MIT", "dependencies": { - "@sentry/browser": "9.18.0", - "@sentry/core": "9.18.0", + "@sentry/browser": "9.19.0", + "@sentry/core": "9.19.0", "hoist-non-react-statics": "^3.3.2" }, "engines": { @@ -11833,9 +11885,9 @@ } }, "node_modules/memfs": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.1.tgz", - "integrity": "sha512-thuTRd7F4m4dReCIy7vv4eNYnU6XI/tHMLSMMHLiortw/Y0QxqKtinG523U2aerzwYWGi606oBP4oMPy4+edag==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", + "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/client/package.json b/client/package.json index 1779b36c5..4018d9629 100644 --- a/client/package.json +++ b/client/package.json @@ -14,13 +14,13 @@ "@fingerprintjs/fingerprintjs": "^4.6.1", "@firebase/analytics": "^0.10.13", "@firebase/app": "^0.12.1", - "@firebase/auth": "^1.10.2", - "@firebase/firestore": "^4.7.12", + "@firebase/auth": "^1.10.4", + "@firebase/firestore": "^4.7.14", "@firebase/messaging": "^0.12.18", "@jsreport/browser-client": "^3.1.0", - "@reduxjs/toolkit": "^2.8.1", + "@reduxjs/toolkit": "^2.8.2", "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.18.0", + "@sentry/react": "^9.19.0", "@sentry/vite-plugin": "^3.4.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", @@ -148,7 +148,7 @@ "eslint-plugin-react": "^7.37.5", "globals": "^15.15.0", "jsdom": "^26.0.0", - "memfs": "^4.17.1", + "memfs": "^4.17.2", "os-browserify": "^0.3.0", "playwright": "^1.51.1", "react-error-overlay": "^6.1.0", diff --git a/hasura/migrations/1747158530097_alter_table_public_bodyshops_add_column_enforce_sms_consent/down.sql b/hasura/migrations/1747158530097_alter_table_public_bodyshops_add_column_enforce_sms_consent/down.sql new file mode 100644 index 000000000..fd9787812 --- /dev/null +++ b/hasura/migrations/1747158530097_alter_table_public_bodyshops_add_column_enforce_sms_consent/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "enforce_sms_consent" boolean +-- null; diff --git a/hasura/migrations/1747158530097_alter_table_public_bodyshops_add_column_enforce_sms_consent/up.sql b/hasura/migrations/1747158530097_alter_table_public_bodyshops_add_column_enforce_sms_consent/up.sql new file mode 100644 index 000000000..dfb9ef133 --- /dev/null +++ b/hasura/migrations/1747158530097_alter_table_public_bodyshops_add_column_enforce_sms_consent/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "enforce_sms_consent" boolean + null; diff --git a/hasura/migrations/1747158981002_create_table_public_phone_number_consent/down.sql b/hasura/migrations/1747158981002_create_table_public_phone_number_consent/down.sql new file mode 100644 index 000000000..d113cb9a7 --- /dev/null +++ b/hasura/migrations/1747158981002_create_table_public_phone_number_consent/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."phone_number_consent"; diff --git a/hasura/migrations/1747158981002_create_table_public_phone_number_consent/up.sql b/hasura/migrations/1747158981002_create_table_public_phone_number_consent/up.sql new file mode 100644 index 000000000..fc7cddeba --- /dev/null +++ b/hasura/migrations/1747158981002_create_table_public_phone_number_consent/up.sql @@ -0,0 +1,2 @@ +CREATE TABLE "public"."phone_number_consent" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "bodyshopid" uuid NOT NULL, "phone_number" text NOT NULL, "consent_status" boolean NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "consent_updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE restrict ON DELETE restrict, UNIQUE ("id"), UNIQUE ("bodyshopid", "phone_number")); +CREATE EXTENSION IF NOT EXISTS pgcrypto; From 9a3a971da6a749f22ca6b2cb2d2f739367294e52 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 15 May 2025 18:55:20 -0400 Subject: [PATCH 02/45] Clear Stage --- hasura/metadata/tables.yaml | 64 +++++ package-lock.json | 476 ++++++++++++++++++------------------ package.json | 20 +- 3 files changed, 312 insertions(+), 248 deletions(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index cd68acf61..1b4faac05 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -957,6 +957,7 @@ - enforce_conversion_category - enforce_conversion_csr - enforce_referral + - enforce_sms_consent - entegral_configuration - entegral_id - features @@ -1067,6 +1068,7 @@ - enforce_conversion_category - enforce_conversion_csr - enforce_referral + - enforce_sms_consent - federal_tax_id - id - inhousevendorid @@ -5861,6 +5863,68 @@ template_engine: Kriti url: '{{$base_url}}/opensearch' version: 2 +- table: + name: phone_number_consent + schema: public + object_relationships: + - name: bodyshop + using: + foreign_key_constraint_on: bodyshopid + 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: phonebook schema: public diff --git a/package-lock.json b/package-lock.json index 06038085f..9f5a49234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "0.2.0", "license": "UNLICENSED", "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.808.0", - "@aws-sdk/client-elasticache": "^3.808.0", - "@aws-sdk/client-s3": "^3.808.0", - "@aws-sdk/client-secrets-manager": "^3.808.0", - "@aws-sdk/client-ses": "^3.808.0", - "@aws-sdk/credential-provider-node": "^3.808.0", - "@aws-sdk/lib-storage": "^3.808.0", - "@aws-sdk/s3-request-presigner": "^3.808.0", + "@aws-sdk/client-cloudwatch-logs": "^3.810.0", + "@aws-sdk/client-elasticache": "^3.810.0", + "@aws-sdk/client-s3": "^3.810.0", + "@aws-sdk/client-secrets-manager": "^3.810.0", + "@aws-sdk/client-ses": "^3.810.0", + "@aws-sdk/credential-provider-node": "^3.810.0", + "@aws-sdk/lib-storage": "^3.810.0", + "@aws-sdk/s3-request-presigner": "^3.810.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -29,9 +29,9 @@ "cloudinary": "^2.6.1", "compression": "^1.8.0", "cookie-parser": "^1.4.7", - "cors": "2.8.5", + "cors": "^2.8.5", "crisp-status-reporter": "^1.2.2", - "dd-trace": "^5.51.0", + "dd-trace": "^5.52.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -284,26 +284,26 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.808.0.tgz", - "integrity": "sha512-bD57b5re12DS+GECrBR2vmwcREBDgoEj33gAV1zhTdQ38ZgyA2yhaFxfaltB+qYRfPqu95pcc+ZQTnI0Nyu1ag==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.810.0.tgz", + "integrity": "sha512-1FLkuei0pnBIGJJktYhY03LNcdGiYyxn8TcugUl4A3+7fOs9HCmcA+a5vTUThvnDqFKn4xtptQgYo4oiSLxtwA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.808.0", - "@aws-sdk/credential-provider-node": "3.808.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/credential-provider-node": "3.810.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.808.0", + "@aws-sdk/middleware-user-agent": "3.810.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.808.0", + "@aws-sdk/util-user-agent-node": "3.810.0", "@smithy/config-resolver": "^4.1.2", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/eventstream-serde-browser": "^4.0.2", "@smithy/eventstream-serde-config-resolver": "^4.1.0", "@smithy/eventstream-serde-node": "^4.0.2", @@ -311,21 +311,21 @@ "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/middleware-retry": "^4.1.5", - "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.12", - "@smithy/util-defaults-mode-node": "^4.0.12", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", @@ -352,45 +352,45 @@ } }, "node_modules/@aws-sdk/client-elasticache": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.808.0.tgz", - "integrity": "sha512-sUfbx2H8wEYLLHZLCdPG0h5CtcqQ7y2pB4BLnzjIQMrlIuxzqCumpww7XVL9yCIPAooPoeKVjUCr9tJkSa6MvQ==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.810.0.tgz", + "integrity": "sha512-tB7bBIoAz+qoFq8b1oR6TdjIru3Ui9VL+TWzM6jxxgS2LtyH3MS6hu0qsJWIVeDZO9MEOdb66u+aQoyxJecasA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.808.0", - "@aws-sdk/credential-provider-node": "3.808.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/credential-provider-node": "3.810.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.808.0", + "@aws-sdk/middleware-user-agent": "3.810.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.808.0", + "@aws-sdk/util-user-agent-node": "3.810.0", "@smithy/config-resolver": "^4.1.2", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/middleware-retry": "^4.1.5", - "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.12", - "@smithy/util-defaults-mode-node": "^4.0.12", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", @@ -403,35 +403,35 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.808.0.tgz", - "integrity": "sha512-8RY3Jsm84twmYfiqnMkxznuY6pBX7y2GiuEJVdW1ZJLXRDOiCPkTBHsO6jUwppfMua7HRhO2OTAdWr7aSBAdPw==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.810.0.tgz", + "integrity": "sha512-wM8M0BrqRkbZ9fGaLmAl24CUgVmmLjiKuNTqGOHsdCIc7RV+IGv5CnGK7ciOltAttrFBxvDlhy2lg5F8gNw8Bg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.808.0", - "@aws-sdk/credential-provider-node": "3.808.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/credential-provider-node": "3.810.0", "@aws-sdk/middleware-bucket-endpoint": "3.808.0", "@aws-sdk/middleware-expect-continue": "3.804.0", - "@aws-sdk/middleware-flexible-checksums": "3.808.0", + "@aws-sdk/middleware-flexible-checksums": "3.810.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-location-constraint": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-sdk-s3": "3.808.0", + "@aws-sdk/middleware-sdk-s3": "3.810.0", "@aws-sdk/middleware-ssec": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.808.0", + "@aws-sdk/middleware-user-agent": "3.810.0", "@aws-sdk/region-config-resolver": "3.808.0", - "@aws-sdk/signature-v4-multi-region": "3.808.0", + "@aws-sdk/signature-v4-multi-region": "3.810.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.808.0", + "@aws-sdk/util-user-agent-node": "3.810.0", "@aws-sdk/xml-builder": "3.804.0", "@smithy/config-resolver": "^4.1.2", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/eventstream-serde-browser": "^4.0.2", "@smithy/eventstream-serde-config-resolver": "^4.1.0", "@smithy/eventstream-serde-node": "^4.0.2", @@ -442,21 +442,21 @@ "@smithy/invalid-dependency": "^4.0.2", "@smithy/md5-js": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/middleware-retry": "^4.1.5", - "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.12", - "@smithy/util-defaults-mode-node": "^4.0.12", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", @@ -470,45 +470,45 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.808.0.tgz", - "integrity": "sha512-uEAnM0bXA1KtsI17Fg/8TG4ereiLY0lPqFlYM58MGDNj3mJlBTCokN4VgLBDvxOyx1rEuWH/1LrgsL9d78Kgsw==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.810.0.tgz", + "integrity": "sha512-FKqvgtBOQ69SXFfuMpgx81GmaJMAmCyfb6JjSdSay0Gj585dzb6Jn7ye8zkuy4k6XAt8flnbeaFNORbY9BT38g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.808.0", - "@aws-sdk/credential-provider-node": "3.808.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/credential-provider-node": "3.810.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.808.0", + "@aws-sdk/middleware-user-agent": "3.810.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.808.0", + "@aws-sdk/util-user-agent-node": "3.810.0", "@smithy/config-resolver": "^4.1.2", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/middleware-retry": "^4.1.5", - "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.12", - "@smithy/util-defaults-mode-node": "^4.0.12", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", @@ -535,45 +535,45 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.808.0.tgz", - "integrity": "sha512-RwVaoHBCbH3TGCqDXVRHPcAA2U9y2DzTx2RahhcwmthH3U22cdADRmOZBwJzZ3Be4pEultsj3jT1wkJ1fuSw4g==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.810.0.tgz", + "integrity": "sha512-QUaW4bJIpqNyIPWvRun8avLDuIfbWOAyspgnG5JSkQtIK3wWeeeR8lXrCNFtgzY0X8MPdy0Ns50YbVEsVWnmMA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.808.0", - "@aws-sdk/credential-provider-node": "3.808.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/credential-provider-node": "3.810.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.808.0", + "@aws-sdk/middleware-user-agent": "3.810.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.808.0", + "@aws-sdk/util-user-agent-node": "3.810.0", "@smithy/config-resolver": "^4.1.2", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/middleware-retry": "^4.1.5", - "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.12", - "@smithy/util-defaults-mode-node": "^4.0.12", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", @@ -586,44 +586,44 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.808.0.tgz", - "integrity": "sha512-NxGomD0x9q30LPOXf4x7haOm6l2BJdLEzpiC/bPEXUkf2+4XudMQumMA/hDfErY5hCE19mFAouoO465m3Gl3JQ==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.810.0.tgz", + "integrity": "sha512-Txp/3jHqkfA4BTklQEOGiZ1yTUxg+hITislfaWEzJ904vlDt4DvAljTlhfaz7pceCLA2+LhRlYZYSv7t5b0Ltw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.808.0", + "@aws-sdk/core": "3.810.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.808.0", + "@aws-sdk/middleware-user-agent": "3.810.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.808.0", + "@aws-sdk/util-user-agent-node": "3.810.0", "@smithy/config-resolver": "^4.1.2", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/middleware-retry": "^4.1.5", - "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.12", - "@smithy/util-defaults-mode-node": "^4.0.12", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", @@ -635,18 +635,18 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.808.0.tgz", - "integrity": "sha512-+nTmxJVIPtAarGq9Fd/uU2qU/Ngfb9EntT0/kwXdKKMI0wU9fQNWi10xSTVeqOtzWERbQpOJgBAdta+v3W7cng==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.810.0.tgz", + "integrity": "sha512-s2IJk+qa/15YZcv3pbdQNATDR+YdYnHf94MrAeVAWubtRLnzD8JciC+gh4LSPp7JzrWSvVOg2Ut1S+0y89xqCg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.804.0", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/node-config-provider": "^4.1.1", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", @@ -657,12 +657,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.808.0.tgz", - "integrity": "sha512-snPRQnwG9PV4kYHQimo1tenf7P974RcdxkHUThzWSxPEV7HpjxTFYNWGlKbOKBhL4AcgeCVeiZ/j+zveF2lEPA==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.810.0.tgz", + "integrity": "sha512-iwHqF+KryKONfbdFk3iKhhPk4fHxh5QP5fXXR//jhYwmszaLOwc7CLCE9AxhgiMzAs+kV8nBFQZvdjFpPzVGOA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.808.0", + "@aws-sdk/core": "3.810.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -673,18 +673,18 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.808.0.tgz", - "integrity": "sha512-gNXjlx3BIUeX7QpVqxbjBxG6zm45lC39QvUIo92WzEJd2OTPcR8TU0OTTsgq/lpn2FrKcISj5qXvhWykd41+CA==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.810.0.tgz", + "integrity": "sha512-SKzjLd+8ugif7yy9sOAAdnPE1vCBHQe6jKgs2AadMpCmWm34DiHz/KuulHdvURUGMIi7CvmaC8aH77twDPYbtg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.808.0", + "@aws-sdk/core": "3.810.0", "@aws-sdk/types": "3.804.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" @@ -694,20 +694,20 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.808.0.tgz", - "integrity": "sha512-Y53CW0pCvFQQEvtVFwExCCMbTg+6NOl8b3YOuZVzPmVmDoW7M1JIn9IScesqoGERXL3VoXny6nYTsZj+vfpp7Q==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.810.0.tgz", + "integrity": "sha512-H2QCSnxWJ/mj8HTcyHmCmyQ5bO/+imRi4mlBIpUyKjiYKro52WD3gXlGgPIDo2q3UFIHq37kmYvS00i+qIY9tw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.808.0", - "@aws-sdk/credential-provider-env": "3.808.0", - "@aws-sdk/credential-provider-http": "3.808.0", - "@aws-sdk/credential-provider-process": "3.808.0", - "@aws-sdk/credential-provider-sso": "3.808.0", - "@aws-sdk/credential-provider-web-identity": "3.808.0", - "@aws-sdk/nested-clients": "3.808.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/credential-provider-env": "3.810.0", + "@aws-sdk/credential-provider-http": "3.810.0", + "@aws-sdk/credential-provider-process": "3.810.0", + "@aws-sdk/credential-provider-sso": "3.810.0", + "@aws-sdk/credential-provider-web-identity": "3.810.0", + "@aws-sdk/nested-clients": "3.810.0", "@aws-sdk/types": "3.804.0", - "@smithy/credential-provider-imds": "^4.0.2", + "@smithy/credential-provider-imds": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", @@ -718,19 +718,19 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.808.0.tgz", - "integrity": "sha512-lASHlXJ6U5Cpnt9Gs+mWaaSmWcEibr1AFGhp+5UNvfyd+UU2Oiwgbo7rYXygmaVDGkbfXEiTkgYtoNOBSddnWQ==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.810.0.tgz", + "integrity": "sha512-9E3Chv3x+RBM3N1bwLCyvXxoiPAckCI74wG7ePN4F3b/7ieIkbEl/3Hd67j1fnt62Xa1cjUHRu2tz5pdEv5G1Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.808.0", - "@aws-sdk/credential-provider-http": "3.808.0", - "@aws-sdk/credential-provider-ini": "3.808.0", - "@aws-sdk/credential-provider-process": "3.808.0", - "@aws-sdk/credential-provider-sso": "3.808.0", - "@aws-sdk/credential-provider-web-identity": "3.808.0", + "@aws-sdk/credential-provider-env": "3.810.0", + "@aws-sdk/credential-provider-http": "3.810.0", + "@aws-sdk/credential-provider-ini": "3.810.0", + "@aws-sdk/credential-provider-process": "3.810.0", + "@aws-sdk/credential-provider-sso": "3.810.0", + "@aws-sdk/credential-provider-web-identity": "3.810.0", "@aws-sdk/types": "3.804.0", - "@smithy/credential-provider-imds": "^4.0.2", + "@smithy/credential-provider-imds": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", @@ -741,12 +741,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.808.0.tgz", - "integrity": "sha512-ZLqp+xsQUatoo8pMozcfLwf/pwfXeIk0w3n0Lo/rWBgT3RcdECmmPCRcnkYBqxHQyE66aS9HiJezZUwMYPqh6w==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.810.0.tgz", + "integrity": "sha512-42kE6MLdsmMGp1id3Gisal4MbMiF7PIc0tAznTeIuE8r7cIF8yeQWw/PBOIvjyI57DxbyKzLUAMEJuigUpApCw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.808.0", + "@aws-sdk/core": "3.810.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -758,14 +758,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.808.0.tgz", - "integrity": "sha512-gWZByAokHX+aps1+syIW/hbKUBrjE2RpPRd/RGQvrBbVVgwsJzsHKsW0zy1B6mgARPG6IahmSUMjNkBCVsiAgw==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.810.0.tgz", + "integrity": "sha512-8WjX6tz+FCvM93Y33gsr13p/HiiTJmVn5AK1O8PTkvHBclQDzmtAW5FdPqTpAJGswLW2FB0xRqdsSMN2dQEjNw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.808.0", - "@aws-sdk/core": "3.808.0", - "@aws-sdk/token-providers": "3.808.0", + "@aws-sdk/client-sso": "3.810.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/token-providers": "3.810.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -777,13 +777,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.808.0.tgz", - "integrity": "sha512-SsGa1Gfa05aJM/qYOtHmfg0OKKW6Fl6kyMCcai63jWDVDYy0QSHcesnqRayJolISkdsVK6bqoWoFcPxiopcFcg==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.810.0.tgz", + "integrity": "sha512-uKQJY0AcPyrvMmfGLo36semgjqJ4vmLTqOSW9u40qQDspRnG73/P09lAO2ntqKlhwvMBt3XfcNnOpyyhKRcOfA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.808.0", - "@aws-sdk/nested-clients": "3.808.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/nested-clients": "3.810.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -794,14 +794,14 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.808.0.tgz", - "integrity": "sha512-gEdiBuqPjmMA0Z2RtppIdcMQH1KDeRM//+DWcVC9gU9pMJ5S/LEwCmWEVh3/C2n/Sehv71b7x0GKGxHO/yBrwA==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.810.0.tgz", + "integrity": "sha512-Ri3Kgkk9XK7wmgKaQUA4ZNgr4lTASFlt2pd4xo7sbpX7K984fYkWe7ecrgbRptIiIYUjeJiYCRb0FS34diSaZw==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/smithy-client": "^4.2.4", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/smithy-client": "^4.2.6", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", @@ -811,7 +811,7 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.808.0" + "@aws-sdk/client-s3": "^3.810.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -848,15 +848,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.808.0.tgz", - "integrity": "sha512-NW1yoTYDH2h8ycqMPNkvW3d1XT2vEeXfXclagL2tv82P7Qt7vPXYcObs/YtETvNZ7hdnmOftJ/IJv7YrFC8vtQ==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.810.0.tgz", + "integrity": "sha512-lF5fse+26hluElOtDZMsi5EH50G13OEqglFgpSc6xWnqNhbDc+CnPQRMwTVlOJBDR1/YVbJ15LOKf4pkat46Eg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.808.0", + "@aws-sdk/core": "3.810.0", "@aws-sdk/types": "3.804.0", "@smithy/is-array-buffer": "^4.0.0", "@smithy/node-config-provider": "^4.1.1", @@ -930,19 +930,19 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.808.0.tgz", - "integrity": "sha512-qvyJTDf0HIsPpZzBUqhNQm5g8stAn2EOwVsaAolsOHuBsdaBAE/s/NgPzazDlSXwdF0ITvsIouUVDCn4fJGJqQ==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.810.0.tgz", + "integrity": "sha512-CmQHPVopJYDiGQOmqn5AcmhksQ9qNETF0VgU251Q4tsP9s3R9nBR1r2bZwLt5+dCtf9UCa7cBw4jXKHtH38JTg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.808.0", + "@aws-sdk/core": "3.810.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-arn-parser": "3.804.0", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/node-config-provider": "^4.1.1", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.2", @@ -969,15 +969,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.808.0.tgz", - "integrity": "sha512-VckV6l5cf/rL3EtgzSHVTTD4mI0gd8UxDDWbKJsxbQ2bpNPDQG2L1wWGLaolTSzjEJ5f3ijDwQrNDbY9l85Mmg==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.810.0.tgz", + "integrity": "sha512-gLMJcqgIq7k9skX8u0Yyi+jil4elbsmLf3TuDuqNdlqiZ44/AKdDFfU3mU5tRUtMfP42a3gvb2U3elP0BIeybQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.808.0", + "@aws-sdk/core": "3.810.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" @@ -987,44 +987,44 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.808.0.tgz", - "integrity": "sha512-NparPojwoBul7XPCasy4psFMJbw7Ys4bz8lVB93ljEUD4VV7mM7zwK27Uhz20B8mBFGmFEoAprPsVymJcK9Vcw==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.810.0.tgz", + "integrity": "sha512-w+tGXFSQjzvJ3j2sQ4GJRdD+YXLTgwLd9eG/A+7pjrv2yLLV70M4HqRrFqH06JBjqT5rsOxonc/QSjROyxk+IA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.808.0", + "@aws-sdk/core": "3.810.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.808.0", + "@aws-sdk/middleware-user-agent": "3.810.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.808.0", + "@aws-sdk/util-user-agent-node": "3.810.0", "@smithy/config-resolver": "^4.1.2", - "@smithy/core": "^3.3.1", + "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/middleware-retry": "^4.1.5", - "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.12", - "@smithy/util-defaults-mode-node": "^4.0.12", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", @@ -1053,17 +1053,17 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.808.0.tgz", - "integrity": "sha512-viKWGrb7ZTN2rDkjX5rSkPeyKEVFDiyJS8HWVLBUJGwOLb65wluX+Rr/9fuCFyhCbBzhzpXFdzczZ0kCHOM6Qw==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.810.0.tgz", + "integrity": "sha512-/6TISQT3HUOoY3Gli1EvQVISNGp8XawGKD4igJqwyF3l35VvajzaAfhwk+Tm5TKTCkFh8EFnqWeT+mId5AaUbA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "3.808.0", + "@aws-sdk/signature-v4-multi-region": "3.810.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-format-url": "3.804.0", - "@smithy/middleware-endpoint": "^4.1.4", + "@smithy/middleware-endpoint": "^4.1.6", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, @@ -1072,12 +1072,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.808.0.tgz", - "integrity": "sha512-lQuEB6JK81eKV7fdiktmRq06Y1KCcJbx9fLf7b19nSfYUbJSn/kfSpHPv/tOkJK2HKnN61JsfG19YU8k4SOU8Q==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.810.0.tgz", + "integrity": "sha512-6F6evHRrA0OG/H67YuZBcI7EH4A0O5dIhczo2N0AK9z495uSIv+0xUrSrDhFTZxZjo6gkADIXroRjP1kwqK6ew==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.808.0", + "@aws-sdk/middleware-sdk-s3": "3.810.0", "@aws-sdk/types": "3.804.0", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", @@ -1089,12 +1089,12 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.808.0.tgz", - "integrity": "sha512-PsfKanHmnyO7FxowXqxbLQ+QjURCdSGxyhUiSdZbfvlvme/wqaMyIoMV/i4jppndksoSdPbW2kZXjzOqhQF+ew==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.810.0.tgz", + "integrity": "sha512-fdgHRCDpnzsD+0km7zuRbHRysJECfS8o9T9/pZ6XAr1z2FNV/UveHtnUYq0j6XpDMrIm0/suvXbshIjQU+a+sw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/nested-clients": "3.808.0", + "@aws-sdk/nested-clients": "3.810.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -1185,12 +1185,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.808.0.tgz", - "integrity": "sha512-5UmB6u7RBSinXZAVP2iDgqyeVA/odO2SLEcrXaeTCw8ICXEoqF0K+GL36T4iDbzCBOAIugOZ6OcQX5vH3ck5UA==", + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.810.0.tgz", + "integrity": "sha512-T56/ANEGNuvhqVoWZdr+0ZY2hjV93cH2OfGHIlVTVSAMACWG54XehDPESEso1CJNhJGYZPsE+FE42HGCk/XDMg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.808.0", + "@aws-sdk/middleware-user-agent": "3.810.0", "@aws-sdk/types": "3.804.0", "@smithy/node-config-provider": "^4.1.1", "@smithy/types": "^4.2.0", @@ -1261,9 +1261,9 @@ } }, "node_modules/@datadog/native-iast-taint-tracking": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.3.1.tgz", - "integrity": "sha512-TgXpoX/CDgPfYAKu9qLmEyb9UXvRVC00D71islcSb70MCFmxQwkgXGl/gAk6YA6/NmZ4j8+cgY1lSNqStGvOMg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-4.0.0.tgz", + "integrity": "sha512-2uF8RnQkJO5bmLi26Zkhxg+RFJn/uEsesYTflScI/Cz/BWv+792bxI+OaCKvhgmpLkm8EElenlpidcJyZm7GYw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1285,9 +1285,9 @@ } }, "node_modules/@datadog/pprof": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@datadog/pprof/-/pprof-5.7.1.tgz", - "integrity": "sha512-D5XTxsaPG36x41vZZn8hsAeC7QQDx0rv1a1Uhxo5xCXUB/9rc19+I7iCnjgJS5aH0ShXdPVOWRClo16hOSKKSw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@datadog/pprof/-/pprof-5.8.0.tgz", + "integrity": "sha512-3FL2qpkFWvIEptSk7x9RVs1PJeF+VCrGxQpKViloQkrnH5mjcwaIQiWyNZYyV1H1vhKJIS+ummSBcsOLkV49qA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3138,12 +3138,12 @@ } }, "node_modules/@smithy/core": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.3.2.tgz", - "integrity": "sha512-GlLv+syoWolhtjX12XplL9BXBu10cjjD8iQC69fiKTrVNOB3Fjt8CVI9ccm6G3bLbMNe1gzrLD7yyMkYo4hchw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.3.3.tgz", + "integrity": "sha512-CiJNc0b/WdnttAfQ6uMkxPQ3Z8hG/ba8wF89x9KtBBLDdZk6CX52K4F8hbe94uNbc8LDUuZFtbqfdhM3T21naw==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.0.4", + "@smithy/middleware-serde": "^4.0.5", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "@smithy/util-body-length-browser": "^4.0.0", @@ -3356,13 +3356,13 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.5.tgz", - "integrity": "sha512-WlpC9KVkajQf7RaGwi3n6lhHZzYTgm2PyX/2JjcwSHG417gFloNmYqN8rzDRXjT527/ZxZuvCsqq1gWZPW8lag==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.6.tgz", + "integrity": "sha512-Zdieg07c3ua3ap5ungdcyNnY1OsxmsXXtKDTk28+/YbwIPju0Z1ZX9X5AnkjmDE3+AbqgvhtC/ZuCMSr6VSfPw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.3.2", - "@smithy/middleware-serde": "^4.0.4", + "@smithy/core": "^3.3.3", + "@smithy/middleware-serde": "^4.0.5", "@smithy/node-config-provider": "^4.1.1", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", @@ -3375,15 +3375,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.6.tgz", - "integrity": "sha512-bl8q95nvCf7d22spxsBfs2giUDFf7prWLAxF5tmfgGBYHbUNW+OfnwMnabC15GMLA2AoE4HOtQR18a59lx6Blw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.7.tgz", + "integrity": "sha512-lFIFUJ0E/4I0UaIDY5usNUzNKAghhxO0lDH4TZktXMmE+e4ActD9F154Si0Unc01aCPzcwd+NcOwQw6AfXXRRQ==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.1.1", "@smithy/protocol-http": "^5.1.0", "@smithy/service-error-classification": "^4.0.3", - "@smithy/smithy-client": "^4.2.5", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", @@ -3408,9 +3408,9 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.4.tgz", - "integrity": "sha512-CaLvBtz+Xgs7eOwoinTXhZ02/9u8b28RT8lQAaDh7Q59nygeYYp1UiJjwl6zsay+lp0qVT/S7qLVI5RgcxjyfQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.5.tgz", + "integrity": "sha512-yREC3q/HXqQigq29xX3hiy6tFi+kjPKXoYUQmwQdgPORLbQ0n6V2Z/Iw9Nnlu66da9fM/WhDtGvYvqwecrCljQ==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.1.0", @@ -3563,13 +3563,13 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.5.tgz", - "integrity": "sha512-T3gA/TShe52Ln0ywWGVoDiqRvaxqvrU0CKRRmzT71/I1rRBD8mY85rvMMME6vw5RpBLJC9ADmXSLmpohF7RRhA==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.6.tgz", + "integrity": "sha512-WEqP0wQ1N/lVS4pwNK1Vk+0i6QIr66cq/xbu1dVy1tM0A0qYwAYyz0JhbquzM5pMa8s89lyDBtoGKxo7iG74GA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.3.2", - "@smithy/middleware-endpoint": "^4.1.5", + "@smithy/core": "^3.3.3", + "@smithy/middleware-endpoint": "^4.1.6", "@smithy/middleware-stack": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", @@ -3670,13 +3670,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.13.tgz", - "integrity": "sha512-HCLfXAyTEpVWLuyxDABg8UQukeRwChL1UErpSQ4KJK2ZoadmXuQY68pTL9KcuEtasTkIjnzyLUL9vhLdJ3VFHQ==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.14.tgz", + "integrity": "sha512-l7QnMX8VcDOH6n/fBRu4zqguSlOBZxFzWqp58dXFSARFBjNlmEDk5G/z4T7BMGr+rI0Pg8MkhmMUfEtHFgpy2g==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.5", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -3686,16 +3686,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.13.tgz", - "integrity": "sha512-lu8E2RyzKzzFbNu4ICmY/2HltMZlJxMNg3saJ+r8I9vWbWbwdX7GOWUJdP4fbjEOm6aa52mnnd+uIRrT3dNEyA==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.14.tgz", + "integrity": "sha512-Ujs1gsWDo3m/T63VWBTBmHLTD2UlU6J6FEokLCEp7OZQv45jcjLHoxTwgWsi8ULpsYozvH4MTWkRP+bhwr0vDg==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.1.2", "@smithy/credential-provider-imds": "^4.0.4", "@smithy/node-config-provider": "^4.1.1", "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.5", + "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, @@ -5791,17 +5791,17 @@ } }, "node_modules/dd-trace": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.51.0.tgz", - "integrity": "sha512-/LH1RAoTau2KileM5mx58oGPBhdjejCYMqSMbbe0Rcqkipn/9RaxNMePGcfrnEa7Rwmar8PjPfDM5m7nn2d+Aw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.52.0.tgz", + "integrity": "sha512-ZF+OWLMcgVUWJEAIYIl76LocgnbbkPJ6WgJCG1fhLk4UCsUvoHRvBx9qlexbytL0jkktk1pvzODcjL0wyxLAOQ==", "hasInstallScript": true, "license": "(Apache-2.0 OR BSD-3-Clause)", "dependencies": { "@datadog/libdatadog": "^0.5.1", "@datadog/native-appsec": "8.5.2", - "@datadog/native-iast-taint-tracking": "3.3.1", + "@datadog/native-iast-taint-tracking": "4.0.0", "@datadog/native-metrics": "^3.1.1", - "@datadog/pprof": "5.7.1", + "@datadog/pprof": "5.8.0", "@datadog/sketches-js": "^2.1.0", "@datadog/wasm-js-rewriter": "4.0.1", "@isaacs/ttlcache": "^1.4.1", diff --git a/package.json b/package.json index ce5dcaa0c..bc550232b 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,14 @@ "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js" }, "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.808.0", - "@aws-sdk/client-elasticache": "^3.808.0", - "@aws-sdk/client-s3": "^3.808.0", - "@aws-sdk/client-secrets-manager": "^3.808.0", - "@aws-sdk/client-ses": "^3.808.0", - "@aws-sdk/credential-provider-node": "^3.808.0", - "@aws-sdk/lib-storage": "^3.808.0", - "@aws-sdk/s3-request-presigner": "^3.808.0", + "@aws-sdk/client-cloudwatch-logs": "^3.810.0", + "@aws-sdk/client-elasticache": "^3.810.0", + "@aws-sdk/client-s3": "^3.810.0", + "@aws-sdk/client-secrets-manager": "^3.810.0", + "@aws-sdk/client-ses": "^3.810.0", + "@aws-sdk/credential-provider-node": "^3.810.0", + "@aws-sdk/lib-storage": "^3.810.0", + "@aws-sdk/s3-request-presigner": "^3.810.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -36,9 +36,9 @@ "cloudinary": "^2.6.1", "compression": "^1.8.0", "cookie-parser": "^1.4.7", - "cors": "2.8.5", + "cors": "^2.8.5", "crisp-status-reporter": "^1.2.2", - "dd-trace": "^5.51.0", + "dd-trace": "^5.52.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.5", "express": "^4.21.1", From b0147449402b9ccc245fcbb76163b9918822dce4 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 19 May 2025 11:10:02 -0700 Subject: [PATCH 03/45] IO-3239 Add integration log statements on QBO. --- server/accounting/qbo/qbo-callback.js | 1 - server/accounting/qbo/qbo-payables.js | 72 ++++++++++--- server/accounting/qbo/qbo-payments.js | 96 ++++++++++++----- server/accounting/qbo/qbo-receivables.js | 126 +++++++++++------------ server/graphql-client/queries.js | 8 ++ server/utils/logger.js | 33 ++++-- 6 files changed, 229 insertions(+), 107 deletions(-) diff --git a/server/accounting/qbo/qbo-callback.js b/server/accounting/qbo/qbo-callback.js index f1de7d551..3045dbcc8 100644 --- a/server/accounting/qbo/qbo-callback.js +++ b/server/accounting/qbo/qbo-callback.js @@ -14,7 +14,6 @@ const oauthClient = new OAuthClient({ clientSecret: process.env.QBO_SECRET, environment: process.env.NODE_ENV === "production" ? "production" : "sandbox", redirectUri: process.env.QBO_REDIRECT_URI, - logging: true }); //TODO:AIO Add in QBO callbacks. diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 04b0c7e08..0e234c0e8 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -20,7 +20,6 @@ exports.default = async (req, res) => { clientSecret: process.env.QBO_SECRET, environment: process.env.NODE_ENV === "production" ? "production" : "sandbox", redirectUri: process.env.QBO_REDIRECT_URI, - logging: true }); try { @@ -149,6 +148,15 @@ async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) { "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "QueryVendorRecord", + billid: bill.id, + statusCode: result.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return ( result.json && @@ -178,6 +186,15 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) { }, body: JSON.stringify(Vendor) }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "InsertVendorRecord", + billid: bill.id, + statusCode: result.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return result && result.json && result.json.Vendor; } catch (error) { @@ -246,11 +263,11 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) .format("YYYY-MM-DD"), ...(!bill.is_credit_memo && bill.vendor.due_date && { - DueDate: moment(bill.date) - //.tz(bill.job.bodyshop.timezone) - .add(bill.vendor.due_date, "days") - .format("YYYY-MM-DD") - }), + DueDate: moment(bill.date) + //.tz(bill.job.bodyshop.timezone) + .add(bill.vendor.due_date, "days") + .format("YYYY-MM-DD") + }), DocNumber: bill.invoice_number, //...(bill.job.class ? { ClassRef: { Id: classes[bill.job.class] } } : {}), ...(!( @@ -263,8 +280,8 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) : {}), ...(bodyshop.accountingconfig.qbo_departmentid && bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && { - DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid } - }), + DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid } + }), PrivateNote: `RO ${bill.job.ro_number || ""}`, Line: lines }; @@ -280,6 +297,15 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) }, body: JSON.stringify(billQbo) }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "InsertBill", + billid: bill.id, + statusCode: result.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return result && result.json && result.json.Bill; } catch (error) { @@ -327,8 +353,8 @@ const generateBillLine = ( accountingconfig.qbo && accountingconfig.qbo_usa && region_config.includes("CA_") ? {} : { - value: taxCodes[findTaxCode(billLine.applicable_taxes, ioSalesTaxCodes)] - }, + value: taxCodes[findTaxCode(billLine.applicable_taxes, ioSalesTaxCodes)] + }, AccountRef: { value: accounts[account.accountname] } @@ -354,6 +380,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "QueryAccountType", + statusCode: accounts.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) setNewRefreshToken(req.user.email, accounts); const taxCodes = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From TaxCode`), @@ -362,7 +396,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { "Content-Type": "application/json" } }); - + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "QueryTaxCode", + statusCode: taxCodes.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) const classes = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From Class`), method: "POST", @@ -370,7 +411,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { "Content-Type": "application/json" } }); - + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "QueryClasses", + statusCode: classes.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) const taxCodeMapping = {}; taxCodes.json && diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js index 19ee23bc0..1b54ac8c0 100644 --- a/server/accounting/qbo/qbo-payments.js +++ b/server/accounting/qbo/qbo-payments.js @@ -29,7 +29,6 @@ exports.default = async (req, res) => { clientSecret: process.env.QBO_SECRET, environment: process.env.NODE_ENV === "production" ? "production" : "sandbox", redirectUri: process.env.QBO_REDIRECT_URI, - logging: true }); try { //Fetch the API Access Tokens & Set them for the session. @@ -227,20 +226,20 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef, PaymentRefNum: payment.transactionid, ...(invoices && invoices.length === 1 && invoices[0] ? { - Line: [ - { - Amount: Dinero({ - amount: Math.round(payment.amount * 100) - }).toFormat(DineroQbFormat), - LinkedTxn: [ - { - TxnId: invoices[0].Id, - TxnType: "Invoice" - } - ] - } - ] - } + Line: [ + { + Amount: Dinero({ + amount: Math.round(payment.amount * 100) + }).toFormat(DineroQbFormat), + LinkedTxn: [ + { + TxnId: invoices[0].Id, + TxnType: "Invoice" + } + ] + } + ] + } : {}) }; logger.log("qbo-payments-objectlog", "DEBUG", req.user.email, payment.id, { @@ -255,6 +254,15 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef, }, body: JSON.stringify(paymentQbo) }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "InsertPayment", + paymentid: payment.id, + statusCode: result.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return result && result.Bill; } catch (error) { @@ -274,7 +282,15 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "QueryInvoice", + statusCode: invoice.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) const paymentMethods = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From PaymentMethod`), method: "POST", @@ -282,6 +298,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "QueryPaymentMethod", + statusCode: paymentMethods.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) setNewRefreshToken(req.user.email, paymentMethods); // const classes = await oauthClient.makeApiCall({ @@ -325,6 +349,15 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "QueryTaxCode", + + statusCode: taxCodes.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) const items = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From Item`), method: "POST", @@ -332,6 +365,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "QueryItems", + statusCode: items.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) setNewRefreshToken(req.user.email, items); const itemMapping = {}; @@ -406,14 +447,14 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe TaxCodeRef: { value: taxCodes[ - findTaxCode( - { - local: false, - federal: false, - state: false - }, - payment.job.bodyshop.md_responsibility_centers.sales_tax_codes - ) + findTaxCode( + { + local: false, + federal: false, + state: false + }, + payment.job.bodyshop.md_responsibility_centers.sales_tax_codes + ) ] } } @@ -432,6 +473,15 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe }, body: JSON.stringify(paymentQbo) }); + logger.LogIntegrationCall({ + platform: "QBO", + methodType: "POST", + methodName: "InsertCreditMemo", + paymentid: payment.id, + statusCode: result.status, + bodyshopid: req.user.bodyshopid, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return result && result.Bill; } catch (error) { diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index 36f3e5113..881a6fae5 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -22,8 +22,8 @@ exports.default = async (req, res) => { clientSecret: process.env.QBO_SECRET, environment: process.env.NODE_ENV === "production" ? "production" : "sandbox", redirectUri: process.env.QBO_REDIRECT_URI, - logging: true }); + try { //Fetch the API Access Tokens & Set them for the session. const response = await apiGqlClient.request(queries.GET_QBO_AUTH, { @@ -331,11 +331,11 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare ...(job.ownr_ea ? { PrimaryEmailAddr: { Address: job.ownr_ea.trim() } } : {}), ...(isThreeTier ? { - Job: true, - ParentRef: { - value: parentTierRef.Id - } + Job: true, + ParentRef: { + value: parentTierRef.Id } + } : {}) }; try { @@ -499,57 +499,55 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren DocNumber: job.ro_number, ...(job.class ? { ClassRef: { value: classes[job.class] } } : {}), CustomerMemo: { - value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${ - job.po_number ? `PO No: ${job.po_number}` : `` - } Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${ - job.v_model_desc || "" - } ${job.v_vin || ""} ${job.plate_no || ""} `.trim() + value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${job.po_number ? `PO No: ${job.po_number}` : `` + } Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || "" + } ${job.v_vin || ""} ${job.plate_no || ""} `.trim() }, CustomerRef: { value: parentTierRef.Id }, ...(bodyshop.accountingconfig.qbo_departmentid && bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && { - DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid } - }), + DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid } + }), CustomField: [ ...(bodyshop.accountingconfig.ReceivableCustomField1 ? [ - { - DefinitionId: "1", - StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1], - Type: "StringType" - } - ] + { + DefinitionId: "1", + StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1], + Type: "StringType" + } + ] : []), ...(bodyshop.accountingconfig.ReceivableCustomField2 ? [ - { - DefinitionId: "2", - StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2], - Type: "StringType" - } - ] + { + DefinitionId: "2", + StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2], + Type: "StringType" + } + ] : []), ...(bodyshop.accountingconfig.ReceivableCustomField3 ? [ - { - DefinitionId: "3", - StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3], - Type: "StringType" - } - ] + { + DefinitionId: "3", + StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3], + Type: "StringType" + } + ] : []) ], ...(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && bodyshop.accountingconfig.qbo_usa && { - TxnTaxDetail: { - TxnTaxCodeRef: { - value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem] - } + TxnTaxDetail: { + TxnTaxCodeRef: { + value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem] } - }), + } + }), ...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}), ...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}), @@ -616,58 +614,56 @@ async function InsertInvoiceMultiPayerInvoice( DocNumber: job.ro_number + suffix, ...(job.class ? { ClassRef: { value: classes[job.class] } } : {}), CustomerMemo: { - value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${ - job.po_number ? `PO No: ${job.po_number}` : `` - } Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${ - job.v_model_desc || "" - } ${job.v_vin || ""} ${job.plate_no || ""} `.trim() + value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${job.po_number ? `PO No: ${job.po_number}` : `` + } Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || "" + } ${job.v_vin || ""} ${job.plate_no || ""} `.trim() }, CustomerRef: { value: parentTierRef.Id }, ...(bodyshop.accountingconfig.qbo_departmentid && bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && { - DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid } - }), + DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid } + }), CustomField: [ ...(bodyshop.accountingconfig.ReceivableCustomField1 ? [ - { - DefinitionId: "1", - StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1], - Type: "StringType" - } - ] + { + DefinitionId: "1", + StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1], + Type: "StringType" + } + ] : []), ...(bodyshop.accountingconfig.ReceivableCustomField2 ? [ - { - DefinitionId: "2", - StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2], - Type: "StringType" - } - ] + { + DefinitionId: "2", + StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2], + Type: "StringType" + } + ] : []), ...(bodyshop.accountingconfig.ReceivableCustomField3 ? [ - { - DefinitionId: "3", - StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3], - Type: "StringType" - } - ] + { + DefinitionId: "3", + StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3], + Type: "StringType" + } + ] : []) ], ...(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && bodyshop.accountingconfig.qbo_usa && bodyshop.region_config.includes("CA_") && { - TxnTaxDetail: { - TxnTaxCodeRef: { - value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem] - } + TxnTaxDetail: { + TxnTaxCodeRef: { + value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem] } - }), + } + }), ...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}), ...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}), diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index d61d814b9..aec90bac5 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2968,3 +2968,11 @@ exports.GET_JOB_WATCHERS_MINIMAL = ` } } `; + +exports.INSERT_INTEGRATION_LOG = ` + mutation INSERT_INTEGRATION_LOG($log: integration_log_insert_input!) { + insert_integration_log_one(object: $log) { + id + } + } +`; \ No newline at end of file diff --git a/server/utils/logger.js b/server/utils/logger.js index 9a27105fd..560a77b8b 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -12,6 +12,9 @@ const { uploadFileToS3 } = require("./s3"); const { v4 } = require("uuid"); const { InstanceRegion } = require("./instanceMgr"); const getHostNameOrIP = require("./getHostNameOrIP"); +const client = require("../graphql-client/graphql-client").client; +const queries = require("../graphql-client/queries"); + const LOG_LEVELS = { error: { level: 0, name: "error" }, @@ -99,13 +102,11 @@ const createLogger = () => { const labelColor = "\x1b[33m"; // Yellow const separatorColor = "\x1b[35m|\x1b[0m"; // Magenta for separators - return `${timestampColor} [${hostnameColor}] [${level}]: ${message} ${ - user ? `${separatorColor} ${labelColor}user:\x1b[0m ${JSON.stringify(user)}` : "" - } ${record ? `${separatorColor} ${labelColor}record:\x1b[0m ${JSON.stringify(record)}` : ""}${ - meta + return `${timestampColor} [${hostnameColor}] [${level}]: ${message} ${user ? `${separatorColor} ${labelColor}user:\x1b[0m ${JSON.stringify(user)}` : "" + } ${record ? `${separatorColor} ${labelColor}record:\x1b[0m ${JSON.stringify(record)}` : ""}${meta ? `\n${separatorColor} ${labelColor}meta:\x1b[0m ${JSON.stringify(meta, null, 2)} ${separatorColor}` : "" - }`; + }`; }) ) }) @@ -194,9 +195,29 @@ const createLogger = () => { winstonLogger.log(logEntry); }; + const LogIntegrationCall = async ({ platform, methodType, methodName, jobid, paymentid, billid, statusCode, bodyshopid, email }) => { + try { + //Insert the record. + await client.request(queries.INSERT_INTEGRATION_LOG, { + platform, + methodType, + methodName, jobid, paymentid, billid, + statusCode, + bodyshopid, + email + }); + + } catch (error) { + log("integration-log-error", "ERROR", email, null, { + error + }); + } + }; + return { log, - logger: winstonLogger + logger: winstonLogger, + LogIntegrationCall }; } catch (e) { console.error("Error setting up enhanced Logger, defaulting to console.: " + e?.message || ""); From f770b2f1b1fde045c3f35d721f4698e5bde7f15f Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 19 May 2025 12:28:50 -0700 Subject: [PATCH 04/45] IO-3105 Add QBO Minor Version. --- server/accounting/qbo/qbo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/accounting/qbo/qbo.js b/server/accounting/qbo/qbo.js index 5c7e9d874..9abce0805 100644 --- a/server/accounting/qbo/qbo.js +++ b/server/accounting/qbo/qbo.js @@ -6,7 +6,7 @@ require("dotenv").config({ function urlBuilder(realmId, object, query = null) { return `https://${ process.env.NODE_ENV === "production" ? "" : "sandbox-" - }quickbooks.api.intuit.com/v3/company/${realmId}/${object}${query ? `?query=${encodeURIComponent(query)}` : ""}`; + }quickbooks.api.intuit.com/v3/company/${realmId}/${object}?minorversion=75${query ? `&query=${encodeURIComponent(query)}` : ""}`; } function StandardizeName(str) { From 6ad9e27d1d1e72bf87b9f6c7dc6ff2e986833b80 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 20 May 2025 12:38:31 -0400 Subject: [PATCH 05/45] feature/IO-3182-Phone-Number-Consent - Merge master / bump deps --- client/package-lock.json | 357 +++++------ client/package.json | 24 +- package-lock.json | 1255 +++++++++++++------------------------- package.json | 24 +- 4 files changed, 586 insertions(+), 1074 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 496885fb9..2939dbd7c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,19 +13,19 @@ "@apollo/client": "^3.13.6", "@emotion/is-prop-valid": "^1.3.1", "@fingerprintjs/fingerprintjs": "^4.6.1", - "@firebase/analytics": "^0.10.13", - "@firebase/app": "^0.12.1", - "@firebase/auth": "^1.10.4", - "@firebase/firestore": "^4.7.14", - "@firebase/messaging": "^0.12.18", + "@firebase/analytics": "^0.10.16", + "@firebase/app": "^0.13.0", + "@firebase/auth": "^1.10.5", + "@firebase/firestore": "^4.7.15", + "@firebase/messaging": "^0.12.21", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.19.0", + "@sentry/react": "^9.21.0", "@sentry/vite-plugin": "^3.4.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", - "antd": "^5.25.1", + "antd": "^5.25.2", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^4.3.0", "autosize": "^6.0.1", @@ -49,7 +49,7 @@ "normalize-url": "^8.0.1", "object-hash": "^3.0.0", "prop-types": "^15.8.1", - "query-string": "^9.1.2", + "query-string": "^9.2.0", "raf-schd": "^4.0.3", "react": "^18.3.1", "react-big-calendar": "^1.18.0", @@ -78,7 +78,7 @@ "redux-saga": "^1.3.0", "redux-state-sync": "^3.1.4", "reselect": "^5.1.1", - "sass": "^1.88.0", + "sass": "^1.89.0", "socket.io-client": "^4.8.1", "styled-components": "^6.1.18", "subscriptions-transport-ws": "^0.11.0", @@ -90,10 +90,10 @@ "@ant-design/icons": "^6.0.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.27.1", - "@dotenvx/dotenvx": "^1.44.0", + "@dotenvx/dotenvx": "^1.44.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", - "@eslint/js": "^9.26.0", + "@eslint/js": "^9.27.0", "@playwright/test": "^1.51.1", "@sentry/webpack-plugin": "^3.4.0", "@testing-library/dom": "^10.4.0", @@ -120,7 +120,7 @@ "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-pwa": "^1.0.0", "vite-plugin-style-import": "^2.0.0", - "vitest": "^3.1.3", + "vitest": "^3.1.4", "workbox-window": "^7.3.0" }, "engines": { @@ -2587,9 +2587,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.44.0.tgz", - "integrity": "sha512-18Aa+7KP/L2Kj9lxmT4EJZnsCq/xGIHgzU26rdzsKMhjpeT3YY+qin/dNAnIaVHPZnee7kXpZL55M9htd30r7Q==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.44.1.tgz", + "integrity": "sha512-j1QImCqf/XJmhIjC1OPpgiZV9g370HG9MNT9s/CDwCKsoYzNCPEKK+GfsidahJx7yIlBbm+4dPLlGec+bKn7oA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2912,13 +2912,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@fingerprintjs/fingerprintjs": { @@ -2931,15 +2934,15 @@ } }, "node_modules/@firebase/analytics": { - "version": "0.10.13", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.13.tgz", - "integrity": "sha512-X+6wMOPgA9l0AeeMdMcMfaCP4XKPvrhx55MGuMrfHvUrOvFKldpzBum7KkoGJMoexKmqmKP+mCmJMY9Fb8K6Hw==", + "version": "0.10.16", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.16.tgz", + "integrity": "sha512-cMtp19He7Fd6uaj/nDEul+8JwvJsN8aRSJyuA1QN3QrKvfDDp+efjVurJO61sJpkVftw9O9nNMdhFbRcTmTfRQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", - "@firebase/installations": "0.6.14", + "@firebase/component": "0.6.17", + "@firebase/installations": "0.6.17", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -2947,14 +2950,14 @@ } }, "node_modules/@firebase/app": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.12.1.tgz", - "integrity": "sha512-ASExOlmmjRMdwOQ65Oj6R9JBqa7iiT1/LgZjtbU7FqxoJZNWHrt39NJ/z2bjyYDdAHX8jkY7muFqzahScCXgfA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.0.tgz", + "integrity": "sha512-Vj3MST245nq+V5UmmfEkB3isIgPouyUr8yGJlFeL9Trg/umG5ogAvrjAYvQ8gV7daKDoQSRnJKWI2JFpQqRsuQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", + "@firebase/component": "0.6.17", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -2963,14 +2966,14 @@ } }, "node_modules/@firebase/auth": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.4.tgz", - "integrity": "sha512-rZQZQkn5x7BcHenYJi9RYWoOMJHdM/CsF6DMclb/CKbntzjUaZj+R45Iyzf/BFUJ9L2sA4bNPhJK9x+l9VKvLQ==", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.5.tgz", + "integrity": "sha512-6wF/NdMTwObL4RNQePunuzMr9O3gyftisvFZFFKf57D2HONXo87YymogRV8d+Z7SLA0rcNBN1gLJVk2D0y97gA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.16", + "@firebase/component": "0.6.17", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.3", + "@firebase/util": "1.12.0", "tslib": "^2.1.0" }, "engines": { @@ -2986,39 +2989,13 @@ } } }, - "node_modules/@firebase/auth/node_modules/@firebase/component": { - "version": "0.6.16", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.16.tgz", - "integrity": "sha512-whx+e3pgC3J9O6t4LOB8jiLk3tpWtnXaQ+xt/ys/4IGUPRI+nnWooVdtWrEnMga/gT03ug9SdEAEJLl6I1BIlg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/util": "1.11.3", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@firebase/auth/node_modules/@firebase/util": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.3.tgz", - "integrity": "sha512-4wYnOV9FpwdCq3rHQOCrdx4AQBUbfH1p2DhWGQxlQ+D3Xl/wSxc/HttcyPN4NNFiynxoNCFGWQH/zdhRfxP1Zg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@firebase/component": { - "version": "0.6.14", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.14.tgz", - "integrity": "sha512-kf/zAT8GQJ9nYoHuj0mv7twp1QzifKYrO+GsmsVHHM+Hi9KkmI7E3B3J0CtihHpb34vinl4gbJrYJ2p2wfvc9A==", + "version": "0.6.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.17.tgz", + "integrity": "sha512-M6DOg7OySrKEFS8kxA3MU5/xc37fiOpKPMz6cTsMUcsuKB6CiZxxNAvgFta8HGRgEpZbi8WjGIj6Uf+TpOhyzg==", "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "tslib": "^2.1.0" }, "engines": { @@ -3026,14 +3003,14 @@ } }, "node_modules/@firebase/firestore": { - "version": "4.7.14", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.14.tgz", - "integrity": "sha512-YLz71p96ACfILNjnqh7H6ilsT3AZZyDpCCE+wpl8mJklAbdpyd2ahNIqS1eBCjseqls8vQO/XTaIcbpkSgQFIg==", + "version": "4.7.15", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.15.tgz", + "integrity": "sha512-FgWTmkNBEXdKCoN2ngBNjrMaXuBx6QwjiZZVnOGg+VjUmiBq5gAqlDIW5bZY6i/NYvLUrWugdqIs7y9GHEqwww==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.16", + "@firebase/component": "0.6.17", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.3", + "@firebase/util": "1.12.0", "@firebase/webchannel-wrapper": "1.0.3", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", @@ -3046,40 +3023,14 @@ "@firebase/app": "0.x" } }, - "node_modules/@firebase/firestore/node_modules/@firebase/component": { - "version": "0.6.16", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.16.tgz", - "integrity": "sha512-whx+e3pgC3J9O6t4LOB8jiLk3tpWtnXaQ+xt/ys/4IGUPRI+nnWooVdtWrEnMga/gT03ug9SdEAEJLl6I1BIlg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/util": "1.11.3", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@firebase/firestore/node_modules/@firebase/util": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.3.tgz", - "integrity": "sha512-4wYnOV9FpwdCq3rHQOCrdx4AQBUbfH1p2DhWGQxlQ+D3Xl/wSxc/HttcyPN4NNFiynxoNCFGWQH/zdhRfxP1Zg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@firebase/installations": { - "version": "0.6.14", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.14.tgz", - "integrity": "sha512-uE837g9+sv6PfjWPgOfG3JtjZ+hJ7KBHO4UVenVsvhzgOxFkvLjO/bgE7fyvsaD3fOHSXunx3adRIg4eUEMPyA==", + "version": "0.6.17", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.17.tgz", + "integrity": "sha512-zfhqCNJZRe12KyADtRrtOj+SeSbD1H/K8J24oQAJVv/u02eQajEGlhZtcx9Qk7vhGWF5z9dvIygVDYqLL4o1XQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", - "@firebase/util": "1.11.1", + "@firebase/component": "0.6.17", + "@firebase/util": "1.12.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -3100,15 +3051,15 @@ } }, "node_modules/@firebase/messaging": { - "version": "0.12.18", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.18.tgz", - "integrity": "sha512-2MGhUGoCZloB7ysoYzG/T2nnRmHYLT+AcqYouZuD6APabpkDhF8lHsmSQq4MFSlXhI3DKFOXxjuvbY8ec4C2JQ==", + "version": "0.12.21", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.21.tgz", + "integrity": "sha512-bYJ2Evj167Z+lJ1ach6UglXz5dUKY1zrJZd15GagBUJSR7d9KfiM1W8dsyL0lDxcmhmA/sLaBYAAhF1uilwN0g==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", - "@firebase/installations": "0.6.14", + "@firebase/component": "0.6.17", + "@firebase/installations": "0.6.17", "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -3123,9 +3074,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/util": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.1.tgz", - "integrity": "sha512-RXg4WE8C2LUrvoV/TMGRTu223zZf9Dq9MR8yHZio9nF9TpLnpCPURw9VWWB2WATDl6HfIdWfl2x2SJYtHkN4hw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.12.0.tgz", + "integrity": "sha512-Z4rK23xBCwgKDqmzGVMef+Vb4xso2j5Q8OG0vVL4m4fA5ZjPMYQazu8OJJC3vtQRC3SQ/Pgx/6TPNVsCd70QRw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4510,50 +4461,50 @@ "license": "MIT" }, "node_modules/@sentry-internal/browser-utils": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.19.0.tgz", - "integrity": "sha512-DlEHX4eIHe5yIuh/cFu9OiaFuk1CTnFK95zj61I7Q2fxmN43dIwC3xAAGJ/Hy+GDQi7kU+BiS2sudSHSTq81BA==", + "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==", "license": "MIT", "dependencies": { - "@sentry/core": "9.19.0" + "@sentry/core": "9.21.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.19.0.tgz", - "integrity": "sha512-yixRrv4NfpjhFW56AuUTjVwZlignB9FWAXXyrmRP3SsFeJCFrAsSD8HOxV9RXNr9ePYl7MEU0Agi43YWhJsiAw==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.21.0.tgz", + "integrity": "sha512-Z234NgcWolFpmztCh+9smC6WlO8By5t4KucHNfYSQ0xQYQCxPL5iChj3JpF4dwv+qCYXhDFLQFQbK0U3Px056g==", "license": "MIT", "dependencies": { - "@sentry/core": "9.19.0" + "@sentry/core": "9.21.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.19.0.tgz", - "integrity": "sha512-i/X9brRchbAF25yjxLTI7E8eoESRPBgIyQOWoWRXXt2n51iBRTjLXSaEfGvjdN+qrMq/yd6nC1/UqJVxXHeIhA==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.21.0.tgz", + "integrity": "sha512-7mq3Bsp8EJa3YTIYgmWfNgJdvbeaAJ6VYsqi0yxR/vNGxY3qH+PLlv+ZOEXI2U0CL6vhqFPbqmxiUOCuAjnpGg==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.19.0", - "@sentry/core": "9.19.0" + "@sentry-internal/browser-utils": "9.21.0", + "@sentry/core": "9.21.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.19.0.tgz", - "integrity": "sha512-YC8yrOjuKSfQgGniJnzkdbFsWEPTlNpzeeYPTxS4ouH1FwfGrSkPmcddjor2YHaLfiuHHqQ/Vvq70n+zruJH7A==", + "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==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "9.19.0", - "@sentry/core": "9.19.0" + "@sentry-internal/replay": "9.21.0", + "@sentry/core": "9.21.0" }, "engines": { "node": ">=18" @@ -4569,16 +4520,16 @@ } }, "node_modules/@sentry/browser": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.19.0.tgz", - "integrity": "sha512-efKfPQ0yQkdIkC7qJ5TIHxnecLNENGUYl1YD/TC8yyzW2JRf/3OYo5yg1hY2rhsP5RwQShXlT7uA03ABVIkA4A==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.21.0.tgz", + "integrity": "sha512-NF0G104JRP2TZ2hpMHElO4bEEUdBWknKSh2d0SRyGpJFVfOQG3oRHczXWH08A5InA/lNrS9LEdodUhiFue+F3A==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.19.0", - "@sentry-internal/feedback": "9.19.0", - "@sentry-internal/replay": "9.19.0", - "@sentry-internal/replay-canvas": "9.19.0", - "@sentry/core": "9.19.0" + "@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" }, "engines": { "node": ">=18" @@ -4951,22 +4902,22 @@ } }, "node_modules/@sentry/core": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.19.0.tgz", - "integrity": "sha512-I41rKpMJHHZb0z0Nja+Lxto6IkEEmX3uWjnECypF8Z1HIjeJB0+PXl8p/7TeaKYqw2J2GYcRTg7jQZDmvKle1w==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.21.0.tgz", + "integrity": "sha512-K0a72Evg0fzc52Oe8R8Op5TyUMzORkk4ytt3G24lSnF4hh8NPf0m6VGkEUgQRPj27g2bF6tq9fCNsJILsf1PDA==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.19.0.tgz", - "integrity": "sha512-tHuzPVbqKsONlFQsy7FqqGjBaujQoLRIDBLlPPMNoiGvP3rodBl6t1v5zoNAq4m47i3MhvpLEYf6C00j1w5UMQ==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.21.0.tgz", + "integrity": "sha512-RGbyVo4fS7SX2AjEpdRXDo4C4IYIx0zQcI5bSTgySuhxL0JAxohcuSsNWpx48QkJwK/avtmlmCIPKgbvhF16TQ==", "license": "MIT", "dependencies": { - "@sentry/browser": "9.19.0", - "@sentry/core": "9.19.0", + "@sentry/browser": "9.21.0", + "@sentry/core": "9.21.0", "hoist-non-react-statics": "^3.3.2" }, "engines": { @@ -5862,14 +5813,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -5878,13 +5829,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -5925,9 +5876,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", "dev": true, "license": "MIT", "dependencies": { @@ -5938,13 +5889,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", + "@vitest/utils": "3.1.4", "pathe": "^2.0.3" }, "funding": { @@ -5959,13 +5910,13 @@ "license": "MIT" }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -5991,9 +5942,9 @@ "license": "MIT" }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -6004,13 +5955,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -6140,9 +6091,9 @@ } }, "node_modules/antd": { - "version": "5.25.1", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.25.1.tgz", - "integrity": "sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==", + "version": "5.25.2", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.25.2.tgz", + "integrity": "sha512-7R2nUvlHhey7Trx64+hCtGXOiy+DTUs1Lv5bwbV1LzEIZIhWb0at1AM6V3K108a5lyoR9n7DX3ptlLF7uYV/DQ==", "license": "MIT", "dependencies": { "@ant-design/colors": "^7.2.0", @@ -6180,11 +6131,11 @@ "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", - "rc-select": "~14.16.7", + "rc-select": "~14.16.8", "rc-slider": "~11.1.8", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", - "rc-table": "~7.50.4", + "rc-table": "~7.50.5", "rc-tabs": "~15.6.1", "rc-textarea": "~1.10.0", "rc-tooltip": "~6.4.0", @@ -13621,9 +13572,9 @@ } }, "node_modules/query-string": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.2.tgz", - "integrity": "sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.2.0.tgz", + "integrity": "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ==", "license": "MIT", "dependencies": { "decode-uri-component": "^0.4.1", @@ -14076,9 +14027,9 @@ } }, "node_modules/rc-select": { - "version": "14.16.7", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.7.tgz", - "integrity": "sha512-lT9kO5gFHQdJzu9a0btcOtNaJHkhenSl8H5mcpgXN9VIMXP59rnkpbdHmPrteixWs1D5zFOTyoTYX3b7joADIQ==", + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -14149,9 +14100,9 @@ } }, "node_modules/rc-table": { - "version": "7.50.4", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.4.tgz", - "integrity": "sha512-Y+YuncnQqoS5e7yHvfvlv8BmCvwDYDX/2VixTBEhkMDk9itS9aBINp4nhzXFKiBP/frG4w0pS9d9Rgisl0T1Bw==", + "version": "7.50.5", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.5.tgz", + "integrity": "sha512-FDZu8aolhSYd3v9KOc3lZOVAU77wmRRu44R0Wfb8Oj1dXRUsloFaXMSl6f7yuWZUxArJTli7k8TEOX2mvhDl4A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -15427,9 +15378,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.88.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", - "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz", + "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -17585,9 +17536,9 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", "dev": true, "license": "MIT", "dependencies": { @@ -17783,19 +17734,19 @@ } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", @@ -17808,7 +17759,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -17824,8 +17775,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, diff --git a/client/package.json b/client/package.json index 4018d9629..c6bc4f515 100644 --- a/client/package.json +++ b/client/package.json @@ -12,19 +12,19 @@ "@apollo/client": "^3.13.6", "@emotion/is-prop-valid": "^1.3.1", "@fingerprintjs/fingerprintjs": "^4.6.1", - "@firebase/analytics": "^0.10.13", - "@firebase/app": "^0.12.1", - "@firebase/auth": "^1.10.4", - "@firebase/firestore": "^4.7.14", - "@firebase/messaging": "^0.12.18", + "@firebase/analytics": "^0.10.16", + "@firebase/app": "^0.13.0", + "@firebase/auth": "^1.10.5", + "@firebase/firestore": "^4.7.15", + "@firebase/messaging": "^0.12.21", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.19.0", + "@sentry/react": "^9.21.0", "@sentry/vite-plugin": "^3.4.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", - "antd": "^5.25.1", + "antd": "^5.25.2", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^4.3.0", "autosize": "^6.0.1", @@ -48,7 +48,7 @@ "normalize-url": "^8.0.1", "object-hash": "^3.0.0", "prop-types": "^15.8.1", - "query-string": "^9.1.2", + "query-string": "^9.2.0", "raf-schd": "^4.0.3", "react": "^18.3.1", "react-big-calendar": "^1.18.0", @@ -77,7 +77,7 @@ "redux-saga": "^1.3.0", "redux-state-sync": "^3.1.4", "reselect": "^5.1.1", - "sass": "^1.88.0", + "sass": "^1.89.0", "socket.io-client": "^4.8.1", "styled-components": "^6.1.18", "subscriptions-transport-ws": "^0.11.0", @@ -130,10 +130,10 @@ "@ant-design/icons": "^6.0.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.27.1", - "@dotenvx/dotenvx": "^1.44.0", + "@dotenvx/dotenvx": "^1.44.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", - "@eslint/js": "^9.26.0", + "@eslint/js": "^9.27.0", "@playwright/test": "^1.51.1", "@sentry/webpack-plugin": "^3.4.0", "@testing-library/dom": "^10.4.0", @@ -160,7 +160,7 @@ "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-pwa": "^1.0.0", "vite-plugin-style-import": "^2.0.0", - "vitest": "^3.1.3", + "vitest": "^3.1.4", "workbox-window": "^7.3.0" } } diff --git a/package-lock.json b/package-lock.json index 807f273d1..8e0440f9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "0.2.0", "license": "UNLICENSED", "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.810.0", - "@aws-sdk/client-elasticache": "^3.810.0", - "@aws-sdk/client-s3": "^3.810.0", - "@aws-sdk/client-secrets-manager": "^3.810.0", - "@aws-sdk/client-ses": "^3.810.0", - "@aws-sdk/credential-provider-node": "^3.810.0", - "@aws-sdk/lib-storage": "^3.810.0", - "@aws-sdk/s3-request-presigner": "^3.810.0", + "@aws-sdk/client-cloudwatch-logs": "^3.812.0", + "@aws-sdk/client-elasticache": "^3.812.0", + "@aws-sdk/client-s3": "^3.812.0", + "@aws-sdk/client-secrets-manager": "^3.812.0", + "@aws-sdk/client-ses": "^3.812.0", + "@aws-sdk/credential-provider-node": "^3.812.0", + "@aws-sdk/lib-storage": "^3.812.0", + "@aws-sdk/s3-request-presigner": "^3.812.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -24,7 +24,7 @@ "aws4": "^1.13.2", "axios": "^1.8.4", "better-queue": "^3.8.12", - "bullmq": "^5.52.2", + "bullmq": "^5.52.3", "chart.js": "^4.4.8", "cloudinary": "^2.6.1", "compression": "^1.8.0", @@ -66,15 +66,15 @@ "xmlbuilder2": "^3.1.1" }, "devDependencies": { - "@eslint/js": "^9.26.0", - "eslint": "^9.26.0", + "@eslint/js": "^9.27.0", + "eslint": "^9.27.0", "eslint-plugin-react": "^7.37.5", "globals": "^15.15.0", "mock-require": "^3.0.3", "p-limit": "^3.1.0", "prettier": "^3.5.3", "supertest": "^7.1.1", - "vitest": "^3.1.3" + "vitest": "^3.1.4" }, "engines": { "node": ">=22.13.0", @@ -284,24 +284,24 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.810.0.tgz", - "integrity": "sha512-1FLkuei0pnBIGJJktYhY03LNcdGiYyxn8TcugUl4A3+7fOs9HCmcA+a5vTUThvnDqFKn4xtptQgYo4oiSLxtwA==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.812.0.tgz", + "integrity": "sha512-SLvqaMwRviAwb+z4XAq2QmlbUjr7rXN6zAEr4/x2ltyrsxEV95gBo0KHeroAsWhd4eD19USjAgg64KJgvUtNGw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.810.0", - "@aws-sdk/credential-provider-node": "3.810.0", + "@aws-sdk/core": "3.812.0", + "@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.810.0", + "@aws-sdk/util-user-agent-node": "3.812.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/eventstream-serde-browser": "^4.0.2", @@ -352,24 +352,24 @@ } }, "node_modules/@aws-sdk/client-elasticache": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.810.0.tgz", - "integrity": "sha512-tB7bBIoAz+qoFq8b1oR6TdjIru3Ui9VL+TWzM6jxxgS2LtyH3MS6hu0qsJWIVeDZO9MEOdb66u+aQoyxJecasA==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.812.0.tgz", + "integrity": "sha512-o1KC5Glo3c0T/RN2XBanHu40k3M99MJyq+e/02tIMgEGKIPmnvB8A8muE2F3rQ2A0qLCxvjhm+kprlmDwzpryw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.810.0", - "@aws-sdk/credential-provider-node": "3.810.0", + "@aws-sdk/core": "3.812.0", + "@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.810.0", + "@aws-sdk/util-user-agent-node": "3.812.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -403,32 +403,32 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.810.0.tgz", - "integrity": "sha512-wM8M0BrqRkbZ9fGaLmAl24CUgVmmLjiKuNTqGOHsdCIc7RV+IGv5CnGK7ciOltAttrFBxvDlhy2lg5F8gNw8Bg==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.812.0.tgz", + "integrity": "sha512-kHgw9JDXNPLa/mHtWpOd5btBVXFSe+wwp1Ed9+bqz9uLkv0iV4joZrdQwnydkO8zlTs60Sc5ez+P2OiZ76i2Qg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.810.0", - "@aws-sdk/credential-provider-node": "3.810.0", + "@aws-sdk/core": "3.812.0", + "@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/middleware-bucket-endpoint": "3.808.0", "@aws-sdk/middleware-expect-continue": "3.804.0", - "@aws-sdk/middleware-flexible-checksums": "3.810.0", + "@aws-sdk/middleware-flexible-checksums": "3.812.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-location-constraint": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-sdk-s3": "3.810.0", + "@aws-sdk/middleware-sdk-s3": "3.812.0", "@aws-sdk/middleware-ssec": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", - "@aws-sdk/signature-v4-multi-region": "3.810.0", + "@aws-sdk/signature-v4-multi-region": "3.812.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.810.0", + "@aws-sdk/util-user-agent-node": "3.812.0", "@aws-sdk/xml-builder": "3.804.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", @@ -470,24 +470,24 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.810.0.tgz", - "integrity": "sha512-FKqvgtBOQ69SXFfuMpgx81GmaJMAmCyfb6JjSdSay0Gj585dzb6Jn7ye8zkuy4k6XAt8flnbeaFNORbY9BT38g==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.812.0.tgz", + "integrity": "sha512-RyGzi7kkacjPd0QgVjw6OYvZVvuqtd1wRwG0Aek32dPUYu8eOs9FDaqBsDnNIqdw+lAqC/pKIOPYWtLu2OxE0Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.810.0", - "@aws-sdk/credential-provider-node": "3.810.0", + "@aws-sdk/core": "3.812.0", + "@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.810.0", + "@aws-sdk/util-user-agent-node": "3.812.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -535,24 +535,24 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.810.0.tgz", - "integrity": "sha512-QUaW4bJIpqNyIPWvRun8avLDuIfbWOAyspgnG5JSkQtIK3wWeeeR8lXrCNFtgzY0X8MPdy0Ns50YbVEsVWnmMA==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.812.0.tgz", + "integrity": "sha512-7JUS2u0AKMYiEmRrxAYQj8ifFwVUgMAHt5H/KjMhh+1El0NqAQDt3JLD4Asmzy7/TvTAWZfk5np2LQPNB2wZpw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.810.0", - "@aws-sdk/credential-provider-node": "3.810.0", + "@aws-sdk/core": "3.812.0", + "@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.810.0", + "@aws-sdk/util-user-agent-node": "3.812.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -586,23 +586,23 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.810.0.tgz", - "integrity": "sha512-Txp/3jHqkfA4BTklQEOGiZ1yTUxg+hITislfaWEzJ904vlDt4DvAljTlhfaz7pceCLA2+LhRlYZYSv7t5b0Ltw==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.812.0.tgz", + "integrity": "sha512-O//smQRj1+RXELB7xX54s5pZB0V69KHXpUZmz8V+8GAYO1FKTHfbpUgK+zyMNb+lFZxG9B69yl8pWPZ/K8bvxA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.810.0", + "@aws-sdk/core": "3.812.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.810.0", + "@aws-sdk/util-user-agent-node": "3.812.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -635,9 +635,9 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.810.0.tgz", - "integrity": "sha512-s2IJk+qa/15YZcv3pbdQNATDR+YdYnHf94MrAeVAWubtRLnzD8JciC+gh4LSPp7JzrWSvVOg2Ut1S+0y89xqCg==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.812.0.tgz", + "integrity": "sha512-myWA9oHMBVDObKrxG+puAkIGs8igcWInQ1PWCRTS/zN4BkhUMFjjh/JPV/4Vzvtvj5E36iujq2WtlrDLl1PpOw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.804.0", @@ -657,12 +657,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.810.0.tgz", - "integrity": "sha512-iwHqF+KryKONfbdFk3iKhhPk4fHxh5QP5fXXR//jhYwmszaLOwc7CLCE9AxhgiMzAs+kV8nBFQZvdjFpPzVGOA==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.812.0.tgz", + "integrity": "sha512-Ge7IEu06ANurGBZx39q9CNN/ncqb1K8lpKZCY969uNWO0/7YPhnplrRJGMZYIS35nD2mBm3ortEKjY/wMZZd5g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.810.0", + "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -673,12 +673,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.810.0.tgz", - "integrity": "sha512-SKzjLd+8ugif7yy9sOAAdnPE1vCBHQe6jKgs2AadMpCmWm34DiHz/KuulHdvURUGMIi7CvmaC8aH77twDPYbtg==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.812.0.tgz", + "integrity": "sha512-Vux2U42vPGXeE407Lp6v3yVA65J7hBO9rB67LXshyGVi7VZLAYWc4mrZxNJNqabEkjcDEmMQQakLPT6zc5SvFw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.810.0", + "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", @@ -694,18 +694,18 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.810.0.tgz", - "integrity": "sha512-H2QCSnxWJ/mj8HTcyHmCmyQ5bO/+imRi4mlBIpUyKjiYKro52WD3gXlGgPIDo2q3UFIHq37kmYvS00i+qIY9tw==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.812.0.tgz", + "integrity": "sha512-oltqGvQ488xtPY5wrNjbD+qQYYkuCjn30IDE1qKMxJ58EM6UVTQl3XV44Xq07xfF5gKwVJQkfIyOkRAguOVybg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.810.0", - "@aws-sdk/credential-provider-env": "3.810.0", - "@aws-sdk/credential-provider-http": "3.810.0", - "@aws-sdk/credential-provider-process": "3.810.0", - "@aws-sdk/credential-provider-sso": "3.810.0", - "@aws-sdk/credential-provider-web-identity": "3.810.0", - "@aws-sdk/nested-clients": "3.810.0", + "@aws-sdk/core": "3.812.0", + "@aws-sdk/credential-provider-env": "3.812.0", + "@aws-sdk/credential-provider-http": "3.812.0", + "@aws-sdk/credential-provider-process": "3.812.0", + "@aws-sdk/credential-provider-sso": "3.812.0", + "@aws-sdk/credential-provider-web-identity": "3.812.0", + "@aws-sdk/nested-clients": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/credential-provider-imds": "^4.0.4", "@smithy/property-provider": "^4.0.2", @@ -718,17 +718,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.810.0.tgz", - "integrity": "sha512-9E3Chv3x+RBM3N1bwLCyvXxoiPAckCI74wG7ePN4F3b/7ieIkbEl/3Hd67j1fnt62Xa1cjUHRu2tz5pdEv5G1Q==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.812.0.tgz", + "integrity": "sha512-SnvSWBP6cr9nqx784eETnL2Zl7ZnMB/oJgFVEG1aejAGbT1H9gTpMwuUsBXk4u/mEYe3f1lh1Wqo+HwDgNkfrg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.810.0", - "@aws-sdk/credential-provider-http": "3.810.0", - "@aws-sdk/credential-provider-ini": "3.810.0", - "@aws-sdk/credential-provider-process": "3.810.0", - "@aws-sdk/credential-provider-sso": "3.810.0", - "@aws-sdk/credential-provider-web-identity": "3.810.0", + "@aws-sdk/credential-provider-env": "3.812.0", + "@aws-sdk/credential-provider-http": "3.812.0", + "@aws-sdk/credential-provider-ini": "3.812.0", + "@aws-sdk/credential-provider-process": "3.812.0", + "@aws-sdk/credential-provider-sso": "3.812.0", + "@aws-sdk/credential-provider-web-identity": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/credential-provider-imds": "^4.0.4", "@smithy/property-provider": "^4.0.2", @@ -741,12 +741,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.810.0.tgz", - "integrity": "sha512-42kE6MLdsmMGp1id3Gisal4MbMiF7PIc0tAznTeIuE8r7cIF8yeQWw/PBOIvjyI57DxbyKzLUAMEJuigUpApCw==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.812.0.tgz", + "integrity": "sha512-YI8bb153XeEOb59F9KtTZEwDAc14s2YHZz58+OFiJ2udnKsPV87mNiFhJPW6ba9nmOLXVat5XDcwtVT1b664wg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.810.0", + "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -758,14 +758,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.810.0.tgz", - "integrity": "sha512-8WjX6tz+FCvM93Y33gsr13p/HiiTJmVn5AK1O8PTkvHBclQDzmtAW5FdPqTpAJGswLW2FB0xRqdsSMN2dQEjNw==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.812.0.tgz", + "integrity": "sha512-ODsPcNhgiO6GOa82TVNskM97mml9rioe9Cbhemz48lkfDQPv1u06NaCR0o3FsvprX1sEhMvJTR3sE1fyEOzvJQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.810.0", - "@aws-sdk/core": "3.810.0", - "@aws-sdk/token-providers": "3.810.0", + "@aws-sdk/client-sso": "3.812.0", + "@aws-sdk/core": "3.812.0", + "@aws-sdk/token-providers": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -777,13 +777,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.810.0.tgz", - "integrity": "sha512-uKQJY0AcPyrvMmfGLo36semgjqJ4vmLTqOSW9u40qQDspRnG73/P09lAO2ntqKlhwvMBt3XfcNnOpyyhKRcOfA==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.812.0.tgz", + "integrity": "sha512-E9Bmiujvm/Hp9DM/Vc1S+D0pQbx8/x4dR/zyAEZU9EoRq0duQOQ1reWYWbebYmL1OklcVpTfKV0a/VCwuAtGSg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.810.0", - "@aws-sdk/nested-clients": "3.810.0", + "@aws-sdk/core": "3.812.0", + "@aws-sdk/nested-clients": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -794,9 +794,9 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.810.0.tgz", - "integrity": "sha512-Ri3Kgkk9XK7wmgKaQUA4ZNgr4lTASFlt2pd4xo7sbpX7K984fYkWe7ecrgbRptIiIYUjeJiYCRb0FS34diSaZw==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.812.0.tgz", + "integrity": "sha512-z37ykuXQXfGO7dqQFbEnj1Wu9UwUUXpZhr4iWXsehbIzSqyl5FiCMp0cI5XK8jLVACCfSCssZCz6QD4oDYdKlQ==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.2", @@ -811,7 +811,7 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.810.0" + "@aws-sdk/client-s3": "^3.812.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -848,15 +848,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.810.0.tgz", - "integrity": "sha512-lF5fse+26hluElOtDZMsi5EH50G13OEqglFgpSc6xWnqNhbDc+CnPQRMwTVlOJBDR1/YVbJ15LOKf4pkat46Eg==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.812.0.tgz", + "integrity": "sha512-/ayAooUZvV1GTomNMrfbhjUHAEaz0Wmio3lKyaTJsW4WdLJXBuzdo57YADRmYYUqx6awzJ6VJ6HGc1Uc6tOlbw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.810.0", + "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/is-array-buffer": "^4.0.0", "@smithy/node-config-provider": "^4.1.1", @@ -930,12 +930,12 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.810.0.tgz", - "integrity": "sha512-CmQHPVopJYDiGQOmqn5AcmhksQ9qNETF0VgU251Q4tsP9s3R9nBR1r2bZwLt5+dCtf9UCa7cBw4jXKHtH38JTg==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.812.0.tgz", + "integrity": "sha512-e8AqRRIaTsunL1hqtO1hksa9oTYdsIbfezHUyVpPGugUIB1lMqPt/DlBsanI85OzUD711UfNSEcZ1mqAxpDOoA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.810.0", + "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-arn-parser": "3.804.0", "@smithy/core": "^3.3.3", @@ -969,12 +969,12 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.810.0.tgz", - "integrity": "sha512-gLMJcqgIq7k9skX8u0Yyi+jil4elbsmLf3TuDuqNdlqiZ44/AKdDFfU3mU5tRUtMfP42a3gvb2U3elP0BIeybQ==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.812.0.tgz", + "integrity": "sha512-r+HFwtSvnAs6Fydp4mijylrTX0og9p/xfxOcKsqhMuk3HpZAIcf9sSjRQI6MBusYklg7pnM4sGEnPAZIrdRotA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.810.0", + "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@smithy/core": "^3.3.3", @@ -987,23 +987,23 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.810.0.tgz", - "integrity": "sha512-w+tGXFSQjzvJ3j2sQ4GJRdD+YXLTgwLd9eG/A+7pjrv2yLLV70M4HqRrFqH06JBjqT5rsOxonc/QSjROyxk+IA==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.812.0.tgz", + "integrity": "sha512-FS/fImbEpJU3cXtBGR9fyVd+CP51eNKlvTMi3f4/6lSk3RmHjudNC9yEF/og3jtpT3O+7vsNOUW9mHco5IjdQQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.810.0", + "@aws-sdk/core": "3.812.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.810.0", + "@aws-sdk/util-user-agent-node": "3.812.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -1053,12 +1053,12 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.810.0.tgz", - "integrity": "sha512-/6TISQT3HUOoY3Gli1EvQVISNGp8XawGKD4igJqwyF3l35VvajzaAfhwk+Tm5TKTCkFh8EFnqWeT+mId5AaUbA==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.812.0.tgz", + "integrity": "sha512-OpyANELjcD2oknkd3/qWanaRaZDx4SSV6NwYuWIk+fuxDZ+KxZZrrfue1X7OAdaP2TdSapbs7xLisxtTuptWYg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "3.810.0", + "@aws-sdk/signature-v4-multi-region": "3.812.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-format-url": "3.804.0", "@smithy/middleware-endpoint": "^4.1.6", @@ -1072,12 +1072,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.810.0.tgz", - "integrity": "sha512-6F6evHRrA0OG/H67YuZBcI7EH4A0O5dIhczo2N0AK9z495uSIv+0xUrSrDhFTZxZjo6gkADIXroRjP1kwqK6ew==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.812.0.tgz", + "integrity": "sha512-JTpk3ZHf7TXYbicKfOKi+VrsBTqcAszg9QR9fQmT9aCxPp39gsF3WsXq7NjepwZ5So11ixGIsPE/jtMym399QQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.810.0", + "@aws-sdk/middleware-sdk-s3": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", @@ -1089,12 +1089,12 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.810.0.tgz", - "integrity": "sha512-fdgHRCDpnzsD+0km7zuRbHRysJECfS8o9T9/pZ6XAr1z2FNV/UveHtnUYq0j6XpDMrIm0/suvXbshIjQU+a+sw==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.812.0.tgz", + "integrity": "sha512-dbVBaKxrxE708ub5uH3w+cmKIeRQas+2Xf6rpckhohYY+IiflGOdK6aLrp3T6dOQgr/FJ37iQtcYNonAG+yVBQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/nested-clients": "3.810.0", + "@aws-sdk/nested-clients": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -1185,12 +1185,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.810.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.810.0.tgz", - "integrity": "sha512-T56/ANEGNuvhqVoWZdr+0ZY2hjV93cH2OfGHIlVTVSAMACWG54XehDPESEso1CJNhJGYZPsE+FE42HGCk/XDMg==", + "version": "3.812.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.812.0.tgz", + "integrity": "sha512-8pt+OkHhS2U0LDwnzwRnFxyKn8sjSe752OIZQCNv263odud8jQu9pYO2pKqb2kRBk9h9szynjZBDLXfnvSQ7Bg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/node-config-provider": "^4.1.1", "@smithy/types": "^4.2.0", @@ -1334,9 +1334,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "cpu": [ "ppc64" ], @@ -1351,9 +1351,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "cpu": [ "arm" ], @@ -1368,9 +1368,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "cpu": [ "arm64" ], @@ -1385,9 +1385,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "cpu": [ "x64" ], @@ -1402,9 +1402,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "cpu": [ "arm64" ], @@ -1419,9 +1419,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "cpu": [ "x64" ], @@ -1436,9 +1436,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "cpu": [ "arm64" ], @@ -1453,9 +1453,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "cpu": [ "x64" ], @@ -1470,9 +1470,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "cpu": [ "arm" ], @@ -1487,9 +1487,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "cpu": [ "arm64" ], @@ -1504,9 +1504,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "cpu": [ "ia32" ], @@ -1521,9 +1521,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "cpu": [ "loong64" ], @@ -1538,9 +1538,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "cpu": [ "mips64el" ], @@ -1555,9 +1555,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "cpu": [ "ppc64" ], @@ -1572,9 +1572,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "cpu": [ "riscv64" ], @@ -1589,9 +1589,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "cpu": [ "s390x" ], @@ -1606,9 +1606,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "cpu": [ "x64" ], @@ -1623,9 +1623,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "cpu": [ "arm64" ], @@ -1640,9 +1640,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "cpu": [ "x64" ], @@ -1657,9 +1657,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", "cpu": [ "arm64" ], @@ -1674,9 +1674,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "cpu": [ "x64" ], @@ -1691,9 +1691,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "cpu": [ "x64" ], @@ -1708,9 +1708,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "cpu": [ "arm64" ], @@ -1725,9 +1725,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "cpu": [ "ia32" ], @@ -1742,9 +1742,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "cpu": [ "x64" ], @@ -1826,9 +1826,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1876,13 +1876,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -1896,13 +1899,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -2350,252 +2353,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", - "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", @@ -2804,9 +2561,9 @@ "license": "BSD-3-Clause" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", "cpu": [ "arm" ], @@ -2818,9 +2575,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", "cpu": [ "arm64" ], @@ -2832,9 +2589,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", "cpu": [ "arm64" ], @@ -2846,9 +2603,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", "cpu": [ "x64" ], @@ -2860,9 +2617,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", "cpu": [ "arm64" ], @@ -2874,9 +2631,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", "cpu": [ "x64" ], @@ -2888,9 +2645,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", "cpu": [ "arm" ], @@ -2902,9 +2659,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", "cpu": [ "arm" ], @@ -2916,9 +2673,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", "cpu": [ "arm64" ], @@ -2930,9 +2687,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", + "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", "cpu": [ "arm64" ], @@ -2944,9 +2701,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", "cpu": [ "loong64" ], @@ -2958,9 +2715,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", "cpu": [ "ppc64" ], @@ -2972,9 +2729,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", "cpu": [ "riscv64" ], @@ -2986,9 +2743,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", "cpu": [ "riscv64" ], @@ -3000,9 +2757,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", "cpu": [ "s390x" ], @@ -3014,9 +2771,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", + "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", "cpu": [ "x64" ], @@ -3028,9 +2785,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", + "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", "cpu": [ "x64" ], @@ -3042,9 +2799,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", "cpu": [ "arm64" ], @@ -3056,9 +2813,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", "cpu": [ "ia32" ], @@ -3070,9 +2827,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", "cpu": [ "x64" ], @@ -3479,12 +3236,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.1.tgz", + "integrity": "sha512-Vsay2mzq05DwNi9jK01yCFtfvu9HimmgC7a4HTs7lhX12Sx8aWsH0mfz6q/02yspSp+lOB+Q2HJwi4IV2GKz7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.2.0", + "@smithy/types": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -3544,16 +3301,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.0.tgz", - "integrity": "sha512-4t5WX60sL3zGJF/CtZsUQTs3UrZEDO2P7pEaElrekbLqkWPYkgqNW1oeiNYC6xXifBnT9dVBOnNQRvOE9riU9w==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.1.tgz", + "integrity": "sha512-zy8Repr5zvT0ja+Tf5wjV/Ba6vRrhdiDcp/ww6cvqYbSEudIkziDe3uppNRlFoCViyJXdPnLcwyZdDLA4CHzSg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", + "@smithy/protocol-http": "^5.1.1", + "@smithy/types": "^4.3.0", "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.2", + "@smithy/util-middleware": "^4.0.3", "@smithy/util-uri-escape": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" @@ -3581,9 +3338,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.2.0.tgz", - "integrity": "sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.0.tgz", + "integrity": "sha512-+1iaIQHthDh9yaLhRzaoQxRk+l9xlk+JjMFxGRhNLz+m9vKOkjNeU8QuB4w3xvzHyVR/BVlp/4AXDHjoRIkfgQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3730,12 +3487,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.2.tgz", - "integrity": "sha512-6GDamTGLuBQVAEuQ4yDQ+ti/YINf/MEmIegrEeg7DdB/sld8BX1lqt9RRuIcABOhAGTA50bRbPzErez7SlDtDQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.3.tgz", + "integrity": "sha512-iIsC6qZXxkD7V3BzTw3b1uK8RVC1M8WvwNxK1PKrH9FnxntCd30CSunXjL/8iJBE8Z0J14r2P69njwIpRG4FBQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.2.0", + "@smithy/types": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4101,14 +3858,14 @@ "license": "MIT" }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -4117,13 +3874,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -4144,9 +3901,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", "dev": true, "license": "MIT", "dependencies": { @@ -4157,13 +3914,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", + "@vitest/utils": "3.1.4", "pathe": "^2.0.3" }, "funding": { @@ -4171,13 +3928,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -4186,9 +3943,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -4199,13 +3956,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -4814,81 +4571,6 @@ "node": "*" } }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/body-parser/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/body-parser/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -4952,9 +4634,9 @@ } }, "node_modules/bullmq": { - "version": "5.52.2", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.52.2.tgz", - "integrity": "sha512-fK/dKIv8ymyys4K+zeNEPA+yuYWzRPmBWUmwIMz8DvYekadl8VG19yUx94Na0n0cLAi+spdn3a/+ufkYK7CBUg==", + "version": "5.52.3", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.52.3.tgz", + "integrity": "sha512-UaVkg+uSgylYWjD6/d8TVm87SjDVZ5jKwDVh/pJACmStn71aIzOIpGazh2JrkGISgT10Q/lG2I40FhPg0KgNCQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -6484,9 +6166,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6497,31 +6179,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.3", - "@esbuild/android-arm": "0.25.3", - "@esbuild/android-arm64": "0.25.3", - "@esbuild/android-x64": "0.25.3", - "@esbuild/darwin-arm64": "0.25.3", - "@esbuild/darwin-x64": "0.25.3", - "@esbuild/freebsd-arm64": "0.25.3", - "@esbuild/freebsd-x64": "0.25.3", - "@esbuild/linux-arm": "0.25.3", - "@esbuild/linux-arm64": "0.25.3", - "@esbuild/linux-ia32": "0.25.3", - "@esbuild/linux-loong64": "0.25.3", - "@esbuild/linux-mips64el": "0.25.3", - "@esbuild/linux-ppc64": "0.25.3", - "@esbuild/linux-riscv64": "0.25.3", - "@esbuild/linux-s390x": "0.25.3", - "@esbuild/linux-x64": "0.25.3", - "@esbuild/netbsd-arm64": "0.25.3", - "@esbuild/netbsd-x64": "0.25.3", - "@esbuild/openbsd-arm64": "0.25.3", - "@esbuild/openbsd-x64": "0.25.3", - "@esbuild/sunos-x64": "0.25.3", - "@esbuild/win32-arm64": "0.25.3", - "@esbuild/win32-ia32": "0.25.3", - "@esbuild/win32-x64": "0.25.3" + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" } }, "node_modules/escalade": { @@ -6566,9 +6248,9 @@ } }, "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6576,14 +6258,13 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -6607,8 +6288,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -6815,29 +6495,6 @@ "node": ">=0.8.x" } }, - "node_modules/eventsource": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", - "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/expect-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", @@ -6894,22 +6551,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, "node_modules/express/node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -8418,13 +8059,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -10095,16 +9729,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -10359,22 +9983,6 @@ "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -10711,9 +10319,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", + "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", "dev": true, "license": "MIT", "dependencies": { @@ -10727,56 +10335,29 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", + "@rollup/rollup-android-arm-eabi": "4.41.0", + "@rollup/rollup-android-arm64": "4.41.0", + "@rollup/rollup-darwin-arm64": "4.41.0", + "@rollup/rollup-darwin-x64": "4.41.0", + "@rollup/rollup-freebsd-arm64": "4.41.0", + "@rollup/rollup-freebsd-x64": "4.41.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", + "@rollup/rollup-linux-arm-musleabihf": "4.41.0", + "@rollup/rollup-linux-arm64-gnu": "4.41.0", + "@rollup/rollup-linux-arm64-musl": "4.41.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-musl": "4.41.0", + "@rollup/rollup-linux-s390x-gnu": "4.41.0", + "@rollup/rollup-linux-x64-gnu": "4.41.0", + "@rollup/rollup-linux-x64-musl": "4.41.0", + "@rollup/rollup-win32-arm64-msvc": "4.41.0", + "@rollup/rollup-win32-ia32-msvc": "4.41.0", + "@rollup/rollup-win32-x64-msvc": "4.41.0", "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/rsa-pem-from-mod-exp": { "version": "0.8.6", "resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.6.tgz", @@ -12572,9 +12153,9 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", "dev": true, "license": "MIT", "dependencies": { @@ -12595,19 +12176,19 @@ } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", @@ -12620,7 +12201,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -12636,8 +12217,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, @@ -13454,26 +13035,6 @@ "engines": { "node": ">= 14" } - }, - "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/package.json b/package.json index 10afa03ac..a2da483f7 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,14 @@ "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js" }, "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.810.0", - "@aws-sdk/client-elasticache": "^3.810.0", - "@aws-sdk/client-s3": "^3.810.0", - "@aws-sdk/client-secrets-manager": "^3.810.0", - "@aws-sdk/client-ses": "^3.810.0", - "@aws-sdk/credential-provider-node": "^3.810.0", - "@aws-sdk/lib-storage": "^3.810.0", - "@aws-sdk/s3-request-presigner": "^3.810.0", + "@aws-sdk/client-cloudwatch-logs": "^3.812.0", + "@aws-sdk/client-elasticache": "^3.812.0", + "@aws-sdk/client-s3": "^3.812.0", + "@aws-sdk/client-secrets-manager": "^3.812.0", + "@aws-sdk/client-ses": "^3.812.0", + "@aws-sdk/credential-provider-node": "^3.812.0", + "@aws-sdk/lib-storage": "^3.812.0", + "@aws-sdk/s3-request-presigner": "^3.812.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -31,7 +31,7 @@ "aws4": "^1.13.2", "axios": "^1.8.4", "better-queue": "^3.8.12", - "bullmq": "^5.52.2", + "bullmq": "^5.52.3", "chart.js": "^4.4.8", "cloudinary": "^2.6.1", "compression": "^1.8.0", @@ -73,14 +73,14 @@ "xmlbuilder2": "^3.1.1" }, "devDependencies": { - "@eslint/js": "^9.26.0", - "eslint": "^9.26.0", + "@eslint/js": "^9.27.0", + "eslint": "^9.27.0", "eslint-plugin-react": "^7.37.5", "globals": "^15.15.0", "mock-require": "^3.0.3", "p-limit": "^3.1.0", "prettier": "^3.5.3", "supertest": "^7.1.1", - "vitest": "^3.1.3" + "vitest": "^3.1.4" } } From 985d0669788e772788592b8141d005fc5685e411 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 20 May 2025 12:49:32 -0400 Subject: [PATCH 06/45] feature/IO-3182-Phone-Number-Consent - Finish Database changes --- hasura/metadata/tables.yaml | 37 +++++++++++++++++++ .../down.sql | 1 + .../up.sql | 2 + 3 files changed, 40 insertions(+) create mode 100644 hasura/migrations/1747759446622_create_table_public_phone_number_consent_history/down.sql create mode 100644 hasura/migrations/1747759446622_create_table_public_phone_number_consent_history/up.sql diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 1b4faac05..17c162e07 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -5870,6 +5870,14 @@ - 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: @@ -5925,6 +5933,35 @@ 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/1747759446622_create_table_public_phone_number_consent_history/down.sql b/hasura/migrations/1747759446622_create_table_public_phone_number_consent_history/down.sql new file mode 100644 index 000000000..311862382 --- /dev/null +++ b/hasura/migrations/1747759446622_create_table_public_phone_number_consent_history/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."phone_number_consent_history"; diff --git a/hasura/migrations/1747759446622_create_table_public_phone_number_consent_history/up.sql b/hasura/migrations/1747759446622_create_table_public_phone_number_consent_history/up.sql new file mode 100644 index 000000000..10a07721f --- /dev/null +++ b/hasura/migrations/1747759446622_create_table_public_phone_number_consent_history/up.sql @@ -0,0 +1,2 @@ +CREATE TABLE "public"."phone_number_consent_history" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "phone_number_consent_id" uuid NOT NULL, "old_value" boolean NOT NULL, "new_value" boolean NOT NULL, "reason" text NOT NULL, "changed_at" timestamptz NOT NULL DEFAULT now(), "changed_by" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("phone_number_consent_id") REFERENCES "public"."phone_number_consent"("id") ON UPDATE restrict ON DELETE restrict, UNIQUE ("id")); +CREATE EXTENSION IF NOT EXISTS pgcrypto; From 9d81c68a4d11e418d76513b4cd26d27a4854ebcb Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 20 May 2025 13:14:05 -0400 Subject: [PATCH 07/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- server/sms/receive.js | 96 ++++++++++++++++++++++++--------------- server/sms/send.js | 24 ++++++---- server/sms/status.js | 32 +++++++++---- server/utils/ioHelpers.js | 8 ++++ 4 files changed, 105 insertions(+), 55 deletions(-) diff --git a/server/sms/receive.js b/server/sms/receive.js index f08cf727e..f880105e9 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -1,16 +1,61 @@ -const path = require("path"); -require("dotenv").config({ - path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) -}); - const client = require("../graphql-client/graphql-client").client; -const queries = require("../graphql-client/queries"); +const { + FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, + UNARCHIVE_CONVERSATION, + CREATE_CONVERSATION, + INSERT_MESSAGE +} = 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; -exports.receive = async (req, res) => { +/** + * 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 + * @param req + * @param res + * @returns {Promise<*>} + */ +const receive = async (req, res) => { const { ioRedis, ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } @@ -35,7 +80,7 @@ exports.receive = async (req, res) => { try { // Step 1: Find the bodyshop and existing conversation - const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, { + const response = await client.request(FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, { mssid: req.body.MessagingServiceSid, phone: phone(req.body.From).phoneNumber }); @@ -68,14 +113,14 @@ exports.receive = async (req, res) => { // Unarchive the conversation if necessary if (existingConversation.archived) { - await client.request(queries.UNARCHIVE_CONVERSATION, { + await client.request(UNARCHIVE_CONVERSATION, { id: conversationid, archived: false }); } } else { // Create a new conversation - const newConversationResponse = await client.request(queries.CREATE_CONVERSATION, { + const newConversationResponse = await client.request(CREATE_CONVERSATION, { conversation: { bodyshopid: bodyshop.id, phone_num: phone(req.body.From).phoneNumber, @@ -90,7 +135,7 @@ exports.receive = async (req, res) => { newMessage.conversationid = conversationid; // Step 3: Insert the message into the conversation - const insertresp = await client.request(queries.INSERT_MESSAGE, { + const insertresp = await client.request(INSERT_MESSAGE, { msg: newMessage, conversationid: conversationid }); @@ -137,7 +182,7 @@ exports.receive = async (req, res) => { notification: { title: InstanceManager({ imex: `ImEX Online Message - ${message.conversation.phone_num}`, - rome: `Rome Online Message - ${message.conversation.phone_num}`, + rome: `Rome Online Message - ${message.conversation.phone_num}` }), body: message.image_path ? `Image ${message.text}` : message.text }, @@ -161,29 +206,6 @@ exports.receive = async (req, res) => { } }; -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; - } -}; - -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" }); +module.exports = { + receive }; diff --git a/server/sms/send.js b/server/sms/send.js index eb8cb6e5f..bc0a95da9 100644 --- a/server/sms/send.js +++ b/server/sms/send.js @@ -1,16 +1,17 @@ -const path = require("path"); -require("dotenv").config({ - path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) -}); - const twilio = require("twilio"); const { phone } = require("phone"); -const queries = require("../graphql-client/queries"); +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; -exports.send = async (req, res) => { +/** + * 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 { ioRedis, @@ -64,7 +65,10 @@ exports.send = async (req, res) => { }; try { - const gqlResponse = await gqlClient.request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid }); + const gqlResponse = await gqlClient.request(INSERT_MESSAGE, { + msg: newMessage, + conversationid + }); logger.log("sms-outbound-success", "DEBUG", req.user.email, null, { msid: message.sid, @@ -111,3 +115,7 @@ exports.send = async (req, res) => { res.status(500).json({ success: false, message: "Failed to send message through Twilio." }); } }; + +module.exports = { + send +}; diff --git a/server/sms/status.js b/server/sms/status.js index 0e29bbf8f..509c76d6b 100644 --- a/server/sms/status.js +++ b/server/sms/status.js @@ -1,13 +1,14 @@ -const path = require("path"); -require("dotenv").config({ - path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) -}); - const client = require("../graphql-client/graphql-client").client; -const queries = require("../graphql-client/queries"); +const { UPDATE_MESSAGE_STATUS, MARK_MESSAGES_AS_READ } = require("../graphql-client/queries"); const logger = require("../utils/logger"); -exports.status = async (req, res) => { +/** + * Handle the status of an SMS message + * @param req + * @param res + * @returns {Promise<*>} + */ +const status = async (req, res) => { const { SmsSid, SmsStatus } = req.body; const { ioRedis, @@ -21,7 +22,7 @@ exports.status = async (req, res) => { } // Update message status in the database - const response = await client.request(queries.UPDATE_MESSAGE_STATUS, { + const response = await client.request(UPDATE_MESSAGE_STATUS, { msid: SmsSid, fields: { status: SmsStatus } }); @@ -65,7 +66,13 @@ exports.status = async (req, res) => { } }; -exports.markConversationRead = async (req, res) => { +/** + * Mark a conversation as read + * @param req + * @param res + * @returns {Promise<*>} + */ +const markConversationRead = async (req, res) => { const { ioRedis, ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } @@ -80,7 +87,7 @@ exports.markConversationRead = async (req, res) => { } try { - const response = await client.request(queries.MARK_MESSAGES_AS_READ, { + const response = await client.request(MARK_MESSAGES_AS_READ, { conversationId }); @@ -104,3 +111,8 @@ exports.markConversationRead = async (req, res) => { res.status(500).json({ error: "Failed to mark conversation as read." }); } }; + +module.exports = { + status, + markConversationRead +}; diff --git a/server/utils/ioHelpers.js b/server/utils/ioHelpers.js index 584d45ce7..f26440a50 100644 --- a/server/utils/ioHelpers.js +++ b/server/utils/ioHelpers.js @@ -1,3 +1,11 @@ +/** + * @module ioHelpers + * @param app + * @param api + * @param io + * @param logger + * @returns {{getBodyshopRoom: (function(*): string), getBodyshopConversationRoom: (function({bodyshopId: *, conversationId: *}): string)}} + */ const applyIOHelpers = ({ app, api, io, logger }) => { // Global Bodyshop Room const getBodyshopRoom = (bodyshopId) => `bodyshop-broadcast-room:${bodyshopId}`; From 83860152a9faedc32fc84b425dd0b4bf6a014418 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 20 May 2025 16:04:36 -0400 Subject: [PATCH 08/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../chat-affix/chat-affix.container.jsx | 56 +++++++- .../chat-conversation-list.component.jsx | 51 +++++-- .../chat-conversation-list.styles.scss | 2 +- .../chat-media-selector.component.jsx | 8 +- .../chat-send-message.component.jsx | 27 +++- .../phone-number-consent.component.jsx | 131 ++++++++++++++++++ .../shop-info/shop-info.consent.component.jsx | 51 +++++++ client/src/graphql/bodyshop.queries.js | 10 ++ client/src/graphql/consent.queries.js | 90 ++++++++++++ client/src/pages/shop/shop.page.component.jsx | 10 +- server/graphql-client/queries.js | 101 ++++++++++++++ server/sms/receive.js | 46 ++++-- 12 files changed, 540 insertions(+), 43 deletions(-) create mode 100644 client/src/components/phone-number-consent/phone-number-consent.component.jsx create mode 100644 client/src/components/shop-info/shop-info.consent.component.jsx create mode 100644 client/src/graphql/consent.queries.js diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index 9885b8551..cf8afce3b 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -8,6 +8,7 @@ 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"; export function ChatAffixContainer({ bodyshop, chatVisible }) { const { t } = useTranslation(); @@ -34,16 +35,59 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) { SubscribeToTopicForFCMNotification(); - //Register WS handlers + // Register WebSocket handlers if (socket && socket.connected) { registerMessagingHandlers({ socket, client }); - } - return () => { - if (socket && socket.connected) { + // Handle consent-changed events + const handleConsentChanged = ({ bodyshopId, phone_number, consent_status }) => { + try { + client.cache.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() + } + ] + }; + } + ); + } catch (error) { + console.error("Error updating consent cache:", error); + } + }; + + socket.on("consent-changed", handleConsentChanged); + + return () => { + socket.off("consent-changed", handleConsentChanged); unregisterMessagingHandlers({ socket }); - } - }; + }; + } }, [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 16d4c0bf1..ede2a2570 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 @@ -10,6 +10,10 @@ 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.js"; +import { phone } from "phone"; +import { useTranslation } from "react-i18next"; const mapStateToProps = createStructuredSelector({ selectedConversation: selectSelectedConversation @@ -20,25 +24,45 @@ const mapDispatchToProps = (dispatch) => ({ }); function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) { - // That comma is there for a reason, do not remove it + const { t } = useTranslation(); const [, forceUpdate] = useState(false); - // Re-render every minute + // Normalize phone numbers and fetch consent statuses + 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, + fetchPolicy: "cache-and-network" + }); + + // Create a map of phone number to consent status + const consentMap = React.useMemo(() => { + const map = new Map(); + consentData?.phone_number_consent?.forEach((consent) => { + map.set(consent.phone_number, consent.consent_status); + }); + return map; + }, [consentData]); + useEffect(() => { const interval = setInterval(() => { - forceUpdate((prev) => !prev); // Toggle state to trigger re-render - }, 60000); // 1 minute in milliseconds - - return () => clearInterval(interval); // Cleanup on unmount + forceUpdate((prev) => !prev); + }, 60000); + return () => clearInterval(interval); }, []); - // Memoize the sorted conversation list const sortedConversationList = React.useMemo(() => { return _.orderBy(conversationList, ["updated_at"], ["desc"]); }, [conversationList]); - const renderConversation = (index) => { + 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 cardContentRight = {item.updated_at}; const cardContentLeft = item.job_conversations.length > 0 @@ -60,7 +84,12 @@ function ChatConversationListComponent({ conversationList, selectedConversation, ); - const cardExtra = ; + const cardExtra = ( + <> + + {!isConsented && {t("messaging.labels.no_consent")}} + + ); const getCardStyle = () => item.id === selectedConversation @@ -73,7 +102,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation, onClick={() => setSelectedConversation(item.id)} className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`} > - +
{cardContentLeft}
{cardContentRight}
@@ -85,7 +114,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..29e5bb8c6 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 @@ -10,6 +10,10 @@ 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 { GET_PHONE_NUMBER_CONSENT } from "../../graphql/consent.queries"; +import AlertComponent from "../alert/alert.component"; +import { phone } from "phone"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -25,16 +29,23 @@ 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: consentData } = useQuery(GET_PHONE_NUMBER_CONSENT, { + variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone }, + fetchPolicy: "cache-and-network" + }); + const isConsented = consentData?.phone_number_consent?.[0]?.consent_status ?? false; 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 (!isConsented) return; 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,9 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi return (
+ {!isConsented && ( + + )} setMessage(e.target.value)} onPressEnter={(event) => { event.preventDefault(); - if (!!!event.shiftKey) handleEnter(); + if (!event.shiftKey && isConsented) handleEnter(); }} /> ({}); + +function PhoneNumberConsentList({ bodyshop }) { + const { t } = useTranslation(); + const [search, setSearch] = useState(""); + const { loading, data } = useQuery(GET_PHONE_NUMBER_CONSENTS, { + variables: { bodyshopid: bodyshop.id, search }, + fetchPolicy: "network-only" + }); + const [setConsent] = useMutation(SET_PHONE_NUMBER_CONSENT); + const [bulkSetConsent] = useMutation(BULK_SET_PHONE_NUMBER_CONSENT); + + const columns = [ + { + title: t("consent.phone_number"), + dataIndex: "phone_number", + render: (text) => {text} + }, + { + 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() + } + } + }) + } + /> + + ) + }, + { + title: t("consent.updated_at"), + dataIndex: "consent_updated_at", + render: (text) => {text} + } + ]; + + 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 ( +
+ 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..74975e02d --- /dev/null +++ b/client/src/components/shop-info/shop-info.consent.component.jsx @@ -0,0 +1,51 @@ +import { useMutation } from "@apollo/client"; +import { Switch, 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 { UPDATE_BODYSHOP_ENFORCE_CONSENT } from "../../graphql/bodyshop.queries"; +import PhoneNumberConsentList from "../phone-number-consent/phone-number-consent.component"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = () => ({}); + +function ShopInfoConsentComponent({ bodyshop }) { + const { t } = useTranslation(); + + const [updateEnforceConsent] = useMutation(UPDATE_BODYSHOP_ENFORCE_CONSENT); + + console.dir(bodyshop); + + const enforceConsent = bodyshop?.enforce_sms_consent ?? false; + + return ( +
+ {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 + } + } + }) + } + /> +
+ +
+ ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoConsentComponent); diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index 7faff13a2..af16899cf 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -142,6 +142,7 @@ 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 @@ -363,3 +364,12 @@ 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/graphql/consent.queries.js b/client/src/graphql/consent.queries.js new file mode 100644 index 000000000..8a3f78c8f --- /dev/null +++ b/client/src/graphql/consent.queries.js @@ -0,0 +1,90 @@ +import { gql } from "@apollo/client"; + +export const GET_PHONE_NUMBER_CONSENT = gql` + 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 + history(order_by: { changed_at: desc }, limit: 1) { + reason + } + } + } +`; + +export const GET_PHONE_NUMBER_CONSENTS = gql` + query GET_PHONE_NUMBER_CONSENTS($bodyshopid: uuid!, $phone_numbers: [String!]) { + phone_number_consent( + where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _in: $phone_numbers } } + order_by: { consent_updated_at: desc } + ) { + id + bodyshopid + phone_number + consent_status + 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 { + id + bodyshopid + phone_number + consent_status + consent_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/server/graphql-client/queries.js b/server/graphql-client/queries.js index d61d814b9..4ed9e5905 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2805,6 +2805,7 @@ exports.GET_BODYSHOP_BY_ID = ` intellipay_config state notification_followers + enforce_sms_consent } } `; @@ -2968,3 +2969,103 @@ exports.GET_JOB_WATCHERS_MINIMAL = ` } } `; + +// Query to get consent status for a 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 + history(order_by: { changed_at: desc }) { + id + old_value + new_value + reason + changed_at + changed_by + } + } + } +`; + +// 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 }) { + id + phone_number_consent_id + old_value + new_value + reason + changed_at + changed_by + } + } +`; + +// Mutation to set consent status +exports.SET_PHONE_NUMBER_CONSENT = ` + mutation SET_PHONE_NUMBER_CONSENT($bodyshopid: uuid!, $phone_number: String!, $consent_status: Boolean!, $reason: 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 + } + 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 +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 + consent_updated_at + } + } + } +`; diff --git a/server/sms/receive.js b/server/sms/receive.js index f880105e9..128fda2ff 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -3,7 +3,8 @@ const { FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, UNARCHIVE_CONVERSATION, CREATE_CONVERSATION, - INSERT_MESSAGE + INSERT_MESSAGE, + SET_PHONE_NUMBER_CONSENT } = require("../graphql-client/queries"); const { phone } = require("phone"); const { admin } = require("../firebase/firebase-handler"); @@ -91,7 +92,30 @@ const receive = async (req, res) => { const bodyshop = response.bodyshops[0]; - // Sort conversations by `updated_at` (or `created_at`) and pick the last one + // 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"; + + 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 3: Process conversation const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); const existingConversation = sortedConversations.length ? sortedConversations[sortedConversations.length - 1] @@ -104,14 +128,11 @@ const receive = async (req, res) => { image: !!req.body.MediaUrl0, image_path: generateMediaArray(req.body), 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,11 +140,10 @@ const receive = async (req, res) => { }); } } else { - // Create a new conversation const newConversationResponse = await client.request(CREATE_CONVERSATION, { conversation: { bodyshopid: bodyshop.id, - phone_num: phone(req.body.From).phoneNumber, + phone_num: normalizedPhone, archived: false } }); @@ -131,13 +151,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 4: Insert the message const insertresp = await client.request(INSERT_MESSAGE, { msg: newMessage, - conversationid: conversationid + conversationid }); const message = insertresp?.insert_messages?.returning?.[0]; @@ -147,8 +166,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 5: Notify clients const conversationRoom = getBodyshopConversationRoom({ bodyshopId: conversation.bodyshop.id, conversationId: conversation.id @@ -176,7 +194,7 @@ const receive = async (req, res) => { summary: false }); - // Step 5: Send FCM notification + // Step 6: Send FCM notification const fcmresp = await admin.messaging().send({ topic: `${message.conversation.bodyshop.imexshopid}-messaging`, notification: { From 7bd5190bf2f813bf5477b45297ee3b5108ba550f Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 20 May 2025 18:19:39 -0400 Subject: [PATCH 09/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../chat-affix/chat-affix.container.jsx | 75 +++-- .../chat-conversation-list.component.jsx | 44 ++- .../chat-send-message.component.jsx | 13 +- .../phone-number-consent.component.jsx | 300 +++++++++++++----- .../shop-info/shop-info.consent.component.jsx | 58 ++-- client/src/graphql/consent.queries.js | 72 +---- client/src/redux/user/user.actions.js | 5 + client/src/redux/user/user.reducer.js | 9 +- client/src/redux/user/user.types.js | 3 +- hasura/metadata/tables.yaml | 2 +- .../down.sql | 1 + .../up.sql | 1 + server/graphql-client/queries.js | 87 +++-- server/routes/smsRoutes.js | 4 +- server/sms/consent.js | 215 +++++++++++++ server/sms/receive.js | 150 +++++---- server/sms/send.js | 53 +++- 17 files changed, 772 insertions(+), 320 deletions(-) create mode 100644 hasura/migrations/1747775597734_alter_table_public_phone_number_consent_history_alter_column_old_value/down.sql create mode 100644 hasura/migrations/1747775597734_alter_table_public_phone_number_consent_history_alter_column_old_value/up.sql create mode 100644 server/sms/consent.js 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 { From 831802f5af7681bfe1607d1a59bd672201ea52e1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 20 May 2025 15:25:29 -0700 Subject: [PATCH 10/45] IO-3235 FeatureAccess on VisualBoard for SmartSchedule Option of Color Cards Signed-off-by: Allan Carr --- .../settings/LayoutSettings.jsx | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/client/src/components/production-board-kanban/settings/LayoutSettings.jsx b/client/src/components/production-board-kanban/settings/LayoutSettings.jsx index 888503df2..8155a4ace 100644 --- a/client/src/components/production-board-kanban/settings/LayoutSettings.jsx +++ b/client/src/components/production-board-kanban/settings/LayoutSettings.jsx @@ -1,7 +1,15 @@ import { Card, Col, Form, Radio, Row } from "antd"; import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../../redux/user/user.selectors"; +import { HasFeatureAccess } from "../../feature-wrapper/feature-wrapper.component"; -const LayoutSettings = ({ t }) => ( +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const LayoutSettings = ({ t, bodyshop }) => ( {[ @@ -30,14 +38,18 @@ const LayoutSettings = ({ t }) => ( { value: false, label: t("production.labels.wide") } ] }, - { - name: "cardcolor", - label: t("production.labels.cardcolor"), - options: [ - { value: true, label: t("production.labels.on") }, - { value: false, label: t("production.labels.off") } - ] - }, + ...(HasFeatureAccess({ bodyshop, featureName: "smartscheduling" }) + ? [ + { + name: "cardcolor", + label: t("production.labels.cardcolor"), + options: [ + { value: true, label: t("production.labels.on") }, + { value: false, label: t("production.labels.off") } + ] + } + ] + : []), { name: "kiosk", label: t("production.labels.kiosk_mode"), @@ -67,4 +79,4 @@ LayoutSettings.propTypes = { t: PropTypes.func.isRequired }; -export default LayoutSettings; +export default connect(mapStateToProps)(LayoutSettings); From 079dffce4d6c31abc6b36ef8244b114bec0ec209 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 20 May 2025 17:16:27 -0700 Subject: [PATCH 11/45] IO-3236 HasFeatureAccess Date Signed-off-by: Allan Carr --- .../components/feature-wrapper/feature-wrapper.component.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/components/feature-wrapper/feature-wrapper.component.jsx b/client/src/components/feature-wrapper/feature-wrapper.component.jsx index 7dabea2ac..507a05923 100644 --- a/client/src/components/feature-wrapper/feature-wrapper.component.jsx +++ b/client/src/components/feature-wrapper/feature-wrapper.component.jsx @@ -81,8 +81,9 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false } return ( bodyshop?.features?.allAccess || - bodyshop?.features?.[featureName] || - dayjs(bodyshop?.features[featureName]).isAfter(dayjs()) + (typeof bodyshop?.features?.[featureName] === "boolean" + ? bodyshop?.features?.[featureName] + : dayjs(bodyshop?.features?.[featureName]).isAfter(dayjs())) ); } From 8ee52598e80a1ca68b2961e6bf622fe30bb6bd7d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 21 May 2025 14:32:35 -0400 Subject: [PATCH 12/45] 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, From 8c8c68867dc603b0ff7964ec635307ae1395c945 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 21 May 2025 14:39:17 -0400 Subject: [PATCH 13/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- server/routes/smsRoutes.js | 1 + server/sms/receive.js | 2 +- server/sms/send.js | 4 ++-- server/sms/status.js | 1 - 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/routes/smsRoutes.js b/server/routes/smsRoutes.js index bb23d24e8..c09cc1632 100644 --- a/server/routes/smsRoutes.js +++ b/server/routes/smsRoutes.js @@ -7,6 +7,7 @@ const { status, markConversationRead } = require("../sms/status"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); // Twilio Webhook Middleware for production +// 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 ad539dbe4..bf5262b25 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -16,7 +16,6 @@ const InstanceManager = require("../utils/instanceMgr").default; * @returns {Promise<*>} */ const receive = async (req, res) => { - console.dir(req.body); const { logger, ioRedis, @@ -118,6 +117,7 @@ const receive = async (req, res) => { }; const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id); + ioRedis.to(broadcastRoom).emit("new-message-summary", { ...commonPayload, existingConversation: !!existingConversation, diff --git a/server/sms/send.js b/server/sms/send.js index aa3c5a84c..c5e897a98 100644 --- a/server/sms/send.js +++ b/server/sms/send.js @@ -5,7 +5,7 @@ const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY const gqlClient = require("../graphql-client/graphql-client").client; const send = async (req, res) => { - const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid, bodyshopid } = req.body; + const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body; const { ioRedis, logger, @@ -25,7 +25,7 @@ const send = async (req, res) => { image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [] }); - if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid || !bodyshopid) { + if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid) { logger.log("sms-outbound-error", "ERROR", req.user.email, null, { type: "missing-parameters", messagingServiceSid, diff --git a/server/sms/status.js b/server/sms/status.js index 385dbaa40..509c76d6b 100644 --- a/server/sms/status.js +++ b/server/sms/status.js @@ -9,7 +9,6 @@ const logger = require("../utils/logger"); * @returns {Promise<*>} */ const status = async (req, res) => { - console.dir(req.body); const { SmsSid, SmsStatus } = req.body; const { ioRedis, From 6afa50332b8be3ebc3a1b10568cdbc25e1efbe13 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 21 May 2025 15:03:02 -0400 Subject: [PATCH 14/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../chat-conversation-list.component.jsx | 83 +++++++++++++++---- .../chat-send-message.component.jsx | 16 ++-- .../phone-number-consent.component.jsx | 5 +- client/src/graphql/consent.queries.js | 48 ----------- .../graphql/phone-number-opt-out.queries.js | 28 +++++++ client/src/translations/en_us/common.json | 4 +- server/sms/send.js | 9 +- 7 files changed, 114 insertions(+), 79 deletions(-) delete mode 100644 client/src/graphql/consent.queries.js create mode 100644 client/src/graphql/phone-number-opt-out.queries.js 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 16d4c0bf1..70bfc5b5d 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, useMemo, useState } from "react"; import { connect } from "react-redux"; import { Virtuoso } from "react-virtuoso"; import { createStructuredSelector } from "reselect"; @@ -10,35 +10,63 @@ 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_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js"; +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 }) { - // That comma is there for a reason, do not remove it +function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) { + const { t } = useTranslation(); const [, forceUpdate] = useState(false); - // Re-render every minute + const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "")); + + const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, { + variables: { + bodyshopid: bodyshop.id, + phone_numbers: phoneNumbers + }, + skip: !conversationList.length, + fetchPolicy: "cache-and-network" + }); + + const optOutMap = useMemo(() => { + const map = new Map(); + optOutData?.phone_number_opt_out?.forEach((optOut) => { + map.set(optOut.phone_number, true); + }); + return map; + }, [optOutData?.phone_number_opt_out]); + useEffect(() => { const interval = setInterval(() => { - forceUpdate((prev) => !prev); // Toggle state to trigger re-render - }, 60000); // 1 minute in milliseconds - - return () => clearInterval(interval); // Cleanup on unmount + forceUpdate((prev) => !prev); + }, 60000); + return () => clearInterval(interval); }, []); - // Memoize the sorted conversation list - const sortedConversationList = React.useMemo(() => { + const sortedConversationList = useMemo(() => { return _.orderBy(conversationList, ["updated_at"], ["desc"]); }, [conversationList]); - const renderConversation = (index) => { + const renderConversation = (index, t) => { const item = sortedConversationList[index]; + const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""); + // Check if the phone number exists in the consentMap + const hasOptOutEntry = optOutMap.has(normalizedPhone); + // Only consider it non-consented if it exists and consent_status is false + const isOptedOut = hasOptOutEntry ? optOutMap.get(normalizedPhone) : true; + const cardContentRight = {item.updated_at}; const cardContentLeft = item.job_conversations.length > 0 @@ -60,7 +88,12 @@ function ChatConversationListComponent({ conversationList, selectedConversation, ); - const cardExtra = ; + const cardExtra = ( + <> + + {hasOptOutEntry && !isOptedOut && {t("messaging.labels.no_consent")}} + + ); const getCardStyle = () => item.id === selectedConversation @@ -73,9 +106,25 @@ function ChatConversationListComponent({ conversationList, selectedConversation, onClick={() => setSelectedConversation(item.id)} className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`} > - -
{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-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx index aa5b3f035..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 @@ -11,8 +11,8 @@ 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 { GET_PHONE_NUMBER_CONSENT } from "../../graphql/consent.queries"; import { phone } from "phone"; +import { GET_PHONE_NUMBER_OPT_OUT } from "../../graphql/phone-number-opt-out.queries"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -31,12 +31,12 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi const { t } = useTranslation(); const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, ""); - const { data: consentData } = useQuery(GET_PHONE_NUMBER_CONSENT, { + const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUT, { variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone }, fetchPolicy: "cache-and-network" }); - const isConsented = consentData?.phone_number_consent?.[0]?.consent_status ?? false; + const isOptedOut = !!optOutData?.phone_number_opt_out?.[0]; useEffect(() => { inputArea.current.focus(); @@ -45,7 +45,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi const handleEnter = () => { const selectedImages = selectedMedia.filter((i) => i.isSelected); if ((message === "" || !message) && selectedImages.length === 0) return; - if (!isConsented) return; + if (isOptedOut) return; // Prevent sending if phone number is opted out logImEXEvent("messaging_send_message"); if (selectedImages.length < 11) { @@ -69,7 +69,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi return (
- {!isConsented && } + {isOptedOut && } setMessage(e.target.value)} onPressEnter={(event) => { event.preventDefault(); - if (!event.shiftKey && isConsented) handleEnter(); + if (!event.shiftKey && !isOptedOut) handleEnter(); }} /> RO", "2tiersetup": "2 Tier Setup", "2tiersource": "Source => RO", @@ -3872,7 +3872,7 @@ "updated_at": "Last Updated" }, "settings": { - "title": "Phone Number Opt-Out list" + "title": "Phone Number Opt-Out List" } } } diff --git a/server/sms/send.js b/server/sms/send.js index c5e897a98..fdd2c81ae 100644 --- a/server/sms/send.js +++ b/server/sms/send.js @@ -4,13 +4,18 @@ 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; +/** + * 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 { ioRedis, logger, - ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }, - sessionUtils: { getBodyshopFromRedis } + ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } } = req; logger.log("sms-outbound", "DEBUG", req.user.email, null, { From 0541afceb8855511160fcc74944f3b42b4862f93 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 21 May 2025 15:17:11 -0400 Subject: [PATCH 15/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../chat-conversation-list.component.jsx | 5 +---- .../phone-number-consent.component.jsx | 9 +++++---- .../graphql/phone-number-opt-out.queries.js | 6 +++--- client/src/translations/en_us/common.json | 2 +- hasura/metadata/tables.yaml | 19 +++++++++++++++++++ 5 files changed, 29 insertions(+), 12 deletions(-) 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 70bfc5b5d..8bf185756 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 @@ -62,10 +62,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation, const renderConversation = (index, t) => { const item = sortedConversationList[index]; const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""); - // Check if the phone number exists in the consentMap const hasOptOutEntry = optOutMap.has(normalizedPhone); - // Only consider it non-consented if it exists and consent_status is false - const isOptedOut = hasOptOutEntry ? optOutMap.get(normalizedPhone) : true; const cardContentRight = {item.updated_at}; const cardContentLeft = @@ -91,7 +88,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation, const cardExtra = ( <> - {hasOptOutEntry && !isOptedOut && {t("messaging.labels.no_consent")}} + {hasOptOutEntry && {t("messaging.labels.no_consent")}} ); diff --git a/client/src/components/phone-number-consent/phone-number-consent.component.jsx b/client/src/components/phone-number-consent/phone-number-consent.component.jsx index fa0f810c0..020bb1750 100644 --- a/client/src/components/phone-number-consent/phone-number-consent.component.jsx +++ b/client/src/components/phone-number-consent/phone-number-consent.component.jsx @@ -39,9 +39,10 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) { sorter: (a, b) => a.phone_number.localeCompare(b.phone_number) }, { - title: t("consent.updated_at"), - dataIndex: "consent_updated_at", - render: (text) => {text} + title: t("consent.created_at"), + dataIndex: "created_at", + render: (text) => {text}, + sorter: (a, b) => new Date(a.created_at) - new Date(b.created_at) } ]; @@ -55,7 +56,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
Date: Wed, 21 May 2025 16:28:09 -0700 Subject: [PATCH 16/45] IO-3230 Customer List Excel Signed-off-by: Allan Carr --- .../report-center-modal.component.jsx | 8 +++++--- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + client/src/utils/TemplateConstants.js | 14 +++++++++++++- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index ddea0aab2..336124eaa 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -6,6 +6,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { QUERY_ACTIVE_EMPLOYEES, QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL } from "../../graphql/employees.queries"; import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries"; import { selectReportCenter } from "../../redux/modals/modals.selectors"; @@ -18,11 +19,10 @@ import EmployeeSearchSelectEmail from "../employee-search-select/employee-search import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component"; import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; +import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component"; import "./report-center-modal.styles.scss"; -import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component"; -import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; const mapStateToProps = createStructuredSelector({ reportCenterModal: selectReportCenter, @@ -389,5 +389,7 @@ const restrictedReports = [ { key: "job_costing_ro_date_detail", days: 183 }, { key: "job_costing_ro_estimator", days: 183 }, { key: "job_lifecycle_date_detail", days: 183 }, - { key: "job_lifecycle_date_summary", days: 183 } + { key: "job_lifecycle_date_summary", days: 183 }, + { key: "customer_list", days: 183 }, + { key: "customer_list_excel", days: 183 } ]; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index e753871c4..8ca3dadd3 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -3099,6 +3099,7 @@ "credits_not_received_date_vendorid": "Credits not Received by Vendor", "csi": "CSI Responses", "customer_list": "Customer List", + "customer_list_excel": "Customer List - Excel", "cycle_time_analysis": "Cycle Time Analysis", "estimates_written_converted": "Estimates Written/Converted", "estimator_detail": "Jobs by Estimator (Detail)", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index ad31f3cc6..909ce1936 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -3100,6 +3100,7 @@ "credits_not_received_date_vendorid": "", "csi": "", "customer_list": "", + "customer_list_excel": "", "cycle_time_analysis": "", "estimates_written_converted": "", "estimator_detail": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index e7ac5f8ac..68b8ad43e 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -3100,6 +3100,7 @@ "credits_not_received_date_vendorid": "", "csi": "", "customer_list": "", + "customer_list_excel": "", "cycle_time_analysis": "", "estimates_written_converted": "", "estimator_detail": "", diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index f9d53306c..867979a0c 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2004,6 +2004,18 @@ export const TemplateList = (type, context) => { }, group: "customers" }, + customer_list_excel: { + title: i18n.t("reportcenter.templates.customer_list_excel"), + subject: i18n.t("reportcenter.templates.customer_list_excel"), + key: "customer_list_excel", + reporttype: "excel", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_invoiced") + }, + group: "customers" + }, exported_gsr_by_ro: { title: i18n.t("reportcenter.templates.exported_gsr_by_ro"), subject: i18n.t("reportcenter.templates.exported_gsr_by_ro"), @@ -2241,7 +2253,7 @@ export const TemplateList = (type, context) => { field: i18n.t("bills.fields.date") }, group: "purchases" - }, + } } : {}), ...(!type || type === "courtesycarcontract" From e3d854e02ba7b3e609b13ea917de37150af20952 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 21 May 2025 17:53:33 -0700 Subject: [PATCH 17/45] IO-3243 Job Costing TOW Signed-off-by: Allan Carr --- server/graphql-client/queries.js | 2 ++ server/job/job-costing.js | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index d61d814b9..386e21a9f 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1596,6 +1596,7 @@ query QUERY_JOB_COSTING_DETAILS($id: uuid!) { ca_customer_gst dms_allocation cieca_pfl + cieca_stl materials joblines(where: { removed: { _eq: false } }) { id @@ -1712,6 +1713,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT ca_customer_gst dms_allocation cieca_pfl + cieca_stl materials joblines(where: {removed: {_eq: false}}) { id diff --git a/server/job/job-costing.js b/server/job/job-costing.js index 1210b92e9..29c9dbfce 100644 --- a/server/job/job-costing.js +++ b/server/job/job-costing.js @@ -567,6 +567,29 @@ function GenerateCostingData(job) { ); } + if (InstanceManager({ imex: false, rome: true })) { + const stlTowing = job.cieca_stl?.data.find((c) => c.ttl_type === "OTTW"); + const stlStorage = job.cieca_stl?.data.find((c) => c.ttl_type === "OTST"); + + if (!jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]]) + jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]] = Dinero(); + + jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]] = stlTowing + ? Dinero({ amount: Math.round(stlTowing.ttl_amt * 100) }) + : Dinero({ + amount: Math.round((job.towing_payable || 0) * 100) + }); + + if (!jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]]) + jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]] = Dinero(); + + jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]] = stlStorage + ? Dinero({ amount: Math.round(stlStorage.ttl_amt * 100) }) + : Dinero({ + amount: Math.round((job.storage_payable || 0) * 100) + }); + } + //Is it a DMS Setup? const selectedDmsAllocationConfig = (job.bodyshop.md_responsibility_centers.dms_defaults && From 5c47088b11c542c71e4c3c860f7c4cfdba361bd5 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 22 May 2025 11:37:47 -0400 Subject: [PATCH 18/45] release/2025-06-02 - Lint Updates --- .../phone-number-consent.component.jsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/client/src/components/phone-number-consent/phone-number-consent.component.jsx b/client/src/components/phone-number-consent/phone-number-consent.component.jsx index 020bb1750..7981a3db2 100644 --- a/client/src/components/phone-number-consent/phone-number-consent.component.jsx +++ b/client/src/components/phone-number-consent/phone-number-consent.component.jsx @@ -1,4 +1,4 @@ -import { useApolloClient, useQuery } from "@apollo/client"; +import { useQuery } from "@apollo/client"; import { Input, Table } from "antd"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -9,9 +9,6 @@ import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.qu import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import { TimeAgoFormatter } from "../../utils/DateFormatter"; -import { phone } from "phone"; -import { useSocket } from "../../contexts/SocketIO/useSocket.js"; -import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -23,13 +20,10 @@ const mapDispatchToProps = () => ({}); function PhoneNumberConsentList({ bodyshop, currentUser }) { const { t } = useTranslation(); const [search, setSearch] = useState(""); - const notification = useNotification(); - const { loading, data, refetch } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, { + const { loading, data } = 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 = [ { From 2c508cf1a167fbec293138d0ff68d2a7231a6ab7 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 22 May 2025 11:54:17 -0700 Subject: [PATCH 19/45] IO-3239 QBO Logging and integration log schema changes. --- hasura/metadata/tables.yaml | 3 + .../down.sql | 1 + .../up.sql | 18 +++ server/accounting/qbo/qbo-callback.js | 1 - server/accounting/qbo/qbo-payables.js | 52 ++++----- server/accounting/qbo/qbo-payments.js | 54 ++++----- server/accounting/qbo/qbo-receivables.js | 104 +++++++++++++++++- server/graphql-client/queries.js | 3 + server/utils/logger.js | 32 ++++-- 9 files changed, 202 insertions(+), 66 deletions(-) create mode 100644 hasura/migrations/1747932876194_create_table_public_integration_log/down.sql create mode 100644 hasura/migrations/1747932876194_create_table_public_integration_log/up.sql diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index c43b4e1dd..c10ac7604 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -2681,6 +2681,9 @@ - active: _eq: true allow_aggregations: true +- table: + name: integration_log + schema: public - table: name: inventory schema: public diff --git a/hasura/migrations/1747932876194_create_table_public_integration_log/down.sql b/hasura/migrations/1747932876194_create_table_public_integration_log/down.sql new file mode 100644 index 000000000..76dd504df --- /dev/null +++ b/hasura/migrations/1747932876194_create_table_public_integration_log/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."integration_log"; diff --git a/hasura/migrations/1747932876194_create_table_public_integration_log/up.sql b/hasura/migrations/1747932876194_create_table_public_integration_log/up.sql new file mode 100644 index 000000000..73baf287b --- /dev/null +++ b/hasura/migrations/1747932876194_create_table_public_integration_log/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "public"."integration_log" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid NOT NULL, "email" text NOT NULL, "jobid" uuid, "billid" uuid, "paymentid" uuid, "method" text NOT NULL, "name" text NOT NULL, "status" text NOT NULL, "platform" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("email") REFERENCES "public"."users"("email") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("billid") REFERENCES "public"."bills"("id") ON UPDATE restrict ON DELETE set null, FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("paymentid") REFERENCES "public"."payments"("id") ON UPDATE restrict ON DELETE set null); +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_integration_log_updated_at" +BEFORE UPDATE ON "public"."integration_log" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_public_integration_log_updated_at" ON "public"."integration_log" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/server/accounting/qbo/qbo-callback.js b/server/accounting/qbo/qbo-callback.js index 3045dbcc8..7dfa6adfa 100644 --- a/server/accounting/qbo/qbo-callback.js +++ b/server/accounting/qbo/qbo-callback.js @@ -16,7 +16,6 @@ const oauthClient = new OAuthClient({ redirectUri: process.env.QBO_REDIRECT_URI, }); -//TODO:AIO Add in QBO callbacks. const url = InstanceEndpoints(); exports.default = async (req, res) => { diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 0e234c0e8..9ed0670de 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -150,11 +150,11 @@ async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) { }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "QueryVendorRecord", + method: "POST", + name: "QueryVendorRecord", billid: bill.id, - statusCode: result.status, - bodyshopid: req.user.bodyshopid, + status: result.response?.status, + bodyshopid: bill.job.shopid, email: req.user.email }) setNewRefreshToken(req.user.email, result); @@ -188,11 +188,11 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) { }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "InsertVendorRecord", + method: "POST", + name: "InsertVendorRecord", billid: bill.id, - statusCode: result.status, - bodyshopid: req.user.bodyshopid, + status: result.response?.status, + bodyshopid: bill.job.shopid, email: req.user.email }) setNewRefreshToken(req.user.email, result); @@ -207,7 +207,7 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) { } async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) { - const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req); + const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, bill.job.shopid); const lines = bill.billlines.map((il) => generateBillLine( @@ -299,11 +299,11 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "InsertBill", + method: "POST", + name: "InsertBill", billid: bill.id, - statusCode: result.status, - bodyshopid: req.user.bodyshopid, + status: result.response?.status, + bodyshopid: bill.job.shopid, email: req.user.email }) setNewRefreshToken(req.user.email, result); @@ -368,7 +368,7 @@ const generateBillLine = ( }; }; -async function QueryMetaData(oauthClient, qbo_realmId, req) { +async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) { const accounts = await oauthClient.makeApiCall({ url: urlBuilder( qbo_realmId, @@ -382,10 +382,10 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "QueryAccountType", - statusCode: accounts.status, - bodyshopid: req.user.bodyshopid, + method: "POST", + name: "QueryAccountType", + status: accounts.response?.status, + bodyshopid, email: req.user.email }) setNewRefreshToken(req.user.email, accounts); @@ -398,10 +398,10 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "QueryTaxCode", - statusCode: taxCodes.status, - bodyshopid: req.user.bodyshopid, + method: "POST", + name: "QueryTaxCode", + status: taxCodes.status, + bodyshopid, email: req.user.email }) const classes = await oauthClient.makeApiCall({ @@ -413,10 +413,10 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "QueryClasses", - statusCode: classes.status, - bodyshopid: req.user.bodyshopid, + method: "POST", + name: "QueryClasses", + status: classes.status, + bodyshopid, email: req.user.email }) const taxCodeMapping = {}; diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js index 1b54ac8c0..f0d4ccdde 100644 --- a/server/accounting/qbo/qbo-payments.js +++ b/server/accounting/qbo/qbo-payments.js @@ -197,7 +197,8 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef, req, payment.job.ro_number, false, - parentRef + parentRef, + payment.job.shopid ); if (invoices && invoices.length !== 1) { @@ -256,11 +257,11 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef, }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "InsertPayment", + method: "POST", + name: "InsertPayment", paymentid: payment.id, - statusCode: result.status, - bodyshopid: req.user.bodyshopid, + status: result.response?.status, + bodyshopid: payment.job.shopid, email: req.user.email }) setNewRefreshToken(req.user.email, result); @@ -274,7 +275,7 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef, } } -async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditMemo, parentTierRef) { +async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditMemo, parentTierRef, bodyshopid) { const invoice = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From Invoice where DocNumber like '${ro_number}%'`), method: "POST", @@ -284,11 +285,11 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "QueryInvoice", + method: "POST", + name: "QueryInvoice", - statusCode: invoice.status, - bodyshopid: req.user.bodyshopid, + status: invoice.response?.status, + bodyshopid, email: req.user.email }) const paymentMethods = await oauthClient.makeApiCall({ @@ -300,10 +301,10 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "QueryPaymentMethod", - statusCode: paymentMethods.status, - bodyshopid: req.user.bodyshopid, + method: "POST", + name: "QueryPaymentMethod", + status: paymentMethods.response?.status, + bodyshopid, email: req.user.email }) setNewRefreshToken(req.user.email, paymentMethods); @@ -351,11 +352,11 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "QueryTaxCode", + method: "POST", + name: "QueryTaxCode", - statusCode: taxCodes.status, - bodyshopid: req.user.bodyshopid, + status: taxCodes.response?.status, + bodyshopid, email: req.user.email }) const items = await oauthClient.makeApiCall({ @@ -367,10 +368,10 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "QueryItems", - statusCode: items.status, - bodyshopid: req.user.bodyshopid, + method: "POST", + name: "QueryItems", + status: items.response?.status, + bodyshopid, email: req.user.email }) setNewRefreshToken(req.user.email, items); @@ -418,7 +419,8 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe req, payment.job.ro_number, true, - parentRef + parentRef, + payment.job.shopid ); if (invoices && invoices.length !== 1) { @@ -475,10 +477,10 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe }); logger.LogIntegrationCall({ platform: "QBO", - methodType: "POST", - methodName: "InsertCreditMemo", + method: "POST", + name: "InsertCreditMemo", paymentid: payment.id, - statusCode: result.status, + status: result.response?.status, bodyshopid: req.user.bodyshopid, email: req.user.email }) diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index 881a6fae5..e2a0fa307 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -233,6 +233,15 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) { "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "QueryCustomer", + status: result.response?.status, + bodyshopid: job.shopid, + jobid: job.id, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return ( result.json && @@ -279,6 +288,15 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) { }, body: JSON.stringify(Customer) }); + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "InsertCustomer", + status: result.response.status, + bodyshopid: job.shopid, + jobid: job.id, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return result && result.json.Customer; } catch (error) { @@ -305,6 +323,15 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job, isThreeTier, paren "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "QueryCustomer", + status: result.response?.status, + bodyshopid: job.shopid, + jobid: job.id, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return ( result.json && @@ -347,6 +374,15 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare }, body: JSON.stringify(Customer) }); + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "InsertCustomer", + status: result.response?.status, + bodyshopid: job.shopid, + jobid: job.id, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return result && result.json.Customer; } catch (error) { @@ -372,6 +408,15 @@ async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) { "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "QueryCustomer", + status: result.response?.status, + bodyshopid: job.shopid, + jobid: job.id, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return ( result.json && @@ -411,6 +456,15 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { }, body: JSON.stringify(Customer) }); + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "InsertCustomer", + status: result.response?.status, + bodyshopid: job.shopid, + jobid: job.id, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return result && result.json.Customer; } catch (error) { @@ -424,7 +478,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { exports.InsertJob = InsertJob; -async function QueryMetaData(oauthClient, qbo_realmId, req) { +async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) { const items = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From Item where active=true maxresults 1000`), method: "POST", @@ -432,6 +486,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { "Content-Type": "application/json" } }); + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "QueryItems", + status: items.response?.status, + bodyshopid, + email: req.user.email + }) setNewRefreshToken(req.user.email, items); const taxCodes = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From TaxCode where active=true`), @@ -440,7 +502,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { "Content-Type": "application/json" } }); - + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "QueryTaxCodes", + status: taxCodes.response?.status, + bodyshopid, + email: req.user.email + }) const classes = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From Class`), method: "POST", @@ -448,7 +517,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { "Content-Type": "application/json" } }); - + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "QueryClasses", + status: classes.response?.status, + bodyshopid, + email: req.user.email + }) const taxCodeMapping = {}; taxCodes.json && @@ -483,7 +559,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) { } async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, parentTierRef) { - const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req); + const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, job.shopid); const InvoiceLineAdd = CreateInvoiceLines({ bodyshop, jobs_by_pk: job, @@ -573,6 +649,15 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren }, body: JSON.stringify(invoiceObj) }); + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "InsertInvoice", + status: result.status, + bodyshopid: job.shopid, + jobid: job.id, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return result && result.json && result.json.Invoice; } catch (error) { @@ -596,7 +681,7 @@ async function InsertInvoiceMultiPayerInvoice( payer, suffix ) { - const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req); + const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, job.shopid); const InvoiceLineAdd = createMultiQbPayerLines({ bodyshop, jobs_by_pk: job, @@ -689,6 +774,15 @@ async function InsertInvoiceMultiPayerInvoice( }, body: JSON.stringify(invoiceObj) }); + logger.LogIntegrationCall({ + platform: "QBO", + method: "POST", + name: "InsertInvoice", + status: result.response.status, + bodyshopid: job.shopid, + jobid: job.id, + email: req.user.email + }) setNewRefreshToken(req.user.email, result); return result && result.json && result.json.Invoice; } catch (error) { diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index c99a5d67b..ccaf566c7 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -222,6 +222,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) { rate_mash rate_matd class + shopid ca_bc_pvrt ca_customer_gst towing_payable @@ -480,6 +481,7 @@ query QUERY_BILLS_FOR_PAYABLES_EXPORT($bills: [uuid!]!) { ownr_ln ownr_co_nm class + shopid } billlines{ id @@ -530,6 +532,7 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = ` ownr_fn ownr_ln ownr_co_nm + shopid bodyshop { accountingconfig md_responsibility_centers diff --git a/server/utils/logger.js b/server/utils/logger.js index 560a77b8b..41d71cea6 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -195,21 +195,37 @@ const createLogger = () => { winstonLogger.log(logEntry); }; - const LogIntegrationCall = async ({ platform, methodType, methodName, jobid, paymentid, billid, statusCode, bodyshopid, email }) => { + const LogIntegrationCall = async ({ platform, method, name, jobid, paymentid, billid, status, bodyshopid, email }) => { try { //Insert the record. await client.request(queries.INSERT_INTEGRATION_LOG, { - platform, - methodType, - methodName, jobid, paymentid, billid, - statusCode, - bodyshopid, - email + log: { + platform, + method, + name, + jobid, + paymentid, + billid, + status: status.toString() ?? "0", + bodyshopid, + email + } }); } catch (error) { + console.trace("Stack", error?.stack); log("integration-log-error", "ERROR", email, null, { - error + message: error?.message, + stack: error?.stack, + platform, + method, + name, + jobid, + paymentid, + billid, + status, + bodyshopid, + email }); } }; From 2d764921ff941f22877ae1d22d0a4cdb970816f2 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 23 May 2025 10:43:15 -0700 Subject: [PATCH 20/45] IO-3075 Crisp Basic Info Signed-off-by: Allan Carr --- client/src/redux/user/user.sagas.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js index 0bb9dc664..79e3926f7 100644 --- a/client/src/redux/user/user.sagas.js +++ b/client/src/redux/user/user.sagas.js @@ -335,20 +335,12 @@ export function* SetAuthLevelFromShopDetails({ payload }) { } try { - InstanceRenderManager({ - executeFunction: true, - args: [], - imex: () => { - window.$crisp.push(["set", "user:company", [payload.shopname]]); - window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]); - if (authRecord[0] && authRecord[0].user.validemail) { - window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]); - } - }, - rome: () => { - window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname }); - } - }); + window.$crisp.push(["set", "user:company", [payload.shopname]]); + window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]); + if (authRecord[0] && authRecord[0].user.validemail) { + window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]); + } + payload.features?.allAccess === true ? window.$crisp.push(["set", "session:segments", [["allAccess"]]]) : (() => { @@ -359,6 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) { ); window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]); })(); + + InstanceRenderManager({ + executeFunction: true, + args: [], + rome: () => { + window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname }); + } + }); } catch (error) { console.warn("Couldnt find $crisp.", error.message); } From 99cf95daf0d672b9bd9b267cbce65b030b8adf57 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 23 May 2025 16:45:24 -0700 Subject: [PATCH 21/45] IO-3246 Remote Assist Signed-off-by: Allan Carr --- .../components/header/header.component.jsx | 43 +++++++++++++------ client/src/translations/en_us/common.json | 4 +- client/src/translations/es/common.json | 5 ++- client/src/translations/fr/common.json | 5 ++- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index a13c258e1..d65f78496 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -15,6 +15,7 @@ import { HomeFilled, ImportOutlined, LineChartOutlined, + OneToOneOutlined, PaperClipOutlined, PhoneOutlined, PlusCircleOutlined, @@ -24,6 +25,7 @@ import { TeamOutlined, ToolFilled, UnorderedListOutlined, + UsergroupAddOutlined, UserOutlined } from "@ant-design/icons"; import { useQuery } from "@apollo/client"; @@ -40,6 +42,7 @@ import { RiSurveyLine } from "react-icons/ri"; import { connect } from "react-redux"; import { Link } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js"; import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; @@ -47,11 +50,10 @@ import { signOutStart } from "../../redux/user/user.actions"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import day from "../../utils/day.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import { useIsEmployee } from "../../utils/useIsEmployee.js"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import LockWrapper from "../lock-wrapper/lock-wrapper.component"; import NotificationCenterContainer from "../notification-center/notification-center.container.jsx"; -import { useSocket } from "../../contexts/SocketIO/useSocket.js"; -import { useIsEmployee } from "../../utils/useIsEmployee.js"; // Redux mappings const mapStateToProps = createStructuredSelector({ @@ -642,17 +644,32 @@ function Header({ label: t("menus.header.help"), onClick: () => window.open("https://help.imex.online/", "_blank") }, - ...(InstanceRenderManager({ imex: true, rome: false }) - ? [ - { - key: "rescue", - id: "header-rescue", - icon: , - label: t("menus.header.rescueme"), - onClick: () => window.open("https://imexrescue.com/", "_blank") - } - ] - : []), + { + key: "remoteassist", + id: "header-remote-assist", + icon: , + label: t("menus.header.remoteassist"), + children: [ + ...(InstanceRenderManager({ imex: true, rome: true }) + ? [ + { + key: "rescue", + id: "header-rescue", + icon: , + label: t("menus.header.rescueme"), + onClick: () => window.open("https://imexrescue.com/", "_blank") + } + ] + : []), + { + key: "rescue", + id: "header-rescue-zoho", + icon: , + label: t("menus.header.rescuemezoho"), + onClick: () => window.open("https://join.zoho.com/", "_blank") + } + ] + }, { key: "shiftclock", id: "header-shiftclock", diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 7d580b05e..a5b724154 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2302,8 +2302,10 @@ "productionlist": "Production Board - List", "readyjobs": "Ready Jobs", "recent": "Recent Items", + "remoteassist": "Remote Assist", "reportcenter": "Report Center", - "rescueme": "Rescue me!", + "rescueme": "Rescue Me!", + "rescuemezoho": "Remote Me In!", "schedule": "Schedule", "scoreboard": "Scoreboard", "search": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 909ce1936..7651eae4b 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2301,8 +2301,10 @@ "productionlist": "", "readyjobs": "", "recent": "", + "remoteassist": "", "reportcenter": "", "rescueme": "", + "rescuemezoho": "", "schedule": "Programar", "scoreboard": "", "search": { @@ -2498,7 +2500,8 @@ }, "tooltips": { "job-watchers": "", - "not-employee": "" + "not-employee": "", + "not-employee-notifications": "" } }, "owner": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 68b8ad43e..316effe11 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2301,8 +2301,10 @@ "productionlist": "", "readyjobs": "", "recent": "", + "remoteassist": "", "reportcenter": "", "rescueme": "", + "rescuemezoho": "", "schedule": "Programme", "scoreboard": "", "search": { @@ -2498,7 +2500,8 @@ }, "tooltips": { "job-watchers": "", - "not-employee": "" + "not-employee": "", + "not-employee-notifications": "" } }, "owner": { From 0135281bcd586f3b4a5e75fea8489690759ab4cd Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Sat, 24 May 2025 13:15:53 -0400 Subject: [PATCH 22/45] release/2025-06-02 - test push --- server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server.js b/server.js index 18f286918..5484a12be 100644 --- a/server.js +++ b/server.js @@ -282,6 +282,7 @@ const applySocketIO = async ({ server, app }) => { logger.log("Redis connections closed.", "INFO", "redis", "api"); }); + // IO Redis const ioRedis = new Server(server, { path: "/wss", adapter: createAdapter(pubClient, subClient), From 49ae107fdea5142ed25846b0def169830e944b22 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Sat, 24 May 2025 13:23:38 -0400 Subject: [PATCH 23/45] release/2025-06-02 - add phone --- client/package-lock.json | 10 ++++++++++ client/package.json | 1 + 2 files changed, 11 insertions(+) diff --git a/client/package-lock.json b/client/package-lock.json index 12ba5dfb3..0c47083c8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -48,6 +48,7 @@ "memoize-one": "^6.0.0", "normalize-url": "^8.0.1", "object-hash": "^3.0.0", + "phone": "^3.1.59", "prop-types": "^15.8.1", "query-string": "^9.2.0", "raf-schd": "^4.0.3", @@ -13266,6 +13267,15 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT" }, + "node_modules/phone": { + "version": "3.1.59", + "resolved": "https://registry.npmjs.org/phone/-/phone-3.1.59.tgz", + "integrity": "sha512-CUv22jw0Zgrb/h7v3sEd262zJXS/66h7zyCCRIynx+2FswAJuuFsXsJkIxMUT4UcosKxDx1bJwdZeGnDELLsCw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/client/package.json b/client/package.json index 40c52b409..f78b924ca 100644 --- a/client/package.json +++ b/client/package.json @@ -47,6 +47,7 @@ "memoize-one": "^6.0.0", "normalize-url": "^8.0.1", "object-hash": "^3.0.0", + "phone": "^3.1.59", "prop-types": "^15.8.1", "query-string": "^9.2.0", "raf-schd": "^4.0.3", From 67d5dcb062328ec95202f84d01e08f8ad2d3a82f Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 26 May 2025 13:09:07 -0400 Subject: [PATCH 24/45] feature/IO-3182-Phone-Number-Consent - Up Deps --- client/package-lock.json | 56 ++++---- client/package.json | 10 +- package-lock.json | 287 ++++++++++++++++++++------------------- package.json | 20 +-- 4 files changed, 191 insertions(+), 182 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 0c47083c8..fd218cc50 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,8 +15,8 @@ "@fingerprintjs/fingerprintjs": "^4.6.1", "@firebase/analytics": "^0.10.16", "@firebase/app": "^0.13.0", - "@firebase/auth": "^1.10.5", - "@firebase/firestore": "^4.7.15", + "@firebase/auth": "^1.10.6", + "@firebase/firestore": "^4.7.16", "@firebase/messaging": "^0.12.21", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", @@ -25,7 +25,7 @@ "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", - "antd": "^5.25.2", + "antd": "^5.25.3", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^4.3.0", "autosize": "^6.0.1", @@ -60,7 +60,7 @@ "react-drag-listview": "^2.0.0", "react-grid-gallery": "^1.0.1", "react-grid-layout": "1.3.4", - "react-i18next": "^15.4.1", + "react-i18next": "^15.5.2", "react-icons": "^5.5.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", @@ -100,7 +100,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^4.5.0", "browserslist": "^4.24.5", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.4.1", @@ -2967,9 +2967,9 @@ } }, "node_modules/@firebase/auth": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.5.tgz", - "integrity": "sha512-6wF/NdMTwObL4RNQePunuzMr9O3gyftisvFZFFKf57D2HONXo87YymogRV8d+Z7SLA0rcNBN1gLJVk2D0y97gA==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.6.tgz", + "integrity": "sha512-cFbo2FymQltog4atI9cKTO6CxKxS0dOMXslTQrlNZRH7qhDG44/d7QeI6GXLweFZtrnlecf52ESnNz1DU6ek8w==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.17", @@ -3004,9 +3004,9 @@ } }, "node_modules/@firebase/firestore": { - "version": "4.7.15", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.15.tgz", - "integrity": "sha512-FgWTmkNBEXdKCoN2ngBNjrMaXuBx6QwjiZZVnOGg+VjUmiBq5gAqlDIW5bZY6i/NYvLUrWugdqIs7y9GHEqwww==", + "version": "4.7.16", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.16.tgz", + "integrity": "sha512-5OpvlwYVUTLEnqewOlXmtIpH8t2ISlZHDW0NDbKROM2D0ATMqFkMHdvl+/wz9zOAcb8GMQYlhCihOnVAliUbpQ==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.17", @@ -3886,6 +3886,13 @@ "react": ">=16.8.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", + "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -5794,15 +5801,16 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", + "integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -6092,12 +6100,12 @@ } }, "node_modules/antd": { - "version": "5.25.2", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.25.2.tgz", - "integrity": "sha512-7R2nUvlHhey7Trx64+hCtGXOiy+DTUs1Lv5bwbV1LzEIZIhWb0at1AM6V3K108a5lyoR9n7DX3ptlLF7uYV/DQ==", + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.25.3.tgz", + "integrity": "sha512-tBBcAFRjmWM3sitxrL/FEbQL+MTQntYY5bGa5c1ZZZHXWCynkhS3Ch/gy25mGMUY1M/9Uw3pH029v/RGht1x3w==", "license": "MIT", "dependencies": { - "@ant-design/colors": "^7.2.0", + "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", @@ -6157,9 +6165,9 @@ } }, "node_modules/antd/node_modules/@ant-design/colors": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz", - "integrity": "sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", "license": "MIT", "dependencies": { "@ant-design/fast-color": "^2.0.6" @@ -14413,9 +14421,9 @@ } }, "node_modules/react-i18next": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", - "integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.2.tgz", + "integrity": "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", diff --git a/client/package.json b/client/package.json index f78b924ca..729aee7e2 100644 --- a/client/package.json +++ b/client/package.json @@ -14,8 +14,8 @@ "@fingerprintjs/fingerprintjs": "^4.6.1", "@firebase/analytics": "^0.10.16", "@firebase/app": "^0.13.0", - "@firebase/auth": "^1.10.5", - "@firebase/firestore": "^4.7.15", + "@firebase/auth": "^1.10.6", + "@firebase/firestore": "^4.7.16", "@firebase/messaging": "^0.12.21", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", @@ -24,7 +24,7 @@ "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", - "antd": "^5.25.2", + "antd": "^5.25.3", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^4.3.0", "autosize": "^6.0.1", @@ -59,7 +59,7 @@ "react-drag-listview": "^2.0.0", "react-grid-gallery": "^1.0.1", "react-grid-layout": "1.3.4", - "react-i18next": "^15.4.1", + "react-i18next": "^15.5.2", "react-icons": "^5.5.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", @@ -140,7 +140,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^4.5.0", "browserslist": "^4.24.5", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.4.1", diff --git a/package-lock.json b/package-lock.json index 0ee5c9545..502a5d8ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "0.2.0", "license": "UNLICENSED", "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.812.0", - "@aws-sdk/client-elasticache": "^3.812.0", - "@aws-sdk/client-s3": "^3.812.0", - "@aws-sdk/client-secrets-manager": "^3.812.0", - "@aws-sdk/client-ses": "^3.812.0", - "@aws-sdk/credential-provider-node": "^3.812.0", - "@aws-sdk/lib-storage": "^3.812.0", - "@aws-sdk/s3-request-presigner": "^3.812.0", + "@aws-sdk/client-cloudwatch-logs": "^3.817.0", + "@aws-sdk/client-elasticache": "^3.817.0", + "@aws-sdk/client-s3": "^3.817.0", + "@aws-sdk/client-secrets-manager": "^3.817.0", + "@aws-sdk/client-ses": "^3.817.0", + "@aws-sdk/credential-provider-node": "^3.817.0", + "@aws-sdk/lib-storage": "^3.817.0", + "@aws-sdk/s3-request-presigner": "^3.817.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -31,7 +31,7 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.5", "crisp-status-reporter": "^1.2.2", - "dd-trace": "^5.52.0", + "dd-trace": "^5.53.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -45,7 +45,7 @@ "juice": "^11.0.1", "lodash": "^4.17.21", "moment": "^2.30.1", - "moment-timezone": "^0.5.48", + "moment-timezone": "^0.6.0", "multer": "^1.4.5-lts.1", "node-persist": "^4.0.4", "nodemailer": "^6.10.0", @@ -284,24 +284,24 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.812.0.tgz", - "integrity": "sha512-SLvqaMwRviAwb+z4XAq2QmlbUjr7rXN6zAEr4/x2ltyrsxEV95gBo0KHeroAsWhd4eD19USjAgg64KJgvUtNGw==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.817.0.tgz", + "integrity": "sha512-dbR4YZZ2wulMzblgSSE43yd9jgbXDMSrZS7w7r0DqDNAbsXrp79qU2CvA+lb47wGpDxMNppgvoCMu5kcIP5gXw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.812.0", - "@aws-sdk/credential-provider-node": "3.812.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/credential-provider-node": "3.817.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.812.0", + "@aws-sdk/middleware-user-agent": "3.816.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.812.0", + "@aws-sdk/util-user-agent-node": "3.816.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/eventstream-serde-browser": "^4.0.2", @@ -352,24 +352,24 @@ } }, "node_modules/@aws-sdk/client-elasticache": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.812.0.tgz", - "integrity": "sha512-o1KC5Glo3c0T/RN2XBanHu40k3M99MJyq+e/02tIMgEGKIPmnvB8A8muE2F3rQ2A0qLCxvjhm+kprlmDwzpryw==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.817.0.tgz", + "integrity": "sha512-TO1Zfv3racKQsRoll4owV2q4kNcw1x64D19KFrWd87rQ517ahbXRcPpaKOqe9CYG1Zo3SIzRySaJLoPftXDfRQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.812.0", - "@aws-sdk/credential-provider-node": "3.812.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/credential-provider-node": "3.817.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.812.0", + "@aws-sdk/middleware-user-agent": "3.816.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.812.0", + "@aws-sdk/util-user-agent-node": "3.816.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -403,32 +403,32 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.812.0.tgz", - "integrity": "sha512-kHgw9JDXNPLa/mHtWpOd5btBVXFSe+wwp1Ed9+bqz9uLkv0iV4joZrdQwnydkO8zlTs60Sc5ez+P2OiZ76i2Qg==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.817.0.tgz", + "integrity": "sha512-nZyjhlLMEXDs0ofWbpikI8tKoeKuuSgYcIb6eEZJk90Nt5HkkXn6nkWOs/kp2FdhpoGJyTILOVsDgdm7eutnLA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.812.0", - "@aws-sdk/credential-provider-node": "3.812.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/credential-provider-node": "3.817.0", "@aws-sdk/middleware-bucket-endpoint": "3.808.0", "@aws-sdk/middleware-expect-continue": "3.804.0", - "@aws-sdk/middleware-flexible-checksums": "3.812.0", + "@aws-sdk/middleware-flexible-checksums": "3.816.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-location-constraint": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-sdk-s3": "3.812.0", + "@aws-sdk/middleware-sdk-s3": "3.816.0", "@aws-sdk/middleware-ssec": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.812.0", + "@aws-sdk/middleware-user-agent": "3.816.0", "@aws-sdk/region-config-resolver": "3.808.0", - "@aws-sdk/signature-v4-multi-region": "3.812.0", + "@aws-sdk/signature-v4-multi-region": "3.816.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.812.0", + "@aws-sdk/util-user-agent-node": "3.816.0", "@aws-sdk/xml-builder": "3.804.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", @@ -470,24 +470,24 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.812.0.tgz", - "integrity": "sha512-RyGzi7kkacjPd0QgVjw6OYvZVvuqtd1wRwG0Aek32dPUYu8eOs9FDaqBsDnNIqdw+lAqC/pKIOPYWtLu2OxE0Q==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.817.0.tgz", + "integrity": "sha512-Hx74xmJo9xPeHRFtFGdsT5qFx6p9V13ptQ3HICnkmcbtA+CX8soTuc5mglkp9vTdTjvRwKVAmQhx6NPf9ELcjQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.812.0", - "@aws-sdk/credential-provider-node": "3.812.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/credential-provider-node": "3.817.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.812.0", + "@aws-sdk/middleware-user-agent": "3.816.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.812.0", + "@aws-sdk/util-user-agent-node": "3.816.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -535,24 +535,24 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.812.0.tgz", - "integrity": "sha512-7JUS2u0AKMYiEmRrxAYQj8ifFwVUgMAHt5H/KjMhh+1El0NqAQDt3JLD4Asmzy7/TvTAWZfk5np2LQPNB2wZpw==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.817.0.tgz", + "integrity": "sha512-cf2FsdcTT5HiOFOnWk3tzRc84iXcrUNXe4O4KaH75tRToBuQkTaidPI/K9wHnOybNDEkkCcgJo9skv4ftz8qYA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.812.0", - "@aws-sdk/credential-provider-node": "3.812.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/credential-provider-node": "3.817.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.812.0", + "@aws-sdk/middleware-user-agent": "3.816.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.812.0", + "@aws-sdk/util-user-agent-node": "3.816.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -586,23 +586,23 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.812.0.tgz", - "integrity": "sha512-O//smQRj1+RXELB7xX54s5pZB0V69KHXpUZmz8V+8GAYO1FKTHfbpUgK+zyMNb+lFZxG9B69yl8pWPZ/K8bvxA==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.817.0.tgz", + "integrity": "sha512-fCh5rUHmWmWDvw70NNoWpE5+BRdtNi45kDnIoeoszqVg7UKF79SlG+qYooUT52HKCgDNHqgbWaXxMOSqd2I/OQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.812.0", + "@aws-sdk/core": "3.816.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.812.0", + "@aws-sdk/middleware-user-agent": "3.816.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.812.0", + "@aws-sdk/util-user-agent-node": "3.816.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -635,9 +635,9 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.812.0.tgz", - "integrity": "sha512-myWA9oHMBVDObKrxG+puAkIGs8igcWInQ1PWCRTS/zN4BkhUMFjjh/JPV/4Vzvtvj5E36iujq2WtlrDLl1PpOw==", + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.816.0.tgz", + "integrity": "sha512-Lx50wjtyarzKpMFV6V+gjbSZDgsA/71iyifbClGUSiNPoIQ4OCV0KVOmAAj7mQRVvGJqUMWKVM+WzK79CjbjWA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.804.0", @@ -657,12 +657,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.812.0.tgz", - "integrity": "sha512-Ge7IEu06ANurGBZx39q9CNN/ncqb1K8lpKZCY969uNWO0/7YPhnplrRJGMZYIS35nD2mBm3ortEKjY/wMZZd5g==", + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.816.0.tgz", + "integrity": "sha512-wUJZwRLe+SxPxRV9AENYBLrJZRrNIo+fva7ZzejsC83iz7hdfq6Rv6B/aHEdPwG/nQC4+q7UUvcRPlomyrpsBA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.812.0", + "@aws-sdk/core": "3.816.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -673,12 +673,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.812.0.tgz", - "integrity": "sha512-Vux2U42vPGXeE407Lp6v3yVA65J7hBO9rB67LXshyGVi7VZLAYWc4mrZxNJNqabEkjcDEmMQQakLPT6zc5SvFw==", + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.816.0.tgz", + "integrity": "sha512-gcWGzMQ7yRIF+ljTkR8Vzp7727UY6cmeaPrFQrvcFB8PhOqWpf7g0JsgOf5BSaP8CkkSQcTQHc0C5ZYAzUFwPg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.812.0", + "@aws-sdk/core": "3.816.0", "@aws-sdk/types": "3.804.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", @@ -694,18 +694,18 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.812.0.tgz", - "integrity": "sha512-oltqGvQ488xtPY5wrNjbD+qQYYkuCjn30IDE1qKMxJ58EM6UVTQl3XV44Xq07xfF5gKwVJQkfIyOkRAguOVybg==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.817.0.tgz", + "integrity": "sha512-kyEwbQyuXE+phWVzloMdkFv6qM6NOon+asMXY5W0fhDKwBz9zQLObDRWBrvQX9lmqq8BbDL1sCfZjOh82Y+RFw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.812.0", - "@aws-sdk/credential-provider-env": "3.812.0", - "@aws-sdk/credential-provider-http": "3.812.0", - "@aws-sdk/credential-provider-process": "3.812.0", - "@aws-sdk/credential-provider-sso": "3.812.0", - "@aws-sdk/credential-provider-web-identity": "3.812.0", - "@aws-sdk/nested-clients": "3.812.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/credential-provider-env": "3.816.0", + "@aws-sdk/credential-provider-http": "3.816.0", + "@aws-sdk/credential-provider-process": "3.816.0", + "@aws-sdk/credential-provider-sso": "3.817.0", + "@aws-sdk/credential-provider-web-identity": "3.817.0", + "@aws-sdk/nested-clients": "3.817.0", "@aws-sdk/types": "3.804.0", "@smithy/credential-provider-imds": "^4.0.4", "@smithy/property-provider": "^4.0.2", @@ -718,17 +718,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.812.0.tgz", - "integrity": "sha512-SnvSWBP6cr9nqx784eETnL2Zl7ZnMB/oJgFVEG1aejAGbT1H9gTpMwuUsBXk4u/mEYe3f1lh1Wqo+HwDgNkfrg==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.817.0.tgz", + "integrity": "sha512-b5mz7av0Lhavs1Bz3Zb+jrs0Pki93+8XNctnVO0drBW98x1fM4AR38cWvGbM/w9F9Q0/WEH3TinkmrMPrP4T/w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.812.0", - "@aws-sdk/credential-provider-http": "3.812.0", - "@aws-sdk/credential-provider-ini": "3.812.0", - "@aws-sdk/credential-provider-process": "3.812.0", - "@aws-sdk/credential-provider-sso": "3.812.0", - "@aws-sdk/credential-provider-web-identity": "3.812.0", + "@aws-sdk/credential-provider-env": "3.816.0", + "@aws-sdk/credential-provider-http": "3.816.0", + "@aws-sdk/credential-provider-ini": "3.817.0", + "@aws-sdk/credential-provider-process": "3.816.0", + "@aws-sdk/credential-provider-sso": "3.817.0", + "@aws-sdk/credential-provider-web-identity": "3.817.0", "@aws-sdk/types": "3.804.0", "@smithy/credential-provider-imds": "^4.0.4", "@smithy/property-provider": "^4.0.2", @@ -741,12 +741,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.812.0.tgz", - "integrity": "sha512-YI8bb153XeEOb59F9KtTZEwDAc14s2YHZz58+OFiJ2udnKsPV87mNiFhJPW6ba9nmOLXVat5XDcwtVT1b664wg==", + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.816.0.tgz", + "integrity": "sha512-9Tm+AxMoV2Izvl5b9tyMQRbBwaex8JP06HN7ZeCXgC5sAsSN+o8dsThnEhf8jKN+uBpT6CLWKN1TXuUMrAmW1A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.812.0", + "@aws-sdk/core": "3.816.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -758,14 +758,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.812.0.tgz", - "integrity": "sha512-ODsPcNhgiO6GOa82TVNskM97mml9rioe9Cbhemz48lkfDQPv1u06NaCR0o3FsvprX1sEhMvJTR3sE1fyEOzvJQ==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.817.0.tgz", + "integrity": "sha512-gFUAW3VmGvdnueK1bh6TOcRX+j99Xm0men1+gz3cA4RE+rZGNy1Qjj8YHlv0hPwI9OnTPZquvPzA5fkviGREWg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.812.0", - "@aws-sdk/core": "3.812.0", - "@aws-sdk/token-providers": "3.812.0", + "@aws-sdk/client-sso": "3.817.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/token-providers": "3.817.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -777,13 +777,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.812.0.tgz", - "integrity": "sha512-E9Bmiujvm/Hp9DM/Vc1S+D0pQbx8/x4dR/zyAEZU9EoRq0duQOQ1reWYWbebYmL1OklcVpTfKV0a/VCwuAtGSg==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.817.0.tgz", + "integrity": "sha512-A2kgkS9g6NY0OMT2f2EdXHpL17Ym81NhbGnQ8bRXPqESIi7TFypFD2U6osB2VnsFv+MhwM+Ke4PKXSmLun22/A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.812.0", - "@aws-sdk/nested-clients": "3.812.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/nested-clients": "3.817.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -794,9 +794,9 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.812.0.tgz", - "integrity": "sha512-z37ykuXQXfGO7dqQFbEnj1Wu9UwUUXpZhr4iWXsehbIzSqyl5FiCMp0cI5XK8jLVACCfSCssZCz6QD4oDYdKlQ==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.817.0.tgz", + "integrity": "sha512-2zOO8+2EmiS049PjLSNdqmmZMQj7fzE1hZJ70A94vO+KNaVhVZYuMOOiOmwMw6ePkTCcFwK40vZIIXwEQQ1v1g==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.2", @@ -811,7 +811,7 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.812.0" + "@aws-sdk/client-s3": "^3.817.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -848,15 +848,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.812.0.tgz", - "integrity": "sha512-/ayAooUZvV1GTomNMrfbhjUHAEaz0Wmio3lKyaTJsW4WdLJXBuzdo57YADRmYYUqx6awzJ6VJ6HGc1Uc6tOlbw==", + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.816.0.tgz", + "integrity": "sha512-kftcwDxB/VoCBsUiRgkm5CIuKbTfCN1WLPbis9LRwX3kQhKgGVxG2gG78SHk4TBB0qviWVAd/t+i/KaUgwiAcA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.812.0", + "@aws-sdk/core": "3.816.0", "@aws-sdk/types": "3.804.0", "@smithy/is-array-buffer": "^4.0.0", "@smithy/node-config-provider": "^4.1.1", @@ -930,12 +930,12 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.812.0.tgz", - "integrity": "sha512-e8AqRRIaTsunL1hqtO1hksa9oTYdsIbfezHUyVpPGugUIB1lMqPt/DlBsanI85OzUD711UfNSEcZ1mqAxpDOoA==", + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.816.0.tgz", + "integrity": "sha512-jJ+EAXM7gnOwiCM6rrl4AUNY5urmtIsX7roTkxtb4DevJxcS+wFYRRg3/j33fQbuxQZrvk21HqxyZYx5UH70PA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.812.0", + "@aws-sdk/core": "3.816.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-arn-parser": "3.804.0", "@smithy/core": "^3.3.3", @@ -969,12 +969,12 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.812.0.tgz", - "integrity": "sha512-r+HFwtSvnAs6Fydp4mijylrTX0og9p/xfxOcKsqhMuk3HpZAIcf9sSjRQI6MBusYklg7pnM4sGEnPAZIrdRotA==", + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.816.0.tgz", + "integrity": "sha512-bHRSlWZ0xDsFR8E2FwDb//0Ff6wMkVx4O+UKsfyNlAbtqCiiHRt5ANNfKPafr95cN2CCxLxiPvFTFVblQM5TsQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.812.0", + "@aws-sdk/core": "3.816.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@smithy/core": "^3.3.3", @@ -987,23 +987,23 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.812.0.tgz", - "integrity": "sha512-FS/fImbEpJU3cXtBGR9fyVd+CP51eNKlvTMi3f4/6lSk3RmHjudNC9yEF/og3jtpT3O+7vsNOUW9mHco5IjdQQ==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.817.0.tgz", + "integrity": "sha512-vQ2E06A48STJFssueJQgxYD8lh1iGJoLJnHdshRDWOQb8gy1wVQR+a7MkPGhGR6lGoS0SCnF/Qp6CZhnwLsqsQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.812.0", + "@aws-sdk/core": "3.816.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.812.0", + "@aws-sdk/middleware-user-agent": "3.816.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.812.0", + "@aws-sdk/util-user-agent-node": "3.816.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", @@ -1053,12 +1053,12 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.812.0.tgz", - "integrity": "sha512-OpyANELjcD2oknkd3/qWanaRaZDx4SSV6NwYuWIk+fuxDZ+KxZZrrfue1X7OAdaP2TdSapbs7xLisxtTuptWYg==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.817.0.tgz", + "integrity": "sha512-FMV0YefefGwPqIbGcHdkkHaiVWKIZoI0wOhYhYDZI129aUD5+CEOtTi7KFp1iJjAK+Cx9bW5tAYc+e9shaWEyQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "3.812.0", + "@aws-sdk/signature-v4-multi-region": "3.816.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-format-url": "3.804.0", "@smithy/middleware-endpoint": "^4.1.6", @@ -1072,12 +1072,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.812.0.tgz", - "integrity": "sha512-JTpk3ZHf7TXYbicKfOKi+VrsBTqcAszg9QR9fQmT9aCxPp39gsF3WsXq7NjepwZ5So11ixGIsPE/jtMym399QQ==", + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.816.0.tgz", + "integrity": "sha512-idcr9NW86sSIXASSej3423Selu6fxlhhJJtMgpAqoCH/HJh1eQrONJwNKuI9huiruPE8+02pwxuePvLW46X2mw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.812.0", + "@aws-sdk/middleware-sdk-s3": "3.816.0", "@aws-sdk/types": "3.804.0", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", @@ -1089,12 +1089,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.812.0.tgz", - "integrity": "sha512-dbVBaKxrxE708ub5uH3w+cmKIeRQas+2Xf6rpckhohYY+IiflGOdK6aLrp3T6dOQgr/FJ37iQtcYNonAG+yVBQ==", + "version": "3.817.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.817.0.tgz", + "integrity": "sha512-CYN4/UO0VaqyHf46ogZzNrVX7jI3/CfiuktwKlwtpKA6hjf2+ivfgHSKzPpgPBcSEfiibA/26EeLuMnB6cpSrQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/nested-clients": "3.812.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/nested-clients": "3.817.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -1185,12 +1186,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.812.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.812.0.tgz", - "integrity": "sha512-8pt+OkHhS2U0LDwnzwRnFxyKn8sjSe752OIZQCNv263odud8jQu9pYO2pKqb2kRBk9h9szynjZBDLXfnvSQ7Bg==", + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.816.0.tgz", + "integrity": "sha512-Q6dxmuj4hL7pudhrneWEQ7yVHIQRBFr0wqKLF1opwOi1cIePuoEbPyJ2jkel6PDEv1YMfvsAKaRshp6eNA8VHg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.812.0", + "@aws-sdk/middleware-user-agent": "3.816.0", "@aws-sdk/types": "3.804.0", "@smithy/node-config-provider": "^4.1.1", "@smithy/types": "^4.2.0", @@ -5473,9 +5474,9 @@ } }, "node_modules/dd-trace": { - "version": "5.52.0", - "resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.52.0.tgz", - "integrity": "sha512-ZF+OWLMcgVUWJEAIYIl76LocgnbbkPJ6WgJCG1fhLk4UCsUvoHRvBx9qlexbytL0jkktk1pvzODcjL0wyxLAOQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.53.0.tgz", + "integrity": "sha512-ayraB+H05yAag5Ia70YwNkkAS4q0O/Bx1suijTUaYBXirTVlfK9CDSpZRf0Rcjk2uRqf8ANNNsws1fesP4cRmQ==", "hasInstallScript": true, "license": "(Apache-2.0 OR BSD-3-Clause)", "dependencies": { @@ -9034,9 +9035,9 @@ } }, "node_modules/moment-timezone": { - "version": "0.5.48", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", - "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.0.tgz", + "integrity": "sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q==", "license": "MIT", "dependencies": { "moment": "^2.29.4" diff --git a/package.json b/package.json index 5eec68f9b..d5cd16b50 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,14 @@ "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js" }, "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.812.0", - "@aws-sdk/client-elasticache": "^3.812.0", - "@aws-sdk/client-s3": "^3.812.0", - "@aws-sdk/client-secrets-manager": "^3.812.0", - "@aws-sdk/client-ses": "^3.812.0", - "@aws-sdk/credential-provider-node": "^3.812.0", - "@aws-sdk/lib-storage": "^3.812.0", - "@aws-sdk/s3-request-presigner": "^3.812.0", + "@aws-sdk/client-cloudwatch-logs": "^3.817.0", + "@aws-sdk/client-elasticache": "^3.817.0", + "@aws-sdk/client-s3": "^3.817.0", + "@aws-sdk/client-secrets-manager": "^3.817.0", + "@aws-sdk/client-ses": "^3.817.0", + "@aws-sdk/credential-provider-node": "^3.817.0", + "@aws-sdk/lib-storage": "^3.817.0", + "@aws-sdk/s3-request-presigner": "^3.817.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -38,7 +38,7 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.5", "crisp-status-reporter": "^1.2.2", - "dd-trace": "^5.52.0", + "dd-trace": "^5.53.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -52,7 +52,7 @@ "juice": "^11.0.1", "lodash": "^4.17.21", "moment": "^2.30.1", - "moment-timezone": "^0.5.48", + "moment-timezone": "^0.6.0", "multer": "^1.4.5-lts.1", "node-persist": "^4.0.4", "nodemailer": "^6.10.0", From d4d2db2cac2b3e05a6c6fa0fa256c4bc28c2fe36 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 26 May 2025 10:29:03 -0700 Subject: [PATCH 25/45] IO-3247 Quick Deliver Require Delivery Signed-off-by: Allan Carr --- ...jobs-detail-header-actions.toggle-production.jsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.toggle-production.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.toggle-production.jsx index c14099f89..2a331769f 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.toggle-production.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.toggle-production.jsx @@ -167,7 +167,18 @@ export function JobsDetailHeaderActionsToggleProduction({ - + From 51748ce28ddfa2e06f8c5e08ef7737b10f8309b2 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 26 May 2025 14:44:27 -0400 Subject: [PATCH 26/45] feature/IO-3182-Phone-Number-Consent - Front/Back Start/stop logic complete --- .../ca-bc-pvrt-calculator.component.jsx | 3 +- .../registerMessagingSocketHandlers.js | 99 +++++++++++++++++-- .../chat-messages-list/renderMessage.jsx | 17 ++-- .../chat-send-message.component.jsx | 94 ++++++++++-------- .../components/header/header.component.jsx | 4 +- .../job-employee-assignments.component.jsx | 2 +- ...eate-vehicle-info.predefined.component.jsx | 2 +- .../parts-order-backorder-eta.component.jsx | 2 +- ...-order-line-backorder-button.component.jsx | 2 +- ...n-list-columns.empassignment.component.jsx | 2 +- .../time-ticket-calculator.component.jsx | 2 +- server/graphql-client/queries.js | 57 ++++++++++- server/sms/receive.js | 86 +++++++++++++++- server/sms/status.js | 86 +++++++++++++--- 14 files changed, 375 insertions(+), 83 deletions(-) diff --git a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx index 6d296edd1..c48875f4c 100644 --- a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx +++ b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx @@ -3,6 +3,7 @@ import { Button, Form, InputNumber, Popover, Space } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { logImEXEvent } from "../../firebase/firebase.utils"; + export default function CABCpvrtCalculator({ disabled, form }) { const [visibility, setVisibility] = useState(false); @@ -39,7 +40,7 @@ export default function CABCpvrtCalculator({ disabled, form }) { ); return ( - + diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index f2d9d1ef9..6c6e96ad2 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -202,8 +202,6 @@ export const registerMessagingHandlers = ({ socket, client }) => { text: message.text }; - // Add cases for other known message types as needed - default: // Log a warning for unhandled message types logLocal("handleMessageChanged - Unhandled message type", { type: message.type }); @@ -211,7 +209,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { } } - return messageRef; // Keep other messages unchanged + return messageRef; }); } } @@ -245,11 +243,8 @@ export const registerMessagingHandlers = ({ socket, client }) => { }); const updatedList = existingList?.conversations - ? [ - newConversation, - ...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates - ] - : [newConversation]; + ? [newConversation, ...existingList.conversations.filter((conv) => conv.id !== newConversation.id)] + : [newConversation]; // Prevent duplicates client.cache.writeQuery({ query: CONVERSATION_LIST_QUERY, @@ -403,6 +398,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { } break; + default: logLocal("handleConversationChanged - Unhandled type", { type }); client.cache.modify({ @@ -419,10 +415,95 @@ export const registerMessagingHandlers = ({ socket, client }) => { } }; + // Existing handler for phone number opt-out + const handlePhoneNumberOptedOut = async (data) => { + const { bodyshopid, phone_number } = data; + logLocal("handlePhoneNumberOptedOut - Start", data); + + try { + client.cache.modify({ + id: "ROOT_QUERY", + fields: { + phone_number_opt_out(existing = [], { readField }) { + const phoneNumberExists = existing.some( + (ref) => readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid + ); + + if (phoneNumberExists) { + logLocal("handlePhoneNumberOptedOut - Phone number already in cache", { phone_number, bodyshopid }); + return existing; + } + + const newOptOut = { + __typename: "phone_number_opt_out", + id: `temporary-${phone_number}-${Date.now()}`, + bodyshopid, + phone_number, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + + return [...existing, newOptOut]; + } + }, + broadcast: true + }); + + client.cache.evict({ + id: "ROOT_QUERY", + fieldName: "phone_number_opt_out", + args: { bodyshopid, search: phone_number } + }); + client.cache.gc(); + + logLocal("handlePhoneNumberOptedOut - Cache updated successfully", data); + } catch (error) { + console.error("Error updating cache for phone number opt-out:", error); + logLocal("handlePhoneNumberOptedOut - Error", { error: error.message }); + } + }; + + // New handler for phone number opt-in + const handlePhoneNumberOptedIn = async (data) => { + const { bodyshopid, phone_number } = data; + logLocal("handlePhoneNumberOptedIn - Start", data); + + try { + // Update the Apollo cache for GET_PHONE_NUMBER_OPT_OUTS by removing the phone number + client.cache.modify({ + id: "ROOT_QUERY", + fields: { + phone_number_opt_out(existing = [], { readField }) { + // Filter out the phone number from the opt-out list + return existing.filter( + (ref) => !(readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid) + ); + } + }, + broadcast: true // Trigger UI updates + }); + + // Evict the cache entry to force a refetch on next query + client.cache.evict({ + id: "ROOT_QUERY", + fieldName: "phone_number_opt_out", + args: { bodyshopid, search: phone_number } + }); + client.cache.gc(); + + logLocal("handlePhoneNumberOptedIn - Cache updated successfully", data); + } catch (error) { + console.error("Error updating cache for phone number opt-in:", error); + logLocal("handlePhoneNumberOptedIn - Error", { error: error.message }); + } + }; + socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); socket.on("message-changed", handleMessageChanged); socket.on("conversation-changed", handleConversationChanged); + socket.on("phone-number-opted-out", handlePhoneNumberOptedOut); + socket.on("phone-number-opted-in", handlePhoneNumberOptedIn); }; export const unregisterMessagingHandlers = ({ socket }) => { @@ -431,4 +512,6 @@ export const unregisterMessagingHandlers = ({ socket }) => { socket.off("new-message-detailed"); socket.off("message-changed"); socket.off("conversation-changed"); + socket.off("phone-number-opted-out"); + socket.off("phone-number-opted-in"); }; diff --git a/client/src/components/chat-messages-list/renderMessage.jsx b/client/src/components/chat-messages-list/renderMessage.jsx index b94e69ee0..c572d77e3 100644 --- a/client/src/components/chat-messages-list/renderMessage.jsx +++ b/client/src/components/chat-messages-list/renderMessage.jsx @@ -2,7 +2,7 @@ import Icon from "@ant-design/icons"; import { Tooltip } from "antd"; import i18n from "i18next"; import dayjs from "../../utils/day"; -import { MdDone, MdDoneAll } from "react-icons/md"; +import { MdClose, MdDone, MdDoneAll } from "react-icons/md"; import { DateTimeFormatter } from "../../utils/DateFormatter"; export const renderMessage = (messages, index) => { @@ -31,13 +31,16 @@ export const renderMessage = (messages, index) => { {/* Message status icons */} - {message.status && (message.status === "sent" || message.status === "delivered") && ( -
- -
- )} + {message.status && + (message.status === "sent" || message.status === "delivered" || message.status === "failed") && ( +
+ +
+ )} - {/* Outbound message metadata */} {message.isoutbound && (
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 798532a3e..c3e91b2b1 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,6 +1,6 @@ import { LoadingOutlined, SendOutlined } from "@ant-design/icons"; -import { Alert, Input, Spin } from "antd"; -import React, { useEffect, useRef, useState } from "react"; +import { Alert, Input, Space, Spin } from "antd"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -68,48 +68,58 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi }; return ( -
- {isOptedOut && } - - - - setMessage(e.target.value)} - onPressEnter={(event) => { - event.preventDefault(); - if (!event.shiftKey && !isOptedOut) handleEnter(); - }} - /> - - - + {isOptedOut && } +
+ {!isOptedOut && ( + <> + + + + )} + + setMessage(e.target.value)} + onPressEnter={(event) => { + event.preventDefault(); + if (!event.shiftKey && !isOptedOut) handleEnter(); }} - spin /> - } - /> -
+ + {!isOptedOut && ( + + )} + + + } + /> +
+ ); } diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index d65f78496..826edea54 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -650,7 +650,7 @@ function Header({ icon: , label: t("menus.header.remoteassist"), children: [ - ...(InstanceRenderManager({ imex: true, rome: true }) + ...(InstanceRenderManager({ imex: true, rome: false }) ? [ { key: "rescue", @@ -662,7 +662,7 @@ function Header({ ] : []), { - key: "rescue", + key: "rescue-zoho", id: "header-rescue-zoho", icon: , label: t("menus.header.rescuemezoho"), diff --git a/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx b/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx index 85e037e86..680428a57 100644 --- a/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx +++ b/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx @@ -80,7 +80,7 @@ export function JobEmployeeAssignments({ ); return ( - + {body ? ( diff --git a/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx b/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx index 338e873dd..5f0c3a2dc 100644 --- a/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx +++ b/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx @@ -72,7 +72,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) { open={open} placement="left" onOpenChange={handleOpenChange} - destroyTooltipOnHide + destroyOnHidden > diff --git a/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx b/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx index d8895803b..7fd2cf2e4 100644 --- a/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx +++ b/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx @@ -75,7 +75,7 @@ export function PartsOrderBackorderEta({ ); return ( - + {backordered_eta} {isAlreadyBackordered && } {loading && } diff --git a/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx b/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx index 26910643c..e38b30ecb 100644 --- a/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx +++ b/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx @@ -84,7 +84,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j ); return ( - + diff --git a/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx b/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx index 3e7cdbdcd..e326a455f 100644 --- a/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx @@ -140,7 +140,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]); return ( - + {record[type] ? (
diff --git a/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx index c5b174f26..be74b1f99 100644 --- a/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx +++ b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx @@ -107,7 +107,7 @@ export default function TimeTicketCalculatorComponent({ open={visible} onOpenChange={handleOpenChange} placement="right" - destroyTooltipOnHide + destroyOnHidden >
diff --git a/client/src/graphql/phone-number-opt-out.queries.js b/client/src/graphql/phone-number-opt-out.queries.js index 861bdf64d..123d06744 100644 --- a/client/src/graphql/phone-number-opt-out.queries.js +++ b/client/src/graphql/phone-number-opt-out.queries.js @@ -26,3 +26,25 @@ export const GET_PHONE_NUMBER_OPT_OUTS = gql` } } `; + +export const SEARCH_OWNERS_BY_PHONE_NUMBERS = gql` + query SEARCH_OWNERS_BY_PHONE_NUMBERS($bodyshopid: uuid!, $phone_numbers: [String!]) { + owners( + where: { + shopid: { _eq: $bodyshopid }, + _or: [ + { ownr_ph1: { _in: $phone_numbers } }, + { ownr_ph2: { _in: $phone_numbers } } + ] + } + ) { + id + ownr_fn + ownr_ln + ownr_co_nm + ownr_ph1 + ownr_ph2 + __typename + } + } +`; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index ac5833e86..dca4f62ef 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -3871,7 +3871,11 @@ }, "consent": { "phone_number": "Phone Number", - "created_at": "Created At" + "associated_owners": "Associated Owners", + "created_at": "Opt-Out Date", + "no_owners": "No Associated Owners", + "phone_1": "Phone 1", + "phone_2": "Phone 2" }, "settings": { "title": "Phone Number Opt-Out List" diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 2303f9610..cfc349043 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -3873,7 +3873,11 @@ }, "consent": { "phone_number": "", - "created_at": "" + "associated_owners": "", + "created_at": "", + "no_owners": "", + "phone_1": "", + "phone_2": "" }, "settings": { "title": "" diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index e7bcccbf0..0e49be0e1 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -3872,8 +3872,12 @@ } }, "consent": { - "phone_number": "", - "created_at": "" + "phone_number": "Phone Number", + "associated_owners": "Associated Owners", + "created_at": "Opt-Out Date", + "no_owners": "No Associated Owners", + "phone_1": "Phone 1", + "phone_2": "Phone 2" }, "settings": { "title": "" From e279bf41a407fd2035cff45f6c3f8ef00224b2c7 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 26 May 2025 12:11:26 -0700 Subject: [PATCH 29/45] IO-3214 Extend New Fields to Audit Log Signed-off-by: Allan Carr --- .../jobs-detail-header.component.jsx | 32 +++++++++++++++---- client/src/utils/DateFormatter.jsx | 1 - 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx index 746b40abd..141cab456 100644 --- a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx +++ b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx @@ -1,17 +1,20 @@ import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link } from "react-router-dom"; -import { useMutation } from "@apollo/client"; import { createStructuredSelector } from "reselect"; +import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions.js"; import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import { DateTimeFormatter } from "../../utils/DateFormatter"; +import { DateTimeFormatter, DateTimeFormatterFunction } from "../../utils/DateFormatter"; import dayjs from "../../utils/day"; import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; @@ -24,7 +27,6 @@ import ProductionListColumnComment from "../production-list-columns/production-l import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import "./jobs-detail-header.styles.scss"; -import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; const mapStateToProps = createStructuredSelector({ jobRO: selectJobReadOnly, @@ -38,6 +40,14 @@ const mapDispatchToProps = (dispatch) => ({ context: context, modal: "printCenter" }) + ), + insertAuditTrail: ({ jobid, operation, type }) => + dispatch( + insertAuditTrail({ + jobid, + operation, + type + }) ) }); @@ -49,7 +59,7 @@ const colSpan = { xl: { span: 6 } }; -export function JobsDetailHeader({ job, bodyshop, disabled }) { +export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail }) { const { t } = useTranslation(); const { notification } = useNotification(); const [notesClamped, setNotesClamped] = useState(true); @@ -66,7 +76,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { const handleCheckboxChange = async (field, checked) => { const value = checked ? dayjs().toISOString() : null; try { - await updateJob({ + const ret = await updateJob({ variables: { jobId: job.id, job: { [field]: value } @@ -74,6 +84,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { refetchQueries: ["GET_JOB_BY_PK"], awaitRefetchQueries: true }); + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobfieldchange( + field, + ret.data.update_jobs.returning[0][field] + ? DateTimeFormatterFunction(ret.data.update_jobs.returning[0][field]) + : checked + ), + type: "jobfieldchange" + }); } catch (error) { notification.error({ message: t("jobs.errors.saving", { error: error.message }) diff --git a/client/src/utils/DateFormatter.jsx b/client/src/utils/DateFormatter.jsx index fe8315beb..574c1d229 100644 --- a/client/src/utils/DateFormatter.jsx +++ b/client/src/utils/DateFormatter.jsx @@ -1,6 +1,5 @@ import { Tooltip } from "antd"; import dayjs from "../utils/day"; -import React from "react"; export function DateFormatter(props) { return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null; From 310321d0abdde4e5c98a22b6c87025f5b6bb8752 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 26 May 2025 12:49:58 -0700 Subject: [PATCH 30/45] IO-3249 Delete Job Watchers Signed-off-by: Allan Carr --- .../jobs-detail-header-actions.component.jsx | 37 ++++++++++++------- client/src/translations/en_us/common.json | 3 +- client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx index 1db81a3b1..ec1ec32b6 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx @@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries"; @@ -32,7 +33,6 @@ import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx"; import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production"; -import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -1078,17 +1078,26 @@ export function JobsDetailHeaderActions({ menuItems.push({ key: "deletejob", id: "job-actions-deletejob", - label: ( - e.stopPropagation()} - onConfirm={handleDeleteJob} - > - {t("menus.jobsactions.deletejob")} - - ) + label: + job.job_watchers.length === 0 ? ( + e.stopPropagation()} + onConfirm={handleDeleteJob} + > + {t("menus.jobsactions.deletejob")} + + ) : ( + e.stopPropagation()} + showCancel={false} + > + {t("menus.jobsactions.deletejob")} + + ) }); } @@ -1109,8 +1118,8 @@ export function JobsDetailHeaderActions({ e.stopPropagation()} onConfirm={handleVoidJob} > diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index dca4f62ef..52d6c1c00 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2032,9 +2032,10 @@ "stands": "Stands", "waived": "Waived" }, - "deleteconfirm": "Are you sure you want to delete this Job? This cannot be undone. ", + "deleteconfirm": "Are you sure you want to delete this Job? This cannot be undone.", "deletedelivery": "Delete Delivery Checklist", "deleteintake": "Delete Intake Checklist", + "deletewatchers": "Remove Watchers before deleting this Job.", "deliverchecklist": "Deliver Checklist", "difference": "Difference", "diskscan": "Scan Disk for Estimates", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index cfc349043..b01353c1b 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2035,6 +2035,7 @@ "deleteconfirm": "", "deletedelivery": "", "deleteintake": "", + "deletewatchers": "", "deliverchecklist": "", "difference": "", "diskscan": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 0e49be0e1..e77bd39c8 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2035,6 +2035,7 @@ "deleteconfirm": "", "deletedelivery": "", "deleteintake": "", + "deletewatchers": "", "deliverchecklist": "", "difference": "", "diskscan": "", From fe2600029f48d7024ed3389e687ff3e7aa2a0de6 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 27 May 2025 11:03:09 -0400 Subject: [PATCH 31/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../phone-number-consent.component.jsx | 31 +++++++++---------- client/src/pages/shop/shop.page.component.jsx | 16 +++++----- client/src/translations/en_us/common.json | 3 +- client/src/translations/es/common.json | 3 +- client/src/translations/fr/common.json | 3 +- server/sms/receive.js | 8 +++-- 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/client/src/components/phone-number-consent/phone-number-consent.component.jsx b/client/src/components/phone-number-consent/phone-number-consent.component.jsx index 0df53ca30..c0872997d 100644 --- a/client/src/components/phone-number-consent/phone-number-consent.component.jsx +++ b/client/src/components/phone-number-consent/phone-number-consent.component.jsx @@ -1,14 +1,17 @@ import { useQuery } from "@apollo/client"; -import { Input, Table } from "antd"; +import { Input, Space, Table, Typography } from "antd"; +import { Link } from "react-router-dom"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { GET_PHONE_NUMBER_OPT_OUTS, SEARCH_OWNERS_BY_PHONE_NUMBERS } from "../../graphql/phone-number-opt-out.queries"; - import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import { TimeAgoFormatter } from "../../utils/DateFormatter"; +import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; + +const { Paragraph } = Typography; // Destructure Paragraph from Typography const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -44,18 +47,6 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) { fetchPolicy: "network-only" }); - // Format owner names for display - const formatOwnerName = (owner) => { - const parts = []; - if (owner.ownr_fn || owner.ownr_ln) { - parts.push([owner.ownr_fn, owner.ownr_ln].filter(Boolean).join(" ")); - } - if (owner.ownr_co_nm) { - parts.push(owner.ownr_co_nm); - } - return parts.join(", ") || "-"; - }; - // Map phone numbers to their associated owners and identify phone field const getAssociatedOwners = (phoneNumber) => { if (!ownersData?.owners) return []; @@ -102,15 +93,20 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) { } return owners.map((owner) => (
- {formatOwnerName(owner)} ({owner.phoneField}) + + + + + ({owner.phoneField}) +
)); }, sorter: (a, b) => { const aOwners = getAssociatedOwners(a.phone_number); const bOwners = getAssociatedOwners(b.phone_number); - const aName = aOwners[0] ? formatOwnerName(aOwners[0]) : ""; - const bName = bOwners[0] ? formatOwnerName(bOwners[0]) : ""; + const aName = aOwners[0] ? `${aOwners[0].ownr_fn} ${aOwners[0].ownr_ln}` : ""; + const bName = bOwners[0] ? `${bOwners[0].ownr_fn} ${bOwners[0].ownr_ln}` : ""; return aName.localeCompare(bName); } }, @@ -124,6 +120,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) { return (
+ {t("consent.text_body")} setSearch(value)} diff --git a/client/src/pages/shop/shop.page.component.jsx b/client/src/pages/shop/shop.page.component.jsx index b6ded16f6..f26f06d5a 100644 --- a/client/src/pages/shop/shop.page.component.jsx +++ b/client/src/pages/shop/shop.page.component.jsx @@ -92,13 +92,15 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) { }); } - // Add Consent Settings tab - items.push({ - key: "consent", - label: t("bodyshop.labels.consent_settings"), - children: - }); - + if (bodyshop.messagingservicesid) { + // 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/translations/en_us/common.json b/client/src/translations/en_us/common.json index dca4f62ef..143bb14df 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -3875,7 +3875,8 @@ "created_at": "Opt-Out Date", "no_owners": "No Associated Owners", "phone_1": "Phone 1", - "phone_2": "Phone 2" + "phone_2": "Phone 2", + "text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in." }, "settings": { "title": "Phone Number Opt-Out List" diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index cfc349043..1ff5d7fc9 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -3877,7 +3877,8 @@ "created_at": "", "no_owners": "", "phone_1": "", - "phone_2": "" + "phone_2": "", + "text_body": "" }, "settings": { "title": "" diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 0e49be0e1..6c2b0b171 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -3877,7 +3877,8 @@ "created_at": "Opt-Out Date", "no_owners": "No Associated Owners", "phone_1": "Phone 1", - "phone_2": "Phone 2" + "phone_2": "Phone 2", + "text_body": "" }, "settings": { "title": "" diff --git a/server/sms/receive.js b/server/sms/receive.js index 514d375b4..864531f36 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -12,6 +12,11 @@ const { phone } = require("phone"); const { admin } = require("../firebase/firebase-handler"); const InstanceManager = require("../utils/instanceMgr").default; +/ TWILLIO KEYWORDS; +// Note: When we handle different languages, we might need to adjust these keywords accordingly. +const optInKeywords = ["START", "YES", "UNSTOP"]; +const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT", "REVOKE", "OPTOUT"]; + /** * Receive SMS messages from Twilio and process them * @param req @@ -58,9 +63,6 @@ const receive = async (req, res) => { const messageText = (req.body.Body || "").trim().toUpperCase(); // Step 2: Check for opt-in or opt-out keywords - const optInKeywords = ["START", "YES", "UNSTOP"]; - const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"]; - if (optInKeywords.includes(messageText) || optOutKeywords.includes(messageText)) { // Check if the phone number is in phone_number_opt_out const optOutCheck = await client.request(CHECK_PHONE_NUMBER_OPT_OUT, { From 6513434bd7db1f6b477ec71fff0cf1f928ec537f Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 27 May 2025 11:34:16 -0400 Subject: [PATCH 32/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- bodyshop_translations.babel | 42 ------------------- .../job-checklist-form.component.jsx | 27 ------------ .../jobs-create-owner-info.new.component.jsx | 9 +--- .../owner-detail-form.component.jsx | 7 +--- client/src/graphql/bodyshop.queries.js | 1 - client/src/graphql/jobs.queries.js | 3 -- client/src/graphql/owners.queries.js | 3 -- .../jobs-create/jobs-create.container.jsx | 1 - client/src/translations/en_us/common.json | 2 - client/src/translations/es/common.json | 2 - client/src/translations/fr/common.json | 2 - new_bodyshop_translations.babel | 38 ----------------- server/sms/receive.js | 3 +- 13 files changed, 4 insertions(+), 136 deletions(-) diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 8969c93eb..399953905 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -12791,27 +12791,6 @@ - - allow_text_message - false - - - - - - en-US - false - - - es-MX - false - - - fr-CA - false - - - checklist false @@ -42614,27 +42593,6 @@ - - allow_text_message - false - - - - - - en-US - false - - - es-MX - false - - - fr-CA - false - - - name false diff --git a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx index 18f5d025e..eca3147dc 100644 --- a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx +++ b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx @@ -129,24 +129,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren } } - if (type === "intake" && job.owner && job.owner.id) { - //Updae Owner Allow to Text - const updateOwnerResult = await updateOwner({ - variables: { - ownerId: job.owner.id, - owner: { allow_text_message: values.allow_text_message } - } - }); - - if (!!updateOwnerResult.errors) { - notification["error"]({ - message: t("checklist.errors.complete", { - error: JSON.stringify(result.errors) - }) - }); - } - } - setLoading(false); if (!!!result.errors) { @@ -189,7 +171,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren initialValues={{ ...(type === "intake" && { addToProduction: true, - allow_text_message: job.owner && job.owner.allow_text_message, scheduled_completion: (job && job.scheduled_completion && dayjs(job.scheduled_completion)) || (job && @@ -228,14 +209,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren > - - - - - -
); diff --git a/client/src/components/owner-detail-form/owner-detail-form.component.jsx b/client/src/components/owner-detail-form/owner-detail-form.component.jsx index 5de4231f7..a71cbc836 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.component.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.component.jsx @@ -1,4 +1,4 @@ -import { Form, Input, Switch } from "antd"; +import { Form, Input } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; @@ -26,7 +26,7 @@ export default function OwnerDetailFormComponent({ form, loading }) { - + @@ -50,9 +50,6 @@ export default function OwnerDetailFormComponent({ form, loading }) { - - - - - allow_text_message - - - - - en-US - false - - - es-ES - false - - - fr-CA - false - - - checklist @@ -33000,25 +32981,6 @@ - - allow_text_message - - - - - en-US - false - - - es-ES - false - - - fr-CA - false - - - name diff --git a/server/sms/receive.js b/server/sms/receive.js index 864531f36..2106b206b 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -12,8 +12,7 @@ const { phone } = require("phone"); const { admin } = require("../firebase/firebase-handler"); const InstanceManager = require("../utils/instanceMgr").default; -/ TWILLIO KEYWORDS; -// Note: When we handle different languages, we might need to adjust these keywords accordingly. +9; // Note: When we handle different languages, we might need to adjust these keywords accordingly. const optInKeywords = ["START", "YES", "UNSTOP"]; const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT", "REVOKE", "OPTOUT"]; From 8a22897cdd83a4970f266440633d52308b4d143f Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 27 May 2025 11:35:16 -0400 Subject: [PATCH 33/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- server/sms/receive.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/sms/receive.js b/server/sms/receive.js index 2106b206b..a9670b76a 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -12,10 +12,10 @@ const { phone } = require("phone"); const { admin } = require("../firebase/firebase-handler"); const InstanceManager = require("../utils/instanceMgr").default; -9; // Note: When we handle different languages, we might need to adjust these keywords accordingly. +// Note: When we handle different languages, we might need to adjust these keywords accordingly. const optInKeywords = ["START", "YES", "UNSTOP"]; const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT", "REVOKE", "OPTOUT"]; - + /** * Receive SMS messages from Twilio and process them * @param req From 0a654082c26482e9ac1ed4c6293b612e38782313 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 27 May 2025 11:41:31 -0400 Subject: [PATCH 34/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../job-checklist-form/job-checklist-form.component.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx index eca3147dc..29ced4f09 100644 --- a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx +++ b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx @@ -1,7 +1,7 @@ import { useMutation } from "@apollo/client"; import { Button, Card, Form, Input, Switch } from "antd"; import queryString from "query-string"; -import React, { useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { useLocation, useNavigate, useParams } from "react-router-dom"; @@ -9,7 +9,6 @@ import { createStructuredSelector } from "reselect"; import { logImEXEvent } from "../../../../firebase/firebase.utils"; import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries"; import { UPDATE_JOB } from "../../../../graphql/jobs.queries"; -import { UPDATE_OWNER } from "../../../../graphql/owners.queries"; import { insertAuditTrail } from "../../../../redux/application/application.actions"; import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors"; import AuditTrailMapping from "../../../../utils/AuditTrailMappings"; @@ -32,7 +31,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren const [loading, setLoading] = useState(false); const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED); const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED); - const [updateOwner] = useMutation(UPDATE_OWNER); const notification = useNotification(); const { jobId } = useParams(); From 2e95fa25af7edb7ab6e50a4c5fb91d37b1efb1aa Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 28 May 2025 12:17:43 -0400 Subject: [PATCH 35/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- client/package-lock.json | 154 ++++++++--------- client/package.json | 4 +- .../chat-conversation-list.component.jsx | 11 +- .../chat-conversation.container.jsx | 1 + .../chat-message-list.styles.scss | 40 ++++- .../chat-messages-list/renderMessage.jsx | 34 +++- .../chat-send-message.component.jsx | 15 +- .../phone-number-consent.component.jsx | 33 ++-- client/src/graphql/conversations.queries.js | 2 + hasura/metadata/tables.yaml | 1 + .../down.sql | 4 + .../up.sql | 2 + server/sms/receive.js | 159 ++++++++++++------ 13 files changed, 303 insertions(+), 157 deletions(-) create mode 100644 hasura/migrations/1748443780878_alter_table_public_messages_add_column_is_system/down.sql create mode 100644 hasura/migrations/1748443780878_alter_table_public_messages_add_column_is_system/up.sql diff --git a/client/package-lock.json b/client/package-lock.json index fd218cc50..42436e972 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -20,8 +20,8 @@ "@firebase/messaging": "^0.12.21", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", - "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.22.0", + "@sentry/cli": "^2.46.0", + "@sentry/react": "^9.23.0", "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", @@ -4469,50 +4469,50 @@ "license": "MIT" }, "node_modules/@sentry-internal/browser-utils": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.22.0.tgz", - "integrity": "sha512-Ou1tBnVxFAIn8i9gvrWzRotNJQYiu3awNXpsFCw6qFwmiKAVPa6b13vCdolhXnrIiuR77jY1LQnKh9hXpoRzsg==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.23.0.tgz", + "integrity": "sha512-hyN2Q6mh7ggw8sDVHeRyWz5LR6gjvf8zHSzQnMaF7QkeSyaeGM/SVSL4ODwqR9TRH7U2ku6nZFMbKhaBPV+Hfg==", "license": "MIT", "dependencies": { - "@sentry/core": "9.22.0" + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.22.0.tgz", - "integrity": "sha512-zgMVkoC61fgi41zLcSZA59vOtKxcLrKBo1ECYhPD1hxEaneNqY5fhXDwlQBw96P5l2yqkgfX6YZtSdU4ejI9yA==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.23.0.tgz", + "integrity": "sha512-Xf+KqV69TBiPo1gk2EsU6O/dumuTMxWOF52uVWJddQYI3pQTU5DqSeoZ5AY76bIIhV9n6AEFDGqNPXmuj4Acrw==", "license": "MIT", "dependencies": { - "@sentry/core": "9.22.0" + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.22.0.tgz", - "integrity": "sha512-9GOycoKbrclcRXfcbNV8svbmAsOS5R4wXBQmKF4pFLkmFA/lJv9kdZSNYkRvkrxdNfbMIJXP+DV9EqTZcryXig==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.23.0.tgz", + "integrity": "sha512-0/q15tvSboaK7/05BFQhs71bqgHKejJoDJgXmH0lySqgsRh/S18867ZxQNiuYhuVt337h07u1QaCyjnNJKHfuA==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.22.0", - "@sentry/core": "9.22.0" + "@sentry-internal/browser-utils": "9.23.0", + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.22.0.tgz", - "integrity": "sha512-EcG9IMSEalFe49kowBTJObWjof/iHteDwpyuAszsFDdQUYATrVUtwpwN7o52vDYWJud4arhjrQnMamIGxa79eQ==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.23.0.tgz", + "integrity": "sha512-cYlw5svJjyPequm0PJjFGLpee86L1NieONEHlujOXkIG6IEriiorMm+8bNpGsHRuyvg41B+4P/YmcQAGtEGxXg==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "9.22.0", - "@sentry/core": "9.22.0" + "@sentry-internal/replay": "9.23.0", + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" @@ -4528,16 +4528,16 @@ } }, "node_modules/@sentry/browser": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.22.0.tgz", - "integrity": "sha512-3TeRm74dvX0JdjX0AgkQa+22iUHwHnY+Q6M05NZ+tDeCNHGK/mEBTeqquS1oQX67jWyuvYmG3VV6RJUxtG9Paw==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.23.0.tgz", + "integrity": "sha512-QRkNxWys8e088260vByztoTsEVZ0W6v/XnZfKT6wkPPGn0aFeOrg/xjgxfI8D5huqZCxT28Cf23akOOly4FXjg==", "license": "MIT", "dependencies": { - "@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" + "@sentry-internal/browser-utils": "9.23.0", + "@sentry-internal/feedback": "9.23.0", + "@sentry-internal/replay": "9.23.0", + "@sentry-internal/replay-canvas": "9.23.0", + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" @@ -4728,9 +4728,9 @@ } }, "node_modules/@sentry/cli": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.45.0.tgz", - "integrity": "sha512-4sWu7zgzgHAjIxIjXUA/66qgeEf5ZOlloO+/JaGD5qXNSW0G7KMTR6iYjReNKMgdBCTH6bUUt9qiuA+Ex9Masw==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.46.0.tgz", + "integrity": "sha512-nqoPl7UCr446QFkylrsRrUXF51x8Z9dGquyf4jaQU+OzbOJMqclnYEvU6iwbwvaw3tu/2DnoZE/Og+Nq1h63sA==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -4747,20 +4747,20 @@ "node": ">= 10" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.45.0", - "@sentry/cli-linux-arm": "2.45.0", - "@sentry/cli-linux-arm64": "2.45.0", - "@sentry/cli-linux-i686": "2.45.0", - "@sentry/cli-linux-x64": "2.45.0", - "@sentry/cli-win32-arm64": "2.45.0", - "@sentry/cli-win32-i686": "2.45.0", - "@sentry/cli-win32-x64": "2.45.0" + "@sentry/cli-darwin": "2.46.0", + "@sentry/cli-linux-arm": "2.46.0", + "@sentry/cli-linux-arm64": "2.46.0", + "@sentry/cli-linux-i686": "2.46.0", + "@sentry/cli-linux-x64": "2.46.0", + "@sentry/cli-win32-arm64": "2.46.0", + "@sentry/cli-win32-i686": "2.46.0", + "@sentry/cli-win32-x64": "2.46.0" } }, "node_modules/@sentry/cli-darwin": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.45.0.tgz", - "integrity": "sha512-p4Uxfv/L2fQdP3/wYnKVVz9gzZJf/1Xp9D+6raax/3Bu5y87yHYUqcdt98y/VAXQD4ofp2QgmhGUVPofvQNZmg==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.46.0.tgz", + "integrity": "sha512-5Ll+e5KAdIk9OYiZO8aifMBRNWmNyPjSqdjaHlBC1Qfh7pE3b1zyzoHlsUazG0bv0sNrSGea8e7kF5wIO1hvyg==", "license": "BSD-3-Clause", "optional": true, "os": [ @@ -4771,9 +4771,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.45.0.tgz", - "integrity": "sha512-6sEskFLlFKJ+e0MOYgIclBTUX5jYMyYhHIxXahEkI/4vx6JO0uvpyRAkUJRpJkRh/lPog0FM+tbP3so+VxB2qQ==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.46.0.tgz", + "integrity": "sha512-WRrLNq/TEX/TNJkGqq6Ad0tGyapd5dwlxtsPbVBrIdryuL1mA7VCBoaHBr3kcwJLsgBHFH0lmkMee2ubNZZdkg==", "cpu": [ "arm" ], @@ -4781,16 +4781,17 @@ "optional": true, "os": [ "linux", - "freebsd" + "freebsd", + "android" ], "engines": { "node": ">=10" } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.45.0.tgz", - "integrity": "sha512-gUcLoEjzg7AIc4QQGEZwRHri+EHf3Gcms9zAR1VHiNF3/C/jL4WeDPJF2YiWAQt6EtH84tHiyhw1Ab/R8XFClg==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.46.0.tgz", + "integrity": "sha512-OEJN8yAjI9y5B4telyqzu27Hi3+S4T8VxZCqJz1+z2Mp0Q/MZ622AahVPpcrVq/5bxrnlZR16+lKh8L1QwNFPg==", "cpu": [ "arm64" ], @@ -4798,16 +4799,17 @@ "optional": true, "os": [ "linux", - "freebsd" + "freebsd", + "android" ], "engines": { "node": ">=10" } }, "node_modules/@sentry/cli-linux-i686": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.45.0.tgz", - "integrity": "sha512-VmmOaEAzSW23YdGNdy/+oQjCNAMY+HmOGA77A25/ep/9AV7PQB6FI7xO5Y1PVvlkxZFJ23e373njSsEeg4uDZw==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.46.0.tgz", + "integrity": "sha512-xko3/BVa4LX8EmRxVOCipV+PwfcK5Xs8lP6lgF+7NeuAHMNL4DqF6iV9rrN8gkGUHCUI9RXSve37uuZnFy55+Q==", "cpu": [ "x86", "ia32" @@ -4816,16 +4818,17 @@ "optional": true, "os": [ "linux", - "freebsd" + "freebsd", + "android" ], "engines": { "node": ">=10" } }, "node_modules/@sentry/cli-linux-x64": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.45.0.tgz", - "integrity": "sha512-a0Oj68mrb25a0WjX/ShZ6AAd4PPiuLcgyzQr7bl2+DvYxIOajwkGbR+CZFEhOVZcfhTnixKy/qIXEzApEPHPQg==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.46.0.tgz", + "integrity": "sha512-hJ1g5UEboYcOuRia96LxjJ0jhnmk8EWLDvlGnXLnYHkwy3ree/L7sNgdp/QsY8Z4j2PGO5f22Va+UDhSjhzlfQ==", "cpu": [ "x64" ], @@ -4833,16 +4836,17 @@ "optional": true, "os": [ "linux", - "freebsd" + "freebsd", + "android" ], "engines": { "node": ">=10" } }, "node_modules/@sentry/cli-win32-arm64": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.45.0.tgz", - "integrity": "sha512-vn+CwS4p+52pQSLNPoi20ZOrQmv01ZgAmuMnjkh1oUZfTyBAwWLrAh6Cy4cztcN8DfL5dOWKQBo8DBKURE4ttg==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.46.0.tgz", + "integrity": "sha512-mN7cpPoCv2VExFRGHt+IoK11yx4pM4ADZQGEso5BAUZ5duViXB2WrAXCLd8DrwMnP0OE978a7N8OtzsFqjkbNA==", "cpu": [ "arm64" ], @@ -4856,9 +4860,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.45.0.tgz", - "integrity": "sha512-8mMoDdlwxtcdNIMtteMK7dbi7054jak8wKSHJ5yzMw8UmWxC5thc/gXBc1uPduiaI56VjoJV+phWHBKCD+6I4w==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.46.0.tgz", + "integrity": "sha512-6F73AUE3lm71BISUO19OmlnkFD5WVe4/wA1YivtLZTc1RU3eUYJLYxhDfaH3P77+ycDppQ2yCgemLRaA4A8mNQ==", "cpu": [ "x86", "ia32" @@ -4873,9 +4877,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.45.0.tgz", - "integrity": "sha512-ZvK9cIqFaq7vZ0jkHJ/xh5au6902Dr+AUxSk6L6vCL7JCe2p93KGL/4d8VFB5PD/P7Y9b+105G/e0QIFKzpeOw==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.46.0.tgz", + "integrity": "sha512-yuGVcfepnNL84LGA0GjHzdMIcOzMe0bjPhq/rwPsPN+zu11N+nPR2wV2Bum4U0eQdqYH3iAlMdL5/BEQfuLJww==", "cpu": [ "x64" ], @@ -4910,22 +4914,22 @@ } }, "node_modules/@sentry/core": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.22.0.tgz", - "integrity": "sha512-ixvtKmPF42Y6ckGUbFlB54OWI75H2gO5UYHojO6eXFpS7xO3ZGgV/QH6wb40mWK+0w5XZ0233FuU9VpsuE6mKA==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.23.0.tgz", + "integrity": "sha512-9846pn/BvASGgl7WsnKY4xry98WreP9ToeLfCQTQOf+pNfD/qNPn1/0xPInGni3LVMAXRtfHHMPm2Ghz255N7A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.22.0.tgz", - "integrity": "sha512-mI43NnioBYdG5TiXqRlhV1feZs9bnrrl+k5HOHBK7VQtymaXO0fkcsRLZTkdSgLRLMJGasZuvVhq2xK+18QyWQ==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.23.0.tgz", + "integrity": "sha512-2J/oOx8jd7Jr2koYIe5IcJyStHBXpjkQnxawo54Zyyvzc96MftyM2Dv5TeYdz7fChU1NIXw7BVbEpkQ9XEQlqg==", "license": "MIT", "dependencies": { - "@sentry/browser": "9.22.0", - "@sentry/core": "9.22.0", + "@sentry/browser": "9.23.0", + "@sentry/core": "9.23.0", "hoist-non-react-statics": "^3.3.2" }, "engines": { diff --git a/client/package.json b/client/package.json index 729aee7e2..61af4372d 100644 --- a/client/package.json +++ b/client/package.json @@ -19,8 +19,8 @@ "@firebase/messaging": "^0.12.21", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.8.2", - "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.22.0", + "@sentry/cli": "^2.46.0", + "@sentry/react": "^9.23.0", "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", 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 8bf185756..e5fe4b9ca 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,4 +1,4 @@ -import { Badge, Card, List, Space, Tag } from "antd"; +import { Badge, Card, List, Space, Tag, Tooltip } from "antd"; import { useEffect, useMemo, useState } from "react"; import { connect } from "react-redux"; import { Virtuoso } from "react-virtuoso"; @@ -9,6 +9,7 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import _ from "lodash"; +import { ExclamationCircleOutlined } from "@ant-design/icons"; import "./chat-conversation-list.styles.scss"; import { useQuery } from "@apollo/client"; import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js"; @@ -88,7 +89,13 @@ function ChatConversationListComponent({ conversationList, selectedConversation, const cardExtra = ( <> - {hasOptOutEntry && {t("messaging.labels.no_consent")}} + {hasOptOutEntry && ( + + }> + {t("messaging.labels.no_consent")} + + + )} ); diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index f951aabd1..e53ec8172 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -58,6 +58,7 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) { userid created_at read + is_system } `, data: message diff --git a/client/src/components/chat-messages-list/chat-message-list.styles.scss b/client/src/components/chat-messages-list/chat-message-list.styles.scss index 98f5ab8a4..969de0c82 100644 --- a/client/src/components/chat-messages-list/chat-message-list.styles.scss +++ b/client/src/components/chat-messages-list/chat-message-list.styles.scss @@ -4,13 +4,16 @@ height: 100%; width: 100%; } + .archive-button { height: 20px; border-radius: 4px; } + .chat-title { margin-bottom: 5px; } + .messages { display: flex; flex-direction: column; @@ -37,11 +40,13 @@ gap: 8px; } } -.chat-send-message-button{ + +.chat-send-message-button { margin: 0.3rem; padding-left: 0.5rem; - + } + .message-icon { position: absolute; bottom: 0.1rem; @@ -125,6 +130,37 @@ } } +.system { + align-items: center; + margin: 0.5rem 10%; + + .message { + background-color: #f5f5f5; + border-radius: 10px; + padding: 0.5rem 1rem; + text-align: center; + font-style: italic; + color: #555; + width: fit-content; + max-width: 80%; + } + + .system-label { + font-size: 0.75rem; + color: #888; + margin-bottom: 0.2rem; + display: block; + } + + .system-date { + font-size: 0.75rem; + color: #888; + margin-top: 0.2rem; + text-align: center; + } +} + + .virtuoso-container { flex: 1; overflow: auto; diff --git a/client/src/components/chat-messages-list/renderMessage.jsx b/client/src/components/chat-messages-list/renderMessage.jsx index c572d77e3..2d1e1ce75 100644 --- a/client/src/components/chat-messages-list/renderMessage.jsx +++ b/client/src/components/chat-messages-list/renderMessage.jsx @@ -7,12 +7,24 @@ import { DateTimeFormatter } from "../../utils/DateFormatter"; export const renderMessage = (messages, index) => { const message = messages[index]; + const isSystem = message.is_system; + + // Determine message class + const messageClass = isSystem ? "system messages" : message.isoutbound ? "mine messages" : "yours messages"; + + // Tooltip content based on message type + const tooltipTitle = isSystem ? ( + i18n.t("consent.text_body") + ) : ( + {message.created_at} + ); return ( -
+
- +
+ {isSystem && System} {/* Render images if available */} {message.image && message.image_path?.length > 0 && (
@@ -26,23 +38,31 @@ export const renderMessage = (messages, index) => {
)} {/* Render text if available */} - {message.text &&
{message.text}
} + {message.text &&
{message.text}
} + {/* Render date for system messages */} + {isSystem && ( +
+ {message.created_at} +
+ )}
- {/* Message status icons */} - {message.status && + {/* Message status icons for non-system messages */} + {!isSystem && + message.status && (message.status === "sent" || message.status === "delivered" || message.status === "failed") && (
)}
- {/* Outbound message metadata */} - {message.isoutbound && ( + {/* Outbound message metadata for non-system messages */} + {!isSystem && message.isoutbound && (
{i18n.t("messaging.labels.sentby", { by: message.userid, 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 c3e91b2b1..5b4d31d14 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 { Alert, Input, Space, Spin } from "antd"; +import { ExclamationCircleOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons"; +import { Alert, Input, Space, Spin, Tooltip } from "antd"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -69,7 +69,16 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi return ( - {isOptedOut && } + {isOptedOut && ( + + } + message={t("messaging.errors.no_consent")} + type="error" + /> + + )}
{!isOptedOut && ( <> diff --git a/client/src/components/phone-number-consent/phone-number-consent.component.jsx b/client/src/components/phone-number-consent/phone-number-consent.component.jsx index c0872997d..9a9e32e51 100644 --- a/client/src/components/phone-number-consent/phone-number-consent.component.jsx +++ b/client/src/components/phone-number-consent/phone-number-consent.component.jsx @@ -1,17 +1,20 @@ import { useQuery } from "@apollo/client"; -import { Input, Space, Table, Typography } from "antd"; -import { Link } from "react-router-dom"; -import { useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { Input, Table, Typography } from "antd"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; -import { GET_PHONE_NUMBER_OPT_OUTS, SEARCH_OWNERS_BY_PHONE_NUMBERS } from "../../graphql/phone-number-opt-out.queries"; -import PhoneNumberFormatter from "../../utils/PhoneFormatter"; +import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries"; import { TimeAgoFormatter } from "../../utils/DateFormatter"; -import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import ChatOpenButton from "../chat-open-button/chat-open-button.component"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; -const { Paragraph } = Typography; // Destructure Paragraph from Typography +const { Paragraph } = Typography; + +// Commented out Associated Owners section for now +//import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +//import { Link } from "react-router-dom"; +//import { useMemo, useState } from "react"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -30,7 +33,8 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) { fetchPolicy: "network-only" }); - // Prepare phone numbers for owner query + // Commented out Associated Owners section for now + /*// Prepare phone numbers for owner query const phoneNumbers = useMemo(() => { return optOutData?.phone_number_opt_out?.map((item) => item.phone_number) || []; }, [optOutData?.phone_number_opt_out]); @@ -74,16 +78,17 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) { : t("consent.phone_2") : null })); - }; + };*/ const columns = [ { title: t("consent.phone_number"), dataIndex: "phone_number", - render: (text) => {text}, + render: (text) => , sorter: (a, b) => a.phone_number.localeCompare(b.phone_number) }, - { + // Commented out Associated Owners section for now + /*{ title: t("consent.associated_owners"), dataIndex: "phone_number", render: (phoneNumber) => { @@ -109,7 +114,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) { const bName = bOwners[0] ? `${bOwners[0].ownr_fn} ${bOwners[0].ownr_ln}` : ""; return aName.localeCompare(bName); } - }, + },*/ { title: t("consent.created_at"), dataIndex: "created_at", @@ -130,7 +135,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
diff --git a/client/src/graphql/conversations.queries.js b/client/src/graphql/conversations.queries.js index 2379eb922..598be73dd 100644 --- a/client/src/graphql/conversations.queries.js +++ b/client/src/graphql/conversations.queries.js @@ -43,6 +43,7 @@ export const CONVERSATION_SUBSCRIPTION_BY_PK = gql` id status text + is_system isoutbound image image_path @@ -77,6 +78,7 @@ export const GET_CONVERSATION_DETAILS = gql` id status text + is_system isoutbound image image_path diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index c10ac7604..92d857181 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -4742,6 +4742,7 @@ - id - image - image_path + - is_system - isoutbound - msid - read diff --git a/hasura/migrations/1748443780878_alter_table_public_messages_add_column_is_system/down.sql b/hasura/migrations/1748443780878_alter_table_public_messages_add_column_is_system/down.sql new file mode 100644 index 000000000..0fbcf43ae --- /dev/null +++ b/hasura/migrations/1748443780878_alter_table_public_messages_add_column_is_system/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."messages" add column "is_system" boolean +-- null default 'false'; diff --git a/hasura/migrations/1748443780878_alter_table_public_messages_add_column_is_system/up.sql b/hasura/migrations/1748443780878_alter_table_public_messages_add_column_is_system/up.sql new file mode 100644 index 000000000..5c568631b --- /dev/null +++ b/hasura/migrations/1748443780878_alter_table_public_messages_add_column_is_system/up.sql @@ -0,0 +1,2 @@ +alter table "public"."messages" add column "is_system" boolean + null default 'false'; diff --git a/server/sms/receive.js b/server/sms/receive.js index a9670b76a..2df82047c 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -15,7 +15,13 @@ const InstanceManager = require("../utils/instanceMgr").default; // Note: When we handle different languages, we might need to adjust these keywords accordingly. const optInKeywords = ["START", "YES", "UNSTOP"]; const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT", "REVOKE", "OPTOUT"]; - + +// System Message text, will also need to be localized if we support multiple languages +const systemMessageOptions = { + optIn: "Customer has Opted-in", + optOut: "Customer has Opted-out" +}; + /** * Receive SMS messages from Twilio and process them * @param req @@ -61,7 +67,38 @@ const receive = async (req, res) => { const normalizedPhone = phone(req.body.From).phoneNumber.replace(/^\+1/, ""); // Normalize phone number (remove +1 for CA numbers) const messageText = (req.body.Body || "").trim().toUpperCase(); - // Step 2: Check for opt-in or opt-out keywords + // Step 2: Process conversation + const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); + const existingConversation = sortedConversations.length + ? sortedConversations[sortedConversations.length - 1] + : null; + + let conversationid; + + if (existingConversation) { + conversationid = existingConversation.id; + if (existingConversation.archived) { + await client.request(UNARCHIVE_CONVERSATION, { + id: conversationid, + archived: false + }); + } + } else { + const newConversationResponse = await client.request(CREATE_CONVERSATION, { + conversation: { + bodyshopid: bodyshop.id, + phone_num: phone(req.body.From).phoneNumber, + archived: false + } + }); + const createdConversation = newConversationResponse.insert_conversations.returning[0]; + conversationid = createdConversation.id; + } + + // Step 3: Handle opt-in or opt-out keywords + let systemMessageText = ""; + let socketEventType = ""; + if (optInKeywords.includes(messageText) || optOutKeywords.includes(messageText)) { // Check if the phone number is in phone_number_opt_out const optOutCheck = await client.request(CHECK_PHONE_NUMBER_OPT_OUT, { @@ -69,6 +106,7 @@ const receive = async (req, res) => { phone_number: normalizedPhone }); + // Opt In if (optInKeywords.includes(messageText)) { // Handle opt-in if (optOutCheck.phone_number_opt_out.length > 0) { @@ -85,14 +123,12 @@ const receive = async (req, res) => { affected_rows: deleteResponse.delete_phone_number_opt_out.affected_rows }); - // Emit WebSocket event to notify clients - const broadcastRoom = getBodyshopRoom(bodyshop.id); - ioRedis.to(broadcastRoom).emit("phone-number-opted-in", { - bodyshopid: bodyshop.id, - phone_number: normalizedPhone - }); + systemMessageText = systemMessageOptions.optIn; + socketEventType = "phone-number-opted-in"; } - } else if (optOutKeywords.includes(messageText)) { + } + // Opt Out + else if (optOutKeywords.includes(messageText)) { // Handle opt-out if (optOutCheck.phone_number_opt_out.length === 0) { // Phone number is not opted out; insert a new record @@ -115,59 +151,78 @@ const receive = async (req, res) => { affected_rows: insertResponse.insert_phone_number_opt_out.affected_rows }); - // Emit WebSocket event to notify clients - const broadcastRoom = getBodyshopRoom(bodyshop.id); - ioRedis.to(broadcastRoom).emit("phone-number-opted-out", { - bodyshopid: bodyshop.id, - phone_number: normalizedPhone - }); + systemMessageText = systemMessageOptions.optOut; + socketEventType = "phone-number-opted-out"; } } - // Respond immediately without processing as a regular message - res.status(200).send(""); - return; + // Insert system message if an opt-in or opt-out action was taken + if (systemMessageText) { + const systemMessage = { + msid: `SYS_${req.body.SmsMessageSid}_${Date.now()}`, // Unique ID for system message + text: systemMessageText, + conversationid, + isoutbound: false, + userid: null, + image: false, + image_path: null, + is_system: true + }; + + const systemMessageResponse = await client.request(INSERT_MESSAGE, { + msg: systemMessage, + conversationid + }); + + const insertedSystemMessage = systemMessageResponse.insert_messages.returning[0]; + + // Emit WebSocket events for system message + const broadcastRoom = getBodyshopRoom(bodyshop.id); + const conversationRoom = getBodyshopConversationRoom({ + bodyshopId: bodyshop.id, + conversationId: conversationid + }); + + const systemPayload = { + isoutbound: false, + conversationId: conversationid, + updated_at: insertedSystemMessage.updated_at, + msid: insertedSystemMessage.msid, + existingConversation: !!existingConversation, + newConversation: !existingConversation ? insertedSystemMessage.conversation : null + }; + + ioRedis.to(broadcastRoom).emit("new-message-summary", { + ...systemPayload, + summary: true + }); + + ioRedis.to(conversationRoom).emit("new-message-detailed", { + newMessage: insertedSystemMessage, + ...systemPayload, + summary: false + }); + + // Emit opt-in or opt-out event + ioRedis.to(broadcastRoom).emit(socketEventType, { + bodyshopid: bodyshop.id, + phone_number: normalizedPhone + }); + } } - // Step 3: Process conversation - const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); - const existingConversation = sortedConversations.length - ? sortedConversations[sortedConversations.length - 1] - : null; - - let conversationid; - let newMessage = { + // Step 4: Insert the original message + const newMessage = { msid: req.body.SmsMessageSid, text: req.body.Body, image: !!req.body.MediaUrl0, image_path: generateMediaArray(req.body, logger), isoutbound: false, - userid: null + userid: null, + conversationid, + is_system: false }; - if (existingConversation) { - conversationid = existingConversation.id; - if (existingConversation.archived) { - await client.request(UNARCHIVE_CONVERSATION, { - id: conversationid, - archived: false - }); - } - } else { - const newConversationResponse = await client.request(CREATE_CONVERSATION, { - conversation: { - bodyshopid: bodyshop.id, - phone_num: phone(req.body.From).phoneNumber, - archived: false - } - }); - const createdConversation = newConversationResponse.insert_conversations.returning[0]; - conversationid = createdConversation.id; - } - - newMessage.conversationid = conversationid; - - // Step 4: Insert the message const insertresp = await client.request(INSERT_MESSAGE, { msg: newMessage, conversationid @@ -180,7 +235,7 @@ const receive = async (req, res) => { throw new Error("Conversation data is missing from the response."); } - // Step 5: Notify clients + // Step 5: Notify clients for original message const conversationRoom = getBodyshopConversationRoom({ bodyshopId: conversation.bodyshop.id, conversationId: conversation.id @@ -190,7 +245,7 @@ const receive = async (req, res) => { isoutbound: false, conversationId: conversation.id, updated_at: message.updated_at, - msid: message.sid + msid: message.msid }; const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id); From da7e637183c076380251f004cb5960cc9474ec39 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 28 May 2025 12:26:48 -0400 Subject: [PATCH 36/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../owner-detail-form/owner-detail-form.container.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/components/owner-detail-form/owner-detail-form.container.jsx b/client/src/components/owner-detail-form/owner-detail-form.container.jsx index fdf2df339..d1f34b82b 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.container.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.container.jsx @@ -25,14 +25,14 @@ function OwnerDetailFormContainer({ owner, refetch }) { }); console.log(result); if (result.errors) { - notification["error"]({ + notification.error({ message: t("owners.errors.deleting", { error: JSON.stringify(result.errors) }) }); setLoading(false); } else { - notification["success"]({ + notification.success({ message: t("owners.successes.delete") }); setLoading(false); @@ -47,7 +47,7 @@ function OwnerDetailFormContainer({ owner, refetch }) { }); if (!!result.errors) { - notification["error"]({ + notification.error({ message: t("owners.errors.saving", { error: JSON.stringify(result.errors) }) @@ -56,7 +56,7 @@ function OwnerDetailFormContainer({ owner, refetch }) { return; } - notification["success"]({ + notification.success({ message: t("owners.successes.save") }); From 412efb06e5da986a6a2ade84b64c03689144eaa9 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 28 May 2025 13:07:11 -0400 Subject: [PATCH 37/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../owner-detail-form.component.jsx | 69 +++++++--- .../owner-detail-form.container.jsx | 126 +++++++++++++----- client/src/utils/phoneOptOutService.js | 44 ++++++ 3 files changed, 187 insertions(+), 52 deletions(-) create mode 100644 client/src/utils/phoneOptOutService.js diff --git a/client/src/components/owner-detail-form/owner-detail-form.component.jsx b/client/src/components/owner-detail-form/owner-detail-form.component.jsx index a71cbc836..99bacd566 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.component.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.component.jsx @@ -1,14 +1,15 @@ -import { Form, Input } from "antd"; -import React from "react"; +import { Form, Input, Tooltip } from "antd"; +import { CloseCircleFilled } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component"; -import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; -export default function OwnerDetailFormComponent({ form, loading }) { +export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedOut, isPhone2OptedOut }) { const { t } = useTranslation(); const { getFieldValue } = form; + return (
@@ -62,19 +63,55 @@ export default function OwnerDetailFormComponent({ form, loading }) { > - PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]} - > - + +
+ PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]} + > + + + {isPhone1OptedOut && ( + + + + )} +
- PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]} - > - + +
+ PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]} + > + + + {isPhone2OptedOut && ( + + + + )} +
diff --git a/client/src/components/owner-detail-form/owner-detail-form.container.jsx b/client/src/components/owner-detail-form/owner-detail-form.container.jsx index d1f34b82b..8bb0bab85 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.container.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.container.jsx @@ -1,69 +1,113 @@ import { Button, Form, Popconfirm } from "antd"; import { PageHeader } from "@ant-design/pro-layout"; - -import React, { useState } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { useMutation } from "@apollo/client"; +import { useApolloClient, useMutation } from "@apollo/client"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; // Adjust path +import { checkPhoneOptOutStatus } from "../../utils/phoneOptOutService.js"; // Adjust path import OwnerDetailFormComponent from "./owner-detail-form.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { phone } from "phone"; // Import phone utility for formatting -function OwnerDetailFormContainer({ owner, refetch }) { +// Connect to Redux to access bodyshop +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +function OwnerDetailFormContainer({ owner, refetch, bodyshop }) { const { t } = useTranslation(); const [form] = Form.useForm(); - const history = useNavigate(); + const navigate = useNavigate(); const [loading, setLoading] = useState(false); + const [optedOutPhones, setOptedOutPhones] = useState(new Set()); const [updateOwner] = useMutation(UPDATE_OWNER); const [deleteOwner] = useMutation(DELETE_OWNER); const notification = useNotification(); + const apolloClient = useApolloClient(); + + // Fetch opt-out status on mount + useEffect(() => { + const fetchOptOutStatus = async () => { + if (bodyshop?.id && (owner?.ownr_ph1 || owner?.ownr_ph2)) { + const phoneNumbers = [owner.ownr_ph1, owner.ownr_ph2].filter(Boolean); + const optOutSet = await checkPhoneOptOutStatus(apolloClient, bodyshop.id, phoneNumbers); + setOptedOutPhones(optOutSet); + } + }; + + fetchOptOutStatus(); + }, [apolloClient, bodyshop?.id, owner?.ownr_ph1, owner?.ownr_ph2]); + + // Reset form fields when owner changes + useEffect(() => { + form.setFieldsValue({ + ownr_ph1: owner?.ownr_ph1, + ownr_ph2: owner?.ownr_ph2, + ...owner + }); + }, [owner, form]); const handleDelete = async () => { setLoading(true); - const result = await deleteOwner({ - variables: { id: owner.id } - }); - console.log(result); - if (result.errors) { + try { + const result = await deleteOwner({ + variables: { id: owner.id } + }); + if (result.errors) { + notification.error({ + message: t("owners.errors.deleting", { + error: JSON.stringify(result.errors) + }) + }); + } else { + notification.success({ + message: t("owners.successes.delete") + }); + navigate(`/manage/owners`); + } + } catch (error) { notification.error({ message: t("owners.errors.deleting", { - error: JSON.stringify(result.errors) + error: error.message }) }); + } finally { setLoading(false); - } else { - notification.success({ - message: t("owners.successes.delete") - }); - setLoading(false); - history(`/manage/owners`); } }; const handleFinish = async (values) => { setLoading(true); - const result = await updateOwner({ - variables: { ownerId: owner.id, owner: values } - }); - - if (!!result.errors) { + try { + const result = await updateOwner({ + variables: { ownerId: owner.id, owner: values } + }); + if (result.errors) { + notification.error({ + message: t("owners.errors.saving", { + error: JSON.stringify(result.errors) + }) + }); + } else { + notification.success({ + message: t("owners.successes.save") + }); + if (refetch) await refetch(); + form.resetFields(); + } + } catch (error) { notification.error({ message: t("owners.errors.saving", { - error: JSON.stringify(result.errors) + error: error.message }) }); + } finally { setLoading(false); - return; } - - notification.success({ - message: t("owners.successes.save") - }); - - if (refetch) await refetch(); - form.resetFields(); - form.resetFields(); - setLoading(false); }; return ( @@ -72,6 +116,7 @@ function OwnerDetailFormContainer({ owner, refetch }) { title={t("menus.header.owners")} extra={[ , - ]} />
- + ); } -export default OwnerDetailFormContainer; +export default connect(mapStateToProps)(OwnerDetailFormContainer); diff --git a/client/src/utils/phoneOptOutService.js b/client/src/utils/phoneOptOutService.js new file mode 100644 index 000000000..037e21c11 --- /dev/null +++ b/client/src/utils/phoneOptOutService.js @@ -0,0 +1,44 @@ +import { phone } from "phone"; +import { GET_PHONE_NUMBER_OPT_OUT } from "../graphql/phone-number-opt-out.queries"; + +/** + * Check if phone numbers are opted out for a given bodyshop + * @param {Object} apolloClient - Apollo Client instance + * @param {string} bodyshopId - The ID of the bodyshop + * @param {string[]} phoneNumbers - Array of phone numbers to check + * @returns {Promise>} - Set of normalized opted-out phone numbers + */ +export const checkPhoneOptOutStatus = async (apolloClient, bodyshopId, phoneNumbers) => { + if (!apolloClient || !bodyshopId || !phoneNumbers?.length) { + return new Set(); + } + + // Normalize phone numbers (remove +1 for CA numbers) + const normalizedPhones = phoneNumbers + .filter(Boolean) + .map((num) => phone(num, "CA").phoneNumber?.replace(/^\+1/, "")) + .filter(Boolean); + + const optedOutPhones = new Set(); + + for (const phoneNum of normalizedPhones) { + try { + const { data } = await apolloClient.query({ + query: GET_PHONE_NUMBER_OPT_OUT, + variables: { + bodyshopid: bodyshopId, + phone_number: phoneNum // Single string + }, + fetchPolicy: "network-only" + }); + + if (data?.phone_number_opt_out?.length) { + optedOutPhones.add(phoneNum); + } + } catch (error) { + console.error(`Error checking opt-out for ${phoneNum}:`, error); + } + } + + return optedOutPhones; +}; From 9466d36e69284a1cfce682e2b8318bd62be53cac Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 28 May 2025 13:17:21 -0400 Subject: [PATCH 38/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../owner-detail-form.container.jsx | 4 +- .../graphql/phone-number-opt-out.queries.js | 14 +++++++ client/src/utils/phoneOptOutService.js | 38 ++++++++++--------- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/client/src/components/owner-detail-form/owner-detail-form.container.jsx b/client/src/components/owner-detail-form/owner-detail-form.container.jsx index 8bb0bab85..a92913d30 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.container.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.container.jsx @@ -8,7 +8,7 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; // Adjust path -import { checkPhoneOptOutStatus } from "../../utils/phoneOptOutService.js"; // Adjust path +import { phoneNumberOptOutService } from "../../utils/phoneOptOutService.js"; // Adjust path import OwnerDetailFormComponent from "./owner-detail-form.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { phone } from "phone"; // Import phone utility for formatting @@ -34,7 +34,7 @@ function OwnerDetailFormContainer({ owner, refetch, bodyshop }) { const fetchOptOutStatus = async () => { if (bodyshop?.id && (owner?.ownr_ph1 || owner?.ownr_ph2)) { const phoneNumbers = [owner.ownr_ph1, owner.ownr_ph2].filter(Boolean); - const optOutSet = await checkPhoneOptOutStatus(apolloClient, bodyshop.id, phoneNumbers); + const optOutSet = await phoneNumberOptOutService(apolloClient, bodyshop.id, phoneNumbers); setOptedOutPhones(optOutSet); } }; diff --git a/client/src/graphql/phone-number-opt-out.queries.js b/client/src/graphql/phone-number-opt-out.queries.js index 123d06744..25f982fd1 100644 --- a/client/src/graphql/phone-number-opt-out.queries.js +++ b/client/src/graphql/phone-number-opt-out.queries.js @@ -27,6 +27,20 @@ export const GET_PHONE_NUMBER_OPT_OUTS = gql` } `; +export const GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS = gql` + query GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS($bodyshopid: uuid!, $phone_numbers: [String!]) { + phone_number_opt_out( + where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _in: $phone_numbers } } + ) { + id + bodyshopid + phone_number + created_at + updated_at + } + } +`; + export const SEARCH_OWNERS_BY_PHONE_NUMBERS = gql` query SEARCH_OWNERS_BY_PHONE_NUMBERS($bodyshopid: uuid!, $phone_numbers: [String!]) { owners( diff --git a/client/src/utils/phoneOptOutService.js b/client/src/utils/phoneOptOutService.js index 037e21c11..9abbef82f 100644 --- a/client/src/utils/phoneOptOutService.js +++ b/client/src/utils/phoneOptOutService.js @@ -1,5 +1,5 @@ import { phone } from "phone"; -import { GET_PHONE_NUMBER_OPT_OUT } from "../graphql/phone-number-opt-out.queries"; +import { GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS } from "../graphql/phone-number-opt-out.queries"; /** * Check if phone numbers are opted out for a given bodyshop @@ -8,7 +8,7 @@ import { GET_PHONE_NUMBER_OPT_OUT } from "../graphql/phone-number-opt-out.querie * @param {string[]} phoneNumbers - Array of phone numbers to check * @returns {Promise>} - Set of normalized opted-out phone numbers */ -export const checkPhoneOptOutStatus = async (apolloClient, bodyshopId, phoneNumbers) => { +export const phoneNumberOptOutService = async (apolloClient, bodyshopId, phoneNumbers) => { if (!apolloClient || !bodyshopId || !phoneNumbers?.length) { return new Set(); } @@ -19,25 +19,29 @@ export const checkPhoneOptOutStatus = async (apolloClient, bodyshopId, phoneNumb .map((num) => phone(num, "CA").phoneNumber?.replace(/^\+1/, "")) .filter(Boolean); + if (!normalizedPhones.length) { + return new Set(); + } + const optedOutPhones = new Set(); - for (const phoneNum of normalizedPhones) { - try { - const { data } = await apolloClient.query({ - query: GET_PHONE_NUMBER_OPT_OUT, - variables: { - bodyshopid: bodyshopId, - phone_number: phoneNum // Single string - }, - fetchPolicy: "network-only" - }); + try { + const { data } = await apolloClient.query({ + query: GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS, + variables: { + bodyshopid: bodyshopId, + phone_numbers: normalizedPhones // Array of phone numbers + }, + fetchPolicy: "network-only" + }); - if (data?.phone_number_opt_out?.length) { - optedOutPhones.add(phoneNum); - } - } catch (error) { - console.error(`Error checking opt-out for ${phoneNum}:`, error); + if (data?.phone_number_opt_out?.length) { + data.phone_number_opt_out.forEach((optOut) => { + optedOutPhones.add(optOut.phone_number); + }); } + } catch (error) { + console.error("Error checking opt-out statuses:", error); } return optedOutPhones; From 94bdc6c43f82357d6ab7a9e905a23da7877b96d6 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 28 May 2025 13:45:12 -0400 Subject: [PATCH 39/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- client/src/translations/en_us/common.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index fd3dd5134..178b4ceaf 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2381,7 +2381,7 @@ "invalidphone": "The phone number is invalid. Unable to open conversation. ", "noattachedjobs": "No Jobs have been associated to this conversation. ", "updatinglabel": "Error updating label. {{error}}", - "no_consent": "This phone number has Opted-out of Messaging." + "no_consent": "This phone number has opted-out of Messaging." }, "labels": { "addlabel": "Add a label to this conversation.", @@ -2398,7 +2398,7 @@ "sentby": "Sent by {{by}} at {{time}}", "typeamessage": "Send a message...", "unarchive": "Unarchive", - "no_consent": "Opt-out" + "no_consent": "Opted-out" }, "render": { "conversation_list": "Conversation List" From 909a21023aa912723cb032036781720a09963db6 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 28 May 2025 14:01:24 -0400 Subject: [PATCH 40/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../chat-media-selector.component.jsx | 25 +++++---- .../chat-media-selector.styles.scss | 52 +++++++++++++++++++ 2 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 client/src/components/chat-media-selector/chat-media-selector.styles.scss 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 162789fb6..95a917e7e 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 @@ -13,13 +13,14 @@ import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-document import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component"; import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; +import "./chat-media-selector.styles.scss"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); + +const mapDispatchToProps = (dispatch) => ({}); + export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector); export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) { @@ -37,9 +38,8 @@ 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]?.jobid }, - skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0 }); @@ -56,11 +56,11 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c //If Imageproxy is on, rely only on the LMS selector //If not on, use the old methods. const content = ( -
+
{loading && } {error && } {selectedMedia.filter((s) => s.isSelected).length >= 10 ? ( -
{t("messaging.labels.maxtenimages")}
+
{t("messaging.labels.maxtenimages")}
) : null} {Imgproxy.treatment === "on" ? ( @@ -74,7 +74,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c {bodyshop.uselocalmediaserver && open && ( )} @@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c {bodyshop.uselocalmediaserver && open && ( )} @@ -100,12 +100,17 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c return ( {t("messaging.errors.noattachedjobs")}
: content + conversation.job_conversations.length === 0 ? ( +
{t("messaging.errors.noattachedjobs")}
+ ) : ( + content + ) } title={t("messaging.labels.selectmedia")} trigger="click" open={open} onOpenChange={handleVisibleChange} + overlayClassName="media-selector-popover" > s.isSelected).length}> diff --git a/client/src/components/chat-media-selector/chat-media-selector.styles.scss b/client/src/components/chat-media-selector/chat-media-selector.styles.scss new file mode 100644 index 000000000..58e2d06b4 --- /dev/null +++ b/client/src/components/chat-media-selector/chat-media-selector.styles.scss @@ -0,0 +1,52 @@ +.media-selector-popover { + .ant-popover-inner-content { + max-width: 640px; + max-height: 480px; + overflow-y: auto; + padding: 8px; + background-color: #fff; + border-radius: 8px; + } +} + +.media-selector-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.error-message { + color: red; + font-size: 12px; + text-align: center; + margin-bottom: 8px; +} + +.no-jobs-message { + font-size: 14px; + color: #888; + text-align: center; + padding: 8px; +} + +/* Style images within gallery components */ +.media-selector-content img { + + object-fit: cover; + border-radius: 4px; + margin: 4px; + cursor: pointer; + transition: transform 0.2s; + + &:hover { + transform: scale(1.05); + } +} + +/* Grid layout for gallery components */ +.media-selector-content .ant-image, /* Assuming gallery components use Ant Design's Image */ +.media-selector-content .gallery-container { /* Fallback for custom gallery classes */ + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 4px; +} From 64a280b111e10e67697d55df5cc698115d4c3682 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 28 May 2025 13:26:13 -0700 Subject: [PATCH 41/45] IO-3251 Quick Intake Jobs at Change Signed-off-by: Allan Carr --- .../schedule-event.component.jsx | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/client/src/components/job-at-change/schedule-event.component.jsx b/client/src/components/job-at-change/schedule-event.component.jsx index 6f7780ba8..6438308da 100644 --- a/client/src/components/job-at-change/schedule-event.component.jsx +++ b/client/src/components/job-at-change/schedule-event.component.jsx @@ -395,32 +395,33 @@ export function ScheduleEventComponent({ ) : ( )} - {event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? ( - - - - ) : ( - { - if (event.job?.id) { - e.stopPropagation(); - getJobDetails(); - } - }} - getPopupContainer={(trigger) => trigger.parentNode} - trigger="click" - > - - - )} + {event.job && + (HasFeatureAccess({ featureName: "checklist", bodyshop }) ? ( + + + + ) : ( + { + if (event.job?.id) { + e.stopPropagation(); + getJobDetails(); + } + }} + getPopupContainer={(trigger) => trigger.parentNode} + trigger="click" + > + + + ))}
); From d52426f5f5f5411a2d270b75dd1af42ffadacd1c Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 28 May 2025 15:21:42 -0700 Subject: [PATCH 42/45] IO-3239 Additional logging fixes. --- server/accounting/qbo/qbo-receivables.js | 11 +++++++---- server/utils/logger.js | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index e2a0fa307..008e4b3f0 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -478,7 +478,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { exports.InsertJob = InsertJob; -async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) { +async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid, jobid) { const items = await oauthClient.makeApiCall({ url: urlBuilder(qbo_realmId, "query", `select * From Item where active=true maxresults 1000`), method: "POST", @@ -492,6 +492,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) { name: "QueryItems", status: items.response?.status, bodyshopid, + jobid: jobid, email: req.user.email }) setNewRefreshToken(req.user.email, items); @@ -508,6 +509,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) { name: "QueryTaxCodes", status: taxCodes.response?.status, bodyshopid, + jobid: jobid, email: req.user.email }) const classes = await oauthClient.makeApiCall({ @@ -523,6 +525,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) { name: "QueryClasses", status: classes.response?.status, bodyshopid, + jobid: jobid, email: req.user.email }) const taxCodeMapping = {}; @@ -559,7 +562,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) { } async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, parentTierRef) { - const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, job.shopid); + const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, job.shopid, job.id); const InvoiceLineAdd = CreateInvoiceLines({ bodyshop, jobs_by_pk: job, @@ -653,7 +656,7 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren platform: "QBO", method: "POST", name: "InsertInvoice", - status: result.status, + status: result.response?.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email @@ -778,7 +781,7 @@ async function InsertInvoiceMultiPayerInvoice( platform: "QBO", method: "POST", name: "InsertInvoice", - status: result.response.status, + status: result.response?.status, bodyshopid: job.shopid, jobid: job.id, email: req.user.email diff --git a/server/utils/logger.js b/server/utils/logger.js index 41d71cea6..ccb2e905e 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -206,7 +206,7 @@ const createLogger = () => { jobid, paymentid, billid, - status: status.toString() ?? "0", + status: status?.toString() ?? "0", bodyshopid, email } From a0566e76ab837fd7fdd4299b21559fe584a0a3e0 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 29 May 2025 13:49:56 -0400 Subject: [PATCH 43/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../owner-detail-form.container.jsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/src/components/owner-detail-form/owner-detail-form.container.jsx b/client/src/components/owner-detail-form/owner-detail-form.container.jsx index a92913d30..d78a11f04 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.container.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.container.jsx @@ -32,15 +32,17 @@ function OwnerDetailFormContainer({ owner, refetch, bodyshop }) { // Fetch opt-out status on mount useEffect(() => { const fetchOptOutStatus = async () => { - if (bodyshop?.id && (owner?.ownr_ph1 || owner?.ownr_ph2)) { + if (bodyshop?.id && bodyshop?.messagingservicesid && (owner?.ownr_ph1 || owner?.ownr_ph2)) { const phoneNumbers = [owner.ownr_ph1, owner.ownr_ph2].filter(Boolean); const optOutSet = await phoneNumberOptOutService(apolloClient, bodyshop.id, phoneNumbers); setOptedOutPhones(optOutSet); + } else { + setOptedOutPhones(new Set()); } }; fetchOptOutStatus(); - }, [apolloClient, bodyshop?.id, owner?.ownr_ph1, owner?.ownr_ph2]); + }, [apolloClient, bodyshop?.id, bodyshop?.messagingservicesid, owner?.ownr_ph1, owner?.ownr_ph2]); // Reset form fields when owner changes useEffect(() => { @@ -136,10 +138,14 @@ function OwnerDetailFormContainer({ owner, refetch, bodyshop }) { loading={loading} form={form} isPhone1OptedOut={ - owner?.ownr_ph1 && optedOutPhones.has(phone(owner.ownr_ph1, "CA").phoneNumber?.replace(/^\+1/, "")) + bodyshop?.messagingservicesid && + owner?.ownr_ph1 && + optedOutPhones.has(phone(owner.ownr_ph1, "CA").phoneNumber?.replace(/^\+1/, "")) } isPhone2OptedOut={ - owner?.ownr_ph2 && optedOutPhones.has(phone(owner.ownr_ph2, "CA").phoneNumber?.replace(/^\+1/, "")) + bodyshop?.messagingservicesid && + owner?.ownr_ph2 && + optedOutPhones.has(phone(owner.ownr_ph2, "CA").phoneNumber?.replace(/^\+1/, "")) } /> From e2c5a4cba47a61841beb0ded5ccb6d011082d84d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 30 May 2025 14:14:05 -0400 Subject: [PATCH 44/45] feature/IO-3182-Phone-Number-Consent - Checkpoint --- server/sms/receive.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/sms/receive.js b/server/sms/receive.js index 2df82047c..e51007726 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -18,8 +18,8 @@ const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT // System Message text, will also need to be localized if we support multiple languages const systemMessageOptions = { - optIn: "Customer has Opted-in", - optOut: "Customer has Opted-out" + optIn: "Customer has opted-in", + optOut: "Customer has opted-out" }; /** From d835021069f43a35082f24d0ff114cdcee55e0b2 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 2 Jun 2025 11:03:58 -0700 Subject: [PATCH 45/45] IO-3092 Resolve imgproxy limit fetch. --- .../jobs-documents-gallery/jobs-documents-gallery.container.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx index 5bbd95f94..ea4489426 100644 --- a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx +++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx @@ -42,7 +42,7 @@ export function JobsDocumentsContainer({ variables: { jobId: jobId }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", - skip: Imgproxy.treatment === "on" || !!billId + skip: !!billId }); if (loading) return ;