@ontemper/edi 1.2.0-beta.1 → 1.2.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.
package/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # Temper EDI Client
2
2
 
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.
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.
4
4
 
5
5
  ## Features
6
6
 
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)
7
+ - **X12 EDI Processing**: Complete X12 document parsing, validation, and generation
9
8
  - **Structured Logging**: Built-in integration with Temper Logger for comprehensive operation tracking
10
9
  - **Distributed Tracing**: Automatic span creation for all EDI operations with detailed payload information
11
10
  - **Type Safety**: Full TypeScript support with comprehensive type definitions
@@ -351,43 +350,83 @@ IEA*1*000001000`;
351
350
  run().catch(console.error);
352
351
  ```
353
352
 
354
- ## Control Numbers
353
+ ## Control numbers
355
354
 
356
- The SDK provides functions for managing EDI control numbers (ISA13, GS06) via the centralized counter service:
355
+ The SDK manages ISA13 and GS06 control numbers for each sender and receiver pair. `toX12()` automatically stamps
356
+ empty or all-zero ISA13, GS06, and ST02 fields. It preserves valid non-zero values. ISA13 and GS06 use the
357
+ centralized counters. ST02 is assigned `"0001"`.
358
+
359
+ **By default, every environment draws from one shared sequence per trading partner. No setup is required.**
360
+ Separating test traffic onto its own sequence is an optional, per-partner opt-in for agreements that demand it.
361
+
362
+ ### Separate test counters (optional)
363
+
364
+ Skip this section unless a trading-partner agreement requires test traffic on an independent sequence. To opt in,
365
+ configure the trading partner once, then seed and verify the lanes before sending test traffic. A seeded value is the
366
+ last-used number, so the next allocation returns the seed plus one. To make the next ISA13 value 4601, seed 4600:
357
367
 
358
368
  ```typescript
359
- import { getNextControlNumber, getCurrentControlNumber, setControlNumber } from '@ontemper/edi';
369
+ import {
370
+ getControlNumberPolicy,
371
+ getCurrentControlNumber,
372
+ setControlNumber,
373
+ setControlNumberPolicy,
374
+ } from '@ontemper/edi';
360
375
 
361
- // Get next control number (atomic increment)
362
- const next = await getNextControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER');
376
+ const senderId = 'ZZ:SENDER';
377
+ const receiverId = 'ZZ:RECEIVER';
363
378
 
364
- // Peek at current value without incrementing
365
- const current = await getCurrentControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER');
379
+ await setControlNumberPolicy(senderId, receiverId, { separateCounters: true });
366
380
 
367
- // Set a starting value before the counter has incremented (for migrations)
368
- await setControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER', 1000);
381
+ // Seed the production lane explicitly before it starts.
382
+ await setControlNumber('ISA13', senderId, receiverId, 4600, 9, { lane: 'production' });
383
+
384
+ const policy = await getControlNumberPolicy(senderId, receiverId);
385
+ const production = await getCurrentControlNumber('ISA13', senderId, receiverId, 9, { lane: 'production' });
386
+ const test = await getCurrentControlNumber('ISA13', senderId, receiverId, 9, { lane: 'test' });
369
387
  ```
370
388
 
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.
374
- Once a counter has incremented, the API rejects later `setControlNumber` calls to avoid resetting active sequences.
375
- Retried `setControlNumber` calls with the same already-stored seed are treated as idempotent no-ops.
376
- The SDK surfaces that as a `ControlNumberError` with `code: "counter_already_started"`:
389
+ `getControlNumberPolicy()` returns `configured` and `separateCounters`. After test traffic starts, you cannot turn
390
+ separate counters off. `setControlNumberPolicy()` reports this as a `ControlNumberError` with the
391
+ `counter_policy_locked` code.
392
+
393
+ ### Automatic environment routing
394
+
395
+ The SDK reads `UNNBOUND_ENVIRONMENT` and sends it with counter operations only when its value is `sandbox`,
396
+ `staging`, or `production`. The service selects the lane:
397
+
398
+ | Policy | Production environment | Sandbox or staging |
399
+ | --- | --- | --- |
400
+ | No policy row | Production lane | Production lane |
401
+ | `separateCounters: false` | Production lane | Production lane |
402
+ | `separateCounters: true` | Production lane | Test lane |
403
+
404
+ Older SDK versions do not send the environment, so their operations use the production lane. This is the historical
405
+ sequence that all environments use when separation is off. ISA15
406
+ (`UsageIndicator_15`) never selects a counter. Set ISA15 independently to the value required by the interchange.
407
+ `toX12()` does not change it.
408
+
409
+ ### Manage counters directly
377
410
 
