@plusscommunities/pluss-maintenance-aws 2.1.44 → 2.1.46

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.
Files changed (45) hide show
  1. package/createJob.js +201 -114
  2. package/createJobType.js +49 -49
  3. package/db/maintenance/addMaintenanceJob.js +100 -90
  4. package/db/maintenance/editMaintenanceJob.js +22 -12
  5. package/db/maintenance/getJobEmail.js +22 -22
  6. package/deleteJob.js +58 -58
  7. package/deleteJobType.js +40 -40
  8. package/editJob.js +126 -65
  9. package/editJobStatus.js +57 -57
  10. package/editJobType.js +61 -61
  11. package/editNote.js +117 -117
  12. package/feature.config.js +279 -262
  13. package/getData.js +48 -41
  14. package/getJob.js +4 -4
  15. package/getJobType.js +35 -35
  16. package/getJobTypes.js +66 -66
  17. package/getJobs.js +48 -48
  18. package/integration/IntegrationStrategy.js +52 -52
  19. package/integration/archibus/ArchibusStrategy.js +42 -28
  20. package/integration/index.js +19 -19
  21. package/jobChanged.js +88 -89
  22. package/jobTypesChanged.js +52 -39
  23. package/package.json +3 -3
  24. package/requests/assignRequest.js +81 -81
  25. package/requests/getAssignees.js +27 -27
  26. package/requests/getExternalSync.js +100 -100
  27. package/requests/getPropertyRequests.js +89 -0
  28. package/requests/getRequest.js +77 -77
  29. package/requests/getRequests.js +210 -144
  30. package/requests/helper/hasRequestPermission.js +12 -12
  31. package/requests/helper/isValidAssignee.js +10 -10
  32. package/requests/retrySync.js +90 -90
  33. package/requests/setExternalJobId.js +113 -113
  34. package/requests/updatePriority.js +33 -33
  35. package/scheduleJobImport.js +63 -63
  36. package/sendJobEmail.js +105 -104
  37. package/updateData.js +34 -34
  38. package/values.config.a.js +26 -25
  39. package/values.config.default.js +30 -28
  40. package/values.config.enquiry.js +222 -221
  41. package/values.config.feedback.js +194 -193
  42. package/values.config.food.js +29 -28
  43. package/values.config.forms.js +29 -28
  44. package/values.config.js +30 -28
  45. package/watchJobs.js +154 -154
package/createJob.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const _ = require("lodash");
2
2
  const config = require("./config.json");
3
3
  const { init } = require("@plusscommunities/pluss-core-aws/config");
4
- const { getBody } = require("@plusscommunities/pluss-core-aws/helper");
4
+ const { getBody, log } = require("@plusscommunities/pluss-core-aws/helper");
5
5
  const generateJsonResponse = require("@plusscommunities/pluss-core-aws/helper/generateJsonResponse");
6
6
  const addMaintenanceJob = require("./db/maintenance/addMaintenanceJob");
7
7
  const validateSiteAccess = require("@plusscommunities/pluss-core-aws/helper/auth/validateSiteAccess");
