@tiledesk/tiledesk-server 2.15.1 → 2.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,12 +12,20 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v2
16
- name: Check out the repo
17
- - uses: docker/build-push-action@v4
18
- with:
15
+ - name: Check out the repo
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Login to Docker Hub
19
+ uses: docker/login-action@v3
20
+ with:
19
21
  username: ${{ secrets.DOCKERHUB_USERNAME }}
20
22
  password: ${{ secrets.DOCKERHUB_TOKEN }}
21
- repository: tiledesk/tiledesk-server
22
- dockerfile: Dockerfile-profiler
23
- tags: latest-profiler
23
+
24
+ - name: Build and push
25
+ uses: docker/build-push-action@v6
26
+ with:
27
+ context: .
28
+ file: ./Dockerfile-profiler
29
+ push: true
30
+ tags: tiledesk/tiledesk-server:latest-profiler
31
+
@@ -12,11 +12,19 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v2
16
- name: Check out the repo
17
- - uses: docker/build-push-action@v4
18
- with:
15
+ - name: Check out the repo
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Login to Docker Hub
19
+ uses: docker/login-action@v3
20
+ with:
19
21
  username: ${{ secrets.DOCKERHUB_USERNAME }}
20
22
  password: ${{ secrets.DOCKERHUB_TOKEN }}
21
- repository: tiledesk/tiledesk-server
22
- tags: latest
23
+
24
+ - name: Build and push
25
+ uses: docker/build-push-action@v6
26
+ with:
27
+ context: .
28
+ file: ./Dockerfile
29
+ push: true
30
+ tags: tiledesk/tiledesk-server:latest
@@ -1,4 +1,4 @@
1
- name: Docker Image Community latest CI
1
+ name: Docker Image Community Worker latest CI
2
2
 
3
3
  on:
4
4
  push:
@@ -12,12 +12,19 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v2
16
- name: Check out the repo
17
- - uses: docker/build-push-action@v4
18
- with:
15
+ - name: Check out the repo
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Login to Docker Hub
19
+ uses: docker/login-action@v3
20
+ with:
19
21
  username: ${{ secrets.DOCKERHUB_USERNAME }}
20
22
  password: ${{ secrets.DOCKERHUB_TOKEN }}
21
- repository: tiledesk/tiledesk-server-worker
22
- dockerfile: Dockerfile-jobs
23
- tags: latest
23
+
24
+ - name: Build and push
25
+ uses: docker/build-push-action@v6
26
+ with:
27
+ context: .
28
+ file: ./Dockerfile-jobs
29
+ push: true
30
+ tags: tiledesk/tiledesk-server-worker:latest
@@ -11,15 +11,13 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  - name: Check out the repo
14
- uses: actions/checkout@v2
14
+ uses: actions/checkout@v4
15
15
 
16
16
  - name: Login to Docker Hub
17
17
  uses: docker/login-action@v3
18
18
  with:
19
19
  username: ${{ secrets.DOCKER_USERNAME }}
20
20
  password: ${{ secrets.DOCKER_PASSWORD }}
21
- repository: tiledeskrepo/tiledesk-server-enterprise
22
- tag_with_ref: true
23
21
 
24
22
  - name: Log Voice Token
25
23
  run: |
@@ -29,9 +27,13 @@ jobs:
29
27
  run: |
30
28
  echo "Voice Twilio token log: ${{ secrets.VOICE_TWILIO_TOKEN }}"
31
29
 
30
+ - name: Log Voice Enghouse Token
31
+ run: |
32
+ echo "Voice Enghouse token log: ${{ secrets.VOICE_ENGHOUSE_TOKEN }}"
33
+
32
34
  - name: Generate Docker metadata
33
35
  id: meta
34
- uses: docker/metadata-action@v3
36
+ uses: docker/metadata-action@v5
35
37
  with:
36
38
  images: tiledeskrepo/tiledesk-server-enterprise
37
39
  tags: |
@@ -39,7 +41,7 @@ jobs:
39
41
  type=semver,pattern={{version}}
40
42
 
41
43
  - name: Push to Docker Hub
42
- uses: docker/build-push-action@v4
44
+ uses: docker/build-push-action@v6
43
45
  with:
44
46
  context: .
45
47
  file: ./Dockerfile-en