378
411
  ```typescript
379
- import { isControlNumberError, setControlNumber } from '@ontemper/edi';
412
+ import { getCurrentControlNumber, getNextControlNumber, setControlNumber } from '@ontemper/edi';
380
413
 
381
- try {
382
- await setControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER', 1000);
383
- } catch (error) {
384
- if (isControlNumberError(error) && error.code === 'counter_already_started') {
385
- // Counter is already active; do not reset it.
386
- }
387
- }
414
+ const next = await getNextControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER');
415
+ const current = await getCurrentControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER');
416
+ // The seed is last-used. To allocate 4601 next, seed 4600.
417
+ await setControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER', 4600, 9, { lane: 'production' });
388
418
  ```
389
419
 
390
- Requires `UNNBOUND_API_URL` environment variable (pre-configured in all workflow environments).
420
+ Use the optional `{ lane: 'production' | 'test' }` argument on `getCurrentControlNumber()` and `setControlNumber()`
421
+ to inspect or seed a specific lane. `getNextControlNumber()` always uses automatic environment routing and does not
422
+ accept a lane.
423
+
424
+ The seeded value is the last-used number, and the next allocation returns the seed plus one. For example, if you
425
+ expect the next ISA13 value to be 4601, seed 4600. Set this value before the counter increments. The service treats a
426
+ repeated seed with the same stored value as an idempotent operation. It rejects other updates after the counter
427
+ starts with a `ControlNumberError` whose code is `counter_already_started`.
428
+
429
+ These helpers require `UNNBOUND_API_URL`, which is preconfigured in workflow environments.
391
430
 
392
431
  ## Requirements
393
432
 
@@ -1,5 +1,27 @@
1
1
  /** @public */
2
- export type ControlNumberErrorCode = 'counter_already_started' | 'control_number_bad_request' | 'control_number_unauthorized' | 'control_number_unknown_error';
2
+ export type ControlNumberCounterName = 'ISA13' | 'GS06';
3
+ /** @public */
4
+ export type ControlNumberLane = 'production' | 'test';
5
+ /** @public */
6
+ export interface ControlNumberLaneOptions {
7
+ readonly lane?: ControlNumberLane;
8
+ }
9
+ /** @public */
10
+ export interface SetControlNumberPolicyOptions {
11
+ readonly separateCounters: boolean;
12
+ }
13
+ /** @public */
14
+ export interface SetControlNumberPolicyResponse {
15
+ readonly tradingPartnerKey: string;
16
+ readonly separateCounters: boolean;
17
+ }
18
+ /** @public */
19
+ export interface GetControlNumberPolicyResponse {
20
+ readonly configured: boolean;
21
+ readonly separateCounters: boolean;
22
+ }
23
+ /** @public */
24
+ export type ControlNumberErrorCode = 'counter_already_started' | 'counter_policy_locked' | 'control_number_bad_request' | 'control_number_unauthorized' | 'control_number_unknown_error';
3
25
  interface ControlNumberErrorOptions extends ErrorOptions {
4
26
  code: ControlNumberErrorCode;
5
27
  message: string;
@@ -23,15 +45,19 @@ export declare const isControlNumberError: (error: unknown) => error is ControlN
23
45
  * @param maxDigits - Zero-pad to this width (default: 9 for ISA13)
24
46
  */
25
47
  /** @public */
26
- export declare function getNextControlNumber(counterName: string, senderId: string, receiverId: string, maxDigits?: number): Promise<string>;
48
+ export declare function getNextControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, maxDigits?: number): Promise<string>;
27
49
  /**
28
50
  * Get the current control number value without incrementing.
29
51
  */
