@ontemper/edi 1.1.5 → 1.1.7

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
@@ -54,10 +54,15 @@ const x12Document = await edi.toX12({
54
54
  input: x12Interchanges[0],
55
55
  });
56
56
 
57
- // Generate acknowledgment
58
- const acknowledgment = await edi.acknowledgeX12({
57
+ // Generate and serialize acknowledgments
58
+ const acknowledgments = await edi.acknowledgeX12({
59
59
  input: x12Interchanges[0],
60
60
  });
61
+
62
+ for (const acknowledgment of acknowledgments) {
63
+ // Apply partner-specific outbound envelope IDs here when needed.
64
+ const acknowledgmentX12 = await edi.toX12({ input: acknowledgment });
65
+ }
61
66
  ```
62
67
 
63
68
  ## API Reference
@@ -104,7 +109,7 @@ const x12Document = await edi.toX12({
104
109
 
105
110
  **Returns:** X12 document as string
106
111
 
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.
112
+ **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
113
 
109
114
  #### `validateX12(options: ValidateX12Options): Promise<OperationResult>`
110
115
 
@@ -122,30 +127,36 @@ const validationResult = await edi.validateX12({
122
127
 
123
128
  **Returns:** Validation result with operation details
124
129
 
125
- #### `acknowledgeX12(options: AcknolwedgeX12Options): Promise<OperationResult>`
130
+ #### `acknowledgeX12(options: AcknolwedgeX12Options): Promise<X12Interchange[]>`
126
131
 
127
- Generates acknowledgment for X12 interchange objects.
132
+ Generates acknowledgment interchange objects. Apply any partner-specific outbound envelope IDs, then serialize
133
+ each acknowledgment with `toX12()`. During serialization, `toX12()` assigns control numbers using the final
134
+ sender and receiver. The method always returns an array because one input can produce more than one
135
+ acknowledgment.
128
136
 
129
137
  ```typescript
130
- const acknowledgment = await edi.acknowledgeX12({
138
+ const acknowledgments = await edi.acknowledgeX12({
131
139
  input: x12InterchangeObject,
132
140
  });
141
+
142
+ for (const acknowledgment of acknowledgments) {
143
+ // Apply partner-specific outbound envelope IDs here when needed.
144
+ const serialized = await edi.toX12({ input: acknowledgment });
145
+ }
133
146
  ```
134
147
 
135
148
  **Parameters:**
136
149
 
137
150
  - `options.input` - The X12 interchange object to acknowledge
138
151
 
139
- **Returns:** Acknowledgment result with operation details
152
+ **Returns:** The generated acknowledgment interchanges
140
153
 
141
154
  ## Error Handling
142
155
 
143
156
  The EDI client throws two distinct error classes. The distinction matters for file routing:
144
157
  a `TemperEdiClientError` is a verdict on the input document (bad file — don't retry) or a
145
- persistent auth failure, while an `EdiInfrastructureError` is a transient platform fault —
146
- the SDK's own plumbing failed (tracing/logging/serialization) or the EDI service was
147
- transiently unavailable (network failure, timeout, 5xx, rate limit). It says nothing about
148
- the document, so keep the file and retry (`error.retryable === true`).
158
+ persistent authentication failure, while an `EdiInfrastructureError` is retryable and does
159
+ not indicate that the document is invalid (`error.retryable === true`).
149
160
 
150
161
  ```typescript
151
162
  import { isEdiInfrastructureError, isTemperEdiClientError } from '@ontemper/edi';
@@ -155,11 +166,14 @@ async function processFile(rawX12: string) {
155
166
  return await edi.fromX12({ input: rawX12 });
156
167
  } catch (error) {
157
168
  if (isEdiInfrastructureError(error)) {
158
- // Transient platform fault — the document is fine. Keep the file and rethrow
159
- // so your poll loop / caller retries.
169
+ // Keep the file and rethrow so the caller can retry.
160
170
  throw error;
161
171
  }
162
172
  if (isTemperEdiClientError(error)) {
173
+ if (error.code === 'edi_unauthorized_error') {
174
+ // Surface authentication failures instead of quarantining the file.
175
+ throw error;
176
+ }
163
177
  // A verdict on the document (see codes below) — handle it terminally (e.g.
164
178
  // quarantine the file) and STOP. Do not rethrow into retry paths.
165
179
  console.error('EDI Error:', error.code, error.message);
@@ -183,11 +197,11 @@ async function processFile(rawX12: string) {
183
197
  | `edi_unauthorized_error` | Unauthorized access to EDI service |
184
198
  | `edi_unknown_error` | Legacy — no longer produced |
185
199
 
186
- `EdiInfrastructureError` (transient platform fault, `retryable: true`):
200
+ `EdiInfrastructureError` (`retryable: true`):
187
201
 
188
202
  | Code | Description |
189
203
  | ---------------------- | ------------------------------------------------------------------------------------ |
190
- | `infrastructure_error` | SDK plumbing failure or transient EDI-service fault — not caused by the input document |
204
+ | `infrastructure_error` | Retryable failure unrelated to the input document |
191
205
 
192
206
  ## Type Definitions
193
207
 
@@ -319,9 +333,13 @@ IEA*1*000001000`;
319
333
  const x12Output = await edi.toX12({ input: interchange });
320
334
  logger.info({ x12Output }, 'Converted back to X12');
321
335
 
322
- // Generate acknowledgment
323
- const acknowledgment = await edi.acknowledgeX12({ input: interchange });
324
- logger.info({ acknowledgment }, 'Generated acknowledgment');
336
+ // Generate and serialize acknowledgments. toX12 assigns control numbers
337
+ // after any partner-specific outbound envelope changes.
338
+ const acknowledgments = await edi.acknowledgeX12({ input: interchange });
339
+ for (const acknowledgment of acknowledgments) {
340
+ const acknowledgmentX12 = await edi.toX12({ input: acknowledgment });
341
+ logger.info({ acknowledgmentX12 }, 'Generated acknowledgment');
342
+ }
325
343
  }
326
344
  } catch (error) {
327
345
  logger.error({ err: error }, 'EDI processing failed');
@@ -349,7 +367,9 @@ const current = await getCurrentControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER
349
367
  await setControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER', 1000);
350
368
  ```
351
369
 
352
- In most cases you don't need these directly — `toX12()` calls `getNextControlNumber` automatically for any empty ISA13/GS06 fields.
370
+ In most cases you don't need these directly — `toX12()` automatically stamps any empty or all-zero
371
+ ISA13/GS06/ST02 fields. ISA13 and GS06 use `getNextControlNumber`; ST02 is assigned `"0001"`. This includes the
372
+ zero placeholders returned by acknowledgment generation.
353
373
  Once a counter has incremented, the API rejects later `setControlNumber` calls to avoid resetting active sequences.
354
374
  Retried `setControlNumber` calls with the same already-stored seed are treated as idempotent no-ops.
355
375
  The SDK surfaces that as a `ControlNumberError` with `code: "counter_already_started"`:
package/dist/index.d.ts CHANGED
@@ -16,10 +16,7 @@ export declare class TemperEdiClientError extends UnnboundError<TemperEdiClientE
16
16
  }
17
17
  export declare const isTemperEdiClientError: (error: unknown) => error is TemperEdiClientError;
18
18
  /**
19
- * A failure in the SDK's own plumbing (logging/instrumentation/tracing), not a verdict
20
- * on the EDI payload. Deliberately NOT a TemperEdiClientError and NOT an `edi_*` code:
21
- * workflows route `edi_*` errors as bad files, while these are transient platform
22
- * faults the workflow should retry (T-3235).
19
+ * A retryable failure that does not indicate an invalid EDI document.
23
20
  */
24
21
  export declare class EdiInfrastructureError extends UnnboundError<'infrastructure_error'> {
25
22
  readonly retryable = true;
@@ -45,10 +42,10 @@ export declare class TemperEdiClient {
45
42
  private unwrapError;
46
43
  fromX12({ input }: FromX12Options): Promise<X12Interchange[]>;
47
44
  /**
48
- * Stamp ISA13/GS06 control numbers on the interchange if not already set.
45
+ * Stamp ISA13/GS06/ST02 control numbers on the interchange if not already set.
49
46
  * Uses the Restate-backed counter service for atomic, per-trading-partner sequencing.
50
47
  * ST02 is always "0001" (one transaction per file per ticket).
51
- * If a control number is already set, it is left unchanged (opt-out for manual overrides).
48
+ * Zero-filled values from acknowledgment generation are placeholders, not manual overrides.
52
49
  */
53
50
  private stampControlNumbers;
54
51
  toX12({ input }: ToX12Options): Promise<unknown>;
package/dist/index.js CHANGED
@@ -66,10 +66,7 @@ exports.TemperEdiClientError = TemperEdiClientError;
66
66
  const isTemperEdiClientError = (error) => error instanceof TemperEdiClientError;
67
67
  exports.isTemperEdiClientError = isTemperEdiClientError;
68
68
  /**
69
- * A failure in the SDK's own plumbing (logging/instrumentation/tracing), not a verdict
70
- * on the EDI payload. Deliberately NOT a TemperEdiClientError and NOT an `edi_*` code:
71
- * workflows route `edi_*` errors as bad files, while these are transient platform
72
- * faults the workflow should retry (T-3235).
69
+ * A retryable failure that does not indicate an invalid EDI document.
73
70
  */
74
71
  class EdiInfrastructureError extends UnnboundError {
75
72
  retryable = true;
@@ -83,10 +80,10 @@ const isEdiInfrastructureError = (error) => error instanceof EdiInfrastructureEr
83
80
  exports.isEdiInfrastructureError = isEdiInfrastructureError;
84
81
  const buildEdiPayload = (edi) => ({ type: 'edi', edi });
85
82
  const buildEdiX12Payload = (operation, x12) => buildEdiPayload({ operation, type: 'x12', x12 });
86
- // One traced instance for the whole process, created once at module load. Never trace
87
- // the global axios export: in-place wrapping there accumulated a span layer per
88
- // TemperEdiClient construction (stack overflow after enough polls) and leaked EDI
89
- // payload capture into unrelated code sharing global axios (T-3235).
83
+ const needsControlNumber = (value) => {
84
+ const normalized = value?.trim();
85
+ return !normalized || /^0+$/.test(normalized);
86
+ };
90
87
  const ediAxios = (0, unnbound_logger_sdk_1.traceAxios)(axios_1.default.create(), { getPayload: internal_1.internal });
91
88
  class TemperEdiClient {
92
89
  X12;
@@ -112,24 +109,12 @@ class TemperEdiClient {
112
109
  if (error instanceof EdiInfrastructureError)
113
110
  throw error;
114
111
  if ((0, axios_1.isAxiosError)(error)) {
115
- // 401 = missing/expired/invalid credential (e.g. the sandbox EDI-gateway JWT),
116
- // 403 = access denied. Neither is a verdict on the document.
117
- if (error.status === 401 || error.status === 403)
112
+ const status = error.response?.status ?? error.status;
113
+ if (status === 401 || status === 403)
118
114
  throw new TemperEdiClientError({
119
115
  message: 'Unauthorized access to EDI service (credentials missing, expired, or invalid). Reach out to support.',
120
116
  code: 'edi_unauthorized_error',
121
117
  });
122
- // Transport failures (no response: ECONNREFUSED/timeout/DNS), 5xx, and
123
- // rate-limit/timeout statuses are transient service faults, not verdicts on
124
- // the document — a brief EDI-service outage must never quarantine valid
125
- // files in /error (T-3235).
126
- const status = error.response?.status;
127
- if (status === undefined || status >= 500 || status === 408 || status === 429) {
128
- throw new EdiInfrastructureError({
129
- message: `EDI service unreachable or transiently failing (${status ?? error.code ?? 'no response'}): ${error.message}`,
130
- cause: error,
131
- });
132
- }
133
118
  // Gateway normalises EdiFabric's camelCase to PascalCase before forwarding
134
119
  const responseData = error.response?.data;
135
120
  const details = typeof responseData === 'object' &&
@@ -138,18 +123,18 @@ class TemperEdiClient {
138
123
  Array.isArray(responseData.Details)
139
124
  ? responseData.Details
140
125
  : undefined;
141
- const message = error.response?.status === 400 && details?.length
142
- ? `EDI validation failed: ${details.join('; ')}`
143
- : responseData && typeof responseData === 'object'
144
- ? `EDI request failed (${error.response?.status}): ${JSON.stringify(responseData)}`
145
- : error.message;
146
- throw new TemperEdiClientError({ message, code, cause: error });
126
+ if (status === 400 && details?.length) {
127
+ throw new TemperEdiClientError({
128
+ message: `EDI validation failed: ${details.join('; ')}`,
129
+ code,
130
+ cause: error,
131
+ });
132
+ }
133
+ throw new EdiInfrastructureError({
134
+ message: `EDI service request failed (${status ?? error.code ?? 'no response'}): ${error.message}`,
135
+ cause: error,
136
+ });
147
137
  }
148
- // Not an axios error → the failure never reached the EDI HTTP exchange. It was
149
- // raised by the SDK's own plumbing (span/logging instrumentation, request
150
- // serialization), so it must not be reported as a verdict on the file: workflows
151
- // route `edi_*` errors to /error, and that misrouted valid partner files when the
152
- // instrumentation layer crashed (T-3235).
153
138
  throw new EdiInfrastructureError({
154
139
  message: `EDI client infrastructure failure (not caused by the input document): ${error instanceof Error ? error.message : String(error)}`,
155
140
  cause: error,
@@ -163,10 +148,10 @@ class TemperEdiClient {
163
148
  }, (o) => buildEdiX12Payload('fromX12', { input, output: o?.result }));
164
149
  }
165
150
  /**
166
- * Stamp ISA13/GS06 control numbers on the interchange if not already set.
151
+ * Stamp ISA13/GS06/ST02 control numbers on the interchange if not already set.
167
152
  * Uses the Restate-backed counter service for atomic, per-trading-partner sequencing.
168
153
  * ST02 is always "0001" (one transaction per file per ticket).
169
- * If a control number is already set, it is left unchanged (opt-out for manual overrides).
154
+ * Zero-filled values from acknowledgment generation are placeholders, not manual overrides.
170
155
  */
171
156
  async stampControlNumbers(input) {
172
157
  const { getNextControlNumber } = await import('./control-numbers.js');
@@ -179,12 +164,13 @@ class TemperEdiClient {
179
164
  const receiverKey = `${receiverQual}:${receiverId}`;
180
165
  // Phase 1: Fetch all needed control numbers (no mutations yet).
181
166
  // If any fetch fails, no fields are mutated — avoids partial stamping.
182
- const isa13 = !isa.InterchangeControlNumber_13
167
+ const isa13 = needsControlNumber(isa.InterchangeControlNumber_13)
183
168
  ? await getNextControlNumber('ISA13', senderKey, receiverKey, 9)
184
169
  : null;
185
170
  const gs06Values = [];
186
- for (let i = 0; i < input.Groups.length; i++) {
187
- if (!input.Groups[i].GS.GroupControlNumber_6) {
171
+ const groups = input.Groups ?? [];
172
+ for (let i = 0; i < groups.length; i++) {
173
+ if (needsControlNumber(groups[i].GS.GroupControlNumber_6)) {
188
174
  const value = await getNextControlNumber('GS06', senderKey, receiverKey, 9);
189
175
  gs06Values.push({ index: i, value });
190
176
  }
@@ -197,16 +183,16 @@ class TemperEdiClient {
197
183
  }
198
184
  }
199
185
  for (const { index, value } of gs06Values) {
200
- const group = input.Groups[index];
186
+ const group = groups[index];
201
187
  group.GS.GroupControlNumber_6 = value;
202
188
  if (group.GETrailers?.length) {
203
189
  group.GETrailers[0].GroupControlNumber_2 = value;
204
190
  }
205
191
  }
206
192
  // ST02 — always "0001" (pure assignment, no external calls)
207
- for (const group of input.Groups) {
193
+ for (const group of groups) {
208
194
  for (const tx of group.Transactions) {
209
- if (!tx?.ST?.TransactionSetControlNumber_02) {
195
+ if (needsControlNumber(tx?.ST?.TransactionSetControlNumber_02)) {
210
196
  if (tx?.ST)
211
197
  tx.ST.TransactionSetControlNumber_02 = '0001';
212
198
  if (tx?.SE)
@@ -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.5",
4
+ "version": "1.1.7",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "author": "Unnbound Team",