@machinemetrics/mm-erp-sdk 0.1.9-beta.1 → 0.1.9-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +5 -0
  2. package/dist/{config-CvA-mFWF.js → config-Bax6Ofp5.js} +2 -2
  3. package/dist/{config-CvA-mFWF.js.map → config-Bax6Ofp5.js.map} +1 -1
  4. package/dist/{connector-factory-BPm2GVVF.js → connector-factory-BaMIlES8.js} +2 -2
  5. package/dist/{connector-factory-BPm2GVVF.js.map → connector-factory-BaMIlES8.js.map} +1 -1
  6. package/dist/{hashed-cache-manager-B15NN8hK.js → hashed-cache-manager-C1u9jQgY.js} +4 -4
  7. package/dist/{hashed-cache-manager-B15NN8hK.js.map → hashed-cache-manager-C1u9jQgY.js.map} +1 -1
  8. package/dist/{index-D8qO1NyK.js → index-BkVlW0ZW.js} +2 -2
  9. package/dist/{index-D8qO1NyK.js.map → index-BkVlW0ZW.js.map} +1 -1
  10. package/dist/index.d.ts +1 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/{logger-BWw0_z9q.js → logger-DW5fyhVS.js} +114 -78
  13. package/dist/logger-DW5fyhVS.js.map +1 -0
  14. package/dist/mm-erp-sdk.js +7 -716
  15. package/dist/mm-erp-sdk.js.map +1 -1
  16. package/dist/services/data-sync-service/data-sync-service.d.ts.map +1 -1
  17. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +4 -4
  18. package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
  19. package/dist/services/data-sync-service/jobs/from-erp.js +12 -5
  20. package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
  21. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +3 -3
  22. package/dist/services/data-sync-service/jobs/run-migrations.js +1 -1
  23. package/dist/services/data-sync-service/jobs/to-erp.d.ts.map +1 -1
  24. package/dist/services/data-sync-service/jobs/to-erp.js +4 -7
  25. package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -1
  26. package/dist/services/mm-api-service/index.d.ts +0 -7
  27. package/dist/services/mm-api-service/index.d.ts.map +1 -1
  28. package/dist/services/mm-api-service/mm-api-service.d.ts +0 -6
  29. package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
  30. package/dist/services/reporting-service/logger.d.ts.map +1 -1
  31. package/dist/utils/index.d.ts +0 -5
  32. package/dist/utils/index.d.ts.map +1 -1
  33. package/package.json +2 -5
  34. package/src/index.ts +0 -3
  35. package/src/services/data-sync-service/data-sync-service.ts +0 -10
  36. package/src/services/data-sync-service/jobs/from-erp.ts +7 -2
  37. package/src/services/data-sync-service/jobs/to-erp.ts +1 -5
  38. package/src/services/mm-api-service/index.ts +0 -8
  39. package/src/services/mm-api-service/mm-api-service.ts +2 -19
  40. package/src/services/reporting-service/logger.ts +111 -81
  41. package/src/utils/index.ts +0 -6
  42. package/dist/logger-BWw0_z9q.js.map +0 -1
  43. package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts +0 -30
  44. package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts.map +0 -1
  45. package/dist/services/mm-api-service/company-info.d.ts +0 -13
  46. package/dist/services/mm-api-service/company-info.d.ts.map +0 -1
  47. package/dist/services/nats-service/nats-service.d.ts +0 -114
  48. package/dist/services/nats-service/nats-service.d.ts.map +0 -1
  49. package/dist/services/nats-service/test-nats-subscriber.d.ts +0 -6
  50. package/dist/services/nats-service/test-nats-subscriber.d.ts.map +0 -1
  51. package/dist/utils/error-formatter.d.ts +0 -19
  52. package/dist/utils/error-formatter.d.ts.map +0 -1
  53. package/src/services/data-sync-service/nats-labor-ticket-listener.ts +0 -341
  54. package/src/services/mm-api-service/company-info.ts +0 -87
  55. package/src/services/nats-service/nats-service.ts +0 -351
  56. package/src/services/nats-service/test-nats-subscriber.ts +0 -96
  57. package/src/utils/error-formatter.ts +0 -205
