@machinemetrics/mm-erp-sdk 0.1.9 → 0.2.0-beta.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.
Files changed (86) hide show
  1. package/dist/services/data-sync-service/configuration-manager.d.ts.map +1 -1
  2. package/dist/services/data-sync-service/configuration-manager.js +30 -30
  3. package/dist/services/data-sync-service/configuration-manager.js.map +1 -1
  4. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts.map +1 -1
  5. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +2 -1
  6. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js.map +1 -1
  7. package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
  8. package/dist/services/data-sync-service/jobs/from-erp.js +2 -1
  9. package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
  10. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts.map +1 -1
  11. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +2 -1
  12. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js.map +1 -1
  13. package/dist/services/data-sync-service/jobs/run-migrations.d.ts.map +1 -1
  14. package/dist/services/data-sync-service/jobs/run-migrations.js +2 -1
  15. package/dist/services/data-sync-service/jobs/run-migrations.js.map +1 -1
  16. package/dist/services/data-sync-service/jobs/to-erp.d.ts.map +1 -1
  17. package/dist/services/data-sync-service/jobs/to-erp.js +2 -1
  18. package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -1
  19. package/dist/services/mm-api-service/mm-api-service.js +1 -1
  20. package/dist/services/mm-api-service/mm-api-service.js.map +1 -1
  21. package/dist/services/reporting-service/logger.d.ts.map +1 -1
  22. package/dist/services/reporting-service/logger.js +5 -1
  23. package/dist/services/reporting-service/logger.js.map +1 -1
  24. package/dist/types/erp-connector.d.ts +8 -1
  25. package/dist/types/erp-connector.d.ts.map +1 -1
  26. package/dist/types/flattened-work-order.d.ts +99 -0
  27. package/dist/types/flattened-work-order.d.ts.map +1 -0
  28. package/dist/types/flattened-work-order.js +2 -0
  29. package/dist/types/flattened-work-order.js.map +1 -0
  30. package/dist/types/index.d.ts +1 -0
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/dist/utils/env.d.ts +8 -0
  33. package/dist/utils/env.d.ts.map +1 -0
  34. package/dist/utils/env.js +58 -0
  35. package/dist/utils/env.js.map +1 -0
  36. package/dist/utils/erp-timezone-utils.d.ts +20 -0
  37. package/dist/utils/erp-timezone-utils.d.ts.map +1 -0
  38. package/dist/utils/erp-timezone-utils.js +75 -0
  39. package/dist/utils/erp-timezone-utils.js.map +1 -0
  40. package/dist/utils/index.d.ts +1 -0
  41. package/dist/utils/index.d.ts.map +1 -1
  42. package/dist/utils/index.js +1 -0
  43. package/dist/utils/index.js.map +1 -1
  44. package/dist/utils/local-data-store/jobs-shared-data.d.ts +2 -0
  45. package/dist/utils/local-data-store/jobs-shared-data.d.ts.map +1 -1
  46. package/dist/utils/local-data-store/jobs-shared-data.js +2 -0
  47. package/dist/utils/local-data-store/jobs-shared-data.js.map +1 -1
  48. package/dist/utils/mm-labor-ticket-helpers.d.ts +3 -4
  49. package/dist/utils/mm-labor-ticket-helpers.d.ts.map +1 -1
  50. package/dist/utils/mm-labor-ticket-helpers.js +12 -7
  51. package/dist/utils/mm-labor-ticket-helpers.js.map +1 -1
  52. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.d.ts.map +1 -1
  53. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.js +13 -4
  54. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.js.map +1 -1
  55. package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts +7 -1
  56. package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts.map +1 -1
  57. package/dist/utils/standard-process-drivers/mm-entity-processor.js +7 -1
  58. package/dist/utils/standard-process-drivers/mm-entity-processor.js.map +1 -1
  59. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts +8 -2
  60. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts.map +1 -1
  61. package/dist/utils/standard-process-drivers/standard-process-drivers.js +27 -18
  62. package/dist/utils/standard-process-drivers/standard-process-drivers.js.map +1 -1
  63. package/dist/utils/time-utils.d.ts.map +1 -1
  64. package/dist/utils/time-utils.js +7 -0
  65. package/dist/utils/time-utils.js.map +1 -1
  66. package/package.json +3 -1
  67. package/src/services/data-sync-service/configuration-manager.ts +50 -37
  68. package/src/services/data-sync-service/jobs/clean-up-expired-cache.ts +2 -1
  69. package/src/services/data-sync-service/jobs/from-erp.ts +2 -1
  70. package/src/services/data-sync-service/jobs/retry-failed-labor-tickets.ts +2 -1
  71. package/src/services/data-sync-service/jobs/run-migrations.ts +2 -1
  72. package/src/services/data-sync-service/jobs/to-erp.ts +2 -1
  73. package/src/services/mm-api-service/mm-api-service.ts +1 -1
  74. package/src/services/reporting-service/logger.ts +6 -1
  75. package/src/types/erp-connector.ts +8 -1
  76. package/src/types/flattened-work-order.ts +108 -0
  77. package/src/types/index.ts +8 -0
  78. package/src/utils/env.ts +75 -0
  79. package/src/utils/erp-timezone-utils.ts +99 -0
  80. package/src/utils/index.ts +5 -0
  81. package/src/utils/local-data-store/jobs-shared-data.ts +2 -0
  82. package/src/utils/mm-labor-ticket-helpers.ts +11 -8
  83. package/src/utils/standard-process-drivers/labor-ticket-erp-synchronizer.ts +11 -6
  84. package/src/utils/standard-process-drivers/mm-entity-processor.ts +7 -1
  85. package/src/utils/standard-process-drivers/standard-process-drivers.ts +33 -19
  86. package/src/utils/time-utils.ts +11 -0
