@plusscommunities/pluss-maintenance-aws 2.1.21 → 2.1.23

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/getData.js CHANGED
@@ -5,6 +5,7 @@ const generateJsonResponse = require("@plusscommunities/pluss-core-aws/helper/ge
5
5
  const getAssignees = require("./requests/getAssignees");
6
6
  const getRequests = require("./requests/getRequests");
7
7
  const getRequest = require("./requests/getRequest");
8
+ const getExternalSync = require("./requests/getExternalSync");
8
9
 
9
10
  module.exports.getData = async (event, context, callback) => {
10
11
  init(config);
@@ -34,6 +35,12 @@ module.exports.getData = async (event, context, callback) => {
34
35
  log(action, "ResponseLength", response.data.Users.length, logId);
35
36
  }
36
37
  break;
38
+ case "externalsync":
39
+ response = await getExternalSync(event);
40
+ if (response.status === 200) {
41
+ log(action, "ExternalSystem", response.data.externalSystem, logId);
42
+ }
43
+ break;
37
44
  default:
38
45
  break;
39
46
  }
@@ -139,7 +139,7 @@ class ArchibusStrategy extends IntegrationStrategy {
139
139
  if (!updatedJob.history) updatedJob.history = [];
140
140
  updatedJob.history.push({
141
141
  timestamp: moment.utc().valueOf(),
142
- action: "ExternalIDSet",
142
+ EntryType: "ExternalIDSet",
143
143
  externalId: response.data.wrId,
144
144
  user: {
145
145
  displayName: "Archibus Integration",
@@ -176,7 +176,7 @@ class ArchibusStrategy extends IntegrationStrategy {
176
176
  if (!failedJob.history) failedJob.history = [];
177
177
  failedJob.history.push({
178
178
  timestamp: moment.utc().valueOf(),
179
- action: "ExternalIDSetFailed",
179
+ EntryType: "ExternalIDSetFailed",
180
180
  user: {
181
181
  displayName: "Archibus Integration",
182
182
  id: "system",
@@ -1,6 +1,7 @@
1
1
  const getString = require("@plusscommunities/pluss-core-aws/db/strings/getString");
2
2
  const { getRowId, log } = require("@plusscommunities/pluss-core-aws/helper");
3
3
  const ArchibusStrategy = require("./archibus/ArchibusStrategy");
4
+ const SeeStuffStrategy = require("./seestuff/SeeStuffStrategy");
4
5
  const IntegrationStrategy = require("./IntegrationStrategy");
5
6
  const { values } = require("../values.config");
6
7
 
@@ -16,6 +17,8 @@ exports.getStrategy = async (site) => {
16
17
  switch (importConfig.Strategy) {
17
18
  case "Archibus":
18
19
  return new ArchibusStrategy(importConfig);
20
+ case "SeeStuff":
21
+ return new SeeStuffStrategy(importConfig);
19
22
  default:
20
23
  return new IntegrationStrategy();
21
24
  }
@@ -0,0 +1,293 @@
1
+ const axios = require("axios");
2
+ const moment = require("moment");
3
+ const _ = require("lodash");
4
+ const IntegrationStrategy = require("../IntegrationStrategy");
5
+ const { log, getRowId } = require("@plusscommunities/pluss-core-aws/helper");
6
+ const editRef = require("@plusscommunities/pluss-core-aws/db/common/editRef");
7
+ const updateRef = require("@plusscommunities/pluss-core-aws/db/common/updateRef");
8
+ const getRef = require("@plusscommunities/pluss-core-aws/db/common/getRef");
9
+ const indexQuery = require("@plusscommunities/pluss-core-aws/db/common/indexQuery");
10
+ const { values } = require("../../values.config");
11
+
12
+ class SeeStuffStrategy extends IntegrationStrategy {
13
+ constructor(config) {
14
+ super();
15
+ this.baseUrl = config.BaseUrl; // base URL for the API
16
+ this.apiKeyHeader = config.APIKeyHeader; // header to use for API key
17
+ this.apiKey = config.APIKey; // API key
18
+
19
+ const url = new URL(this.baseUrl);
20
+ this.host = url.host;
21
+
22
+ this.siteMap = config.SiteMap ?? {};
23
+ }
24
+
25
+ /**
26
+ * Gets the entity type for the integration
27
+ *
28
+ * @returns {String} The entity type for the integration
29
+ */
30
+ getEntityType = () => {
31
+ return `${values.serviceKey}_SeeStuff`;
32
+ };
33
+
34
+ /**
35
+ * Validates the integration
36
+ *
37
+ * @returns {Boolean} Whether the integration is valid
38
+ */
39
+ isValidIntegration = () => {
40
+ return true;
41
+ };
42
+
43
+ /**
44
+ * Gets the refresh interval for the SeeStuff system
45
+ *
46
+ * @returns {Number} The refresh interval in milliseconds
47
+ */
48
+ getRefreshInterval = () => {
49
+ return Number.MAX_SAFE_INTEGER; // should never refresh
50
+ };
51
+
52
+ /**
53
+ * Creates a request in the SeeStuff system
54
+ *
55
+ * @param {Object} request - Request definition on Pluss
56
+ * @param {Object} mockResponse - Mock response from SeeStuff to simulate response
57
+ * @returns {Boolean} Whether the request was created on SeeStuff
58
+ */
59
+ createRequest = async (request, mockResponse = null) => {
60
+ const logId = log("SeeStuff:CreateRequest", "Start", request);
61
+
62
+ try {
63
+ // Get user details for room/address and CRMID
64
+ let userRoom = request.room || "";
65
+ let crmResidentId = "";
66
+
67
+ // Map site to location code if available
68
+ const location = this.siteMap[request.site];
69
+ if (!location) {
70
+ log("SeeStuff:CreateRequest", "LocationNotFound", request.site, logId);
71
+ return false;
72
+ }
73
+
74
+ if (request.userID) {
75
+ try {
76
+ // Look up user from users table
77
+ const user = await getRef("users", "Id", request.userID);
78
+ log("SeeStuff:CreateRequest", "User", user, logId);
79
+
80
+ if (user) {
81
+ // Get user's address/room
82
+ if (user.unit) {
83
+ userRoom = user.unit;
84
+ }
85
+
86
+ // Get CRMID from user fields
87
+ try {
88
+ const crmIdField = await getRef(
89
+ "userfields",
90
+ "RowId",
91
+ getRowId(request.userID, "CRMID")
92
+ );
93
+ if (crmIdField && crmIdField.Value) {
94
+ crmResidentId = crmIdField.Value;
95
+ }
96
+ log("SeeStuff:CreateRequest", "CRMID", crmResidentId, logId);
97
+ } catch (fieldError) {
98
+ log(
99
+ "SeeStuff:CreateRequest",
100
+ "CRMIDLookupError",
101
+ fieldError,
102
+ logId
103
+ );
104
+ }
105
+ }
106
+ } catch (userError) {
107
+ log("SeeStuff:CreateRequest", "UserLookupError", userError, logId);
108
+ }
109
+ }
110
+
111
+ // Format description similar to Archibus
112
+ const description = `${request.title}${
113
+ _.isEmpty(request.description) ? "" : `\n\n${request.description}`
114
+ }${_.isEmpty(request.room) ? "" : `\n\nLocation: ${request.room}`}${
115
+ _.isEmpty(request.userName) ? "" : `\n\nName: ${request.userName}`
116
+ }${_.isEmpty(request.phone) ? "" : `\n\nPhone: ${request.phone}`}${
117
+ _.isEmpty(request.type) ? "" : `\n\nJob Type: ${request.type}`
118
+ }${
119
+ _.isEmpty(request.images)
120
+ ? ""
121
+ : `\n\nImages: ${request.images.join("\n")}`
122
+ }`;
123
+
124
+ // Format date_reported
125
+ const dateReported = moment().format("YYYY-MM-DD HH:mm:ss");
126
+
127
+ // Build payload
128
+ const data = {
129
+ externalId: request.id,
130
+ description: description,
131
+ priority: "P3",
132
+ location: location,
133
+ room: userRoom,
134
+ date_reported: dateReported,
135
+ crmResidentId: crmResidentId,
136
+ };
137
+
138
+ log("SeeStuff:CreateRequest", "Request", data, logId);
139
+
140
+ const response =
141
+ mockResponse ??
142
+ (await axios({
143
+ method: "POST",
144
+ url: `${this.baseUrl}/assetmgmt/servicerequests`,
145
+ timeout: 30000,
146
+ headers: {
147
+ [this.apiKeyHeader]: this.apiKey,
148
+ "Content-Type": "application/json",
149
+ Host: this.host,
150
+ },
151
+ data,
152
+ }));
153
+
154
+ log("SeeStuff:CreateRequest", "Response", response.data, logId);
155
+
156
+ return true;
157
+ } catch (e) {
158
+ log("SeeStuff:CreateRequest", "Error", e, logId);
159
+
160
+ // Add history entry for failed integration
161
+ try {
162
+ const failedJob = await getRef(
163
+ values.tableNameMaintenance,
164
+ "id",
165
+ request.id
166
+ );
167
+ if (!failedJob.history) failedJob.history = [];
168
+ failedJob.history.push({
169
+ timestamp: moment.utc().valueOf(),
170
+ EntryType: "ExternalIDSetFailed",
171
+ user: {
172
+ displayName: "SeeStuff Integration",
173
+ id: "system",
174
+ },
175
+ });
176
+
177
+ await editRef(values.tableNameMaintenance, "id", request.id, {
178
+ history: failedJob.history,
179
+ });
180
+ } catch (historyError) {
181
+ log("SeeStuff:CreateRequest", "HistoryError", historyError, logId);
182
+ }
183
+ }
184
+ return false;
185
+ };
186
+
187
+ /**
188
+ * Fetches a request from the SeeStuff system
189
+ * Not implemented yet
190
+ *
191
+ * @param {String} externalId - Id of the request on SeeStuff
192
+ * @returns {Object} The request as it exists on SeeStuff
193
+ */
194
+ getRequest = async (externalId) => {
195
+ return null;
196
+ };
197
+
198
+ /**
199
+ * Refreshes a request from the SeeStuff system
200
+ * Not implemented yet
201
+ *
202
+ * @param {String} requestId - Id of the request on Pluss
203
+ * @param {String} externalId - Id of the request on SeeStuff
204
+ * @param {Object} trackedData - The set of fields tracked
205
+ * @returns {Boolean} Whether the request had any changes on SeeStuff
206
+ */
207
+ refreshFromSource = async (requestId, externalId, trackedData) => {
208
+ return false;
209
+ };
210
+
211
+ /**
212
+ * Get SeeStuff Id of the request
213
+ * Queries externalentities table to find the external ID
214
+ *
215
+ * @param {Object} request - Request definition on Pluss
216
+ * @returns {String} Id of the request on SeeStuff
217
+ */
218
+ getExternalId = async (request) => {
219
+ const logId = log("SeeStuff:GetExternalId", "Start", { Id: request.id });
220
+
221
+ // get external id
222
+ const externalEntityQuery = await indexQuery("externalentities", {
223
+ IndexName: "InternalIdIndex",
224
+ KeyConditionExpression:
225
+ "EntityType = :entityType AND InternalId = :internalId",
226
+ ExpressionAttributeValues: {
227
+ ":entityType": this.getEntityType(),
228
+ ":internalId": request.id,
229
+ },
230
+ });
231
+
232
+ log(
233
+ "SeeStuff:GetExternalId",
234
+ "ExternalLength",
235
+ externalEntityQuery.Items.length,
236
+ logId
237
+ );
238
+ if (_.isEmpty(externalEntityQuery.Items)) {
239
+ return null;
240
+ }
241
+
242
+ const externalId = externalEntityQuery.Items[0].ExternalId;
243
+ log("SeeStuff:GetExternalId", "ExternalId", externalId, logId);
244
+
245
+ return externalId;
246
+ };
247
+
248
+ /**
249
+ * Perform actions when a task's status has changed
250
+ * Not implemented yet
251
+ *
252
+ * @param {Object} request - Request definition on Pluss
253
+ * @returns {Boolean} Represents whether the actions were successful
254
+ */
255
+ onStatusChanged = async (request) => {
256
+ return true;
257
+ };
258
+
259
+ /**
260
+ * Perform actions when a comment has been added to a task
261
+ * Not implemented yet
262
+ *
263
+ * @param {Object} request - Request definition on Pluss
264
+ * @returns {Boolean} Represents whether the actions were successful
265
+ */
266
+ onCommentAdded = async (request) => {
267
+ return true;
268
+ };
269
+
270
+ /**
271
+ * Perform actions when a note has been added to a task
272
+ * Not implemented yet
273
+ *
274
+ * @param {Object} request - Request definition on Pluss
275
+ * @returns {Boolean} Represents whether the actions were successful
276
+ */
277
+ onNotesAdded = async (request) => {
278
+ return true;
279
+ };
280
+
281
+ /**
282
+ * Perform completion actions when a task is completed
283
+ * Not implemented yet
284
+ *
285
+ * @param {Object} request - Request definition on Pluss
286
+ * @returns {Boolean} Represents whether the actions were successful
287
+ */
288
+ onCompleteRequest = async (request) => {
289
+ return true;
290
+ };
291
+ }
292
+
293
+ module.exports = SeeStuffStrategy;
package/package-lock.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plusscommunities/pluss-maintenance-aws",
3
- "version": "2.1.21",
3
+ "version": "2.1.23",
4
4
  "lockfileVersion": 1,
5
5
  "requires": true,
6
6
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plusscommunities/pluss-maintenance-aws",
3
- "version": "2.1.21",
3
+ "version": "2.1.23",
4
4
  "description": "Extension package to enable maintenance on Pluss Communities Platform",
5
5
  "scripts": {
6
6
  "gc": "node ../../tools/gc ./",
@@ -0,0 +1,171 @@
1
+ const getRef = require("@plusscommunities/pluss-core-aws/db/common/getRef");
2
+ const indexQuery = require("@plusscommunities/pluss-core-aws/db/common/indexQuery");
3
+ const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
4
+ const isValidAssignee = require("./helper/isValidAssignee");
5
+ const getSessionUserFromReqAuthKey = require("@plusscommunities/pluss-core-aws/helper/auth/getSessionUserFromReqAuthKey");
6
+ const { log } = require("@plusscommunities/pluss-core-aws/helper");
7
+ const { values } = require("../values.config");
8
+
9
+ /**
10
+ * Retrieves external system sync information for a maintenance request
11
+ *
12
+ * Checks both SeeStuff and Archibus external entity tables to find
13
+ * if the request has been synced to an external system.
14
+ *
15
+ * @param {Object} event - Lambda event object
16
+ * @param {Object} params - Query parameters (optional, uses event.queryStringParameters if not provided)
17
+ * @returns {Object} Response object with status and data
18
+ *
19
+ * Response format (200):
20
+ * {
21
+ * externalSystem: "SeeStuff" | "Archibus",
22
+ * externalId: string,
23
+ * syncedAt: number (Unix timestamp),
24
+ * trackedData: object
25
+ * }
26
+ *
27
+ * Error responses:
28
+ * - 422: Missing required field (id)
29
+ * - 404: Request not found OR no external sync exists
30
+ * - 403: Not authorized (user doesn't have permission to view request)
31
+ * - 500: Internal server error
32
+ */
33
+ module.exports = async (event, params) => {
34
+ const data = params || event.queryStringParameters || {};
35
+ const logId = log("getExternalSync", "Params", data);
36
+
37
+ // Validate input
38
+ if (!data.id) {
39
+ log("getExternalSync", "MissingId", true, logId);
40
+ return {
41
+ status: 422,
42
+ data: { error: "Missing required field: id" },
43
+ };
44
+ }
45
+
46
+ try {
47
+ // Get the request to verify it exists and get site for permission check
48
+ let request;
49
+ try {
50
+ request = await getRef(values.tableNameMaintenance, "id", data.id);
51
+ } catch (error) {
52
+ log("getExternalSync", "RequestNotFound", { id: data.id }, logId);
53
+ return {
54
+ status: 404,
55
+ data: { error: "Request not found" },
56
+ };
57
+ }
58
+
59
+ if (!request) {
60
+ log("getExternalSync", "RequestNotFound", { id: data.id }, logId);
61
+ return {
62
+ status: 404,
63
+ data: { error: "Request not found" },
64
+ };
65
+ }
66
+
67
+ // Check permissions (same pattern as getRequest.js)
68
+ // Three levels of access:
69
+ // 1. Full admin with maintenance tracking permission
70
+ // 2. User assigned to the request
71
+ // 3. User who created the request
72
+ const authorised = await validateMasterAuth(
73
+ event,
74
+ values.permissionMaintenanceTracking,
75
+ request.site
76
+ );
77
+
78
+ const assignAuthorised = await isValidAssignee(
79
+ event,
80
+ request.site,
81
+ request.AssigneeId
82
+ );
83
+
84
+ if (!authorised && !assignAuthorised) {
85
+ // Check if user is the request owner
86
+ const userId = await getSessionUserFromReqAuthKey(event);
87
+ if (userId !== request.userID) {
88
+ log(
89
+ "getExternalSync",
90
+ "NotAuthorised",
91
+ { userId, requestOwner: request.userID },
92
+ logId
93
+ );
94
+ return {
95
+ status: 403,
96
+ data: { error: "Not authorised" },
97
+ };
98
+ }
99
+ }
100
+
101
+ log("getExternalSync", "Authorised", true, logId);
102
+
103
+ // Query externalentities table for this request
104
+ // Check both SeeStuff and Archibus entity types
105
+ const entityTypes = [
106
+ `${values.serviceKey}_SeeStuff`,
107
+ `${values.serviceKey}_Archibus`,
108
+ ];
109
+
110
+ log("getExternalSync", "CheckingEntityTypes", entityTypes, logId);
111
+
112
+ let externalSync = null;
113
+
114
+ // Check each external system type
115
+ for (const entityType of entityTypes) {
116
+ const query = await indexQuery("externalentities", {
117
+ IndexName: "InternalIdIndex",
118
+ KeyConditionExpression:
119
+ "EntityType = :entityType AND InternalId = :internalId",
120
+ ExpressionAttributeValues: {
121
+ ":entityType": entityType,
122
+ ":internalId": request.id,
123
+ },
124
+ });
125
+
126
+ log(
127
+ "getExternalSync",
128
+ `${entityType}_Results`,
129
+ query.Items.length,
130
+ logId
131
+ );
132
+
133
+ if (query.Items && query.Items.length > 0) {
134
+ const item = query.Items[0];
135
+ // Extract system name from entity type (e.g., "maintenance_SeeStuff" -> "SeeStuff")
136
+ const systemName = entityType.split("_")[1];
137
+
138
+ externalSync = {
139
+ externalSystem: systemName,
140
+ externalId: item.ExternalId,
141
+ syncedAt: item.LastUpdated,
142
+ trackedData: item.TrackedData || {},
143
+ };
144
+
145
+ log("getExternalSync", "FoundSync", externalSync, logId);
146
+ break; // Stop after finding first match
147
+ }
148
+ }
149
+
150
+ // If no external sync found, return 404
151
+ if (!externalSync) {
152
+ log("getExternalSync", "NoSyncFound", { id: data.id }, logId);
153
+ return {
154
+ status: 404,
155
+ data: { error: "No external sync found" },
156
+ };
157
+ }
158
+
159
+ log("getExternalSync", "Success", externalSync, logId);
160
+ return {
161
+ status: 200,
162
+ data: externalSync,
163
+ };
164
+ } catch (error) {
165
+ log("getExternalSync", "InternalError", error.toString(), logId);
166
+ return {
167
+ status: 500,
168
+ data: { error: "Internal error", message: error.message },
169
+ };
170
+ }
171
+ };
@@ -75,7 +75,7 @@ module.exports = async (event, data) => {
75
75
  if (!job.history) job.history = [];
76
76
  job.history.push({
77
77
  timestamp: moment.utc().valueOf(),
78
- action: "ExternalIDSet",
78
+ EntryType: "ExternalIDSet",
79
79
  externalId: data.externalId,
80
80
  user,
81
81
  });