@machinemetrics/mm-erp-sdk 0.1.1-beta.1

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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/dist/config-2l5vnNkA.js +418 -0
  4. package/dist/config-2l5vnNkA.js.map +1 -0
  5. package/dist/connector-factory-CQ8e7Tae.js +21 -0
  6. package/dist/connector-factory-CQ8e7Tae.js.map +1 -0
  7. package/dist/hashed-cache-manager-Ci9X3GAB.js +292 -0
  8. package/dist/hashed-cache-manager-Ci9X3GAB.js.map +1 -0
  9. package/dist/index-Cn9ccxOO.js +179 -0
  10. package/dist/index-Cn9ccxOO.js.map +1 -0
  11. package/dist/index.d.ts +35 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/knexfile-1qKKIORB.js +20 -0
  14. package/dist/knexfile-1qKKIORB.js.map +1 -0
  15. package/dist/knexfile.d.ts +6 -0
  16. package/dist/knexfile.d.ts.map +1 -0
  17. package/dist/logger-QG73MndU.js +17523 -0
  18. package/dist/logger-QG73MndU.js.map +1 -0
  19. package/dist/migrations/20241015162631_create_cache_table.js +17 -0
  20. package/dist/migrations/20241015162631_create_cache_table.js.map +1 -0
  21. package/dist/migrations/20241015162632_create_sdk_cache_table.js +17 -0
  22. package/dist/migrations/20241015162632_create_sdk_cache_table.js.map +1 -0
  23. package/dist/migrations/20250103162631_create_record_tracking_table.js +17 -0
  24. package/dist/migrations/20250103162631_create_record_tracking_table.js.map +1 -0
  25. package/dist/mm-erp-sdk.js +3503 -0
  26. package/dist/mm-erp-sdk.js.map +1 -0
  27. package/dist/services/caching-service/batch-cache-manager.d.ts +52 -0
  28. package/dist/services/caching-service/batch-cache-manager.d.ts.map +1 -0
  29. package/dist/services/caching-service/hashed-cache-manager.d.ts +83 -0
  30. package/dist/services/caching-service/hashed-cache-manager.d.ts.map +1 -0
  31. package/dist/services/caching-service/index.d.ts +92 -0
  32. package/dist/services/caching-service/index.d.ts.map +1 -0
  33. package/dist/services/caching-service/record-tracking-manager.d.ts +15 -0
  34. package/dist/services/caching-service/record-tracking-manager.d.ts.map +1 -0
  35. package/dist/services/data-sync-service/configuration-manager.d.ts +50 -0
  36. package/dist/services/data-sync-service/configuration-manager.d.ts.map +1 -0
  37. package/dist/services/data-sync-service/data-sync-service.d.ts +2 -0
  38. package/dist/services/data-sync-service/data-sync-service.d.ts.map +1 -0
  39. package/dist/services/data-sync-service/index.d.ts +10 -0
  40. package/dist/services/data-sync-service/index.d.ts.map +1 -0
  41. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts +2 -0
  42. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts.map +1 -0
  43. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +41 -0
  44. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js.map +1 -0
  45. package/dist/services/data-sync-service/jobs/from-erp.d.ts +4 -0
  46. package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -0
  47. package/dist/services/data-sync-service/jobs/from-erp.js +42 -0
  48. package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -0
  49. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts +2 -0
  50. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts.map +1 -0
  51. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +41 -0
  52. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js.map +1 -0
  53. package/dist/services/data-sync-service/jobs/run-migrations.d.ts +2 -0
  54. package/dist/services/data-sync-service/jobs/run-migrations.d.ts.map +1 -0
  55. package/dist/services/data-sync-service/jobs/run-migrations.js +21 -0
  56. package/dist/services/data-sync-service/jobs/run-migrations.js.map +1 -0
  57. package/dist/services/data-sync-service/jobs/to-erp.d.ts +4 -0
  58. package/dist/services/data-sync-service/jobs/to-erp.d.ts.map +1 -0
  59. package/dist/services/data-sync-service/jobs/to-erp.js +39 -0
  60. package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -0
  61. package/dist/services/erp-api-services/errors.d.ts +22 -0
  62. package/dist/services/erp-api-services/errors.d.ts.map +1 -0
  63. package/dist/services/erp-api-services/graphql/graphql-service.d.ts +40 -0
  64. package/dist/services/erp-api-services/graphql/graphql-service.d.ts.map +1 -0
  65. package/dist/services/erp-api-services/graphql/types.d.ts +32 -0
  66. package/dist/services/erp-api-services/graphql/types.d.ts.map +1 -0
  67. package/dist/services/erp-api-services/index.d.ts +9 -0
  68. package/dist/services/erp-api-services/index.d.ts.map +1 -0
  69. package/dist/services/erp-api-services/oauth-client.d.ts +45 -0
  70. package/dist/services/erp-api-services/oauth-client.d.ts.map +1 -0
  71. package/dist/services/erp-api-services/rest/get-query-params.d.ts +50 -0
  72. package/dist/services/erp-api-services/rest/get-query-params.d.ts.map +1 -0
  73. package/dist/services/erp-api-services/rest/rest-api-service.d.ts +35 -0
  74. package/dist/services/erp-api-services/rest/rest-api-service.d.ts.map +1 -0
  75. package/dist/services/erp-api-services/types.d.ts +27 -0
  76. package/dist/services/erp-api-services/types.d.ts.map +1 -0
  77. package/dist/services/mm-api-service/index.d.ts +31 -0
  78. package/dist/services/mm-api-service/index.d.ts.map +1 -0
  79. package/dist/services/mm-api-service/mm-api-service.d.ts +214 -0
  80. package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -0
  81. package/dist/services/mm-api-service/token-mgr.d.ts +33 -0
  82. package/dist/services/mm-api-service/token-mgr.d.ts.map +1 -0
  83. package/dist/services/mm-api-service/types/checkpoint.d.ts +13 -0
  84. package/dist/services/mm-api-service/types/checkpoint.d.ts.map +1 -0
  85. package/dist/services/mm-api-service/types/entity-transformer.d.ts +58 -0
  86. package/dist/services/mm-api-service/types/entity-transformer.d.ts.map +1 -0
  87. package/dist/services/mm-api-service/types/mm-response-interfaces.d.ts +263 -0
  88. package/dist/services/mm-api-service/types/mm-response-interfaces.d.ts.map +1 -0
  89. package/dist/services/mm-api-service/types/receive-types.d.ts +57 -0
  90. package/dist/services/mm-api-service/types/receive-types.d.ts.map +1 -0
  91. package/dist/services/mm-api-service/types/send-types.d.ts +147 -0
  92. package/dist/services/mm-api-service/types/send-types.d.ts.map +1 -0
  93. package/dist/services/reporting-service/index.d.ts +5 -0
  94. package/dist/services/reporting-service/index.d.ts.map +1 -0
  95. package/dist/services/reporting-service/logger.d.ts +4 -0
  96. package/dist/services/reporting-service/logger.d.ts.map +1 -0
  97. package/dist/services/sql-server-erp-service/configuration.d.ts +15 -0
  98. package/dist/services/sql-server-erp-service/configuration.d.ts.map +1 -0
  99. package/dist/services/sql-server-erp-service/index.d.ts +16 -0
  100. package/dist/services/sql-server-erp-service/index.d.ts.map +1 -0
  101. package/dist/services/sql-server-erp-service/internal/sql-labor-ticket-operations.d.ts +31 -0
  102. package/dist/services/sql-server-erp-service/internal/sql-labor-ticket-operations.d.ts.map +1 -0
  103. package/dist/services/sql-server-erp-service/internal/sql-server-config.d.ts +65 -0
  104. package/dist/services/sql-server-erp-service/internal/sql-server-config.d.ts.map +1 -0
  105. package/dist/services/sql-server-erp-service/internal/sql-transaction-manager.d.ts +20 -0
  106. package/dist/services/sql-server-erp-service/internal/sql-transaction-manager.d.ts.map +1 -0
  107. package/dist/services/sql-server-erp-service/internal/types/sql-server-types.d.ts +3 -0
  108. package/dist/services/sql-server-erp-service/internal/types/sql-server-types.d.ts.map +1 -0
  109. package/dist/services/sql-server-erp-service/sql-server-helpers.d.ts +35 -0
  110. package/dist/services/sql-server-erp-service/sql-server-helpers.d.ts.map +1 -0
  111. package/dist/services/sql-server-erp-service/sql-server-service.d.ts +37 -0
  112. package/dist/services/sql-server-erp-service/sql-server-service.d.ts.map +1 -0
  113. package/dist/services/sql-server-erp-service/types/sql-input-param.d.ts +10 -0
  114. package/dist/services/sql-server-erp-service/types/sql-input-param.d.ts.map +1 -0
  115. package/dist/services/sqlite-service/index.d.ts +2 -0
  116. package/dist/services/sqlite-service/index.d.ts.map +1 -0
  117. package/dist/services/sqlite-service/sqlite-coordinator.d.ts +28 -0
  118. package/dist/services/sqlite-service/sqlite-coordinator.d.ts.map +1 -0
  119. package/dist/types/erp-connector.d.ts +40 -0
  120. package/dist/types/erp-connector.d.ts.map +1 -0
  121. package/dist/types/erp-types.d.ts +32 -0
  122. package/dist/types/erp-types.d.ts.map +1 -0
  123. package/dist/types/index.d.ts +7 -0
  124. package/dist/types/index.d.ts.map +1 -0
  125. package/dist/utils/application-initializer.d.ts +15 -0
  126. package/dist/utils/application-initializer.d.ts.map +1 -0
  127. package/dist/utils/cleanup-numbers.d.ts +2 -0
  128. package/dist/utils/cleanup-numbers.d.ts.map +1 -0
  129. package/dist/utils/connector-factory.d.ts +8 -0
  130. package/dist/utils/connector-factory.d.ts.map +1 -0
  131. package/dist/utils/data-transformation.d.ts +20 -0
  132. package/dist/utils/data-transformation.d.ts.map +1 -0
  133. package/dist/utils/erp-type-from-entity.d.ts +5 -0
  134. package/dist/utils/erp-type-from-entity.d.ts.map +1 -0
  135. package/dist/utils/http-client.d.ts +35 -0
  136. package/dist/utils/http-client.d.ts.map +1 -0
  137. package/dist/utils/index.d.ts +57 -0
  138. package/dist/utils/index.d.ts.map +1 -0
  139. package/dist/utils/local-data-store/database-lock.d.ts +29 -0
  140. package/dist/utils/local-data-store/database-lock.d.ts.map +1 -0
  141. package/dist/utils/local-data-store/jobs-shared-data.d.ts +34 -0
  142. package/dist/utils/local-data-store/jobs-shared-data.d.ts.map +1 -0
  143. package/dist/utils/mm-labor-ticket-helpers.d.ts +9 -0
  144. package/dist/utils/mm-labor-ticket-helpers.d.ts.map +1 -0
  145. package/dist/utils/removeExtraneousFields.d.ts +6 -0
  146. package/dist/utils/removeExtraneousFields.d.ts.map +1 -0
  147. package/dist/utils/removeIdFieldFromPayload.d.ts +6 -0
  148. package/dist/utils/removeIdFieldFromPayload.d.ts.map +1 -0
  149. package/dist/utils/resource-group.d.ts +11 -0
  150. package/dist/utils/resource-group.d.ts.map +1 -0
  151. package/dist/utils/standard-process-drivers/error-processor.d.ts +68 -0
  152. package/dist/utils/standard-process-drivers/error-processor.d.ts.map +1 -0
  153. package/dist/utils/standard-process-drivers/index.d.ts +3 -0
  154. package/dist/utils/standard-process-drivers/index.d.ts.map +1 -0
  155. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.d.ts +18 -0
  156. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.d.ts.map +1 -0
  157. package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts +40 -0
  158. package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts.map +1 -0
  159. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts +178 -0
  160. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts.map +1 -0
  161. package/dist/utils/time-utils.d.ts +31 -0
  162. package/dist/utils/time-utils.d.ts.map +1 -0
  163. package/dist/utils/timezone.d.ts +21 -0
  164. package/dist/utils/timezone.d.ts.map +1 -0
  165. package/dist/utils/trimObjectValues.d.ts +5 -0
  166. package/dist/utils/trimObjectValues.d.ts.map +1 -0
  167. package/dist/utils/uniqueRows.d.ts +9 -0
  168. package/dist/utils/uniqueRows.d.ts.map +1 -0
  169. package/package.json +50 -0
  170. package/src/index.ts +98 -0
  171. package/src/knexfile.ts +21 -0
  172. package/src/migrations/20241015162631_create_cache_table.ts +15 -0
  173. package/src/migrations/20241015162632_create_sdk_cache_table.ts +15 -0
  174. package/src/migrations/20250103162631_create_record_tracking_table.ts +18 -0
  175. package/src/services/caching-service/batch-cache-manager.ts +111 -0
  176. package/src/services/caching-service/hashed-cache-manager.ts +253 -0
  177. package/src/services/caching-service/index.ts +114 -0
  178. package/src/services/caching-service/record-tracking-manager.ts +41 -0
  179. package/src/services/data-sync-service/configuration-manager.ts +153 -0
  180. package/src/services/data-sync-service/data-sync-service.ts +100 -0
  181. package/src/services/data-sync-service/index.ts +14 -0
  182. package/src/services/data-sync-service/jobs/clean-up-expired-cache.ts +38 -0
  183. package/src/services/data-sync-service/jobs/from-erp.ts +55 -0
  184. package/src/services/data-sync-service/jobs/retry-failed-labor-tickets.ts +44 -0
  185. package/src/services/data-sync-service/jobs/run-migrations.ts +21 -0
  186. package/src/services/data-sync-service/jobs/to-erp.ts +53 -0
  187. package/src/services/erp-api-services/errors.ts +115 -0
  188. package/src/services/erp-api-services/graphql/graphql-service.ts +116 -0
  189. package/src/services/erp-api-services/graphql/types.ts +30 -0
  190. package/src/services/erp-api-services/index.ts +14 -0
  191. package/src/services/erp-api-services/oauth-client.ts +72 -0
  192. package/src/services/erp-api-services/rest/get-query-params.ts +63 -0
  193. package/src/services/erp-api-services/rest/rest-api-service.ts +212 -0
  194. package/src/services/erp-api-services/types.ts +28 -0
  195. package/src/services/mm-api-service/index.ts +83 -0
  196. package/src/services/mm-api-service/mm-api-service.ts +685 -0
  197. package/src/services/mm-api-service/token-mgr.ts +123 -0
  198. package/src/services/mm-api-service/types/checkpoint.ts +13 -0
  199. package/src/services/mm-api-service/types/entity-transformer.ts +298 -0
  200. package/src/services/mm-api-service/types/mm-response-interfaces.ts +293 -0
  201. package/src/services/mm-api-service/types/receive-types.ts +89 -0
  202. package/src/services/mm-api-service/types/send-types.ts +383 -0
  203. package/src/services/reporting-service/index.ts +4 -0
  204. package/src/services/reporting-service/logger.ts +117 -0
  205. package/src/services/sql-server-erp-service/configuration.ts +14 -0
  206. package/src/services/sql-server-erp-service/index.ts +18 -0
  207. package/src/services/sql-server-erp-service/internal/sql-labor-ticket-operations.ts +66 -0
  208. package/src/services/sql-server-erp-service/internal/sql-server-config.ts +38 -0
  209. package/src/services/sql-server-erp-service/internal/sql-transaction-manager.ts +45 -0
  210. package/src/services/sql-server-erp-service/internal/types/sql-server-types.ts +23 -0
  211. package/src/services/sql-server-erp-service/sql-server-helpers.ts +99 -0
  212. package/src/services/sql-server-erp-service/sql-server-service.ts +191 -0
  213. package/src/services/sql-server-erp-service/types/sql-input-param.ts +14 -0
  214. package/src/services/sqlite-service/index.ts +1 -0
  215. package/src/services/sqlite-service/sqlite-coordinator.ts +80 -0
  216. package/src/types/erp-connector.ts +46 -0
  217. package/src/types/erp-types.ts +37 -0
  218. package/src/types/index.ts +13 -0
  219. package/src/utils/application-initializer.ts +62 -0
  220. package/src/utils/cleanup-numbers.ts +5 -0
  221. package/src/utils/connector-factory.ts +34 -0
  222. package/src/utils/data-transformation.ts +58 -0
  223. package/src/utils/erp-type-from-entity.ts +12 -0
  224. package/src/utils/http-client.ts +137 -0
  225. package/src/utils/index.ts +71 -0
  226. package/src/utils/local-data-store/database-lock.ts +86 -0
  227. package/src/utils/local-data-store/jobs-shared-data.ts +111 -0
  228. package/src/utils/mm-labor-ticket-helpers.ts +28 -0
  229. package/src/utils/removeExtraneousFields.ts +22 -0
  230. package/src/utils/removeIdFieldFromPayload.ts +22 -0
  231. package/src/utils/resource-group.ts +92 -0
  232. package/src/utils/standard-process-drivers/error-processor.ts +417 -0
  233. package/src/utils/standard-process-drivers/index.ts +6 -0
  234. package/src/utils/standard-process-drivers/labor-ticket-erp-synchronizer.ts +261 -0
  235. package/src/utils/standard-process-drivers/mm-entity-processor.ts +265 -0
  236. package/src/utils/standard-process-drivers/standard-process-drivers.ts +459 -0
  237. package/src/utils/time-utils.ts +131 -0
  238. package/src/utils/timezone.ts +96 -0
  239. package/src/utils/trimObjectValues.ts +12 -0
  240. package/src/utils/uniqueRows.ts +40 -0
