@machinemetrics/mm-erp-sdk 0.3.0-beta.0 → 0.3.0-beta.2

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 (141) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/services/data-sync-service/configuration-manager.d.ts.map +1 -1
  6. package/dist/services/data-sync-service/configuration-manager.js +30 -30
  7. package/dist/services/data-sync-service/configuration-manager.js.map +1 -1
  8. package/dist/services/data-sync-service/data-sync-service.d.ts.map +1 -1
  9. package/dist/services/data-sync-service/data-sync-service.js +9 -0
  10. package/dist/services/data-sync-service/data-sync-service.js.map +1 -1
  11. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts.map +1 -1
  12. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +1 -2
  13. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js.map +1 -1
  14. package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
  15. package/dist/services/data-sync-service/jobs/from-erp.js +7 -13
  16. package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
  17. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts.map +1 -1
  18. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +1 -2
  19. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js.map +1 -1
  20. package/dist/services/data-sync-service/jobs/run-migrations.d.ts.map +1 -1
  21. package/dist/services/data-sync-service/jobs/run-migrations.js +1 -2
  22. package/dist/services/data-sync-service/jobs/run-migrations.js.map +1 -1
  23. package/dist/services/data-sync-service/jobs/to-erp.d.ts.map +1 -1
  24. package/dist/services/data-sync-service/jobs/to-erp.js +12 -3
  25. package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -1
  26. package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts +30 -0
  27. package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts.map +1 -0
  28. package/dist/services/data-sync-service/nats-labor-ticket-listener.js +290 -0
  29. package/dist/services/data-sync-service/nats-labor-ticket-listener.js.map +1 -0
  30. package/dist/services/mm-api-service/company-info.d.ts +13 -0
  31. package/dist/services/mm-api-service/company-info.d.ts.map +1 -0
  32. package/dist/services/mm-api-service/company-info.js +60 -0
  33. package/dist/services/mm-api-service/company-info.js.map +1 -0
  34. package/dist/services/mm-api-service/index.d.ts +7 -0
  35. package/dist/services/mm-api-service/index.d.ts.map +1 -1
  36. package/dist/services/mm-api-service/index.js +5 -0
  37. package/dist/services/mm-api-service/index.js.map +1 -1
  38. package/dist/services/mm-api-service/mm-api-service.d.ts +6 -0
  39. package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
  40. package/dist/services/mm-api-service/mm-api-service.js +15 -3
  41. package/dist/services/mm-api-service/mm-api-service.js.map +1 -1
  42. package/dist/services/mm-api-service/types/receive-types.d.ts +3 -0
  43. package/dist/services/mm-api-service/types/receive-types.d.ts.map +1 -1
  44. package/dist/services/mm-api-service/types/receive-types.js +1 -0
  45. package/dist/services/mm-api-service/types/receive-types.js.map +1 -1
  46. package/dist/services/mm-api-service/types/send-types.d.ts +17 -8
  47. package/dist/services/mm-api-service/types/send-types.d.ts.map +1 -1
  48. package/dist/services/mm-api-service/types/send-types.js +41 -17
  49. package/dist/services/mm-api-service/types/send-types.js.map +1 -1
  50. package/dist/services/nats-service/nats-service.d.ts +114 -0
  51. package/dist/services/nats-service/nats-service.d.ts.map +1 -0
  52. package/dist/services/nats-service/nats-service.js +244 -0
  53. package/dist/services/nats-service/nats-service.js.map +1 -0
  54. package/dist/services/nats-service/test-nats-subscriber.d.ts +6 -0
  55. package/dist/services/nats-service/test-nats-subscriber.d.ts.map +1 -0
  56. package/dist/services/nats-service/test-nats-subscriber.js +79 -0
  57. package/dist/services/nats-service/test-nats-subscriber.js.map +1 -0
  58. package/dist/services/reporting-service/logger.d.ts.map +1 -1
  59. package/dist/services/reporting-service/logger.js +31 -6
  60. package/dist/services/reporting-service/logger.js.map +1 -1
  61. package/dist/types/erp-connector.d.ts +1 -8
  62. package/dist/types/erp-connector.d.ts.map +1 -1
  63. package/dist/types/index.d.ts +0 -1
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/utils/error-formatter.d.ts +19 -0
  66. package/dist/utils/error-formatter.d.ts.map +1 -0
  67. package/dist/utils/error-formatter.js +184 -0
  68. package/dist/utils/error-formatter.js.map +1 -0
  69. package/dist/utils/http-client.js +2 -4
  70. package/dist/utils/http-client.js.map +1 -1
  71. package/dist/utils/index.d.ts +5 -1
  72. package/dist/utils/index.d.ts.map +1 -1
  73. package/dist/utils/index.js +4 -1
  74. package/dist/utils/index.js.map +1 -1
  75. package/dist/utils/local-data-store/jobs-shared-data.d.ts +0 -2
  76. package/dist/utils/local-data-store/jobs-shared-data.d.ts.map +1 -1
  77. package/dist/utils/local-data-store/jobs-shared-data.js +0 -2
  78. package/dist/utils/local-data-store/jobs-shared-data.js.map +1 -1
  79. package/dist/utils/mm-labor-ticket-helpers.d.ts +4 -3
  80. package/dist/utils/mm-labor-ticket-helpers.d.ts.map +1 -1
  81. package/dist/utils/mm-labor-ticket-helpers.js +7 -12
  82. package/dist/utils/mm-labor-ticket-helpers.js.map +1 -1
  83. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.d.ts +0 -15
  84. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.d.ts.map +1 -1
  85. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.js +46 -180
  86. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.js.map +1 -1
  87. package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts +1 -7
  88. package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts.map +1 -1
  89. package/dist/utils/standard-process-drivers/mm-entity-processor.js +1 -7
  90. package/dist/utils/standard-process-drivers/mm-entity-processor.js.map +1 -1
  91. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts +2 -8
  92. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts.map +1 -1
  93. package/dist/utils/standard-process-drivers/standard-process-drivers.js +18 -27
  94. package/dist/utils/standard-process-drivers/standard-process-drivers.js.map +1 -1
  95. package/dist/utils/time-utils.d.ts.map +1 -1
  96. package/dist/utils/time-utils.js +0 -7
  97. package/dist/utils/time-utils.js.map +1 -1
  98. package/package.json +5 -4
  99. package/src/index.ts +3 -0
  100. package/src/services/data-sync-service/configuration-manager.ts +37 -50
  101. package/src/services/data-sync-service/data-sync-service.ts +10 -0
  102. package/src/services/data-sync-service/jobs/clean-up-expired-cache.ts +1 -2
  103. package/src/services/data-sync-service/jobs/from-erp.ts +7 -13
  104. package/src/services/data-sync-service/jobs/retry-failed-labor-tickets.ts +1 -2
  105. package/src/services/data-sync-service/jobs/run-migrations.ts +1 -2
  106. package/src/services/data-sync-service/jobs/to-erp.ts +12 -3
  107. package/src/services/data-sync-service/nats-labor-ticket-listener.ts +342 -0
  108. package/src/services/mm-api-service/company-info.ts +87 -0
  109. package/src/services/mm-api-service/index.ts +8 -0
  110. package/src/services/mm-api-service/mm-api-service.ts +20 -3
  111. package/src/services/mm-api-service/types/receive-types.ts +1 -0
  112. package/src/services/mm-api-service/types/send-types.ts +40 -16
  113. package/src/services/nats-service/nats-service.ts +351 -0
  114. package/src/services/nats-service/test-nats-subscriber.ts +96 -0
  115. package/src/services/reporting-service/logger.ts +39 -7
  116. package/src/types/erp-connector.ts +1 -8
  117. package/src/types/index.ts +0 -8
  118. package/src/utils/error-formatter.ts +205 -0
  119. package/src/utils/http-client.ts +3 -4
  120. package/src/utils/index.ts +6 -5
  121. package/src/utils/local-data-store/jobs-shared-data.ts +0 -2
  122. package/src/utils/mm-labor-ticket-helpers.ts +8 -11
  123. package/src/utils/standard-process-drivers/labor-ticket-erp-synchronizer.ts +64 -220
  124. package/src/utils/standard-process-drivers/mm-entity-processor.ts +1 -7
  125. package/src/utils/standard-process-drivers/standard-process-drivers.ts +19 -33
  126. package/src/utils/time-utils.ts +0 -11
  127. package/dist/types/flattened-work-order.d.ts +0 -99
  128. package/dist/types/flattened-work-order.d.ts.map +0 -1
  129. package/dist/types/flattened-work-order.js +0 -2
  130. package/dist/types/flattened-work-order.js.map +0 -1
  131. package/dist/utils/env.d.ts +0 -8
  132. package/dist/utils/env.d.ts.map +0 -1
  133. package/dist/utils/env.js +0 -58
  134. package/dist/utils/env.js.map +0 -1
  135. package/dist/utils/erp-timezone-utils.d.ts +0 -20
  136. package/dist/utils/erp-timezone-utils.d.ts.map +0 -1
  137. package/dist/utils/erp-timezone-utils.js +0 -75
  138. package/dist/utils/erp-timezone-utils.js.map +0 -1
  139. package/src/types/flattened-work-order.ts +0 -108
  140. package/src/utils/env.ts +0 -75
  141. package/src/utils/erp-timezone-utils.ts +0 -99
