@machinemetrics/mm-erp-sdk 0.3.0-beta.0 → 0.3.0-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 (141) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/services/data-sync-service/configuration-manager.d.ts.map +1 -1
  6. package/dist/services/data-sync-service/configuration-manager.js +30 -30
  7. package/dist/services/data-sync-service/configuration-manager.js.map +1 -1
  8. package/dist/services/data-sync-service/data-sync-service.d.ts.map +1 -1
  9. package/dist/services/data-sync-service/data-sync-service.js +9 -0
  10. package/dist/services/data-sync-service/data-sync-service.js.map +1 -1
  11. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts.map +1 -1
  12. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +1 -2
  13. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js.map +1 -1
  14. package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
  15. package/dist/services/data-sync-service/jobs/from-erp.js +7 -13
  16. package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
  17. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts.map +1 -1
  18. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +1 -2
  19. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js.map +1 -1
  20. package/dist/services/data-sync-service/jobs/run-migrations.d.ts.map +1 -1
  21. package/dist/services/data-sync-service/jobs/run-migrations.js +1 -2
  22. package/dist/services/data-sync-service/jobs/run-migrations.js.map +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 +12 -3
  25. package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -1
  26. package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts +30 -0
  27. package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts.map +1 -0
  28. package/dist/services/data-sync-service/nats-labor-ticket-listener.js +290 -0
  29. package/dist/services/data-sync-service/nats-labor-ticket-listener.js.map +1 -0
  30. package/dist/services/mm-api-service/company-info.d.ts +13 -0
  31. package/dist/services/mm-api-service/company-info.d.ts.map +1 -0
  32. package/dist/services/mm-api-service/company-info.js +60 -0
  33. package/dist/services/mm-api-service/company-info.js.map +1 -0
  34. package/dist/services/mm-api-service/index.d.ts +7 -0
  35. package/dist/services/mm-api-service/index.d.ts.map +1 -1
  36. package/dist/services/mm-api-service/index.js +5 -0
  37. package/dist/services/mm-api-service/index.js.map +1 -1
  38. package/dist/services/mm-api-service/mm-api-service.d.ts +6 -0
  39. package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
  40. package/dist/services/mm-api-service/mm-api-service.js +15 -3
  41. package/dist/services/mm-api-service/mm-api-service.js.map +1 -1
  42. package/dist/services/mm-api-service/types/receive-types.d.ts +3 -0
  43. package/dist/services/mm-api-service/types/receive-types.d.ts.map +1 -1
  44. package/dist/services/mm-api-service/types/receive-types.js +1 -0
  45. package/dist/services/mm-api-service/types/receive-types.js.map +1 -1
  46. package/dist/services/mm-api-service/types/send-types.d.ts +17 -8
  47. package/dist/services/mm-api-service/types/send-types.d.ts.map +1 -1
  48. package/dist/services/mm-api-service/types/send-types.js +41 -17
  49. package/dist/services/mm-api-service/types/send-types.js.map +1 -1
  50. package/dist/services/nats-service/nats-service.d.ts +114 -0
  51. package/dist/services/nats-service/nats-service.d.ts.map +1 -0
  52. package/dist/services/nats-service/nats-service.js +244 -0
  53. package/dist/services/nats-service/nats-service.js.map +1 -0
  54. package/dist/services/nats-service/test-nats-subscriber.d.ts +6 -0
  55. package/dist/services/nats-service/test-nats-subscriber.d.ts.map +1 -0
  56. package/dist/services/nats-service/test-nats-subscriber.js +79 -0
  57. package/dist/services/nats-service/test-nats-subscriber.js.map +1 -0
  58. package/dist/services/reporting-service/logger.d.ts.map +1 -1
  59. package/dist/services/reporting-service/logger.js +31 -6
  60. package/dist/services/reporting-service/logger.js.map +1 -1
  61. package/dist/types/erp-connector.d.ts +1 -8
  62. package/dist/types/erp-connector.d.ts.map +1 -1
  63. package/dist/types/index.d.ts +0 -1
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/utils/error-formatter.d.ts +19 -0
  66. package/dist/utils/error-formatter.d.ts.map +1 -0
  67. package/dist/utils/error-formatter.js +184 -0
  68. package/dist/utils/error-formatter.js.map +1 -0
  69. package/dist/utils/http-client.js +2 -4
  70. package/dist/utils/http-client.js.map +1 -1
  71. package/dist/utils/index.d.ts +5 -1
  72. package/dist/utils/index.d.ts.map +1 -1
  73. package/dist/utils/index.js +4 -1
  74. package/dist/utils/index.js.map +1 -1
  75. package/dist/utils/local-data-store/jobs-shared-data.d.ts +0 -2
  76. package/dist/utils/local-data-store/jobs-shared-data.d.ts.map +1 -1
  77. package/dist/utils/local-data-store/jobs-shared-data.js +0 -2
  78. package/dist/utils/local-data-store/jobs-shared-data.js.map +1 -1
  79. package/dist/utils/mm-labor-ticket-helpers.d.ts +4 -3
  80. package/dist/utils/mm-labor-ticket-helpers.d.ts.map +1 -1
  81. package/dist/utils/mm-labor-ticket-helpers.js +7 -12
  82. package/dist/utils/mm-labor-ticket-helpers.js.map +1 -1
  83. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.d.ts +0 -15
  84. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.d.ts.map +1 -1
  85. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.js +46 -180
  86. package/dist/utils/standard-process-drivers/labor-ticket-erp-synchronizer.js.map +1 -1
  87. package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts +1 -7
  88. package/dist/utils/standard-process-drivers/mm-entity-processor.d.ts.map +1 -1
  89. package/dist/utils/standard-process-drivers/mm-entity-processor.js +1 -7
  90. package/dist/utils/standard-process-drivers/mm-entity-processor.js.map +1 -1
  91. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts +2 -8
  92. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts.map +1 -1
  93. package/dist/utils/standard-process-drivers/standard-process-drivers.js +18 -27
  94. package/dist/utils/standard-process-drivers/standard-process-drivers.js.map +1 -1
  95. package/dist/utils/time-utils.d.ts.map +1 -1
  96. package/dist/utils/time-utils.js +0 -7
  97. package/dist/utils/time-utils.js.map +1 -1
  98. package/package.json +5 -4
  99. package/src/index.ts +3 -0
  100. package/src/services/data-sync-service/configuration-manager.ts +37 -50
  101. package/src/services/data-sync-service/data-sync-service.ts +10 -0
  102. package/src/services/data-sync-service/jobs/clean-up-expired-cache.ts +1 -2
  103. package/src/services/data-sync-service/jobs/from-erp.ts +7 -13
  104. package/src/services/data-sync-service/jobs/retry-failed-labor-tickets.ts +1 -2
  105. package/src/services/data-sync-service/jobs/run-migrations.ts +1 -2
  106. package/src/services/data-sync-service/jobs/to-erp.ts +12 -3
  107. package/src/services/data-sync-service/nats-labor-ticket-listener.ts +342 -0
  108. package/src/services/mm-api-service/company-info.ts +87 -0
  109. package/src/services/mm-api-service/index.ts +8 -0
  110. package/src/services/mm-api-service/mm-api-service.ts +20 -3
  111. package/src/services/mm-api-service/types/receive-types.ts +1 -0
  112. package/src/services/mm-api-service/types/send-types.ts +40 -16
  113. package/src/services/nats-service/nats-service.ts +351 -0
  114. package/src/services/nats-service/test-nats-subscriber.ts +96 -0
  115. package/src/services/reporting-service/logger.ts +39 -7
  116. package/src/types/erp-connector.ts +1 -8
  117. package/src/types/index.ts +0 -8
  118. package/src/utils/error-formatter.ts +205 -0
  119. package/src/utils/http-client.ts +3 -4
  120. package/src/utils/index.ts +6 -5
  121. package/src/utils/local-data-store/jobs-shared-data.ts +0 -2
  122. package/src/utils/mm-labor-ticket-helpers.ts +8 -11
  123. package/src/utils/standard-process-drivers/labor-ticket-erp-synchronizer.ts +64 -220
  124. package/src/utils/standard-process-drivers/mm-entity-processor.ts +1 -7
  125. package/src/utils/standard-process-drivers/standard-process-drivers.ts +19 -33
  126. package/src/utils/time-utils.ts +0 -11
  127. package/dist/types/flattened-work-order.d.ts +0 -99
  128. package/dist/types/flattened-work-order.d.ts.map +0 -1
  129. package/dist/types/flattened-work-order.js +0 -2
  130. package/dist/types/flattened-work-order.js.map +0 -1
  131. package/dist/utils/env.d.ts +0 -8
  132. package/dist/utils/env.d.ts.map +0 -1
  133. package/dist/utils/env.js +0 -58
  134. package/dist/utils/env.js.map +0 -1
  135. package/dist/utils/erp-timezone-utils.d.ts +0 -20
  136. package/dist/utils/erp-timezone-utils.d.ts.map +0 -1
  137. package/dist/utils/erp-timezone-utils.js +0 -75
  138. package/dist/utils/erp-timezone-utils.js.map +0 -1
  139. package/src/types/flattened-work-order.ts +0 -108
  140. package/src/utils/env.ts +0 -75
  141. package/src/utils/erp-timezone-utils.ts +0 -99