@@ -11,126 +11,213 @@ const getUserPreview = require("@plusscommunities/pluss-core-aws/helper/getUserP
11
11
  const publishActivity = require("@plusscommunities/pluss-core-aws/db/activity/publishActivity");
12
12
  const sendJobEmail = require("./sendJobEmail");
13
13
  const logAnalyticsActivity = require("@plusscommunities/pluss-core-aws/db/analytics/logAnalyticsActivity");
14
+ const {
15
+ PropertyRepository,
16
+ } = require("@plusscommunities/pluss-core-aws/db/property/PropertyRepository");
14
17
  const { values } = require("./values.config");
15
18
 
19
+ const properties = new PropertyRepository();
20
+
16
21
  module.exports.createJob = (event, context, callback) => {
17
- init(config);
18
- const data = getBody(event);
22
+ // Avoids waiting on the pooled DB connection to close before returning.
23
+ context.callbackWaitsForEmptyEventLoop = false;
24
+ init(config);
25
+ const data = getBody(event);
26
+
27
+ validateSiteAccess(event, data.location)
28
+ .then(async (authorised) => {
29
+ if (!authorised) {
30
+ return callback(
31
+ null,
32
+ generateJsonResponse(403, { fail: true, error: "not authorised" }),
33
+ );
34
+ }
35
+ // With linking on, location is resolved later; without it, room is required now.
36
+ const hasLocation = values.linkToProperty || !_.isUndefined(data.room);
37
+ if (
38
+ !data.userID ||
39
+ _.isUndefined(data.userName) ||
40
+ !hasLocation ||
41
+ _.isUndefined(data.title)
42
+ ) {
43
+ log("createJob", "InsufficientInput", data.userID);
44
+ return callback(
45
+ null,
46
+ generateJsonResponse(422, { error: "Insufficient input" }),
47
+ );
48
+ }
49
+ let user = await getUserPreviewFromReq(event);
50
+ // Capture submitter before `user` is reassigned to the subject below.
51
+ const submittingUserId = user.id;
52
+ const isOnBehalf = data.userID !== "undefined" && user.id !== data.userID;
53
+ if (isOnBehalf) {
54
+ const canRequest = await validateMasterAuth(
55
+ event,
56
+ values.permissionMaintenanceTracking,
57
+ data.location,
58
+ );
59
+ if (!canRequest) {
60
+ return callback(
61
+ null,
62
+ generateJsonResponse(422, {
63
+ fail: true,
64
+ error: "cannot request on behalf",
65
+ }),
66
+ );
67
+ }
19
68
 
20
- validateSiteAccess(event, data.location)
21
- .then(async (authorised) => {
22
- if (!authorised) {
23
- return callback(
24
- null,
25
- generateJsonResponse(403, { fail: true, error: "not authorised" })
26
- );
27
- }
28
- if (
29
- !data.userID ||
30
- _.isUndefined(data.userName) ||
31
- _.isUndefined(data.room) ||
32
- _.isUndefined(data.title)
33
- ) {
34
- console.error("insufficient input", data.userID);
35
- return callback(
36
- null,
37
- generateJsonResponse(422, { error: "Insufficient input" })
38
- );
39
- }
40
- let user = await getUserPreviewFromReq(event);
41
- if (data.userID !== "undefined" && user.id !== data.userID) {
42
- const canRequest = await validateMasterAuth(
43
- event,
44
- values.permissionMaintenanceTracking,
45
- data.location
46
- );
47
- if (!canRequest) {
48
- return callback(
49
- null,
50
- generateJsonResponse(422, {
51
- fail: true,
52
- error: "cannot request on behalf",
53
- })
54
- );
55
- }
69
+ user = await getUserPreview(data.userID);
70
+ }
56
71
 
57
- user = await getUserPreview(data.userID);
58
- }
72
+ // Resolved server-side, never taken from the request, so a resident cannot spoof a property.
73
+ let resolvedProperty = null;
74
+ if (values.linkToProperty) {
75
+ // An explicit propertyId is honoured for any actor holding the tracking
76
+ // permission (staff can also submit as themselves); it is client input,
77
+ // so resolution fails closed.
78
+ const picksProperty =
79
+ !!data.propertyId &&
80
+ (isOnBehalf ||
81
+ (await validateMasterAuth(
82
+ event,
83
+ values.permissionMaintenanceTracking,
84
+ data.location,
85
+ )));
86
+ if (picksProperty) {
87
+ try {
88
+ resolvedProperty = await properties.byId({
89
+ site: data.location,
90
+ propertyId: data.propertyId,
91
+ });
92
+ } catch (error) {
93
+ // A resolver outage is not invalid input.
94
+ log("createJob", "PropertyResolveError", error.message);
95
+ return callback(
96
+ null,
97
+ generateJsonResponse(500, { error: "Internal Error" }),
98
+ );
99
+ }
100
+ if (!resolvedProperty) {
101
+ return callback(
102
+ null,
103
+ generateJsonResponse(422, {
104
+ error: "Invalid property for this site",
105
+ }),
106
+ );
107
+ }
108
+ } else if (!isOnBehalf) {
109
+ // Self-submission: resolve the submitter's own link, failing open so a
110
+ // resolver outage cannot block residents from reporting.
111
+ try {
112
+ resolvedProperty = await properties.forUser({
113
+ site: data.location,
114
+ userId: submittingUserId,
115
+ });
116
+ } catch (error) {
117
+ log("createJob", "PropertyResolveError", error.message);
118
+ resolvedProperty = null;
119
+ }
120
+ }
121
+ }
59
122
 
60
- addMaintenanceJob(
61
- user.id,
62
- user.displayName,
63
- user,
64
- data.phone,
65
- data.room,
66
- data.title,
67
- data.description,
68
- data.type,
69
- data.isHome,
70
- data.homeText,
71
- data.images || data.image,
72
- data.location,
73
- data.audience,
74
- data.customFields
75
- ).then((id) => {
76
- sendJobEmail({
77
- userID: user.id,
78
- userName: user.displayName,
79
- phone: data.phone,
80
- room: data.room,
81
- title: data.title,
82
- description: data.description,
83
- type: data.type,
84
- isHome: data.isHome,
85
- homeText: data.homeText,
86
- image: data.image,
87
- images: data.images,
88
- site: data.location,
89
- audience: data.audience,
90
- id,
91
- customFields: data.customFields,
92
- });
123
+ // Record and email use the same resolved name so they never disagree.
124
+ const propertyId = resolvedProperty ? resolvedProperty.id : undefined;
125
+ const propertyName = resolvedProperty ? resolvedProperty.name : undefined;
126
+ const room = propertyName || data.room;
93
127
 
94
- publishActivity(
95
- values.activityAddMaintenanceJob,
96
- data.location,
97
- id,
98
- user,
99
- {
100
- title: data.title,
101
- description: data.description,
102
- }
103
- );
128
+ // No property link and no room means no location.
129
+ if (values.linkToProperty && !room) {
130
+ log("createJob", "InsufficientInput", data.userID);
131
+ return callback(
132
+ null,
133
+ generateJsonResponse(422, { error: "Insufficient input" }),
134
+ );
135
+ }
104
136
 
105
- logAnalyticsActivity(
106
- `Request`,
107
- values.entityKey,
108
- data.location,
109
- user,
110
- id
111
- );
137
+ return addMaintenanceJob(
138
+ user.id,
139
+ user.displayName,
140
+ user,
141
+ data.phone,
142
+ room,
143
+ data.title,
144
+ data.description,
145
+ data.type,
146
+ data.isHome,
147
+ data.homeText,
148
+ data.images || data.image,
149
+ data.location,
150
+ data.audience,
151
+ data.customFields,
152
+ propertyId,
153
+ propertyName,
154
+ ).then(async (id) => {
155
+ // Awaited (allSettled) — the frozen event loop would drop fire-and-forget
156
+ // work, one failure must not strand the others, and a failed side effect
157
+ // must not fail the create.
158
+ const settled = await Promise.allSettled([
159
+ sendJobEmail({
160
+ userID: user.id,
161
+ userName: user.displayName,
162
+ phone: data.phone,
163
+ room,
164
+ title: data.title,
165
+ description: data.description,
166
+ type: data.type,
167
+ isHome: data.isHome,
168
+ homeText: data.homeText,
169
+ image: data.image,
170
+ images: data.images,
171
+ site: data.location,
172
+ audience: data.audience,
173
+ id,
174
+ customFields: data.customFields,
175
+ }),
176
+ publishActivity(
177
+ values.activityAddMaintenanceJob,
178
+ data.location,
179
+ id,
180
+ user,
181
+ {
182
+ title: data.title,
183
+ description: data.description,
184
+ },
185
+ ),
186
+ logAnalyticsActivity(
187
+ `Request`,
188
+ values.entityKey,
189
+ data.location,
190
+ user,
191
+ id,
192
+ ),
193
+ ]);
194
+ settled
195
+ .filter((result) => result.status === "rejected")
196
+ .forEach((result) => {
197
+ log("createJob", "SideEffectError", result.reason);
198
+ });
112
199
 
113
- return callback(
114
- null,
115
- generateJsonResponse(200, {
116
- success: true,
117
- id,
118
- searchResult: id,
119
- })
120
- );
121
- });
122
- })
123
- .catch((error) => {
124
- console.error(
125
- "Failed to save maintenance node --> email successful",
126
- data.userID
127
- );
128
- console.log(error);
129
- return callback(
130
- null,
131
- generateJsonResponse(422, {
132
- error,
133
- })
134
- );
135
- });
200
+ return callback(
201
+ null,
202
+ generateJsonResponse(200, {
203
+ success: true,
204
+ id,
205
+ searchResult: id,
206
+ }),
207
+ );
208
+ });
209
+ })
210
+ .catch((error) => {
211
+ console.error(
212
+ "Failed to save maintenance node --> email successful",
213
+ data.userID,
214
+ );
215
+ console.log(error);
216
+ return callback(
217
+ null,
218
+ generateJsonResponse(422, {
219
+ error,
220
+ }),
221
+ );
222
+ });
136
223
  };
package/createJobType.js CHANGED
@@ -8,57 +8,57 @@ const updateRef = require("@plusscommunities/pluss-core-aws/db/common/updateRef"
8
8
  const { values } = require("./values.config");
9
9
 
10
10
  module.exports.createJobType = (event, context, callback) => {
11
- init(config);
12
- const data = getBody(event);
11
+ init(config);
12
+ const data = getBody(event);
13
13
 
14
- if (!data.site || !data.name || !data.email || !data.description) {
15
- return callback(
16
- null,
17
- generateJsonResponse(422, { error: "No site, type or email attached" })
18
- );
19
- }
14
+ if (!data.site || !data.name || !data.email || !data.description) {
15
+ return callback(
16
+ null,
17
+ generateJsonResponse(422, { error: "No site, type or email attached" }),
18
+ );
19
+ }
20
20
 
21
- validateMasterAuth(event, values.permissionMaintenanceTypes).then(
22
- (authorised) => {
23
- if (!authorised) {
24
- console.error("Authorization not valid");
25
- return callback(
26
- null,
27
- generateJsonResponse(422, {
28
- error: { message: "Admin - Create User - not authorized." },
29
- })
30
- );
31
- }
21
+ validateMasterAuth(event, values.permissionMaintenanceTypes).then(
22
+ (authorised) => {
23
+ if (!authorised) {
24
+ console.error("Authorization not valid");
25
+ return callback(
26
+ null,
27
+ generateJsonResponse(422, {
28
+ error: { message: "Admin - Create User - not authorized." },
29
+ }),
30
+ );
31
+ }
32
32
 
33
- const jobType = {
34
- id: createGuid().substring(0, 8),
35
- email: data.email,
36
- typeName: data.name,
37
- description: data.description,
38
- level: data.level,
39
- site: data.site,
40
- hasCustomFields: data.hasCustomFields ?? false,
41
- customFields: data.customFields ?? [],
42
- };
33
+ const jobType = {
34
+ id: createGuid().substring(0, 8),
35
+ email: data.email,
36
+ typeName: data.name,
37
+ description: data.description,
38
+ level: data.level,
39
+ site: data.site,
40
+ hasCustomFields: data.hasCustomFields ?? false,
41
+ customFields: data.customFields ?? [],
42
+ };
43
43
 
44
- updateRef(values.tableNameJobTypes, jobType)
45
- .then((result) => {
46
- return callback(
47
- null,
48
- generateJsonResponse(200, {
49
- success: true,
50
- jobType: result,
51
- })
52
- );
53
- })
54
- .catch((error) => {
55
- return callback(
56
- null,
57
- generateJsonResponse(422, {
58
- error,
59
- })
60
- );
61
- });
62
- }
63
- );
44
+ updateRef(values.tableNameJobTypes, jobType)
45
+ .then((result) => {
46
+ return callback(
47
+ null,
48
+ generateJsonResponse(200, {
49
+ success: true,
50
+ jobType: result,
51
+ }),
52
+ );
53
+ })
54
+ .catch((error) => {
55
+ return callback(
56
+ null,
57
+ generateJsonResponse(422, {
58
+ error,
59
+ }),
60
+ );
61
+ });
62
+ },
63
+ );
64
64
  };
@@ -7,100 +7,110 @@ const { getConfig } = require("@plusscommunities/pluss-core-aws/config");
7
7
  const { values } = require("../../values.config");
8
8
 
9
9
  module.exports = async (
10
- userID,
11
- userName,
12
- user,
13
- phone,
14
- room,
15
- title,
16
- description,
17
- type,
18
- isHome,
19
- homeText,
20
- images,
21
- location,
22
- audience,
23
- customFields
10
+ userID,
11
+ userName,
12
+ user,
13
+ phone,
14
+ room,
15
+ title,
16
+ description,
17
+ type,
18
+ isHome,
19
+ homeText,
20
+ images,
21
+ location,
22
+ audience,
23
+ customFields,
24
+ propertyId,
25
+ propertyName,
24
26
  ) => {
25
- const requestToSave = {
26
- userID,
27
- userName,
28
- userProfilePic: user ? user.profilePic : null,
29
- phone,
30
- room,
31
- title,
32
- description,
33
- type,
34
- isHome,
35
- homeText,
36
- images,
37
- site: location,
38
- audience,
39
- customFields,
40
- };
27
+ // Property name overrides room so existing readers of room keep working.
28
+ const resolvedRoom = propertyName || room;
41
29
 
42
- if (requestToSave.images) {
43
- const thumbSource = Array.isArray(requestToSave.images)
44
- ? requestToSave.images[0]
45
- : requestToSave.images;
46
- if (thumbSource) {
47
- requestToSave.thumbnail = thumbSource
48
- .replace("/general1400/", "/general300/")
49
- .replace("/uploads1400/", "/uploads300/");
50
- }
51
- }
52
- if (_.isEmpty(requestToSave.audience)) {
53
- requestToSave.audience = [];
54
- }
55
- requestToSave.status = getConfig().maintenanceInstantComplete
56
- ? "Completed"
57
- : "Unassigned";
58
- requestToSave.history = [
59
- {
60
- timestamp: moment.utc().valueOf(),
61
- status: requestToSave.status,
62
- user,
63
- },
64
- ];
30
+ const requestToSave = {
31
+ userID,
32
+ userName,
33
+ userProfilePic: user ? user.profilePic : null,
34
+ phone,
35
+ room: resolvedRoom,
36
+ title,
37
+ description,
38
+ type,
39
+ isHome,
40
+ homeText,
41
+ images,
42
+ site: location,
43
+ audience,
44
+ customFields,
45
+ };
65
46
 
66
- requestToSave.id = uuid.v1();
67
- requestToSave.createdTime = moment.utc().toISOString();
68
- requestToSave.createdUnix = moment.utc().valueOf();
69
- if (!requestToSave.jobId) {
70
- const jobNoQ = {
71
- IndexName: "MaintenanceSiteJobNoIndex",
72
- KeyConditionExpression: "site = :site",
73
- ExpressionAttributeValues: {
74
- ":site": requestToSave.site,
75
- },
76
- Limit: 1,
77
- ScanIndexForward: false,
78
- };
47
+ // Omit propertyId when absent so the property index stays sparse (linked rows only).
48
+ if (propertyId) {
49
+ requestToSave.propertyId = propertyId;
50
+ }
79
51
 
80
- const queryRes = await indexQuery(values.tableNameMaintenance, jobNoQ);
52
+ if (requestToSave.images) {
53
+ const thumbSource = Array.isArray(requestToSave.images)
54
+ ? requestToSave.images[0]
55
+ : requestToSave.images;
56
+ if (thumbSource) {
57
+ requestToSave.thumbnail = thumbSource
58
+ .replace("/general1400/", "/general300/")
59
+ .replace("/uploads1400/", "/uploads300/");
60
+ }
61
+ }
62
+ if (_.isEmpty(requestToSave.audience)) {
63
+ requestToSave.audience = [];
64
+ }
65
+ requestToSave.status = getConfig().maintenanceInstantComplete
66
+ ? "Completed"
67
+ : "Unassigned";
68
+ requestToSave.history = [
69
+ {
70
+ timestamp: moment.utc().valueOf(),
71
+ status: requestToSave.status,
72
+ user,
73
+ },
74
+ ];
81
75
 
82
- if (!_.isEmpty(queryRes.Items)) {
83
- requestToSave.jobNo = queryRes.Items[0].jobNo + 1;
84
- } else {
85
- const query = {
86
- IndexName: "MaintenanceSiteJobIdIndex",
87
- KeyConditionExpression: "site = :site",
88
- ExpressionAttributeValues: {
89
- ":site": requestToSave.site,
90
- },
91
- Limit: 1,
92
- ScanIndexForward: false,
93
- };
94
- const { Items } = await indexQuery(values.tableNameMaintenance, query);
95
- requestToSave.jobNo =
96
- !_.isEmpty(Items) && Items[0].jobId
97
- ? Number.parseInt(Items[0].jobId) + 1
98
- : 0;
99
- }
100
- requestToSave.jobId = (requestToSave.jobNo || 0) + "";
101
- }
76
+ requestToSave.id = uuid.v1();
77
+ requestToSave.createdTime = moment.utc().toISOString();
78
+ requestToSave.createdUnix = moment.utc().valueOf();
79
+ if (!requestToSave.jobId) {
80
+ const jobNoQ = {
81
+ IndexName: "MaintenanceSiteJobNoIndex",
82
+ KeyConditionExpression: "site = :site",
83
+ ExpressionAttributeValues: {
84
+ ":site": requestToSave.site,
85
+ },
86
+ Limit: 1,
87
+ ScanIndexForward: false,
88
+ };
102
89
 
103
- requestToSave.priority = "Low";
104
- await updateRef(values.tableNameMaintenance, requestToSave);
105
- return requestToSave.id;
90
+ const queryRes = await indexQuery(values.tableNameMaintenance, jobNoQ);
91
+
92
+ if (!_.isEmpty(queryRes.Items)) {
93
+ requestToSave.jobNo = queryRes.Items[0].jobNo + 1;
94
+ } else {
95
+ const query = {
96
+ IndexName: "MaintenanceSiteJobIdIndex",
97
+ KeyConditionExpression: "site = :site",
98
+ ExpressionAttributeValues: {
99
+ ":site": requestToSave.site,
100
+ },
101
+ Limit: 1,
102
+ ScanIndexForward: false,
103
+ };
104
+ const { Items } = await indexQuery(values.tableNameMaintenance, query);
105
+ requestToSave.jobNo =
106
+ !_.isEmpty(Items) && Items[0].jobId
107
+ ? Number.parseInt(Items[0].jobId) + 1
108
+ : 0;
109
+ }
110
+ requestToSave.jobId = (requestToSave.jobNo || 0) + "";
111
+ }
112
+
113
+ requestToSave.priority = "Low";
114
+ await updateRef(values.tableNameMaintenance, requestToSave);
115
+ return requestToSave.id;
106
116
  };
@@ -1,18 +1,28 @@
1
1
  const moment = require("moment");
2
+ const _ = require("lodash");
2
3
  const editRef = require("@plusscommunities/pluss-core-aws/db/common/editRef");
4
+ const getRef = require("@plusscommunities/pluss-core-aws/db/common/getRef");
5
+ const updateRef = require("@plusscommunities/pluss-core-aws/db/common/updateRef");
3
6
  const { values } = require("../../values.config");
4
7
 
5
- module.exports = (job) => {
6
- return new Promise((resolve, reject) => {
7
- job.lastActivity = moment.utc().toISOString();
8
- job.lastActivityUnix = moment.utc().valueOf();
8
+ module.exports = async (job) => {
9
+ job.lastActivity = moment.utc().toISOString();
10
+ job.lastActivityUnix = moment.utc().valueOf();
9
11
 
10
- editRef(values.tableNameMaintenance, "id", job.id, job)
11
- .then((result) => {
12
- resolve(result);
13
- })
14
- .catch((error) => {
15
- reject(error);
16
- });
17
- });
12
+ const clearsProperty =
13
+ !_.isUndefined(job.propertyId) && job.propertyId == null;
14
+ if (!clearsProperty) {
15
+ return editRef(values.tableNameMaintenance, "id", job.id, job);
16
+ }
17
+
18
+ const item = await getRef(values.tableNameMaintenance, "id", job.id);
19
+ if (!item) {
20
+ throw new Error(`Maintenance job not found: ${job.id}`);
21
+ }
22
+ // GSI hash keys can't be null; drop propertyId/propertyName in the same
23
+ // single put as the edit so a partial failure can't leave the pair split.
24
+ const merged = { ...item, ...job, id: job.id };
25
+ delete merged.propertyId;
26
+ delete merged.propertyName;
27
+ return updateRef(values.tableNameMaintenance, merged);
18
28
  };