@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.
- package/dist/services/data-sync-service/configuration-manager.d.ts.map +1 -1
- package/dist/services/data-sync-service/configuration-manager.js +30 -30
- package/dist/services/data-sync-service/configuration-manager.js.map +1 -1
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +2 -1
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js.map +1 -1
- package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/from-erp.js +2 -1
- package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +2 -1
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js.map +1 -1
- package/dist/services/data-sync-service/jobs/run-migrations.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/run-migrations.js +2 -1
- package/dist/services/data-sync-service/jobs/run-migrations.js.map +1 -1
- package/dist/services/data-sync-service/jobs/to-erp.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/to-erp.js +2 -1
- package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -1
- package/dist/services/mm-api-service/mm-api-service.js +1 -1
- package/dist/services/mm-api-service/mm-api-service.js.map +1 -1
- package/dist/services/reporting-service/logger.d.ts.map +1 -1
- package/dist/services/reporting-service/logger.js +5 -1
- package/dist/services/reporting-service/logger.js.map +1 -1
- package/dist/types/erp-connector.d.ts +8 -1
- package/dist/types/erp-connector.d.ts.map +1 -1
- package/dist/types/flattened-work-order.d.ts +99 -0
- package/dist/types/flattened-work-order.d.ts.map +1 -0
- package/dist/types/flattened-work-order.js +2 -0
- package/dist/types/flattened-work-order.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/env.d.ts +8 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +58 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/erp-timezone-utils.d.ts +20 -0
- package/dist/utils/erp-timezone-utils.d.ts.map +1 -0
- package/dist/utils/erp-timezone-utils.js +75 -0
- package/dist/utils/erp-timezone-utils.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/local-data-store/jobs-shared-data.d.ts +2 -0
- package/dist/utils/local-data-store/jobs-shared-data.d.ts.map +1 -1
- package/dist/utils/local-data-store/jobs-shared-data.js +2 -0
- package/dist/utils/local-data-store/jobs-shared-data.js.map +1 -1
- package/dist/utils/mm-labor-ticket-helpers.d.ts +3 -4
- package/dist/utils/mm-labor-ticket-helpers.d.ts.map +1 -1
- package/dist/utils/mm-labor-ticket-helpers.js +12 -7
- package/dist/utils/mm-labor-ticket-helpers.js.map +1 -1
- package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.d.ts.map +1 -1
- package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.js +13 -4
- package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.js.map +1 -1
- package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts +7 -1
- package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts.map +1 -1
- package/dist/utils/standard-process-drivers/mm-entity-processor.js +7 -1
- package/dist/utils/standard-process-drivers/mm-entity-processor.js.map +1 -1
- package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts +8 -2
- package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts.map +1 -1
- package/dist/utils/standard-process-drivers/standard-process-drivers.js +27 -18
- package/dist/utils/standard-process-drivers/standard-process-drivers.js.map +1 -1
- package/dist/utils/time-utils.d.ts.map +1 -1
- package/dist/utils/time-utils.js +7 -0
- package/dist/utils/time-utils.js.map +1 -1
- package/package.json +3 -1
- package/src/services/data-sync-service/configuration-manager.ts +50 -37
- package/src/services/data-sync-service/jobs/clean-up-expired-cache.ts +2 -1
- package/src/services/data-sync-service/jobs/from-erp.ts +2 -1
- package/src/services/data-sync-service/jobs/retry-failed-labor-tickets.ts +2 -1
- package/src/services/data-sync-service/jobs/run-migrations.ts +2 -1
- package/src/services/data-sync-service/jobs/to-erp.ts +2 -1
- package/src/services/mm-api-service/mm-api-service.ts +1 -1
- package/src/services/reporting-service/logger.ts +6 -1
- package/src/types/erp-connector.ts +8 -1
- package/src/types/flattened-work-order.ts +108 -0
- package/src/types/index.ts +8 -0
- package/src/utils/env.ts +75 -0
- package/src/utils/erp-timezone-utils.ts +99 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/local-data-store/jobs-shared-data.ts +2 -0
- package/src/utils/mm-labor-ticket-helpers.ts +11 -8
- package/src/utils/standard-process-drivers/labor-ticket-erp-synchronizer.ts +11 -6
- package/src/utils/standard-process-drivers/mm-entity-processor.ts +7 -1
- package/src/utils/standard-process-drivers/standard-process-drivers.ts +33 -19
- package/src/utils/time-utils.ts +11 -0
package/src/utils/env.ts
ADDED
|
@@ -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
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
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
|
|
25
|
-
|
|
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 (!
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
|
|
301
|
-
quantityPerPart: row.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
|
|
355
|
-
item.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
|
|
364
|
-
item.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.
|
|
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
|
|
380
|
-
item.split
|
|
381
|
-
item.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
|
|
394
|
-
item.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
|
|
404
|
-
item.split
|
|
405
|
-
item.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
|
|
421
|
-
parseFloat(item.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
|
package/src/utils/time-utils.ts
CHANGED
|
@@ -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) {
|