@@ -19,7 +19,8 @@ export class MMSendPerson implements IToRESTApiObject {
19
19
  public personId: string,
20
20
  public firstName: string,
21
21
  public lastName: string,
22
- public isActive: boolean
22
+ public isActive: boolean,
23
+ public customFields: Record<string, any> = {}
23
24
  ) {}
24
25
 
25
26
  toRESTApiObject(): Record<string, string> {
@@ -28,6 +29,7 @@ export class MMSendPerson implements IToRESTApiObject {
28
29
  firstName: this.firstName,
29
30
  lastName: this.lastName,
30
31
  isActive: this.isActive ? "1" : "0",
32
+ customFields: JSON.stringify(this.customFields),
31
33
  };
32
34
  }
33
35
 
@@ -36,7 +38,8 @@ export class MMSendPerson implements IToRESTApiObject {
36
38
  data.personId || "",
37
39
  data.firstName || "",
38
40
  data.lastName || "",
39
- data.isActive === "1"
41
+ data.isActive === "1",
42
+ data.customFields ? JSON.parse(data.customFields) : {}
40
43
  );
41
44
  }
42
45
  }
@@ -49,7 +52,8 @@ export class MMSendResource implements IToRESTApiObject {
49
52
  public description: string, // Text description of the resource (optional)
50
53
  public type: string, // The type of resource (optional)
51
54
  public productionBurdenRateHourly: number, // The cost associated with running this machine in production/hour (optional)
52
- public setupBurdenRateHourly: number // The cost associated with running this machine in setup/hour (optional)
55
+ public setupBurdenRateHourly: number, // The cost associated with running this machine in setup/hour (optional)
56
+ public customFields: Record<string, any> = {}
53
57
  ) {}
