@plusscommunities/pluss-maintenance-aws-forms 2.1.7-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.
package/getJobs.js ADDED
@@ -0,0 +1,63 @@
1
+ const config = require("./config.json");
2
+ const { init } = require("@plusscommunities/pluss-core-aws/config");
3
+ const { getBody } = require("@plusscommunities/pluss-core-aws/helper");
4
+ const generateJsonResponse = require("@plusscommunities/pluss-core-aws/helper/generateJsonResponse");
5
+ const indexQuery = require("@plusscommunities/pluss-core-aws/db/common/indexQuery");
6
+ const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
7
+ const getSessionUser = require("@plusscommunities/pluss-core-aws/helper/auth/getSessionUser");
8
+ const { values } = require("./values.config");
9
+
10
+ module.exports.getJobs = async (event, context, callback) => {
11
+ init(config);
12
+ const data = getBody(event);
13
+
14
+ if (!data.site) {
15
+ return callback(
16
+ null,
17
+ generateJsonResponse(422, {
18
+ error: { message: "User Types fetch -- no site detected." },
19
+ })
20
+ );
21
+ }
22
+
23
+ try {
24
+ const authorised = await validateMasterAuth(
25
+ event,
26
+ values.permissionMaintenanceTracking
27
+ );
28
+ const userId = authorised
29
+ ? null
30
+ : await getSessionUser(event.headers.authkey);
31
+ console.log("getting jobs for user", userId);
32
+
33
+ const query = {
34
+ IndexName: "MaintenanceSiteIndex",
35
+ KeyConditionExpression: "site = :site",
36
+ ExpressionAttributeValues: {
37
+ ":site": data.site,
38
+ },
39
+ };
40
+
41
+ try {
42
+ const res = await indexQuery(values.tableNameMaintenance, query);
43
+ let jobs = userId
44
+ ? res.Items.filter((job) => job.userID === userId)
45
+ : res.Items;
46
+ if (data.status)
47
+ jobs = jobs.filter((j) => data.status.includes(j.status));
48
+ if (data.type) jobs = jobs.filter((j) => data.type.includes(j.type));
49
+ console.log("jobs count", jobs?.length);
50
+ return callback(null, generateJsonResponse(200, jobs));
51
+ } catch (error1) {
52
+ return callback(
53
+ null,
54
+ generateJsonResponse(200, {
55
+ userFetchFail: true,
56
+ message: "Fail on user query. Please try again.",
57
+ })
58
+ );
59
+ }
60
+ } catch (error) {
61
+ callback(null, generateJsonResponse(422, { fail: true, error }));
62
+ }
63
+ };
@@ -0,0 +1,35 @@
1
+ class IntegrationStrategy {
2
+ getEntityType = () => {
3
+ return "maintenance_null";
4
+ };
5
+
6
+ isValidIntegration = () => {
7
+ return false;
8
+ };
9
+
10
+ getRefreshInterval = () => {
11
+ return 15 * 60 * 1000; // 15 minutes
12
+ };
13
+
14
+ // creates a request on the integrated system
15
+ createRequest = async (request) => {
16
+ return null;
17
+ };
18
+
19
+ // gets a request from the integrated system
20
+ getRequest = async (requestId) => {
21
+ return null;
22
+ };
23
+
24
+ // refreshed a request from the integrated system
25
+ refreshFromSource = async (requestId) => {
26
+ return null;
27
+ };
28
+
29
+ // actions taken when a request is marked as completed
30
+ onCompleteRequest = async (request) => {
31
+ return null;
32
+ };
33
+ }
34
+
35
+ module.exports = IntegrationStrategy;
@@ -0,0 +1,463 @@
1
+ const axios = require("axios");
2
+ const moment = require("moment");
3
+ const _ = require("lodash");
4
+ const { encode } = require("base64-arraybuffer");
5
+ const IntegrationStrategy = require("../IntegrationStrategy");
6
+ const { log, getRowId } = require("@plusscommunities/pluss-core-aws/helper");
7
+ const editRef = require("@plusscommunities/pluss-core-aws/db/common/editRef");
8
+ const updateRef = require("@plusscommunities/pluss-core-aws/db/common/updateRef");
9
+ const indexQuery = require("@plusscommunities/pluss-core-aws/db/common/indexQuery");
10
+ const publishNotifications = require("@plusscommunities/pluss-core-aws/db/notifications/publishNotifications");
11
+ const getRef = require("@plusscommunities/pluss-core-aws/db/common/getRef");
12
+ const { values } = require("../../values.config");
13
+
14
+ class ArchibusStrategy extends IntegrationStrategy {
15
+ constructor(config) {
16
+ super();
17
+ this.baseUrl = config.BaseUrl; // base URL for the API
18
+ this.apiKeyHeader = config.APIKeyHeader; // header to use for API key
19
+ this.apiKey = config.APIKey; // API key
20
+
21
+ const url = new URL(this.baseUrl);
22
+ this.host = url.host;
23
+
24
+ this.statusMap = config.StatusMap;
25
+ }
26
+
27
+ /**
28
+ * Gets the entity type for the integration
29
+ *
30
+ * @returns {String} The entity type for the integration
31
+ */
32
+ getEntityType = () => {
33
+ return `${values.serviceKey}_Archibus`;
34
+ };
35
+
36
+ /**
37
+ * Validates the integration
38
+ *
39
+ * @returns {Boolean} Whether the integration is valid
40
+ */
41
+ isValidIntegration = () => {
42
+ return true;
43
+ };
44
+
45
+ /**
46
+ * Gets the refresh interval for the Archibus system
47
+ *
48
+ * @returns {Number} The refresh interval in milliseconds
49
+ */
50
+ getRefreshInterval = () => {
51
+ return 15 * 60 * 1000; // 15 minutes
52
+ };
53
+
54
+ /**
55
+ * Creates a request from the Archibus system
56
+ *
57
+ * @param {Object} request - Request definition on Pluss
58
+ * @returns {Boolean} Whether the request was created on Archibus
59
+ */
60
+ createRequest = async (request) => {
61
+ const logId = log("Archibus:CreateRequest", "Start", request);
62
+ try {
63
+ const response = await axios({
64
+ method: "PUT",
65
+ url: `${this.baseUrl}/createWorkRequest`,
66
+ timeout: 15000,
67
+ headers: {
68
+ [this.apiKeyHeader]: this.apiKey,
69
+ "Content-Type": "application/json",
70
+ Host: this.host,
71
+ },
72
+ data: {
73
+ prob_type: "1.ON-SITE|1. MAINTENANCE",
74
+ requestor: "PLUSS",
75
+ description: `${request.title}${
76
+ _.isEmpty(request.description) ? "" : `\n\n${request.description}`
77
+ }${_.isEmpty(request.room) ? "" : `\n\nLocation: ${request.room}`}${
78
+ _.isEmpty(request.userName) ? "" : `\n\nName: ${request.userName}`
79
+ }${_.isEmpty(request.phone) ? "" : `\n\nPhone: ${request.phone}`}${
80
+ _.isEmpty(request.type) ? "" : `\n\nJob Type: ${request.type}`
81
+ }${
82
+ _.isEmpty(request.images)
83
+ ? ""
84
+ : `\n\nImages: ${request.images.join("\n")}`
85
+ }`,
86
+ site_id: "2025", //TODO need to do site mapping 2025 is WooloowareShores in prod
87
+ },
88
+ });
89
+ log("Archibus:CreateRequest", "Response", response.data, logId);
90
+
91
+ // Save the ID to external entities for future reference
92
+ await updateRef("externalentities", {
93
+ RowId: `${this.getEntityType()}_${response.data.wrId}`,
94
+ LastUpdated: moment().valueOf(),
95
+ EntityType: this.getEntityType(),
96
+ InternalId: request.id,
97
+ ExternalId: response.data.wrId,
98
+ TrackedData: {},
99
+ });
100
+
101
+ // Save the Archibus ID as the job number
102
+ await editRef(values.tableNameMaintenance, "id", request.id, {
103
+ jobNo: response.data.wrId,
104
+ jobId: response.data.wrId + "",
105
+ });
106
+
107
+ // add images
108
+ if (!_.isEmpty(request.images)) {
109
+ const imagePromises = [];
110
+ request.images.forEach((url) => {
111
+ imagePromises.push(this.addFileToRequest(response.data.wrId, url));
112
+ });
113
+ await Promise.all(imagePromises);
114
+ }
115
+
116
+ return true;
117
+ } catch (e) {
118
+ log("Archibus:CreateRequest", "Error", e, logId);
119
+ }
120
+ return false;
121
+ };
122
+
123
+ /**
124
+ * Fetches a request from the Archibus system
125
+ *
126
+ * @param {String} externalId - Id of the request on the Archibus
127
+ * @returns {Object} The request as it exists on Archibus
128
+ */
129
+ getRequest = async (externalId) => {
130
+ const logId = log("Archibus:GetRequest", "Start", externalId);
131
+ try {
132
+ const response = await axios({
133
+ method: "GET",
134
+ url: `${this.baseUrl}/getWorkRequest/${externalId}`,
135
+ timeout: 15000,
136
+ headers: {
137
+ [this.apiKeyHeader]: this.apiKey,
138
+ "Content-Type": "application/json",
139
+ Host: this.host,
140
+ },
141
+ });
142
+ log("Archibus:GetRequest", "Response", response.data, logId);
143
+
144
+ return response.data;
145
+ } catch (e) {
146
+ log("Archibus:GetRequest", "Error", e, logId);
147
+ }
148
+ return null;
149
+ };
150
+
151
+ /**
152
+ * Refreshes a request from the Archibus system
153
+ *
154
+ * @param {String} requestId - Id of the request on Pluss.
155
+ * @param {String} externalId - Id of the request on Archibus.
156
+ * @param {Object} trackedData - The set of fields tracked.
157
+ * @returns {Boolean} Whether the request had any changes on Archibus.
158
+ */
159
+ refreshFromSource = async (requestId, externalId, trackedData) => {
160
+ const logId = log("Archibus:RefreshFromSource", "Start", {
161
+ requestId,
162
+ externalId,
163
+ trackedData,
164
+ });
165
+ const request = await this.getRequest(externalId);
166
+ if (!request) {
167
+ log("Archibus:RefreshFromSource", "Result:NoResponse", false, logId);
168
+ return false;
169
+ }
170
+
171
+ if (trackedData && trackedData.status === request.status) {
172
+ log("Archibus:RefreshFromSource", "Result:UnchangedStatus", false, logId);
173
+ // status has not changed
174
+ return false;
175
+ }
176
+
177
+ const statusToUse = this.statusMap[request.status];
178
+ log("Archibus:RefreshFromSource", "UpdatedStatus", statusToUse, logId);
179
+
180
+ if (statusToUse) {
181
+ let plussRequest = await getRef(
182
+ values.tableNameMaintenance,
183
+ "id",
184
+ requestId
185
+ );
186
+ // check how the new status map to what is saved in Pluss
187
+
188
+ if (statusToUse.Status !== plussRequest.status) {
189
+ // save updated status
190
+
191
+ plussRequest = await editRef(
192
+ values.tableNameMaintenance,
193
+ "id",
194
+ requestId,
195
+ {
196
+ status: statusToUse.Status,
197
+ }
198
+ );
199
+ log(
200
+ "Archibus:RefreshFromSource",
201
+ "SavedStatus",
202
+ statusToUse.Status,
203
+ logId
204
+ );
205
+
206
+ if (statusToUse.Notification) {
207
+ publishNotifications(
208
+ [plussRequest.userID],
209
+ values.activityMaintenanceJobStatusChanged,
210
+ plussRequest.site,
211
+ plussRequest.id,
212
+ plussRequest,
213
+ true
214
+ );
215
+ }
216
+ }
217
+ }
218
+
219
+ // save tracked data
220
+ await editRef(
221
+ "externalentities",
222
+ "RowId",
223
+ `${this.getEntityType()}_${externalId}`,
224
+ {
225
+ TrackedData: {
226
+ status: request.status,
227
+ },
228
+ }
229
+ );
230
+
231
+ log("Archibus:RefreshFromSource", "Result", true, logId);
232
+ return true;
233
+ };
234
+
235
+ /**
236
+ * Adds a comment to a request in Archibus
237
+ *
238
+ * @param {Number} externalId - Id of the request on Archibus.
239
+ * @param {String} comment - Text to add to Archibus request.
240
+ * @param {moment.Moment} time - The time to save against the comment
241
+ */
242
+ addCommentToRequest = async (externalId, comment, time) => {
243
+ const logId = log("Archibus:AddComment", "Start", {
244
+ externalId,
245
+ comment,
246
+ time,
247
+ });
248
+ try {
249
+ // Format the time
250
+ const timeToUse = (time || moment()).format("YYYY-MM-DD hh:mm:ss");
251
+
252
+ // Generate the POST data
253
+ const postData = {
254
+ actionTime: timeToUse,
255
+ comments: comment,
256
+ wr_id: externalId,
257
+ };
258
+
259
+ log("Archibus:AddComment", "PostData", postData, logId);
260
+
261
+ // Save to Archibus
262
+ const response = await axios({
263
+ method: "POST",
264
+ url: `${this.baseUrl}/addCommentsToWRSteps`,
265
+ data: postData,
266
+ timeout: 15000,
267
+ headers: {
268
+ [this.apiKeyHeader]: this.apiKey,
269
+ "Content-Type": "application/json",
270
+ Host: this.host,
271
+ },
272
+ });
273
+
274
+ log("Archibus:AddComment", "ResponseStatus", response.status, logId);
275
+
276
+ return response;
277
+ } catch (error) {
278
+ log("Archibus:AddComment", "Error", error, logId);
279
+ }
280
+ return false;
281
+ };
282
+
283
+ /**
284
+ * Adds a file to a request in Archibus
285
+ *
286
+ * @param {Number} externalId - Id of the request on Archibus.
287
+ * @param {String} fileUrl - URL of the file to attach
288
+ * @param {moment.Moment} time - The time to save against the file
289
+ * @returns
290
+ */
291
+ addFileToRequest = async (externalId, fileUrl, time) => {
292
+ const logId = log("Archibus:AddFile", "Start", {
293
+ externalId,
294
+ fileUrl,
295
+ time,
296
+ });
297
+
298
+ // This endpoint is not currently functional. Instead, use the addCommentToRequest method to add a comment with the file URL.
299
+ // return this.addCommentToRequest(externalId, fileUrl, time);
300
+
301
+ try {
302
+ // Download the file content from the URL
303
+ const fileResponse = await axios.get(fileUrl, {
304
+ responseType: "arraybuffer",
305
+ });
306
+ log("Archibus:AddFile", "GotFileResponse", fileResponse.status, logId);
307
+
308
+ // Encode the file content in base64
309
+ const fileContentBase64 = encode(fileResponse.data);
310
+ log("Archibus:AddFile", "EncodedFileResponse", true, logId);
311
+
312
+ // Extract file name from the URL
313
+ const fileName = fileUrl.split("/").pop();
314
+
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
+ docName: fileName,
322
+ docUrl: "https://anglicare.plusscommunities.com/",
323
+ fileContent: fileContentBase64,
324
+ fileName: fileName,
325
+ wrId: externalId,
326
+ };
327
+
328
+ log("Archibus:AddFile", "PostData", postData, logId);
329
+
330
+ // Save to Archibus
331
+ const response = await axios({
332
+ method: "POST",
333
+ url: `${this.baseUrl}/addDocumentToWorkRequest`,
334
+ data: postData,
335
+ timeout: 60000,
336
+ headers: {
337
+ [this.apiKeyHeader]: this.apiKey,
338
+ "Content-Type": "application/json",
339
+ Host: this.host,
340
+ },
341
+ });
342
+
343
+ log("Archibus:AddFile", "ResponseStatus", response.status, logId);
344
+
345
+ return response;
346
+ } catch (error) {
347
+ log("Archibus:AddFile", "Error", error, logId);
348
+ }
349
+ return false;
350
+ };
351
+
352
+ /**
353
+ *
354
+ * @param {Object} request - Request definition on Pluss
355
+ * @returns {Boolean} Represents whether the actions were successful
356
+ */
357
+ onCompleteRequest = async (request) => {
358
+ const logId = log("Archibus:OnComplete", "Start", {
359
+ Id: request.id,
360
+ });
361
+ try {
362
+ // get external id
363
+ const externalEntityQuery = await indexQuery("externalentities", {
364
+ IndexName: "InternalIdIndex",
365
+ KeyConditionExpression:
366
+ "EntityType = :entityType AND InternalId = :internalId",
367
+ ExpressionAttributeValues: {
368
+ ":entityType": this.getEntityType(),
369
+ ":internalId": request.id,
370
+ },
371
+ });
372
+
373
+ log(
374
+ "Archibus:OnComplete",
375
+ "ExternalLength",
376
+ externalEntityQuery.Items.length,
377
+ logId
378
+ );
379
+ if (_.isEmpty(externalEntityQuery.Items)) {
380
+ return true;
381
+ }
382
+
383
+ const externalId = externalEntityQuery.Items[0].ExternalId;
384
+ log("Archibus:OnComplete", "ExternalId", externalId, logId);
385
+
386
+ // get comments
387
+ const commentsQuery = {
388
+ IndexName: "CommentsEntityIdIndex",
389
+ KeyConditionExpression: "EntityId = :groupId",
390
+ ExpressionAttributeValues: {
391
+ ":groupId": getRowId(request.id, values.serviceKey),
392
+ },
393
+ };
394
+
395
+ const commentsQueryRes = await indexQuery("comments", commentsQuery);
396
+ const comments = commentsQueryRes.Items;
397
+ log("Archibus:OnComplete", "CommentsLength", comments.length, logId);
398
+
399
+ const promises = [];
400
+
401
+ // save history as a comment
402
+ if (!_.isEmpty(request.history)) {
403
+ const historyComment = _.map(request.history, (entry) => {
404
+ if (entry.EntryType === "assignment") {
405
+ return "";
406
+ }
407
+ const time = moment(Number.parseFloat(entry.timestamp + "")).format(
408
+ "D MMM YYYY HH:mm:ss"
409
+ );
410
+ const user = entry.user ? ` by ${entry.user.displayName}` : "";
411
+ return `${time}: Marked ${entry.status}${user}`;
412
+ }).join("\n");
413
+ promises.push(this.addCommentToRequest(externalId, historyComment));
414
+ }
415
+
416
+ // save history as a comment
417
+ if (!_.isEmpty(request.Notes)) {
418
+ // format comment to save
419
+ const notesComment = _.map(request.Notes, (entry) => {
420
+ const time = moment(Number.parseFloat(entry.Timestamp + "")).format(
421
+ "YYYY-MM-DD HH:mm:ss"
422
+ );
423
+ const user = entry.User ? entry.User.displayName : "Unknown User";
424
+ const note = entry.Note ? `Note: ${entry.Note}` : "No Note";
425
+ if (!_.isEmpty(entry.Attachments)) {
426
+ entry.Attachments.forEach((att) => {
427
+ promises.push(this.addFileToRequest(externalId, att.Source));
428
+ });
429
+ }
430
+ const attachments =
431
+ entry.Attachments && entry.Attachments.length > 0
432
+ ? `Attachments: ${entry.Attachments.map(
433
+ (att) => `${att.Title} (${att.Source})`
434
+ ).join(", ")}`
435
+ : "No Attachments";
436
+ return `${time} by ${user}\n${note}\n${attachments}`;
437
+ }).join("\n\n");
438
+ // save comment
439
+ promises.push(this.addCommentToRequest(externalId, notesComment));
440
+ }
441
+
442
+ // save comments to Archibus
443
+ comments.forEach((comment) => {
444
+ const commentText = `${comment.User.displayName}:\n\n${
445
+ comment.Comment
446
+ }${!_.isEmpty(comment.Image) ? `\n\nImage: ${comment.Image}` : ""}}`;
447
+ const commentTime = moment(comment.Timestamp);
448
+ promises.push(
449
+ this.addCommentToRequest(externalId, commentText, commentTime)
450
+ );
451
+ });
452
+
453
+ await Promise.all(promises);
454
+
455
+ return true;
456
+ } catch (error) {
457
+ log("Archibus:OnComplete", "Error", error.toString(), logId);
458
+ }
459
+ return false;
460
+ };
461
+ }
462
+
463
+ module.exports = ArchibusStrategy;
@@ -0,0 +1,23 @@
1
+ const getString = require("@plusscommunities/pluss-core-aws/db/strings/getString");
2
+ const { getRowId, log } = require("@plusscommunities/pluss-core-aws/helper");
3
+ const ArchibusStrategy = require("./archibus/ArchibusStrategy");
4
+ const IntegrationStrategy = require("./IntegrationStrategy");
5
+ const { values } = require("../values.config");
6
+
7
+ exports.getStrategy = async (site) => {
8
+ let importConfig;
9
+ const logId = log("getStrategy", "Site", site);
10
+ try {
11
+ importConfig = await getString(
12
+ getRowId(site, `${values.serviceKey}integration`),
13
+ "plussSpace"
14
+ );
15
+
16
+ switch (importConfig.Strategy) {
17
+ case "Archibus":
18
+ return new ArchibusStrategy(importConfig);
19
+ default:
20
+ return new IntegrationStrategy();
21
+ }
22
+ } catch (e) {}
23
+ };
package/jobChanged.js ADDED
@@ -0,0 +1,73 @@
1
+ const { Marshaller } = require("@aws/dynamodb-auto-marshaller");
2
+ const config = require("./config.json");
3
+ const { init } = require("@plusscommunities/pluss-core-aws/config");
4
+ const logUpdate = require("@plusscommunities/pluss-core-aws/db/strings/logUpdate");
5
+ const { getStrategy } = require("./integration");
6
+ const { log } = require("@plusscommunities/pluss-core-aws/helper");
7
+ const { values } = require("./values.config");
8
+
9
+ const marshaller = new Marshaller();
10
+
11
+ const pushRequestToIntegration = async (request) => {
12
+ const integrationStrategy = await getStrategy(request.site);
13
+ await integrationStrategy.createRequest(request);
14
+ };
15
+
16
+ const onCompletedRequest = async (request) => {
17
+ const integrationStrategy = await getStrategy(request.site);
18
+ await integrationStrategy.onCompleteRequest(request);
19
+ };
20
+
21
+ /**
22
+ * Checks for any necessary integration actions based on the change in the request data
23
+ * @param {Object} request The new request data
24
+ * @param {Object} prevRequest The previous request data
25
+ */
26
+ const checkIntegrationActions = async (request, prevRequest) => {
27
+ if (
28
+ request.ExtCreateRetry &&
29
+ request.ExtCreateRetry !== prevRequest.ExtCreateRetry
30
+ ) {
31
+ log("checkIntegrationActions", "Retrying", true);
32
+ await pushRequestToIntegration(request);
33
+ }
34
+
35
+ if (request.status === "Completed" && prevRequest.status !== "Completed") {
36
+ log("checkIntegrationActions", "CompletedRequest", true);
37
+ await onCompletedRequest(request);
38
+ }
39
+ };
40
+
41
+ module.exports.jobChanged = (event, context, callback) => {
42
+ init(config);
43
+
44
+ event.Records.forEach((record) => {
45
+ let site;
46
+ if (record.eventName == "INSERT") {
47
+ console.log("INSERT");
48
+ const data = marshaller.unmarshallItem(record.dynamodb.NewImage);
49
+ console.log("Record: ", JSON.stringify(data));
50
+
51
+ site = data.site;
52
+
53
+ pushRequestToIntegration(data);
54
+ } else if (record.eventName == "MODIFY") {
55
+ console.log("MODIFY");
56
+ const data = marshaller.unmarshallItem(record.dynamodb.NewImage);
57
+ const previousData = marshaller.unmarshallItem(record.dynamodb.OldImage);
58
+ console.log("New record: ", JSON.stringify(data));
59
+ console.log("Old record: ", JSON.stringify(previousData));
60
+
61
+ checkIntegrationActions(data, previousData);
62
+
63
+ site = data.site;
64
+ } else if (record.eventName == "REMOVE") {
65
+ console.log("REMOVE");
66
+ const previousData = marshaller.unmarshallItem(record.dynamodb.OldImage);
67
+ console.log("Old record: ", JSON.stringify(previousData));
68
+
69
+ site = previousData.site;
70
+ }
71
+ logUpdate(site, values.updateKey);
72
+ });
73
+ };