@plusscommunities/pluss-maintenance-aws-feedback 2.1.34-beta.0 → 2.1.35-beta.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,678 +13,684 @@ 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.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
- const request = await this.getRequest(externalId);
244
- if (!request) {
245
- log("Archibus:RefreshFromSource", "Result:NoResponse", false, logId);
246
- return false;
247
- }
248
-
249
- if (trackedData && trackedData.status === request.status) {
250
- log("Archibus:RefreshFromSource", "Result:UnchangedStatus", false, logId);
251
- // status has not changed
252
- return false;
253
- }
254
-
255
- const statusToUse = this.statusMap[request.status];
256
- log("Archibus:RefreshFromSource", "UpdatedStatus", statusToUse, logId);
257
-
258
- if (statusToUse) {
259
- let plussRequest = await getRef(
260
- values.tableNameMaintenance,
261
- "id",
262
- requestId
263
- );
264
- // check how the new status map to what is saved in Pluss
265
-
266
- if (statusToUse.Status !== plussRequest.status) {
267
- // save updated status
268
-
269
- plussRequest = await editRef(
270
- values.tableNameMaintenance,
271
- "id",
272
- requestId,
273
- {
274
- status: statusToUse.Status,
275
- }
276
- );
277
- log(
278
- "Archibus:RefreshFromSource",
279
- "SavedStatus",
280
- statusToUse.Status,
281
- logId
282
- );
283
-
284
- if (statusToUse.Notification) {
285
- publishNotifications(
286
- [plussRequest.userID],
287
- values.activityMaintenanceJobStatusChanged,
288
- plussRequest.site,
289
- plussRequest.id,
290
- plussRequest,
291
- true
292
- );
293
- }
294
- }
295
- }
296
-
297
- // save tracked data
298
- await editRef(
299
- "externalentities",
300
- "RowId",
301
- `${this.getEntityType()}_${externalId}`,
302
- {
303
- TrackedData: {
304
- status: request.status,
305
- },
306
- }
307
- );
308
-
309
- // If the external status is terminal, remove ActiveEntityType to exclude from sparse GSI
310
- if (this.completedStatuses.includes(request.status)) {
311
- log("Archibus:RefreshFromSource", "TerminalStatus", request.status, logId);
312
- const entityRowId = `${this.getEntityType()}_${externalId}`;
313
- const entity = await getRef("externalentities", "RowId", entityRowId);
314
- if (entity && entity.ActiveEntityType) {
315
- delete entity.ActiveEntityType;
316
- await updateRef("externalentities", entity);
317
- }
318
- }
319
-
320
- log("Archibus:RefreshFromSource", "Result", true, logId);
321
- return true;
322
- };
323
-
324
- /**
325
- * Adds a comment to a request in Archibus
326
- *
327
- * @param {Number} externalId - Id of the request on Archibus.
328
- * @param {String} comment - Text to add to Archibus request.
329
- * @param {moment.Moment} time - The time to save against the comment
330
- */
331
- addCommentToRequest = async (externalId, comment, time) => {
332
- const logId = log("Archibus:AddComment", "Start", {
333
- externalId,
334
- comment,
335
- time,
336
- });
337
- try {
338
- // Format the time
339
- const timeToUse = (time || moment()).format("YYYY-MM-DD hh:mm:ss");
340
-
341
- // Generate the POST data
342
- const postData = {
343
- actionTime: timeToUse,
344
- comments: comment,
345
- wr_id: externalId,
346
- };
347
-
348
- log("Archibus:AddComment", "PostData", postData, logId);
349
-
350
- // Save to Archibus
351
- const response = await axios({
352
- method: "POST",
353
- url: `${this.baseUrl}/addCommentsToWRSteps`,
354
- data: postData,
355
- timeout: 30000,
356
- headers: {
357
- [this.apiKeyHeader]: this.apiKey,
358
- "Content-Type": "application/json",
359
- Host: this.host,
360
- },
361
- });
362
-
363
- log("Archibus:AddComment", "ResponseStatus", response.status, logId);
364
-
365
- return response;
366
- } catch (error) {
367
- log("Archibus:AddComment", "Error", error, logId);
368
- }
369
- return false;
370
- };
371
-
372
- /**
373
- * Adds a file to a request in Archibus
374
- *
375
- * @param {Number} externalId - Id of the request on Archibus.
376
- * @param {String} fileUrl - URL of the file to attach
377
- * @param {moment.Moment} time - The time to save against the file
378
- * @returns
379
- */
380
- addFileToRequest = async (externalId, fileUrl, time) => {
381
- const logId = log("Archibus:AddFile", "Start", {
382
- externalId,
383
- fileUrl,
384
- time,
385
- });
386
-
387
- // This endpoint is not currently functional. Instead, use the addCommentToRequest method to add a comment with the file URL.
388
- // return this.addCommentToRequest(externalId, fileUrl, time);
389
-
390
- try {
391
- // Download the file content from the URL
392
- const fileResponse = await axios.get(fileUrl, {
393
- responseType: "arraybuffer",
394
- });
395
- log("Archibus:AddFile", "GotFileResponse", fileResponse.status, logId);
396
-
397
- // Encode the file content in base64
398
- const fileContentBase64 = encode(fileResponse.data);
399
- log("Archibus:AddFile", "EncodedFileResponse", true, logId);
400
-
401
- // Extract file name from the URL
402
- const fileName = fileUrl.split("/").pop();
403
-
404
- // Format the time
405
- const timeToUse = (time || moment()).format("YYYY-MM-DD hh:mm:ss");
406
-
407
- // Generate the POST data
408
- const postData = {
409
- actionTime: timeToUse,
410
- docName: fileName,
411
- docUrl: "https://anglicare.plusscommunities.com/",
412
- fileContent: fileContentBase64,
413
- fileName: fileName,
414
- wrId: externalId,
415
- };
416
-
417
- log("Archibus:AddFile", "PostData", postData, logId);
418
-
419
- // Save to Archibus
420
- const response = await axios({
421
- method: "POST",
422
- url: `${this.baseUrl}/addDocumentToWorkRequest`,
423
- data: postData,
424
- timeout: 60000,
425
- headers: {
426
- [this.apiKeyHeader]: this.apiKey,
427
- "Content-Type": "application/json",
428
- Host: this.host,
429
- },
430
- });
431
-
432
- log("Archibus:AddFile", "ResponseStatus", response.status, logId);
433
-
434
- return response;
435
- } catch (error) {
436
- log("Archibus:AddFile", "Error", error, logId);
437
- }
438
- return false;
439
- };
440
-
441
- /**
442
- * Get Archibus Id of the request
443
- *
444
- * @param {Object} request - Request definition on Pluss
445
- * @returns {Number} Id of the request on Archibus (null if not exists)
446
- */
447
- getExternalId = async (request) => {
448
- const logId = log("Archibus:GetExternalId", "Start", { Id: request.id });
449
-
450
- // get external id
451
- const externalEntityQuery = await indexQuery("externalentities", {
452
- IndexName: "InternalIdIndex",
453
- KeyConditionExpression:
454
- "EntityType = :entityType AND InternalId = :internalId",
455
- ExpressionAttributeValues: {
456
- ":entityType": this.getEntityType(),
457
- ":internalId": request.id,
458
- },
459
- });
460
-
461
- log(
462
- "Archibus:GetExternalId",
463
- "ExternalLength",
464
- externalEntityQuery.Items.length,
465
- logId
466
- );
467
- if (_.isEmpty(externalEntityQuery.Items)) {
468
- return null;
469
- }
470
-
471
- const externalId = externalEntityQuery.Items[0].ExternalId;
472
- log("Archibus:GetExternalId", "ExternalId", externalId, logId);
473
-
474
- return externalId;
475
- };
476
-
477
- /**
478
- * Get latest comment for a request
479
- *
480
- * @param {Object} request - Request definition on Pluss
481
- * @param {String} entityKey - Request entity key (e.g. maintenancerequest)
482
- * @returns {Object} Latest comment (null if not exists)
483
- */
484
- getLatestComment = async (request, entityKey) => {
485
- const logId = log("Archibus:GetLatestComment", "Start", { Id: request.id });
486
-
487
- // get comments
488
- const commentsQuery = {
489
- IndexName: "CommentsEntityIdIndex",
490
- KeyConditionExpression: "EntityId = :groupId",
491
- ExpressionAttributeValues: {
492
- ":groupId": getRowId(request.id, entityKey),
493
- },
494
- };
495
-
496
- const commentsQueryRes = await indexQuery("comments", commentsQuery);
497
- const comments = _.orderBy(commentsQueryRes.Items, "Timestamp", "desc");
498
- log(
499
- "Archibus:GetLatestComment",
500
- `CommentsLength - ${entityKey}`,
501
- comments.length,
502
- logId
503
- );
504
-
505
- return comments.length > 0 ? comments[0] : null;
506
- };
507
-
508
- /**
509
- * Perform actions when a task's status has changed
510
- *
511
- * @param {Object} request - Request definition on Pluss
512
- * @returns {Boolean} Represents whether the actions were successful
513
- */
514
- onStatusChanged = async (request) => {
515
- const logId = log("Archibus:OnStatusChanged", "Start", { Id: request.id });
516
-
517
- try {
518
- const externalId = await this.getExternalId(request);
519
- if (_.isNil(externalId)) return true;
520
-
521
- const statues = _.orderBy(
522
- (request.history ?? []).filter(
523
- (entry) => entry.EntryType !== "assignment"
524
- ),
525
- "timestamp",
526
- "desc"
527
- );
528
- const latest = statues.length > 0 ? statues[0] : null;
529
-
530
- if (latest) {
531
- const time = moment(Number.parseFloat(latest.timestamp + "")).format(
532
- "D MMM YYYY HH:mm:ss"
533
- );
534
- const user = latest.user ? ` by ${latest.user.displayName}` : "";
535
- const comment = `${time}: Marked ${latest.status}${user}`;
536
- await this.addCommentToRequest(externalId, comment);
537
- }
538
-
539
- return true;
540
- } catch (error) {
541
- log("Archibus:OnStatusChanged", "Error", error.toString(), logId);
542
- }
543
- return false;
544
- };
545
-
546
- /**
547
- * Perform actions when a a comment has been added to a task
548
- *
549
- * @param {Object} request - Request definition on Pluss
550
- * @returns {Boolean} Represents whether the actions were successful
551
- */
552
- onCommentAdded = async (request) => {
553
- const logId = log("Archibus:OnCommentAdded", "Start", {
554
- Id: request.id,
555
- });
556
-
557
- try {
558
- const externalId = await this.getExternalId(request);
559
- if (_.isNil(externalId)) return true;
560
-
561
- let comment = await this.getLatestComment(request, values.serviceKey);
562
- if (!comment)
563
- comment = await this.getLatestComment(request, values.entityKey);
564
-
565
- if (comment) {
566
- const commentText = `${comment.User.displayName}:\n\n${
567
- comment.Comment
568
- }${!_.isEmpty(comment.Image) ? `\n\nImage: ${comment.Image}` : ""}}`;
569
- const commentTime = moment(comment.Timestamp);
570
- await this.addCommentToRequest(externalId, commentText, commentTime);
571
- }
572
-
573
- return true;
574
- } catch (error) {
575
- log("Archibus:OnCommentAdded", "Error", error.toString(), logId);
576
- }
577
- return false;
578
- };
579
-
580
- /**
581
- * Perform actions when a note 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
- onNotesAdded = async (request) => {
587
- const logId = log("Archibus:OnNotesAdded", "Start", { Id: request.id });
588
-
589
- try {
590
- const externalId = await this.getExternalId(request);
591
- if (_.isNil(externalId)) return true;
592
-
593
- const notes = _.orderBy(request.Notes ?? [], "Timestamp", "desc");
594
- const latest = notes.length > 0 ? notes[0] : null;
595
-
596
- if (latest) {
597
- const promises = [];
598
-
599
- const time = moment(Number.parseFloat(latest.Timestamp + "")).format(
600
- "YYYY-MM-DD HH:mm:ss"
601
- );
602
- const user = latest.User ? latest.User.displayName : "Unknown User";
603
- const note = latest.Note ? `Note: ${latest.Note}` : "No Note";
604
- if (!_.isEmpty(latest.Attachments)) {
605
- latest.Attachments.forEach((att) => {
606
- promises.push(this.addFileToRequest(externalId, att.Source));
607
- });
608
- }
609
- const attachments =
610
- latest.Attachments && latest.Attachments.length > 0
611
- ? `Attachments: ${latest.Attachments.map(
612
- (att) => `${att.Title} (${att.Source})`
613
- ).join(", ")}`
614
- : "No Attachments";
615
- if (!_.isEmpty(latest.Images)) {
616
- latest.Images.forEach((image) => {
617
- promises.push(this.addFileToRequest(externalId, image));
618
- });
619
- }
620
- const images =
621
- latest.Images && latest.Images.length > 0
622
- ? `Images: ${latest.Images.map((image) => image).join(", ")}`
623
- : "No Images";
624
- const comment = `${time} by ${user}\n${note}\n${attachments}\n${images}`;
625
- promises.push(this.addCommentToRequest(externalId, comment));
626
-
627
- await Promise.all(promises);
628
- }
629
-
630
- return true;
631
- } catch (error) {
632
- log("Archibus:OnNotesAdded", "Error", error.toString(), logId);
633
- }
634
- return false;
635
- };
636
-
637
- /**
638
- * Perform completion actions when a task is completed
639
- *
640
- * @param {Object} request - Request definition on Pluss
641
- * @returns {Boolean} Represents whether the actions were successful
642
- */
643
- onCompleteRequest = async (request) => {
644
- return true;
645
- };
646
-
647
- // TODO: Remove once all existing records have ActiveEntityType (PC-1382)
648
- // Backfills ActiveEntityType on existing externalentities records for the sparse GSI migration.
649
- // Runs as part of the cron job after normal processing, respecting the given timeout.
650
- backfillActiveEntityType = async (timeout) => {
651
- const logId = log("Archibus:Backfill", "Start", true);
652
- const startTime = moment().valueOf();
653
- let backfillCount = 0;
654
- let lastEvaluatedKey = null;
655
-
656
- do {
657
- const query = {
658
- IndexName: "EntityTypeIndex",
659
- KeyConditionExpression: "EntityType = :entityType",
660
- ExpressionAttributeValues: {
661
- ":entityType": this.getEntityType(),
662
- },
663
- Limit: 25,
664
- };
665
- if (lastEvaluatedKey) query.ExclusiveStartKey = lastEvaluatedKey;
666
-
667
- const result = await indexQuery("externalentities", query);
668
- lastEvaluatedKey = result.LastEvaluatedKey;
669
-
670
- for (const item of result.Items) {
671
- if (!item.ActiveEntityType) {
672
- // Skip records already in a terminal status — they should stay out of the sparse GSI
673
- const trackedStatus = item.TrackedData && item.TrackedData.status;
674
- if (trackedStatus && this.completedStatuses.includes(trackedStatus)) {
675
- continue;
676
- }
677
- item.ActiveEntityType = item.EntityType;
678
- await updateRef("externalentities", item);
679
- backfillCount++;
680
- }
681
- }
682
- } while (lastEvaluatedKey && moment().valueOf() < startTime + timeout);
683
-
684
- const complete = !lastEvaluatedKey;
685
- log("Archibus:Backfill", "End", { backfillCount, complete }, logId);
686
- return { backfillCount, complete };
687
- };
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
+ // If the external status is terminal, remove ActiveEntityType to exclude from sparse GSI
245
+ if (this.completedStatuses.includes(trackedData.status)) {
246
+ log(
247
+ "Archibus:RefreshFromSource",
248
+ "TerminalStatus",
249
+ trackedData.status,
250
+ logId,
251
+ );
252
+ const entityRowId = `${this.getEntityType()}_${externalId}`;
253
+ const entity = await getRef("externalentities", "RowId", entityRowId);
254
+ if (entity?.ActiveEntityType) {
255
+ delete entity.ActiveEntityType;
256
+ await updateRef("externalentities", entity);
257
+ }
258
+ }
259
+
260
+ const request = await this.getRequest(externalId);
261
+ if (!request) {
262
+ log("Archibus:RefreshFromSource", "Result:NoResponse", false, logId);
263
+ return false;
264
+ }
265
+
266
+ if (trackedData && trackedData.status === request.status) {
267
+ log("Archibus:RefreshFromSource", "Result:UnchangedStatus", false, logId);
268
+ // status has not changed
269
+ return false;
270
+ }
271
+
272
+ const statusToUse = this.statusMap[request.status];
273
+ log("Archibus:RefreshFromSource", "UpdatedStatus", statusToUse, logId);
274
+
275
+ if (statusToUse) {
276
+ let plussRequest = await getRef(
277
+ values.tableNameMaintenance,
278
+ "id",
279
+ requestId,
280
+ );
281
+ // check how the new status map to what is saved in Pluss
282
+
283
+ if (statusToUse.Status !== plussRequest.status) {
284
+ // save updated status
285
+
286
+ plussRequest = await editRef(
287
+ values.tableNameMaintenance,
288
+ "id",
289
+ requestId,
290
+ {
291
+ status: statusToUse.Status,
292
+ },
293
+ );
294
+ log(
295
+ "Archibus:RefreshFromSource",
296
+ "SavedStatus",
297
+ statusToUse.Status,
298
+ logId,
299
+ );
300
+
301
+ if (statusToUse.Notification) {
302
+ publishNotifications(
303
+ [plussRequest.userID],
304
+ values.activityMaintenanceJobStatusChanged,
305
+ plussRequest.site,
306
+ plussRequest.id,
307
+ plussRequest,
308
+ true,
309
+ );
310
+ }
311
+ }
312
+ }
313
+
314
+ // save tracked data
315
+ await editRef(
316
+ "externalentities",
317
+ "RowId",
318
+ `${this.getEntityType()}_${externalId}`,
319
+ {
320
+ TrackedData: {
321
+ status: request.status,
322
+ },
323
+ },
324
+ );
325
+
326
+ log("Archibus:RefreshFromSource", "Result", true, logId);
327
+ return true;
328
+ };
329
+
330
+ /**
331
+ * Adds a comment to a request in Archibus
332
+ *
333
+ * @param {Number} externalId - Id of the request on Archibus.
334
+ * @param {String} comment - Text to add to Archibus request.
335
+ * @param {moment.Moment} time - The time to save against the comment
336
+ */
337
+ addCommentToRequest = async (externalId, comment, time) => {
338
+ const logId = log("Archibus:AddComment", "Start", {
339
+ externalId,
340
+ comment,
341
+ time,
342
+ });
343
+ try {
344
+ // Format the time
345
+ const timeToUse = (time || moment()).format("YYYY-MM-DD hh:mm:ss");
346
+
347
+ // Generate the POST data
348
+ const postData = {
349
+ actionTime: timeToUse,
350
+ comments: comment,
351
+ wr_id: externalId,
352
+ };
353
+
354
+ log("Archibus:AddComment", "PostData", postData, logId);
355
+
356
+ // Save to Archibus
357
+ const response = await axios({
358
+ method: "POST",
359
+ url: `${this.baseUrl}/addCommentsToWRSteps`,
360
+ data: postData,
361
+ timeout: 30000,
362
+ headers: {
363
+ [this.apiKeyHeader]: this.apiKey,
364
+ "Content-Type": "application/json",
365
+ Host: this.host,
366
+ },
367
+ });
368
+
369
+ log("Archibus:AddComment", "ResponseStatus", response.status, logId);
370
+
371
+ return response;
372
+ } catch (error) {
373
+ log("Archibus:AddComment", "Error", error, logId);
374
+ }
375
+ return false;
376
+ };
377
+
378
+ /**
379
+ * Adds a file to a request in Archibus
380
+ *
381
+ * @param {Number} externalId - Id of the request on Archibus.
382
+ * @param {String} fileUrl - URL of the file to attach
383
+ * @param {moment.Moment} time - The time to save against the file
384
+ * @returns
385
+ */
386
+ addFileToRequest = async (externalId, fileUrl, time) => {
387
+ const logId = log("Archibus:AddFile", "Start", {
388
+ externalId,
389
+ fileUrl,
390
+ time,
391
+ });
392
+
393
+ // This endpoint is not currently functional. Instead, use the addCommentToRequest method to add a comment with the file URL.
394
+ // return this.addCommentToRequest(externalId, fileUrl, time);
395
+
396
+ try {
397
+ // Download the file content from the URL
398
+ const fileResponse = await axios.get(fileUrl, {
399
+ responseType: "arraybuffer",
400
+ });
401
+ log("Archibus:AddFile", "GotFileResponse", fileResponse.status, logId);
402
+
403
+ // Encode the file content in base64
404
+ const fileContentBase64 = encode(fileResponse.data);
405
+ log("Archibus:AddFile", "EncodedFileResponse", true, logId);
406
+
407
+ // Extract file name from the URL
408
+ const fileName = fileUrl.split("/").pop();
409
+
410
+ // Format the time
411
+ const timeToUse = (time || moment()).format("YYYY-MM-DD hh:mm:ss");
412
+
413
+ // Generate the POST data
414
+ const postData = {
415
+ actionTime: timeToUse,
416
+ docName: fileName,
417
+ docUrl: "https://anglicare.plusscommunities.com/",
418
+ fileContent: fileContentBase64,
419
+ fileName: fileName,
420
+ wrId: externalId,
421
+ };
422
+
423
+ log("Archibus:AddFile", "PostData", postData, logId);
424
+
425
+ // Save to Archibus
426
+ const response = await axios({
427
+ method: "POST",
428
+ url: `${this.baseUrl}/addDocumentToWorkRequest`,
429
+ data: postData,
430
+ timeout: 60000,
431
+ headers: {
432
+ [this.apiKeyHeader]: this.apiKey,
433
+ "Content-Type": "application/json",
434
+ Host: this.host,
435
+ },
436
+ });
437
+
438
+ log("Archibus:AddFile", "ResponseStatus", response.status, logId);
439
+
440
+ return response;
441
+ } catch (error) {
442
+ log("Archibus:AddFile", "Error", error, logId);
443
+ }
444
+ return false;
445
+ };
446
+
447
+ /**
448
+ * Get Archibus Id of the request
449
+ *
450
+ * @param {Object} request - Request definition on Pluss
451
+ * @returns {Number} Id of the request on Archibus (null if not exists)
452
+ */
453
+ getExternalId = async (request) => {
454
+ const logId = log("Archibus:GetExternalId", "Start", { Id: request.id });
455
+
456
+ // get external id
457
+ const externalEntityQuery = await indexQuery("externalentities", {
458
+ IndexName: "InternalIdIndex",
459
+ KeyConditionExpression:
460
+ "EntityType = :entityType AND InternalId = :internalId",
461
+ ExpressionAttributeValues: {
462
+ ":entityType": this.getEntityType(),
463
+ ":internalId": request.id,
464
+ },
465
+ });
466
+
467
+ log(
468
+ "Archibus:GetExternalId",
469
+ "ExternalLength",
470
+ externalEntityQuery.Items.length,
471
+ logId,
472
+ );
473
+ if (_.isEmpty(externalEntityQuery.Items)) {
474
+ return null;
475
+ }
476
+
477
+ const externalId = externalEntityQuery.Items[0].ExternalId;
478
+ log("Archibus:GetExternalId", "ExternalId", externalId, logId);
479
+
480
+ return externalId;
481
+ };
482
+
483
+ /**
484
+ * Get latest comment for a request
485
+ *
486
+ * @param {Object} request - Request definition on Pluss
487
+ * @param {String} entityKey - Request entity key (e.g. maintenancerequest)
488
+ * @returns {Object} Latest comment (null if not exists)
489
+ */
490
+ getLatestComment = async (request, entityKey) => {
491
+ const logId = log("Archibus:GetLatestComment", "Start", { Id: request.id });
492
+
493
+ // get comments
494
+ const commentsQuery = {
495
+ IndexName: "CommentsEntityIdIndex",
496
+ KeyConditionExpression: "EntityId = :groupId",
497
+ ExpressionAttributeValues: {
498
+ ":groupId": getRowId(request.id, entityKey),
499
+ },
500
+ };
501
+
502
+ const commentsQueryRes = await indexQuery("comments", commentsQuery);
503
+ const comments = _.orderBy(commentsQueryRes.Items, "Timestamp", "desc");
504
+ log(
505
+ "Archibus:GetLatestComment",
506
+ `CommentsLength - ${entityKey}`,
507
+ comments.length,
508
+ logId,
509
+ );
510
+
511
+ return comments.length > 0 ? comments[0] : null;
512
+ };
513
+
514
+ /**
515
+ * Perform actions when a task's status has changed
516
+ *
517
+ * @param {Object} request - Request definition on Pluss
518
+ * @returns {Boolean} Represents whether the actions were successful
519
+ */
520
+ onStatusChanged = async (request) => {
521
+ const logId = log("Archibus:OnStatusChanged", "Start", { Id: request.id });
522
+
523
+ try {
524
+ const externalId = await this.getExternalId(request);
525
+ if (_.isNil(externalId)) return true;
526
+
527
+ const statues = _.orderBy(
528
+ (request.history ?? []).filter(
529
+ (entry) => entry.EntryType !== "assignment",
530
+ ),
531
+ "timestamp",
532
+ "desc",
533
+ );
534
+ const latest = statues.length > 0 ? statues[0] : null;
535
+
536
+ if (latest) {
537
+ const time = moment(Number.parseFloat(latest.timestamp + "")).format(
538
+ "D MMM YYYY HH:mm:ss",
539
+ );
540
+ const user = latest.user ? ` by ${latest.user.displayName}` : "";
541
+ const comment = `${time}: Marked ${latest.status}${user}`;
542
+ await this.addCommentToRequest(externalId, comment);
543
+ }
544
+
545
+ return true;
546
+ } catch (error) {
547
+ log("Archibus:OnStatusChanged", "Error", error.toString(), logId);
548
+ }
549
+ return false;
550
+ };
551
+
552
+ /**
553
+ * Perform actions when a a comment has been added to a task
554
+ *
555
+ * @param {Object} request - Request definition on Pluss
556
+ * @returns {Boolean} Represents whether the actions were successful
557
+ */
558
+ onCommentAdded = async (request) => {
559
+ const logId = log("Archibus:OnCommentAdded", "Start", {
560
+ Id: request.id,
561
+ });
562
+
563
+ try {
564
+ const externalId = await this.getExternalId(request);
565
+ if (_.isNil(externalId)) return true;
566
+
567
+ let comment = await this.getLatestComment(request, values.serviceKey);
568
+ if (!comment)
569
+ comment = await this.getLatestComment(request, values.entityKey);
570
+
571
+ if (comment) {
572
+ const commentText = `${comment.User.displayName}:\n\n${
573
+ comment.Comment
574
+ }${!_.isEmpty(comment.Image) ? `\n\nImage: ${comment.Image}` : ""}}`;
575
+ const commentTime = moment(comment.Timestamp);
576
+ await this.addCommentToRequest(externalId, commentText, commentTime);
577
+ }
578
+
579
+ return true;
580
+ } catch (error) {
581
+ log("Archibus:OnCommentAdded", "Error", error.toString(), logId);
582
+ }
583
+ return false;
584
+ };
585
+
586
+ /**
587
+ * Perform actions when a note has been added to a task
588
+ *
589
+ * @param {Object} request - Request definition on Pluss
590
+ * @returns {Boolean} Represents whether the actions were successful
591
+ */
592
+ onNotesAdded = async (request) => {
593
+ const logId = log("Archibus:OnNotesAdded", "Start", { Id: request.id });
594
+
595
+ try {
596
+ const externalId = await this.getExternalId(request);
597
+ if (_.isNil(externalId)) return true;
598
+
599
+ const notes = _.orderBy(request.Notes ?? [], "Timestamp", "desc");
600
+ const latest = notes.length > 0 ? notes[0] : null;
601
+
602
+ if (latest) {
603
+ const promises = [];
604
+
605
+ const time = moment(Number.parseFloat(latest.Timestamp + "")).format(
606
+ "YYYY-MM-DD HH:mm:ss",
607
+ );
608
+ const user = latest.User ? latest.User.displayName : "Unknown User";
609
+ const note = latest.Note ? `Note: ${latest.Note}` : "No Note";
610
+ if (!_.isEmpty(latest.Attachments)) {
611
+ latest.Attachments.forEach((att) => {
612
+ promises.push(this.addFileToRequest(externalId, att.Source));
613
+ });
614
+ }
615
+ const attachments =
616
+ latest.Attachments && latest.Attachments.length > 0
617
+ ? `Attachments: ${latest.Attachments.map(
618
+ (att) => `${att.Title} (${att.Source})`,
619
+ ).join(", ")}`
620
+ : "No Attachments";
621
+ if (!_.isEmpty(latest.Images)) {
622
+ latest.Images.forEach((image) => {
623
+ promises.push(this.addFileToRequest(externalId, image));
624
+ });
625
+ }
626
+ const images =
627
+ latest.Images && latest.Images.length > 0
628
+ ? `Images: ${latest.Images.map((image) => image).join(", ")}`
629
+ : "No Images";
630
+ const comment = `${time} by ${user}\n${note}\n${attachments}\n${images}`;
631
+ promises.push(this.addCommentToRequest(externalId, comment));
632
+
633
+ await Promise.all(promises);
634
+ }
635
+
636
+ return true;
637
+ } catch (error) {
638
+ log("Archibus:OnNotesAdded", "Error", error.toString(), logId);
639
+ }
640
+ return false;
641
+ };
642
+
643
+ /**
644
+ * Perform completion actions when a task is completed
645
+ *
646
+ * @param {Object} request - Request definition on Pluss
647
+ * @returns {Boolean} Represents whether the actions were successful
648
+ */
649
+ onCompleteRequest = async (request) => {
650
+ return true;
651
+ };
652
+
653
+ // TODO: Remove once all existing records have ActiveEntityType (PC-1382)
654
+ // Backfills ActiveEntityType on existing externalentities records for the sparse GSI migration.
655
+ // Runs as part of the cron job after normal processing, respecting the given timeout.
656
+ backfillActiveEntityType = async (timeout) => {
657
+ const logId = log("Archibus:Backfill", "Start", true);
658
+ const startTime = moment().valueOf();
659
+ let backfillCount = 0;
660
+ let lastEvaluatedKey = null;
661
+
662
+ do {
663
+ const query = {
664
+ IndexName: "EntityTypeIndex",
665
+ KeyConditionExpression: "EntityType = :entityType",
666
+ ExpressionAttributeValues: {
667
+ ":entityType": this.getEntityType(),
668
+ },
669
+ Limit: 25,
670
+ };
671
+ if (lastEvaluatedKey) query.ExclusiveStartKey = lastEvaluatedKey;
672
+
673
+ const result = await indexQuery("externalentities", query);
674
+ lastEvaluatedKey = result.LastEvaluatedKey;
675
+
676
+ for (const item of result.Items) {
677
+ if (!item.ActiveEntityType) {
678
+ // Skip records already in a terminal status — they should stay out of the sparse GSI
679
+ const trackedStatus = item.TrackedData && item.TrackedData.status;
680
+ if (trackedStatus && this.completedStatuses.includes(trackedStatus)) {
681
+ continue;
682
+ }
683
+ item.ActiveEntityType = item.EntityType;
684
+ await updateRef("externalentities", item);
685
+ backfillCount++;
686
+ }
687
+ }
688
+ } while (lastEvaluatedKey && moment().valueOf() < startTime + timeout);
689
+
690
+ const complete = !lastEvaluatedKey;
691
+ log("Archibus:Backfill", "End", { backfillCount, complete }, logId);
692
+ return { backfillCount, complete };
693
+ };
688
694
  }
689
695
 
690
696
  module.exports = ArchibusStrategy;