@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.
Files changed (58) hide show
  1. package/dist/{config-cB7h4yvc.js → config-CvA-mFWF.js} +20 -20
  2. package/dist/{config-cB7h4yvc.js.map → config-CvA-mFWF.js.map} +1 -1
  3. package/dist/{connector-factory-CKm74_WZ.js → connector-factory-BPm2GVVF.js} +2 -2
  4. package/dist/{connector-factory-CKm74_WZ.js.map → connector-factory-BPm2GVVF.js.map} +1 -1
  5. package/dist/{hashed-cache-manager-Ds-HksA0.js → hashed-cache-manager-B15NN8hK.js} +5 -5
  6. package/dist/{hashed-cache-manager-Ds-HksA0.js.map → hashed-cache-manager-B15NN8hK.js.map} +1 -1
  7. package/dist/{index-DTtmv8Iq.js → index-D8qO1NyK.js} +2 -2
  8. package/dist/{index-DTtmv8Iq.js.map → index-D8qO1NyK.js.map} +1 -1
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/{knexfile-1qKKIORB.js → knexfile-Bng2Ru9c.js} +3 -3
  12. package/dist/{knexfile-1qKKIORB.js.map → knexfile-Bng2Ru9c.js.map} +1 -1
  13. package/dist/{logger-CBDNtsMq.js → logger-BWw0_z9q.js} +328 -303
  14. package/dist/logger-BWw0_z9q.js.map +1 -0
  15. package/dist/mm-erp-sdk.js +717 -8
  16. package/dist/mm-erp-sdk.js.map +1 -1
  17. package/dist/services/data-sync-service/data-sync-service.d.ts.map +1 -1
  18. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +4 -4
  19. package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
  20. package/dist/services/data-sync-service/jobs/from-erp.js +5 -12
  21. package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
  22. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +3 -3
  23. package/dist/services/data-sync-service/jobs/run-migrations.js +2 -2
  24. package/dist/services/data-sync-service/jobs/to-erp.d.ts.map +1 -1
  25. package/dist/services/data-sync-service/jobs/to-erp.js +7 -4
  26. package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -1
  27. package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts +30 -0
  28. package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts.map +1 -0
  29. package/dist/services/mm-api-service/company-info.d.ts +13 -0
  30. package/dist/services/mm-api-service/company-info.d.ts.map +1 -0
  31. package/dist/services/mm-api-service/index.d.ts +7 -0
  32. package/dist/services/mm-api-service/index.d.ts.map +1 -1
  33. package/dist/services/mm-api-service/mm-api-service.d.ts +6 -0
  34. package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
  35. package/dist/services/nats-service/nats-service.d.ts +114 -0
  36. package/dist/services/nats-service/nats-service.d.ts.map +1 -0
  37. package/dist/services/nats-service/test-nats-subscriber.d.ts +6 -0
  38. package/dist/services/nats-service/test-nats-subscriber.d.ts.map +1 -0
  39. package/dist/services/reporting-service/logger.d.ts.map +1 -1
  40. package/dist/utils/error-formatter.d.ts +19 -0
  41. package/dist/utils/error-formatter.d.ts.map +1 -0
  42. package/dist/utils/index.d.ts +5 -0
  43. package/dist/utils/index.d.ts.map +1 -1
  44. package/package.json +5 -2
  45. package/src/index.ts +3 -0
  46. package/src/services/data-sync-service/data-sync-service.ts +10 -0
  47. package/src/services/data-sync-service/jobs/from-erp.ts +2 -7
  48. package/src/services/data-sync-service/jobs/to-erp.ts +5 -1
  49. package/src/services/data-sync-service/nats-labor-ticket-listener.ts +341 -0
  50. package/src/services/mm-api-service/company-info.ts +87 -0
  51. package/src/services/mm-api-service/index.ts +8 -0
  52. package/src/services/mm-api-service/mm-api-service.ts +19 -2
  53. package/src/services/nats-service/nats-service.ts +351 -0
  54. package/src/services/nats-service/test-nats-subscriber.ts +96 -0
  55. package/src/services/reporting-service/logger.ts +38 -1
  56. package/src/utils/error-formatter.ts +205 -0
  57. package/src/utils/index.ts +6 -0
  58. package/dist/logger-CBDNtsMq.js.map +0 -1
@@ -1,17 +1,18 @@
1
- import { C as CoreConfiguration, H as HashedCacheManager } from "./hashed-cache-manager-Ds-HksA0.js";
2
- import { E, g, a } from "./hashed-cache-manager-Ds-HksA0.js";
3
- import { l as logger } from "./logger-CBDNtsMq.js";
4
- import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as setTimezoneNameInCache, c as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-DTtmv8Iq.js";
5
- import { f, d, e } from "./index-DTtmv8Iq.js";
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-1qKKIORB.js";
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-CKm74_WZ.js";
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,