@ontemper/edi 1.2.0-beta.1 → 1.2.0-beta.3
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 +39 -29
- package/dist/control-numbers.d.ts +12 -4
- package/dist/control-numbers.js +34 -14
- package/dist/index.d.ts +2 -30
- package/dist/index.js +19 -104
- package/examples/node-edi.ts +0 -1
- package/package.json +5 -5
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,54 @@ 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"`.
|
|
357
358
|
|
|
358
|
-
|
|
359
|
-
import { getNextControlNumber, getCurrentControlNumber, setControlNumber } from '@ontemper/edi';
|
|
359
|
+
Each Temper environment now has its own ISA13 and GS06 counter ledger.
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
const next = await getNextControlNumber('ISA13', 'ZZ:SENDER', 'ZZ:RECEIVER');
|
|
361
|
+
### Automatic environment routing
|
|
363
362
|
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
Automatic allocation reads `UNNBOUND_ENVIRONMENT`. Its value must be exactly `sandbox`, `staging`, or
|
|
364
|
+
`production`. If the variable is missing or invalid, the SDK throws a `ControlNumberError` with the
|
|
365
|
+
`control_number_environment_missing` code before making an HTTP request. This fail-closed behavior prevents an
|
|
366
|
+
automatic allocation from using the wrong environment.
|
|
366
367
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
```
|
|
368
|
+
`getNextControlNumber()` and `toX12()` always use this ambient environment. ISA15 (`UsageIndicator_15`) does not
|
|
369
|
+
select a counter, and `toX12()` does not change it.
|
|
370
370
|
|
|
371
|
-
|
|
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"`:
|
|
371
|
+
### Manage counters directly
|
|
377
372
|
|
|
378
373
|
```typescript
|
|
379
|
-
import {
|
|
374
|
+
import { getCurrentControlNumber, getNextControlNumber, setControlNumber } from '@ontemper/edi';
|
|
380
375
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
376
|
+
const senderId = 'ZZ:SENDER';
|
|
377
|
+
const receiverId = 'ZZ:RECEIVER';
|
|
378
|
+
|
|
379
|
+
const next = await getNextControlNumber('ISA13', senderId, receiverId);
|
|
380
|
+
const current = await getCurrentControlNumber('ISA13', senderId, receiverId);
|
|
381
|
+
|
|
382
|
+
// A sandbox onboarding workflow can inspect and seed the production ledger explicitly.
|
|
383
|
+
const productionCurrent = await getCurrentControlNumber('ISA13', senderId, receiverId, 9, {
|
|
384
|
+
environment: 'production',
|
|
385
|
+
});
|
|
386
|
+
await setControlNumber('ISA13', senderId, receiverId, 4600, 9, {
|
|
387
|
+
environment: 'production',
|
|
388
|
+
});
|
|
388
389
|
```
|
|
389
390
|
|
|
390
|
-
|
|
391
|
+
`getCurrentControlNumber()` and `setControlNumber()` accept an optional
|
|
392
|
+
`{ environment: 'sandbox' | 'staging' | 'production' }` argument for cross-environment inspection and seeding.
|
|
393
|
+
An explicit environment takes precedence over `UNNBOUND_ENVIRONMENT`. Without this option, both methods require
|
|
394
|
+
the ambient environment. `getNextControlNumber()` accepts no environment override.
|
|
395
|
+
|
|
396
|
+
A seed is the last-used value: seeding 4600 makes the next allocation 4601. Seed a counter only before its first
|
|
397
|
+
increment. A repeated seed with the same stored value is idempotent; a different seed after the counter starts
|
|
398
|
+
throws a `ControlNumberError` with the `counter_already_started` code.
|
|
399
|
+
|
|
400
|
+
These helpers require `UNNBOUND_API_URL`, which is preconfigured in workflow environments.
|
|
391
401
|
|
|
392
402
|
## Requirements
|
|
393
403
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** @public */
|
|
2
|
-
export type
|
|
2
|
+
export type TemperEnvironment = 'sandbox' | 'staging' | 'production';
|
|
3
|
+
/** @public */
|
|
4
|
+
export type ControlNumberCounterName = 'ISA13' | 'GS06';
|
|
5
|
+
/** @public */
|
|
6
|
+
export interface ControlNumberEnvironmentOptions {
|
|
7
|
+
readonly environment?: TemperEnvironment;
|
|
8
|
+
}
|
|
9
|
+
/** @public */
|
|
10
|
+
export type ControlNumberErrorCode = 'control_number_environment_missing' | 'counter_already_started' | 'control_number_bad_request' | 'control_number_unauthorized' | 'control_number_unknown_error';
|
|
3
11
|
interface ControlNumberErrorOptions extends ErrorOptions {
|
|
4
12
|
code: ControlNumberErrorCode;
|
|
5
13
|
message: string;
|
|
@@ -23,15 +31,15 @@ export declare const isControlNumberError: (error: unknown) => error is ControlN
|
|
|
23
31
|
* @param maxDigits - Zero-pad to this width (default: 9 for ISA13)
|
|
24
32
|
*/
|
|
25
33
|
/** @public */
|
|
26
|
-
export declare function getNextControlNumber(counterName:
|
|
34
|
+
export declare function getNextControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, maxDigits?: number): Promise<string>;
|
|
27
35
|
/**
|
|
28
36
|
* Get the current control number value without incrementing.
|
|
29
37
|
*/
|
|
30
38
|
/** @public */
|
|
31
|
-
export declare function getCurrentControlNumber(counterName:
|
|
39
|
+
export declare function getCurrentControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, maxDigits?: number, options?: ControlNumberEnvironmentOptions): Promise<string>;
|
|
32
40
|
/**
|
|
33
41
|
* Set a control number to a specific value before the counter starts (for migration/seeding).
|
|
34
42
|
*/
|
|
35
43
|
/** @public */
|
|
36
|
-
export declare function setControlNumber(counterName:
|
|
44
|
+
export declare function setControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, value: number, maxDigits?: number, options?: ControlNumberEnvironmentOptions): Promise<string>;
|
|
37
45
|
export {};
|
package/dist/control-numbers.js
CHANGED
|
@@ -78,6 +78,16 @@ exports.ControlNumberError = ControlNumberError;
|
|
|
78
78
|
/** @public */
|
|
79
79
|
const isControlNumberError = (error) => error instanceof ControlNumberError;
|
|
80
80
|
exports.isControlNumberError = isControlNumberError;
|
|
81
|
+
const requireEnvironment = () => {
|
|
82
|
+
const environment = process.env.UNNBOUND_ENVIRONMENT;
|
|
83
|
+
if (environment === 'sandbox' || environment === 'staging' || environment === 'production') {
|
|
84
|
+
return environment;
|
|
85
|
+
}
|
|
86
|
+
throw new ControlNumberError({
|
|
87
|
+
code: 'control_number_environment_missing',
|
|
88
|
+
message: "UNNBOUND_ENVIRONMENT must be set to 'sandbox', 'staging', or 'production' because automatic control-number routing needs it.",
|
|
89
|
+
});
|
|
90
|
+
};
|
|
81
91
|
function unwrapControlNumberError(error) {
|
|
82
92
|
if (error instanceof ControlNumberError)
|
|
83
93
|
throw error;
|
|
@@ -92,7 +102,7 @@ function unwrapControlNumberError(error) {
|
|
|
92
102
|
if (error.response?.status === 400) {
|
|
93
103
|
throw new ControlNumberError({
|
|
94
104
|
code: 'control_number_bad_request',
|
|
95
|
-
message:
|
|
105
|
+
message: "Invalid EDI control number request. Use counter name 'ISA13' or 'GS06' with trading partner IDs and, for set, a value.",
|
|
96
106
|
cause: error,
|
|
97
107
|
});
|
|
98
108
|
}
|
|
@@ -110,6 +120,20 @@ function unwrapControlNumberError(error) {
|
|
|
110
120
|
cause: error,
|
|
111
121
|
});
|
|
112
122
|
}
|
|
123
|
+
async function requestControlNumber(operation, input, environment) {
|
|
124
|
+
const request = { ...input, environment: environment ?? requireEnvironment() };
|
|
125
|
+
try {
|
|
126
|
+
const response = operation === 'count'
|
|
127
|
+
? await getApiClient().get(`/api/v2/internal/edi/control-numbers/${operation}`, {
|
|
128
|
+
params: request,
|
|
129
|
+
})
|
|
130
|
+
: await getApiClient().post(`/api/v2/internal/edi/control-numbers/${operation}`, request);
|
|
131
|
+
return response.data;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return unwrapControlNumberError(error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
113
137
|
/**
|
|
114
138
|
* Get the next control number for an EDI counter.
|
|
115
139
|
*
|
|
@@ -123,7 +147,7 @@ function unwrapControlNumberError(error) {
|
|
|
123
147
|
*/
|
|
124
148
|
/** @public */
|
|
125
149
|
async function getNextControlNumber(counterName, senderId, receiverId, maxDigits = 9) {
|
|
126
|
-
const
|
|
150
|
+
const data = await requestControlNumber('increment', {
|
|
127
151
|
tradingPartnerKey: `${senderId}:${receiverId}`,
|
|
128
152
|
counterName,
|
|
129
153
|
});
|
|
@@ -133,26 +157,22 @@ async function getNextControlNumber(counterName, senderId, receiverId, maxDigits
|
|
|
133
157
|
* Get the current control number value without incrementing.
|
|
134
158
|
*/
|
|
135
159
|
/** @public */
|
|
136
|
-
async function getCurrentControlNumber(counterName, senderId, receiverId, maxDigits = 9) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
},
|
|
142
|
-
});
|
|
160
|
+
async function getCurrentControlNumber(counterName, senderId, receiverId, maxDigits = 9, options) {
|
|
161
|
+
const data = await requestControlNumber('count', {
|
|
162
|
+
tradingPartnerKey: `${senderId}:${receiverId}`,
|
|
163
|
+
counterName,
|
|
164
|
+
}, options?.environment);
|
|
143
165
|
return String(data.value).padStart(maxDigits, '0');
|
|
144
166
|
}
|
|
145
167
|
/**
|
|
146
168
|
* Set a control number to a specific value before the counter starts (for migration/seeding).
|
|
147
169
|
*/
|
|
148
170
|
/** @public */
|
|
149
|
-
async function setControlNumber(counterName, senderId, receiverId, value, maxDigits = 9) {
|
|
150
|
-
const
|
|
151
|
-
.post('/api/v2/internal/edi/control-numbers/set', {
|
|
171
|
+
async function setControlNumber(counterName, senderId, receiverId, value, maxDigits = 9, options) {
|
|
172
|
+
const data = await requestControlNumber('set', {
|
|
152
173
|
tradingPartnerKey: `${senderId}:${receiverId}`,
|
|
153
174
|
counterName,
|
|
154
175
|
value,
|
|
155
|
-
})
|
|
156
|
-
.catch(unwrapControlNumberError);
|
|
176
|
+
}, options?.environment);
|
|
157
177
|
return String(data.value).padStart(maxDigits, '0');
|
|
158
178
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
export type { ControlNumberErrorCode } from './control-numbers';
|
|
1
|
+
import type { X12Interchange } from './edination-client';
|
|
2
|
+
export type { ControlNumberCounterName, ControlNumberEnvironmentOptions, ControlNumberErrorCode, } from './control-numbers';
|
|
3
3
|
export { ControlNumberError, getCurrentControlNumber, getNextControlNumber, isControlNumberError, setControlNumber, } from './control-numbers';
|
|
4
4
|
export * from './edination-client/model';
|
|
5
5
|
interface UnnboundErrorOptions<C extends string = string> extends ErrorOptions {
|
|
@@ -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
|
@@ -40,13 +40,14 @@ exports.TemperEdiClient = exports.isEdiInfrastructureError = exports.EdiInfrastr
|
|
|
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, "getCurrentControlNumber", { enumerable: true, get: function () { return
|
|
47
|
-
Object.defineProperty(exports, "getNextControlNumber", { enumerable: true, get: function () { return
|
|
48
|
-
Object.defineProperty(exports, "isControlNumberError", { enumerable: true, get: function () { return
|
|
49
|
-
Object.defineProperty(exports, "setControlNumber", { enumerable: true, get: function () { return
|
|
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, "getCurrentControlNumber", { enumerable: true, get: function () { return control_numbers_2.getCurrentControlNumber; } });
|
|
48
|
+
Object.defineProperty(exports, "getNextControlNumber", { enumerable: true, get: function () { return control_numbers_2.getNextControlNumber; } });
|
|
49
|
+
Object.defineProperty(exports, "isControlNumberError", { enumerable: true, get: function () { return control_numbers_2.isControlNumberError; } });
|
|
50
|
+
Object.defineProperty(exports, "setControlNumber", { enumerable: true, get: function () { return control_numbers_2.setControlNumber; } });
|
|
50
51
|
__exportStar(require("./edination-client/model"), exports);
|
|
51
52
|
class UnnboundError extends Error {
|
|
52
53
|
code;
|
|
@@ -79,7 +80,7 @@ exports.EdiInfrastructureError = EdiInfrastructureError;
|
|
|
79
80
|
const isEdiInfrastructureError = (error) => error instanceof EdiInfrastructureError;
|
|
80
81
|
exports.isEdiInfrastructureError = isEdiInfrastructureError;
|
|
81
82
|
const buildEdiPayload = (edi) => ({ type: 'edi', edi });
|
|
82
|
-
const
|
|
83
|
+
const buildEdiX12Payload = (operation, x12) => buildEdiPayload({ operation, type: 'x12', x12 });
|
|
83
84
|
const needsControlNumber = (value) => {
|
|
84
85
|
const normalized = value?.trim();
|
|
85
86
|
return !normalized || /^0+$/.test(normalized);
|
|
@@ -87,7 +88,6 @@ const needsControlNumber = (value) => {
|
|
|
87
88
|
const ediAxios = (0, unnbound_logger_sdk_1.traceAxios)(axios_1.default.create(), { getPayload: internal_1.internal });
|
|
88
89
|
class TemperEdiClient {
|
|
89
90
|
X12;
|
|
90
|
-
Edifact;
|
|
91
91
|
constructor() {
|
|
92
92
|
const apiKey = process.env.UNNBOUND_EDI_API_KEY;
|
|
93
93
|
// When UNNBOUND_EDI_BASE_URL is set, use self-hosted EdiFabric InHouse API
|
|
@@ -100,7 +100,6 @@ class TemperEdiClient {
|
|
|
100
100
|
// but the OpenAPI client requires a non-empty value — use a placeholder.
|
|
101
101
|
const config = new edination_client_1.Configuration({ apiKey: apiKey ?? 'self-hosted', basePath });
|
|
102
102
|
this.X12 = new edination_client_1.X12Api(config, undefined, ediAxios);
|
|
103
|
-
this.Edifact = new edination_client_1.EdifactApi(config, undefined, ediAxios);
|
|
104
103
|
}
|
|
105
104
|
unwrap(response) {
|
|
106
105
|
return response.data;
|
|
@@ -147,7 +146,7 @@ class TemperEdiClient {
|
|
|
147
146
|
return this.X12.x12ReadPost({ body: input })
|
|
148
147
|
.then(this.unwrap.bind(this))
|
|
149
148
|
.catch((error) => this.unwrapError(error, 'edi_read_error'));
|
|
150
|
-
}, (o) =>
|
|
149
|
+
}, (o) => buildEdiX12Payload('fromX12', { input, output: o?.result }));
|
|
151
150
|
}
|
|
152
151
|
/**
|
|
153
152
|
* Stamp ISA13/GS06/ST02 control numbers on the interchange if not already set.
|
|
@@ -156,7 +155,6 @@ class TemperEdiClient {
|
|
|
156
155
|
* Zero-filled values from acknowledgment generation are placeholders, not manual overrides.
|
|
157
156
|
*/
|
|
158
157
|
async stampControlNumbers(input) {
|
|
159
|
-
const { getNextControlNumber } = await import('./control-numbers.js');
|
|
160
158
|
const isa = input.ISA;
|
|
161
159
|
const senderQual = (isa.SenderIDQualifier_5 || '').trim();
|
|
162
160
|
const senderId = (isa.InterchangeSenderID_6 || '').trim();
|
|
@@ -164,20 +162,18 @@ class TemperEdiClient {
|
|
|
164
162
|
const receiverId = (isa.InterchangeReceiverID_8 || '').trim();
|
|
165
163
|
const senderKey = `${senderQual}:${senderId}`;
|
|
166
164
|
const receiverKey = `${receiverQual}:${receiverId}`;
|
|
167
|
-
//
|
|
168
|
-
// If any fetch fails, no fields are mutated — avoids partial stamping.
|
|
165
|
+
// Allocate before mutating so a failed request cannot leave partial stamps.
|
|
169
166
|
const isa13 = needsControlNumber(isa.InterchangeControlNumber_13)
|
|
170
|
-
? await getNextControlNumber('ISA13', senderKey, receiverKey, 9)
|
|
167
|
+
? await (0, control_numbers_1.getNextControlNumber)('ISA13', senderKey, receiverKey, 9)
|
|
171
168
|
: null;
|
|
172
169
|
const gs06Values = [];
|
|
173
170
|
const groups = input.Groups ?? [];
|
|
174
171
|
for (let i = 0; i < groups.length; i++) {
|
|
175
172
|
if (needsControlNumber(groups[i].GS.GroupControlNumber_6)) {
|
|
176
|
-
const value = await getNextControlNumber('GS06', senderKey, receiverKey, 9);
|
|
173
|
+
const value = await (0, control_numbers_1.getNextControlNumber)('GS06', senderKey, receiverKey, 9);
|
|
177
174
|
gs06Values.push({ index: i, value });
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
|
-
// Phase 2: Apply all mutations (only reached if all fetches succeeded).
|
|
181
177
|
if (isa13) {
|
|
182
178
|
isa.InterchangeControlNumber_13 = isa13;
|
|
183
179
|
if (input.IEATrailers?.length) {
|
|
@@ -191,7 +187,6 @@ class TemperEdiClient {
|
|
|
191
187
|
group.GETrailers[0].GroupControlNumber_2 = value;
|
|
192
188
|
}
|
|
193
189
|
}
|
|
194
|
-
// ST02 — always "0001" (pure assignment, no external calls)
|
|
195
190
|
for (const group of groups) {
|
|
196
191
|
for (const tx of group.Transactions) {
|
|
197
192
|
if (needsControlNumber(tx?.ST?.TransactionSetControlNumber_02)) {
|
|
@@ -204,20 +199,20 @@ class TemperEdiClient {
|
|
|
204
199
|
}
|
|
205
200
|
}
|
|
206
201
|
async toX12({ input }) {
|
|
207
|
-
// Auto-stamp control numbers before writing X12
|
|
208
202
|
try {
|
|
209
203
|
await this.stampControlNumbers(input);
|
|
210
204
|
}
|
|
211
205
|
catch (error) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
206
|
+
if (error instanceof control_numbers_1.ControlNumberError)
|
|
207
|
+
throw error;
|
|
208
|
+
// Workflows can still serialize documents whose control numbers were set manually.
|
|
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/examples/node-edi.ts
CHANGED
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.3",
|
|
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",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"build": "tsc",
|
|
38
38
|
"test": "vitest run src",
|
|
39
39
|
"typecheck": "tsc --noEmit",
|
|
40
|
-
"format": "
|
|
41
|
-
"format:check": "
|
|
40
|
+
"format": "oxfmt --write .",
|
|
41
|
+
"format:check": "oxfmt --check .",
|
|
42
42
|
"start:example": "tsx watch examples/node-edi.ts",
|
|
43
43
|
"version:bump": "npm version patch",
|
|
44
44
|
"release": "pnpm run build && pnpm publish --access public",
|