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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ const moment = require("moment");
2
+ const getRef = require("@plusscommunities/pluss-core-aws/db/common/getRef");
3
+ const editRef = require("@plusscommunities/pluss-core-aws/db/common/editRef");
4
+ const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
5
+ const { log } = require("@plusscommunities/pluss-core-aws/helper");
6
+ const { values } = require("../values.config");
7
+ const { getStrategy } = require("../integration");
8
+
9
+ /**
10
+ * Checks if a job has already been successfully synced to an external system.
11
+ * Uses two methods to determine sync status:
12
+ * 1. Checks job history for an "ExternalIDSet" entry
13
+ * 2. Queries the integration strategy's getExternalId method if available
14
+ *
15
+ * @param {Object} job - The maintenance job record
16
+ * @param {string} job.id - The job's internal ID
17
+ * @param {string} job.site - The site the job belongs to
18
+ * @param {Array} [job.history] - The job's history entries
19
+ * @param {string} logId - The log ID for tracing
20
+ * @returns {Promise<boolean>} True if the job has already been synced
21
+ */
22
+ const isAlreadySynced = async (job, logId) => {
23
+ const history = job.history || [];
24
+ const hasSuccessfulSync = history.some(
25
+ (entry) => entry.EntryType === "ExternalIDSet"
26
+ );
27
+
28
+ if (hasSuccessfulSync) {
29
+ log("isAlreadySynced", "FoundInHistory", true, logId);
30
+ return true;
31
+ }
32
+
33
+ try {
34
+ const integrationStrategy = await getStrategy(job.site);
35
+ if (integrationStrategy.getExternalId) {
36
+ const externalId = await integrationStrategy.getExternalId(job);
37
+ if (externalId) {
38
+ log("isAlreadySynced", "FoundViaStrategy", externalId, logId);
39
+ return true;
40
+ }
41
+ }
42
+ } catch (error) {
43
+ log("isAlreadySynced", "StrategyCheckError", error.message, logId);
44
+ }
45
+
46
+ return false;
47
+ };
48
+
49
+ /**
50
+ * Initiates a retry of the external system sync for a maintenance job.
51
+ * This endpoint sets the ExtCreateRetry field on the job, which triggers
52
+ * the jobChanged stream handler to attempt pushing the request to the
53
+ * external integration system again.
54
+ *
55
+ * @param {Object} event - The Lambda event object
56
+ * @param {Object} event.headers - Request headers containing auth credentials
57
+ * @param {Object} data - The request body
58
+ * @param {string} data.id - The ID of the job to retry sync for
59
+ * @returns {Promise<Object>} Response object with status and data
60
+ * @returns {number} returns.status - HTTP status code
61
+ * @returns {Object} returns.data - Response payload
62
+ *
63
+ * @example
64
+ * // Request
65
+ * POST /maintenance/update/retrysync
66
+ * { "id": "job-uuid-here" }
67
+ *
68
+ * // Success Response (200)
69
+ * {
70
+ * "success": true,
71
+ * "message": "Sync retry initiated. Check back shortly for results.",
72
+ * "job": { ... }
73
+ * }
74
+ *
75
+ * // Error Responses
76
+ * // 400 - Job already synced to external system
77
+ * // 403 - Not authorised
78
+ * // 404 - Job not found
79
+ * // 422 - Missing required field: id
80
+ * // 500 - Internal error
81
+ */
82
+ module.exports = async (event, data) => {
83
+ const action = "retrySync";
84
+ const logId = log(action, "Input", data);
85
+
86
+ try {
87
+ // Validate required fields
88
+ if (!data.id) {
89
+ return {
90
+ status: 422,
91
+ data: { error: "Missing required field: id" },
92
+ };
93
+ }
94
+
95
+ // Fetch the job record
96
+ let job;
97
+ try {
98
+ job = await getRef(values.tableNameMaintenance, "id", data.id);
99
+ } catch (error) {
100
+ log(action, "Error:JobNotFound", { error: error.message }, logId);
101
+ return { status: 404, data: { error: "Job not found" } };
102
+ }
103
+
104
+ if (!job) {
105
+ log(action, "JobNotFound", { id: data.id }, logId);
106
+ return { status: 404, data: { error: "Job not found" } };
107
+ }
108
+
109
+ // Validate user has permission to manage maintenance for this site
110
+ const authorised = await validateMasterAuth(
111
+ event,
112
+ values.permissionMaintenanceTracking,
113
+ job.site
114
+ );
115
+
116
+ if (!authorised) {
117
+ log(action, "NotAuthorised", { site: job.site }, logId);
118
+ return { status: 403, data: { error: "Not authorised" } };
119
+ }
120
+
121
+ // Prevent duplicate syncs - check if already synced via history or external entity
122
+ const alreadySynced = await isAlreadySynced(job, logId);
123
+ if (alreadySynced) {
124
+ log(action, "AlreadySynced", { id: job.id }, logId);
125
+ return {
126
+ status: 400,
127
+ data: { error: "Job already synced to external system" },
128
+ };
129
+ }
130
+
131
+ // Set ExtCreateRetry to a new timestamp value.
132
+ // The jobChanged stream handler detects this field change and
133
+ // triggers pushRequestToIntegration() to retry the external sync.
134
+ const retryTimestamp = moment.utc().valueOf();
135
+ const updatedJob = await editRef(
136
+ values.tableNameMaintenance,
137
+ "id",
138
+ job.id,
139
+ {
140
+ ExtCreateRetry: retryTimestamp,
141
+ }
142
+ );
143
+
144
+ log(action, "RetryTriggered", { ExtCreateRetry: retryTimestamp }, logId);
145
+
146
+ return {
147
+ status: 200,
148
+ data: {
149
+ success: true,
150
+ message: "Sync retry initiated. Check back shortly for results.",
151
+ job: updatedJob,
152
+ },
153
+ };
154
+ } catch (error) {
155
+ log(action, "InternalError", error, logId);
156
+ return {
157
+ status: 500,
158
+ data: { error: "Internal error", message: error.message },
159
+ };
160
+ }
161
+ };
@@ -62,11 +62,13 @@ module.exports = async (event, data) => {
62
62
  const externalEntity = await updateRef("externalentities", {
63
63
  RowId: rowId,
64
64
  EntityType: entityType,
65
+ ActiveEntityType: entityType,
65
66
  InternalId: job.id,
66
67
  ExternalId: data.externalId,
67
68
  LastUpdated: moment().valueOf(),
68
69
  TrackedData: data.trackedData || {},
69
70
  Site: job.site, // Store for site-specific queries
71
+ SystemType: data.systemType,
70
72
  });
71
73
 
72
74
  log(action, "ExternalEntityCreated", externalEntity, logId);
@@ -75,9 +77,10 @@ module.exports = async (event, data) => {
75
77
  if (!job.history) job.history = [];
76
78
  job.history.push({
77
79
  timestamp: moment.utc().valueOf(),
78
- action: "ExternalIDSet",
80
+ EntryType: "ExternalIDSet",
79
81
  externalId: data.externalId,
80
82
  user,
83
+ systemType: data.systemType,
81
84
  });
82
85
 
83
86
  // 8. Update job with history and optionally update job number for system parity
@@ -29,11 +29,11 @@ const processBatch = async (strategy, startTime) => {
29
29
 
30
30
  // use index query to fetch IDs of requests
31
31
  const query = {
32
- IndexName: "EntityTypeIndex",
32
+ IndexName: "ActiveEntityTypeIndex",
33
33
  KeyConditionExpression:
34
- "EntityType = :entityType AND LastUpdated < :lastUpdate", // only fetch items that haven't been updated in this scan
34
+ "ActiveEntityType = :activeEntityType AND LastUpdated < :lastUpdate", // only fetch items that haven't been updated in this scan
35
35
  ExpressionAttributeValues: {
36
- ":entityType": strategy.getEntityType(),
36
+ ":activeEntityType": strategy.getEntityType(),
37
37
  ":lastUpdate": startTime - strategy.getRefreshInterval(),
38
38
  },
39
39
  Limit: 10,
@@ -77,5 +77,11 @@ module.exports.scheduleJobImport = async (event, context, callback) => {
77
77
  }
78
78
  log("scheduleJobImport", "EndLoop", moment().valueOf(), logId);
79
79
  }
80
+ // TODO: Remove this call once all existing records have ActiveEntityType (PC-1382)
81
+ const remainingTime = (startTime + timeout) - moment().valueOf();
82
+ if (remainingTime > 0) {
83
+ await integrationStrategy.backfillActiveEntityType(remainingTime);
84
+ }
85
+
80
86
  log("scheduleJobImport", "End", moment().valueOf(), logId);
81
87
  };
package/updateData.js CHANGED
@@ -5,6 +5,7 @@ const config = require("./config.json");
5
5
  const assignRequest = require("./requests/assignRequest");
6
6
  const updatePriority = require("./requests/updatePriority");
7
7
  const setExternalJobId = require("./requests/setExternalJobId");
8
+ const retrySync = require("./requests/retrySync");
8
9
 
9
10
  module.exports.updateData = async (event, context, callback) => {
10
11
  init(config);
@@ -25,6 +26,9 @@ module.exports.updateData = async (event, context, callback) => {
25
26
  case "externalid":
26
27
  response = await setExternalJobId(event, data);
27
28
  break;
29
+ case "retrysync":
30
+ response = await retrySync(event, data);
31
+ break;
28
32
  default:
29
33
  response = { status: 404, data: { error: "Action not found" } };
30
34
  break;
@@ -23,5 +23,6 @@ const values = {
23
23
  activityEditMaintenanceNote: "EditMaintenanceNoteA",
24
24
  textJobEmailTitle: "Service Request",
25
25
  triggerMaintenanceStatusChanged: "MaintenanceStatusChangedA",
26
+ enableSiteConfigsPropagation: false,
26
27
  };
27
28
  exports.values = values;
@@ -26,5 +26,6 @@ const values = {
26
26
  allowGeneralType: true,
27
27
  defaultJobTypes: [],
28
28
  triggerMaintenanceStatusChanged: "MaintenanceStatusChanged",
29
+ enableSiteConfigsPropagation: true,
29
30
  };
30
31
  exports.values = values;
@@ -219,5 +219,6 @@ const values = {
219
219
  },
220
220
  ],
221
221
  triggerMaintenanceStatusChanged: "MaintenanceStatusChangedEnquiry",
222
+ enableSiteConfigsPropagation: false,
222
223
  };
223
224
  exports.values = values;
@@ -191,5 +191,6 @@ const values = {
191
191
  },
192
192
  ],
193
193
  triggerMaintenanceStatusChanged: "MaintenanceStatusChangedFeedback",
194
+ enableSiteConfigsPropagation: false,
194
195
  };
195
196
  exports.values = values;
@@ -26,5 +26,6 @@ const values = {
26
26
  allowGeneralType: false,
27
27
  defaultJobTypes: [],
28
28
  triggerMaintenanceStatusChanged: "MaintenanceStatusChangedFood",
29
+ enableSiteConfigsPropagation: false,
29
30
  };
30
31
  exports.values = values;
@@ -26,5 +26,6 @@ const values = {
26
26
  allowGeneralType: false,
27
27
  defaultJobTypes: [],
28
28
  triggerMaintenanceStatusChanged: "MaintenanceStatusChangedForms",
29
+ enableSiteConfigsPropagation: true,
29
30
  };
30
31
  exports.values = values;
package/values.config.js CHANGED
@@ -26,5 +26,6 @@ const values = {
26
26
  allowGeneralType: false,
27
27
  defaultJobTypes: [],
28
28
  triggerMaintenanceStatusChanged: "MaintenanceStatusChangedForms",
29
+ enableSiteConfigsPropagation: true,
29
30
  };
30
31
  exports.values = values;