@ontemper/edi 1.1.6 → 1.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Temper EDI Client
2
2
 
3
- A TypeScript EDI client with structured logging and distributed tracing capabilities, built for processing X12 EDI documents with comprehensive error handling and automatic span tracking.
3
+ A TypeScript EDI client with structured logging and distributed tracing capabilities, built for processing X12 (including HIPAA) and EDIFACT documents with comprehensive error handling and automatic span tracking.
4
4
 
5
5
  ## Features
6
6
 
7
- - **X12 EDI Processing**: Complete X12 document parsing, validation, and generation
7
+ - **X12 EDI Processing**: Complete X12 document parsing, validation, and generation — HIPAA guide models resolve automatically from the ST03 implementation reference
8
+ - **EDIFACT Processing**: `fromEdifact` / `toEdifact` / `validateEdifact` / `acknowledgeEdifact` with the same error handling, tracing, and automatic control-reference stamping (UNB-0020 from the central counter service, UNH-0062 local sequence)
8
9
  - **Structured Logging**: Built-in integration with Temper Logger for comprehensive operation tracking
9
10
  - **Distributed Tracing**: Automatic span creation for all EDI operations with detailed payload information
10
11
  - **Type Safety**: Full TypeScript support with comprehensive type definitions
@@ -54,10 +55,15 @@ const x12Document = await edi.toX12({
54
55
  input: x12Interchanges[0],
55
56
  });
56
57
 