@@ -0,0 +1,417 @@
1
+ import { ERPObjType } from "../../types/erp-types";
2
+ import { BatchCacheManager } from "../../services/caching-service/batch-cache-manager";
3
+ import {
4
+ IToRESTApiObject,
5
+ MM207NonLaborTicketResponse,
6
+ MM207LaborTicketResponse,
7
+ } from "../../services/mm-api-service";
8
+ import { EntityTransformer } from "../../services/mm-api-service/types/entity-transformer";
9
+ import logger from "../../services/reporting-service/logger";
10
+
11
+ /**
12
+ * Handles error processing and record management utilities for MM API operations
13
+ */
14
+ export class ErrorProcessor {
15
+ /**
16
+ * Creates a set of primary keys for all failed records from batch errors
17
+ * @param entityType The type of entity being processed
18
+ * @param batchErrors Array of batch errors containing failed entities
19
+ * @returns Set of primary keys for failed records
20
+ */
21
+ static createFailedRecordKeySet(
22
+ entityType: ERPObjType,
23
+ batchErrors: Array<{
24
+ message: string;
25
+ errorEntities: IToRESTApiObject[];
26
+ }>
27
+ ): Set<string> {
28
+ const failedKeySet = new Set<string>();
29
+
30
+ batchErrors.forEach((batchError) => {
31
+ batchError.errorEntities.forEach((errorEntity) => {
32
+ try {
33
+ const primaryKey = EntityTransformer.extractPrimaryKey(
34
+ entityType,
35
+ errorEntity
36
+ );
37
+ failedKeySet.add(primaryKey);
38
+ } catch (error) {
39
+ logger.warn(
40
+ `Failed to extract primary key from error entity: ${error}`
41
+ );
42
+ // Continue processing other records even if one fails
43
+ }
44
+ });
45
+ });
46
+
47
+ return failedKeySet;
48
+ }
49
+
50
+ /**
51
+ * Filters out failed records, returning only successful ones
52
+ * @param entityType The type of entity being processed
53
+ * @param allRecords All records (typed objects) that were sent to the API
54
+ * @param failedKeySet Set of primary keys for records that failed
55
+ * @returns Array of records that succeeded
56
+ */
57
+ static filterSuccessfulRecords(
58
+ entityType: ERPObjType,
59
+ allRecords: IToRESTApiObject[],
60
+ failedKeySet: Set<string>
61
+ ): IToRESTApiObject[] {
62
+ const successfulRecords: IToRESTApiObject[] = [];
63
+
64
+ allRecords.forEach((record) => {
65
+ try {
66
+ const primaryKey = EntityTransformer.extractPrimaryKey(
67
+ entityType,
68
+ record
69
+ );
70
+ if (!failedKeySet.has(primaryKey)) {
71
+ successfulRecords.push(record);
72
+ }
73
+ } catch (error) {
74
+ logger.warn(
75
+ `Failed to extract primary key from record during filtering: ${error}`
76
+ );
77
+ // If we can't extract the key, we can't determine if it failed, so skip it
78
+ }
79
+ });
80
+
81
+ return successfulRecords;
82
+ }
83
+
84
+ /**
85
+ * Orchestrates the caching of successful records on partial failure
86
+ * @param entityType The type of entity being processed
87
+ * @param toProcess All records that were sent to the API (all are now guaranteed to be typed objects)
88
+ * @param batchErrors Array of batch errors containing failed entities
89
+ * @param batchCacheManager The cache manager instance
90
+ */
91
+ static async cacheSuccessfulRecordsOnPartialFailure(
92
+ entityType: ERPObjType,
93
+ toProcess: IToRESTApiObject[],
94
+ batchErrors: Array<{
95
+ message: string;
96
+ errorEntities: IToRESTApiObject[];
97
+ }>,
98
+ batchCacheManager: BatchCacheManager
99
+ ): Promise<void> {
100
+ // Create a set of failed record keys for efficient lookup
101
+ const failedKeySet = this.createFailedRecordKeySet(entityType, batchErrors);
102
+
103
+ // Filter to get only successful records
104
+ const successfulRecords = this.filterSuccessfulRecords(
105
+ entityType,
106
+ toProcess,
107
+ failedKeySet
108
+ );
109
+
110
+ logger.info(
111
+ `Caching ${successfulRecords.length} successful records out of ${toProcess.length} total records`
112
+ );
113
+
114
+ // All records are now guaranteed to be typed objects, so we can cache them directly
115
+ if (successfulRecords.length > 0) {
116
+ await batchCacheManager.storeBatch(entityType, successfulRecords);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Extracts error count and batch errors from MM API response for partial failures (HTTP 207)
122
+ * This supports all entities, including the slightly different format for labor tickets.
123
+ * In case of labor tickets, the updateErrors and insertErrors arrays are combined into errorEntities.
124
+ * @param mmApiResponse The full MM API response object
125
+ * @param entityType The type of entity being processed (determines response structure)
126
+ * @returns Object containing errorCount and batchErrors
127
+ * See MM207NonLaborTicketResponse and MM207LaborTicketResponse for response structure details
128
+ */
129
+ static extractErrorDetails(
130
+ mmApiResponse: MM207NonLaborTicketResponse | MM207LaborTicketResponse,
131
+ entityType: ERPObjType
132
+ ): {
133
+ errorCount: number;
134
+ batchErrors: Array<{
135
+ message: string;
136
+ errorEntities: IToRESTApiObject[];
137
+ }>;
138
+ } {
139
+ // Type the data property with the expected structure for HTTP 207 responses
140
+ const data = mmApiResponse.data as
141
+ | {
142
+ errors?: Array<{
143
+ message: string;
144
+ batchData?: (IToRESTApiObject | Record<string, string | null>)[];
145
+ }>;
146
+ updateErrors?: Array<{
147
+ message: string;
148
+ batchData?: (IToRESTApiObject | Record<string, string | null>)[];
149
+ }>;
150
+ insertErrors?: Array<{
151
+ message: string;
152
+ batchData?: (IToRESTApiObject | Record<string, string | null>)[];
153
+ }>;
154
+ }
155
+ | undefined;
156
+
157
+ let allErrors: Array<{
158
+ message: string;
159
+ batchData?: (IToRESTApiObject | Record<string, string | null>)[];
160
+ }> = [];
161
+
162
+ if (entityType === ERPObjType.LABOR_TICKETS) {
163
+ // Labor tickets: combine updateErrors and insertErrors
164
+ const updateErrors = data?.updateErrors || [];
165
+ const insertErrors = data?.insertErrors || [];
166
+
167
+ // Defensive validation with actionable warnings
168
+ if (!data?.updateErrors && !data?.insertErrors) {
169
+ logger.warn(
170
+ "Labor tickets partial success response missing both updateErrors and insertErrors arrays"
171
+ );
172
+ }
173
+
174
+ allErrors = [...updateErrors, ...insertErrors];
175
+ } else {
176
+ // Regular entities: use errors array
177
+ const errors = data?.errors || [];
178
+
179
+ // Defensive validation
180
+ if (!data?.errors) {
181
+ logger.warn(
182
+ `${entityType} partial success response missing errors array`
183
+ );
184
+ }
185
+
186
+ allErrors = errors;
187
+ }
188
+
189
+ const batchErrors = allErrors
190
+ .filter(
191
+ (error: {
192
+ message: string;
193
+ batchData?: (IToRESTApiObject | Record<string, string | null>)[];
194
+ }) => {
195
+ return error.batchData && error.batchData.length > 0;
196
+ }
197
+ )
198
+ .map(
199
+ (error: {
200
+ message: string;
201
+ batchData?: (IToRESTApiObject | Record<string, string | null>)[];
202
+ }) => {
203
+ // Reconstruct typed objects from plain data
204
+ const typedErrorEntities = (error.batchData || []).map((entity) => {
205
+ if (
206
+ typeof entity === "object" &&
207
+ entity !== null &&
208
+ "toRESTApiObject" in entity &&
209
+ typeof entity.toRESTApiObject === "function"
210
+ ) {
211
+ // It's already a typed object, return as-is
212
+ return entity as IToRESTApiObject;
213
+ } else {
214
+ // It's plain data, reconstruct as typed object
215
+ return EntityTransformer.reconstructFromPlainData(
216
+ entityType,
217
+ entity as Record<string, string | null>
218
+ );
219
+ }
220
+ });
221
+
222
+ return {
223
+ message: error.message,
224
+ errorEntities: typedErrorEntities,
225
+ };
226
+ }
227
+ );
228
+
229
+ const errorCount = batchErrors.reduce((total: number, batchError) => {
230
+ return total + batchError.errorEntities.length;
231
+ }, 0);
232
+
233
+ return {
234
+ errorCount,
235
+ batchErrors,
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Extracts error details from a 500 HTTP exception when it contains structured error data
241
+ * @param exception The caught exception from MM API call
242
+ * @param entityType The type of entity being processed
243
+ * @returns Object containing errorCount and batchErrors, or null if not a structured 500 error
244
+ * See MM500NonLaborTicketException and MM500LaborTicketException for exception structure details
245
+ */
246
+ static extractErrorDetailsFrom500Exception(
247
+ exception: unknown,
248
+ entityType: ERPObjType
249
+ ): {
250
+ errorCount: number;
251
+ batchErrors: Array<{
252
+ message: string;
253
+ errorEntities: IToRESTApiObject[];
254
+ }>;
255
+ } | null {
256
+ try {
257
+ // Cast exception to expected structure for easier access
258
+ const ex = exception as Record<string, unknown>;
259
+ const data = ex?.data as Record<string, unknown>;
260
+
261
+ // Add diagnostic logging to understand the 500 error structure
262
+ logger.info(
263
+ "writeEntitiesToMM: Analyzing 500 exception structure for diagnostic purposes",
264
+ {
265
+ status: ex?.status,
266
+ code: ex?.code,
267
+ hasResponseData: !!data,
268
+ responseDataKeys: data ? Object.keys(data) : [],
269
+ errorMessage: data?.error,
270
+ hasMessageObject: !!data?.message,
271
+ messageObjectKeys: data?.message
272
+ ? Object.keys(data.message as Record<string, unknown>)
273
+ : [],
274
+ entityType,
275
+ exceptionType: typeof exception,
276
+ exceptionKeys: ex ? Object.keys(ex) : [],
277
+ }
278
+ );
279
+
280
+ // Check if this is a structured 500 error
281
+ if (
282
+ ex?.status !== 500 ||
283
+ typeof data?.error !== "string" ||
284
+ !data.error.startsWith("Failed to import")
285
+ ) {
286
+ logger.info(
287
+ "writeEntitiesToMM: Not a structured 500 error - will re-throw exception as-is",
288
+ {
289
+ status: ex?.status,
290
+ errorMessage: data?.error,
291
+ expectedStatus: 500,
292
+ expectedMessagePrefix: "Failed to import",
293
+ }
294
+ );
295
+ return null;
296
+ }
297
+
298
+ logger.info(
299
+ "writeEntitiesToMM: Detected structured 500 error - extracting error details"
300
+ );
301
+
302
+ const messageObject = data?.message as Record<string, unknown>;
303
+ if (!messageObject) {
304
+ logger.warn(
305
+ "writeEntitiesToMM: Structured 500 error missing message object"
306
+ );
307
+ return null;
308
+ }
309
+
310
+ let allErrors: unknown[] = [];
311
+
312
+ if (entityType === ERPObjType.LABOR_TICKETS) {
313
+ // Labor tickets: combine updateErrors and insertErrors
314
+ const updateErrors = Array.isArray(messageObject?.updateErrors)
315
+ ? messageObject.updateErrors
316
+ : [];
317
+ const insertErrors = Array.isArray(messageObject?.insertErrors)
318
+ ? messageObject.insertErrors
319
+ : [];
320
+
321
+ logger.info("writeEntitiesToMM: Processing labor tickets 500 error", {
322
+ updateErrorsCount: updateErrors.length,
323
+ insertErrorsCount: insertErrors.length,
324
+ });
325
+
326
+ if (updateErrors.length === 0 && insertErrors.length === 0) {
327
+ logger.warn(
328
+ "writeEntitiesToMM: Labor tickets 500 error missing both updateErrors and insertErrors arrays"
329
+ );
330
+ }
331
+
332
+ allErrors = [...updateErrors, ...insertErrors];
333
+ } else {
334
+ // Regular entities: use errors array
335
+ const errors = Array.isArray(messageObject?.errors)
336
+ ? messageObject.errors
337
+ : [];
338
+
339
+ logger.info("writeEntitiesToMM: Processing regular entity 500 error", {
340
+ errorsCount: errors.length,
341
+ });
342
+
343
+ if (errors.length === 0) {
344
+ logger.warn(
345
+ `writeEntitiesToMM: ${entityType} 500 error missing errors array`
346
+ );
347
+ }
348
+
349
+ allErrors = errors;
350
+ }
351
+
352
+ const batchErrors = allErrors
353
+ .filter((error) => {
354
+ const err = error as Record<string, unknown>;
355
+ return Array.isArray(err?.batchData) && err.batchData.length > 0;
356
+ })
357
+ .map((error) => {
358
+ const err = error as Record<string, unknown>;
359
+ const batchData = err?.batchData as (
360
+ | IToRESTApiObject
361
+ | Record<string, string | null>
362
+ )[];
363
+
364
+ // Reconstruct typed objects from plain data
365
+ const typedErrorEntities = (batchData || []).map((entity) => {
366
+ if (
367
+ typeof entity === "object" &&
368
+ entity !== null &&
369
+ "toRESTApiObject" in entity &&
370
+ typeof entity.toRESTApiObject === "function"
371
+ ) {
372
+ // It's already a typed object, return as-is
373
+ return entity as IToRESTApiObject;
374
+ } else {
375
+ // It's plain data, reconstruct as typed object
376
+ return EntityTransformer.reconstructFromPlainData(
377
+ entityType,
378
+ entity as Record<string, string | null>
379
+ );
380
+ }
381
+ });
382
+
383
+ return {
384
+ message:
385
+ typeof err?.message === "string" ? err.message : "Unknown error",
386
+ errorEntities: typedErrorEntities,
387
+ };
388
+ });
389
+
390
+ const errorCount = batchErrors.reduce((total: number, batchError) => {
391
+ return total + batchError.errorEntities.length;
392
+ }, 0);
393
+
394
+ logger.info("writeEntitiesToMM: Extracted 500 error details", {
395
+ batchErrorsCount: batchErrors.length,
396
+ totalErrorCount: errorCount,
397
+ entityType,
398
+ });
399
+
400
+ return {
401
+ errorCount,
402
+ batchErrors,
403
+ };
404
+ } catch (error) {
405
+ // If we can't even parse the structure safely, log what we can and return null
406
+ logger.error(
407
+ "writeEntitiesToMM: Failed to parse 500 exception structure safely",
408
+ {
409
+ error: error instanceof Error ? error.message : String(error),
410
+ exceptionType: typeof exception,
411
+ entityType,
412
+ }
413
+ );
414
+ return null;
415
+ }
416
+ }
417
+ }
@@ -0,0 +1,6 @@
1
+ // Public exports for standard-process-drivers
2
+ export { StandardProcessDrivers } from "./standard-process-drivers";
3
+ export type {
4
+ WriteEntitiesToMMResult,
5
+ MMBatchValidationError,
6
+ } from "./standard-process-drivers";
@@ -0,0 +1,261 @@
1
+ import { ERPType } from "../../types/erp-types";
2
+ import { IERPLaborTicketHandler } from "../../types/erp-connector";
3
+ import { MMApiClient } from "../../services/mm-api-service/mm-api-service";
4
+ import { MMReceiveLaborTicket } from "../../services/mm-api-service/types/receive-types";
5
+ import { convertLaborTicketToLocalTimezone } from "../mm-labor-ticket-helpers";
6
+ import { getCachedTimezoneOffset } from "../local-data-store/jobs-shared-data";
7
+ import logger from "../../services/reporting-service/logger";
8
+
9
+ /**
10
+ * Handles synchronization of labor tickets between MachineMetrics and ERP systems
11
+ */
12
+ export class LaborTicketERPSynchronizer {
13
+ /**
14
+ * Synchronizes updated labor tickets from MachineMetrics to an ERP system
15
+ */
16
+ static async syncToERP(
17
+ connectorType: ERPType,
18
+ connector: IERPLaborTicketHandler
19
+ ): Promise<void> {
20
+ try {
21
+ const mmApiClient = new MMApiClient();
22
+ const failedLaborTicketRefs: string[] = [];
23
+
24
+ await mmApiClient.initializeCheckpoint({
25
+ system: connectorType,
26
+ table: "labor_tickets",
27
+ checkpointType: "export",
28
+ checkpointValue: {
29
+ timestamp: new Date().toISOString(),
30
+ },
31
+ });
32
+
33
+ const fallbackTimestamp = new Date().toISOString();
34
+ const laborTicketsUpdates = await mmApiClient.fetchLaborTicketUpdates({
35
+ system: connectorType,
36
+ checkpointType: "export",
37
+ });
38
+
39
+ if (laborTicketsUpdates.length === 0) {
40
+ logger.info("syncLaborTicketsToERP:No updated labor tickets found");
41
+ return;
42
+ }
43
+
44
+ logger.info(
45
+ `ToERP: Found ${laborTicketsUpdates.length} Labor Ticket Ids and Refs to process`,
46
+ {
47
+ laborTickets: laborTicketsUpdates.map(
48
+ (ticket: MMReceiveLaborTicket) => ({
49
+ ref: ticket.laborTicketRef,
50
+ id: ticket.laborTicketId,
51
+ })
52
+ ),
53
+ }
54
+ );
55
+
56
+ // Find the most recent updatedAt timestamp from labor tickets. This will be used to update
57
+ // the checkpoint to ensure there is no gap of time for the next sync.
58
+ const mostRecentUpdate = laborTicketsUpdates.reduce(
59
+ (latest: string | null, ticket: MMReceiveLaborTicket) => {
60
+ if (!latest || !ticket.updatedAt) return latest;
61
+ return new Date(ticket.updatedAt) > new Date(latest)
62
+ ? ticket.updatedAt
63
+ : latest;
64
+ },
65
+ null as string | null
66
+ );
67
+
68
+ await Promise.all(
69
+ laborTicketsUpdates.map(async (laborTicket: MMReceiveLaborTicket) => {
70
+ if (!laborTicket.laborTicketRef) {
71
+ logger.error(
72
+ "syncLaborTicketsToERP: laborTicketRef is not set for laborTicket pulled from MM:",
73
+ { laborTicket }
74
+ );
75
+ return undefined;
76
+ }
77
+
78
+ try {
79
+ return await this.processLaborTicket(
80
+ connector,
81
+ mmApiClient,
82
+ laborTicket
83
+ );
84
+ } catch (error) {
85
+ failedLaborTicketRefs.push(laborTicket.laborTicketRef);
86
+ logger.error(
87
+ `syncLaborTicketsToERP: Error processing laborTicketRef ${laborTicket.laborTicketRef}:`,
88
+ { error }
89
+ );
90
+ return undefined;
91
+ }
92
+ })
93
+ );
94
+
95
+ logger.info(
96
+ `syncLaborTicketsToERP: ${failedLaborTicketRefs.length} failed labor ticket ids`
97
+ );
98
+ if (failedLaborTicketRefs.length > 0) {
99
+ logger.info(
100
+ `syncLaborTicketsToERP: Reporting ${failedLaborTicketRefs.length} labor ticket failures:`,
101
+ {
102
+ failedLaborTicketRefs,
103
+ }
104
+ );
105
+ const addFailedResult = await mmApiClient.addFailedLaborTicketRefs(
106
+ connectorType,
107
+ failedLaborTicketRefs
108
+ );
109
+ logger.info("syncLaborTicketsToERP: addFailedResult:", {
110
+ addFailedResult,
111
+ });
112
+ }
113
+
114
+ mmApiClient.saveCheckpoint({
115
+ system: connectorType,
116
+ table: "labor_tickets",
117
+ checkpointType: "export",
118
+ checkpointValue: {
119
+ timestamp: mostRecentUpdate || fallbackTimestamp,
120
+ },
121
+ });
122
+ } catch (error) {
123
+ logger.error("syncLaborTicketsToERP: Error:", error);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Retries labor tickets that have failed to be created or updated in the ERP during the sync
129
+ */
130
+ static async retryFailed(
131
+ connectorType: ERPType,
132
+ connector: IERPLaborTicketHandler
133
+ ): Promise<void> {
134
+ try {
135
+ const mmApiClient = new MMApiClient();
136
+ const successLaborTicketIds: string[] = [];
137
+
138
+ const laborTickets =
139
+ await mmApiClient.fetchFailedLaborTickets(connectorType);
140
+ if (laborTickets.length === 0) {
141
+ logger.info("retryFailedLaborTickets: No failed labor tickets found");
142
+ return;
143
+ }
144
+ logger.info(
145
+ "retryFailedLaborTickets: Failed Labor Tickets count:" +
146
+ laborTickets.length
147
+ );
148
+
149
+ await Promise.all(
150
+ laborTickets.map(async (laborTicket: MMReceiveLaborTicket) => {
151
+ if (!laborTicket.laborTicketRef) {
152
+ logger.error(
153
+ "retryFailedLaborTickets: laborTicketRef is not set for laborTicket pulled from MM:",
154
+ { laborTicket }
155
+ );
156
+ return undefined;
157
+ }
158
+
159
+ try {
160
+ const laborTicketResult = await this.processLaborTicket(
161
+ connector,
162
+ mmApiClient,
163
+ laborTicket
164
+ );
165
+ successLaborTicketIds.push(laborTicket.laborTicketRef);
166
+ return laborTicketResult;
167
+ } catch (error) {
168
+ logger.error(
169
+ "retryFailedLaborTickets: Error processing laborTicketRef:",
170
+ { laborTicketRef: laborTicket.laborTicketRef, error }
171
+ );
172
+ return undefined;
173
+ }
174
+ })
175
+ );
176
+
177
+ if (successLaborTicketIds.length > 0) {
178
+ logger.info("Deleting failed labor ticket ids:", {
179
+ successLaborTicketIds,
180
+ });
181
+ const deleteFailedResult = await mmApiClient.deleteFailedLaborTicketIds(
182
+ connectorType,
183
+ successLaborTicketIds
184
+ );
185
+ logger.info("deleteFailedResult:", { deleteFailedResult });
186
+ }
187
+ } catch (error) {
188
+ logger.error("retryFailedLaborTickets: Error:", error);
189
+ }
190
+ }
191
+
192
+ // ============================================================================
193
+ // PRIVATE HELPER METHODS
194
+ // ============================================================================
195
+
196
+ private static async writeLaborTicketIdToMM(
197
+ mmApiClient: MMApiClient,
198
+ laborTicket: MMReceiveLaborTicket,
199
+ laborTicketResult: MMReceiveLaborTicket
200
+ ): Promise<void> {
201
+ const updateRefAPIResponse = await mmApiClient.updateLaborTicketIdByRef(
202
+ laborTicket.laborTicketRef,
203
+ laborTicketResult.laborTicketId
204
+ );
205
+ logger.info(
206
+ `Updated laborTicketId ${laborTicketResult.laborTicketId} for laborTicketRef ${laborTicket.laborTicketRef} in MM:`,
207
+ { updateRefAPIResponse }
208
+ );
209
+ }
210
+
211
+ private static async processLaborTicket(
212
+ connector: IERPLaborTicketHandler,
213
+ mmApiClient: MMApiClient,
214
+ laborTicket: MMReceiveLaborTicket
215
+ ): Promise<MMReceiveLaborTicket> {
216
+ let laborTicketResult: MMReceiveLaborTicket;
217
+
218
+ laborTicketResult = convertLaborTicketToLocalTimezone(
219
+ laborTicket,
220
+ getCachedTimezoneOffset()
221
+ );
222
+
223
+ logger.info(
224
+ `processing laborTicket, id=${laborTicket.laborTicketId}, ref=${laborTicket.laborTicketRef}`
225
+ );
226
+ logger.debug({ laborTicket });
227
+
228
+ // MLW TODO: Should we always swap out the resource ID for the machine group ID if it is found?
229
+ // Or use a flag? What about the default resource ID as used by syteline? A Rutherford consultation is in order.
230
+ /**
231
+ * Swapping out the resource ID for the group id corresponding to the
232
+ * resource ID in the ERP would be done like so:
233
+ * const machineGroupId = await mmApiClient.getResourceERPGroupId(laborTicket.resourceId);
234
+ * if(machineGroupId) {
235
+ * laborTicket.resourceId = machineGroupId;
236
+ * }
237
+ */
238
+
239
+ if (!laborTicket.laborTicketId) {
240
+ const { laborTicket: laborTicketResult, erpUid } =
241
+ await connector.createLaborTicketInERP(laborTicket);
242
+ laborTicketResult.laborTicketId = erpUid;
243
+ await this.writeLaborTicketIdToMM(
244
+ mmApiClient,
245
+ laborTicket,
246
+ laborTicketResult
247
+ );
248
+ } else {
249
+ laborTicketResult = await connector.updateLaborTicketInERP(laborTicket);
250
+ }
251
+
252
+ logger.info("ToERP: laborTicket update result:", {
253
+ laborTicketResult:
254
+ laborTicketResult || "Failed to create/update labor ticket",
255
+ laborTicketRef: laborTicket.laborTicketRef,
256
+ operation: laborTicket.laborTicketId ? "update" : "create",
257
+ });
258
+
259
+ return laborTicketResult;
260
+ }
261
+ }