@machinemetrics/mm-erp-sdk 0.1.8 → 0.1.9-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.
- package/dist/{config-cB7h4yvc.js → config-CvA-mFWF.js} +20 -20
- package/dist/{config-cB7h4yvc.js.map → config-CvA-mFWF.js.map} +1 -1
- package/dist/{connector-factory-CKm74_WZ.js → connector-factory-BPm2GVVF.js} +2 -2
- package/dist/{connector-factory-CKm74_WZ.js.map → connector-factory-BPm2GVVF.js.map} +1 -1
- package/dist/{hashed-cache-manager-Ds-HksA0.js → hashed-cache-manager-B15NN8hK.js} +5 -5
- package/dist/{hashed-cache-manager-Ds-HksA0.js.map → hashed-cache-manager-B15NN8hK.js.map} +1 -1
- package/dist/{index-DTtmv8Iq.js → index-D8qO1NyK.js} +2 -2
- package/dist/{index-DTtmv8Iq.js.map → index-D8qO1NyK.js.map} +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/{knexfile-1qKKIORB.js → knexfile-Bng2Ru9c.js} +3 -3
- package/dist/{knexfile-1qKKIORB.js.map → knexfile-Bng2Ru9c.js.map} +1 -1
- package/dist/{logger-CBDNtsMq.js → logger-BWw0_z9q.js} +328 -303
- package/dist/logger-BWw0_z9q.js.map +1 -0
- package/dist/mm-erp-sdk.js +717 -8
- package/dist/mm-erp-sdk.js.map +1 -1
- package/dist/services/data-sync-service/data-sync-service.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +4 -4
- package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/from-erp.js +5 -12
- package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +3 -3
- package/dist/services/data-sync-service/jobs/run-migrations.js +2 -2
- package/dist/services/data-sync-service/jobs/to-erp.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/to-erp.js +7 -4
- package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -1
- package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts +30 -0
- package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts.map +1 -0
- package/dist/services/mm-api-service/company-info.d.ts +13 -0
- package/dist/services/mm-api-service/company-info.d.ts.map +1 -0
- package/dist/services/mm-api-service/index.d.ts +7 -0
- package/dist/services/mm-api-service/index.d.ts.map +1 -1
- package/dist/services/mm-api-service/mm-api-service.d.ts +6 -0
- package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
- package/dist/services/nats-service/nats-service.d.ts +114 -0
- package/dist/services/nats-service/nats-service.d.ts.map +1 -0
- package/dist/services/nats-service/test-nats-subscriber.d.ts +6 -0
- package/dist/services/nats-service/test-nats-subscriber.d.ts.map +1 -0
- package/dist/services/reporting-service/logger.d.ts.map +1 -1
- package/dist/utils/error-formatter.d.ts +19 -0
- package/dist/utils/error-formatter.d.ts.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/index.ts +3 -0
- package/src/services/data-sync-service/data-sync-service.ts +10 -0
- package/src/services/data-sync-service/jobs/from-erp.ts +2 -7
- package/src/services/data-sync-service/jobs/to-erp.ts +5 -1
- package/src/services/data-sync-service/nats-labor-ticket-listener.ts +341 -0
- package/src/services/mm-api-service/company-info.ts +87 -0
- package/src/services/mm-api-service/index.ts +8 -0
- package/src/services/mm-api-service/mm-api-service.ts +19 -2
- package/src/services/nats-service/nats-service.ts +351 -0
- package/src/services/nats-service/test-nats-subscriber.ts +96 -0
- package/src/services/reporting-service/logger.ts +38 -1
- package/src/utils/error-formatter.ts +205 -0
- package/src/utils/index.ts +6 -0
- package/dist/logger-CBDNtsMq.js.map +0 -1
package/dist/mm-erp-sdk.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { C as CoreConfiguration, H as HashedCacheManager } from "./hashed-cache-manager-
|
|
2
|
-
import { E, g, a } from "./hashed-cache-manager-
|
|
3
|
-
import { l as logger } from "./logger-
|
|
4
|
-
import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as setTimezoneNameInCache, c as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-
|
|
5
|
-
import { f, d, e } from "./index-
|
|
1
|
+
import { C as CoreConfiguration, H as HashedCacheManager } from "./hashed-cache-manager-B15NN8hK.js";
|
|
2
|
+
import { E, g, a } from "./hashed-cache-manager-B15NN8hK.js";
|
|
3
|
+
import { l as logger } from "./logger-BWw0_z9q.js";
|
|
4
|
+
import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as setTimezoneNameInCache, c as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-D8qO1NyK.js";
|
|
5
|
+
import { f, d, e } from "./index-D8qO1NyK.js";
|
|
6
6
|
import axios, { AxiosError } from "axios";
|
|
7
7
|
import knex from "knex";
|
|
8
|
-
import { c as config } from "./knexfile-
|
|
8
|
+
import { c as config } from "./knexfile-Bng2Ru9c.js";
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import "./connector-factory-
|
|
11
|
+
import { c as createConnectorFromPath } from "./connector-factory-BPm2GVVF.js";
|
|
12
12
|
import Bree from "bree";
|
|
13
13
|
import Graceful from "@ladjs/graceful";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
|
+
import { StringCodec, connect } from "nats";
|
|
15
16
|
import sql from "mssql";
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
var ERPObjType = /* @__PURE__ */ ((ERPObjType2) => {
|
|
@@ -25,6 +26,10 @@ var ERPObjType = /* @__PURE__ */ ((ERPObjType2) => {
|
|
|
25
26
|
ERPObjType2[ERPObjType2["LABOR_TICKETS"] = 7] = "LABOR_TICKETS";
|
|
26
27
|
return ERPObjType2;
|
|
27
28
|
})(ERPObjType || {});
|
|
29
|
+
const erpTypes = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
30
|
+
__proto__: null,
|
|
31
|
+
ERPObjType
|
|
32
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
28
33
|
var Operator = /* @__PURE__ */ ((Operator2) => {
|
|
29
34
|
Operator2["eq"] = "eq";
|
|
30
35
|
Operator2["lt"] = "lt";
|
|
@@ -481,7 +486,7 @@ class MMApiClient {
|
|
|
481
486
|
const hasStatus = (err) => {
|
|
482
487
|
return typeof err === "object" && err !== null && "status" in err;
|
|
483
488
|
};
|
|
484
|
-
const isAuthError = hasStatus(error) && (error.status === 401 || error.status === 403);
|
|
489
|
+
const isAuthError = hasStatus(error) && (error.status === 401 || error.status === 403) || hasStatus(error) && error.status === 500 && typeof error.data?.error === "string" && error.data.error.includes("JWT");
|
|
485
490
|
if (isAuthError && !options.token) {
|
|
486
491
|
logger.info("Retrying request with fresh token due to auth error");
|
|
487
492
|
this.tokenMgr.invalidateToken();
|
|
@@ -805,6 +810,17 @@ class MMApiClient {
|
|
|
805
810
|
);
|
|
806
811
|
return updates.data.map((ticket) => new MMReceiveLaborTicket(ticket));
|
|
807
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* Fetch a single labor ticket by reference from the MM API
|
|
815
|
+
* @param laborTicketRef The labor ticket reference to fetch
|
|
816
|
+
* @returns Promise with the labor ticket data
|
|
817
|
+
*/
|
|
818
|
+
async fetchLaborTicketByRef(laborTicketRef) {
|
|
819
|
+
const response = await this.getData(
|
|
820
|
+
`${this.resourceURLs[ERPObjType.LABOR_TICKETS]}/${laborTicketRef}`
|
|
821
|
+
);
|
|
822
|
+
return new MMReceiveLaborTicket(response.data);
|
|
823
|
+
}
|
|
808
824
|
/**
|
|
809
825
|
* Fetch a checkpoint for a specific system, table, and checkpoint type
|
|
810
826
|
* @param checkpoint The checkpoint to fetch
|
|
@@ -1318,6 +1334,65 @@ class MMSendLaborTicket {
|
|
|
1318
1334
|
);
|
|
1319
1335
|
}
|
|
1320
1336
|
}
|
|
1337
|
+
let companyInfoCache = null;
|
|
1338
|
+
const getCompanyInfo = async () => {
|
|
1339
|
+
if (companyInfoCache) {
|
|
1340
|
+
return companyInfoCache;
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
const config2 = CoreConfiguration.inst();
|
|
1344
|
+
const apiUrl = config2.mmApiBaseUrl;
|
|
1345
|
+
const authToken = config2.mmApiAuthToken;
|
|
1346
|
+
if (!apiUrl || !authToken) {
|
|
1347
|
+
throw new Error("Missing required configuration for company info fetch");
|
|
1348
|
+
}
|
|
1349
|
+
const client = HTTPClientFactory.getInstance({
|
|
1350
|
+
baseUrl: apiUrl,
|
|
1351
|
+
retryAttempts: config2.mmApiRetryAttempts
|
|
1352
|
+
});
|
|
1353
|
+
const response = await client.request({
|
|
1354
|
+
url: "/accounts/current?includeLocation=true",
|
|
1355
|
+
method: "GET",
|
|
1356
|
+
headers: {
|
|
1357
|
+
Authorization: `Bearer ${authToken}`
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
const userInfo = response.data;
|
|
1361
|
+
if (!userInfo?.company) {
|
|
1362
|
+
throw new Error("Unable to retrieve company information from API");
|
|
1363
|
+
}
|
|
1364
|
+
logger.info("Fetched company info from /accounts/current", {
|
|
1365
|
+
locationRef: userInfo.locationRef,
|
|
1366
|
+
companyId: userInfo.company.id,
|
|
1367
|
+
timezone: userInfo.company.timezone
|
|
1368
|
+
});
|
|
1369
|
+
companyInfoCache = {
|
|
1370
|
+
timezone: userInfo.company.timezone,
|
|
1371
|
+
locationRef: String(userInfo.locationRef),
|
|
1372
|
+
// Convert number to string
|
|
1373
|
+
companyId: userInfo.company.id
|
|
1374
|
+
};
|
|
1375
|
+
return companyInfoCache;
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
throw new Error(
|
|
1378
|
+
`Failed to get company info: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1383
|
+
__proto__: null,
|
|
1384
|
+
MMApiClient,
|
|
1385
|
+
MMReceiveLaborTicket,
|
|
1386
|
+
MMSendLaborTicket,
|
|
1387
|
+
MMSendPart,
|
|
1388
|
+
MMSendPartOperation,
|
|
1389
|
+
MMSendPerson,
|
|
1390
|
+
MMSendReason,
|
|
1391
|
+
MMSendResource,
|
|
1392
|
+
MMSendWorkOrder,
|
|
1393
|
+
MMSendWorkOrderOperation,
|
|
1394
|
+
getCompanyInfo
|
|
1395
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1321
1396
|
function getUniqueRows(data, fields, sortFields) {
|
|
1322
1397
|
const createCompositeKey = (item) => {
|
|
1323
1398
|
return fields.map((field) => String(item[field])).join("|");
|
|
@@ -2498,6 +2573,10 @@ class MMEntityProcessor {
|
|
|
2498
2573
|
}
|
|
2499
2574
|
}
|
|
2500
2575
|
}
|
|
2576
|
+
const mmEntityProcessor = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
2577
|
+
__proto__: null,
|
|
2578
|
+
MMEntityProcessor
|
|
2579
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
2501
2580
|
class MMBatchValidationError extends Error {
|
|
2502
2581
|
upsertedEntities;
|
|
2503
2582
|
localDedupeCount;
|
|
@@ -3095,6 +3174,146 @@ function getErrorType(error) {
|
|
|
3095
3174
|
}
|
|
3096
3175
|
return "Error";
|
|
3097
3176
|
}
|
|
3177
|
+
function formatError(error) {
|
|
3178
|
+
if (!error) {
|
|
3179
|
+
return {
|
|
3180
|
+
message: "Unknown error occurred",
|
|
3181
|
+
code: "UNKNOWN_ERROR"
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
if (error.isAxiosError || error.name === "AxiosError") {
|
|
3185
|
+
return formatAxiosError(error);
|
|
3186
|
+
}
|
|
3187
|
+
if (error.message && error.code && typeof error.message === "string" && typeof error.code === "string" && !error.config) {
|
|
3188
|
+
return error;
|
|
3189
|
+
}
|
|
3190
|
+
if (error instanceof Error) {
|
|
3191
|
+
return {
|
|
3192
|
+
message: error.message || "An error occurred",
|
|
3193
|
+
code: "ERROR",
|
|
3194
|
+
metadata: {
|
|
3195
|
+
name: error.name
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
if (typeof error === "string") {
|
|
3200
|
+
return {
|
|
3201
|
+
message: error,
|
|
3202
|
+
code: "ERROR"
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
const message = error.message || error.toString?.() || "Unknown error occurred";
|
|
3206
|
+
return {
|
|
3207
|
+
message: typeof message === "string" ? message : JSON.stringify(message),
|
|
3208
|
+
code: error.code || "UNKNOWN_ERROR"
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
function formatAxiosError(error) {
|
|
3212
|
+
const httpStatus = error.response?.status;
|
|
3213
|
+
const method = error.config?.method?.toUpperCase();
|
|
3214
|
+
const url = error.config?.url;
|
|
3215
|
+
let message = error._extractedMessage;
|
|
3216
|
+
if (!message) {
|
|
3217
|
+
const extractedMessage = extractErrorMessage(error.response?.data);
|
|
3218
|
+
message = extractedMessage || error.response?.statusText || error.message || "Request failed";
|
|
3219
|
+
}
|
|
3220
|
+
const code = categorizeHttpError(httpStatus);
|
|
3221
|
+
const metadata = {
|
|
3222
|
+
method,
|
|
3223
|
+
url
|
|
3224
|
+
};
|
|
3225
|
+
if (httpStatus === 401 || httpStatus === 403) {
|
|
3226
|
+
metadata.hint = "Check authentication credentials";
|
|
3227
|
+
} else if (httpStatus === 404) {
|
|
3228
|
+
metadata.hint = "Resource not found - check endpoint URL";
|
|
3229
|
+
} else if (httpStatus && httpStatus >= 500) {
|
|
3230
|
+
metadata.hint = "ERP system may be temporarily unavailable";
|
|
3231
|
+
}
|
|
3232
|
+
return {
|
|
3233
|
+
message,
|
|
3234
|
+
code,
|
|
3235
|
+
httpStatus,
|
|
3236
|
+
metadata
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
function extractErrorMessage(data) {
|
|
3240
|
+
if (!data) return null;
|
|
3241
|
+
const possibleFields = [
|
|
3242
|
+
"ErrorMessage",
|
|
3243
|
+
// Epicor
|
|
3244
|
+
"error.message",
|
|
3245
|
+
// Common REST format
|
|
3246
|
+
"error",
|
|
3247
|
+
// Simple format
|
|
3248
|
+
"Message",
|
|
3249
|
+
// .NET style
|
|
3250
|
+
"message",
|
|
3251
|
+
// JavaScript style
|
|
3252
|
+
"errorMessage",
|
|
3253
|
+
// Camel case
|
|
3254
|
+
"error_message",
|
|
3255
|
+
// Snake case
|
|
3256
|
+
"errors[0].message",
|
|
3257
|
+
// Array of errors
|
|
3258
|
+
"title",
|
|
3259
|
+
// Problem details format
|
|
3260
|
+
"detail"
|
|
3261
|
+
// Problem details format
|
|
3262
|
+
];
|
|
3263
|
+
for (const field of possibleFields) {
|
|
3264
|
+
const value = getNestedValue(data, field);
|
|
3265
|
+
if (value && typeof value === "string") {
|
|
3266
|
+
return value;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
if (typeof data === "string") {
|
|
3270
|
+
try {
|
|
3271
|
+
const parsed = JSON.parse(data);
|
|
3272
|
+
return extractErrorMessage(parsed);
|
|
3273
|
+
} catch {
|
|
3274
|
+
return data;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
return null;
|
|
3278
|
+
}
|
|
3279
|
+
function getNestedValue(obj, path2) {
|
|
3280
|
+
const parts = path2.split(".");
|
|
3281
|
+
let current = obj;
|
|
3282
|
+
for (const part of parts) {
|
|
3283
|
+
const arrayMatch = part.match(/(\w+)\[(\d+)\]/);
|
|
3284
|
+
if (arrayMatch) {
|
|
3285
|
+
const [, key, index2] = arrayMatch;
|
|
3286
|
+
current = current?.[key]?.[parseInt(index2, 10)];
|
|
3287
|
+
} else {
|
|
3288
|
+
current = current?.[part];
|
|
3289
|
+
}
|
|
3290
|
+
if (current === void 0 || current === null) {
|
|
3291
|
+
return null;
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
return current;
|
|
3295
|
+
}
|
|
3296
|
+
function categorizeHttpError(status) {
|
|
3297
|
+
if (!status) return "NETWORK_ERROR";
|
|
3298
|
+
if (status === 400) return "VALIDATION_ERROR";
|
|
3299
|
+
if (status === 401) return "AUTHENTICATION_ERROR";
|
|
3300
|
+
if (status === 403) return "AUTHORIZATION_ERROR";
|
|
3301
|
+
if (status === 404) return "NOT_FOUND";
|
|
3302
|
+
if (status === 409) return "CONFLICT";
|
|
3303
|
+
if (status === 422) return "VALIDATION_ERROR";
|
|
3304
|
+
if (status === 429) return "RATE_LIMIT";
|
|
3305
|
+
if (status >= 500) return "ERP_SERVER_ERROR";
|
|
3306
|
+
if (status >= 400) return "CLIENT_ERROR";
|
|
3307
|
+
return "HTTP_ERROR";
|
|
3308
|
+
}
|
|
3309
|
+
function formatErrorForLogging(error) {
|
|
3310
|
+
const formatted = formatError(error);
|
|
3311
|
+
let message = `[${formatted.code}] ${formatted.message}`;
|
|
3312
|
+
if (formatted.httpStatus) {
|
|
3313
|
+
message = `[${formatted.httpStatus}] ${message}`;
|
|
3314
|
+
}
|
|
3315
|
+
return message;
|
|
3316
|
+
}
|
|
3098
3317
|
class LogEntry {
|
|
3099
3318
|
level;
|
|
3100
3319
|
message;
|
|
@@ -3600,9 +3819,497 @@ class OAuthClient {
|
|
|
3600
3819
|
return data;
|
|
3601
3820
|
}
|
|
3602
3821
|
}
|
|
3822
|
+
const sc = StringCodec();
|
|
3823
|
+
class NatsService {
|
|
3824
|
+
connection = null;
|
|
3825
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
3826
|
+
config;
|
|
3827
|
+
handlers = [];
|
|
3828
|
+
statusPublishTimer = null;
|
|
3829
|
+
constructor(config2) {
|
|
3830
|
+
this.config = config2;
|
|
3831
|
+
}
|
|
3832
|
+
/**
|
|
3833
|
+
* Register a handler for a specific subject pattern
|
|
3834
|
+
*/
|
|
3835
|
+
registerHandler(registration) {
|
|
3836
|
+
logger.info("Registering NATS handler", {
|
|
3837
|
+
subject: registration.subject,
|
|
3838
|
+
description: registration.description
|
|
3839
|
+
});
|
|
3840
|
+
this.handlers.push(registration);
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* Connect to NATS and start all registered handlers
|
|
3844
|
+
*/
|
|
3845
|
+
async connect() {
|
|
3846
|
+
if (!this.config.enabled) {
|
|
3847
|
+
logger.info("NATS is disabled, skipping connection");
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
try {
|
|
3851
|
+
logger.info("Connecting to NATS...", {
|
|
3852
|
+
servers: this.config.servers,
|
|
3853
|
+
name: this.config.name
|
|
3854
|
+
});
|
|
3855
|
+
this.connection = await connect({
|
|
3856
|
+
servers: this.config.servers,
|
|
3857
|
+
name: this.config.name,
|
|
3858
|
+
reconnect: this.config.reconnect ?? true,
|
|
3859
|
+
maxReconnectAttempts: this.config.maxReconnectAttempts ?? -1,
|
|
3860
|
+
reconnectTimeWait: this.config.reconnectTimeWait ?? 2e3
|
|
3861
|
+
});
|
|
3862
|
+
logger.info("Connected to NATS", {
|
|
3863
|
+
server: this.connection.getServer(),
|
|
3864
|
+
clientId: this.connection.info?.client_id
|
|
3865
|
+
});
|
|
3866
|
+
for (const registration of this.handlers) {
|
|
3867
|
+
await this.startHandler(registration);
|
|
3868
|
+
}
|
|
3869
|
+
this.startStatusPublishing();
|
|
3870
|
+
this.monitorConnection();
|
|
3871
|
+
this.setupShutdown();
|
|
3872
|
+
} catch (error) {
|
|
3873
|
+
logger.error("Failed to connect to NATS", { error });
|
|
3874
|
+
throw error;
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
/**
|
|
3878
|
+
* Start a single handler (subscribe to its subject)
|
|
3879
|
+
*/
|
|
3880
|
+
async startHandler(registration) {
|
|
3881
|
+
if (!this.connection) {
|
|
3882
|
+
throw new Error("NATS connection not established");
|
|
3883
|
+
}
|
|
3884
|
+
const sub = this.connection.subscribe(registration.subject);
|
|
3885
|
+
this.subscriptions.set(registration.subject, sub);
|
|
3886
|
+
logger.info("Started NATS handler", {
|
|
3887
|
+
subject: registration.subject,
|
|
3888
|
+
description: registration.description
|
|
3889
|
+
});
|
|
3890
|
+
(async () => {
|
|
3891
|
+
for await (const msg of sub) {
|
|
3892
|
+
try {
|
|
3893
|
+
const data = sc.decode(msg.data);
|
|
3894
|
+
logger.info("Received NATS message", {
|
|
3895
|
+
subject: msg.subject,
|
|
3896
|
+
hasReply: !!msg.reply
|
|
3897
|
+
});
|
|
3898
|
+
let parsedData;
|
|
3899
|
+
try {
|
|
3900
|
+
parsedData = JSON.parse(data);
|
|
3901
|
+
} catch {
|
|
3902
|
+
parsedData = data;
|
|
3903
|
+
}
|
|
3904
|
+
const response = await registration.handler.handle(parsedData, msg.subject);
|
|
3905
|
+
if (msg.reply && response !== void 0) {
|
|
3906
|
+
const responseStr = JSON.stringify(response);
|
|
3907
|
+
msg.respond(sc.encode(responseStr));
|
|
3908
|
+
logger.info("Sent reply", { replySubject: msg.reply });
|
|
3909
|
+
}
|
|
3910
|
+
} catch (error) {
|
|
3911
|
+
logger.error("Error handling NATS message", {
|
|
3912
|
+
subject: msg.subject,
|
|
3913
|
+
error
|
|
3914
|
+
});
|
|
3915
|
+
if (msg.reply) {
|
|
3916
|
+
const errorResponse = {
|
|
3917
|
+
status: "error",
|
|
3918
|
+
error: {
|
|
3919
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
3920
|
+
code: "HANDLER_ERROR"
|
|
3921
|
+
}
|
|
3922
|
+
};
|
|
3923
|
+
msg.respond(sc.encode(JSON.stringify(errorResponse)));
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
})();
|
|
3928
|
+
}
|
|
3929
|
+
/**
|
|
3930
|
+
* Publish a message to a subject (for pub/sub)
|
|
3931
|
+
*/
|
|
3932
|
+
async publish(subject, data) {
|
|
3933
|
+
if (!this.connection) {
|
|
3934
|
+
throw new Error("NATS connection not established");
|
|
3935
|
+
}
|
|
3936
|
+
const message = typeof data === "string" ? data : JSON.stringify(data);
|
|
3937
|
+
this.connection.publish(subject, sc.encode(message));
|
|
3938
|
+
logger.info("Published NATS message", { subject });
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Send a request and wait for reply (for request-reply)
|
|
3942
|
+
*/
|
|
3943
|
+
async request(subject, data, timeoutMs = 3e4) {
|
|
3944
|
+
if (!this.connection) {
|
|
3945
|
+
throw new Error("NATS connection not established");
|
|
3946
|
+
}
|
|
3947
|
+
const message = typeof data === "string" ? data : JSON.stringify(data);
|
|
3948
|
+
const response = await this.connection.request(
|
|
3949
|
+
subject,
|
|
3950
|
+
sc.encode(message),
|
|
3951
|
+
{ timeout: timeoutMs }
|
|
3952
|
+
);
|
|
3953
|
+
const responseData = sc.decode(response.data);
|
|
3954
|
+
try {
|
|
3955
|
+
return JSON.parse(responseData);
|
|
3956
|
+
} catch {
|
|
3957
|
+
return responseData;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
/**
|
|
3961
|
+
* Check if connected to NATS
|
|
3962
|
+
*/
|
|
3963
|
+
isConnected() {
|
|
3964
|
+
return this.connection !== null && !this.connection.isClosed();
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Start automatic status publishing (every 30 seconds)
|
|
3968
|
+
*/
|
|
3969
|
+
startStatusPublishing() {
|
|
3970
|
+
logger.info("Starting status publishing (every 30 seconds)");
|
|
3971
|
+
this.publishStatus();
|
|
3972
|
+
this.statusPublishTimer = setInterval(() => {
|
|
3973
|
+
this.publishStatus();
|
|
3974
|
+
}, 3e4);
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Publish connector status
|
|
3978
|
+
*/
|
|
3979
|
+
async publishStatus() {
|
|
3980
|
+
try {
|
|
3981
|
+
const status = {
|
|
3982
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3983
|
+
locationRef: this.config.locationRef,
|
|
3984
|
+
erpType: this.config.erpType,
|
|
3985
|
+
natsConnected: this.isConnected()
|
|
3986
|
+
};
|
|
3987
|
+
await this.publish(
|
|
3988
|
+
`mm.14.${this.config.locationRef}.erp.status`,
|
|
3989
|
+
status
|
|
3990
|
+
);
|
|
3991
|
+
logger.debug("Published connector status");
|
|
3992
|
+
} catch (error) {
|
|
3993
|
+
logger.error("Failed to publish status", { error });
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
/**
|
|
3997
|
+
* Monitor connection status
|
|
3998
|
+
*/
|
|
3999
|
+
monitorConnection() {
|
|
4000
|
+
if (!this.connection) return;
|
|
4001
|
+
(async () => {
|
|
4002
|
+
for await (const status of this.connection.status()) {
|
|
4003
|
+
if (status.type !== "pingTimer") {
|
|
4004
|
+
logger.info("NATS connection status", {
|
|
4005
|
+
type: status.type,
|
|
4006
|
+
data: status.data
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
})();
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* Setup graceful shutdown
|
|
4014
|
+
*/
|
|
4015
|
+
setupShutdown() {
|
|
4016
|
+
const shutdown = async () => {
|
|
4017
|
+
logger.info("Shutting down NATS service...");
|
|
4018
|
+
await this.disconnect();
|
|
4019
|
+
process.exit(0);
|
|
4020
|
+
};
|
|
4021
|
+
process.on("SIGINT", shutdown);
|
|
4022
|
+
process.on("SIGTERM", shutdown);
|
|
4023
|
+
}
|
|
4024
|
+
/**
|
|
4025
|
+
* Disconnect from NATS
|
|
4026
|
+
*/
|
|
4027
|
+
async disconnect() {
|
|
4028
|
+
if (this.statusPublishTimer) {
|
|
4029
|
+
clearInterval(this.statusPublishTimer);
|
|
4030
|
+
this.statusPublishTimer = null;
|
|
4031
|
+
}
|
|
4032
|
+
if (this.connection) {
|
|
4033
|
+
await this.connection.drain();
|
|
4034
|
+
this.connection = null;
|
|
4035
|
+
this.subscriptions.clear();
|
|
4036
|
+
logger.info("Disconnected from NATS");
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* Get the location reference
|
|
4041
|
+
*/
|
|
4042
|
+
getLocationRef() {
|
|
4043
|
+
return this.config.locationRef;
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
class NatsLaborTicketListener {
|
|
4047
|
+
connector;
|
|
4048
|
+
natsService;
|
|
4049
|
+
constructor(connector) {
|
|
4050
|
+
this.connector = connector;
|
|
4051
|
+
}
|
|
4052
|
+
/**
|
|
4053
|
+
* Start listening for labor ticket events via NATS
|
|
4054
|
+
*/
|
|
4055
|
+
async start() {
|
|
4056
|
+
try {
|
|
4057
|
+
const companyInfo = await getCompanyInfo();
|
|
4058
|
+
const erpType = this.connector.type || "unknown";
|
|
4059
|
+
logger.info("Starting NATS listener for labor tickets", {
|
|
4060
|
+
locationRef: companyInfo.locationRef,
|
|
4061
|
+
companyId: companyInfo.companyId,
|
|
4062
|
+
erpType,
|
|
4063
|
+
servers: process.env.NATS_SERVERS || "nats://localhost:4222"
|
|
4064
|
+
});
|
|
4065
|
+
this.natsService = new NatsService({
|
|
4066
|
+
servers: process.env.NATS_SERVERS || "nats://localhost:4222",
|
|
4067
|
+
name: `${erpType}-connector`,
|
|
4068
|
+
locationRef: companyInfo.locationRef,
|
|
4069
|
+
erpType,
|
|
4070
|
+
enabled: true,
|
|
4071
|
+
reconnect: true,
|
|
4072
|
+
maxReconnectAttempts: -1,
|
|
4073
|
+
reconnectTimeWait: 2e3
|
|
4074
|
+
});
|
|
4075
|
+
this.registerHealthCheckHandler(companyInfo.locationRef, erpType);
|
|
4076
|
+
this.registerLaborTicketHandler(companyInfo.locationRef, erpType);
|
|
4077
|
+
await this.natsService.connect();
|
|
4078
|
+
logger.info("NATS listener started successfully", {
|
|
4079
|
+
subject: `mm.14.${companyInfo.locationRef}.labor-ticket.*`
|
|
4080
|
+
});
|
|
4081
|
+
} catch (error) {
|
|
4082
|
+
logger.error("Failed to start NATS listener", { error });
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
/**
|
|
4086
|
+
* Register health check handler - responds immediately to let Dashboard know connector is online
|
|
4087
|
+
*/
|
|
4088
|
+
registerHealthCheckHandler(locationRef, erpType) {
|
|
4089
|
+
if (!this.natsService) return;
|
|
4090
|
+
this.natsService.registerHandler({
|
|
4091
|
+
subject: `mm.14.${locationRef}.erp.health`,
|
|
4092
|
+
description: "Health check - responds immediately to indicate connector is online",
|
|
4093
|
+
handler: {
|
|
4094
|
+
handle: async () => {
|
|
4095
|
+
logger.debug("Health check received, sending pong");
|
|
4096
|
+
return {
|
|
4097
|
+
status: "online",
|
|
4098
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4099
|
+
locationRef,
|
|
4100
|
+
erpType
|
|
4101
|
+
};
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* Register labor ticket processing handler
|
|
4108
|
+
*/
|
|
4109
|
+
registerLaborTicketHandler(locationRef, erpType) {
|
|
4110
|
+
if (!this.natsService) return;
|
|
4111
|
+
this.natsService.registerHandler({
|
|
4112
|
+
subject: `mm.14.${locationRef}.labor-ticket.*`,
|
|
4113
|
+
description: "Process labor tickets in real-time from NATS",
|
|
4114
|
+
handler: {
|
|
4115
|
+
handle: async ({ data }, subject) => {
|
|
4116
|
+
const action = subject.split(".").pop();
|
|
4117
|
+
const { actionPayload } = data;
|
|
4118
|
+
const startTime = Date.now();
|
|
4119
|
+
const { laborTicketRef } = actionPayload;
|
|
4120
|
+
logger.info("Received labor ticket via NATS", {
|
|
4121
|
+
action,
|
|
4122
|
+
requestId: data.requestId,
|
|
4123
|
+
laborTicketRef
|
|
4124
|
+
});
|
|
4125
|
+
return await SQLiteCoordinator.executeWithLock("to-erp", async () => {
|
|
4126
|
+
try {
|
|
4127
|
+
let laborTicketData;
|
|
4128
|
+
if (laborTicketRef) {
|
|
4129
|
+
const mmApiClient = new MMApiClient();
|
|
4130
|
+
const laborTicket = await mmApiClient.fetchLaborTicketByRef(laborTicketRef);
|
|
4131
|
+
logger.info("Fetched labor ticket data from MM API", {
|
|
4132
|
+
laborTicketRef,
|
|
4133
|
+
laborTicketId: laborTicket.laborTicketId
|
|
4134
|
+
});
|
|
4135
|
+
laborTicketData = {
|
|
4136
|
+
...laborTicket,
|
|
4137
|
+
...actionPayload
|
|
4138
|
+
};
|
|
4139
|
+
} else {
|
|
4140
|
+
logger.info("No laborTicketRef provided, using actionPayload directly", {
|
|
4141
|
+
requestId: data.requestId
|
|
4142
|
+
});
|
|
4143
|
+
laborTicketData = actionPayload;
|
|
4144
|
+
}
|
|
4145
|
+
const mergedLaborTicket = new MMReceiveLaborTicket(laborTicketData);
|
|
4146
|
+
const result = await this.processLaborTicket(
|
|
4147
|
+
mergedLaborTicket,
|
|
4148
|
+
action || "unknown"
|
|
4149
|
+
);
|
|
4150
|
+
await this.updateCheckpoint(erpType, result);
|
|
4151
|
+
return {
|
|
4152
|
+
status: "success",
|
|
4153
|
+
requestId: data.requestId,
|
|
4154
|
+
action,
|
|
4155
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4156
|
+
processingTimeMs: Date.now() - startTime,
|
|
4157
|
+
laborTicketRef: result.laborTicketRef,
|
|
4158
|
+
laborTicket: result.laborTicket
|
|
4159
|
+
};
|
|
4160
|
+
} catch (error) {
|
|
4161
|
+
const formattedError = error?._formatted || formatError(error);
|
|
4162
|
+
logger.debug("Error details", {
|
|
4163
|
+
hasFormatted: !!error?._formatted,
|
|
4164
|
+
isAxiosError: error?.isAxiosError,
|
|
4165
|
+
errorMessage: error?.message,
|
|
4166
|
+
responseStatus: error?.response?.status,
|
|
4167
|
+
responseData: error?.response?.data
|
|
4168
|
+
});
|
|
4169
|
+
logger.error("Error handling labor ticket from NATS", {
|
|
4170
|
+
error: formattedError.message,
|
|
4171
|
+
code: formattedError.code,
|
|
4172
|
+
requestId: data.requestId,
|
|
4173
|
+
laborTicketRef
|
|
4174
|
+
});
|
|
4175
|
+
return {
|
|
4176
|
+
status: "error",
|
|
4177
|
+
requestId: data.requestId,
|
|
4178
|
+
action,
|
|
4179
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4180
|
+
processingTimeMs: Date.now() - startTime,
|
|
4181
|
+
error: {
|
|
4182
|
+
message: formattedError.message,
|
|
4183
|
+
code: formattedError.code,
|
|
4184
|
+
httpStatus: formattedError.httpStatus,
|
|
4185
|
+
metadata: formattedError.metadata,
|
|
4186
|
+
laborTicketRef
|
|
4187
|
+
}
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
});
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* Process labor ticket in ERP and then create MM entities with ERP ID attached
|
|
4197
|
+
*/
|
|
4198
|
+
async processLaborTicket(laborTicket, action) {
|
|
4199
|
+
const { MMEntityProcessor: MMEntityProcessor2 } = await Promise.resolve().then(() => mmEntityProcessor);
|
|
4200
|
+
const { ERPObjType: ERPObjType2 } = await Promise.resolve().then(() => erpTypes);
|
|
4201
|
+
const { MMSendLaborTicket: MMSendLaborTicket2 } = await Promise.resolve().then(() => index);
|
|
4202
|
+
logger.info("Processing labor ticket: ERP first, then MM creation", {
|
|
4203
|
+
laborTicketRef: laborTicket.laborTicketRef,
|
|
4204
|
+
action,
|
|
4205
|
+
hasLaborTicketId: !!laborTicket.laborTicketId
|
|
4206
|
+
});
|
|
4207
|
+
try {
|
|
4208
|
+
let erpResult;
|
|
4209
|
+
if (action === "create" || !laborTicket.laborTicketId) {
|
|
4210
|
+
erpResult = await this.connector.createLaborTicketInERP(laborTicket);
|
|
4211
|
+
logger.info("Successfully created labor ticket in ERP", {
|
|
4212
|
+
laborTicketRef: laborTicket.laborTicketRef,
|
|
4213
|
+
erpUid: erpResult.erpUid
|
|
4214
|
+
});
|
|
4215
|
+
} else {
|
|
4216
|
+
erpResult = { laborTicket: await this.connector.updateLaborTicketInERP(laborTicket) };
|
|
4217
|
+
logger.info("Successfully updated labor ticket in ERP", {
|
|
4218
|
+
laborTicketRef: laborTicket.laborTicketRef
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
4221
|
+
const laborTicketForMM = { ...laborTicket };
|
|
4222
|
+
if (erpResult.erpUid) {
|
|
4223
|
+
laborTicketForMM.laborTicketId = erpResult.erpUid;
|
|
4224
|
+
if (!laborTicket.laborTicketId && laborTicketForMM.laborTicketRef) {
|
|
4225
|
+
const mmApiClient = new MMApiClient();
|
|
4226
|
+
await mmApiClient.updateLaborTicketIdByRef(
|
|
4227
|
+
laborTicketForMM.laborTicketRef,
|
|
4228
|
+
erpResult.erpUid
|
|
4229
|
+
);
|
|
4230
|
+
logger.info("Patched existing MM labor ticket with new ERP ID", {
|
|
4231
|
+
laborTicketRef: laborTicketForMM.laborTicketRef,
|
|
4232
|
+
laborTicketId: erpResult.erpUid
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
laborTicketForMM.state = laborTicketForMM.clockOut ? "CLOSED" : "OPEN";
|
|
4237
|
+
const mmLaborTicket = MMSendLaborTicket2.fromPlainObject(laborTicketForMM);
|
|
4238
|
+
const mmResult = await MMEntityProcessor2.writeEntities(
|
|
4239
|
+
ERPObjType2.LABOR_TICKETS,
|
|
4240
|
+
[mmLaborTicket],
|
|
4241
|
+
null
|
|
4242
|
+
// No caching for real-time operations
|
|
4243
|
+
);
|
|
4244
|
+
logger.info("Successfully updated MM entities after ERP operation", {
|
|
4245
|
+
laborTicketRef: laborTicketForMM.laborTicketRef,
|
|
4246
|
+
laborTicketId: laborTicketForMM.laborTicketId,
|
|
4247
|
+
entitiesCreated: mmResult.upsertedEntities
|
|
4248
|
+
});
|
|
4249
|
+
return {
|
|
4250
|
+
laborTicketRef: laborTicketForMM.laborTicketRef,
|
|
4251
|
+
laborTicket: laborTicketForMM
|
|
4252
|
+
};
|
|
4253
|
+
} catch (error) {
|
|
4254
|
+
const formattedError = formatError(error);
|
|
4255
|
+
logger.error("Failed to process labor ticket with MM creation", {
|
|
4256
|
+
laborTicketRef: laborTicket.laborTicketRef,
|
|
4257
|
+
action,
|
|
4258
|
+
error: formattedError.message,
|
|
4259
|
+
code: formattedError.code
|
|
4260
|
+
});
|
|
4261
|
+
const enhancedError = error;
|
|
4262
|
+
enhancedError._formatted = formattedError;
|
|
4263
|
+
throw enhancedError;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
/**
|
|
4267
|
+
* Update checkpoint to prevent to-erp polling job from reprocessing this ticket
|
|
4268
|
+
* Only updates if the new timestamp is later than the current checkpoint (prevents moving backwards)
|
|
4269
|
+
*/
|
|
4270
|
+
async updateCheckpoint(erpType, result) {
|
|
4271
|
+
const mmApiClient = new MMApiClient();
|
|
4272
|
+
const currentCheckpoint = await mmApiClient.getCheckpoint({
|
|
4273
|
+
system: erpType,
|
|
4274
|
+
table: "labor_tickets",
|
|
4275
|
+
checkpointType: "export",
|
|
4276
|
+
checkpointValue: {
|
|
4277
|
+
timestamp: ""
|
|
4278
|
+
}
|
|
4279
|
+
});
|
|
4280
|
+
const currentTimestamp = currentCheckpoint?.timestamp;
|
|
4281
|
+
const newTimestamp = result.laborTicket.updatedAt || (/* @__PURE__ */ new Date()).toISOString();
|
|
4282
|
+
if (!currentTimestamp || new Date(newTimestamp) > new Date(currentTimestamp)) {
|
|
4283
|
+
await mmApiClient.saveCheckpoint({
|
|
4284
|
+
system: erpType,
|
|
4285
|
+
table: "labor_tickets",
|
|
4286
|
+
checkpointType: "export",
|
|
4287
|
+
checkpointValue: {
|
|
4288
|
+
timestamp: newTimestamp
|
|
4289
|
+
}
|
|
4290
|
+
});
|
|
4291
|
+
logger.debug("Updated export checkpoint after NATS processing", {
|
|
4292
|
+
laborTicketRef: result.laborTicketRef,
|
|
4293
|
+
previousCheckpoint: currentTimestamp,
|
|
4294
|
+
newCheckpoint: newTimestamp
|
|
4295
|
+
});
|
|
4296
|
+
} else {
|
|
4297
|
+
logger.debug("Skipped checkpoint update (timestamp not newer)", {
|
|
4298
|
+
laborTicketRef: result.laborTicketRef,
|
|
4299
|
+
currentCheckpoint: currentTimestamp,
|
|
4300
|
+
ticketTimestamp: newTimestamp
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
3603
4305
|
const runDataSyncService = async (connectorPath) => {
|
|
3604
4306
|
const config2 = CoreConfiguration.inst();
|
|
3605
4307
|
try {
|
|
4308
|
+
const connector = await createConnectorFromPath(connectorPath);
|
|
4309
|
+
if (process.env.NATS_ENABLED === "true") {
|
|
4310
|
+
const natsListener = new NatsLaborTicketListener(connector);
|
|
4311
|
+
await natsListener.start();
|
|
4312
|
+
}
|
|
3606
4313
|
const currentFileUrl = import.meta.url;
|
|
3607
4314
|
const currentFilePath = fileURLToPath(currentFileUrl);
|
|
3608
4315
|
const sdkDistPath = path.dirname(currentFilePath);
|
|
@@ -4245,6 +4952,8 @@ export {
|
|
|
4245
4952
|
combinePsqlDateTime,
|
|
4246
4953
|
convertToLocalTime,
|
|
4247
4954
|
formatDateWithTZOffset,
|
|
4955
|
+
formatError,
|
|
4956
|
+
formatErrorForLogging,
|
|
4248
4957
|
formatPsqlDate,
|
|
4249
4958
|
formatPsqlTime,
|
|
4250
4959
|
f as getCachedTimezoneName,
|