57
- // Generate acknowledgment
58
- const acknowledgment = await edi.acknowledgeX12({
58
+ // Generate and serialize acknowledgments
59
+ const acknowledgments = await edi.acknowledgeX12({
59
60
  input: x12Interchanges[0],
60
61
  });
62
+
63
+ for (const acknowledgment of acknowledgments) {
64
+ // Apply partner-specific outbound envelope IDs here when needed.
65
+ const acknowledgmentX12 = await edi.toX12({ input: acknowledgment });
66
+ }
61
67
  ```
62
68
 
63
69
  ## API Reference
@@ -104,7 +110,7 @@ const x12Document = await edi.toX12({
104
110
 
105
111
  **Returns:** X12 document as string
106
112
 
107
- **Control number auto-stamping:** Leave `InterchangeControlNumber_13`, `GroupControlNumber_6`, and `TransactionSetControlNumber_02` empty or omit them. The SDK fetches atomic, per-trading-partner control numbers from a centralized counter service. If values are already set, they are left unchanged.
113
+ **Control number auto-stamping:** Leave `InterchangeControlNumber_13`, `GroupControlNumber_6`, and `TransactionSetControlNumber_02` empty or omit them. Empty and all-zero ISA13, GS06, or ST02 values are treated as unassigned. Valid non-zero values are preserved.
108
114
 
109
115
  #### `validateX12(options: ValidateX12Options): Promise<OperationResult>`
110
116
 
@@ -122,21 +128,29 @@ const validationResult = await edi.validateX12({
122
128
 
123
129
  **Returns:** Validation result with operation details
124
130
 
125
- #### `acknowledgeX12(options: AcknolwedgeX12Options): Promise<OperationResult>`
131
+ #### `acknowledgeX12(options: AcknolwedgeX12Options): Promise<X12Interchange[]>`
126
132
 
127
- Generates acknowledgment for X12 interchange objects.
133
+ Generates acknowledgment interchange objects. Apply any partner-specific outbound envelope IDs, then serialize
134
+ each acknowledgment with `toX12()`. During serialization, `toX12()` assigns control numbers using the final
135
+ sender and receiver. The method always returns an array because one input can produce more than one
136
+ acknowledgment.
128
137
 
129
138
  ```typescript
130
- const acknowledgment = await edi.acknowledgeX12({
139
+ const acknowledgments = await edi.acknowledgeX12({
131
140
  input: x12InterchangeObject,
132
141
  });
142
+
143
+ for (const acknowledgment of acknowledgments) {
144
+ // Apply partner-specific outbound envelope IDs here when needed.
145
+ const serialized = await edi.toX12({ input: acknowledgment });
146
+ }
133
147
  ```
134
148
 
135
149
  **Parameters:**
136
150
 
137
151
  - `options.input` - The X12 interchange object to acknowledge
138
152
 
139
- **Returns:** Acknowledgment result with operation details
153
+ **Returns:** The generated acknowledgment interchanges
140
154
 
141
155
  ## Error Handling
142
156
 
@@ -320,9 +334,13 @@ IEA*1*000001000`;
320
334
  const x12Output = await edi.toX12({ input: interchange });
321
335
  logger.info({ x12Output }, 'Converted back to X12');
322
336
 
323
- // Generate acknowledgment
324
- const acknowledgment = await edi.acknowledgeX12({ input: interchange });
325
- logger.info({ acknowledgment }, 'Generated acknowledgment');
337
+ // Generate and serialize acknowledgments. toX12 assigns control numbers
338
+ // after any partner-specific outbound envelope changes.
339
+ const acknowledgments = await edi.acknowledgeX12({ input: interchange });
340
+ for (const acknowledgment of acknowledgments) {
341
+ const acknowledgmentX12 = await edi.toX12({ input: acknowledgment });
342
+ logger.info({ acknowledgmentX12 }, 'Generated acknowledgment');
343
+ }
326
344
  }
327
345
  } catch (error) {
328
346
  logger.error({ err: error }, 'EDI processing failed');
@@ -350,7 +368,9 @@ const current = await getCurrentControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER
350
368
  await setControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER', 1000);
351
369
  ```
352
370
 
353
- In most cases you don't need these directly — `toX12()` calls `getNextControlNumber` automatically for any empty ISA13/GS06 fields.
371
+ In most cases you don't need these directly — `toX12()` automatically stamps any empty or all-zero
372
+ ISA13/GS06/ST02 fields. ISA13 and GS06 use `getNextControlNumber`; ST02 is assigned `"0001"`. This includes the
373
+ zero placeholders returned by acknowledgment generation.
354
374
  Once a counter has incremented, the API rejects later `setControlNumber` calls to avoid resetting active sequences.
355
375
  Retried `setControlNumber` calls with the same already-stored seed are treated as idempotent no-ops.
356
376
  The SDK surfaces that as a `ControlNumberError` with `code: "counter_already_started"`:
@@ -123,7 +123,7 @@ function unwrapControlNumberError(error) {
123
123
  */
124
124
  /** @public */
125
125
  async function getNextControlNumber(counterName, senderId, receiverId, maxDigits = 9) {
126
- const { data } = await getApiClient().post('/api/internal/edi/control-numbers/increment', {
126
+ const { data } = await getApiClient().post('/api/v2/internal/edi/control-numbers/increment', {
127
127
  tradingPartnerKey: `${senderId}:${receiverId}`,
128
128
  counterName,
129
129
  });
@@ -134,7 +134,7 @@ async function getNextControlNumber(counterName, senderId, receiverId, maxDigits
134
134
  */
135
135
  /** @public */
136
136
  async function getCurrentControlNumber(counterName, senderId, receiverId, maxDigits = 9) {
137
- const { data } = await getApiClient().get('/api/internal/edi/control-numbers/count', {
137
+ const { data } = await getApiClient().get('/api/v2/internal/edi/control-numbers/count', {
138
138
  params: {
139
139
  tradingPartnerKey: `${senderId}:${receiverId}`,
140
140
  counterName,
@@ -148,7 +148,7 @@ async function getCurrentControlNumber(counterName, senderId, receiverId, maxDig
148
148
  /** @public */
149
149
  async function setControlNumber(counterName, senderId, receiverId, value, maxDigits = 9) {
150
150
  const { data } = await getApiClient()
151
- .post('/api/internal/edi/control-numbers/set', {
151
+ .post('/api/v2/internal/edi/control-numbers/set', {
152
152
  tradingPartnerKey: `${senderId}:${receiverId}`,
153
153
  counterName,
154
154
  value,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type X12Interchange } from './edination-client';
1
+ import { type EdifactInterchange, type X12Interchange } from './edination-client';
2
2
  export type { ControlNumberErrorCode } from './control-numbers';
3
3
  export { ControlNumberError, getCurrentControlNumber, getNextControlNumber, isControlNumberError, setControlNumber, } from './control-numbers';
4
4
  export * from './edination-client/model';
@@ -35,20 +35,48 @@ export interface ValidateX12Options {
35
35
  export interface AcknolwedgeX12Options {
36
36
  input: X12Interchange;
37
37
  }
38
+ export interface FromEdifactOptions {
39
+ input: unknown;
40
+ }
41
+ export interface ToEdifactOptions {
42
+ input: EdifactInterchange;
43
+ }
44
+ export interface ValidateEdifactOptions {
45
+ input: EdifactInterchange;
46
+ }
47
+ export interface AcknowledgeEdifactOptions {
48
+ input: EdifactInterchange;
49
+ }
38
50
  export declare class TemperEdiClient {
39
51
  private X12;
52
+ private Edifact;
40
53
  constructor();
41
54
  private unwrap;
42
55
  private unwrapError;
43
56
  fromX12({ input }: FromX12Options): Promise<X12Interchange[]>;
44
57
  /**
45
- * Stamp ISA13/GS06 control numbers on the interchange if not already set.
58
+ * Stamp ISA13/GS06/ST02 control numbers on the interchange if not already set.
46
59
  * Uses the Restate-backed counter service for atomic, per-trading-partner sequencing.
47
60
  * ST02 is always "0001" (one transaction per file per ticket).
48
- * If a control number is already set, it is left unchanged (opt-out for manual overrides).
61
+ * Zero-filled values from acknowledgment generation are placeholders, not manual overrides.
49
62
  */
50
63
  private stampControlNumbers;
51
64
  toX12({ input }: ToX12Options): Promise<unknown>;
52
65
  validateX12({ input }: ValidateX12Options): Promise<import("./edination-client").OperationResult>;
53
66
  acknowledgeX12({ input }: AcknolwedgeX12Options): Promise<X12Interchange[]>;
67
+ fromEdifact({ input }: FromEdifactOptions): Promise<EdifactInterchange[]>;
68
+ /**
69
+ * Stamp UNB-0020/UNH-0062 control references on the interchange if not already set.
70
+ *
71
+ * UNB-0020 (interchange control reference) comes from the same Restate-backed
72
+ * per-trading-partner counter service X12 uses, under the platform counter name
73
+ * `UNB0020` — unpadded, since EDIFACT 0020 is an..14 with no fixed width. The
74
+ * UNZ trailer echo is set to match. UNH-0062 (message reference number) only
75
+ * needs uniqueness within the interchange, so it is a local 1..n sequence with
76
+ * no service call, echoed into each UNT — the EDIFACT sibling of ST02="0001".
77
+ */
78
+ private stampEdifactControlNumbers;
79
+ toEdifact({ input }: ToEdifactOptions): Promise<unknown>;
80
+ validateEdifact({ input }: ValidateEdifactOptions): Promise<import("./edination-client").OperationResult>;
81
+ acknowledgeEdifact({ input }: AcknowledgeEdifactOptions): Promise<EdifactInterchange[]>;
54
82
  }
package/dist/index.js CHANGED
@@ -79,10 +79,15 @@ exports.EdiInfrastructureError = EdiInfrastructureError;
79
79
  const isEdiInfrastructureError = (error) => error instanceof EdiInfrastructureError;
80
80
  exports.isEdiInfrastructureError = isEdiInfrastructureError;
81
81
  const buildEdiPayload = (edi) => ({ type: 'edi', edi });
82
- const buildEdiX12Payload = (operation, x12) => buildEdiPayload({ operation, type: 'x12', x12 });
82
+ const buildEdiTransactionPayload = (operation, format, transaction) => buildEdiPayload({ operation, type: format, transaction });
83
+ const needsControlNumber = (value) => {
84
+ const normalized = value?.trim();
85
+ return !normalized || /^0+$/.test(normalized);
86
+ };
83
87
  const ediAxios = (0, unnbound_logger_sdk_1.traceAxios)(axios_1.default.create(), { getPayload: internal_1.internal });
84
88
  class TemperEdiClient {
85
89
  X12;
90
+ Edifact;
86
91
  constructor() {
87
92
  const apiKey = process.env.UNNBOUND_EDI_API_KEY;
88
93
  // When UNNBOUND_EDI_BASE_URL is set, use self-hosted EdiFabric InHouse API
@@ -95,6 +100,7 @@ class TemperEdiClient {
95
100
  // but the OpenAPI client requires a non-empty value — use a placeholder.
96
101
  const config = new edination_client_1.Configuration({ apiKey: apiKey ?? 'self-hosted', basePath });
97
102
  this.X12 = new edination_client_1.X12Api(config, undefined, ediAxios);
103
+ this.Edifact = new edination_client_1.EdifactApi(config, undefined, ediAxios);
98
104
  }
99
105
  unwrap(response) {
100
106
  return response.data;
@@ -141,13 +147,13 @@ class TemperEdiClient {
141
147
  return this.X12.x12ReadPost({ body: input })
142
148
  .then(this.unwrap.bind(this))
143
149
  .catch((error) => this.unwrapError(error, 'edi_read_error'));
144
- }, (o) => buildEdiX12Payload('fromX12', { input, output: o?.result }));
150
+ }, (o) => buildEdiTransactionPayload('fromX12', 'x12', { input, output: o?.result }));
145
151
  }
146
152
  /**
147
- * Stamp ISA13/GS06 control numbers on the interchange if not already set.
153
+ * Stamp ISA13/GS06/ST02 control numbers on the interchange if not already set.
148
154
  * Uses the Restate-backed counter service for atomic, per-trading-partner sequencing.
149
155
  * ST02 is always "0001" (one transaction per file per ticket).
150
- * If a control number is already set, it is left unchanged (opt-out for manual overrides).
156
+ * Zero-filled values from acknowledgment generation are placeholders, not manual overrides.
151
157
  */
152
158
  async stampControlNumbers(input) {
153
159
  const { getNextControlNumber } = await import('./control-numbers.js');
@@ -160,12 +166,13 @@ class TemperEdiClient {
160
166
  const receiverKey = `${receiverQual}:${receiverId}`;
161
167
  // Phase 1: Fetch all needed control numbers (no mutations yet).
162
168
  // If any fetch fails, no fields are mutated — avoids partial stamping.
163
- const isa13 = !isa.InterchangeControlNumber_13
169
+ const isa13 = needsControlNumber(isa.InterchangeControlNumber_13)
164
170
  ? await getNextControlNumber('ISA13', senderKey, receiverKey, 9)
165
171
  : null;
166
172
  const gs06Values = [];
167
- for (let i = 0; i < input.Groups.length; i++) {
168
- if (!input.Groups[i].GS.GroupControlNumber_6) {
173
+ const groups = input.Groups ?? [];
174
+ for (let i = 0; i < groups.length; i++) {
175
+ if (needsControlNumber(groups[i].GS.GroupControlNumber_6)) {
169
176
  const value = await getNextControlNumber('GS06', senderKey, receiverKey, 9);
170
177
  gs06Values.push({ index: i, value });
171
178
  }
@@ -178,16 +185,16 @@ class TemperEdiClient {
178
185
  }
179
186
  }
180
187
  for (const { index, value } of gs06Values) {
181
- const group = input.Groups[index];
188
+ const group = groups[index];
182
189
  group.GS.GroupControlNumber_6 = value;
183
190
  if (group.GETrailers?.length) {
184
191
  group.GETrailers[0].GroupControlNumber_2 = value;
185
192
  }
186
193
  }
187
194
  // ST02 — always "0001" (pure assignment, no external calls)
188
- for (const group of input.Groups) {
195
+ for (const group of groups) {
189
196
  for (const tx of group.Transactions) {
190
- if (!tx?.ST?.TransactionSetControlNumber_02) {
197
+ if (needsControlNumber(tx?.ST?.TransactionSetControlNumber_02)) {
191
198
  if (tx?.ST)
192
199
  tx.ST.TransactionSetControlNumber_02 = '0001';
193
200
  if (tx?.SE)
@@ -210,7 +217,7 @@ class TemperEdiClient {
210
217
  return this.X12.x12WritePost({ x12Interchange: input })
211
218
  .then(this.unwrap.bind(this))
212
219
  .catch((error) => this.unwrapError(error, 'edi_write_error'));
213
- }, (o) => buildEdiX12Payload('toX12', { input, output: o?.result }));
220
+ }, (o) => buildEdiTransactionPayload('toX12', 'x12', { input, output: o?.result }));
214
221
  }
215
222
  validateX12({ input }) {
216
223
  return (0, unnbound_logger_sdk_1.startSpan)('Validate X12', () => {
@@ -218,7 +225,7 @@ class TemperEdiClient {
218
225
  .then(this.unwrap.bind(this))
219
226
  .catch((error) => this.unwrapError(error, 'edi_validate_error'));
220
227
  }, (o) => ({
221
- ...buildEdiX12Payload('validateX12', { input, output: o?.result }),
228
+ ...buildEdiTransactionPayload('validateX12', 'x12', { input, output: o?.result }),
222
229
  ...(o?.result?.Status === 'error' && { level: 'warn' }),
223
230
  }));
224
231
  }
@@ -227,7 +234,87 @@ class TemperEdiClient {
227
234
  return this.X12.x12AckPost({ x12Interchange: input })
228
235
  .then(this.unwrap.bind(this))
229
236
  .catch((error) => this.unwrapError(error, 'edi_acknowledge_error'));
230
- }, (o) => buildEdiX12Payload('acknowledgeX12', { input, output: o?.result }));
237
+ }, (o) => buildEdiTransactionPayload('acknowledgeX12', 'x12', { input, output: o?.result }));
238
+ }
239
+ fromEdifact({ input }) {
240
+ return (0, unnbound_logger_sdk_1.startSpan)('EDIFACT to JSON', () => {
241
+ return this.Edifact.edifactReadPost({ body: input })
242
+ .then(this.unwrap.bind(this))
243
+ .catch((error) => this.unwrapError(error, 'edi_read_error'));
244
+ }, (o) => buildEdiTransactionPayload('fromEdifact', 'edifact', { input, output: o?.result }));
245
+ }
246
+ /**
247
+ * Stamp UNB-0020/UNH-0062 control references on the interchange if not already set.
248
+ *
249
+ * UNB-0020 (interchange control reference) comes from the same Restate-backed
250
+ * per-trading-partner counter service X12 uses, under the platform counter name
251
+ * `UNB0020` — unpadded, since EDIFACT 0020 is an..14 with no fixed width. The
252
+ * UNZ trailer echo is set to match. UNH-0062 (message reference number) only
253
+ * needs uniqueness within the interchange, so it is a local 1..n sequence with
254
+ * no service call, echoed into each UNT — the EDIFACT sibling of ST02="0001".
255
+ */
256
+ async stampEdifactControlNumbers(input) {
257
+ const { getNextControlNumber } = await import('./control-numbers.js');
258
+ const sender = input.UNB?.INTERCHANGESENDER_2;
259
+ const recipient = input.UNB?.INTERCHANGERECIPIENT_3;
260
+ const senderKey = `${(sender?.IdentificationCodeQualifier_2 || '').trim()}:${(sender?.InterchangeSenderIdentification_1 || '').trim()}`;
261
+ const receiverKey = `${(recipient?.IdentificationCodeQualifier_2 || '').trim()}:${(recipient?.InterchangeRecipientIdentification_1 || '').trim()}`;
262
+ // Fetch before mutating — if the fetch fails, no fields change.
263
+ const unb0020 = needsControlNumber(input.UNB?.InterchangeControlReference_5)
264
+ ? await getNextControlNumber('UNB0020', senderKey, receiverKey, 0)
265
+ : null;
266
+ if (unb0020) {
267
+ input.UNB.InterchangeControlReference_5 = unb0020;
268
+ if (input.UNZTrailers?.length) {
269
+ input.UNZTrailers[0].InterchangeControlReference_2 = unb0020;
270
+ }
271
+ }
272
+ // UNH-0062 — local sequence across the interchange's messages (no external calls)
273
+ let messageReference = 0;
274
+ for (const group of input.Groups ?? []) {
275
+ for (const message of group.Transactions ?? []) {
276
+ messageReference += 1;
277
+ if (needsControlNumber(message?.UNH?.MessageReferenceNumber_01)) {
278
+ if (message?.UNH)
279
+ message.UNH.MessageReferenceNumber_01 = String(messageReference);
280
+ if (message?.UNT)
281
+ message.UNT.MessageReferenceNumber_02 = String(messageReference);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ async toEdifact({ input }) {
287
+ // Auto-stamp control references before writing EDIFACT
288
+ try {
289
+ await this.stampEdifactControlNumbers(input);
290
+ }
291
+ catch (error) {
292
+ // Log but don't fail — workflows can still set control references manually
293
+ const { logger } = await import('unnbound-logger-sdk');
294
+ logger.warn({ err: error }, '[EDI SDK] Failed to auto-stamp control references, proceeding without');
295
+ }
296
+ return (0, unnbound_logger_sdk_1.startSpan)('JSON to EDIFACT', () => {
297
+ return this.Edifact.edifactWritePost({ edifactInterchange: input })
298
+ .then(this.unwrap.bind(this))
299
+ .catch((error) => this.unwrapError(error, 'edi_write_error'));
300
+ }, (o) => buildEdiTransactionPayload('toEdifact', 'edifact', { input, output: o?.result }));
301
+ }
302
+ validateEdifact({ input }) {
303
+ return (0, unnbound_logger_sdk_1.startSpan)('Validate EDIFACT', () => {
304
+ return this.Edifact.edifactValidatePost({ edifactInterchange: input })
305
+ .then(this.unwrap.bind(this))
306
+ .catch((error) => this.unwrapError(error, 'edi_validate_error'));
307
+ }, (o) => ({
308
+ ...buildEdiTransactionPayload('validateEdifact', 'edifact', { input, output: o?.result }),
309
+ ...(o?.result?.Status === 'error' && { level: 'warn' }),
310
+ }));
311
+ }
312
+ acknowledgeEdifact({ input }) {
313
+ return (0, unnbound_logger_sdk_1.startSpan)('Acknowledge EDIFACT', () => {
314
+ return this.Edifact.edifactAckPost({ edifactInterchange: input })
315
+ .then(this.unwrap.bind(this))
316
+ .catch((error) => this.unwrapError(error, 'edi_acknowledge_error'));
317
+ }, (o) => buildEdiTransactionPayload('acknowledgeEdifact', 'edifact', { input, output: o?.result }));
231
318
  }
232
319
  }
233
320
  exports.TemperEdiClient = TemperEdiClient;
@@ -46,9 +46,13 @@ IEA*1*000001000`;
46
46
 
47
47
  logger.info({ validated }, 'X12 validated.');
48
48
 
49
- const acknowledged = await edi.acknowledgeX12({ input: x12Interchange });
49
+ const acknowledgments = await edi.acknowledgeX12({ input: x12Interchange });
50
50
 
51
- logger.info({ acknowledged }, 'X12 acknowledged.');
51
+ for (const acknowledgment of acknowledgments) {
52
+ // Apply partner-specific outbound envelope IDs before serialization when needed.
53
+ const acknowledgmentX12 = await edi.toX12({ input: acknowledgment });
54
+ logger.info({ acknowledgmentX12 }, 'X12 acknowledgment generated.');
55
+ }
52
56
  }),
53
57
  );
54
58
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ontemper/edi",
3
3
  "description": "An EDI client with structured logging.",
4
- "version": "1.1.6",
4
+ "version": "1.2.0-beta.1",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "author": "Unnbound Team",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "axios": "1.16.0",
19
- "unnbound-logger-sdk": "3.0.37"
19
+ "unnbound-logger-sdk": "3.2.0-beta.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/jest": "^29.5.12",