54
58
 
55
59
  toRESTApiObject(): Record<string, string> {
@@ -61,6 +65,7 @@ export class MMSendResource implements IToRESTApiObject {
61
65
  type: this.type,
62
66
  productionBurdenRateHourly: this.productionBurdenRateHourly.toString(),
63
67
  setupBurdenRateHourly: this.setupBurdenRateHourly.toString(),
68
+ customFields: JSON.stringify(this.customFields),
64
69
  };
65
70
  }
66
71
 
@@ -72,7 +77,8 @@ export class MMSendResource implements IToRESTApiObject {
72
77
  data.description || "",
73
78
  data.type || "",
74
79
  parseFloat(data.productionBurdenRateHourly || "0"),
75
- parseFloat(data.setupBurdenRateHourly || "0")
80
+ parseFloat(data.setupBurdenRateHourly || "0"),
81
+ data.customFields ? JSON.parse(data.customFields) : {}
76
82
  );
77
83
  }
78
84
  }
@@ -81,7 +87,8 @@ export class MMSendPart implements IToRESTApiObject {
81
87
  constructor(
82
88
  public partNumber: string,
83
89
  public partRevision: string,
84
- public method: string
90
+ public method: string,
91
+ public customFields: Record<string, any> = {}
85
92
  ) {}
86
93
 
87
94
  toRESTApiObject(): Record<string, string> {
@@ -89,6 +96,7 @@ export class MMSendPart implements IToRESTApiObject {
89
96
  partNumber: this.partNumber,
90
97
  partRevision: this.partRevision,
91
98
  method: this.method,
99
+ customFields: JSON.stringify(this.customFields),
92
100
  };
93
101
  }
94
102
 
@@ -96,7 +104,8 @@ export class MMSendPart implements IToRESTApiObject {
96
104
  return new MMSendPart(
97
105
  data.partNumber || "",
98
106
  data.partRevision || "",
99
- data.method || ""
107
+ data.method || "",
108
+ data.customFields ? JSON.parse(data.customFields) : {}
100
109
  );
101
110
  }
102
111
  }