@@ -48,4 +50,5 @@ jobs:
48
50
  NPM_TOKEN=${{ secrets.NPM_TOKEN }}
49
51
  VOICE_TOKEN=${{ secrets.VOICE_TOKEN }}
50
52
  VOICE_TWILIO_TOKEN=${{ secrets.VOICE_TWILIO_TOKEN }}
53
+ VOICE_ENGHOUSE_TOKEN=${{ secrets.VOICE_ENGHOUSE_TOKEN }}
51
54
  tags: ${{ steps.meta.outputs.tags }}
@@ -11,11 +11,28 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  - name: Check out the repo
14
- uses: actions/checkout@v2
15
- - name: Push to Docker Hub
16
- uses: docker/build-push-action@v4
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Login to Docker Hub
17
+ uses: docker/login-action@v3
17
18
  with:
18
19
  username: ${{ secrets.DOCKERHUB_USERNAME }}
19
20
  password: ${{ secrets.DOCKERHUB_TOKEN }}
20
- repository: tiledesk/tiledesk-server
21
- tag_with_ref: true
21
+
22
+ - name: Generate Docker metadata
23
+ id: meta
24
+ uses: docker/metadata-action@v5
25
+ with:
26
+ images: tiledesk/tiledesk-server
27
+ tags: |
28
+ type=ref,event=branch
29
+ type=semver,pattern={{version}}
30
+
31
+ - name: Build and push
32
+ uses: docker/build-push-action@v6
33
+ with:
34
+ context: .
35
+ file: ./Dockerfile
36
+ push: true
37
+ tags: ${{ steps.meta.outputs.tags }}
38
+
@@ -1,4 +1,4 @@
1
- name: Publish Docker Community image tags
1
+ name: Publish Docker Community Worker image tags
2
2
 
3
3
  on:
4
4
  push:
@@ -11,12 +11,27 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  - name: Check out the repo
14
- uses: actions/checkout@v2
15
- - name: Push to Docker Hub
16
- uses: docker/build-push-action@v4
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Login to Docker Hub
17
+ uses: docker/login-action@v3
17
18
  with:
18
19
  username: ${{ secrets.DOCKERHUB_USERNAME }}
19
20
  password: ${{ secrets.DOCKERHUB_TOKEN }}
20
- dockerfile: Dockerfile-jobs
21
- repository: tiledesk/tiledesk-server-worker
22
- tag_with_ref: true
21
+
22
+ - name: Generate Docker metadata
23
+ id: meta
24
+ uses: docker/metadata-action@v5
25
+ with:
26
+ images: tiledesk/tiledesk-server-worker
27
+ tags: |
28
+ type=ref,event=branch
29
+ type=semver,pattern={{version}}
30
+
31
+ - name: Build and push
32
+ uses: docker/build-push-action@v6
33
+ with:
34
+ context: .
35
+ file: ./Dockerfile-jobs
36
+ push: true
37
+ tags: ${{ steps.meta.outputs.tags }}
@@ -12,13 +12,22 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v2
16
- name: Check out the repo
17
- - uses: docker/build-push-action@v4
18
- with:
19
- username: ${{ secrets.DOCKER_USERNAME }}
20
- password: ${{ secrets.DOCKER_PASSWORD }}
21
- build_args: NPM_TOKEN=${{ secrets.NPM_TOKEN }}
22
- dockerfile: Dockerfile-en
23
- repository: tiledeskrepo/tiledesk-server-enterprise
24
- tags: latest
15
+ - name: Check out the repo
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Login to Docker Hub
19
+ uses: docker/login-action@v3
20
+ with:
21
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
22
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
23
+
24
+ - name: Build and push
25
+ uses: docker/build-push-action@v6
26
+ with:
27
+ context: .
28
+ file: ./Dockerfile-en
29
+ push: true
30
+ build-args: |
31
+ NPM_TOKEN=${{ secrets.NPM_TOKEN }}
32
+ tags: tiledeskrepo/tiledesk-server-enterprise
33
+
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
+ # 2.15.3
9
+ - Updated whatsapp-connector to 1.0.25
10
+ - Updated sms-connector to 0.1.13
11
+ - Bug fix: join a conversation with a note without text.
12
+ - Added phone number filter when searching for conversations in history
13
+ - Added migration script to add the contact field in request object improving the search by phone number
14
+
15
+ # 2.15.2
16
+ - Updated GitHub actions
17
+
8
18
  # 2.15.1