@@ -0,0 +1,75 @@
1
+ /**
2
+ * This module contains utility functions to get environment variables with default values and validation.
3
+ */
4
+
5
+ const normalizeValue = (value?: string | null): string | undefined => {
6
+ if (value === undefined || value === null) {
7
+ return undefined;
8
+ }
9
+
10
+ const trimmed = value.trim();
11
+ if (!trimmed) {
12
+ return undefined;
13
+ }
14
+
15
+ const lowered = trimmed.toLowerCase();
16
+ if (lowered === "undefined" || lowered === "null") {
17
+ return undefined;
18
+ }
19
+
20
+ return trimmed;
21
+ };
22
+
23
+ const warnInvalid = (key: string, rawValue: string | undefined, reason: string) => {
24
+ const received = rawValue !== undefined ? `; received "${rawValue}"` : "";
25
+ console.warn(`[config] ${key} ${reason}${received}`); // eslint-disable-line no-console
26
+ };
27
+
28
+ export const getEnvString = (key: string, defaultValue = ""): string => {
29
+ const normalized = normalizeValue(process.env[key]);
30
+ return normalized ?? defaultValue;
31
+ };
32
+
33
+ export const getEnvNumber = (
34
+ key: string,
35
+ defaultValue: number
36
+ ): number => {
37
+ const normalized = normalizeValue(process.env[key]);
38
+ if (normalized === undefined) {
39
+ return defaultValue;
40
+ }
41
+
42
+ const parsed = Number.parseInt(normalized, 10);
43
+ if (Number.isNaN(parsed)) {
44
+ warnInvalid(key, normalized, `is not a valid number for key ${key}; falling back to default ${defaultValue}`);
45
+ return defaultValue;
46
+ }
47
+
48
+ return parsed;
49
+ };
50
+
51
+ export const getEnvBoolean = (key: string, defaultValue = false): boolean => {
52
+ const normalized = normalizeValue(process.env[key]);
53
+ if (normalized === undefined) {
54
+ return defaultValue;
55
+ }
56
+
57
+ const truthyValues = new Set(["true", "1", "yes", "y", "on"]);
58
+ const falsyValues = new Set(["false", "0", "no", "n", "off"]);
59
+ const lowered = normalized.toLowerCase();
60
+
61
+ if (truthyValues.has(lowered)) {
62
+ return true;
63
+ }
64
+ if (falsyValues.has(lowered)) {
65
+ return false;
66
+ }
67
+
68
+ warnInvalid(key, normalized, "is not a recognized boolean; falling back to default");
69
+ return defaultValue;
70
+ };
71
+
72
+ export const getOptionalEnv = (key: string): string | undefined => {
73
+ return normalizeValue(process.env[key]);
74
+ };
75
+
@@ -0,0 +1,99 @@
1
+ import { DateTime } from "luxon";
2
+ import logger from "../services/reporting-service/logger.js";
3
+ import { getCachedTimezoneName } from "./local-data-store/jobs-shared-data.js";
4
+
5
+ /**
6
+ * Retrieves the ERP timezone name that should have been cached during initialization.
7
+ * @throws Error when the timezone cache has not been populated.
8
+ */
9
+ export function getERPTimezone(): string {
10
+ const timezone = getCachedTimezoneName();
11
+ if (!timezone) {
12
+ throw new Error(
13
+ "ERP timezone is not cached. Run getTimezoneOffsetAndPersist() to populate it."
14
+ );
15
+ }
16
+ return timezone;
17
+ }
18
+
19
+ /**
20
+ * Converts a UTC ISO-8601 datetime string to the ERP's local timezone (also ISO-8601).
21
+ * @param utcDateTime ISO-8601 datetime string representing UTC time.
22
+ * @param timezone Optional timezone override; defaults to the cached ERP timezone.
23
+ * @returns ISO-8601 string in the ERP timezone.
24
+ */
25
+ export function convertUtcDateTimeToErpLocal(
26
+ utcDateTime: string,
27
+ timezone?: string
28
+ ): string {
29
+ if (!utcDateTime) {
30
+ throw new Error("convertUtcDateTimeToErpLocal requires a non-empty UTC datetime string");
31
+ }
32
+ try {
33
+ const targetZone = timezone ?? getERPTimezone();
34
+ const parsedUtc = DateTime.fromISO(utcDateTime, { zone: "utc" });
35
+ if (!parsedUtc.isValid) {
36
+ throw new Error(
37
+ `Invalid UTC datetime "${utcDateTime}": ${parsedUtc.invalidReason}`
38
+ );
39
+ }
40
+
41
+ const localized = parsedUtc.setZone(targetZone);
42
+ if (!localized.isValid) {
43
+ throw new Error(
44
+ `Unable to convert UTC datetime "${utcDateTime}" to target zone "${targetZone}": ${localized.invalidReason}`
45
+ );
46
+ }
47
+
48
+ return localized.toISO();
49
+ } catch (error) {
50
+ logger.error("convertUtcDateTimeToErpLocal: Failed to convert datetime", {
51
+ utcDateTime,
52
+ error,
53
+ });
54
+ throw new Error(
55
+ "Failed to convert UTC datetime to ERP local timezone. See logs for details."
56
+ );
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Converts an ERP-local ISO-8601 datetime string to UTC (still ISO-8601).
62
+ * @param localDateTime ISO-8601 datetime string representing ERP local time.
63
+ * @param timezone Optional timezone override; defaults to the cached ERP timezone.
64
+ * @returns ISO-8601 string in UTC.
65
+ */
66
+ export function convertErpLocalDateTimeToUtc(
67
+ localDateTime: string,
68
+ timezone?: string
69
+ ): string {
70
+ if (!localDateTime) {
71
+ throw new Error("convertErpLocalDateTimeToUtc requires a non-empty local datetime string");
72
+ }
73
+ try {
74
+ const sourceZone = timezone ?? getERPTimezone();
75
+ const parsedLocal = DateTime.fromISO(localDateTime, { zone: sourceZone });
76
+ if (!parsedLocal.isValid) {
77
+ throw new Error(
78
+ `Invalid ERP local datetime "${localDateTime}" for zone "${sourceZone}": ${parsedLocal.invalidReason}`
79
+ );
80
+ }
81
+
82
+ const utcDateTime = parsedLocal.setZone("utc");
83
+ if (!utcDateTime.isValid) {
84
+ throw new Error(
85
+ `Unable to convert ERP local datetime "${localDateTime}" to UTC: ${utcDateTime.invalidReason}`
86
+ );
87
+ }
88
+
89
+ return utcDateTime.toISO();
90
+ } catch (error) {
91
+ logger.error("convertErpLocalDateTimeToUtc: Failed to convert datetime", {
92
+ localDateTime,
93
+ error,
94
+ });
95
+ throw new Error(
96
+ "Failed to convert ERP local datetime to UTC. See logs for details."
97
+ );
98
+ }
99
+ }
@@ -17,6 +17,11 @@ export {
17
17
  export { getTimezoneOffsetAndPersist } from "./time-utils.js";
18
18
  export { formatDateWithTZOffset, convertToLocalTime, toISOWithOffset } from "./timezone.js";
19
19
  export { applyTimezoneOffsetsToFields } from "./time-utils.js";
20
+ export {
21
+ convertUtcDateTimeToErpLocal,
22
+ convertErpLocalDateTimeToUtc,
23
+ getERPTimezone,
24
+ } from "./erp-timezone-utils.js";
20
25
  export * from "./time-utils.js";
21
26
 
22
27
  /**
@@ -72,6 +72,8 @@ export const setInitialLoadComplete = (complete: boolean): void => {
72
72
  };
73
73
 
74
74
  /**
75
+ * @deprecated The cached numeric offset cannot account for DST changes.
76
+ * Prefer using the cached timezone name with Luxon helpers for conversions.
75
77
  * Gets the company's cached current timezone offset (e.g., -5)
76
78
  * @returns The cached timezone offset or 0 if not found
77
79
  */
@@ -1,15 +1,13 @@
1
- import { convertToLocalTime, toISOWithOffset } from "./timezone.js";
2
1
  import { MMReceiveLaborTicket } from "../services/mm-api-service/types/receive-types.js";
2
+ import { convertUtcDateTimeToErpLocal, getERPTimezone } from "./erp-timezone-utils.js";
3
3
 
4
4
  /**
5
- * Apply timezone offsets to datetime fields specifically for the MMApiReceiveLaborTicket object from the Zulu-based MM API
6
- * (presumably) before sending to the ERP
5
+ * Converts key datetime fields from UTC (MachineMetrics API) to the ERP's local timezone.
6
+ * All inputs/outputs are ISO-8601 strings, and invalid inputs throw errors.
7
7
  * @param laborTicket The MMApiReceiveLaborTicket object to convert
8
- * @param timezoneOffset The timezone offset to apply
9
8
  */
10
9
  export function convertLaborTicketToLocalTimezone(
11
- laborTicket: MMReceiveLaborTicket,
12
- timezoneOffset: number
10
+ laborTicket: MMReceiveLaborTicket
13
11
  ): MMReceiveLaborTicket {
14
12
  const timeFields = [
15
13
  "clockIn",
@@ -20,9 +18,14 @@ export function convertLaborTicketToLocalTimezone(
20
18
  "workOrderOperationClosedDate",
21
19
  ] as const;
22
20
 
21
+ const timezone = getERPTimezone();
23
22
  timeFields.forEach((field) => {
24
- const localTime = convertToLocalTime(laborTicket[field], timezoneOffset);
25
- laborTicket[field] = localTime ? toISOWithOffset(localTime, timezoneOffset) : null;
23
+ const value = laborTicket[field];
24
+ if (value) {
25
+ laborTicket[field] = convertUtcDateTimeToErpLocal(value, timezone);
26
+ } else {
27
+ laborTicket[field] = null;
28
+ }
26
29
  });
27
30
  return laborTicket;
28
31
  }
@@ -2,7 +2,6 @@ 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";
6
5
  import logger from "../../services/reporting-service/logger.js";
7
6
 
8
7
  /**
@@ -54,9 +53,12 @@ export class LaborTicketERPSynchronizer {
54
53
 
55
54
  // Find the most recent updatedAt timestamp from labor tickets. This will be used to update
56
55
  // the checkpoint to ensure there is no gap of time for the next sync.
56
+ // Consider, for instance, a time offset between an edge and the cloud, or that
57
+ // a DST change may have occurred between the last checkpoint and Now.
57
58
  const mostRecentUpdate = laborTicketsUpdates.reduce(
58
59
  (latest: string | null, ticket: MMReceiveLaborTicket) => {
59
- if (!latest || !ticket.updatedAt) return latest;
60
+ if (!ticket.updatedAt) return latest; // Skip tickets without updatedAt
61
+ if (!latest) return ticket.updatedAt; // Initialize on first valid value
60
62
  return new Date(ticket.updatedAt) > new Date(latest)
61
63
  ? ticket.updatedAt
62
64
  : latest;
@@ -118,6 +120,9 @@ export class LaborTicketERPSynchronizer {
118
120
  timestamp: mostRecentUpdate || fallbackTimestamp,
119
121
  },
120
122
  });
123
+ logger.info("syncLaborTicketsToERP: Checkpoint saved:", {
124
+ checkpointTimestamp: (mostRecentUpdate || fallbackTimestamp)
125
+ });
121
126
  } catch (error) {
122
127
  logger.error("syncLaborTicketsToERP: Error:", error);
123
128
  }
@@ -214,10 +219,10 @@ export class LaborTicketERPSynchronizer {
214
219
  ): Promise<MMReceiveLaborTicket> {
215
220
  let laborTicketResult: MMReceiveLaborTicket;
216
221
 
217
- laborTicketResult = convertLaborTicketToLocalTimezone(
218
- laborTicket,
219
- getCachedTimezoneOffset()
220
- );
222
+ // Convert MM's UTC timestamps into the ERP's local timezone before invoking connector code.
223
+ // Connector implementations should treat the incoming values as localized wall time and avoid
224
+ // applying any further timezone shifts.
225
+ laborTicketResult = convertLaborTicketToLocalTimezone(laborTicket);
221
226
 
222
227
  logger.info(
223
228
  `processing laborTicket, id=${laborTicket.laborTicketId}, ref=${laborTicket.laborTicketRef}`
@@ -29,7 +29,13 @@ 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
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.
33
39
  */
34
40
  static async writeEntities(
35
41
  entityType: ERPObjType,
@@ -1,5 +1,6 @@
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";
3
4
  import { BatchCacheManager } from "../../services/caching-service/batch-cache-manager.js";
4
5
  import {
5
6
  IToRESTApiObject,
@@ -248,7 +249,12 @@ export class StandardProcessDrivers {
248
249
  * parts, part operations, work orders, and work order operations, then process them in the correct order
249
250
  * to maintain referential integrity.
250
251
  *
251
- * @param flattenedData Array of flattened rows containing both work order and operation data (camelCase fields)
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`)
252
258
  * @param batchCacheManager The batch cache manager instance; pass in null if caching is not desired
253
259
  *
254
260
  * @returns Combined results from all entity processing with detailed logging information
@@ -256,7 +262,7 @@ export class StandardProcessDrivers {
256
262
  * @throws Error on other underlying issues (network, authentication, etc.)
257
263
  */
258
264
  static async syncWorkOrderBatchFromFlattened(
259
- flattenedData: any[],
265
+ flattenedData: ReadonlyArray<FlattenedWorkOrderRow>,
260
266
  batchCacheManager: BatchCacheManager | null
261
267
  ): Promise<{
262
268
  parts: WriteEntitiesToMMResult;
@@ -268,6 +274,11 @@ export class StandardProcessDrivers {
268
274
  throw new Error("No flattened work order data provided");
269
275
  }
270
276
 
277
+ const toStringOrFallback = (
278
+ value: string | number | null | undefined,
279
+ fallback = ""
280
+ ): string => (value === undefined || value === null ? fallback : String(value));
281
+
271
282
  // Process the flattened data - each row contains both work order and operation info
272
283
  const uniqueParts = new Map();
273
284
  const uniquePartOperations = new Map();
@@ -297,8 +308,8 @@ export class StandardProcessDrivers {
297
308
  resourceId: row.resourceId, // → resourceId
298
309
  cycleTimeMs: row.cycleTimeMs, // → cycleTimeMs
299
310
  setupTimeMs: row.setupTimeMs, // → setupTimeMs
300
- description: row.operationDescription || "", // → description
301
- quantityPerPart: row.quantityPerPart || 1, // → quantityPerPart
311
+ operationDescription: row.operationDescription, // → description
312
+ quantityPerPart: row.quantityPerPart ?? 1, // → quantityPerPart
302
313
  });
303
314
  }
304
315
 
@@ -351,8 +362,8 @@ export class StandardProcessDrivers {
351
362
  const parts = Array.from(uniqueParts.values()).map(
352
363
  (item) =>
353
364
  new MMSendPart(
354
- item.partNumber || "", // partNumber
355
- item.partRevision || "", // partRevision
365
+ toStringOrFallback(item.partNumber), // partNumber
366
+ toStringOrFallback(item.partRevision), // partRevision
356
367
  item.method || "Standard" // method
357
368
  )
358
369
  );
@@ -360,14 +371,14 @@ export class StandardProcessDrivers {
360
371
  const partOperations = Array.from(uniquePartOperations.values()).map(
361
372
  (item) =>
362
373
  new MMSendPartOperation(
363
- item.partNumber || "", // partNumber
364
- item.partRevision || "", // partRevision
374
+ toStringOrFallback(item.partNumber), // partNumber
375
+ toStringOrFallback(item.partRevision), // partRevision
365
376
  item.method || "Standard", // method
366
377
  item.sequenceNumber?.toString() || "", // sequenceNumber
367
378
  item.resourceId?.toString() || "", // resourceId
368
379
  item.cycleTimeMs || 0, // cycleTimeMs
369
380
  item.setupTimeMs || 0, // setupTimeMs
370
- item.description || "", // description
381
+ item.operationDescription || "", // description
371
382
  item.quantityPerPart || 1 // quantityPerPart
372
383
  )
373
384
  );
@@ -376,10 +387,12 @@ export class StandardProcessDrivers {
376
387
  (item) =>
377
388
  new MMSendWorkOrder(
378
389
  item.workOrderId?.toString() || "", // workOrderId
379
- item.lot || "", // lot
380
- item.split || "", // split
381
- item.sub || "", // sub
390
+ toStringOrFallback(item.lot), // lot
391
+ toStringOrFallback(item.split), // split
392
+ toStringOrFallback(item.sub), // sub
382
393
  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.
383
396
  item.dueDate ? new Date(item.dueDate).toISOString() : null, // dueDate
384
397
  item.description || "", // description
385
398
  item.scheduledStartDate
@@ -390,8 +403,8 @@ export class StandardProcessDrivers {
390
403
  : null, // scheduledEndDate
391
404
  item.closedDate ? new Date(item.closedDate).toISOString() : null, // closedDate
392
405
  item.quantityRequired || 0, // quantityRequired
393
- item.partNumber || "", // partNumber
394
- item.partRevision || "", // partRevision
406
+ toStringOrFallback(item.partNumber), // partNumber
407
+ toStringOrFallback(item.partRevision), // partRevision
395
408
  item.method || "Standard" // method
396
409
  )
397
410
  );
@@ -400,14 +413,15 @@ export class StandardProcessDrivers {
400
413
  (item) =>
401
414
  new MMSendWorkOrderOperation(
402
415
  item.workOrderId?.toString() || "", // workOrderId
403
- item.lot || "", // lot
404
- item.split || "", // split
405
- item.sub || "", // sub
416
+ toStringOrFallback(item.lot), // lot
417
+ toStringOrFallback(item.split), // split
418
+ toStringOrFallback(item.sub), // sub
406
419
  item.sequenceNumber?.toString() || "", // sequenceNumber
407
420
  item.resourceId?.toString() || "", // resourceId
408
421
  item.startQuantity || 0, // startQuantity
409
422
  item.finishQuantity || 0, // finishQuantity
410
423
  item.expectedRejectRate || 0, // expectedRejectRate
424
+ // Same UTC expectation as above; reserialize to ensure valid ISO.
411
425
  item.scheduledStartDate
412
426
  ? new Date(item.scheduledStartDate).toISOString()
413
427
  : null, // scheduledStartDate
@@ -417,8 +431,8 @@ export class StandardProcessDrivers {
417
431
  item.closedDate ? new Date(item.closedDate).toISOString() : null, // closedDate
418
432
  item.cycleTimeMs || 0, // cycleTimeMs
419
433
  item.setupTimeMs || 0, // setupTimeMs
420
- parseFloat(item.productionburdenRateHourly || "0"), // productionburdenRateHourly
421
- parseFloat(item.setupburdenRatehourly || "0"), // setupburdenRatehourly
434
+ parseFloat(toStringOrFallback(item.productionburdenRateHourly, "0")), // productionburdenRateHourly
435
+ parseFloat(toStringOrFallback(item.setupburdenRatehourly, "0")), // setupburdenRatehourly
422
436
  item.operationType || "Production", // operationType
423
437
  item.quantityPerPart || 1, // quantityPerPart
424
438
  item.status || "Open" // status
@@ -1,3 +1,4 @@
1
+ import { DateTime } from 'luxon';
1
2
  import logger from "../services/reporting-service/logger.js";
2
3
  import {
3
4
  setTimezoneOffsetInCache,
@@ -117,6 +118,16 @@ export const getTimezoneOffsetAndPersist = async (
117
118
  const { offset, timezone } = await getTimezoneOffset();
118
119
  logger.info(`Timezone offset: ${offset} hours, timezone: ${timezone}`);
119
120
  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
+
120
131
  setTimezoneNameInCache(timezone);
121
132
  success = true;
122
133
  } catch (error) {