@@ -111,7 +120,8 @@ export class MMSendPartOperation implements IToRESTApiObject {
111
120
  public cycleTimeMs: number,
112
121
  public setupTimeMs: number,
113
122
  public description: string,
114
- public quantityPerPart: number
123
+ public quantityPerPart: number,
124
+ public customFields: Record<string, any> = {}
115
125
  ) {}
116
126
 
117
127
  toRESTApiObject(): Record<string, string | null> {
@@ -125,6 +135,7 @@ export class MMSendPartOperation implements IToRESTApiObject {
125
135
  setupTimeMs: this.setupTimeMs.toString(),
126
136
  description: this.description,
127
137
  quantityPerPart: this.quantityPerPart.toString(),
138
+ customFields: JSON.stringify(this.customFields),
128
139
  };
129
140
  }
130
141
 
@@ -140,7 +151,8 @@ export class MMSendPartOperation implements IToRESTApiObject {
140
151
  parseInt(data.cycleTimeMs || "0"),
141
152
  parseInt(data.setupTimeMs || "0"),
142
153
  data.description || "",
143
- parseFloat(data.quantityPerPart || "1")
154
+ parseFloat(data.quantityPerPart || "1"),
155
+ data.customFields ? JSON.parse(data.customFields) : {}
144
156
  );
145
157
  }
146
158
  }
@@ -160,7 +172,8 @@ export class MMSendWorkOrder implements IToRESTApiObject {
160
172
  public quantityRequired: number,
161
173
  public partNumber: string,
162
174
  public partRevision: string,
163
- public method: string
175
+ public method: string,
176
+ public customFields: Record<string, any> = {}
164
177
  ) {}
165
178
 
166
179
  toRESTApiObject(): Record<string, string | null> {
@@ -179,6 +192,7 @@ export class MMSendWorkOrder implements IToRESTApiObject {
179
192
  partNumber: this.partNumber,
180
193
  partRevision: this.partRevision,
181
194
  method: this.method,
195
+ customFields: JSON.stringify(this.customFields),
182
196
  };
183
197
  }
184
198
 
@@ -197,7 +211,8 @@ export class MMSendWorkOrder implements IToRESTApiObject {
197
211
  parseFloat(data.quantityRequired || "0"),
198
212
  data.partNumber || "",
199
213
  data.partRevision || "",
200
- data.method || ""
214
+ data.method || "",
215
+ data.customFields ? JSON.parse(data.customFields) : {}
201
216
  );
202
217
  }
203
218
  }
@@ -222,7 +237,8 @@ export class MMSendWorkOrderOperation implements IToRESTApiObject {
222
237
  public setupburdenRatehourly: number,
223
238
  public operationType: string,
224
239
  public quantityPerPart: number,
225
- public status: string
240
+ public status: string,
241
+ public customFields: Record<string, any> = {}
226
242
  ) {}
227
243
 
228
244
  toRESTApiObject(): Record<string, string | null> {
@@ -246,6 +262,7 @@ export class MMSendWorkOrderOperation implements IToRESTApiObject {
246
262
  operationType: this.operationType,
247
263
  quantityPerPart: this.quantityPerPart.toString(),
248
264
  status: this.status,
265
+ customFields: JSON.stringify(this.customFields),
249
266
  };
250
267
  }