9
19
  - Updated whatsapp-connector to 1.0.24
10
20
 
@@ -0,0 +1,80 @@
1
+ var winston = require('../config/winston');
2
+ const Request = require('../models/request');
3
+ const phoneUtil = require('../utils/phoneUtil');
4
+
5
+ const VOICE_CHANNEL_NAMES = ['voice_twilio', 'voice-twilio', 'voice-vxml', 'voice-vxml-enghouse'];
6
+ const BATCH_SIZE = 100;
7
+
8
+ async function updateManyWithNormalizedPhone(filter, getPhoneFromDoc) {
9
+ let matched = 0;
10
+ let modified = 0;
11
+ const cursor = Request.find(filter).select('_id attributes createdBy').lean().cursor();
12
+ let batch = [];
13
+ for await (const doc of cursor) {
14
+ const rawPhone = getPhoneFromDoc(doc);
15
+ if (rawPhone == null || String(rawPhone).trim() === '') continue;
16
+ matched++;
17
+ const normalized = phoneUtil.normalizePhone(rawPhone);
18
+ const value = normalized != null && normalized !== '' ? normalized : String(rawPhone).trim();
19
+ if (value === '') continue;
20
+ batch.push({
21
+ updateOne: {
22
+ filter: { _id: doc._id },
23
+ update: { $set: { 'contact.phone': value } }
24
+ }
25
+ });
26
+ if (batch.length >= BATCH_SIZE) {
27
+ const result = await Request.bulkWrite(batch);
28
+ modified += result.modifiedCount;
29
+ batch = [];
30
+ }
31
+ }
32
+ if (batch.length > 0) {
33
+ const result = await Request.bulkWrite(batch);
34
+ modified += result.modifiedCount;
35
+ }
36
+ return { matched, modified };
37
+ }
38
+
39
+ async function up() {
40
+ try {
41
+ // Voice channels
42
+ const voiceFilter = {
43
+ 'channel.name': { $in: VOICE_CHANNEL_NAMES },
44
+ 'attributes.caller_phone': { $exists: true, $nin: [null, ''] }
45
+ };
46
+ const voiceResult = await updateManyWithNormalizedPhone(voiceFilter, (doc) => doc.attributes?.caller_phone);
47
+ winston.info(
48
+ `[phone-channels-migration] Voice channels: matched ${voiceResult.matched}, modified ${voiceResult.modified}`
49
+ );
50
+
51
+ // WhatsApp
52
+ const wabFilter = {
53
+ 'channel.name': 'whatsapp',
54
+ createdBy: { $regex: /^wab-/ }
55
+ };
56
+ const wabResult = await updateManyWithNormalizedPhone(wabFilter, (doc) =>
57
+ doc.createdBy && doc.createdBy.startsWith('wab-') ? doc.createdBy.replace(/^wab-/, '') : null
58
+ );
59
+ winston.info(
60
+ `[phone-channels-migration] WhatsApp: matched ${wabResult.matched}, modified ${wabResult.modified}`
61
+ );
62
+
63
+ // SMS-Twilio
64
+ const smsFilter = {
65
+ 'channel.name': 'sms-twilio',
66
+ createdBy: { $regex: /^sms-twilio-/ }
67
+ };
68
+ const smsResult = await updateManyWithNormalizedPhone(smsFilter, (doc) =>
69
+ doc.createdBy && doc.createdBy.startsWith('sms-twilio-') ? doc.createdBy.replace(/^sms-twilio-/, '') : null
70
+ );
71
+ winston.info(
72
+ `[phone-channels-migration] SMS-Twilio: matched ${smsResult.matched}, modified ${smsResult.modified}`
73
+ );
74
+ } catch (err) {
75
+ winston.error('[phone-channels-migration] Error:', err);
76
+ throw err;
77
+ }
78
+ }
79
+
80
+ module.exports = { up };
@@ -0,0 +1,26 @@
1
+ const mongoose = require('mongoose');
2
+ const Schema = mongoose.Schema;
3
+
4
+ const ContactSchema = new Schema({
5
+ phone: {
6
+ type: String,
7
+ required: false,
8
+ trim: true
9
+ },
10
+ email: {
11
+ type: String,
12
+ required: false,
13
+ trim: true,
14
+ lowercase: true
15
+ },
16
+ external_id: {
17
+ type: String,
18
+ required: false,
19
+ trim: true
20
+ }
21
+ }, {
22
+ timestamps: false,
23
+ _id: false
24
+ });
25
+
26
+ module.exports = ContactSchema;
package/models/request.js CHANGED
@@ -14,7 +14,7 @@ var NoteSchema = require("../models/note").schema;
14
14
  var TagSchema = require("../models/tag");