30
52
  /** @public */
31
- export declare function getCurrentControlNumber(counterName: string, senderId: string, receiverId: string, maxDigits?: number): Promise<string>;
53
+ export declare function getCurrentControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, maxDigits?: number, options?: ControlNumberLaneOptions): Promise<string>;
32
54
  /**
33
55
  * Set a control number to a specific value before the counter starts (for migration/seeding).
34
56
  */
35
57
  /** @public */
36
- export declare function setControlNumber(counterName: string, senderId: string, receiverId: string, value: number, maxDigits?: number): Promise<string>;
58
+ export declare function setControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, value: number, maxDigits?: number, options?: ControlNumberLaneOptions): Promise<string>;
59
+ /** @public */
60
+ export declare function setControlNumberPolicy(senderId: string, receiverId: string, { separateCounters }: SetControlNumberPolicyOptions): Promise<SetControlNumberPolicyResponse>;
61
+ /** @public */
62
+ export declare function getControlNumberPolicy(senderId: string, receiverId: string): Promise<GetControlNumberPolicyResponse>;
37
63
  export {};
@@ -37,6 +37,8 @@ exports.isControlNumberError = exports.ControlNumberError = void 0;
37
37
  exports.getNextControlNumber = getNextControlNumber;
38
38
  exports.getCurrentControlNumber = getCurrentControlNumber;
39
39
  exports.setControlNumber = setControlNumber;
40
+ exports.setControlNumberPolicy = setControlNumberPolicy;
41
+ exports.getControlNumberPolicy = getControlNumberPolicy;
40
42
  const axios_1 = __importStar(require("axios"));
41
43
  let _client = null;