251
268
 
@@ -271,7 +288,8 @@ export class MMSendWorkOrderOperation implements IToRESTApiObject {
271
288
  parseFloat(data.setupburdenRatehourly || "0"),
272
289
  data.operationType || "",
273
290
  parseFloat(data.quantityPerPart || "1"),
274
- data.status || ""
291
+ data.status || "",
292
+ data.customFields ? JSON.parse(data.customFields) : {}
275
293
  );
276
294
  }
277
295
  }
@@ -282,7 +300,8 @@ export class MMSendReason implements IToRESTApiObject {
282
300
  public category: string,
283
301
  public code: string,
284
302
  public description: string,
285
- public entityType: string
303
+ public entityType: string,
304
+ public customFields: Record<string, any> = {}
286
305
  ) {}
287
306
 
288
307
  toRESTApiObject(): Record<string, string> {
@@ -292,6 +311,7 @@ export class MMSendReason implements IToRESTApiObject {
292
311
  code: this.code,
293
312
  description: this.description,
294
313
  entityType: this.entityType,
314
+ customFields: JSON.stringify(this.customFields),
295
315
  };
296
316
  }
297
317
 
@@ -301,7 +321,8 @@ export class MMSendReason implements IToRESTApiObject {
301
321
  data.category || "",
302
322
  data.code || "",
303
323
  data.description || "",
304
- data.entityType || ""
324
+ data.entityType || "",
325
+ data.customFields ? JSON.parse(data.customFields) : {}
305
326
  );
306
327
  }
307
328
  }
@@ -323,7 +344,8 @@ export class MMSendLaborTicket implements IToRESTApiObject {
323
344
  public badParts: number,
324
345
  public type: string,
325
346
  public comment: string,
326
- public state: string
347
+ public state: string,
348
+ public customFields: Record<string, any> = {}
327
349
  ) {}
328
350
 
329
351
  toRESTApiObject(): Record<string, string | null> {
@@ -344,6 +366,7 @@ export class MMSendLaborTicket implements IToRESTApiObject {
344
366
  type: this.type,
345
367
  comment: this.comment,
346
368
  state: this.state,
369
+ customFields: JSON.stringify(this.customFields),
347
370
  };
348
371
  }
349
372
 
@@ -366,7 +389,8 @@ export class MMSendLaborTicket implements IToRESTApiObject {
366
389
  parseInt(data.badParts || "0"),
367
390
  data.type || "",
368
391
  data.comment || "",
369
- data.state || ""
392
+ data.state || "",
393
+ data.customFields ? JSON.parse(data.customFields) : {}
370
394
  );
371
395
  }
372
396
  }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * NATS Service - Central connection and subscription management