15
15
  var LocationSchema = require("../models/location");
16
16
  var RequestSnapshotSchema = require("../models/requestSnapshot");
17
-
17
+ var ContactSchema = require("../models/contact");
18
18
  var defaultFullTextLanguage = process.env.DEFAULT_FULLTEXT_INDEX_LANGUAGE || "none";
19
19
  winston.info("Request defaultFullTextLanguage: "+ defaultFullTextLanguage);
20
20
 
@@ -304,8 +304,8 @@ var RequestSchema = new Schema({
304
304
  type: Boolean,
305
305
  required: false,
306
306
  index: true
307
- }
308
-
307
+ },
308
+ contact: ContactSchema,
309
309
  }, {
310
310
  timestamps: true,
311
311
  toObject: { virtuals: true }, //IMPORTANT FOR trigger used to polulate messages in toJSON// https://mongoosejs.com/docs/populate.html
@@ -508,6 +508,10 @@ RequestSchema.index({ id_project: 1, preflight: 1, smartAssignment: 1, "snapshot
508
508
 
509
509
  RequestSchema.index({ status: 1, hasBot: 1, updatedAt: 1 }) // For closing unresponsive requests
510
510
 
511
+ // Contact search by phone / email
512
+ RequestSchema.index({ id_project: 1, 'contact.phone': 1 });
513
+ RequestSchema.index({ id_project: 1, 'contact.email': 1 });
514
+
511
515
  // ERROR DURING DEPLOY OF 2.10.27
512
516
  //RequestSchema.index({ id_project: 1, participants: 1, "snapshot.agents.id_user": 1, createdAt: -1, status: 1 })
513
517
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-server",
3
3
  "description": "The Tiledesk server module",
4
- "version": "2.15.1",
4
+ "version": "2.15.3",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -47,12 +47,12 @@
47
47
  "@tiledesk/tiledesk-messenger-connector": "^0.1.28",
48
48
  "@tiledesk/tiledesk-multi-worker": "^0.3.3",
49
49
  "@tiledesk/tiledesk-rasa-connector": "^1.0.10",
50
- "@tiledesk/tiledesk-sms-connector": "^0.1.11",
50
+ "@tiledesk/tiledesk-sms-connector": "^0.1.13",
51
51
  "@tiledesk/tiledesk-telegram-connector": "^0.1.14",
52
52
  "@tiledesk/tiledesk-tybot-connector": "^2.0.44",
53
53
  "@tiledesk/tiledesk-voice-twilio-connector": "^0.1.28",
54
54
  "@tiledesk/tiledesk-vxml-connector": "^0.1.89",
55
- "@tiledesk/tiledesk-whatsapp-connector": "1.0.24",
55
+ "@tiledesk/tiledesk-whatsapp-connector": "1.0.25",
56
56
  "@tiledesk/tiledesk-whatsapp-jobworker": "^0.0.13",
57
57
  "amqplib": "^0.5.5",
58
58
  "app-root-path": "^3.0.0",
@@ -85,6 +85,7 @@
85
85
  "jobs-worker-queued": "^0.0.5",
86
86
  "jsdom": "^26.1.0",
87
87
  "jsonwebtoken": "^8.5.1",
88
+ "libphonenumber-js": "^1.12.10",
88
89
  "lodash": "^4.17.21",
89
90
  "marked": "^3.0.4",
90
91
  "maskdata": "^1.1.10",
package/routes/message.js CHANGED
@@ -149,15 +149,19 @@ async (req, res) => {
149
149
  // prende fullname e email da quello loggato
150
150
 
151
151
  // createIfNotExistsWithLeadId(lead_id, fullname, email, id_project, createdBy, attributes) {
152
- return leadService.createIfNotExistsWithLeadId(sender || req.user._id, fullname, email, req.projectid, null, req.body.attributes || req.user.attributes)
152
+ return leadService.createIfNotExistsWithLeadId(sender || req.user._id, fullname, email, req.projectid, null, req.body.attributes || req.user.attributes, undefined, req.user.phone)
153
153
  .then(function(createdLead) {
154
154
 
155
-
155
+ const contact = {
156
+ ...(createdLead.phone && { phone: createdLead.phone }),
157
+ ...(createdLead.email && { email: createdLead.email })
158
+ };
156
159
 
157
160
  var new_request = {
158
161
  request_id: req.params.request_id,
159
- project_user_id: project_user._id,
162
+ project_user_id: project_user._id,
160
163
  lead_id: createdLead._id,
164
+ ...(Object.keys(contact).length && { contact }),
161
165
  id_project:req.projectid,
162
166
  first_text: req.body.text,
163
167
  departmentid: req.body.departmentid,
package/routes/request.js CHANGED
@@ -753,6 +753,10 @@ router.post('/:requestid/notes', async function (req, res) {
753
753
  note.text = req.body.text;
754
754
  note.createdBy = req.user.id;
755
755
 
756
+ if (!note.text || note.text.trim() === '') {
757
+ return res.status(400).send({ success: false, error: "Field 'text' is required. Received value: " + note.text });
758
+ }
759
+
756
760
  let project_user = req.projectuser;
757
761
 
758
762
  if (project_user.role === RoleConstants.AGENT) {
@@ -1221,6 +1225,14 @@ router.get('/', function (req, res, next) {
1221
1225
  query.$text = { "$search": req.query.full_text };
1222
1226
  }
1223
1227
 
1228
+ if (req.query.phone) {
1229
+ // Match by digit sequence so e.g. "3456677888" finds "+393456677888"
1230
+ var phoneDigits = req.query.phone.replace(/\D/g, '');
1231
+ if (phoneDigits.length > 0) {
1232
+ query["contact.phone"] = new RegExp(phoneDigits);
1233
+ }
1234
+ }
1235
+
1224
1236
  var history_search = false;
1225
1237
 
1226
1238
  // Multiple status management
@@ -6,6 +6,7 @@ const leadEvent = require('../event/leadEvent');
6
6
  var winston = require('../config/winston');
7
7
  var cacheUtil = require('../utils/cacheUtil');
8
8
  var cacheEnabler = require("../services/cacheEnabler");
9
+ var phoneUtil = require('../utils/phoneUtil');
9
10
 
10
11
 
11
12
  class LeadService {
@@ -55,7 +56,7 @@ class LeadService {
55
56
 
56
57
 
57
58
 
58
- createIfNotExistsWithLeadId(lead_id, fullname, email, id_project, createdBy, attributes, status) {
59
+ createIfNotExistsWithLeadId(lead_id, fullname, email, id_project, createdBy, attributes, status, phone) {
59
60
  var that = this;
60
61
  return new Promise(function (resolve, reject) {
61
62
  return Lead.findOne({lead_id: lead_id, id_project: id_project})
@@ -63,10 +64,10 @@ class LeadService {
63
64
  .exec(function(err, lead) {
64
65
  if (err) {
65
66
  winston.error("Error createIfNotExistsWithLeadId", err);
66
- return resolve(that.createWitId(lead_id, fullname, email, id_project, createdBy, attributes, status));
67
+ return resolve(that.createWitId(lead_id, fullname, email, id_project, createdBy, attributes, status, phone));
67
68
  }
68
69
  if (!lead) {
69
- return resolve(that.createWitId(lead_id, fullname, email, id_project, createdBy, attributes, status));
70
+ return resolve(that.createWitId(lead_id, fullname, email, id_project, createdBy, attributes, status, phone));
70
71
  }
71
72
 
72
73
  winston.debug("lead.email: " + lead.email);
@@ -77,7 +78,7 @@ class LeadService {
77
78
  return resolve(lead);
78
79
  } else {
79
80
  winston.debug("lead already exists createIfNotExistsWithLeadId but with different email");
80
- return resolve(that.updateWitId(lead_id, fullname, email, id_project, status));
81
+ return resolve(that.updateWitId(lead_id, fullname, email, id_project, status, phone));
81
82
  }
82
83
 
83
84
 
@@ -111,7 +112,7 @@ class LeadService {
111
112
  });
112
113
  }
113
114
 
114
- updateWitId(lead_id, fullname, email, id_project, status) {
115
+ updateWitId(lead_id, fullname, email, id_project, status, phone) {
115
116
  winston.debug("updateWitId lead_id: "+ lead_id);
116
117
  winston.debug("fullname: "+ fullname);
117
118
  winston.debug("email: "+ email);
@@ -124,7 +125,9 @@ class LeadService {
124
125
 
125
126
  update.fullname = fullname;
126
127
  update.email = email;
127
-
128
+ if (phone !== undefined) {
129
+ update.phone = phoneUtil.normalizePhone(phone);
130
+ }
128
131
  if (status) {
129
132
  update.status = status;
130
133
  }
@@ -147,7 +150,7 @@ class LeadService {
147
150
  });
148
151
  }
149
152
 
150
- createWitId(lead_id, fullname, email, id_project, createdBy, attributes, status) {
153
+ createWitId(lead_id, fullname, email, id_project, createdBy, attributes, status, phone) {
151
154
 
152
155
  if (!createdBy) {
153
156
  createdBy = "system";
@@ -163,6 +166,7 @@ class LeadService {
163
166
  lead_id: lead_id,
164
167
  fullname: fullname,
165
168
  email: email,
169
+ phone: phoneUtil.normalizePhone(phone),
166
170
  attributes: attributes,
167
171
  status: status,
168
172
  id_project: id_project,
@@ -485,7 +485,8 @@ class RequestService {
485
485
  notes,
486
486
  priority,
487
487
  auto_close,
488
- followers
488
+ followers,
489
+ contact
489
490
  } = request;
490
491
 
491
492
  let departmentid = request.departmentid || 'default';
@@ -502,7 +503,7 @@ class RequestService {
502
503
  request_id, project_user_id, lead_id, id_project,
503
504
  first_text, departmentid, sourcePage, language, userAgent, status,
504
505
  createdBy, attributes, subject, preflight, channel, location,
505
- participants,tags,notes,priority,auto_close,followers
506
+ participants,tags,notes,priority,auto_close,followers,contact
506
507
  }
507
508
  };
508
509
 
@@ -648,7 +649,8 @@ class RequestService {
648
649
  auto_close,
649
650
  followers,
650
651
  createdAt,
651
- snapshot
652
+ snapshot,
653
+ contact,
652
654
  })
653
655
 
654
656
  if (isTestConversation) {
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+
4
+ const { parsePhoneNumberFromString } = require('libphonenumber-js');
5
+
6
+ function normalizePhone(phone) {
7
+ if (phone == null) return null;
8
+
9
+ if (typeof phone !== 'string') {
10
+ phone = String(phone);
11
+ }
12
+
13
+ let trimmed = phone.trim();
14
+ if (!trimmed) return null;
15
+
16
+ // Convert 00 to +
17
+ if (trimmed.startsWith('00')) {
18
+ trimmed = '+' + trimmed.slice(2);
19
+ }
20
+
21
+ // Remove spaces and strange characters (keep + initial)
22
+ trimmed = trimmed.replace(/[^\d+]/g, '');
23
+
24
+ // If it starts with +
25
+ if (trimmed.startsWith('+')) {
26
+ const parsed = parsePhoneNumberFromString(trimmed);
27
+
28
+ if (parsed && parsed.isValid()) {
29
+ return parsed.number; // Correct E.164
30
+ }
31
+
32
+ // + present but not valid → remove the +
33
+ return trimmed.replace(/^\+/, '');
34
+ }
35
+
36
+ // Try as international without +
37
+ const parsedIntl = parsePhoneNumberFromString('+' + trimmed);
38
+
39
+ if (parsedIntl && parsedIntl.isValid()) {
40
+ return parsedIntl.number;
41
+ }
42
+
43
+ // Locale → return only digits
44
+ const digitsOnly = trimmed.replace(/\D/g, '');
45
+ return digitsOnly || null;
46
+ }
47
+
48
+
49
+ module.exports = {
50
+ normalizePhone: normalizePhone
51
+ };