@plusscommunities/pluss-maintenance-aws-forms 2.1.20 → 2.1.21-auth.0

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.
@@ -13,613 +13,712 @@ const getString = require("@plusscommunities/pluss-core-aws/db/strings/getString
13
13
  const { values } = require("../../values.config");
14
14
 
15
15
  class ArchibusStrategy extends IntegrationStrategy {
16
- constructor(config) {
17
- super();
18
- this.baseUrl = config.BaseUrl; // base URL for the API
19
- this.apiKeyHeader = config.APIKeyHeader; // header to use for API key
20
- this.apiKey = config.APIKey; // API key
21
-
22
- const url = new URL(this.baseUrl);
23
- this.host = url.host;
24
-
25
- this.statusMap = config.StatusMap;
26
- this.siteMap = config.SiteMap ?? {};
27
- this.buildingCodes = [];
28
- }
29
-
30
- /**
31
- * Gets the entity type for the integration
32
- *
33
- * @returns {String} The entity type for the integration
34
- */
35
- getEntityType = () => {
36
- return `${values.serviceKey}_Archibus`;
37
- };
38
-
39
- /**
40
- * Validates the integration
41
- *
42
- * @returns {Boolean} Whether the integration is valid
43
- */
44
- isValidIntegration = () => {
45
- return true;
46
- };
47
-
48
- /**
49
- * Gets the refresh interval for the Archibus system
50
- *
51
- * @returns {Number} The refresh interval in milliseconds
52
- */
53
- getRefreshInterval = () => {
54
- return 15 * 60 * 1000; // 15 minutes
55
- };
56
-
57
- /**
58
- * Creates a request from the Archibus system
59
- *
60
- * @param {Object} request - Request definition on Pluss
61
- * @param {Object} mockResponse - Mock response from Archibus to simulate response
62
- * @returns {Boolean} Whether the request was created on Archibus
63
- */
64
- createRequest = async (request, mockResponse = null) => {
65
- const logId = log("Archibus:CreateRequest", "Start", request);
66
-
67
- let siteId = this.siteMap[request.site];
68
- let buildingCode = null;
69
- if (request.room) {
70
- if (this.buildingCodes.length === 0) {
71
- this.buildingCodes = await getString(
72
- getRowId(request.site, `${values.serviceKey}buildingcodes`),
73
- "plussSpace"
74
- );
75
- }
76
- const building = this.buildingCodes.find((b) => {
77
- const address = request.room.toLowerCase();
78
- const search = b.SearchString.toLowerCase();
79
- const name = b.BuildingName.toLowerCase();
80
- return address.includes(search) || address.includes(name);
81
- });
82
-
83
- if (building?.SiteCode) siteId = building.SiteCode.toString();
84
- buildingCode = building?.BuildingCode;
85
- }
86
- if (!siteId) return false;
87
-
88
- try {
89
- const data = {
90
- prob_type: "1.ON-SITE|1. MAINTENANCE",
91
- requestor: "PLUSS",
92
- description: `${request.title}${
93
- _.isEmpty(request.description) ? "" : `\n\n${request.description}`
94
- }${_.isEmpty(request.room) ? "" : `\n\nLocation: ${request.room}`}${
95
- _.isEmpty(request.userName) ? "" : `\n\nName: ${request.userName}`
96
- }${_.isEmpty(request.phone) ? "" : `\n\nPhone: ${request.phone}`}${
97
- _.isEmpty(request.type) ? "" : `\n\nJob Type: ${request.type}`
98
- }${
99
- _.isEmpty(request.images)
100
- ? ""
101
- : `\n\nImages: ${request.images.join("\n")}`
102
- }`,
103
- site_id: siteId,
104
- };
105
- if (buildingCode) data.bl_id = buildingCode;
106
- log("Archibus:CreateRequest", "Request", data, logId);
107
-
108
- const response =
109
- mockResponse ??
110
- (await axios({
111
- method: "PUT",
112
- url: `${this.baseUrl}/createWorkRequest`,
113
- timeout: 30000,
114
- headers: {
115
- [this.apiKeyHeader]: this.apiKey,
116
- "Content-Type": "application/json",
117
- Host: this.host,
118
- },
119
- data,
120
- }));
121
- log("Archibus:CreateRequest", "Response", response.data, logId);
122
-
123
- // Save the ID to external entities for future reference
124
- await updateRef("externalentities", {
125
- RowId: `${this.getEntityType()}_${response.data.wrId}`,
126
- LastUpdated: moment().valueOf(),
127
- EntityType: this.getEntityType(),
128
- InternalId: request.id,
129
- ExternalId: response.data.wrId,
130
- TrackedData: {},
131
- });
132
-
133
- // Save the Archibus ID as the job number and add history entry
134
- const updatedJob = await getRef(values.tableNameMaintenance, "id", request.id);
135
- if (!updatedJob.history) updatedJob.history = [];
136
- updatedJob.history.push({
137
- timestamp: moment.utc().valueOf(),
138
- action: "ExternalIDSet",
139
- externalId: response.data.wrId,
140
- user: {
141
- displayName: "Archibus Integration",
142
- id: "system",
143
- },
144
- });
145
-
146
- await editRef(values.tableNameMaintenance, "id", request.id, {
147
- jobNo: response.data.wrId,
148
- jobId: response.data.wrId + "",
149
- history: updatedJob.history,
150
- });
151
-
152
- // add images
153
- if (!mockResponse && !_.isEmpty(request.images)) {
154
- const imagePromises = [];
155
- request.images.forEach((url) => {
156
- imagePromises.push(this.addFileToRequest(response.data.wrId, url));
157
- });
158
- await Promise.all(imagePromises);
159
- }
160
-
161
- return true;
162
- } catch (e) {
163
- log("Archibus:CreateRequest", "Error", e, logId);
164
-
165
- // Add history entry for failed integration
166
- try {
167
- const failedJob = await getRef(values.tableNameMaintenance, "id", request.id);
168
- if (!failedJob.history) failedJob.history = [];
169
- failedJob.history.push({
170
- timestamp: moment.utc().valueOf(),
171
- action: "ExternalIDSetFailed",
172
- error: e.message || e.toString(),
173
- user: {
174
- displayName: "Archibus Integration",
175
- id: "system",
176
- },
177
- });
178
-
179
- await editRef(values.tableNameMaintenance, "id", request.id, {
180
- history: failedJob.history,
181
- });
182
- } catch (historyError) {
183
- log("Archibus:CreateRequest", "HistoryError", historyError, logId);
184
- }
185
- }
186
- return false;
187
- };
188
-
189
- /**
190
- * Fetches a request from the Archibus system
191
- *
192
- * @param {String} externalId - Id of the request on the Archibus
193
- * @returns {Object} The request as it exists on Archibus
194
- */
195
- getRequest = async (externalId) => {
196
- const logId = log("Archibus:GetRequest", "Start", externalId);
197
- try {
198
- const response = await axios({
199
- method: "GET",
200
- url: `${this.baseUrl}/getWorkRequest/${externalId}`,
201
- timeout: 30000,
202
- headers: {
203
- [this.apiKeyHeader]: this.apiKey,
204
- "Content-Type": "application/json",
205
- Host: this.host,
206
- },
207
- });
208
- log("Archibus:GetRequest", "Response", response.data, logId);
209
-
210
- return response.data;
211
- } catch (e) {
212
- log("Archibus:GetRequest", "Error", e, logId);
213
- }
214
- return null;
215
- };
216
-
217
- /**
218
- * Refreshes a request from the Archibus system
219
- *
220
- * @param {String} requestId - Id of the request on Pluss.
221
- * @param {String} externalId - Id of the request on Archibus.
222
- * @param {Object} trackedData - The set of fields tracked.
223
- * @returns {Boolean} Whether the request had any changes on Archibus.
224
- */
225
- refreshFromSource = async (requestId, externalId, trackedData) => {
226
- const logId = log("Archibus:RefreshFromSource", "Start", {
227
- requestId,
228
- externalId,
229
- trackedData,
230
- });
231
- const request = await this.getRequest(externalId);
232
- if (!request) {
233
- log("Archibus:RefreshFromSource", "Result:NoResponse", false, logId);
234
- return false;
235
- }
236
-
237
- if (trackedData && trackedData.status === request.status) {
238
- log("Archibus:RefreshFromSource", "Result:UnchangedStatus", false, logId);
239
- // status has not changed
240
- return false;
241
- }
242
-
243
- const statusToUse = this.statusMap[request.status];
244
- log("Archibus:RefreshFromSource", "UpdatedStatus", statusToUse, logId);
245
-
246
- if (statusToUse) {
247
- let plussRequest = await getRef(
248
- values.tableNameMaintenance,
249
- "id",
250
- requestId
251
- );
252
- // check how the new status map to what is saved in Pluss
253
-
254
- if (statusToUse.Status !== plussRequest.status) {
255
- // save updated status
256
-
257
- plussRequest = await editRef(
258
- values.tableNameMaintenance,
259
- "id",
260
- requestId,
261
- {
262
- status: statusToUse.Status,
263
- }
264
- );
265
- log(
266
- "Archibus:RefreshFromSource",
267
- "SavedStatus",
268
- statusToUse.Status,
269
- logId
270
- );
271
-
272
- if (statusToUse.Notification) {
273
- publishNotifications(
274
- [plussRequest.userID],
275
- values.activityMaintenanceJobStatusChanged,
276
- plussRequest.site,
277
- plussRequest.id,
278
- plussRequest,
279
- true
280
- );
281
- }
282
- }
283
- }
284
-
285
- // save tracked data
286
- await editRef(
287
- "externalentities",
288
- "RowId",
289
- `${this.getEntityType()}_${externalId}`,
290
- {
291
- TrackedData: {
292
- status: request.status,
293
- },
294
- }
295
- );
296
-
297
- log("Archibus:RefreshFromSource", "Result", true, logId);
298
- return true;
299
- };
300
-
301
- /**
302
- * Adds a comment to a request in Archibus
303
- *
304
- * @param {Number} externalId - Id of the request on Archibus.
305
- * @param {String} comment - Text to add to Archibus request.
306
- * @param {moment.Moment} time - The time to save against the comment
307
- */
308
- addCommentToRequest = async (externalId, comment, time) => {
309
- const logId = log("Archibus:AddComment", "Start", {
310
- externalId,
311
- comment,
312
- time,
313
- });
314
- try {
315
- // Format the time
316
- const timeToUse = (time || moment()).format("YYYY-MM-DD hh:mm:ss");
317
-
318
- // Generate the POST data
319
- const postData = {
320
- actionTime: timeToUse,
321
- comments: comment,
322
- wr_id: externalId,
323
- };
324
-
325
- log("Archibus:AddComment", "PostData", postData, logId);
326
-
327
- // Save to Archibus
328
- const response = await axios({
329
- method: "POST",
330
- url: `${this.baseUrl}/addCommentsToWRSteps`,
331
- data: postData,
332
- timeout: 30000,
333
- headers: {
334
- [this.apiKeyHeader]: this.apiKey,
335
- "Content-Type": "application/json",
336
- Host: this.host,
337
- },
338
- });
339
-
340
- log("Archibus:AddComment", "ResponseStatus", response.status, logId);
341
-
342
- return response;
343
- } catch (error) {
344
- log("Archibus:AddComment", "Error", error, logId);
345
- }
346
- return false;
347
- };
348
-
349
- /**
350
- * Adds a file to a request in Archibus
351
- *
352
- * @param {Number} externalId - Id of the request on Archibus.
353
- * @param {String} fileUrl - URL of the file to attach
354
- * @param {moment.Moment} time - The time to save against the file
355
- * @returns
356
- */
357
- addFileToRequest = async (externalId, fileUrl, time) => {
358
- const logId = log("Archibus:AddFile", "Start", {
359
- externalId,
360
- fileUrl,
361
- time,
362
- });
363
-
364
- // This endpoint is not currently functional. Instead, use the addCommentToRequest method to add a comment with the file URL.
365
- // return this.addCommentToRequest(externalId, fileUrl, time);
366
-
367
- try {
368
- // Download the file content from the URL
369
- const fileResponse = await axios.get(fileUrl, {
370
- responseType: "arraybuffer",
371
- });
372
- log("Archibus:AddFile", "GotFileResponse", fileResponse.status, logId);
373
-
374
- // Encode the file content in base64
375
- const fileContentBase64 = encode(fileResponse.data);
376
- log("Archibus:AddFile", "EncodedFileResponse", true, logId);
377
-
378
- // Extract file name from the URL
379
- const fileName = fileUrl.split("/").pop();
380
-
381
- // Format the time
382
- const timeToUse = (time || moment()).format("YYYY-MM-DD hh:mm:ss");
383
-
384
- // Generate the POST data
385
- const postData = {
386
- actionTime: timeToUse,
387
- docName: fileName,
388
- docUrl: "https://anglicare.plusscommunities.com/",
389
- fileContent: fileContentBase64,
390
- fileName: fileName,
391
- wrId: externalId,
392
- };
393
-
394
- log("Archibus:AddFile", "PostData", postData, logId);
395
-
396
- // Save to Archibus
397
- const response = await axios({
398
- method: "POST",
399
- url: `${this.baseUrl}/addDocumentToWorkRequest`,
400
- data: postData,
401
- timeout: 60000,
402
- headers: {
403
- [this.apiKeyHeader]: this.apiKey,
404
- "Content-Type": "application/json",
405
- Host: this.host,
406
- },
407
- });
408
-
409
- log("Archibus:AddFile", "ResponseStatus", response.status, logId);
410
-
411
- return response;
412
- } catch (error) {
413
- log("Archibus:AddFile", "Error", error, logId);
414
- }
415
- return false;
416
- };
417
-
418
- /**
419
- * Get Archibus Id of the request
420
- *
421
- * @param {Object} request - Request definition on Pluss
422
- * @returns {Number} Id of the request on Archibus (null if not exists)
423
- */
424
- getExternalId = async (request) => {
425
- const logId = log("Archibus:GetExternalId", "Start", { Id: request.id });
426
-
427
- // get external id
428
- const externalEntityQuery = await indexQuery("externalentities", {
429
- IndexName: "InternalIdIndex",
430
- KeyConditionExpression:
431
- "EntityType = :entityType AND InternalId = :internalId",
432
- ExpressionAttributeValues: {
433
- ":entityType": this.getEntityType(),
434
- ":internalId": request.id,
435
- },
436
- });
437
-
438
- log(
439
- "Archibus:GetExternalId",
440
- "ExternalLength",
441
- externalEntityQuery.Items.length,
442
- logId
443
- );
444
- if (_.isEmpty(externalEntityQuery.Items)) {
445
- return null;
446
- }
447
-
448
- const externalId = externalEntityQuery.Items[0].ExternalId;
449
- log("Archibus:GetExternalId", "ExternalId", externalId, logId);
450
-
451
- return externalId;
452
- };
453
-
454
- /**
455
- * Get latest comment for a request
456
- *
457
- * @param {Object} request - Request definition on Pluss
458
- * @param {String} entityKey - Request entity key (e.g. maintenancerequest)
459
- * @returns {Object} Latest comment (null if not exists)
460
- */
461
- getLatestComment = async (request, entityKey) => {
462
- const logId = log("Archibus:GetLatestComment", "Start", { Id: request.id });
463
-
464
- // get comments
465
- const commentsQuery = {
466
- IndexName: "CommentsEntityIdIndex",
467
- KeyConditionExpression: "EntityId = :groupId",
468
- ExpressionAttributeValues: {
469
- ":groupId": getRowId(request.id, entityKey),
470
- },
471
- };
472
-
473
- const commentsQueryRes = await indexQuery("comments", commentsQuery);
474
- const comments = _.orderBy(commentsQueryRes.Items, "Timestamp", "desc");
475
- log(
476
- "Archibus:GetLatestComment",
477
- `CommentsLength - ${entityKey}`,
478
- comments.length,
479
- logId
480
- );
481
-
482
- return comments.length > 0 ? comments[0] : null;
483
- };
484
-
485
- /**
486
- * Perform actions when a task's status has changed
487
- *
488
- * @param {Object} request - Request definition on Pluss
489
- * @returns {Boolean} Represents whether the actions were successful
490
- */
491
- onStatusChanged = async (request) => {
492
- const logId = log("Archibus:OnStatusChanged", "Start", { Id: request.id });
493
-
494
- try {
495
- const externalId = await this.getExternalId(request);
496
- if (_.isNil(externalId)) return true;
497
-
498
- const statues = _.orderBy(
499
- (request.history ?? []).filter(
500
- (entry) => entry.EntryType !== "assignment"
501
- ),
502
- "timestamp",
503
- "desc"
504
- );
505
- const latest = statues.length > 0 ? statues[0] : null;
506
-
507
- if (latest) {
508
- const time = moment(Number.parseFloat(latest.timestamp + "")).format(
509
- "D MMM YYYY HH:mm:ss"
510
- );
511
- const user = latest.user ? ` by ${latest.user.displayName}` : "";
512
- const comment = `${time}: Marked ${latest.status}${user}`;
513
- await this.addCommentToRequest(externalId, comment);
514
- }
515
-
516
- return true;
517
- } catch (error) {
518
- log("Archibus:OnStatusChanged", "Error", error.toString(), logId);
519
- }
520
- return false;
521
- };
522
-
523
- /**
524
- * Perform actions when a a comment has been added to a task
525
- *
526
- * @param {Object} request - Request definition on Pluss
527
- * @returns {Boolean} Represents whether the actions were successful
528
- */
529
- onCommentAdded = async (request) => {
530
- const logId = log("Archibus:OnCommentAdded", "Start", {
531
- Id: request.id,
532
- });
533
-
534
- try {
535
- const externalId = await this.getExternalId(request);
536
- if (_.isNil(externalId)) return true;
537
-
538
- let comment = await this.getLatestComment(request, values.serviceKey);
539
- if (!comment)
540
- comment = await this.getLatestComment(request, values.entityKey);
541
-
542
- if (comment) {
543
- const commentText = `${comment.User.displayName}:\n\n${
544
- comment.Comment
545
- }${!_.isEmpty(comment.Image) ? `\n\nImage: ${comment.Image}` : ""}}`;
546
- const commentTime = moment(comment.Timestamp);
547
- await this.addCommentToRequest(externalId, commentText, commentTime);
548
- }
549
-
550
- return true;
551
- } catch (error) {
552
- log("Archibus:OnCommentAdded", "Error", error.toString(), logId);
553
- }
554
- return false;
555
- };
556
-
557
- /**
558
- * Perform actions when a note has been added to a task
559
- *
560
- * @param {Object} request - Request definition on Pluss
561
- * @returns {Boolean} Represents whether the actions were successful
562
- */
563
- onNotesAdded = async (request) => {
564
- const logId = log("Archibus:OnNotesAdded", "Start", { Id: request.id });
565
-
566
- try {
567
- const externalId = await this.getExternalId(request);
568
- if (_.isNil(externalId)) return true;
569
-
570
- const notes = _.orderBy(request.Notes ?? [], "Timestamp", "desc");
571
- const latest = notes.length > 0 ? notes[0] : null;
572
-
573
- if (latest) {
574
- const promises = [];
575
-
576
- const time = moment(Number.parseFloat(latest.Timestamp + "")).format(
577
- "YYYY-MM-DD HH:mm:ss"
578
- );
579
- const user = latest.User ? latest.User.displayName : "Unknown User";
580
- const note = latest.Note ? `Note: ${latest.Note}` : "No Note";
581
- if (!_.isEmpty(latest.Attachments)) {
582
- latest.Attachments.forEach((att) => {
583
- promises.push(this.addFileToRequest(externalId, att.Source));
584
- });
585
- }
586
- const attachments =
587
- latest.Attachments && latest.Attachments.length > 0
588
- ? `Attachments: ${latest.Attachments.map(
589
- (att) => `${att.Title} (${att.Source})`
590
- ).join(", ")}`
591
- : "No Attachments";
592
- if (!_.isEmpty(latest.Images)) {
593
- latest.Images.forEach((image) => {
594
- promises.push(this.addFileToRequest(externalId, image));
595
- });
596
- }
597
- const images =
598
- latest.Images && latest.Images.length > 0
599
- ? `Images: ${latest.Images.map((image) => image).join(", ")}`
600
- : "No Images";
601
- const comment = `${time} by ${user}\n${note}\n${attachments}\n${images}`;
602
- promises.push(this.addCommentToRequest(externalId, comment));
603
-
604
- await Promise.all(promises);
605
- }
606
-
607
- return true;
608
- } catch (error) {
609
- log("Archibus:OnNotesAdded", "Error", error.toString(), logId);
610
- }
611
- return false;
612
- };
613
-
614
- /**
615
- * Perform completion actions when a task is completed
616
- *
617
- * @param {Object} request - Request definition on Pluss
618
- * @returns {Boolean} Represents whether the actions were successful
619
- */
620
- onCompleteRequest = async (request) => {
621
- return true;
622
- };
16
+ constructor(config) {
17
+ super();
18
+ this.baseUrl = config.BaseUrl; // base URL for the API
19
+ this.apiKeyHeader = config.APIKeyHeader; // header to use for API key
20
+ this.apiKey = config.APIKey; // API key
21
+
22
+ const url = new URL(this.baseUrl);
23
+ this.host = url.host;
24
+
25
+ this.statusMap = config.StatusMap;
26
+ this.completedStatuses = config.CompletedStatuses ?? [];
27
+ this.siteMap = config.SiteMap ?? {};
28
+ this.buildingCodes = [];
29
+ }
30
+
31
+ /**
32
+ * Gets the entity type for the integration
33
+ *
34
+ * @returns {String} The entity type for the integration
35
+ */
36
+ getEntityType = () => {
37
+ return `${values.serviceKey}_Archibus`;
38
+ };
39
+
40
+ /**
41
+ * Validates the integration
42
+ *
43
+ * @returns {Boolean} Whether the integration is valid
44
+ */
45
+ isValidIntegration = () => {
46
+ return true;
47
+ };
48
+
49
+ /**
50
+ * Gets the refresh interval for the Archibus system
51
+ *
52
+ * @returns {Number} The refresh interval in milliseconds
53
+ */
54
+ getRefreshInterval = () => {
55
+ return 60 * 60 * 1000; // 60 minutes
56
+ };
57
+
58
+ /**
59
+ * Creates a request from the Archibus system
60
+ *
61
+ * @param {Object} request - Request definition on Pluss
62
+ * @param {Object} mockResponse - Mock response from Archibus to simulate response
63
+ * @returns {Boolean} Whether the request was created on Archibus
64
+ */
65
+ createRequest = async (request, mockResponse = null) => {
66
+ const logId = log("Archibus:CreateRequest", "Start", request);
67
+
68
+ let siteId = this.siteMap[request.site];
69
+ let buildingCode = null;
70
+ if (request.room) {
71
+ if (this.buildingCodes.length === 0) {
72
+ this.buildingCodes = await getString(
73
+ getRowId(request.site, `${values.serviceKey}buildingcodes`),
74
+ "plussSpace",
75
+ );
76
+ }
77
+ const building = this.buildingCodes.find((b) => {
78
+ const address = request.room.toLowerCase();
79
+ const search = b.SearchString.toLowerCase();
80
+ const name = b.BuildingName.toLowerCase();
81
+ return address.includes(search) || address.includes(name);
82
+ });
83
+
84
+ if (building?.SiteCode) siteId = building.SiteCode.toString();
85
+ buildingCode = building?.BuildingCode;
86
+ }
87
+ if (!siteId) return false;
88
+
89
+ try {
90
+ const data = {
91
+ prob_type: "1.ON-SITE|1. MAINTENANCE",
92
+ requestor: "PLUSS",
93
+ description: `${request.title}${
94
+ _.isEmpty(request.description) ? "" : `\n\n${request.description}`
95
+ }${_.isEmpty(request.room) ? "" : `\n\nLocation: ${request.room}`}${
96
+ _.isEmpty(request.userName) ? "" : `\n\nName: ${request.userName}`
97
+ }${_.isEmpty(request.phone) ? "" : `\n\nPhone: ${request.phone}`}${
98
+ _.isEmpty(request.type) ? "" : `\n\nJob Type: ${request.type}`
99
+ }${
100
+ _.isEmpty(request.images)
101
+ ? ""
102
+ : `\n\nImages: ${request.images.join("\n")}`
103
+ }`,
104
+ site_id: siteId,
105
+ };
106
+ if (buildingCode) data.bl_id = buildingCode;
107
+ log("Archibus:CreateRequest", "Request", data, logId);
108
+
109
+ const response =
110
+ mockResponse ??
111
+ (await axios({
112
+ method: "PUT",
113
+ url: `${this.baseUrl}/createWorkRequest`,
114
+ timeout: 30000,
115
+ headers: {
116
+ [this.apiKeyHeader]: this.apiKey,
117
+ "Content-Type": "application/json",
118
+ Host: this.host,
119
+ },
120
+ data,
121
+ }));
122
+ log("Archibus:CreateRequest", "Response", response.data, logId);
123
+
124
+ // Save the ID to external entities for future reference
125
+ await updateRef("externalentities", {
126
+ RowId: `${this.getEntityType()}_${response.data.wrId}`,
127
+ LastUpdated: moment().valueOf(),
128
+ EntityType: this.getEntityType(),
129
+ ActiveEntityType: this.getEntityType(),
130
+ InternalId: request.id,
131
+ ExternalId: response.data.wrId,
132
+ TrackedData: {},
133
+ SystemType: "Archibus",
134
+ });
135
+
136
+ // Save the Archibus ID as the job number and add history entry
137
+ const updatedJob = await getRef(
138
+ values.tableNameMaintenance,
139
+ "id",
140
+ request.id,
141
+ );
142
+ if (!updatedJob.history) updatedJob.history = [];
143
+ updatedJob.history.push({
144
+ timestamp: moment.utc().valueOf(),
145
+ EntryType: "ExternalIDSet",
146
+ externalId: response.data.wrId,
147
+ user: {
148
+ displayName: "Archibus Integration",
149
+ id: "system",
150
+ },
151
+ systemType: "Archibus",
152
+ });
153
+
154
+ await editRef(values.tableNameMaintenance, "id", request.id, {
155
+ jobNo: response.data.wrId,
156
+ jobId: response.data.wrId + "",
157
+ history: updatedJob.history,
158
+ });
159
+
160
+ // add images
161
+ if (!mockResponse && !_.isEmpty(request.images)) {
162
+ const imagePromises = [];
163
+ request.images.forEach((url) => {
164
+ imagePromises.push(this.addFileToRequest(response.data.wrId, url));
165
+ });
166
+ await Promise.all(imagePromises);
167
+ }
168
+
169
+ return true;
170
+ } catch (e) {
171
+ log("Archibus:CreateRequest", "Error", e, logId);
172
+
173
+ // Add history entry for failed integration
174
+ try {
175
+ const failedJob = await getRef(
176
+ values.tableNameMaintenance,
177
+ "id",
178
+ request.id,
179
+ );
180
+ if (!failedJob.history) failedJob.history = [];
181
+ failedJob.history.push({
182
+ timestamp: moment.utc().valueOf(),
183
+ EntryType: "ExternalIDSetFailed",
184
+ user: {
185
+ displayName: "Archibus Integration",
186
+ id: "system",
187
+ },
188
+ systemType: "Archibus",
189
+ });
190
+
191
+ await editRef(values.tableNameMaintenance, "id", request.id, {
192
+ history: failedJob.history,
193
+ });
194
+ } catch (historyError) {
195
+ log("Archibus:CreateRequest", "HistoryError", historyError, logId);
196
+ }
197
+ }
198
+ return false;
199
+ };
200
+
201
+ /**
202
+ * Fetches a request from the Archibus system
203
+ *
204
+ * @param {String} externalId - Id of the request on the Archibus
205
+ * @returns {Object} The request as it exists on Archibus
206
+ */
207
+ getRequest = async (externalId) => {
208
+ const logId = log("Archibus:GetRequest", "Start", externalId);
209
+ try {
210
+ const response = await axios({
211
+ method: "GET",
212
+ url: `${this.baseUrl}/getWorkRequest/${externalId}`,
213
+ timeout: 30000,
214
+ headers: {
215
+ [this.apiKeyHeader]: this.apiKey,
216
+ "Content-Type": "application/json",
217
+ Host: this.host,
218
+ },
219
+ });
220
+ log("Archibus:GetRequest", "Response", response.data, logId);
221
+
222
+ return response.data;
223
+ } catch (e) {
224
+ log("Archibus:GetRequest", "Error", e, logId);
225
+ }
226
+ return null;
227
+ };
228
+
229
+ /**
230
+ * Refreshes a request from the Archibus system
231
+ *
232
+ * @param {String} requestId - Id of the request on Pluss.
233
+ * @param {String} externalId - Id of the request on Archibus.
234
+ * @param {Object} trackedData - The set of fields tracked.
235
+ * @returns {Boolean} Whether the request had any changes on Archibus.
236
+ */
237
+ refreshFromSource = async (requestId, externalId, trackedData) => {
238
+ const logId = log("Archibus:RefreshFromSource", "Start", {
239
+ requestId,
240
+ externalId,
241
+ trackedData,
242
+ });
243
+
244
+ // Skip API calls overnight (8pm–6am Sydney time)
245
+ const sydneyNow = new Date(
246
+ new Date().toLocaleString("en-US", { timeZone: "Australia/Sydney" }),
247
+ );
248
+ const sydneyHour = sydneyNow.getHours();
249
+ if (sydneyHour >= 20 || sydneyHour < 6) {
250
+ log(
251
+ "Archibus:RefreshFromSource",
252
+ "SkippedOvernight",
253
+ { sydneyHour },
254
+ logId,
255
+ );
256
+ return false;
257
+ }
258
+
259
+ // If the external status is terminal, remove ActiveEntityType to exclude from sparse GSI
260
+ if (this.completedStatuses.includes(trackedData.status)) {
261
+ log(
262
+ "Archibus:RefreshFromSource",
263
+ "TerminalStatus",
264
+ trackedData.status,
265
+ logId,
266
+ );
267
+ const entityRowId = `${this.getEntityType()}_${externalId}`;
268
+ const entity = await getRef("externalentities", "RowId", entityRowId);
269
+ if (entity?.ActiveEntityType) {
270
+ delete entity.ActiveEntityType;
271
+ await updateRef("externalentities", entity);
272
+ }
273
+ return false;
274
+ }
275
+
276
+ const request = await this.getRequest(externalId);
277
+ if (!request) {
278
+ log("Archibus:RefreshFromSource", "Result:NoResponse", false, logId);
279
+ return false;
280
+ }
281
+
282
+ if (trackedData && trackedData.status === request.status) {
283
+ log("Archibus:RefreshFromSource", "Result:UnchangedStatus", false, logId);
284
+ // status has not changed
285
+ return false;
286
+ }
287
+
288
+ const statusToUse = this.statusMap[request.status];
289
+ log("Archibus:RefreshFromSource", "UpdatedStatus", statusToUse, logId);
290
+
291
+ if (statusToUse) {
292
+ let plussRequest = await getRef(
293
+ values.tableNameMaintenance,
294
+ "id",
295
+ requestId,
296
+ );
297
+
298
+ if (!plussRequest) {
299
+ log(
300
+ "Archibus:RefreshFromSource",
301
+ "RequestDeletedLocally",
302
+ requestId,
303
+ logId,
304
+ );
305
+ const entityRowId = `${this.getEntityType()}_${externalId}`;
306
+ const entity = await getRef("externalentities", "RowId", entityRowId);
307
+ if (entity?.ActiveEntityType) {
308
+ delete entity.ActiveEntityType;
309
+ await updateRef("externalentities", entity);
310
+ }
311
+ } else if (statusToUse.Status !== plussRequest?.status) {
312
+ // save updated status
313
+
314
+ plussRequest = await editRef(
315
+ values.tableNameMaintenance,
316
+ "id",
317
+ requestId,
318
+ {
319
+ status: statusToUse.Status,
320
+ },
321
+ );
322
+ log(
323
+ "Archibus:RefreshFromSource",
324
+ "SavedStatus",
325
+ statusToUse.Status,
326
+ logId,
327
+ );
328
+
329
+ if (statusToUse.Notification) {
330
+ publishNotifications(
331
+ [plussRequest.userID],
332
+ values.activityMaintenanceJobStatusChanged,
333
+ plussRequest.site,
334
+ plussRequest.id,
335
+ plussRequest,
336
+ true,
337
+ );
338
+ }
339
+ }
340
+ }
341
+
342
+ // save tracked data
343
+ await editRef(
344
+ "externalentities",
345
+ "RowId",
346
+ `${this.getEntityType()}_${externalId}`,
347
+ {
348
+ TrackedData: {
349
+ status: request.status,
350
+ },
351
+ },
352
+ );
353
+
354
+ log("Archibus:RefreshFromSource", "Result", true, logId);
355
+ return true;
356
+ };
357
+
358
+ /**
359
+ * Adds a comment to a request in Archibus
360
+ *
361
+ * @param {Number} externalId - Id of the request on Archibus.
362
+ * @param {String} comment - Text to add to Archibus request.
363
+ * @param {moment.Moment} time - The time to save against the comment
364
+ */
365
+ addCommentToRequest = async (externalId, comment, time) => {
366
+ const logId = log("Archibus:AddComment", "Start", {
367
+ externalId,
368
+ comment,
369
+ time,
370
+ });
371
+ try {
372
+ // Format the time
373
+ const timeToUse = (time || moment()).format("YYYY-MM-DD hh:mm:ss");
374
+
375
+ // Generate the POST data
376
+ const postData = {
377
+ actionTime: timeToUse,
378
+ comments: comment,
379
+ wr_id: externalId,
380
+ };
381
+
382
+ log("Archibus:AddComment", "PostData", postData, logId);
383
+
384
+ // Save to Archibus
385
+ const response = await axios({
386
+ method: "POST",
387
+ url: `${this.baseUrl}/addCommentsToWRSteps`,
388
+ data: postData,
389
+ timeout: 30000,
390
+ headers: {
391
+ [this.apiKeyHeader]: this.apiKey,
392
+ "Content-Type": "application/json",
393
+ Host: this.host,
394
+ },
395
+ });
396
+
397
+ log("Archibus:AddComment", "ResponseStatus", response.status, logId);
398
+
399
+ return response;
400
+ } catch (error) {
401
+ log("Archibus:AddComment", "Error", error, logId);
402
+ }
403
+ return false;
404
+ };
405
+
406
+ /**
407
+ * Adds a file to a request in Archibus
408
+ *
409
+ * @param {Number} externalId - Id of the request on Archibus.
410
+ * @param {String} fileUrl - URL of the file to attach
411
+ * @param {moment.Moment} time - The time to save against the file
412
+ * @returns
413
+ */
414
+ addFileToRequest = async (externalId, fileUrl, time) => {
415
+ const logId = log("Archibus:AddFile", "Start", {
416
+ externalId,
417
+ fileUrl,
418
+ time,
419
+ });
420
+
421
+ // This endpoint is not currently functional. Instead, use the addCommentToRequest method to add a comment with the file URL.
422
+ // return this.addCommentToRequest(externalId, fileUrl, time);
423
+
424
+ try {
425
+ // Download the file content from the URL
426
+ const fileResponse = await axios.get(fileUrl, {
427
+ responseType: "arraybuffer",
428
+ });
429
+ log("Archibus:AddFile", "GotFileResponse", fileResponse.status, logId);
430
+
431
+ // Encode the file content in base64
432
+ const fileContentBase64 = encode(fileResponse.data);
433
+ log("Archibus:AddFile", "EncodedFileResponse", true, logId);
434
+
435
+ // Extract file name from the URL
436
+ const fileName = fileUrl.split("/").pop();
437
+
438
+ // Format the time
439
+ const timeToUse = (time || moment()).format("YYYY-MM-DD hh:mm:ss");
440
+
441
+ // Generate the POST data
442
+ const postData = {
443
+ actionTime: timeToUse,
444
+ docName: fileName,
445
+ docUrl: "https://anglicare.plusscommunities.com/",
446
+ fileContent: fileContentBase64,
447
+ fileName: fileName,
448
+ wrId: externalId,
449
+ };
450
+
451
+ log("Archibus:AddFile", "PostData", postData, logId);
452
+
453
+ // Save to Archibus
454
+ const response = await axios({
455
+ method: "POST",
456
+ url: `${this.baseUrl}/addDocumentToWorkRequest`,
457
+ data: postData,
458
+ timeout: 60000,
459
+ headers: {
460
+ [this.apiKeyHeader]: this.apiKey,
461
+ "Content-Type": "application/json",
462
+ Host: this.host,
463
+ },
464
+ });
465
+
466
+ log("Archibus:AddFile", "ResponseStatus", response.status, logId);
467
+
468
+ return response;
469
+ } catch (error) {
470
+ log("Archibus:AddFile", "Error", error, logId);
471
+ }
472
+ return false;
473
+ };
474
+
475
+ /**
476
+ * Get Archibus Id of the request
477
+ *
478
+ * @param {Object} request - Request definition on Pluss
479
+ * @returns {Number} Id of the request on Archibus (null if not exists)
480
+ */
481
+ getExternalId = async (request) => {
482
+ const logId = log("Archibus:GetExternalId", "Start", { Id: request.id });
483
+
484
+ // get external id
485
+ const externalEntityQuery = await indexQuery("externalentities", {
486
+ IndexName: "InternalIdIndex",
487
+ KeyConditionExpression:
488
+ "EntityType = :entityType AND InternalId = :internalId",
489
+ ExpressionAttributeValues: {
490
+ ":entityType": this.getEntityType(),
491
+ ":internalId": request.id,
492
+ },
493
+ });
494
+
495
+ log(
496
+ "Archibus:GetExternalId",
497
+ "ExternalLength",
498
+ externalEntityQuery.Items.length,
499
+ logId,
500
+ );
501
+ if (_.isEmpty(externalEntityQuery.Items)) {
502
+ return null;
503
+ }
504
+
505
+ const externalId = externalEntityQuery.Items[0].ExternalId;
506
+ log("Archibus:GetExternalId", "ExternalId", externalId, logId);
507
+
508
+ return externalId;
509
+ };
510
+
511
+ /**
512
+ * Get latest comment for a request
513
+ *
514
+ * @param {Object} request - Request definition on Pluss
515
+ * @param {String} entityKey - Request entity key (e.g. maintenancerequest)
516
+ * @returns {Object} Latest comment (null if not exists)
517
+ */
518
+ getLatestComment = async (request, entityKey) => {
519
+ const logId = log("Archibus:GetLatestComment", "Start", { Id: request.id });
520
+
521
+ // get comments
522
+ const commentsQuery = {
523
+ IndexName: "CommentsEntityIdIndex",
524
+ KeyConditionExpression: "EntityId = :groupId",
525
+ ExpressionAttributeValues: {
526
+ ":groupId": getRowId(request.id, entityKey),
527
+ },
528
+ };
529
+
530
+ const commentsQueryRes = await indexQuery("comments", commentsQuery);
531
+ const comments = _.orderBy(commentsQueryRes.Items, "Timestamp", "desc");
532
+ log(
533
+ "Archibus:GetLatestComment",
534
+ `CommentsLength - ${entityKey}`,
535
+ comments.length,
536
+ logId,
537
+ );
538
+
539
+ return comments.length > 0 ? comments[0] : null;
540
+ };
541
+
542
+ /**
543
+ * Perform actions when a task's status has changed
544
+ *
545
+ * @param {Object} request - Request definition on Pluss
546
+ * @returns {Boolean} Represents whether the actions were successful
547
+ */
548
+ onStatusChanged = async (request) => {
549
+ const logId = log("Archibus:OnStatusChanged", "Start", { Id: request.id });
550
+
551
+ try {
552
+ const externalId = await this.getExternalId(request);
553
+ if (_.isNil(externalId)) return true;
554
+
555
+ const statues = _.orderBy(
556
+ (request.history ?? []).filter(
557
+ (entry) => entry.EntryType !== "assignment",
558
+ ),
559
+ "timestamp",
560
+ "desc",
561
+ );
562
+ const latest = statues.length > 0 ? statues[0] : null;
563
+
564
+ if (latest) {
565
+ const time = moment(Number.parseFloat(latest.timestamp + "")).format(
566
+ "D MMM YYYY HH:mm:ss",
567
+ );
568
+ const user = latest.user ? ` by ${latest.user.displayName}` : "";
569
+ const comment = `${time}: Marked ${latest.status}${user}`;
570
+ await this.addCommentToRequest(externalId, comment);
571
+ }
572
+
573
+ return true;
574
+ } catch (error) {
575
+ log("Archibus:OnStatusChanged", "Error", error.toString(), logId);
576
+ }
577
+ return false;
578
+ };
579
+
580
+ /**
581
+ * Perform actions when a a comment has been added to a task
582
+ *
583
+ * @param {Object} request - Request definition on Pluss
584
+ * @returns {Boolean} Represents whether the actions were successful
585
+ */
586
+ onCommentAdded = async (request) => {
587
+ const logId = log("Archibus:OnCommentAdded", "Start", {
588
+ Id: request.id,
589
+ });
590
+
591
+ try {
592
+ const externalId = await this.getExternalId(request);
593
+ if (_.isNil(externalId)) return true;
594
+
595
+ let comment = await this.getLatestComment(request, values.serviceKey);
596
+ if (!comment)
597
+ comment = await this.getLatestComment(request, values.entityKey);
598
+
599
+ if (comment) {
600
+ const commentText = `${comment.User.displayName}:\n\n${
601
+ comment.Comment
602
+ }${!_.isEmpty(comment.Image) ? `\n\nImage: ${comment.Image}` : ""}}`;
603
+ const commentTime = moment(comment.Timestamp);
604
+ await this.addCommentToRequest(externalId, commentText, commentTime);
605
+ }
606
+
607
+ return true;
608
+ } catch (error) {
609
+ log("Archibus:OnCommentAdded", "Error", error.toString(), logId);
610
+ }
611
+ return false;
612
+ };
613
+
614
+ /**
615
+ * Perform actions when a note has been added to a task
616
+ *
617
+ * @param {Object} request - Request definition on Pluss
618
+ * @returns {Boolean} Represents whether the actions were successful
619
+ */
620
+ onNotesAdded = async (request) => {
621
+ const logId = log("Archibus:OnNotesAdded", "Start", { Id: request.id });
622
+
623
+ try {
624
+ const externalId = await this.getExternalId(request);
625
+ if (_.isNil(externalId)) return true;
626
+
627
+ const notes = _.orderBy(request.Notes ?? [], "Timestamp", "desc");
628
+ const latest = notes.length > 0 ? notes[0] : null;
629
+
630
+ if (latest) {
631
+ const promises = [];
632
+
633
+ const time = moment(Number.parseFloat(latest.Timestamp + "")).format(
634
+ "YYYY-MM-DD HH:mm:ss",
635
+ );
636
+ const user = latest.User ? latest.User.displayName : "Unknown User";
637
+ const note = latest.Note ? `Note: ${latest.Note}` : "No Note";
638
+ if (!_.isEmpty(latest.Attachments)) {
639
+ latest.Attachments.forEach((att) => {
640
+ promises.push(this.addFileToRequest(externalId, att.Source));
641
+ });
642
+ }
643
+ const attachments =
644
+ latest.Attachments && latest.Attachments.length > 0
645
+ ? `Attachments: ${latest.Attachments.map(
646
+ (att) => `${att.Title} (${att.Source})`,
647
+ ).join(", ")}`
648
+ : "No Attachments";
649
+ if (!_.isEmpty(latest.Images)) {
650
+ latest.Images.forEach((image) => {
651
+ promises.push(this.addFileToRequest(externalId, image));
652
+ });
653
+ }
654
+ const images =
655
+ latest.Images && latest.Images.length > 0
656
+ ? `Images: ${latest.Images.map((image) => image).join(", ")}`
657
+ : "No Images";
658
+ const comment = `${time} by ${user}\n${note}\n${attachments}\n${images}`;
659
+ promises.push(this.addCommentToRequest(externalId, comment));
660
+
661
+ await Promise.all(promises);
662
+ }
663
+
664
+ return true;
665
+ } catch (error) {
666
+ log("Archibus:OnNotesAdded", "Error", error.toString(), logId);
667
+ }
668
+ return false;
669
+ };
670
+
671
+ /**
672
+ * Perform completion actions when a task is completed
673
+ *
674
+ * @param {Object} request - Request definition on Pluss
675
+ * @returns {Boolean} Represents whether the actions were successful
676
+ */
677
+ onCompleteRequest = async (request) => {
678
+ return true;
679
+ };
680
+
681
+ // TODO: Remove once all existing records have ActiveEntityType (PC-1382)
682
+ // Backfills ActiveEntityType on existing externalentities records for the sparse GSI migration.
683
+ // Runs as part of the cron job after normal processing, respecting the given timeout.
684
+ backfillActiveEntityType = async (timeout) => {
685
+ const logId = log("Archibus:Backfill", "Start", true);
686
+ const startTime = moment().valueOf();
687
+ let backfillCount = 0;
688
+ let lastEvaluatedKey = null;
689
+
690
+ do {
691
+ const query = {
692
+ IndexName: "EntityTypeIndex",
693
+ KeyConditionExpression: "EntityType = :entityType",
694
+ ExpressionAttributeValues: {
695
+ ":entityType": this.getEntityType(),
696
+ },
697
+ Limit: 25,
698
+ };
699
+ if (lastEvaluatedKey) query.ExclusiveStartKey = lastEvaluatedKey;
700
+
701
+ const result = await indexQuery("externalentities", query);
702
+ lastEvaluatedKey = result.LastEvaluatedKey;
703
+
704
+ for (const item of result.Items) {
705
+ if (!item.ActiveEntityType) {
706
+ // Skip records already in a terminal status — they should stay out of the sparse GSI
707
+ const trackedStatus = item.TrackedData && item.TrackedData.status;
708
+ if (trackedStatus && this.completedStatuses.includes(trackedStatus)) {
709
+ continue;
710
+ }
711
+ item.ActiveEntityType = item.EntityType;
712
+ await updateRef("externalentities", item);
713
+ backfillCount++;
714
+ }
715
+ }
716
+ } while (lastEvaluatedKey && moment().valueOf() < startTime + timeout);
717
+
718
+ const complete = !lastEvaluatedKey;
719
+ log("Archibus:Backfill", "End", { backfillCount, complete }, logId);
720
+ return { backfillCount, complete };
721
+ };
623
722
  }
624
723
 
625
724
  module.exports = ArchibusStrategy;