3
+ * Allows connectors to register handlers for different NATS subjects
4
+ */
5
+
6
+ import { connect, NatsConnection, StringCodec, Subscription } from "nats";
7
+ import { logger } from "../reporting-service/index.js";
8
+
9
+ const sc = StringCodec();
10
+
11
+ export interface NatsMessageHandler {
12
+ /**
13
+ * Handler function for NATS messages
14
+ * - Return a value to reply (if msg has reply subject)
15
+ * - Return void/undefined for pub-sub (no reply)
16
+ */
17
+ handle: (data: any, subject: string) => Promise<any | void>;
18
+ }
19
+
20
+ export interface NatsHandlerRegistration {
21
+ /**
22
+ * Subject pattern to subscribe to (supports wildcards: *, >)
23
+ * Examples:
24
+ * - "mm.16.epic01.labor-ticket.*" (single level wildcard)
25
+ * - "mm.16.epic01.*" (multi-level wildcard)
26
+ */
27
+ subject: string;
28
+
29
+ /**
30
+ * Handler for this subject
31
+ */
32
+ handler: NatsMessageHandler;
33
+
34
+ /**
35
+ * Optional description for logging/debugging
36
+ */
37
+ description?: string;
38
+ }
39
+
40
+ export interface NatsServiceConfig {
41
+ /**
42
+ * NATS server URLs
43
+ */
44
+ servers: string | string[];
45
+
46
+ /**
47
+ * Connection name (for monitoring)
48
+ */
49
+ name: string;
50
+
51
+ /**
52
+ * Location reference (for subject namespacing)
53
+ */
54
+ locationRef: string;
55
+
56
+ /**
57
+ * ERP type (epicor, infor, etc.)
58
+ */
59
+ erpType: string;
60
+
61
+ /**
62
+ * Enable/disable NATS
63
+ */
64
+ enabled: boolean;
65
+
66
+ /**
67
+ * Auto-reconnect settings
68
+ */
69
+ reconnect?: boolean;
70
+ maxReconnectAttempts?: number;
71
+ reconnectTimeWait?: number;
72
+ }
73
+
74
+ export class NatsService {
75
+ private connection: NatsConnection | null = null;
76
+ private subscriptions: Map<string, Subscription> = new Map();
77
+ private config: NatsServiceConfig;
78
+ private handlers: NatsHandlerRegistration[] = [];
79
+ private statusPublishTimer: NodeJS.Timeout | null = null;
80
+
81
+ constructor(config: NatsServiceConfig) {
82
+ this.config = config;
83
+ }
84
+
85
+ /**
86
+ * Register a handler for a specific subject pattern
87
+ */
88
+ registerHandler(registration: NatsHandlerRegistration): void {
89
+ logger.info("Registering NATS handler", {
90
+ subject: registration.subject,
91
+ description: registration.description,
92
+ });
93
+ this.handlers.push(registration);
94
+ }
95
+
96
+ /**
97
+ * Connect to NATS and start all registered handlers
98
+ */
99
+ async connect(): Promise<void> {
100
+ if (!this.config.enabled) {
101
+ logger.info("NATS is disabled, skipping connection");
102
+ return;
103
+ }
104
+
105
+ try {
106
+ logger.info("Connecting to NATS...", {
107
+ servers: this.config.servers,
108
+ name: this.config.name,
109
+ });
110
+
111
+ this.connection = await connect({
112
+ servers: this.config.servers,
113
+ name: this.config.name,
114
+ reconnect: this.config.reconnect ?? true,
115
+ maxReconnectAttempts: this.config.maxReconnectAttempts ?? -1,
116
+ reconnectTimeWait: this.config.reconnectTimeWait ?? 2000,
117
+ });
118
+
119
+ logger.info("Connected to NATS", {
120
+ server: this.connection.getServer(),
121
+ clientId: this.connection.info?.client_id,
122
+ });
123
+
124
+ // Start all registered handlers
125
+ for (const registration of this.handlers) {
126
+ await this.startHandler(registration);
127
+ }
128
+
129
+ // Start automatic status publishing
130
+ this.startStatusPublishing();
131
+
132
+ // Monitor connection status
133
+ this.monitorConnection();
134
+
135
+ // Graceful shutdown
136
+ this.setupShutdown();
137
+ } catch (error) {
138
+ logger.error("Failed to connect to NATS", { error });
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Start a single handler (subscribe to its subject)
145
+ */
146
+ private async startHandler(registration: NatsHandlerRegistration): Promise<void> {
147
+ if (!this.connection) {
148
+ throw new Error("NATS connection not established");
149
+ }
150
+
151
+ const sub = this.connection.subscribe(registration.subject);
152
+ this.subscriptions.set(registration.subject, sub);
153
+
154
+ logger.info("Started NATS handler", {
155
+ subject: registration.subject,
156
+ description: registration.description,
157
+ });
158
+
159
+ // Process messages
160
+ (async () => {
161
+ for await (const msg of sub) {
162
+ try {
163
+ const data = sc.decode(msg.data);
164
+
165
+ logger.info("Received NATS message", {
166
+ subject: msg.subject,
167
+ hasReply: !!msg.reply,
168
+ });
169
+
170
+ // Parse JSON if possible
171
+ let parsedData: any;
172
+ try {
173
+ parsedData = JSON.parse(data);
174
+ } catch {
175
+ parsedData = data;
176
+ }
177
+
178
+ // Call handler
179
+ const response = await registration.handler.handle(parsedData, msg.subject);
180
+
181
+ // If there's a reply subject and handler returned something, reply
182
+ if (msg.reply && response !== undefined) {
183
+ const responseStr = JSON.stringify(response);
184
+ msg.respond(sc.encode(responseStr));
185
+ logger.info("Sent reply", { replySubject: msg.reply });
186
+ }
187
+ } catch (error) {
188
+ logger.error("Error handling NATS message", {
189
+ subject: msg.subject,
190
+ error,
191
+ });
192
+
193
+ // Send error response if reply expected
194
+ if (msg.reply) {
195
+ const errorResponse = {
196
+ status: "error",
197
+ error: {
198
+ message: error instanceof Error ? error.message : "Unknown error",
199
+ code: "HANDLER_ERROR",
200
+ },
201
+ };
202
+ msg.respond(sc.encode(JSON.stringify(errorResponse)));
203
+ }
204
+ }
205
+ }
206
+ })();
207
+ }
208
+
209
+ /**
210
+ * Publish a message to a subject (for pub/sub)
211
+ */
212
+ async publish(subject: string, data: any): Promise<void> {
213
+ if (!this.connection) {
214
+ throw new Error("NATS connection not established");
215
+ }
216
+
217
+ const message = typeof data === "string" ? data : JSON.stringify(data);
218
+ this.connection.publish(subject, sc.encode(message));
219
+
220
+ logger.info("Published NATS message", { subject });
221
+ }
222
+
223
+ /**
224
+ * Send a request and wait for reply (for request-reply)
225
+ */
226
+ async request(subject: string, data: any, timeoutMs: number = 30000): Promise<any> {
227
+ if (!this.connection) {
228
+ throw new Error("NATS connection not established");
229
+ }
230
+
231
+ const message = typeof data === "string" ? data : JSON.stringify(data);
232
+ const response = await this.connection.request(
233
+ subject,
234
+ sc.encode(message),
235
+ { timeout: timeoutMs }
236
+ );
237
+
238
+ const responseData = sc.decode(response.data);
239
+
240
+ try {
241
+ return JSON.parse(responseData);
242
+ } catch {
243
+ return responseData;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Check if connected to NATS
249
+ */
250
+ isConnected(): boolean {
251
+ return this.connection !== null && !this.connection.isClosed();
252
+ }
253
+
254
+ /**
255
+ * Start automatic status publishing (every 30 seconds)
256
+ */
257
+ private startStatusPublishing(): void {
258
+ logger.info("Starting status publishing (every 30 seconds)");
259
+
260
+ // Publish immediately on start
261
+ this.publishStatus();
262
+
263
+ // Then publish every 30 seconds
264
+ this.statusPublishTimer = setInterval(() => {
265
+ this.publishStatus();
266
+ }, 30000);
267
+ }
268
+
269
+ /**
270
+ * Publish connector status
271
+ */
272
+ private async publishStatus(): Promise<void> {
273
+ try {
274
+ const status = {
275
+ timestamp: new Date().toISOString(),
276
+ locationRef: this.config.locationRef,
277
+ erpType: this.config.erpType,
278
+ natsConnected: this.isConnected(),
279
+ };
280
+
281
+ await this.publish(
282
+ `mm.16.${this.config.locationRef}.status`,
283
+ status
284
+ );
285
+
286
+ logger.debug("Published connector status");
287
+ } catch (error) {
288
+ logger.error("Failed to publish status", { error });
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Monitor connection status
294
+ */
295
+ private monitorConnection(): void {
296
+ if (!this.connection) return;
297
+
298
+ (async () => {
299
+ for await (const status of this.connection!.status()) {
300
+ // Only log important events, skip pingTimer
301
+ if (status.type !== "pingTimer") {
302
+ logger.info("NATS connection status", {
303
+ type: status.type,
304
+ data: status.data,
305
+ });
306
+ }
307
+ }
308
+ })();
309
+ }
310
+
311
+ /**
312
+ * Setup graceful shutdown
313
+ */
314
+ private setupShutdown(): void {
315
+ const shutdown = async () => {
316
+ logger.info("Shutting down NATS service...");
317
+ await this.disconnect();
318
+ process.exit(0);
319
+ };
320
+
321
+ process.on("SIGINT", shutdown);
322
+ process.on("SIGTERM", shutdown);
323
+ }
324
+
325
+ /**
326
+ * Disconnect from NATS
327
+ */
328
+ async disconnect(): Promise<void> {
329
+ // Stop status publishing
330
+ if (this.statusPublishTimer) {
331
+ clearInterval(this.statusPublishTimer);
332
+ this.statusPublishTimer = null;
333
+ }
334
+
335
+ // Close connection
336
+ if (this.connection) {
337
+ await this.connection.drain();
338
+ this.connection = null;
339
+ this.subscriptions.clear();
340
+ logger.info("Disconnected from NATS");
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Get the location reference
346
+ */
347
+ getLocationRef(): string {
348
+ return this.config.locationRef;
349
+ }
350
+ }
351
+
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Simple proof-of-concept NATS subscriber
3
+ * Subscribes to mm.16.> and replies with "hello world"
4
+ */
5
+
6
+ import { connect, StringCodec } from "nats";
7
+ import { logger } from "../../services/reporting-service/index.js";
8
+
9
+ const sc = StringCodec();
10
+
11
+ async function startTestSubscriber() {
12
+ try {
13
+ // Connect to NATS server
14
+ const nc = await connect({
15
+ servers: "nats://localhost:4222",
16
+ name: "mm-erp-sdk-test-subscriber",
17
+ });
18
+
19
+ logger.info("Connected to NATS server", {
20
+ server: nc.getServer(),
21
+ clientId: nc.info?.client_id,
22
+ });
23
+
24
+ // Subscribe to all messages on mm.16.>
25
+ const sub = nc.subscribe("mm.16.>");
26
+
27
+ logger.info("Subscribed to mm.16.> - waiting for messages...");
28
+
29
+ // Process incoming messages
30
+ (async () => {
31
+ for await (const msg of sub) {
32
+ const data = sc.decode(msg.data);
33
+
34
+ logger.info("📨 Received NATS message", {
35
+ subject: msg.subject,
36
+ data: data,
37
+ reply: msg.reply,
38
+ });
39
+
40
+ console.log("\n=== NATS MESSAGE RECEIVED ===");
41
+ console.log("Subject:", msg.subject);
42
+ console.log("Data:", data);
43
+ console.log("Reply subject:", msg.reply);
44
+ console.log("============================\n");
45
+
46
+ // Reply if there's a reply subject
47
+ if (msg.reply) {
48
+ const replyMessage = JSON.stringify({
49
+ message: "hello world",
50
+ timestamp: new Date().toISOString(),
51
+ receivedSubject: msg.subject,
52
+ receivedData: data,
53
+ });
54
+
55
+ msg.respond(sc.encode(replyMessage));
56
+
57
+ logger.info("✅ Sent reply: hello world", {
58
+ replySubject: msg.reply,
59
+ });
60
+
61
+ console.log("✅ Replied with: hello world\n");
62
+ }
63
+ }
64
+ })();
65
+
66
+ // Handle connection events
67
+ (async () => {
68
+ for await (const status of nc.status()) {
69
+ logger.info("NATS connection status", {
70
+ type: status.type,
71
+ data: status.data,
72
+ });
73
+ console.log(`Connection status: ${status.type}`, status.data);
74
+ }
75
+ })();
76
+
77
+ // Graceful shutdown
78
+ const shutdown = async () => {
79
+ logger.info("Shutting down NATS subscriber...");
80
+ await nc.drain();
81
+ process.exit(0);
82
+ };
83
+
84
+ process.on("SIGINT", shutdown);
85
+ process.on("SIGTERM", shutdown);
86
+
87
+ } catch (error) {
88
+ logger.error("Failed to connect to NATS", { error });
89
+ console.error("Error connecting to NATS:", error);
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ // Start the subscriber
95
+ startTestSubscriber();
96
+