@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 +66 -27
- package/dist/control-numbers.d.ts +30 -4
- package/dist/control-numbers.js +70 -14
- package/dist/index.d.ts +3 -31
- package/dist/index.js +19 -104
- package/package.json +3 -3
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
|
|
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
|
|
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
|
|
353
|
+
## Control numbers
|
|
355
354
|
|
|
356
|
-
The SDK
|
|
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 {
|
|
369
|
+
import {
|
|
370
|
+
getControlNumberPolicy,
|
|
371
|
+
getCurrentControlNumber,
|
|
372
|
+
setControlNumber,
|
|
373
|
+
setControlNumberPolicy,
|
|
374
|
+
} from '@ontemper/edi';
|
|
360
375
|
|
|
361
|
-
|
|
362
|
-
const
|
|
376
|
+
const senderId = 'ZZ:SENDER';
|
|
377
|
+
const receiverId = 'ZZ:RECEIVER';
|
|
363
378
|
|
|
364
|
-
|
|
365
|
-
const current = await getCurrentControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER');
|
|
379
|
+
await setControlNumberPolicy(senderId, receiverId, { separateCounters: true });
|
|
366
380
|
|
|
367
|
-
//
|
|
368
|
-
await setControlNumber('ISA13',
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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 {
|
|
412
|
+
import { getCurrentControlNumber, getNextControlNumber, setControlNumber } from '@ontemper/edi';
|
|
380
413
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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 {};
|
package/dist/control-numbers.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
45
|
-
Object.defineProperty(exports, "ControlNumberError", { enumerable: true, get: function () { return
|
|
46
|
-
Object.defineProperty(exports, "
|
|
47
|
-
Object.defineProperty(exports, "
|
|
48
|
-
Object.defineProperty(exports, "
|
|
49
|
-
Object.defineProperty(exports, "
|
|
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
|
|
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) =>
|
|
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
|
-
//
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
...
|
|
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) =>
|
|
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.
|
|
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.
|
|
19
|
-
"unnbound-logger-sdk": "3.
|
|
18
|
+
"axios": "1.18.0",
|
|
19
|
+
"unnbound-logger-sdk": "3.0.37"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/jest": "^29.5.12",
|