42
44
  function getApiClient() {
@@ -78,10 +80,23 @@ exports.ControlNumberError = ControlNumberError;
78
80
  /** @public */
79
81
  const isControlNumberError = (error) => error instanceof ControlNumberError;
80
82
  exports.isControlNumberError = isControlNumberError;
81
- function unwrapControlNumberError(error) {
83
+ const getEnvironment = () => {
84
+ const environment = process.env.UNNBOUND_ENVIRONMENT;
85
+ return environment === 'sandbox' || environment === 'staging' || environment === 'production'
86
+ ? environment
87
+ : undefined;
88
+ };
89
+ function unwrapControlNumberError(error, operation) {
82
90
  if (error instanceof ControlNumberError)
83
91
  throw error;
84
92
  if ((0, axios_1.isAxiosError)(error)) {
93
+ if (error.response?.status === 409 && operation === 'policy') {
94
+ throw new ControlNumberError({
95
+ code: 'counter_policy_locked',
96
+ message: 'Separate counters cannot be disabled after test traffic has started. Keep separate counters enabled or contact Temper support.',
97
+ cause: error,
98
+ });
99
+ }
85
100
  if (error.response?.status === 409) {
86
101
  throw new ControlNumberError({
87
102
  code: 'counter_already_started',
@@ -92,7 +107,7 @@ function unwrapControlNumberError(error) {
92
107
  if (error.response?.status === 400) {
93
108
  throw new ControlNumberError({
94
109
  code: 'control_number_bad_request',
95
- message: 'Invalid EDI control number request. Counter name, trading partner IDs, and value are required.',
110
+ message: "Invalid EDI control number request. Use counter name 'ISA13' or 'GS06' with trading partner IDs and, for set, a value.",
96
111
  cause: error,
97
112
  });
98
113
  }
@@ -110,6 +125,21 @@ function unwrapControlNumberError(error) {
110
125
  cause: error,
111
126
  });
112
127
  }
128
+ async function requestControlNumber(operation, input) {
129
+ const environment = getEnvironment();
130
+ const request = { ...input, ...(environment ? { environment } : {}) };
131
+ try {
132
+ const response = operation === 'count'
133
+ ? await getApiClient().get(`/api/v2/internal/edi/control-numbers/${operation}`, {
134
+ params: request,
135
+ })
136
+ : await getApiClient().post(`/api/v2/internal/edi/control-numbers/${operation}`, request);
137
+ return response.data;
138
+ }
139
+ catch (error) {
140
+ return unwrapControlNumberError(error, operation);
141
+ }
142
+ }
113
143
  /**
114
144
  * Get the next control number for an EDI counter.
115
145
  *
@@ -123,7 +153,7 @@ function unwrapControlNumberError(error) {
123
153
  */
124
154
  /** @public */
125
155
  async function getNextControlNumber(counterName, senderId, receiverId, maxDigits = 9) {
126
- const { data } = await getApiClient().post('/api/v2/internal/edi/control-numbers/increment', {
156
+ const data = await requestControlNumber('increment', {
127
157
  tradingPartnerKey: `${senderId}:${receiverId}`,
128
158
  counterName,
129
159
  });
@@ -133,12 +163,11 @@ async function getNextControlNumber(counterName, senderId, receiverId, maxDigits
133
163
  * Get the current control number value without incrementing.
134
164
  */
135
165
  /** @public */
136
- async function getCurrentControlNumber(counterName, senderId, receiverId, maxDigits = 9) {
137
- const { data } = await getApiClient().get('/api/v2/internal/edi/control-numbers/count', {
138
- params: {
139
- tradingPartnerKey: `${senderId}:${receiverId}`,
140
- counterName,
141
- },
166
+ async function getCurrentControlNumber(counterName, senderId, receiverId, maxDigits = 9, options) {
167
+ const data = await requestControlNumber('count', {
168
+ tradingPartnerKey: `${senderId}:${receiverId}`,
169
+ counterName,
170
+ ...(options?.lane ? { lane: options.lane } : {}),
142
171
  });
143
172
  return String(data.value).padStart(maxDigits, '0');
144
173
  }
@@ -146,13 +175,40 @@ async function getCurrentControlNumber(counterName, senderId, receiverId, maxDig
146
175
  * Set a control number to a specific value before the counter starts (for migration/seeding).
147
176
  */
148
177
  /** @public */
149
- async function setControlNumber(counterName, senderId, receiverId, value, maxDigits = 9) {
150
- const { data } = await getApiClient()
151
- .post('/api/v2/internal/edi/control-numbers/set', {
178
+ async function setControlNumber(counterName, senderId, receiverId, value, maxDigits = 9, options) {
179
+ const data = await requestControlNumber('set', {
152
180
  tradingPartnerKey: `${senderId}:${receiverId}`,
153
181
  counterName,
154
182
  value,
155
- })
156
- .catch(unwrapControlNumberError);
183
+ ...(options?.lane ? { lane: options.lane } : {}),
184
+ });
157
185
  return String(data.value).padStart(maxDigits, '0');
158
186
  }
187
+ /** @public */
188
+ async function setControlNumberPolicy(senderId, receiverId, { separateCounters }) {
189
+ try {
190
+ const response = await getApiClient().post('/api/v2/internal/edi/control-numbers/policy', {
191
+ tradingPartnerKey: `${senderId}:${receiverId}`,
192
+ separateCounters,
193
+ });
194
+ return response.data;
195
+ }
196
+ catch (error) {
197
+ return unwrapControlNumberError(error, 'policy');
198
+ }
199
+ }
200
+ /** @public */
201
+ async function getControlNumberPolicy(senderId, receiverId) {
202
+ try {
203
+ const response = await getApiClient().get('/api/v2/internal/edi/control-numbers/policy', {
204
+ params: { tradingPartnerKey: `${senderId}:${receiverId}` },
205
+ });
206
+ return {
207
+ configured: response.data.configured,
208
+ separateCounters: response.data.separateCounters,
209
+ };
210
+ }
211
+ catch (error) {
212
+ return unwrapControlNumberError(error, 'policy');
213
+ }
214
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { type EdifactInterchange, type X12Interchange } from './edination-client';
2
- export type { ControlNumberErrorCode } from './control-numbers';
3
- export { ControlNumberError, getCurrentControlNumber, getNextControlNumber, isControlNumberError, setControlNumber, } from './control-numbers';
1
+ import type { X12Interchange } from './edination-client';
2
+ export type { ControlNumberCounterName, ControlNumberErrorCode, ControlNumberLane, ControlNumberLaneOptions, GetControlNumberPolicyResponse, SetControlNumberPolicyOptions, SetControlNumberPolicyResponse, } from './control-numbers';
3
+ export { ControlNumberError, getControlNumberPolicy, getCurrentControlNumber, getNextControlNumber, isControlNumberError, setControlNumber, setControlNumberPolicy, } from './control-numbers';
4
4
  export * from './edination-client/model';
5
5
  interface UnnboundErrorOptions<C extends string = string> extends ErrorOptions {
6
6
  message: string;
@@ -35,21 +35,8 @@ 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
- }
50
38
  export declare class TemperEdiClient {
51
39
  private X12;
52
- private Edifact;
53
40
  constructor();
54
41
  private unwrap;
55
42
  private unwrapError;
@@ -64,19 +51,4 @@ export declare class TemperEdiClient {
64
51
  toX12({ input }: ToX12Options): Promise<unknown>;
65
52
  validateX12({ input }: ValidateX12Options): Promise<import("./edination-client").OperationResult>;
66
53
  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[]>;
82
54
  }
package/dist/index.js CHANGED
@@ -36,17 +36,20 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
36
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.TemperEdiClient = exports.isEdiInfrastructureError = exports.EdiInfrastructureError = exports.isTemperEdiClientError = exports.TemperEdiClientError = exports.setControlNumber = exports.isControlNumberError = exports.getNextControlNumber = exports.getCurrentControlNumber = exports.ControlNumberError = void 0;
39
+ exports.TemperEdiClient = exports.isEdiInfrastructureError = exports.EdiInfrastructureError = exports.isTemperEdiClientError = exports.TemperEdiClientError = exports.setControlNumberPolicy = exports.setControlNumber = exports.isControlNumberError = exports.getNextControlNumber = exports.getCurrentControlNumber = exports.getControlNumberPolicy = exports.ControlNumberError = void 0;
40
40
  const axios_1 = __importStar(require("axios"));
41
41
  const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
42
42
  const internal_1 = require("unnbound-logger-sdk/dist/internal");
43
+ const control_numbers_1 = require("./control-numbers");
43
44
  const edination_client_1 = require("./edination-client");
44
- var control_numbers_1 = require("./control-numbers");
45
- Object.defineProperty(exports, "ControlNumberError", { enumerable: true, get: function () { return control_numbers_1.ControlNumberError; } });
46
- Object.defineProperty(exports, "getCurrentControlNumber", { enumerable: true, get: function () { return control_numbers_1.getCurrentControlNumber; } });
47
- Object.defineProperty(exports, "getNextControlNumber", { enumerable: true, get: function () { return control_numbers_1.getNextControlNumber; } });
48
- Object.defineProperty(exports, "isControlNumberError", { enumerable: true, get: function () { return control_numbers_1.isControlNumberError; } });
49
- Object.defineProperty(exports, "setControlNumber", { enumerable: true, get: function () { return control_numbers_1.setControlNumber; } });
45
+ var control_numbers_2 = require("./control-numbers");
46
+ Object.defineProperty(exports, "ControlNumberError", { enumerable: true, get: function () { return control_numbers_2.ControlNumberError; } });
47
+ Object.defineProperty(exports, "getControlNumberPolicy", { enumerable: true, get: function () { return control_numbers_2.getControlNumberPolicy; } });
48
+ Object.defineProperty(exports, "getCurrentControlNumber", { enumerable: true, get: function () { return control_numbers_2.getCurrentControlNumber; } });
49
+ Object.defineProperty(exports, "getNextControlNumber", { enumerable: true, get: function () { return control_numbers_2.getNextControlNumber; } });
50
+ Object.defineProperty(exports, "isControlNumberError", { enumerable: true, get: function () { return control_numbers_2.isControlNumberError; } });
51
+ Object.defineProperty(exports, "setControlNumber", { enumerable: true, get: function () { return control_numbers_2.setControlNumber; } });
52
+ Object.defineProperty(exports, "setControlNumberPolicy", { enumerable: true, get: function () { return control_numbers_2.setControlNumberPolicy; } });
50
53
  __exportStar(require("./edination-client/model"), exports);
51
54
  class UnnboundError extends Error {
52
55
  code;
@@ -79,7 +82,7 @@ exports.EdiInfrastructureError = EdiInfrastructureError;
79
82
  const isEdiInfrastructureError = (error) => error instanceof EdiInfrastructureError;
80
83
  exports.isEdiInfrastructureError = isEdiInfrastructureError;
81
84
  const buildEdiPayload = (edi) => ({ type: 'edi', edi });
82
- const buildEdiTransactionPayload = (operation, format, transaction) => buildEdiPayload({ operation, type: format, transaction });
85
+ const buildEdiX12Payload = (operation, x12) => buildEdiPayload({ operation, type: 'x12', x12 });
83
86
  const needsControlNumber = (value) => {
84
87
  const normalized = value?.trim();
85
88
  return !normalized || /^0+$/.test(normalized);
@@ -87,7 +90,6 @@ const needsControlNumber = (value) => {
87
90
  const ediAxios = (0, unnbound_logger_sdk_1.traceAxios)(axios_1.default.create(), { getPayload: internal_1.internal });
88
91
  class TemperEdiClient {
89
92
  X12;
90
- Edifact;
91
93
  constructor() {
92
94
  const apiKey = process.env.UNNBOUND_EDI_API_KEY;
93
95
  // When UNNBOUND_EDI_BASE_URL is set, use self-hosted EdiFabric InHouse API
@@ -100,7 +102,6 @@ class TemperEdiClient {
100
102
  // but the OpenAPI client requires a non-empty value — use a placeholder.
101
103
  const config = new edination_client_1.Configuration({ apiKey: apiKey ?? 'self-hosted', basePath });
102
104
  this.X12 = new edination_client_1.X12Api(config, undefined, ediAxios);
103
- this.Edifact = new edination_client_1.EdifactApi(config, undefined, ediAxios);
104
105
  }
105
106
  unwrap(response) {
106
107
  return response.data;
@@ -147,7 +148,7 @@ class TemperEdiClient {
147
148
  return this.X12.x12ReadPost({ body: input })
148
149
  .then(this.unwrap.bind(this))
149
150
  .catch((error) => this.unwrapError(error, 'edi_read_error'));
150
- }, (o) => buildEdiTransactionPayload('fromX12', 'x12', { input, output: o?.result }));
151
+ }, (o) => buildEdiX12Payload('fromX12', { input, output: o?.result }));
151
152
  }
152
153
  /**
153
154
  * Stamp ISA13/GS06/ST02 control numbers on the interchange if not already set.
@@ -156,7 +157,6 @@ class TemperEdiClient {
156
157
  * Zero-filled values from acknowledgment generation are placeholders, not manual overrides.
157
158
  */
158
159
  async stampControlNumbers(input) {
159
- const { getNextControlNumber } = await import('./control-numbers.js');
160
160
  const isa = input.ISA;
161
161
  const senderQual = (isa.SenderIDQualifier_5 || '').trim();
162
162
  const senderId = (isa.InterchangeSenderID_6 || '').trim();
@@ -164,20 +164,18 @@ class TemperEdiClient {
164
164
  const receiverId = (isa.InterchangeReceiverID_8 || '').trim();
165
165
  const senderKey = `${senderQual}:${senderId}`;
166
166
  const receiverKey = `${receiverQual}:${receiverId}`;
167
- // Phase 1: Fetch all needed control numbers (no mutations yet).
168
- // If any fetch fails, no fields are mutated — avoids partial stamping.
167
+ // Allocate before mutating so a failed request cannot leave partial stamps.
169
168
  const isa13 = needsControlNumber(isa.InterchangeControlNumber_13)
170
- ? await getNextControlNumber('ISA13', senderKey, receiverKey, 9)
169
+ ? await (0, control_numbers_1.getNextControlNumber)('ISA13', senderKey, receiverKey, 9)
171
170
  : null;
172
171
  const gs06Values = [];
173
172
  const groups = input.Groups ?? [];
174
173
  for (let i = 0; i < groups.length; i++) {
175
174
  if (needsControlNumber(groups[i].GS.GroupControlNumber_6)) {
176
- const value = await getNextControlNumber('GS06', senderKey, receiverKey, 9);
175
+ const value = await (0, control_numbers_1.getNextControlNumber)('GS06', senderKey, receiverKey, 9);
177
176
  gs06Values.push({ index: i, value });
178
177
  }
179
178
  }
180
- // Phase 2: Apply all mutations (only reached if all fetches succeeded).
181
179
  if (isa13) {
182
180
  isa.InterchangeControlNumber_13 = isa13;
183
181
  if (input.IEATrailers?.length) {
@@ -191,7 +189,6 @@ class TemperEdiClient {
191
189
  group.GETrailers[0].GroupControlNumber_2 = value;
192
190
  }
193
191
  }
194
- // ST02 — always "0001" (pure assignment, no external calls)
195
192
  for (const group of groups) {
196
193
  for (const tx of group.Transactions) {
197
194
  if (needsControlNumber(tx?.ST?.TransactionSetControlNumber_02)) {
@@ -204,20 +201,18 @@ class TemperEdiClient {
204
201
  }
205
202
  }
206
203
  async toX12({ input }) {
207
- // Auto-stamp control numbers before writing X12
208
204
  try {
209
205
  await this.stampControlNumbers(input);
210
206
  }
211
207
  catch (error) {
212
208
  // Log but don't fail — workflows can still set control numbers manually
213
- const { logger } = await import('unnbound-logger-sdk');
214
- logger.warn({ err: error }, '[EDI SDK] Failed to auto-stamp control numbers, proceeding without');
209
+ unnbound_logger_sdk_1.logger.warn({ err: error }, '[EDI SDK] Failed to auto-stamp control numbers, proceeding without');
215
210
  }
216
211
  return (0, unnbound_logger_sdk_1.startSpan)('JSON to X12', () => {
217
212
  return this.X12.x12WritePost({ x12Interchange: input })
218
213
  .then(this.unwrap.bind(this))
219
214
  .catch((error) => this.unwrapError(error, 'edi_write_error'));
220
- }, (o) => buildEdiTransactionPayload('toX12', 'x12', { input, output: o?.result }));
215
+ }, (o) => buildEdiX12Payload('toX12', { input, output: o?.result }));
221
216
  }
222
217
  validateX12({ input }) {
223
218
  return (0, unnbound_logger_sdk_1.startSpan)('Validate X12', () => {
@@ -225,7 +220,7 @@ class TemperEdiClient {
225
220
  .then(this.unwrap.bind(this))
226
221
  .catch((error) => this.unwrapError(error, 'edi_validate_error'));
227
222
  }, (o) => ({
228
- ...buildEdiTransactionPayload('validateX12', 'x12', { input, output: o?.result }),
223
+ ...buildEdiX12Payload('validateX12', { input, output: o?.result }),
229
224
  ...(o?.result?.Status === 'error' && { level: 'warn' }),
230
225
  }));
231
226
  }
@@ -234,87 +229,7 @@ class TemperEdiClient {
234
229
  return this.X12.x12AckPost({ x12Interchange: input })
235
230
  .then(this.unwrap.bind(this))
236
231
  .catch((error) => this.unwrapError(error, 'edi_acknowledge_error'));
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 }));
232
+ }, (o) => buildEdiX12Payload('acknowledgeX12', { input, output: o?.result }));
318
233
  }
319
234
  }
320
235
  exports.TemperEdiClient = TemperEdiClient;
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.2.0-beta.1",
4
+ "version": "1.2.0-beta.2",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "author": "Unnbound Team",
@@ -15,8 +15,8 @@
15
15
  "url": "https://github.com/unnbounddev/unnbound-sdks/issues"
16
16
  },
17
17
  "dependencies": {
18
- "axios": "1.16.0",
19
- "unnbound-logger-sdk": "3.2.0-beta.1"
18
+ "axios": "1.18.0",
19
+ "unnbound-logger-sdk": "3.0.37"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/jest": "^29.5.12",