@@ -1,18 +1,17 @@
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";
1
+ import { C as CoreConfiguration, H as HashedCacheManager } from "./hashed-cache-manager-C1u9jQgY.js";
2
+ import { E, g, a } from "./hashed-cache-manager-C1u9jQgY.js";
3
+ import { l as logger } from "./logger-DW5fyhVS.js";
4
+ import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as setTimezoneNameInCache, c as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-BkVlW0ZW.js";
5
+ import { f, d, e } from "./index-BkVlW0ZW.js";
6
6
  import axios, { AxiosError } from "axios";
7
7
  import knex from "knex";
8
8
  import { c as config } from "./knexfile-Bng2Ru9c.js";
9
9
  import fs from "fs";
10
10
  import path from "path";
11
- import { c as createConnectorFromPath } from "./connector-factory-BPm2GVVF.js";
11
+ import "./connector-factory-BaMIlES8.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";
16
15
  import sql from "mssql";
17
16
  import { z } from "zod";
18
17
  var ERPObjType = /* @__PURE__ */ ((ERPObjType2) => {
@@ -26,10 +25,6 @@ var ERPObjType = /* @__PURE__ */ ((ERPObjType2) => {
26
25
  ERPObjType2[ERPObjType2["LABOR_TICKETS"] = 7] = "LABOR_TICKETS";
27
26
  return ERPObjType2;
28
27
  })(ERPObjType || {});
29
- const erpTypes = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
30
- __proto__: null,
31
- ERPObjType
32
- }, Symbol.toStringTag, { value: "Module" }));
33
28
  var Operator = /* @__PURE__ */ ((Operator2) => {
34
29
  Operator2["eq"] = "eq";
35
30
  Operator2["lt"] = "lt";
@@ -486,7 +481,7 @@ class MMApiClient {
486
481
  const hasStatus = (err) => {
487
482
  return typeof err === "object" && err !== null && "status" in err;
488
483
  };
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");
484
+ const isAuthError = hasStatus(error) && (error.status === 401 || error.status === 403);
490
485
  if (isAuthError && !options.token) {
491
486
  logger.info("Retrying request with fresh token due to auth error");
492
487
  this.tokenMgr.invalidateToken();
@@ -810,17 +805,6 @@ class MMApiClient {
810
805
  );
811
806
  return updates.data.map((ticket) => new MMReceiveLaborTicket(ticket));
812
807
  }
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
- }
824
808
  /**
825
809
  * Fetch a checkpoint for a specific system, table, and checkpoint type
826
810
  * @param checkpoint The checkpoint to fetch
@@ -1334,65 +1318,6 @@ class MMSendLaborTicket {
1334
1318
  );
1335
1319
  }
1336
1320
  }
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" }));
1396
1321
  function getUniqueRows(data, fields, sortFields) {
1397
1322
  const createCompositeKey = (item) => {
1398
1323
  return fields.map((field) => String(item[field])).join("|");
@@ -2573,10 +2498,6 @@ class MMEntityProcessor {
2573
2498
  }
2574
2499
  }
2575
2500
  }
2576
- const mmEntityProcessor = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2577
- __proto__: null,
2578
- MMEntityProcessor
2579
- }, Symbol.toStringTag, { value: "Module" }));
2580
2501
  class MMBatchValidationError extends Error {
2581
2502
  upsertedEntities;
2582
2503
  localDedupeCount;
@@ -3174,146 +3095,6 @@ function getErrorType(error) {
3174
3095
  }
3175
3096
  return "Error";
3176
3097
  }
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
- }
3317
3098
  class LogEntry {
3318
3099
  level;
3319
3100
  message;
@@ -3819,497 +3600,9 @@ class OAuthClient {
3819
3600
  return data;
3820
3601
  }
3821
3602
  }
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
- }
4305
3603
  const runDataSyncService = async (connectorPath) => {
4306
3604
  const config2 = CoreConfiguration.inst();
4307
3605
  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
- }
4313
3606
  const currentFileUrl = import.meta.url;
4314
3607
  const currentFilePath = fileURLToPath(currentFileUrl);
4315
3608
  const sdkDistPath = path.dirname(currentFilePath);
@@ -4952,8 +4245,6 @@ export {
4952
4245
  combinePsqlDateTime,
4953
4246
  convertToLocalTime,
4954
4247
  formatDateWithTZOffset,
4955
- formatError,
4956
- formatErrorForLogging,
4957
4248
  formatPsqlDate,
4958
4249
  formatPsqlTime,
4959
4250
  f as getCachedTimezoneName,