@@ -2,16 +2,13 @@ import { IERPLaborTicketHandler } from "../../types/erp-connector.js";
2
2
  import { MMApiClient } from "../../services/mm-api-service/mm-api-service.js";
3
3
  import { MMReceiveLaborTicket } from "../../services/mm-api-service/types/receive-types.js";
4
4
  import { convertLaborTicketToLocalTimezone } from "../mm-labor-ticket-helpers.js";
5
+ import { getCachedTimezoneOffset } from "../local-data-store/jobs-shared-data.js";
5
6
  import logger from "../../services/reporting-service/logger.js";
6
7
 
7
8
  /**
8
9
  * Handles synchronization of labor tickets between MachineMetrics and ERP systems
9
10
  */
10
11
  export class LaborTicketERPSynchronizer {
11
- // Small allowance to mitigate tiny clock offsets / timestamp granularity differences
12
- // between the connector host and MM's timestamps.
13
- private static readonly CHECKPOINT_SKEW_MS = 2_000;
14
-
15
12
  /**
16
13
  * Synchronizes updated labor tickets from MachineMetrics to an ERP system
17
14
  */
@@ -21,9 +18,9 @@ export class LaborTicketERPSynchronizer {
21
18
  ): Promise<void> {
22
19
  try {
23
20
  const mmApiClient = new MMApiClient();
24
- const failedLaborTicketRefs = new Set<string>();
25
- const attemptedSignatures = new Set<string>();
21
+ const failedLaborTicketRefs: string[] = [];
26
22
 
23
+ // Initialize and fetch checkpoint (quick operations, don't need prolonged lock)
27
24
  await mmApiClient.initializeCheckpoint({
28
25
  system: connectorType,
29
26
  table: "labor_tickets",
@@ -33,168 +30,95 @@ export class LaborTicketERPSynchronizer {
33
30
  },
34
31
  });
35
32
 
36
- /**
37
- * We cannot safely checkpoint to "now" without draining, because updates can arrive mid-run.
38
- * And we cannot checkpoint to "most recent updatedAt from the initial fetch" because we
39
- * update MM (setting laborTicketId on creates), which bumps updatedAt and causes the same
40
- * ticket to be re-pulled next cycle.
41
- *
42
- * We also cannot dedupe only by laborTicketRef, because the same ticket can legitimately
43
- * change multiple times within a single run (parts counts, times, reasons, state, etc.).
44
- * And we cannot dedupe by (laborTicketRef, updatedAt) because updatedAt is exactly what our
45
- * own MM write-back (setting laborTicketId) mutates, creating a false "new version".
46
- *
47
- * Remedy: drain the window up to a moving barrier time (local "now"), re-fetching until MM
48
- * reports no additional unattempted tickets in that window, and dedupe within the run by a
49
- * stable business signature (operational fields only; excludes metadata like updatedAt and
50
- * excludes SDK-mutated fields like laborTicketId). Then checkpoint to a value that is not
51
- * meaningfully ahead of MM's own timestamps (small skew clamp).
52
- */
53
- const maxPasses = 10;
54
- let pass = 0;
55
- let barrierTime = new Date().toISOString();
56
- let maxMmTimestampWithinBarrier: string | null = null;
57
-
58
- while (pass < maxPasses) {
59
- pass += 1;
60
- barrierTime = new Date().toISOString();
61
-
62
- const laborTicketsUpdates = await mmApiClient.fetchLaborTicketUpdates({
63
- system: connectorType,
64
- checkpointType: "export",
65
- });
66
-
67
- if (laborTicketsUpdates.length === 0) {
68
- if (pass === 1) {
69
- logger.info("syncLaborTicketsToERP:No updated labor tickets found");
70
- }
71
- break;
72
- }
33
+ const fallbackTimestamp = new Date().toISOString();
34
+ const laborTicketsUpdates = await mmApiClient.fetchLaborTicketUpdates({
35
+ system: connectorType,
36
+ checkpointType: "export",
37
+ });
73
38
 
74
- // Track the latest MM timestamp we see that is <= the current barrier.
75
- for (const ticket of laborTicketsUpdates) {
76
- const ts = this.getComparableTicketTimestamp(ticket);
77
- if (!ts) continue;
78
- if (new Date(ts) > new Date(barrierTime)) continue;
79
- if (
80
- !maxMmTimestampWithinBarrier ||
81
- new Date(ts) > new Date(maxMmTimestampWithinBarrier)
82
- ) {
83
- maxMmTimestampWithinBarrier = ts;
84
- }
85
- }
39
+ if (laborTicketsUpdates.length === 0) {
40
+ logger.info("syncLaborTicketsToERP:No updated labor tickets found");
41
+ return;
42
+ }
86
43
 
87
- if (pass === 1) {
88
- logger.info(
89
- `ToERP: Found ${laborTicketsUpdates.length} Labor Ticket Ids and Refs to process`,
90
- {
91
- laborTickets: laborTicketsUpdates.map(
92
- (ticket: MMReceiveLaborTicket) => ({
93
- ref: ticket.laborTicketRef,
94
- id: ticket.laborTicketId,
95
- })
96
- ),
97
- }
98
- );
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
+ ),
99
53
  }
54
+ );
100
55
 
101
- // Build a unique pending list by a stable "business signature", and only consider tickets
102
- // whose timestamps are <= the barrierTime (prevents checkpoint gaps if clocks differ).
103
- const pending: Array<{ sig: string; ticket: MMReceiveLaborTicket }> = [];
104
- const dedupeWithinFetch = new Set<string>();
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
+ );
105
67
 
106
- for (const ticket of laborTicketsUpdates) {
107
- if (!ticket.laborTicketRef) {
68
+ await Promise.all(
69
+ laborTicketsUpdates.map(async (laborTicket: MMReceiveLaborTicket) => {
70
+ if (!laborTicket.laborTicketRef) {
108
71
  logger.error(
109
72
  "syncLaborTicketsToERP: laborTicketRef is not set for laborTicket pulled from MM:",
110
- { laborTicket: ticket }
73
+ { laborTicket }
111
74
  );
112
- continue;
75
+ return undefined;
113
76
  }
114
77
 
115
- const ts = this.getComparableTicketTimestamp(ticket);
116
- if (ts && new Date(ts) > new Date(barrierTime)) {
117
- // Defer "future" tickets (relative to local wall clock) to a later run/pass.
118
- continue;
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;
119
91
  }
120
-
121
- const sig = this.laborTicketBusinessSignature(ticket);
122
- if (attemptedSignatures.has(sig) || dedupeWithinFetch.has(sig)) continue;
123
- dedupeWithinFetch.add(sig);
124
- pending.push({ sig, ticket });
125
- }
126
-
127
- if (pending.length === 0) {
128
- // Nothing new to do in this barrier window; safe to checkpoint and exit.
129
- break;
130
- }
131
-
132
- logger.info(
133
- `syncLaborTicketsToERP: pass=${pass}/${maxPasses}, barrier=${barrierTime}, pending=${pending.length}`
134
- );
135
-
136
- await Promise.all(
137
- pending.map(async ({ sig, ticket }) => {
138
- // Mark attempted up-front so we don't re-attempt within this run even if MM reorders.
139
- attemptedSignatures.add(sig);
140
-
141
- try {
142
- return await this.processLaborTicket(connector, mmApiClient, ticket);
143
- } catch (error) {
144
- failedLaborTicketRefs.add(ticket.laborTicketRef);
145
- logger.error(
146
- `syncLaborTicketsToERP: Error processing laborTicketRef ${ticket.laborTicketRef}:`,
147
- { error }
148
- );
149
- return undefined;
150
- }
151
- })
152
- );
153
- }
154
-
155
- if (pass >= maxPasses) {
156
- logger.warn(
157
- `syncLaborTicketsToERP: Reached max passes (${maxPasses}). Checkpointing anyway to avoid infinite loops.`
158
- );
159
- }
92
+ })
93
+ );
160
94
 
161
95
  logger.info(
162
- `syncLaborTicketsToERP: ${failedLaborTicketRefs.size} failed labor ticket ids`
96
+ `syncLaborTicketsToERP: ${failedLaborTicketRefs.length} failed labor ticket ids`
163
97
  );
164
- if (failedLaborTicketRefs.size > 0) {
165
- const failedTicketRefs = Array.from(failedLaborTicketRefs);
98
+ if (failedLaborTicketRefs.length > 0) {
166
99
  logger.info(
167
- `syncLaborTicketsToERP: Reporting ${failedTicketRefs.length} labor ticket failures:`,
100
+ `syncLaborTicketsToERP: Reporting ${failedLaborTicketRefs.length} labor ticket failures:`,
168
101
  {
169
- failedLaborTicketRefs: failedTicketRefs,
102
+ failedLaborTicketRefs,
170
103
  }
171
104
  );
172
105
  const addFailedResult = await mmApiClient.addFailedLaborTicketRefs(
173
106
  connectorType,
174
- failedTicketRefs
107
+ failedLaborTicketRefs
175
108
  );
176
109
  logger.info("syncLaborTicketsToERP: addFailedResult:", {
177
110
  addFailedResult,
178
111
  });
179
112
  }
180
113
 
181
- const checkpointTimestamp = this.computeCheckpointTimestamp({
182
- barrierTime,
183
- maxMmTimestampWithinBarrier,
184
- skewMs: this.CHECKPOINT_SKEW_MS,
185
- });
186
-
187
- await mmApiClient.saveCheckpoint({
114
+ mmApiClient.saveCheckpoint({
188
115
  system: connectorType,
189
116
  table: "labor_tickets",
190
117
  checkpointType: "export",
191
118
  checkpointValue: {
192
- timestamp: checkpointTimestamp,
119
+ timestamp: mostRecentUpdate || fallbackTimestamp,
193
120
  },
194
121
  });
195
- logger.info("syncLaborTicketsToERP: Checkpoint saved:", {
196
- checkpointTimestamp,
197
- });
198
122
  } catch (error) {
199
123
  logger.error("syncLaborTicketsToERP: Error:", error);
200
124
  }
@@ -291,10 +215,10 @@ export class LaborTicketERPSynchronizer {
291
215
  ): Promise<MMReceiveLaborTicket> {
292
216
  let laborTicketResult: MMReceiveLaborTicket;
293
217
 
294
- // Convert MM's UTC timestamps into the ERP's local timezone before invoking connector code.
295
- // Connector implementations should treat the incoming values as localized wall time and avoid
296
- // applying any further timezone shifts.
297
- laborTicketResult = convertLaborTicketToLocalTimezone(laborTicket);
218
+ laborTicketResult = convertLaborTicketToLocalTimezone(
219
+ laborTicket,
220
+ getCachedTimezoneOffset()
221
+ );
298
222
 
299
223
  logger.info(
300
224
  `processing laborTicket, id=${laborTicket.laborTicketId}, ref=${laborTicket.laborTicketRef}`
@@ -334,84 +258,4 @@ export class LaborTicketERPSynchronizer {
334
258
 
335
259
  return laborTicketResult;
336
260
  }
337
-
338
- /**
339
- * A stable identity for "did the business-relevant contents of this labor ticket change?"
340
- *
341
- * IMPORTANT: Excludes fields that the SDK itself mutates (e.g. laborTicketId, updatedAt),
342
- * otherwise we'd reprocess the write-back bump on the next cycle.
343
- */
344
- private static laborTicketBusinessSignature(ticket: MMReceiveLaborTicket): string {
345
- const reasons = (ticket.reasons ?? [])
346
- .map((r) => {
347
- const reason: any = (r as any)?.reason ?? {};
348
- return (
349
- reason.reasonId ??
350
- reason.code ??
351
- reason.reasonRef ??
352
- reason.rejectReasonRef ??
353
- ""
354
- );
355
- })
356
- .filter((v) => v !== "" && v !== null && v !== undefined)
357
- .map(String)
358
- .sort();
359
-
360
- const signatureObject = {
361
- laborTicketRef: ticket.laborTicketRef ?? null,
362
- clockIn: ticket.clockIn ?? null,
363
- clockOut: ticket.clockOut ?? null,
364
- goodParts: ticket.goodParts ?? null,
365
- badParts: ticket.badParts ?? null,
366
- state: ticket.state ?? null,
367
- type: ticket.type ?? null,
368
- transactionDate: ticket.transactionDate ?? null,
369
- workOrderId: ticket.workOrderId ?? null,
370
- sequenceNumber: ticket.sequenceNumber ?? null,
371
- personId: ticket.personId ?? null,
372
- resourceId: ticket.resourceId ?? null,
373
- lot: ticket.lot ?? null,
374
- split: ticket.split ?? null,
375
- sub: ticket.sub ?? null,
376
- comment: ticket.comment ?? null,
377
- workOrderOperationClosedDate: ticket.workOrderOperationClosedDate ?? null,
378
- reasons,
379
- };
380
-
381
- return JSON.stringify(signatureObject);
382
- }
383
-
384
- /**
385
- * Selects a comparable timestamp for windowing decisions.
386
- * Prefer updatedAt; fall back to other stable timestamps if needed.
387
- */
388
- private static getComparableTicketTimestamp(
389
- ticket: MMReceiveLaborTicket
390
- ): string | null {
391
- return (
392
- ticket.updatedAt ??
393
- ticket.createdAt ??
394
- ticket.transactionDate ??
395
- ticket.clockOut ??
396
- ticket.clockIn ??
397
- null
398
- );
399
- }
400
-
401
- private static addMsToIso(iso: string, ms: number): string {
402
- return new Date(new Date(iso).getTime() + ms).toISOString();
403
- }
404
-
405
- private static computeCheckpointTimestamp(options: {
406
- barrierTime: string;
407
- maxMmTimestampWithinBarrier: string | null;
408
- skewMs: number;
409
- }): string {
410
- const { barrierTime, maxMmTimestampWithinBarrier, skewMs } = options;
411
-
412
- if (!maxMmTimestampWithinBarrier) return barrierTime;
413
-
414
- const mmPlusSkew = this.addMsToIso(maxMmTimestampWithinBarrier, skewMs);
415
- return new Date(mmPlusSkew) < new Date(barrierTime) ? mmPlusSkew : barrierTime;
416
- }
417
261
  }
@@ -29,13 +29,7 @@ import { ErrorProcessor } from "./error-processor.js";
29
29
  */
30
30
  export class MMEntityProcessor {
31
31
  /**
32
- * Writes entities to MM API with deduplication and caching.
33
- *
34
- * IMPORTANT: All datetime fields on `mmRecords` MUST already be expressed as
35
- * ISO-8601 UTC strings (either trailing `Z` or an explicit offset). The SDK
36
- * does not apply timezone conversion on this path. It forwards values directly
37
- * to MachineMetrics and only reserializes them for validation. Connector code
38
- * is responsible for converting local ERP times to UTC before invoking this API.
32
+ * Writes entities to MM API with deduplication and caching
39
33
  */
40
34
  static async writeEntities(
41
35
  entityType: ERPObjType,
@@ -1,6 +1,5 @@
1
1
  import { ERPObjType } from "../../types/erp-types.js";
2
2
  import { IERPLaborTicketHandler } from "../../types/erp-connector.js";
3
- import type { FlattenedWorkOrderRow } from "../../types/flattened-work-order.js";
4
3
  import { BatchCacheManager } from "../../services/caching-service/batch-cache-manager.js";
5
4
  import {
6
5
  IToRESTApiObject,
@@ -249,12 +248,7 @@ export class StandardProcessDrivers {
249
248
  * parts, part operations, work orders, and work order operations, then process them in the correct order
250
249
  * to maintain referential integrity.
251
250
  *
252
- * Timezone expectation: all datetime fields provided in `flattenedData` MUST already be expressed
253
- * as ISO-8601 UTC strings (trailing `Z` or explicit offset). The SDK does not attempt to infer or
254
- * convert local ERP timestamps on this path; it simply validates and forwards the values to MM.
255
- * Connector implementations should convert ERP-local times to UTC before invoking this method.
256
- *
257
- * @param flattenedData Array of flattened rows containing both work order and operation data (see `FlattenedWorkOrderRow`)
251
+ * @param flattenedData Array of flattened rows containing both work order and operation data (camelCase fields)
258
252
  * @param batchCacheManager The batch cache manager instance; pass in null if caching is not desired
259
253
  *
260
254
  * @returns Combined results from all entity processing with detailed logging information
@@ -262,7 +256,7 @@ export class StandardProcessDrivers {
262
256
  * @throws Error on other underlying issues (network, authentication, etc.)
263
257
  */
264
258
  static async syncWorkOrderBatchFromFlattened(
265
- flattenedData: ReadonlyArray<FlattenedWorkOrderRow>,
259
+ flattenedData: any[],
266
260
  batchCacheManager: BatchCacheManager | null
267
261
  ): Promise<{
268
262
  parts: WriteEntitiesToMMResult;
@@ -274,11 +268,6 @@ export class StandardProcessDrivers {
274
268
  throw new Error("No flattened work order data provided");
275
269
  }
276
270
 
277
- const toStringOrFallback = (
278
- value: string | number | null | undefined,
279
- fallback = ""
280
- ): string => (value === undefined || value === null ? fallback : String(value));
281
-
282
271
  // Process the flattened data - each row contains both work order and operation info
283
272
  const uniqueParts = new Map();
284
273
  const uniquePartOperations = new Map();
@@ -308,8 +297,8 @@ export class StandardProcessDrivers {
308
297
  resourceId: row.resourceId, // → resourceId
309
298
  cycleTimeMs: row.cycleTimeMs, // → cycleTimeMs
310
299
  setupTimeMs: row.setupTimeMs, // → setupTimeMs
311
- operationDescription: row.operationDescription, // → description
312
- quantityPerPart: row.quantityPerPart ?? 1, // → quantityPerPart
300
+ description: row.operationDescription || "", // → description
301
+ quantityPerPart: row.quantityPerPart || 1, // → quantityPerPart
313
302
  });
314
303
  }
315
304
 
@@ -362,8 +351,8 @@ export class StandardProcessDrivers {
362
351
  const parts = Array.from(uniqueParts.values()).map(
363
352
  (item) =>
364
353
  new MMSendPart(
365
- toStringOrFallback(item.partNumber), // partNumber
366
- toStringOrFallback(item.partRevision), // partRevision
354
+ item.partNumber || "", // partNumber
355
+ item.partRevision || "", // partRevision
367
356
  item.method || "Standard" // method
368
357
  )
369
358
  );
@@ -371,14 +360,14 @@ export class StandardProcessDrivers {
371
360
  const partOperations = Array.from(uniquePartOperations.values()).map(
372
361
  (item) =>
373
362
  new MMSendPartOperation(
374
- toStringOrFallback(item.partNumber), // partNumber
375
- toStringOrFallback(item.partRevision), // partRevision
363
+ item.partNumber || "", // partNumber
364
+ item.partRevision || "", // partRevision
376
365
  item.method || "Standard", // method
377
366
  item.sequenceNumber?.toString() || "", // sequenceNumber
378
367
  item.resourceId?.toString() || "", // resourceId
379
368
  item.cycleTimeMs || 0, // cycleTimeMs
380
369
  item.setupTimeMs || 0, // setupTimeMs
381
- item.operationDescription || "", // description
370
+ item.description || "", // description
382
371
  item.quantityPerPart || 1 // quantityPerPart
383
372
  )
384
373
  );
@@ -387,12 +376,10 @@ export class StandardProcessDrivers {
387
376
  (item) =>
388
377
  new MMSendWorkOrder(
389
378
  item.workOrderId?.toString() || "", // workOrderId
390
- toStringOrFallback(item.lot), // lot
391
- toStringOrFallback(item.split), // split
392
- toStringOrFallback(item.sub), // sub
379
+ item.lot || "", // lot
380
+ item.split || "", // split
381
+ item.sub || "", // sub
393
382
  item.status || "Open", // status
394
- // Datetimes are assumed to already be UTC (or include an explicit offset).
395
- // We reserialize via Date solely to validate the ISO format before sending to MM.
396
383
  item.dueDate ? new Date(item.dueDate).toISOString() : null, // dueDate
397
384
  item.description || "", // description
398
385
  item.scheduledStartDate
@@ -403,8 +390,8 @@ export class StandardProcessDrivers {
403
390
  : null, // scheduledEndDate
404
391
  item.closedDate ? new Date(item.closedDate).toISOString() : null, // closedDate
405
392
  item.quantityRequired || 0, // quantityRequired
406
- toStringOrFallback(item.partNumber), // partNumber
407
- toStringOrFallback(item.partRevision), // partRevision
393
+ item.partNumber || "", // partNumber
394
+ item.partRevision || "", // partRevision
408
395
  item.method || "Standard" // method
409
396
  )
410
397
  );
@@ -413,15 +400,14 @@ export class StandardProcessDrivers {
413
400
  (item) =>
414
401
  new MMSendWorkOrderOperation(
415
402
  item.workOrderId?.toString() || "", // workOrderId
416
- toStringOrFallback(item.lot), // lot
417
- toStringOrFallback(item.split), // split
418
- toStringOrFallback(item.sub), // sub
403
+ item.lot || "", // lot
404
+ item.split || "", // split
405
+ item.sub || "", // sub
419
406
  item.sequenceNumber?.toString() || "", // sequenceNumber
420
407
  item.resourceId?.toString() || "", // resourceId
421
408
  item.startQuantity || 0, // startQuantity
422
409
  item.finishQuantity || 0, // finishQuantity
423
410
  item.expectedRejectRate || 0, // expectedRejectRate
424
- // Same UTC expectation as above; reserialize to ensure valid ISO.
425
411
  item.scheduledStartDate
426
412
  ? new Date(item.scheduledStartDate).toISOString()
427
413
  : null, // scheduledStartDate
@@ -431,8 +417,8 @@ export class StandardProcessDrivers {
431
417
  item.closedDate ? new Date(item.closedDate).toISOString() : null, // closedDate
432
418
  item.cycleTimeMs || 0, // cycleTimeMs
433
419
  item.setupTimeMs || 0, // setupTimeMs
434
- parseFloat(toStringOrFallback(item.productionburdenRateHourly, "0")), // productionburdenRateHourly
435
- parseFloat(toStringOrFallback(item.setupburdenRatehourly, "0")), // setupburdenRatehourly
420
+ parseFloat(item.productionburdenRateHourly || "0"), // productionburdenRateHourly
421
+ parseFloat(item.setupburdenRatehourly || "0"), // setupburdenRatehourly
436
422
  item.operationType || "Production", // operationType
437
423
  item.quantityPerPart || 1, // quantityPerPart
438
424
  item.status || "Open" // status
@@ -1,4 +1,3 @@
1
- import { DateTime } from 'luxon';
2
1
  import logger from "../services/reporting-service/logger.js";
3
2
  import {
4
3
  setTimezoneOffsetInCache,
@@ -118,16 +117,6 @@ export const getTimezoneOffsetAndPersist = async (
118
117
  const { offset, timezone } = await getTimezoneOffset();
119
118
  logger.info(`Timezone offset: ${offset} hours, timezone: ${timezone}`);
120
119
  setTimezoneOffsetInCache(offset);
121
-
122
- const now = DateTime.now().setZone(timezone);
123
- if (!now.isValid) {
124
- throw new Error(
125
- `Invalid timezone name from Company properties: "${timezone}". ` +
126
- `Must be a valid IANA timezone name (e.g., "America/Chicago"). ` +
127
- `See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones`
128
- );
129
- }
130
-
131
120
  setTimezoneNameInCache(timezone);
132
121
  success = true;
133
122
  } catch (error) {
@@ -1,99 +0,0 @@
1
- /**
2
- * Canonical representation of a flattened work order row that contains the
3
- * information needed to build MM work order entities plus their operations.
4
- *
5
- * All properties are optional to maintain backwards compatibility with
6
- * existing connectors.
7
- */
8
- export interface FlattenedWorkOrderPartFields {
9
- /** ERP part number (required for Parts when present). */
10
- partNumber?: string | number;
11
- /** ERP part revision or version string. */
12
- partRevision?: string | number;
13
- /** Routing/method identifier (defaults to "Standard" when omitted). */
14
- method?: string;
15
- }
16
- export interface FlattenedWorkOrderPartOperationFields {
17
- /** Sequence number of the operation on the routing. */
18
- sequenceNumber?: string | number;
19
- /** Resource or machine identifier responsible for the operation. */
20
- resourceId?: string | number;
21
- /** Operation cycle time expressed in milliseconds. */
22
- cycleTimeMs?: number;
23
- /** Operation setup time expressed in milliseconds. */
24
- setupTimeMs?: number;
25
- /** Human-readable description of the operation. */
26
- operationDescription?: string;
27
- /** Quantity of parts operated on per cycle. */
28
- quantityPerPart?: number;
29
- }
30
- export interface FlattenedWorkOrderFields extends FlattenedWorkOrderPartFields {
31
- /** Unique identifier for the work order in the ERP. */
32
- workOrderId?: string | number;
33
- /** Lot number (if the ERP distinguishes lots). */
34
- lot?: string | number;
35
- /** Split identifier (if the ERP splits orders). */
36
- split?: string | number;
37
- /** Sub identifier (for multi-level orders). */
38
- sub?: string | number;
39
- /** Work order status (e.g., Open, Closed, Released). */
40
- status?: string;
41
- /**
42
- * Due date expressed as an ISO-8601 UTC string (trailing `Z` or explicit offset).
43
- * The SDK validates but does not shift timezones.
44
- */
45
- dueDate?: string;
46
- /** Free-form description of the work order. */
47
- description?: string;
48
- /** Scheduled start datetime in ISO-8601 UTC form. */
49
- scheduledStartDate?: string;
50
- /** Scheduled end datetime in ISO-8601 UTC form. */
51
- scheduledEndDate?: string;
52
- /** Closed datetime in ISO-8601 UTC form. */
53
- closedDate?: string;
54
- /** Quantity required by the work order. */
55
- quantityRequired?: number;
56
- }
57
- export interface FlattenedWorkOrderOperationFields {
58
- /** Work order identifier (repeated for each operation). */
59
- workOrderId?: string | number;
60
- /** Lot identifier for the operation row. */
61
- lot?: string | number;
62
- /** Split identifier for the operation row. */
63
- split?: string | number;
64
- /** Sub identifier for the operation row. */
65
- sub?: string | number;
66
- /** Sequence number for the specific operation. */
67
- sequenceNumber?: string | number;
68
- /** Resource or machine identifier for the operation. */
69
- resourceId?: string | number;
70
- /** Quantity started on the operation. */
71
- startQuantity?: number;
72
- /** Quantity finished on the operation. */
73
- finishQuantity?: number;
74
- /** Expected reject percentage (0-1). */
75
- expectedRejectRate?: number;
76
- /** Scheduled start datetime (ISO-8601 UTC). */
77
- opScheduledStartDate?: string;
78
- /** Scheduled finish datetime (ISO-8601 UTC). */
79
- opScheduledFinishDate?: string;
80
- /** Closed datetime for the operation (ISO-8601 UTC). */
81
- opClosedDate?: string;
82
- /** Production burden rate per hour. */
83
- productionburdenRateHourly?: number | string;
84
- /** Setup burden rate per hour. */
85
- setupburdenRatehourly?: number | string;
86
- /** Operation type (Production, Setup, etc.). */
87
- operationType?: string;
88
- /** Quantity per part for the operation. */
89
- quantityPerPart?: number;
90
- /** Operation status (defaults to work-order status when omitted). */
91
- opStatus?: string;
92
- }
93
- export type FlattenedWorkOrderRowBase = FlattenedWorkOrderPartFields & FlattenedWorkOrderPartOperationFields & FlattenedWorkOrderFields & FlattenedWorkOrderOperationFields;
94
- /**
95
- * Canonical flattened row that callers pass into
96
- * `StandardProcessDrivers.syncWorkOrderBatchFromFlattened`.
97
- */
98
- export type FlattenedWorkOrderRow = FlattenedWorkOrderRowBase;
99
- //# sourceMappingURL=flattened-work-order.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"flattened-work-order.d.ts","sourceRoot":"","sources":["../../src/types/flattened-work-order.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=flattened-work-order.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"flattened-work-order.js","sourceRoot":"","sources":["../../src/types/flattened-work-order.ts"],"names":[],"mappings":""}
@@ -1,8 +0,0 @@
1
- /**
2
- * This module contains utility functions to get environment variables with default values and validation.
3
- */
4
- export declare const getEnvString: (key: string, defaultValue?: string) => string;
5
- export declare const getEnvNumber: (key: string, defaultValue: number) => number;
6
- export declare const getEnvBoolean: (key: string, defaultValue?: boolean) => boolean;
7
- export declare const getOptionalEnv: (key: string) => string | undefined;
8
- //# sourceMappingURL=env.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"AAAA;;GAEG;AAyBH,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,EAAE,qBAAiB,KAAG,MAG7D,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,KAAK,MAAM,EACX,cAAc,MAAM,KACnB,MAaF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,EAAE,sBAAoB,KAAG,OAmBjE,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAErD,CAAC"}