feat: phase g - EDIFACT #13

Merged
KofTwentyTwo merged 2 commits from feature/phase-g-edifact into develop 2026-10-14 13:59:27 +00:00
60 changed files with 7776 additions and 66 deletions

No files matched your search

+4
View File
@@ -6,6 +6,10 @@ Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic
### Added
- Phase g EDIFACT syntax 3/4 envelopes (UNA/UNB, optional UNG/UNE, UNH/UNT, UNZ), release escaping, native envelope findings, UNB identity routing, 14-digit UN control allocation and duplicate detection.
- Original D96A/D01B ORDERS, ORDRSP, DESADV, INVOIC and CONTRL guides; ORDERS inbound and retail response outbound sample maps with composite fields and segment groups.
- Standard-neutral document validation/mapping, mixed X12/EDIFACT inbox framing, CONTRL generation and reconciliation with overdue resolution and rejection alerts, native outbound envelopes and archived resend-as-new.
- Lindqvist Retail AB demo partner with ORDERS application, ORDRSP/DESADV/INVOIC delivery and CONTRL in both directions; EDIFACT map preview and HTTPS media type.
- Phase e original HIPAA baselines for 837P/I, 835, 270/271, 276/277 and 834; exact SNIP 3 balancing, occurrence-scoped SNIP 4 rules, SNIP 5 formats and a host `ExternalCodeSetProvider` SPI.
- Test-only healthcare maps, classified sample tables, seeded synthetic fixtures, Cedar Health Clearinghouse demo traffic and a PHI canary scanner covering both ingress paths, 999s, apply/replay/resend, widgets, query exports, alerts and logs.
- Permission-checked, session-bound Download Raw Document actions for inbox/interchange rows, with a value-free user audit event.
+26 -6
View File
@@ -1,17 +1,37 @@
# QBit EDI
EDI for QQQ: trading partners, agreements, X12 validation, encrypted payloads, inbound mapping
and application, and TA1/997/999 acknowledgements. See [the design](docs/DESIGN.md).
EDI for QQQ: trading partners, agreements, X12 and EDIFACT validation, encrypted payloads, inbound mapping
and application, and TA1/997/999/CONTRL acknowledgements. See [the design](docs/DESIGN.md).
## Status
Phases a1, a2, b and c implement inbound/outbound retail EDI over gateway, SFTP, HTTPS and AS2. Original baseline guides cover
Phases a1 to e and g implement inbound/outbound retail, logistics and healthcare EDI (X12 and EDIFACT) over gateway, SFTP, HTTPS and AS2. Original baseline guides cover
850, 855, 856, 810, 846 and 860 in 004010/005010, plus 997/999. Source records and associations
produce validated, encrypted outbound interchanges, with control-number tracking, retries,
acknowledgement monitoring and operator resend/cancel actions. AS2 (signed, encrypted, compressed, synchronous and
asynchronous MDN, certificate rollover) is built in; email sending remains disabled until the qbit-email API ships.
A runnable synthetic [demo](docs/DEMO.md) includes Next UI and needs no external services.
EDIFACT syntax versions 3 and 4 support UNA/UNB, optional UNG groups, UNH messages and their
trailers, release-character escaping, mixed-standard payloads, and CONTRL syntax responses.
Original D96A/D01B baselines cover ORDERS, ORDRSP, DESADV, INVOIC and CONTRL. ORDERS maps to
sample orders and lines; the other three retail documents map outbound. APERAK remains future work.
Configure `standard=EDIFACT` on identities and agreements. `interchangeId` and
`interchangeQualifier` carry the UNB party identifiers and qualifiers. Set `interchangeVersion`
to `3` or `4`, `syntaxIdentifier` to `UNOC`, and outbound separators explicitly: element `+`,
component `:`, segment `'`, release `?`, decimal `.` (or `,`); syntax 4 also uses the configured
repetition separator. Document versions are `D96A` or `D01B`, with `ackExpectation=CONTRL`.
UNB11=`1` uses a test agreement and respects `applyTestDocuments`.
Existing schemas need these additional nullable agreement columns before using this version:
```sql
ALTER TABLE edi_agreement ADD COLUMN release_character VARCHAR(255);
ALTER TABLE edi_agreement ADD COLUMN decimal_mark VARCHAR(255);
ALTER TABLE edi_agreement ADD COLUMN syntax_identifier VARCHAR(255);
```
## Requirements and installation
Java 21, QQQ **4.1.0-RC.5**, an RDBMS backend, and `com.kingsrook.qbits:qbit-edi:0.1.0-SNAPSHOT`.
@@ -66,10 +86,10 @@ Storage failure requests retry in one minute.
`InboxWorker.runOnce()` processes up to 20 messages with compare-and-set claims, fenced database
checkpoints and five-minute stale-claim recovery. Retry delays are 1m, 5m, 15m, 1h and 6h; the next
failure becomes DEAD_LETTER. Each mapped accepted ST applies in its own transaction through QQQ
actions with explicit permission enforcement. ISA15=T applies only with `applyTestDocuments`.
failure becomes DEAD_LETTER. Each mapped accepted ST or UNH applies in its own transaction through QQQ
actions with explicit permission enforcement. ISA15=T or UNB11=1 applies only with `applyTestDocuments`.
Routing checks ISA identities, qualifiers, standard and usage. Linked gateway usernames must match
Routing checks ISA or UNB identities, qualifiers, standard and usage. Linked gateway usernames must match
the routed partner. Findings and failures become exceptions with occurrence counts and value-free
timeline events. Replies allocate controls and remain QUEUED, recording `ACK_QUEUED`. Incoming
acknowledgements reconcile outbound groups and resolve overdue alerts.
+9 -2
View File
@@ -42,10 +42,17 @@ and sends three valid 940 shipping orders and one with a bad unit; the orders an
four 214 status events (one rejected with a bad status code) that build `sample_shipment_status`.
The in-memory gateway accepts every send, and the rejected documents produce rejecting 997s.
Phase g adds a fifth partner, **Lindqvist Retail AB** (LINDQVIST), after the existing scenarios.
Its D96A test agreement receives three synthetic ORDERS documents through the durable inbox;
orders and lines appear in `sample_order`, and CONTRLs enter the gateway queue. ORDRSP, DESADV
and INVOIC are mapped from the retail sample records, delivered through the in-memory gateway,
and reconciled by incoming accepted CONTRLs. Look for ORDERS with status APPLIED and the three
outbound message types with status ACKNOWLEDGED. Released delimiters in buyer/item identifiers
exercise the same parser used for partner input. All contacts remain synthetic `example.invalid`.
The SFTP/HTTPS partner settings are illustrative `.invalid` destinations. Their agreements have
no outbound transport, so they cannot contact a real partner; resulting acknowledgement send
retries are visible. Northwind has the retail outbound documents; the logistics and healthcare
scenarios add their own outbound configurations. The in-memory gateway
retries are visible. Northwind, Harbor, Ridgeline and Lindqvist have configured outbound documents; the healthcare scenario adds its own. The in-memory gateway
is restored after the intentional dead-letter scenario. Standard Send As EDI, Test Connection,
Resend, Resend As New and Cancel actions are available. The gateway stores bytes in memory;
it does not expose a separate partner download UI.
+27 -6
View File
@@ -420,10 +420,10 @@ in memory; no keystore, key or temporary file is written, and as2-lib's file-bac
- **Email in:** through qbit-email's phase 2 inbound (an `InboundEmailHandler` registered by EDI).
Designed here, built when qbit-email inbound ships.
## 6. Parsing (X12 now, EDIFACT later)
## 6. Parsing (X12 and EDIFACT)
- StAEDI (`EDIInputFactory`, `EDIStreamReader`) reads X12 as a stream, with no whole-document tree,
so a 50 MB 837 batch is processed in bounded memory per transaction set.
- StAEDI (`EDIInputFactory`, `EDIStreamReader`) tokenizes X12 and EDIFACT. Inbox framing buffers
one interchange at a time, capped at 50 MiB, and each transaction retains a bounded segment list.
- One payload may hold several interchanges; each ISA/IEA becomes an `edi_interchange` row. Each
ST/SE is buffered as its own segment list (bounded by the guide's maximum segment count, default
100,000) for validation, ack building and mapping.
@@ -433,9 +433,15 @@ in memory; no keystore, key or temporary file is written, and as2-lib's file-bac
(005010 extended). Unrecognised bytes are an envelope error, never silently replaced.
- Writing uses an in-memory serializer with the agreement's separators, fixed-width ISA
padding, component/repetition handling, and optional line endings.
- EDIFACT: StAEDI reads UNA/UNB/UNG/UNH and validates against EDIFACT schemas. It is designed into
the model now (`standard` columns, UNB fields on `edi_identity`, CONTRL as an ack type) and built
in phase g.
- EDIFACT is built: StAEDI reads syntax 3/4 UNA/UNB/UNG/UNH envelopes. UNA advice and release
characters survive round trips; UNT/UNE/UNZ counts and references produce findings. A payload
may contain both X12 and EDIFACT interchanges. In-house D96A/D01B ORDERS/ORDRSP/DESADV/INVOIC
and CONTRL guides use the shared composite and loop grammar.
- EDIFACT routing uses the identity's `interchangeId`/`interchangeQualifier` as UNB identifiers;
UNB11=1 selects test usage. The agreement's syntax identifier, version and service characters
control output. UNB/UNH control references use the allocator's EDIFACT levels (up to 14 digits).
CONTRL reconciles outbound status and resolves overdue alerts; rejected responses open
REJECTED_ACK and raise `edi.ack.rejected`. APERAK remains a later document type.
## 7. Implementation guides as data
@@ -1014,3 +1020,18 @@ RC.5 adaptations implemented in this worktree:
otherwise bypasses field locks inside the already-authorized widget action scope.
- MASKED-user response scanning and Quick Search indexing are not available on the pinned RC.5
test classpath. FULL and actual HIDDEN-field-lock scans run now; no simulated masking is used.
## Phase g implementation notes (2026-10-14)
- Outbound EDIFACT is ungrouped. Readers/writers preserve optional UNG/UNE groups. Ungrouped
interchanges have a logical `edi_functional_group` tracking row, keyed by UNB reference, so
the existing send-time deadline/overdue monitor also covers CONTRL. `groupCount` remains zero
for those wire envelopes. GROUP_ONLY means an interchange-level CONTRL in this case.
- Payload framing buffers one interchange (hard cap 50 MiB) before StAEDI; retained message
segments respect `maxSegmentsPerTransaction`. It uses no filesystem. This follows the existing
buffered storage adapters rather than the original bounded-memory streaming aspiration.
- Retail SG labels are local guide identifiers shared by the two baseline versions, as documented
in GUIDE-FORMAT.md; the baselines are deliberately small, not complete directory content.
- Existing databases require the three agreement columns listed in README.md. Existing X12
separator defaults remain unchanged; EDIFACT agreements explicitly select their outbound UNA
characters. CONTRL action 8 is receipt-only and does not clear a functional acknowledgement deadline.
+28 -1
View File
@@ -1,6 +1,6 @@
# EDI guide JSON
The five files in `src/main/resources/edi/guides/` are original QQQ baseline guides, not licensed
The files in `src/main/resources/edi/guides/` are original QQQ baseline guides, not licensed
X12/TR3 documentation or complete code sets. JSON loads into `guide.Guide` with
`BuiltInGuideLoader.read(InputStream)`. `GuideRepository.load(id)` reconstructs the same model from
the normalized `edi_guide*` and code-list tables.
@@ -49,3 +49,30 @@ historical rows and their children remain addressable by id. A reverted resource
new revision. Built-in guide writes require the loader's security key (`edi.guide.loader`), or a
system service session. Configuration editors can receive `CUSTOM`/`IMPORTED` values of the
`edi_guide_source` key without receiving permission to edit `BUILT_IN` rows.
## EDIFACT baselines
Set `standard` to `EDIFACT`, `transactionSet` to the UNH message type, and `version` to the
concatenated UNH version/release (`D96A`, `D01B`). The ten bundled resources are in-house retail
subsets with short field names and only the code values needed by the maps. They contain no
copied directory descriptions and are not complete UNECE directories or EANCOM certifications.
UNH and UNT appear as actual guide segments. UNB/UNZ and UNG/UNE are checked by the envelope
reader. UNH association codes, such as `EAN008`, remain available as component 5 of UNH02.
EDIFACT mandatory (`M`) maps to `REQUIRED`; conditional (`C`) maps to `SITUATIONAL` with
`condition` or rules where needed. A component uses the existing `position` plus
`componentPosition`, for example NAD02 component 1. EDIFACT decimal values accept `.` or `,`;
DECIMAL/UNIT inbound transforms normalize them and outbound DECIMAL uses the agreement's mark.
Segment groups use the existing loop tree: SG1 starts with RFF, SG2 with NAD, and the retail
line loop SG25 starts with LIN. These are stable local guide loop labels, not a claim that every
message/directory assigns the same published group numbers. DESADV uses SG10 (CPS) containing
SG17 (LIN), matching carton/item associations. Map examples are `NAD[NAD01=BY]/NAD02-1`,
`SG25/LIN/LIN03-1` and `SG25[*]/QTY/QTY01-2`. Named loops take precedence over element parsing,
so `SG25` is a loop rather than an element of a hypothetical `SG` segment.
CONTRL baselines include UCI, optional UCF group responses, UCM message responses and nested
UCS/UCD positional errors. Shared validator diagnostics are translated to EDIFACT 0085 codes;
submitted erroneous values are not copied into the acknowledgement. `GROUP_ONLY` reports the
interchange outcome, `ALL_TRANSACTIONS` includes accepted messages, and `ERRORS_ONLY` includes
rejected message details. A CONTRL is never itself acknowledged. APERAK is deferred.
+16 -3
View File
@@ -52,9 +52,9 @@ for Shipment/Order/Pack/Item loops and parent numbering.
DATE/TIME format QQQ values as X12 dates/times; DECIMAL multiplies by the implied `scale`.
Other registered transforms operate on the source value. Conditions use the existing EDI rule
language over segments already emitted. A missing required mapping input fails mapping;
structurally invalid mapped output opens VALIDATION and is never sent. ST/SE are generated by
the mapper and group-local controls are assigned by the worker. Component-target paths are
currently rejected explicitly; the bundled retail maps use simple elements.
structurally invalid mapped output opens VALIDATION and is never sent. ST/SE or UNH/UNT are generated by
the mapper; the worker allocates controls. Component-target paths use `-`, for example
`LIN03-1` and `LIN03-2`, and retain components separately so the EDIFACT writer can escape data.
## Warehouse and logistics samples
@@ -76,3 +76,16 @@ collide with the header N1 loop) and the stop sequence is mapped from S501. A 21
events; the sample map records the first (`LX[1]/...`) and a host that wants all of them maps an
association instead. Optional outbound qualifiers such as MAN01 and L1102 are constants, so the
sample source rows must carry the value they qualify (the carton SSCC, the bill of lading).
## EDIFACT retail samples
Each of D96A and D01B includes `ORDERS-<version>-sample.json` inbound to `sample_order` and
`lines`, and outbound `ORDRSP` from `sample_order`, `DESADV` from `sample_shipment` with
`cartons.items`, and `INVOIC` from `sample_invoice` with `lines`. Test sources define these
synthetic tables. ORDERS carries its business key in BGM02, date in qualified DTM01 component 2,
buyer in qualified NAD02 component 1, and items in SG25. BGM's document codes are 220, 231, 351
and 380 respectively. These subsets intentionally carry only the fields exercised by the samples.
Composite paths address values, not delimiter-joined strings. An identifier containing `+`, `:`
or `'` remains a single value; the writer supplies release escapes. The outbound agreement owns
UNA settings. Separate map resources per directory keep future version-specific changes isolated.
+3
View File
@@ -3,6 +3,7 @@ CREATE TABLE edi_agreement (
component_separator VARCHAR(255),
control_number_scope VARCHAR(255),
create_date TIMESTAMP,
decimal_mark VARCHAR(255),
duplicate_window_days INTEGER,
element_separator VARCHAR(255),
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
@@ -19,11 +20,13 @@ CREATE TABLE edi_agreement (
outbound_transport_id INTEGER,
partner_id INTEGER,
partner_identity_id INTEGER,
release_character VARCHAR(255),
repetition_separator VARCHAR(255),
request_ta1 BOOLEAN,
segment_terminator VARCHAR(255),
send_ta1 VARCHAR(255),
standard VARCHAR(255),
syntax_identifier VARCHAR(255),
usage_indicator VARCHAR(255),
UNIQUE (local_identity_id, partner_identity_id, standard, usage_indicator)
);
@@ -0,0 +1,46 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.document;
import java.util.List;
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
import com.kingsrook.qbits.edi.x12.X12Segment;
/*******************************************************************************
** Standard-neutral document view over the existing immutable segment value model.
*******************************************************************************/
public interface EdiDocument
{
/*******************************************************************************
** Return the actual message header, including its original composite values.
*******************************************************************************/
X12Segment header();
/*******************************************************************************
** Return ordered segments including the message envelope.
*******************************************************************************/
List<X12Segment> segments();
/*******************************************************************************
** Return envelope diagnostics without submitted values.
*******************************************************************************/
List<EnvelopeFinding> findings();
}
@@ -0,0 +1,120 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.document;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import com.kingsrook.qbits.edi.edifact.EdifactSeparators;
/*******************************************************************************
** Frame concatenated X12 and EDIFACT envelopes without delimiter guessing in data.
*******************************************************************************/
public class EnvelopeReader
{
/*******************************************************************************
** Read one envelope, respecting UNA and released terminators; null means EOF.
*******************************************************************************/
public byte[] next(InputStream input) throws IOException
{
int first;
do
{
first = input.read();
}
while(first != -1 && Character.isWhitespace(first));
if(first == -1)
{
return (null);
}
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(first);
bytes.write(input.readNBytes(2));
String tag = bytes.toString(StandardCharsets.US_ASCII);
char terminator;
char release = 0;
String end;
StringBuilder prefix = new StringBuilder();
if("ISA".equals(tag))
{
bytes.write(input.readNBytes(103));
if(bytes.size() != 106)
{
return (bytes.toByteArray());
}
terminator = (char) bytes.toByteArray()[105];
end = "IEA";
}
else if("UNA".equals(tag) || "UNB".equals(tag))
{
EdifactSeparators separators = EdifactSeparators.DEFAULT;
if("UNA".equals(tag))
{
bytes.write(input.readNBytes(6));
separators = EdifactSeparators.fromUna(bytes.toString(StandardCharsets.US_ASCII));
}
else
{
prefix.append("UNB");
}
terminator = separators.segment();
release = separators.release();
end = "UNZ";
}
else
{
throw new IOException("Unknown EDI envelope");
}
boolean escaped = false;
int value;
while((value = input.read()) != -1)
{
bytes.write(value);
if(bytes.size() > 50 * 1024 * 1024)
{
throw new IOException("EDI envelope exceeds size limit");
}
if(escaped)
{
escaped = false;
continue;
}
if(value == release)
{
escaped = true;
}
else if(value == terminator)
{
if(prefix.toString().equals(end))
{
break;
}
prefix.setLength(0);
}
else if(prefix.length() < 3 && !(prefix.isEmpty() && Character.isWhitespace(value)))
{
prefix.append((char) value);
}
}
return (bytes.toByteArray());
}
}
@@ -0,0 +1,152 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.edi.ack.AckLevel;
import com.kingsrook.qbits.edi.guide.ValidationFinding;
import com.kingsrook.qbits.edi.x12.X12Segment;
/*******************************************************************************
** Positional CONTRL syntax responses; business application failures are not syntax errors.
*******************************************************************************/
public class ContrlBuilder
{
/*******************************************************************************
** Build directory-specific acknowledgement details without echoing data values.
*******************************************************************************/
public EdifactMessage build(EdifactInterchange input, Map<String, List<ValidationFinding>> findings, AckLevel level, String reference, String version)
{
List<X12Segment> segments = new ArrayList<>();
segments.add(EdifactWriter.segment("UNH", 1, List.of(reference), List.of("CONTRL", version.substring(0, 1), version.substring(1), "UN")));
boolean rejected = !input.findings().isEmpty() || level == AckLevel.GROUP_ONLY && (input.groups().stream().anyMatch(g -> !g.findings().isEmpty())
|| input.allMessages().stream().anyMatch(m -> rejected(m, findings)));
var uci = new ArrayList<List<String>>();
uci.add(List.of(input.unb().value(5)));
uci.add(input.unb().elements().get(1).getFirst());
uci.add(input.unb().elements().get(2).getFirst());
uci.add(List.of(rejected ? "4" : "7"));
if(!input.findings().isEmpty())
{
uci.add(List.of(input.findings().getFirst().code()));
}
segments.add(new X12Segment("UCI", 2, uci.stream().map(List::of).toList()));
if(level != AckLevel.GROUP_ONLY && input.findings().isEmpty())
{
messages(input.messages(), findings, level, segments);
for(var group : input.groups())
{
var response = EdifactWriter.segment("UCF", segments.size() + 1, List.of(group.ung().value(5)), group.ung().elements().get(1).getFirst(),
group.ung().elements().get(2).getFirst(), List.of(group.findings().isEmpty() ? "7" : "4"));
if(!group.findings().isEmpty())
{
var fields = new ArrayList<>(response.elements());
fields.add(List.of(List.of(group.findings().getFirst().code())));
response = new X12Segment("UCF", response.position(), fields);
}
segments.add(response);
messages(group.messages(), findings, level, segments);
}
}
segments.add(X12Segment.of("UNT", segments.size() + 1, String.valueOf(segments.size() + 1), reference));
return (new EdifactMessage(segments.getFirst(), segments.getLast(), List.copyOf(segments), List.of()));
}
/*******************************************************************************
** Suppress accepted message detail only under ERRORS_ONLY.
*******************************************************************************/
private void messages(List<EdifactMessage> messages, Map<String, List<ValidationFinding>> findings, AckLevel level, List<X12Segment> segments)
{
for(var message : messages)
{
if("CONTRL".equals(message.type()))
{
continue;
}
boolean rejected = rejected(message, findings);
if(level == AckLevel.ERRORS_ONLY && !rejected)
{
continue;
}
var fields = new ArrayList<List<List<String>>>();
fields.add(List.of(List.of(message.reference())));
fields.add(message.header().elements().get(1));
fields.add(List.of(List.of(rejected ? "4" : "7")));
if(!message.findings().isEmpty())
{
fields.add(List.of(List.of(message.findings().getFirst().code())));
}
segments.add(new X12Segment("UCM", segments.size() + 1, fields));
for(var finding : findings.getOrDefault(message.reference(), List.of()))
{
if(!"ERROR".equals(finding.severity()))
{
continue;
}
String code = syntaxCode(finding);
segments.add(finding.elementPosition() == null
? X12Segment.of("UCS", segments.size() + 1, String.valueOf(finding.segmentPosition()), code)
: X12Segment.of("UCS", segments.size() + 1, String.valueOf(finding.segmentPosition())));
if(finding.elementPosition() != null)
{
List<String> position = finding.componentPosition() == null ? List.of(finding.elementPosition().toString())
: List.of(finding.elementPosition().toString(), finding.componentPosition().toString());
segments.add(EdifactWriter.segment("UCD", segments.size() + 1, List.of(code), position));
}
}
}
}
/*******************************************************************************
** Envelope and structural errors reject the message, warnings do not.
*******************************************************************************/
private boolean rejected(EdifactMessage message, Map<String, List<ValidationFinding>> findings)
{
return (!message.findings().isEmpty() || findings.getOrDefault(message.reference(), List.of()).stream().anyMatch(f -> "ERROR".equals(f.severity())));
}
/*******************************************************************************
** Translate shared validator diagnostics to service syntax codes (0085).
*******************************************************************************/
public static String syntaxCode(ValidationFinding finding)
{
String code = finding.elementPosition() == null ? finding.ak3Code() : finding.ak4Code();
if(code == null)
{
return ("12");
}
return (finding.elementPosition() == null ? switch(code)
{
case "3" -> "13";
case "4", "5" -> "35";
case "2", "6", "7" -> "33";
default -> "12";
} : switch(code)
{
case "1", "2" -> "13";
case "4" -> "14";
case "5" -> "15";
default -> "12";
});
}
}
@@ -0,0 +1,143 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.edi.EdiRuntime;
import com.kingsrook.qbits.edi.flow.EdiJournal;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qbits.edi.x12.X12Segment;
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
import com.kingsrook.qqq.backend.core.alerts.QAlerts;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Reconcile addressed CONTRL controls only within the routed outbound agreement.
*******************************************************************************/
public class ContrlReader
{
/*******************************************************************************
** Apply functional acceptance/rejection; receipt-only code 8 leaves deadlines open.
*******************************************************************************/
public void reconcile(EdifactMessage message, QRecord agreement, QRecord received) throws Exception
{
X12Segment uci = message.segments().stream().filter(s -> "UCI".equals(s.id())).findFirst().orElseThrow();
if("8".equals(uci.value(4)))
{
return;
}
String overall = status(uci.value(4));
var candidates = EdiRecords.query("edi_interchange", EdiRecords.equal("agreementId", agreement.getValue("id")).withCriteria("direction", QCriteriaOperator.EQUALS, "OUTBOUND")
.withCriteria("standard", QCriteriaOperator.EQUALS, "EDIFACT").withCriteria("controlNumber", QCriteriaOperator.EQUALS, uci.value(1))
.withCriteria("senderId", QCriteriaOperator.EQUALS, uci.value(2)).withCriteria("senderQualifier", QCriteriaOperator.EQUALS, uci.value(2, 1, 2))
.withCriteria("receiverId", QCriteriaOperator.EQUALS, uci.value(3)).withCriteria("receiverQualifier", QCriteriaOperator.EQUALS, uci.value(3, 1, 2)));
if(candidates.size() != 1)
{
throw new IllegalArgumentException("Acknowledgement reference is unknown or ambiguous");
}
QRecord interchange = candidates.getFirst();
Map<String, X12Segment> details = new LinkedHashMap<>();
Map<String, String> groupStatuses = new LinkedHashMap<>();
String currentGroup = "";
for(var segment : message.segments())
{
if("UCF".equals(segment.id()))
{
currentGroup = segment.value(1);
groupStatuses.put(currentGroup, status(segment.value(4)));
}
if("UCM".equals(segment.id()))
{
String key = currentGroup + ":" + segment.value(1);
if(details.put(key, segment) != null)
{
throw new IllegalArgumentException("Duplicate acknowledgement message reference");
}
status(segment.value(3));
}
}
boolean rejected = "R".equals(overall);
var groups = EdiRecords.query("edi_functional_group", EdiRecords.equal("interchangeId", interchange.getValue("id")));
for(var group : groups)
{
String scope = interchange.getValueInteger("groupCount") == 0 ? "" : group.getValueString("controlNumber");
String groupStatus = "R".equals(overall) ? "R" : groupStatuses.getOrDefault(scope, overall);
boolean groupRejected = "R".equals(groupStatus);
for(var tx : EdiRecords.query("edi_transaction", EdiRecords.equal("functionalGroupId", group.getValue("id"))))
{
X12Segment detail = details.remove(scope + ":" + tx.getValueString("controlNumber"));
if(detail != null && (!detail.value(2).equals(tx.getValueString("transactionSet"))
|| !(detail.value(2, 1, 2) + detail.value(2, 1, 3)).equals(tx.getValueString("version"))))
{
throw new IllegalArgumentException("Acknowledgement message identification mismatch");
}
String result = "R".equals(groupStatus) || detail != null && "R".equals(status(detail.value(3))) ? "R" : "A";
groupRejected |= "R".equals(result);
SystemWrites.update("edi_transaction", new QRecord().withValue("id", tx.getValue("id")).withValue("ackStatus", result)
.withValue("status", "R".equals(result) ? "REJECTED" : "ACKNOWLEDGED"));
}
rejected |= groupRejected;
SystemWrites.update("edi_functional_group", new QRecord().withValue("id", group.getValue("id")).withValue("ackStatus", groupRejected ? "R" : "A")
.withValue("ackReceivedAt", EdiRuntime.current().config().getClock().instant()).withValue("ackTransactionId", received.getValue("id")));
}
if(!details.isEmpty() || groupStatuses.keySet().stream().anyMatch(c -> groups.stream().noneMatch(g -> c.equals(g.getValueString("controlNumber")))))
{
throw new IllegalArgumentException("Acknowledgement contains an unknown message or group reference");
}
SystemWrites.update("edi_interchange", new QRecord().withValue("id", interchange.getValue("id")).withValue("status", rejected ? "REJECTED" : "ACKNOWLEDGED"));
SystemWrites.update("edi_transaction", new QRecord().withValue("id", received.getValue("id")).withValue("status", "ACKNOWLEDGED").withValue("ackStatus", rejected ? "R" : "A"));
for(var group : groups)
{
QAlerts.resolve("edi.ack.overdue", "edi_functional_group:" + group.getValue("id"), "CONTRL received");
}
for(var exception : EdiRecords.query("edi_exception", EdiRecords.equal("interchangeId", interchange.getValue("id")).withCriteria("type", QCriteriaOperator.EQUALS, "OVERDUE_ACK")
.withCriteria("status", QCriteriaOperator.IN, "OPEN", "IN_PROGRESS")))
{
SystemWrites.update("edi_exception", new QRecord().withValue("id", exception.getValue("id")).withValue("status", "RESOLVED")
.withValue("resolvedAt", EdiRuntime.current().config().getClock().instant()).withValue("resolution", "CONTRL received"));
}
if(rejected)
{
EdiJournal.exception(null, interchange.getValueInteger("id"), null, agreement.getValueInteger("partnerId"), "REJECTED_ACK", "Partner rejected an outbound EDIFACT document");
QAlerts.raise(new AlertSignal("edi.ack.rejected").withSeverity(AlertSeverity.ERROR).withDedupKey("edi_interchange:" + interchange.getValue("id"))
.withTitle("EDI acknowledgement rejected").withSummary("Review the outbound interchange"));
}
EdiJournal.event(null, interchange.getValueInteger("id"), received.getValueInteger("id"), "ACK_RECEIVED");
}
/*******************************************************************************
** Only functional acceptance and rejection can complete an expected response.
*******************************************************************************/
private String status(String action)
{
return (switch(action)
{
case "7" -> "A";
case "4" -> "R";
default -> throw new IllegalArgumentException("Unsupported CONTRL action");
});
}
}
@@ -0,0 +1,267 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import com.kingsrook.qbits.edi.document.EnvelopeReader;
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
import com.kingsrook.qbits.edi.x12.X12Segment;
import io.xlate.edi.stream.EDIInputFactory;
import io.xlate.edi.stream.EDIStreamEvent;
import io.xlate.edi.stream.EDIStreamException;
import io.xlate.edi.stream.EDIStreamReader;
import io.xlate.edi.stream.EDIStreamValidationError;
/*******************************************************************************
** StAEDI tokenizer with explicit UN envelope checks and bounded message retention.
*******************************************************************************/
public class EdifactEnvelopeReader
{
private final int maxSegments;
/*******************************************************************************
** Bound retained message segments, including UNH and UNT.
*******************************************************************************/
public EdifactEnvelopeReader(int maxSegments)
{
if(maxSegments < 2)
{
throw new IllegalArgumentException("At least two segments are required");
}
this.maxSegments = maxSegments;
}
/*******************************************************************************
** Read concatenated EDIFACT interchanges without closing the caller stream.
*******************************************************************************/
public List<EdifactInterchange> read(InputStream input) throws IOException
{
List<EdifactInterchange> result = new ArrayList<>();
EnvelopeReader framing = new EnvelopeReader();
byte[] bytes;
while((bytes = framing.next(input)) != null)
{
result.add(parse(bytes));
}
return (result);
}
/*******************************************************************************
** Parse one framed interchange; diagnostics never include submitted values.
*******************************************************************************/
private EdifactInterchange parse(byte[] bytes) throws IOException
{
String prefix = new String(bytes, 0, Math.min(9, bytes.length), StandardCharsets.US_ASCII);
EdifactSeparators separators = prefix.startsWith("UNA") ? EdifactSeparators.fromUna(prefix) : EdifactSeparators.DEFAULT;
List<EnvelopeFinding> errors = new ArrayList<>();
List<EnvelopeFinding> groupErrors = new ArrayList<>();
List<EnvelopeFinding> messageErrors = new ArrayList<>();
List<EdifactGroup> groups = new ArrayList<>();
List<EdifactMessage> messages = new ArrayList<>();
List<EdifactMessage> grouped = new ArrayList<>();
List<X12Segment> segments = new ArrayList<>();
var references = new HashSet<String>();
X12Segment unb = null;
X12Segment unz = null;
X12Segment ung = null;
X12Segment unh = null;
String tag = "";
int count = 0;
List<List<List<String>>> values = new ArrayList<>();
EDIInputFactory factory = EDIInputFactory.newFactory();
factory.setProperty(EDIInputFactory.EDI_IGNORE_EXTRANEOUS_CHARACTERS, true);
factory.setProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_CODE_VALUES, false);
try(EDIStreamReader reader = factory.createEDIStreamReader(new ByteArrayInputStream(bytes)))
{
while(reader.hasNext())
{
EDIStreamEvent event = reader.next();
if(event == EDIStreamEvent.START_SEGMENT)
{
tag = reader.getText();
values = new ArrayList<>();
}
else if(event == EDIStreamEvent.ELEMENT_DATA)
{
int e = reader.getLocation().getElementPosition();
int r = Math.max(1, reader.getLocation().getElementOccurrence());
int c = Math.max(1, reader.getLocation().getComponentPosition());
while(values.size() < e)
{
values.add(new ArrayList<>());
}
while(values.get(e - 1).size() < r)
{
values.get(e - 1).add(new ArrayList<>());
}
var components = values.get(e - 1).get(r - 1);
while(components.size() < c)
{
components.add("");
}
components.set(c - 1, reader.getText());
}
else if(event.isError() && reader.getErrorType() != EDIStreamValidationError.CONTROL_REFERENCE_MISMATCH
&& reader.getErrorType() != EDIStreamValidationError.CONTROL_COUNT_DOES_NOT_MATCH_ACTUAL_COUNT)
{
if(unh != null)
{
messageErrors.add(error("UNH", Math.max(1, count), "12"));
}
else if(ung != null)
{
groupErrors.add(error("UNG", count, "12"));
}
else
{
errors.add(error("UNB", count, "12"));
}
}
else if(event == EDIStreamEvent.END_SEGMENT)
{
if("UNA".equals(tag))
{
continue;
}
count = "UNH".equals(tag) ? 1 : count + 1;
X12Segment segment = new X12Segment(tag, count, values);
switch(tag)
{
case "UNB" ->
{
unb = segment;
if("4".equals(unb.value(1, 1, 2)) && !prefix.startsWith("UNA"))
{
separators = new EdifactSeparators(':', '+', '.', '?', '*', '\'');
}
if(!List.of("3", "4").contains(unb.value(1, 1, 2)) || !List.of("", "1").contains(unb.value(11))
|| unb.value(5).isEmpty() || unb.value(5).length() > 14)
{
errors.add(error("UNB", 1, "12"));
}
}
case "UNG" ->
{
ung = segment;
grouped = new ArrayList<>();
groupErrors = new ArrayList<>();
}
case "UNH" ->
{
unh = segment;
segments = new ArrayList<>();
segments.add(segment);
messageErrors = new ArrayList<>();
if(!references.add(unh.value(1)))
{
messageErrors.add(error("UNH", 1, "19"));
}
}
case "UNT" ->
{
if(unh == null)
{
errors.add(error("UNB", count, "33"));
break;
}
check(messageErrors, "UNH", segment, segment.value(1), String.valueOf(count), "29");
check(messageErrors, "UNH", segment, segment.value(2), unh.value(1), "28");
if(segments.size() < maxSegments)
{
segments.add(segment);
}
(ung == null ? messages : grouped).add(new EdifactMessage(unh, segment, List.copyOf(segments), List.copyOf(messageErrors)));
unh = null;
}
case "UNE" ->
{
if(ung == null)
{
errors.add(error("UNB", count, "33"));
break;
}
check(groupErrors, "UNG", segment, segment.value(1), String.valueOf(grouped.size()), "29");
check(groupErrors, "UNG", segment, segment.value(2), ung.value(5), "28");
groups.add(new EdifactGroup(ung, segment, List.copyOf(grouped), List.copyOf(groupErrors)));
ung = null;
}
case "UNZ" ->
{
unz = segment;
check(errors, "UNB", segment, segment.value(1), String.valueOf(groups.isEmpty() ? messages.size() : groups.size()), "29");
check(errors, "UNB", segment, segment.value(2), unb == null ? "" : unb.value(5), "28");
}
default ->
{
if(unh == null)
{
errors.add(error("UNB", count, "33"));
}
else if(segments.size() < maxSegments - 1)
{
segments.add(segment);
}
else if(messageErrors.stream().noneMatch(f -> "35".equals(f.code())))
{
messageErrors.add(error("UNH", count, "35"));
}
}
}
}
}
}
catch(EDIStreamException e)
{
errors.add(error("UNB", 0, "12"));
}
if(unb == null || unz == null || ung != null || unh != null || !groups.isEmpty() && !messages.isEmpty())
{
errors.add(error("UNB", 0, "13"));
}
return (new EdifactInterchange(separators, unb, unz, List.copyOf(groups), List.copyOf(messages), List.copyOf(errors)));
}
/*******************************************************************************
** Count fields accept zero padding; references retain their exact spelling.
*******************************************************************************/
private static void check(List<EnvelopeFinding> findings, String level, X12Segment segment, String actual, String expected, String code)
{
boolean equal = "29".equals(code) ? actual.matches("[0-9]+") && new java.math.BigInteger(actual).toString().equals(expected) : actual.equals(expected);
if(!equal)
{
findings.add(error(level, segment.position(), code));
}
}
/*******************************************************************************
** Service syntax error code 0085 with a value-free diagnostic.
*******************************************************************************/
private static EnvelopeFinding error(String level, int position, String code)
{
return (new EnvelopeFinding(level, position, code, "EDIFACT envelope failed validation"));
}
}
@@ -0,0 +1,32 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.util.List;
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
import com.kingsrook.qbits.edi.x12.X12Segment;
/*******************************************************************************
** Optional UNG/UNE functional group; ungrouped messages belong to the interchange.
*******************************************************************************/
public record EdifactGroup(X12Segment ung, X12Segment une, List<EdifactMessage> messages, List<EnvelopeFinding> findings)
{
}
@@ -0,0 +1,307 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qbits.edi.EdiRuntime;
import com.kingsrook.qbits.edi.ack.AckLevel;
import com.kingsrook.qbits.edi.flow.EdiJournal;
import com.kingsrook.qbits.edi.flow.InboundProcessor;
import com.kingsrook.qbits.edi.flow.InboxWorker;
import com.kingsrook.qbits.edi.flow.OutboundWorker;
import com.kingsrook.qbits.edi.guide.GuideRepository;
import com.kingsrook.qbits.edi.guide.ValidationFinding;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.storage.PayloadStore;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Native UNB routing and CONTRL with the shared revision, validation and apply checkpoints.
*******************************************************************************/
public class EdifactInbound extends InboundProcessor
{
private final QRecord inbox;
private final Instant lease;
/*******************************************************************************
** Bind every write to the same inbox lease used by the X12 worker.
*******************************************************************************/
public EdifactInbound(QRecord inbox, Instant lease, PayloadStore store)
{
super(inbox, lease, store);
this.inbox = inbox;
this.lease = lease;
}
/*******************************************************************************
** Route UNB identities before validating, acknowledging or applying messages.
*******************************************************************************/
public void process(EdifactInterchange input, int position) throws Exception
{
if(input.unb() == null)
{
InboxWorker.fenced(inbox, lease, () ->
{
EdiJournal.exception(inbox.getValueInteger("id"), null, null, null, "ENVELOPE", "Invalid EDIFACT header");
return (null);
});
return;
}
QRecord agreement = route(input);
QRecord interchange = InboxWorker.fenced(inbox, lease, () -> interchange(input, agreement, position));
if(agreement == null)
{
problem(interchange, "UNKNOWN_PARTNER");
return;
}
if(inbox.getValueString("username") != null && EdiRecords.query("edi_partner", EdiRecords.equal("gatewayUsername", inbox.getValue("username"))).stream()
.anyMatch(p -> !p.getValue("id").equals(agreement.getValue("partnerId"))))
{
problem(interchange, "PARTNER_MISMATCH");
return;
}
if(inbox.getValueLong("sizeBytes") != null && inbox.getValueLong("sizeBytes") > Objects.requireNonNullElse(agreement.getValueLong("maxInterchangeBytes"), 52428800L))
{
problem(interchange, "ENVELOPE");
return;
}
boolean duplicate = "DUPLICATE".equals(interchange.getValueString("status"));
boolean ackInput = !input.allMessages().isEmpty() && input.allMessages().stream().allMatch(m -> "CONTRL".equals(m.type()));
if(duplicate)
{
problem(interchange, "DUPLICATE");
if(!"PROCESS".equals(agreement.getValueString("onDuplicate")))
{
if(!ackInput && "REJECT_TA1".equals(agreement.getValueString("onDuplicate")))
{
var rejected = new EdifactInterchange(input.separators(), input.unb(), input.unz(), input.groups(), input.messages(),
List.of(new com.kingsrook.qbits.edi.x12.EnvelopeFinding("UNB", 1, "19", "Duplicate interchange")));
acknowledge(rejected, agreement, interchange, Map.of(), AckLevel.GROUP_ONLY);
}
return;
}
}
if(!input.findings().isEmpty())
{
problem(interchange, "ENVELOPE");
if(!ackInput)
{
acknowledge(input, agreement, interchange, Map.of(), AckLevel.GROUP_ONLY);
}
return;
}
Map<String, List<ValidationFinding>> findings = new LinkedHashMap<>();
AckLevel level = AckLevel.ERRORS_ONLY;
boolean sendAck = false;
List<EdifactGroup> batches = input.groups().isEmpty() ? List.of(new EdifactGroup(null, null, input.messages(), List.of())) : input.groups();
int groupPosition = 0;
for(var batch : batches)
{
int gp = ++groupPosition;
QRecord group = InboxWorker.fenced(inbox, lease, () ->
{
var prior = EdiRecords.query("edi_functional_group", EdiRecords.equal("interchangeId", interchange.getValue("id")).withCriteria("inboundPosition", QCriteriaOperator.EQUALS, gp));
return (prior.isEmpty() ? SystemWrites.insert("edi_functional_group", new QRecord().withValue("interchangeId", interchange.getValue("id")).withValue("inboundPosition", gp)
.withValue("senderId", input.unb().value(2)).withValue("controlNumber", batch.ung() == null ? input.unb().value(5) : batch.ung().value(5))
.withValue("version", batch.messages().isEmpty() ? null : batch.messages().getFirst().version())
.withValue("functionalIdCode", batch.ung() == null ? null : batch.ung().value(1)).withValue("transactionCount", batch.messages().size()).withValue("ackStatus", "NONE")) : prior.getFirst());
});
int messagePosition = 0;
for(var message : batch.messages())
{
QRecord document = document(agreement, message);
int mp = ++messagePosition;
QRecord configured = document;
QRecord tx = InboxWorker.fenced(inbox, lease, () ->
{
var prior = EdiRecords.query("edi_transaction", EdiRecords.equal("functionalGroupId", group.getValue("id")).withCriteria("inboundPosition", QCriteriaOperator.EQUALS, mp));
return (prior.isEmpty() ? SystemWrites.insert("edi_transaction", new QRecord().withValue("interchangeId", interchange.getValue("id")).withValue("functionalGroupId", group.getValue("id"))
.withValue("inboundPosition", mp).withValue("partnerId", agreement.getValue("partnerId")).withValue("direction", "INBOUND").withValue("transactionSet", message.type())
.withValue("version", message.version()).withValue("controlNumber", message.reference()).withValue("segmentCount", message.segments().size()).withValue("status", "RECEIVED")
.withValue("agreementDocumentId", configured == null ? null : configured.getValue("id"))
.withValue("containsPhi", configured != null && Boolean.TRUE.equals(configured.getValueBoolean("containsPhi")))) : prior.getFirst());
});
document = revisions(document, tx);
List<ValidationFinding> result = new ArrayList<>();
if(document == null || document.getValue("guideId") == null)
{
result.add(finding("No implementation guide is configured"));
problem(interchange, "NOT_CONFIGURED");
}
else
{
result.addAll(EdiRuntime.current().validator().validate(message, new GuideRepository().load(document.getValueInteger("guideId")), Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))));
}
findings.put(message.reference(), List.copyOf(result));
if(!batch.findings().isEmpty() || !message.findings().isEmpty())
{
result.add(finding("Envelope counts or references failed validation"));
}
QRecord effective = document;
InboxWorker.fenced(inbox, lease, () ->
{
validation(tx, result, effective);
return (null);
});
if("CONTRL".equals(message.type()))
{
if(result.isEmpty())
{
InboxWorker.fenced(inbox, lease, () ->
{
new ContrlReader().reconcile(message, agreement, tx);
return (null);
});
}
continue;
}
if(document != null && "CONTRL".equals(document.getValueString("ackExpectation")))
{
sendAck = true;
AckLevel selected = AckLevel.valueOf(Objects.requireNonNullElse(document.getValueString("ackLevel"), "ERRORS_ONLY"));
if(selected == AckLevel.ALL_TRANSACTIONS || level != AckLevel.ALL_TRANSACTIONS && selected == AckLevel.GROUP_ONLY)
{
level = selected;
}
}
if(document != null && result.stream().noneMatch(f -> "ERROR".equals(f.severity()))
&& (!"T".equals(input.usage()) || Boolean.TRUE.equals(agreement.getValueBoolean("applyTestDocuments"))))
{
apply(message, tx, document, agreement);
}
}
}
if(!ackInput && (sendAck || "1".equals(input.unb().value(9))))
{
acknowledge(input, agreement, interchange, findings, level);
}
}
/*******************************************************************************
** Match qualifier, identifier, standard, usage and identity ownership exactly.
*******************************************************************************/
private QRecord route(EdifactInterchange input) throws Exception
{
for(var agreement : EdiRecords.query("edi_agreement", EdiRecords.equal("isActive", true).withCriteria("standard", QCriteriaOperator.EQUALS, "EDIFACT")
.withCriteria("usageIndicator", QCriteriaOperator.EQUALS, input.usage())))
{
QRecord sender = OutboundWorker.row("edi_identity", agreement.getValue("partnerIdentityId"));
QRecord receiver = OutboundWorker.row("edi_identity", agreement.getValue("localIdentityId"));
if(Boolean.TRUE.equals(sender.getValueBoolean("isActive")) && Boolean.TRUE.equals(receiver.getValueBoolean("isActive"))
&& "EDIFACT".equals(sender.getValueString("standard")) && "EDIFACT".equals(receiver.getValueString("standard"))
&& Objects.equals(sender.getValue("partnerId"), agreement.getValue("partnerId")) && receiver.getValue("partnerId") == null
&& input.unb().value(2).equals(sender.getValueString("interchangeId")) && input.unb().value(3).equals(receiver.getValueString("interchangeId"))
&& input.unb().value(2, 1, 2).equals(Objects.requireNonNullElse(sender.getValueString("interchangeQualifier"), ""))
&& input.unb().value(3, 1, 2).equals(Objects.requireNonNullElse(receiver.getValueString("interchangeQualifier"), "")))
{
return (agreement);
}
}
return (null);
}
/*******************************************************************************
** Preserve retries and serialize reference duplicate checks on the agreement.
*******************************************************************************/
private QRecord interchange(EdifactInterchange input, QRecord agreement, int position) throws Exception
{
var prior = EdiRecords.query("edi_interchange", EdiRecords.equal("inboxId", inbox.getValue("id")).withCriteria("inboundPosition", QCriteriaOperator.EQUALS, position));
if(!prior.isEmpty())
{
QRecord existing = prior.getFirst();
if(agreement != null && existing.getValue("agreementId") == null)
{
existing.withValue("agreementId", agreement.getValue("id")).withValue("partnerId", agreement.getValue("partnerId"));
SystemWrites.update("edi_interchange", existing);
}
return (existing);
}
boolean duplicate = false;
if(agreement != null)
{
SystemWrites.guard("UPDATE edi_agreement SET id=id WHERE id=?", agreement.getValue("id"));
duplicate = EdiRuntime.current().duplicates().edifact(agreement.getValueInteger("id"), input, Objects.requireNonNullElse(agreement.getValueInteger("duplicateWindowDays"), 365), EdiRuntime.current().config().getClock().instant());
SystemWrites.update("edi_inbox", new QRecord().withValue("id", inbox.getValue("id")).withValue("partnerId", agreement.getValue("partnerId")));
}
return (SystemWrites.insert("edi_interchange", new QRecord().withValue("inboxId", inbox.getValue("id")).withValue("inboundPosition", position).withValue("direction", "INBOUND")
.withValue("agreementId", agreement == null ? null : agreement.getValue("id")).withValue("partnerId", agreement == null ? null : agreement.getValue("partnerId"))
.withValue("standard", "EDIFACT").withValue("senderId", input.unb().value(2)).withValue("senderQualifier", input.unb().value(2, 1, 2))
.withValue("receiverId", input.unb().value(3)).withValue("receiverQualifier", input.unb().value(3, 1, 2)).withValue("controlNumber", input.unb().value(5))
.withValue("interchangeVersion", input.unb().value(1, 1, 2)).withValue("usageIndicator", input.usage()).withValue("groupCount", input.groups().size())
.withValue("payloadRef", inbox.getValue("payloadRef")).withValue("payloadSha256", inbox.getValue("payloadSha256")).withValue("sizeBytes", inbox.getValue("sizeBytes"))
.withValue("status", duplicate ? "DUPLICATE" : input.findings().isEmpty() ? "ACCEPTED" : "REJECTED")));
}
/*******************************************************************************
** Match message type and directory independently for every UNH.
*******************************************************************************/
private QRecord document(QRecord agreement, EdifactMessage message) throws Exception
{
var rows = EdiRecords.query("edi_agreement_document", EdiRecords.equal("agreementId", agreement.getValue("id")).withCriteria("direction", QCriteriaOperator.EQUALS, "INBOUND")
.withCriteria("transactionSet", QCriteriaOperator.EQUALS, message.type()).withCriteria("version", QCriteriaOperator.EQUALS, message.version()).withCriteria("isActive", QCriteriaOperator.EQUALS, true));
return (rows.isEmpty() ? null : rows.getFirst());
}
/*******************************************************************************
** Queue at most one CONTRL per received interchange, never acknowledge CONTRL.
*******************************************************************************/
private void acknowledge(EdifactInterchange input, QRecord agreement, QRecord received, Map<String, List<ValidationFinding>> findings, AckLevel level) throws Exception
{
InboxWorker.fenced(inbox, lease, () ->
{
if(EdiRecords.query("edi_outbound_message", EdiRecords.equal("ackForInterchangeId", received.getValue("id"))).isEmpty())
{
String version = input.allMessages().isEmpty() ? "D96A" : input.allMessages().getFirst().version();
var message = new ContrlBuilder().build(input, findings, level, EdifactOutbound.allocate(agreement, "UNH"), version);
EdifactOutbound.envelope(agreement, null, null, List.of(), List.of(message), null, received.getValueInteger("id"));
EdiJournal.event(inbox.getValueInteger("id"), received.getValueInteger("id"), null, "ACK_QUEUED");
}
return (null);
});
}
/*******************************************************************************
** Record a safe positional structural failure.
*******************************************************************************/
private ValidationFinding finding(String text)
{
return (new ValidationFinding(1, "UNH", null, null, null, 1, "3", null, "3", null, "ERROR", text, null));
}
/*******************************************************************************
** Journal failures only while holding the current inbox claim.
*******************************************************************************/
private void problem(QRecord interchange, String type) throws Exception
{
InboxWorker.fenced(inbox, lease, () ->
{
EdiJournal.exception(inbox.getValueInteger("id"), interchange.getValueInteger("id"), null, interchange.getValueInteger("partnerId"), type, "EDIFACT processing requires attention: " + type);
return (null);
});
}
}
@@ -0,0 +1,49 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.util.List;
import java.util.stream.Stream;
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
import com.kingsrook.qbits.edi.x12.X12Segment;
/*******************************************************************************
** EDIFACT service envelope with optional groups and ungrouped messages.
*******************************************************************************/
public record EdifactInterchange(EdifactSeparators separators, X12Segment unb, X12Segment unz,
List<EdifactGroup> groups, List<EdifactMessage> messages, List<EnvelopeFinding> findings)
{
/*******************************************************************************
** Visit every message without inventing a functional group on the wire.
*******************************************************************************/
public List<EdifactMessage> allMessages()
{
return (Stream.concat(messages.stream(), groups.stream().flatMap(g -> g.messages().stream())).toList());
}
/*******************************************************************************
** Normalize the UNB test flag to the agreement's usage vocabulary.
*******************************************************************************/
public String usage()
{
return ("1".equals(unb.value(11)) ? "T" : "P");
}
}
@@ -0,0 +1,78 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import com.kingsrook.qbits.edi.map.OutboundMapper;
import com.kingsrook.qbits.edi.x12.X12Segment;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Reuse mapping transforms and associations with native UNH/UNT envelopes.
*******************************************************************************/
public class EdifactMapper extends OutboundMapper
{
private final char decimalMark;
/*******************************************************************************
** Default numeric formatting follows the default UNA decimal mark.
*******************************************************************************/
public EdifactMapper()
{
this('.');
}
/*******************************************************************************
** Use the agreement's decimal service character for outbound numeric transforms.
*******************************************************************************/
public EdifactMapper(char decimalMark)
{
this.decimalMark = decimalMark;
}
/*******************************************************************************
** Render numeric values using the negotiated decimal mark.
*******************************************************************************/
@Override
protected String decimal(String value)
{
return (value.replace('.', decimalMark));
}
/*******************************************************************************
** Build one directory-specific message with an allocated UNH reference.
*******************************************************************************/
public EdifactMessage message(QRecord source, QRecord map, List<QRecord> rules, String reference) throws Exception
{
if(!"OUTBOUND".equals(map.getValueString("direction")))
{
throw new IllegalArgumentException("An outbound map is required");
}
String version = map.getValueString("version");
List<X12Segment> segments = new ArrayList<>();
segments.add(EdifactWriter.segment("UNH", 1, List.of(reference), List.of(map.getValueString("transactionSet"), version.substring(0, 1), version.substring(1), "UN")));
emit(source, rules.stream().sorted(Comparator.comparing(r -> r.getValueInteger("sequence"))).toList(), "", "", 1, "", new int[] {0}, segments);
segments.add(X12Segment.of("UNT", segments.size() + 1, String.valueOf(segments.size() + 1), reference));
return (new EdifactMessage(segments.getFirst(), segments.getLast(), List.copyOf(segments), List.of()));
}
}
@@ -0,0 +1,56 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.util.List;
import com.kingsrook.qbits.edi.document.EdiDocument;
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
import com.kingsrook.qbits.edi.x12.X12Segment;
/*******************************************************************************
** UNH/UNT message with its complete directory and association identification.
*******************************************************************************/
public record EdifactMessage(X12Segment header, X12Segment unt, List<X12Segment> segments, List<EnvelopeFinding> findings) implements EdiDocument
{
/*******************************************************************************
** Agreement document key, for example ORDERS.
*******************************************************************************/
public String type()
{
return (header.value(2));
}
/*******************************************************************************
** Agreement directory key, for example D96A.
*******************************************************************************/
public String version()
{
return (header.value(2, 1, 2) + header.value(2, 1, 3));
}
/*******************************************************************************
** Message reference, unique within its interchange.
*******************************************************************************/
public String reference()
{
return (header.value(1));
}
}
@@ -0,0 +1,168 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qbits.edi.EdiRuntime;
import com.kingsrook.qbits.edi.flow.EdiJournal;
import com.kingsrook.qbits.edi.flow.OutboundWorker;
import com.kingsrook.qbits.edi.guide.RuleEvaluator;
import com.kingsrook.qbits.edi.storage.PayloadKind;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qbits.edi.x12.X12Segment;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** EDIFACT outbound envelopes and durable tracking through the existing send queue.
*******************************************************************************/
public class EdifactOutbound
{
/*******************************************************************************
** Allocate independent UNB/UNH sequences within the configured control scope.
*******************************************************************************/
public static String allocate(QRecord agreement, String level) throws Exception
{
boolean local = "LOCAL_IDENTITY".equals(agreement.getValueString("controlNumberScope"));
return (String.valueOf(EdiRuntime.current().controls().allocate(local ? null : agreement.getValueInteger("id"),
local ? agreement.getValueInteger("localIdentityId") : null, level, 1, 99999999999999L, true)));
}
/*******************************************************************************
** Build an ungrouped interchange; UNZ counts messages when UNG is absent.
*******************************************************************************/
public static EdifactInterchange wrap(QRecord agreement, QRecord sender, QRecord recipient, List<EdifactMessage> messages, String control, LocalDateTime at)
{
String syntax = Objects.requireNonNullElse(agreement.getValueString("interchangeVersion"), "3");
if(!List.of("3", "4").contains(syntax))
{
throw new IllegalArgumentException("EDIFACT syntax version must be 3 or 4");
}
X12Segment unb = EdifactWriter.segment("UNB", 1, List.of(Objects.requireNonNullElse(agreement.getValueString("syntaxIdentifier"), "UNOC"), syntax),
List.of(sender.getValueString("interchangeId"), Objects.requireNonNullElse(sender.getValueString("interchangeQualifier"), "")),
List.of(recipient.getValueString("interchangeId"), Objects.requireNonNullElse(recipient.getValueString("interchangeQualifier"), "")),
List.of(at.format(DateTimeFormatter.ofPattern("4".equals(syntax) ? "yyyyMMdd" : "yyMMdd")), at.format(DateTimeFormatter.ofPattern("HHmm"))),
List.of(control), List.of(""), List.of(""), List.of(""), List.of(""), List.of(""), List.of("T".equals(agreement.getValueString("usageIndicator")) ? "1" : ""));
return (new EdifactInterchange(EdifactSeparators.fromAgreement(agreement), unb, X12Segment.of("UNZ", 0, String.valueOf(messages.size()), control), List.of(), List.copyOf(messages), List.of()));
}
/*******************************************************************************
** Store bytes and record checkpoints atomically with outbound requests or ack fencing.
** A logical tracking group carries ack deadlines even when UNG is absent on the wire.
*******************************************************************************/
public static QRecord envelope(QRecord agreement, QRecord document, QRecord map, List<QRecord> requests, List<EdifactMessage> messages, Integer original, Integer ackFor) throws Exception
{
var runtime = EdiRuntime.current();
var interchange = wrap(agreement, OutboundWorker.row("edi_identity", agreement.getValue("localIdentityId")), OutboundWorker.row("edi_identity", agreement.getValue("partnerIdentityId")),
messages, allocate(agreement, "UNB"), LocalDateTime.ofInstant(runtime.config().getClock().instant(), ZoneOffset.UTC));
String ending = switch(Objects.requireNonNullElse(agreement.getValueString("lineEnding"), "NONE"))
{
case "LF" -> "\n";
case "CRLF" -> "\r\n";
default -> "";
};
var payload = runtime.payloads().put(PayloadKind.OUTBOUND, new ByteArrayInputStream(new EdifactWriter().write(interchange, ending).getBytes(StandardCharsets.UTF_8)));
QRecord persisted = SystemWrites.insert("edi_interchange", new QRecord().withValue("direction", "OUTBOUND").withValue("partnerId", agreement.getValue("partnerId"))
.withValue("agreementId", agreement.getValue("id")).withValue("transportId", agreement.getValue("outboundTransportId")).withValue("standard", "EDIFACT")
.withValue("senderQualifier", interchange.unb().value(2, 1, 2)).withValue("senderId", interchange.unb().value(2))
.withValue("receiverQualifier", interchange.unb().value(3, 1, 2)).withValue("receiverId", interchange.unb().value(3))
.withValue("controlNumber", interchange.unb().value(5)).withValue("interchangeVersion", interchange.unb().value(1, 1, 2)).withValue("usageIndicator", interchange.usage())
.withValue("interchangeDate", runtime.config().getClock().instant()).withValue("groupCount", 0).withValue("payloadRef", payload.value()).withValue("payloadSha256", payload.sha256())
.withValue("sizeBytes", payload.sizeBytes()).withValue("status", "ENVELOPED").withValue("originalInterchangeId", original)
.withValue("containsPhi", document != null && Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))));
boolean expects = document != null && "CONTRL".equals(document.getValueString("ackExpectation"));
QRecord group = SystemWrites.insert("edi_functional_group", new QRecord().withValue("interchangeId", persisted.getValue("id")).withValue("senderId", interchange.unb().value(2))
.withValue("controlNumber", interchange.unb().value(5)).withValue("version", messages.getFirst().version()).withValue("transactionCount", messages.size()).withValue("ackStatus", expects ? "PENDING" : "NONE"));
for(int i = 0; i < messages.size(); i++)
{
var message = messages.get(i);
String key = map == null || map.getValueString("businessKeyPath") == null ? null : new RuleEvaluator(message.segments(), Map.of()).values(map.getValueString("businessKeyPath")).stream().findFirst().orElse(null);
SystemWrites.insert("edi_transaction", new QRecord().withValue("interchangeId", persisted.getValue("id")).withValue("functionalGroupId", group.getValue("id"))
.withValue("direction", "OUTBOUND").withValue("partnerId", agreement.getValue("partnerId")).withValue("agreementDocumentId", document == null ? null : document.getValue("id"))
.withValue("transactionSet", message.type()).withValue("version", message.version()).withValue("controlNumber", message.reference()).withValue("status", "QUEUED")
.withValue("guideId", document == null ? null : document.getValue("guideId"))
.withValue("guideRevision", document == null || document.getValue("guideId") == null ? null : OutboundWorker.row("edi_guide", document.getValue("guideId")).getValue("revision")).withValue("mapId", map == null ? null : map.getValue("id"))
.withValue("mapRevision", map == null ? null : map.getValue("revision"))
.withValue(document != null && Boolean.TRUE.equals(document.getValueBoolean("containsPhi")) ? "phiBusinessKey" : "businessKey", key)
.withValue("businessKeyType", map == null ? null : map.getValue("businessKeyType"))
.withValue("containsPhi", document != null && Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))).withValue("segmentCount", message.segments().size())
.withValue("targetTableName", i < requests.size() ? requests.get(i).getValue("sourceTableName") : null).withValue("targetRecordId", i < requests.size() ? requests.get(i).getValue("sourceRecordId") : null));
if(i < requests.size())
{
SystemWrites.update("edi_outbound_request", new QRecord().withValue("id", requests.get(i).getValue("id")).withValue("status", "ENVELOPED"));
}
}
QRecord queued = SystemWrites.insert("edi_outbound_message", new QRecord().withValue("interchangeId", persisted.getValue("id")).withValue("agreementId", agreement.getValue("id"))
.withValue("partnerId", agreement.getValue("partnerId")).withValue("transportId", agreement.getValue("outboundTransportId")).withValue("status", "QUEUED").withValue("attempts", 0)
.withValue("ackForInterchangeId", ackFor).withValue("payloadRef", payload.value()).withValue("payloadSha256", payload.sha256()).withValue("sizeBytes", payload.sizeBytes()));
SystemWrites.update("edi_interchange", new QRecord().withValue("id", persisted.getValue("id")).withValue("outboundMessageId", queued.getValue("id")));
EdiJournal.event(null, persisted.getValueInteger("id"), null, "ENVELOPED");
return (queued);
}
/*******************************************************************************
** Re-envelope archived outbound messages without reading changed source records.
*******************************************************************************/
public static void resendAsNew(QRecord original) throws Exception
{
QRecord agreement = OutboundWorker.row("edi_agreement", original.getValue("agreementId"));
QRecord oldInterchange = OutboundWorker.row("edi_interchange", original.getValue("interchangeId"));
EdifactInterchange archive;
try(var stream = EdiRuntime.current().payloads().open(new com.kingsrook.qbits.edi.storage.PayloadRef(original.getValueString("payloadRef"), null, 0)))
{
archive = new EdifactEnvelopeReader(EdiRuntime.current().config().getMaxSegmentsPerTransaction()).read(stream).getFirst();
}
List<EdifactMessage> messages = new java.util.ArrayList<>();
for(var message : archive.allMessages())
{
String reference = allocate(agreement, "UNH");
var segments = new java.util.ArrayList<>(message.segments());
var elements = new java.util.ArrayList<>(message.header().elements());
elements.set(0, List.of(List.of(reference)));
segments.set(0, new X12Segment("UNH", 1, elements));
segments.set(segments.size() - 1, X12Segment.of("UNT", segments.size(), String.valueOf(segments.size()), reference));
messages.add(new EdifactMessage(segments.getFirst(), segments.getLast(), List.copyOf(segments), List.of()));
}
var oldTransactions = com.kingsrook.qbits.edi.storage.EdiRecords.query("edi_transaction", com.kingsrook.qbits.edi.storage.EdiRecords.equal("interchangeId", oldInterchange.getValue("id")));
QRecord first = oldTransactions.getFirst();
QRecord document = first.getValue("agreementDocumentId") == null ? null : OutboundWorker.row("edi_agreement_document", first.getValue("agreementDocumentId"));
QRecord queued = envelope(agreement, document, null, List.of(), messages, oldInterchange.getValueInteger("id"), null);
var newTransactions = com.kingsrook.qbits.edi.storage.EdiRecords.query("edi_transaction", com.kingsrook.qbits.edi.storage.EdiRecords.equal("interchangeId", queued.getValue("interchangeId")));
for(int i = 0; i < archive.allMessages().size(); i++)
{
String reference = archive.allMessages().get(i).reference();
QRecord old = oldTransactions.stream().filter(t -> reference.equals(t.getValueString("controlNumber"))).findFirst().orElseThrow();
QRecord update = new QRecord().withValue("id", newTransactions.get(i).getValue("id"));
for(String field : List.of("guideId", "guideRevision", "mapId", "mapRevision", "businessKey", "phiBusinessKey", "containsPhi", "businessKeyType", "secondaryKey", "targetTableName", "targetRecordId", "agreementDocumentId"))
{
update.withValue(field, old.getValue(field));
}
SystemWrites.update("edi_transaction", update);
}
}
}
@@ -0,0 +1,89 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** UNA service characters; the fifth character is reserved in syntax version 3.
*******************************************************************************/
public record EdifactSeparators(char component, char element, char decimal, char release, char repetition, char segment)
{
public static final EdifactSeparators DEFAULT = new EdifactSeparators(':', '+', '.', '?', ' ', '\'');
/*******************************************************************************
** Ambiguous service characters cannot be safely parsed or escaped.
*******************************************************************************/
public EdifactSeparators
{
var values = List.of(component, element, decimal, release, repetition, segment);
if(!List.of('.', ',').contains(decimal) || new HashSet<>(values).size() != 6 || values.stream().anyMatch(c -> c > 127 || Character.isISOControl(c)))
{
throw new IllegalArgumentException("Invalid EDIFACT service characters");
}
}
/*******************************************************************************
** Read exactly six UNA service characters.
*******************************************************************************/
public static EdifactSeparators fromUna(String una)
{
if(una.length() != 9 || !una.startsWith("UNA"))
{
throw new IllegalArgumentException("Invalid UNA");
}
return (new EdifactSeparators(una.charAt(3), una.charAt(4), una.charAt(5), una.charAt(6), una.charAt(7), una.charAt(8)));
}
/*******************************************************************************
** Use the agreement's EDIFACT service characters.
*******************************************************************************/
public static EdifactSeparators fromAgreement(QRecord agreement)
{
return (new EdifactSeparators(character(agreement, "componentSeparator", ":"), character(agreement, "elementSeparator", "+"),
character(agreement, "decimalMark", "."), character(agreement, "releaseCharacter", "?"),
"4".equals(agreement.getValueString("interchangeVersion")) ? character(agreement, "repetitionSeparator", "*") : ' ', character(agreement, "segmentTerminator", "'")));
}
/*******************************************************************************
** A separator setting must contain exactly one character.
*******************************************************************************/
private static char character(QRecord agreement, String field, String fallback)
{
String value = Objects.requireNonNullElse(agreement.getValueString(field), fallback);
if(value.length() != 1)
{
throw new IllegalArgumentException("Invalid service character setting");
}
return (value.charAt(0));
}
/*******************************************************************************
** Render service string advice, including its segment terminator.
*******************************************************************************/
public String una()
{
return ("UNA" + component + element + decimal + release + repetition + segment);
}
}
@@ -0,0 +1,115 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.edifact;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qbits.edi.x12.X12Segment;
/*******************************************************************************
** In-memory EDIFACT serialization; escape values, never composite structure.
*******************************************************************************/
public class EdifactWriter
{
/*******************************************************************************
** Render a full interchange with explicit UNA advice.
*******************************************************************************/
public String write(EdifactInterchange interchange)
{
return (write(interchange, ""));
}
/*******************************************************************************
** Retain optional groups and caller-selected line endings.
*******************************************************************************/
public String write(EdifactInterchange interchange, String ending)
{
List<X12Segment> segments = new ArrayList<>();
segments.add(interchange.unb());
interchange.messages().forEach(m -> segments.addAll(m.segments()));
for(var group : interchange.groups())
{
segments.add(group.ung());
group.messages().forEach(m -> segments.addAll(m.segments()));
segments.add(group.une());
}
segments.add(interchange.unz());
return (interchange.separators().una() + ending + write(segments, interchange.separators(), ending));
}
/*******************************************************************************
** Serialize component and repetition lists without losing released delimiters.
*******************************************************************************/
public String write(List<X12Segment> segments, EdifactSeparators separators, String ending)
{
if(!List.of("", "\n", "\r\n").contains(ending))
{
throw new IllegalArgumentException("Unsupported line ending");
}
StringBuilder out = new StringBuilder();
for(var segment : segments)
{
out.append(segment.id());
for(var element : segment.elements())
{
out.append(separators.element());
for(int r = 0; r < element.size(); r++)
{
if(r > 0)
{
if(separators.repetition() == ' ')
{
throw new IllegalArgumentException("Repetition requires syntax version 4");
}
out.append(separators.repetition());
}
for(int c = 0; c < element.get(r).size(); c++)
{
if(c > 0)
{
out.append(separators.component());
}
for(char value : element.get(r).get(c).toCharArray())
{
if(value == separators.release() || value == separators.element() || value == separators.component()
|| value == separators.segment() || separators.repetition() != ' ' && value == separators.repetition())
{
out.append(separators.release());
}
out.append(value);
}
}
}
}
out.append(separators.segment()).append(ending);
}
return (out.toString());
}
/*******************************************************************************
** Construct a segment from explicit component lists, with no delimiter parsing.
*******************************************************************************/
@SafeVarargs
public static X12Segment segment(String tag, int position, List<String>... elements)
{
return (new X12Segment(tag, position, java.util.Arrays.stream(elements).map(List::of).toList()));
}
}
@@ -315,7 +315,7 @@ public class InboundProcessor
/*******************************************************************************
** Replays select original persisted definitions or the currently active revision.
*******************************************************************************/
private QRecord revisions(QRecord document, QRecord transaction) throws Exception
protected QRecord revisions(QRecord document, QRecord transaction) throws Exception
{
if(document == null)
{
@@ -383,7 +383,7 @@ public class InboundProcessor
/*******************************************************************************
** Persist findings without repeatedly adding error rows on storage retries.
*******************************************************************************/
private void validation(QRecord tx, List<ValidationFinding> findings, QRecord document) throws Exception
protected void validation(QRecord tx, List<ValidationFinding> findings, QRecord document) throws Exception
{
boolean rejected = findings.stream().anyMatch(f -> "ERROR".equals(f.severity()) && f.snipLevel() <= 2);
if(!"APPLIED".equals(tx.getValueString("status")) || Boolean.TRUE.equals(inbox.getValueBoolean("reprocess")))
@@ -421,7 +421,7 @@ public class InboundProcessor
/*******************************************************************************
** A failed target write rolls back only this set, then records an APPLY exception.
*******************************************************************************/
private void apply(X12TransactionSet x12, QRecord tx, QRecord document, QRecord agreement) throws Exception
protected void apply(com.kingsrook.qbits.edi.document.EdiDocument x12, QRecord tx, QRecord document, QRecord agreement) throws Exception
{
if("APPLIED".equals(tx.getValueString("status")) && !Boolean.TRUE.equals(inbox.getValueBoolean("reprocess")))
{
@@ -88,11 +88,22 @@ public class InboxWorker
}
try(InputStream stream = store.open(new PayloadRef(row.getValueString("payloadRef"), row.getValueString("payloadSha256"), row.getValueLong("sizeBytes"))))
{
var interchanges = EdiRuntime.current().reader().read(stream);
var framing = new com.kingsrook.qbits.edi.document.EnvelopeReader();
byte[] envelope;
int position = 0;
for(var interchange : interchanges)
while((envelope = framing.next(stream)) != null)
{
new InboundProcessor(row, lease, store).process(interchange, ++position);
position++;
if(envelope[0] == 'U')
{
var parsed = new com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader(EdiRuntime.current().config().getMaxSegmentsPerTransaction()).read(new java.io.ByteArrayInputStream(envelope)).getFirst();
new com.kingsrook.qbits.edi.edifact.EdifactInbound(row, lease, store).process(parsed, position);
}
else
{
var parsed = EdiRuntime.current().reader().read(new java.io.ByteArrayInputStream(envelope)).getFirst();
new InboundProcessor(row, lease, store).process(parsed, position);
}
}
fenced(row, lease, () ->
{
@@ -73,6 +73,11 @@ public final class OutboundActions
*******************************************************************************/
private static void reEnvelope(QRecord message) throws Exception
{
if("EDIFACT".equals(OutboundWorker.row("edi_agreement", message.getValue("agreementId")).getValueString("standard")))
{
com.kingsrook.qbits.edi.edifact.EdifactOutbound.resendAsNew(message);
return;
}
var runtime = EdiRuntime.current();
QRecord originalRow = OutboundWorker.row("edi_interchange", message.getValue("interchangeId"));
QRecord agreement = OutboundWorker.row("edi_agreement", message.getValue("agreementId"));
@@ -93,7 +93,7 @@ public class OutboundWorker
return (0);
}
List<QRecord> accepted = new ArrayList<>();
List<X12TransactionSet> sets = new ArrayList<>();
List<com.kingsrook.qbits.edi.document.EdiDocument> sets = new ArrayList<>();
QRecord map = row("edi_map", document.getValueInteger("mapId"));
var rules = EdiRecords.query("edi_map_rule", EdiRecords.equal("mapId", map.getValue("id")));
var associations = new java.util.LinkedHashSet<String>();
@@ -117,7 +117,7 @@ public class OutboundWorker
}
try(var logging = com.kingsrook.qbits.edi.security.PhiLogScope.open(Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))))
{
X12TransactionSet set = InboundApplier.asPartner(EdiRuntime.current().config().getServiceSessionSupplier(), agreement.getValueInteger("partnerId"), () ->
com.kingsrook.qbits.edi.document.EdiDocument set = InboundApplier.asPartner(EdiRuntime.current().config().getServiceSessionSupplier(), agreement.getValueInteger("partnerId"), () ->
{
String table = request.getValueString("sourceTableName");
String primaryKey = QContext.getQInstance().getTable(table).getPrimaryKeyField();
@@ -129,7 +129,9 @@ public class OutboundWorker
{
throw new IllegalArgumentException("Source record or active map unavailable");
}
return (new OutboundMapper().build(records.getFirst(), map, rules, String.format("%04d", sets.size() + 1)));
return ("EDIFACT".equals(agreement.getValueString("standard"))
? new com.kingsrook.qbits.edi.edifact.EdifactMapper(com.kingsrook.qbits.edi.edifact.EdifactSeparators.fromAgreement(agreement).decimal()).message(records.getFirst(), map, rules, com.kingsrook.qbits.edi.edifact.EdifactOutbound.allocate(agreement, "UNH"))
: new OutboundMapper().build(records.getFirst(), map, rules, String.format("%04d", sets.size() + 1)));
});
if(EdiRuntime.current().validator().validate(set, guide, Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))).stream().anyMatch(f -> "ERROR".equals(f.severity())))
{
@@ -170,8 +172,13 @@ public class OutboundWorker
/*******************************************************************************
** Allocate controls once, store exact bytes, then persist all tracking records.
*******************************************************************************/
public static QRecord envelope(QRecord agreement, QRecord document, QRecord map, List<QRecord> requests, List<X12TransactionSet> sets, Integer original) throws Exception
public static QRecord envelope(QRecord agreement, QRecord document, QRecord map, List<QRecord> requests, List<? extends com.kingsrook.qbits.edi.document.EdiDocument> sets, Integer original) throws Exception
{
if("EDIFACT".equals(agreement.getValueString("standard")))
{
return (com.kingsrook.qbits.edi.edifact.EdifactOutbound.envelope(agreement, document, map, requests,
sets.stream().map(s -> (com.kingsrook.qbits.edi.edifact.EdifactMessage) s).toList(), original, null));
}
var runtime = EdiRuntime.current();
QRecord local = row("edi_identity", agreement.getValueInteger("localIdentityId"));
QRecord partner = row("edi_identity", agreement.getValueInteger("partnerIdentityId"));
@@ -187,7 +194,7 @@ public class OutboundWorker
X12Segment gs = X12Segment.of("GS", 2, document.getValueString("functionalIdCode"), local.getValueString("applicationCode"), partner.getValueString("applicationCode"), at.format(DateTimeFormatter.BASIC_ISO_DATE),
at.format(DateTimeFormatter.ofPattern("HHmm")), gsControl, "X", document.getValueString("version"));
var interchange = new X12Interchange(new X12Separators(value(agreement, "elementSeparator", "*").charAt(0), value(agreement, "segmentTerminator", "~").charAt(0), component.charAt(0), version.compareTo("00501") >= 0 ? repetition.charAt(0) : null),
isa, X12Segment.of("IEA", 0, "1", isaControl), List.of(new X12FunctionalGroup(gs, X12Segment.of("GE", 0, String.valueOf(sets.size()), gsControl), List.copyOf(sets), List.of())), List.of(), List.of());
isa, X12Segment.of("IEA", 0, "1", isaControl), List.of(new X12FunctionalGroup(gs, X12Segment.of("GE", 0, String.valueOf(sets.size()), gsControl), sets.stream().map(s -> (X12TransactionSet) s).toList(), List.of())), List.of(), List.of());
String ending = switch(value(agreement, "lineEnding", "NONE"))
{
case "LF" -> "\n";
@@ -211,7 +218,7 @@ public class OutboundWorker
String businessKey = map.getValueString("businessKeyPath") == null ? null : new com.kingsrook.qbits.edi.guide.RuleEvaluator(set.segments(), java.util.Map.of()).values(map.getValueString("businessKeyPath")).stream().findFirst().orElse(null);
SystemWrites.insert("edi_transaction", new QRecord().withValue("interchangeId", persisted.getValue("id")).withValue("functionalGroupId", group.getValue("id"))
.withValue("direction", "OUTBOUND").withValue("partnerId", agreement.getValue("partnerId")).withValue("agreementDocumentId", document.getValue("id"))
.withValue("transactionSet", set.st().value(1)).withValue("version", gs.value(8)).withValue("controlNumber", set.st().value(2)).withValue("status", "QUEUED")
.withValue("transactionSet", set.header().value(1)).withValue("version", gs.value(8)).withValue("controlNumber", set.header().value(2)).withValue("status", "QUEUED")
.withValue("guideId", document.getValue("guideId")).withValue("guideRevision", document.getValue("guideId") == null ? null : row("edi_guide", document.getValue("guideId")).getValue("revision"))
.withValue("targetTableName", i < requests.size() ? requests.get(i).getValue("sourceTableName") : null).withValue("targetRecordId", i < requests.size() ? requests.get(i).getValue("sourceRecordId") : null).withValue("mapId", map.getValue("id")).withValue("mapRevision", map.getValue("revision"))
.withValue("businessKeyType", map.getValue("businessKeyType")).withValue(Boolean.TRUE.equals(document.getValueBoolean("containsPhi")) ? "phiBusinessKey" : "businessKey", businessKey)
@@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
*******************************************************************************/
public class BuiltInGuideLoader
{
public static final List<String> RESOURCES = List.of("850-004010", "850-005010", "997-004010", "997-005010", "999-005010X231A1", "855-004010", "855-005010", "810-004010", "810-005010", "846-004010", "846-005010", "860-004010", "860-005010", "856-004010", "856-005010", "940-004010", "940-005010", "945-004010", "945-005010", "214-004010", "214-005010", "204-004010", "204-005010", "837P-005010X222A1", "837I-005010X223A2", "835-005010X221A1", "270-005010X279A1", "271-005010X279A1", "276-005010X212", "277-005010X212", "834-005010X220A1");
public static final List<String> RESOURCES = List.of("850-004010", "850-005010", "997-004010", "997-005010", "999-005010X231A1", "855-004010", "855-005010", "810-004010", "810-005010", "846-004010", "846-005010", "860-004010", "860-005010", "856-004010", "856-005010", "940-004010", "940-005010", "945-004010", "945-005010", "214-004010", "214-005010", "204-004010", "204-005010", "ORDERS-D96A", "ORDRSP-D96A", "DESADV-D96A", "INVOIC-D96A", "CONTRL-D96A", "ORDERS-D01B", "ORDRSP-D01B", "DESADV-D01B", "INVOIC-D01B", "CONTRL-D01B", "837P-005010X222A1", "837I-005010X223A2", "835-005010X221A1", "270-005010X279A1", "271-005010X279A1", "276-005010X212", "277-005010X212", "834-005010X220A1");
/*******************************************************************************
** Read the documented JSON format in memory.
@@ -25,8 +25,8 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.edi.document.EdiDocument;
import com.kingsrook.qbits.edi.x12.X12Segment;
import com.kingsrook.qbits.edi.x12.X12TransactionSet;
/*******************************************************************************
@@ -55,9 +55,9 @@ public class GuideValidator
/*******************************************************************************
** Validate one transaction; diagnostic text never contains submitted values.
*******************************************************************************/
public List<ValidationFinding> validate(X12TransactionSet transaction, Guide guide, boolean containsPhi)
public List<ValidationFinding> validate(EdiDocument transaction, Guide guide, boolean containsPhi)
{
State state = new State(transaction.segments(), containsPhi);
State state = new State(transaction.segments(), containsPhi, "EDIFACT".equals(guide.standard()));
List<Node> roots = tree(guide);
consume(roots, state, false);
while(state.index < state.segments.size())
@@ -86,7 +86,7 @@ public class GuideValidator
}
for(GuideRule rule : list(guide.rules()))
{
rule(rule, transaction.st(), null, evaluator, state);
rule(rule, transaction.header(), null, evaluator, state);
}
for(Binding binding : state.bindings)
{
@@ -109,9 +109,9 @@ public class GuideValidator
/*******************************************************************************
** Resolve guide-defined loop occurrences for the mapper using the validator grammar.
*******************************************************************************/
public Map<String, List<List<X12Segment>>> loops(X12TransactionSet transaction, Guide guide)
public Map<String, List<List<X12Segment>>> loops(EdiDocument transaction, Guide guide)
{
State state = new State(transaction.segments(), true);
State state = new State(transaction.segments(), true, "EDIFACT".equals(guide.standard()));
List<Node> roots = tree(guide);
consume(roots, state, false);
return (state.loops);
@@ -271,7 +271,7 @@ public class GuideValidator
{
state.add(segment, definition.loopId(), element, "8", "5", "Element too long", value);
}
if(!validType(element.dataType(), value))
if(!validType(element.dataType(), state.edifact && "R".equals(element.dataType()) ? value.replace(',', '.') : value))
{
String code = "DT".equals(element.dataType()) ? "8" : "TM".equals(element.dataType()) ? "9" : "6";
state.add(segment, definition.loopId(), element, "8", code, "Invalid element data type", value);
@@ -453,6 +453,7 @@ public class GuideValidator
{
private final List<X12Segment> segments;
private final boolean phi;
private final boolean edifact;
private final List<ValidationFinding> findings = new ArrayList<>();
private final List<Binding> bindings = new ArrayList<>();
private final Map<String, List<List<X12Segment>>> loops = new LinkedHashMap<>();
@@ -461,10 +462,11 @@ public class GuideValidator
/*******************************************************************************
** Initialize a transaction validation.
*******************************************************************************/
private State(List<X12Segment> segments, boolean phi)
private State(List<X12Segment> segments, boolean phi, boolean edifact)
{
this.segments = segments;
this.phi = phi;
this.edifact = edifact;
}
/*******************************************************************************
@@ -28,8 +28,8 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.edi.document.EdiDocument;
import com.kingsrook.qbits.edi.x12.X12Segment;
import com.kingsrook.qbits.edi.x12.X12TransactionSet;
/*******************************************************************************
@@ -40,7 +40,7 @@ public class HealthcareValidation
/*******************************************************************************
** Exact decimal arithmetic; every diagnostic contains only positions and codes.
*******************************************************************************/
public List<ValidationFinding> validate(X12TransactionSet transaction, Guide guide, Map<String, List<List<X12Segment>>> loops)
public List<ValidationFinding> validate(EdiDocument transaction, Guide guide, Map<String, List<List<X12Segment>>> loops)
{
List<ValidationFinding> findings = new ArrayList<>();
List<X12Segment> segments = transaction.segments();
@@ -189,7 +189,7 @@ public class RuleEvaluator
String name = selector.group(1);
String qualifier = selector.group(2);
Matcher element = Pattern.compile("([A-Z][A-Z0-9]{1,2}?)([0-9]{2})(?:-([0-9]+))?").matcher(name);
if(element.matches())
if(element.matches() && !loops.containsKey(name))
{
List<X12Segment> selected = select(scope, element.group(1), qualifier);
int position = Integer.parseInt(element.group(2));
@@ -25,12 +25,12 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.edi.document.EdiDocument;
import com.kingsrook.qbits.edi.guide.Guide;
import com.kingsrook.qbits.edi.guide.GuideValidator;
import com.kingsrook.qbits.edi.guide.RuleEvaluator;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.x12.X12Segment;
import com.kingsrook.qbits.edi.x12.X12TransactionSet;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
@@ -58,7 +58,7 @@ public class InboundMapper
/*******************************************************************************
** Load ordered rules for a persisted map.
*******************************************************************************/
public Result map(X12TransactionSet transaction, Guide guide, QRecord map) throws Exception
public Result map(EdiDocument transaction, Guide guide, QRecord map) throws Exception
{
List<QRecord> rules = new ArrayList<>(EdiRecords.query("edi_map_rule", EdiRecords.equal("mapId", map.getValue("id"))));
rules.sort(Comparator.comparing(r -> r.getValueInteger("sequence")));
@@ -68,7 +68,7 @@ public class InboundMapper
/*******************************************************************************
** Map in memory; wildcard rules sharing an association use the same loop scope.
*******************************************************************************/
public Result map(X12TransactionSet transaction, Guide guide, QRecord map, List<QRecord> rules) throws Exception
public Result map(EdiDocument transaction, Guide guide, QRecord map, List<QRecord> rules) throws Exception
{
Map<String, List<List<X12Segment>>> loops = new GuideValidator().loops(transaction, guide);
RuleEvaluator evaluator = new RuleEvaluator(transaction.segments(), loops);
@@ -86,7 +86,7 @@ public class InboundMapper
}
if(fields.length == 1)
{
apply(rule, evaluator, result, fields[0], path);
apply(rule, evaluator, result, fields[0], path, "EDIFACT".equals(guide.standard()));
}
else
{
@@ -117,7 +117,7 @@ public class InboundMapper
}
for(int i = 0; i < occurrences.size(); i++)
{
apply(rule, new RuleEvaluator(occurrences.get(i), loops), children.get(i), fields[1], relative);
apply(rule, new RuleEvaluator(occurrences.get(i), loops), children.get(i), fields[1], relative, "EDIFACT".equals(guide.standard()));
}
}
}
@@ -132,7 +132,7 @@ public class InboundMapper
/*******************************************************************************
** Conditions and defaults are evaluated within the current loop occurrence.
*******************************************************************************/
private void apply(QRecord rule, RuleEvaluator evaluator, QRecord target, String field, String path) throws Exception
private void apply(QRecord rule, RuleEvaluator evaluator, QRecord target, String field, String path, boolean edifact) throws Exception
{
String condition = rule.getValueString("condition");
if(condition != null && !condition.isBlank() && !evaluator.test(condition))
@@ -144,6 +144,10 @@ public class InboundMapper
{
values = List.of(rule.getValueString("defaultValue"));
}
if(edifact && List.of("DECIMAL", "UNIT").contains(java.util.Objects.requireNonNullElse(rule.getValueString("transform"), "")))
{
values = values.stream().map(v -> v.replace(',', '.')).toList();
}
Serializable value = transforms.apply(rule.getValueString("transform"), rule.getValueString("transformArgs"), values);
if(Boolean.TRUE.equals(rule.getValueBoolean("required")) && (value == null || value.toString().isBlank()))
{
@@ -53,6 +53,10 @@ public class MapPreview
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
try(var logging = com.kingsrook.qbits.edi.security.PhiLogScope.open(phi); InputStream stream = runtime.payloads().open(new PayloadRef(sample.getValueString("payloadRef"), null, 0)))
{
if("EDIFACT".equals(map.getValueString("standard")))
{
return (edifact(map, sample, stream, phi));
}
var interchanges = runtime.reader().read(stream);
lines.add(new ProcessSummaryLine(Status.INFO, interchanges.size(), "interchanges parsed"));
for(var interchange : interchanges)
@@ -115,6 +119,47 @@ public class MapPreview
return (lines);
}
/*******************************************************************************
** Preview native EDIFACT messages and CONTRL without applying target records.
*******************************************************************************/
private ArrayList<ProcessSummaryLineInterface> edifact(QRecord map, QRecord sample, InputStream stream, boolean phi) throws Exception
{
var runtime = EdiRuntime.current();
var guide = new GuideRepository().load(map.getValueInteger("guideId"));
var rules = EdiRecords.query("edi_map_rule", EdiRecords.equal("mapId", map.getValue("id")));
rules.sort(java.util.Comparator.comparing(r -> r.getValueInteger("sequence")));
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
for(var interchange : new com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader(runtime.config().getMaxSegmentsPerTransaction()).read(stream))
{
Map<String, List<com.kingsrook.qbits.edi.guide.ValidationFinding>> findings = new java.util.LinkedHashMap<>();
lines.add(new ProcessSummaryLine(Status.INFO, interchange.findings().size(), "envelope findings"));
for(var message : interchange.allMessages())
{
var result = runtime.validator().validate(message, guide, phi);
findings.put(message.reference(), result);
lines.add(new ProcessSummaryLine(Status.INFO, result.size(), "validation findings"));
var mapped = InboundApplier.asService(runtime.config().getServiceSessionSupplier(), () -> new InboundMapper(runtime.transforms()).map(message, guide, map, rules));
if(phi)
{
lines.add(new ProcessSummaryLine(Status.INFO, "PHI map result suppressed; no target records written"));
continue;
}
String actual = resultJson(mapped.record());
lines.add(new ProcessSummaryLine(Status.INFO, actual));
if(sample.getValueString("expectedResultJson") != null)
{
diff(lines, "Expected result", new ObjectMapper().readTree(sample.getValueString("expectedResultJson")), new ObjectMapper().readTree(actual));
}
}
if(interchange.unb() != null && !phi)
{
var ack = new com.kingsrook.qbits.edi.edifact.ContrlBuilder().build(interchange, findings, AckLevel.ALL_TRANSACTIONS, "1", map.getValueString("version"));
lines.add(new ProcessSummaryLine(Status.INFO, new com.kingsrook.qbits.edi.edifact.EdifactWriter().write(ack.segments(), interchange.separators(), "")));
}
}
return (lines);
}
/*******************************************************************************
** Stable record JSON excludes QRecord diagnostics and implementation metadata.
*******************************************************************************/
@@ -75,7 +75,7 @@ public class OutboundMapper
/*******************************************************************************
** Walk each loop once per associated record, retaining the nearest HL parent.
*******************************************************************************/
private void emit(QRecord source, List<QRecord> rules, String prefix, String qPrefix, int sequence, String parent,
protected void emit(QRecord source, List<QRecord> rules, String prefix, String qPrefix, int sequence, String parent,
int[] hierarchy, List<X12Segment> output) throws Exception
{
Map<String, List<QRecord>> nodes = new LinkedHashMap<>();
@@ -112,13 +112,13 @@ public class OutboundMapper
}
continue;
}
List<String> elements = new ArrayList<>();
List<List<String>> elements = new ArrayList<>();
String segmentId = null;
for(QRecord rule : node.getValue())
{
String path = rule.getValueString("ediPath");
var match = ELEMENT.matcher(path.substring(path.lastIndexOf('/') + 1));
if(!match.matches() || match.group(3) != null)
if(!match.matches())
{
throw new IllegalArgumentException("Unsupported outbound element path");
}
@@ -130,7 +130,7 @@ public class OutboundMapper
}
while(elements.size() < position)
{
elements.add("");
elements.add(new ArrayList<>());
}
String condition = rule.getValueString("condition");
if(condition != null && !condition.isBlank() && !new RuleEvaluator(output, Map.of()).test(condition))
@@ -172,7 +172,7 @@ public class OutboundMapper
}
else if(transform.equals("DECIMAL"))
{
value = new java.math.BigDecimal(raw.toString()).movePointRight(Integer.parseInt(args.getOrDefault("scale", "0"))).stripTrailingZeros().toPlainString();
value = decimal(new java.math.BigDecimal(raw.toString()).movePointRight(Integer.parseInt(args.getOrDefault("scale", "0"))).stripTrailingZeros().toPlainString());
}
else
{
@@ -183,16 +183,35 @@ public class OutboundMapper
{
throw new IllegalArgumentException("Required outbound field is missing");
}
elements.set(position - 1, Objects.requireNonNullElse(value, ""));
int component = match.group(3) == null ? 1 : Integer.parseInt(match.group(3));
if(component < 1 || component > 99)
{
throw new IllegalArgumentException("Invalid outbound component position");
}
var components = elements.get(position - 1);
while(components.size() < component)
{
components.add("");
}
components.set(component - 1, Objects.requireNonNullElse(value, ""));
}
if(elements.stream().anyMatch(v -> !v.isEmpty()))
if(elements.stream().flatMap(List::stream).anyMatch(v -> !v.isEmpty()))
{
output.add(X12Segment.of(segmentId, output.size() + 1, elements.toArray(String[]::new)));
output.add(new X12Segment(segmentId, output.size() + 1, elements.stream().map(e -> List.of(e.isEmpty() ? List.of("") : e)).toList()));
if("HL".equals(segmentId))
{
currentParent = elements.getFirst();
currentParent = elements.getFirst().getFirst();
}
}
}
}
/*******************************************************************************
** X12 uses a period; EDIFACT can override the numeric service character.
*******************************************************************************/
protected String decimal(String value)
{
return (value);
}
}
@@ -281,11 +281,11 @@ public final class EdiTables
sources.add(enumSource("edi_on_duplicate", "REJECT_TA1", "IGNORE", "PROCESS"));
sources.add(enumSource("edi_direction", "INBOUND", "OUTBOUND"));
sources.add(enumSource("edi_transport_direction", "INBOUND", "OUTBOUND", "BOTH"));
sources.add(enumSource("edi_ack_expectation", "NONE", "997", "999"));
sources.add(enumSource("edi_ack_expectation", "NONE", "997", "999", "CONTRL"));
sources.add(enumSource("edi_ack_level", "GROUP_ONLY", "ALL_TRANSACTIONS", "ERRORS_ONLY"));
sources.add(enumSource("edi_batch_mode", "IMMEDIATE", "BATCH"));
sources.add(enumSource("edi_outbound_trigger", "ESB_EVENT", "RECORD_CHANGE", "MANUAL"));
sources.add(enumSource("edi_level", "ISA", "GS"));
sources.add(enumSource("edi_level", "ISA", "GS", "UNB", "UNG", "UNH"));
sources.add(enumSource("edi_transport_type", "AS2", "SFTP", "GATEWAY", "HTTPS", "EMAIL"));
sources.add(enumSource("edi_sign_algorithm", "SHA256", "SHA384", "SHA512", "SHA1"));
sources.add(enumSource("edi_encrypt_algorithm", "AES128_CBC", "AES256_CBC", "AES128_GCM", "AES256_GCM", "3DES"));
@@ -33,6 +33,15 @@ public class EdiAgreement extends QRecordEntity
{
public static final String TABLE_NAME = "edi_agreement";
@QField(backendName = "release_character", maxLength = 255, defaultValue = "?")
private String releaseCharacter;
@QField(backendName = "decimal_mark", maxLength = 255, defaultValue = ".")
private String decimalMark;
@QField(backendName = "syntax_identifier", maxLength = 255, defaultValue = "UNOC")
private String syntaxIdentifier;
@QField(backendName = "id", isPrimaryKey = true, isEditable = false)
private Integer id;
@@ -839,4 +848,79 @@ public class EdiAgreement extends QRecordEntity
return (this);
}
/*******************************************************************************
** Get releaseCharacter.
*******************************************************************************/
public String getReleaseCharacter()
{
return (releaseCharacter);
}
/*******************************************************************************
** Set releaseCharacter.
*******************************************************************************/
public void setReleaseCharacter(String releaseCharacter)
{
this.releaseCharacter = releaseCharacter;
}
/*******************************************************************************
** Set releaseCharacter fluently.
*******************************************************************************/
public EdiAgreement withReleaseCharacter(String releaseCharacter)
{
this.releaseCharacter = releaseCharacter;
return (this);
}
/*******************************************************************************
** Get decimalMark.
*******************************************************************************/
public String getDecimalMark()
{
return (decimalMark);
}
/*******************************************************************************
** Set decimalMark.
*******************************************************************************/
public void setDecimalMark(String decimalMark)
{
this.decimalMark = decimalMark;
}
/*******************************************************************************
** Set decimalMark fluently.
*******************************************************************************/
public EdiAgreement withDecimalMark(String decimalMark)
{
this.decimalMark = decimalMark;
return (this);
}
/*******************************************************************************
** Get syntaxIdentifier.
*******************************************************************************/
public String getSyntaxIdentifier()
{
return (syntaxIdentifier);
}
/*******************************************************************************
** Set syntaxIdentifier.
*******************************************************************************/
public void setSyntaxIdentifier(String syntaxIdentifier)
{
this.syntaxIdentifier = syntaxIdentifier;
}
/*******************************************************************************
** Set syntaxIdentifier fluently.
*******************************************************************************/
public EdiAgreement withSyntaxIdentifier(String syntaxIdentifier)
{
this.syntaxIdentifier = syntaxIdentifier;
return (this);
}
}
@@ -105,7 +105,7 @@ public class HttpsTransport implements EdiTransportPlugin
bytes = stream.readAllBytes();
}
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(transport.getHttpsUrl())).timeout(Duration.ofSeconds(60))
.header("Content-Type", "application/edi-x12").POST(HttpRequest.BodyPublishers.ofByteArray(bytes));
.header("Content-Type", new String(bytes, java.nio.charset.StandardCharsets.US_ASCII).stripLeading().startsWith("UN") ? "application/EDIFACT" : "application/edi-x12").POST(HttpRequest.BodyPublishers.ofByteArray(bytes));
if(transport.getHttpsHeadersJson() != null)
{
Map<String, String> headers = new ObjectMapper().readValue(transport.getHttpsHeadersJson(), new TypeReference<Map<String, String>>() {});
@@ -50,7 +50,8 @@ public class ControlNumberAllocator
*******************************************************************************/
public long allocate(Integer agreementId, Integer localIdentityId, String level, long min, long max, boolean wrap) throws SQLException
{
if((agreementId == null) == (localIdentityId == null) || !List.of("ISA", "GS").contains(level) || min < 1 || max < min || max > 999999999)
long ceiling = List.of("UNB", "UNG", "UNH").contains(level) ? 99999999999999L : 999999999;
if((agreementId == null) == (localIdentityId == null) || !List.of("ISA", "GS", "UNB", "UNG", "UNH").contains(level) || min < 1 || max < min || max > ceiling)
{
throw new IllegalArgumentException("Invalid control number scope or bounds");
}
@@ -100,7 +101,7 @@ public class ControlNumberAllocator
return (min);
}
}
if(minimum < 1 || maximum < minimum || maximum > 999999999 || previous < minimum - 1 || previous > maximum)
if(minimum < 1 || maximum < minimum || maximum > ceiling || previous < minimum - 1 || previous > maximum)
{
throw new IllegalStateException("Stored control number range is invalid");
}
@@ -55,6 +55,18 @@ public class DuplicateDetector
return (!EdiRecords.query("edi_interchange", filter).isEmpty());
}
/*******************************************************************************
** EDIFACT duplicates use complete UNB identities, reference and test indicator.
*******************************************************************************/
public boolean edifact(Integer agreementId, com.kingsrook.qbits.edi.edifact.EdifactInterchange input, int days, Instant now) throws QException
{
return (!EdiRecords.query("edi_interchange", EdiRecords.equal("agreementId", agreementId).withCriteria("direction", QCriteriaOperator.EQUALS, "INBOUND")
.withCriteria("standard", QCriteriaOperator.EQUALS, "EDIFACT").withCriteria("senderId", QCriteriaOperator.EQUALS, input.unb().value(2))
.withCriteria("senderQualifier", QCriteriaOperator.EQUALS, input.unb().value(2, 1, 2)).withCriteria("receiverId", QCriteriaOperator.EQUALS, input.unb().value(3))
.withCriteria("receiverQualifier", QCriteriaOperator.EQUALS, input.unb().value(3, 1, 2)).withCriteria("controlNumber", QCriteriaOperator.EQUALS, input.unb().value(5))
.withCriteria("usageIndicator", QCriteriaOperator.EQUALS, input.usage()).withCriteria("createDate", QCriteriaOperator.GREATER_THAN_OR_EQUALS, now.minus(days, ChronoUnit.DAYS))).isEmpty());
}
/*******************************************************************************
** Check GS06 for a sender in the duplicate window.
*******************************************************************************/
@@ -27,6 +27,15 @@ import java.util.List;
/*******************************************************************************
** Buffered transaction set, including its ST and SE.
*******************************************************************************/
public record X12TransactionSet(X12Segment st, X12Segment se, List<X12Segment> segments, List<EnvelopeFinding> findings)
public record X12TransactionSet(X12Segment st, X12Segment se, List<X12Segment> segments, List<EnvelopeFinding> findings) implements com.kingsrook.qbits.edi.document.EdiDocument
{
/*******************************************************************************
** Expose the header to standard-neutral validators and maps.
*******************************************************************************/
@Override
public X12Segment header()
{
return (st);
}
}
@@ -0,0 +1,474 @@
{
"name": "QQQ baseline CONTRL D01B",
"standard": "EDIFACT",
"version": "D01B",
"transactionSet": "CONTRL",
"functionalIdCode": "CONTRL",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"CONTRL"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"01B"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "UCI",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 4,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 4,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 4,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"4",
"7",
"8"
]
},
{
"position": 5,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
]
},
{
"segmentId": "UCM",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 3
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 4
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"4",
"7",
"8"
]
},
{
"position": 4,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
],
"loopId": "SG1",
"loopMaxRepeat": 999
},
{
"segmentId": "UCS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
],
"loopId": "SG2",
"loopMaxRepeat": 999,
"parentLoopId": "SG1"
},
{
"segmentId": "UCD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 99,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"12",
"13",
"14",
"15"
]
},
{
"position": 2,
"dataType": "N0",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "N0",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
}
],
"loopId": "SG2",
"loopMaxRepeat": 999,
"parentLoopId": "SG1"
},
{
"segmentId": "UCF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 4,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"4",
"7",
"8"
]
}
],
"loopId": "SG3",
"loopMaxRepeat": 999
},
{
"segmentId": "UCM",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 3
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 4
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"4",
"7",
"8"
]
},
{
"position": 4,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
],
"loopId": "SG4",
"loopMaxRepeat": 999,
"parentLoopId": "SG3"
},
{
"segmentId": "UCS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
],
"loopId": "SG5",
"loopMaxRepeat": 999,
"parentLoopId": "SG4"
},
{
"segmentId": "UCD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 99,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"12",
"13",
"14",
"15"
]
},
{
"position": 2,
"dataType": "N0",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "N0",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
}
],
"loopId": "SG5",
"loopMaxRepeat": 999,
"parentLoopId": "SG4"
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,474 @@
{
"name": "QQQ baseline CONTRL D96A",
"standard": "EDIFACT",
"version": "D96A",
"transactionSet": "CONTRL",
"functionalIdCode": "CONTRL",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"CONTRL"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"96A"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "UCI",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 4,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 4,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 4,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"4",
"7",
"8"
]
},
{
"position": 5,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
]
},
{
"segmentId": "UCM",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 3
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 4
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"4",
"7",
"8"
]
},
{
"position": 4,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
],
"loopId": "SG1",
"loopMaxRepeat": 999
},
{
"segmentId": "UCS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
],
"loopId": "SG2",
"loopMaxRepeat": 999,
"parentLoopId": "SG1"
},
{
"segmentId": "UCD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 99,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"12",
"13",
"14",
"15"
]
},
{
"position": 2,
"dataType": "N0",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "N0",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
}
],
"loopId": "SG2",
"loopMaxRepeat": 999,
"parentLoopId": "SG1"
},
{
"segmentId": "UCF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 4,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"4",
"7",
"8"
]
}
],
"loopId": "SG3",
"loopMaxRepeat": 999
},
{
"segmentId": "UCM",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 3
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 4
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"4",
"7",
"8"
]
},
{
"position": 4,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
],
"loopId": "SG4",
"loopMaxRepeat": 999,
"parentLoopId": "SG3"
},
{
"segmentId": "UCS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL"
}
],
"loopId": "SG5",
"loopMaxRepeat": 999,
"parentLoopId": "SG4"
},
{
"segmentId": "UCD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 99,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"12",
"13",
"14",
"15"
]
},
{
"position": 2,
"dataType": "N0",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "N0",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
}
],
"loopId": "SG5",
"loopMaxRepeat": 999,
"parentLoopId": "SG4"
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,410 @@
{
"name": "QQQ baseline DESADV D01B",
"standard": "EDIFACT",
"version": "D01B",
"transactionSet": "DESADV",
"functionalIdCode": "DESADV",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"DESADV"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"01B"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "BGM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"351"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"9",
"29"
]
}
]
},
{
"segmentId": "DTM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 5,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"137",
"11"
]
},
{
"position": 1,
"dataType": "DT",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"102"
]
}
]
},
{
"segmentId": "RFF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ON"
]
},
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG1",
"loopMaxRepeat": 10
},
{
"segmentId": "NAD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"BY",
"SU",
"DP"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 3,
"codes": [
"9"
]
}
],
"loopId": "SG2",
"loopMaxRepeat": 10
},
{
"segmentId": "CPS",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 12,
"usage": "REQUIRED"
}
],
"loopId": "SG10",
"loopMaxRepeat": 999
},
{
"segmentId": "PAC",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED"
}
],
"loopId": "SG10",
"loopMaxRepeat": 999
},
{
"segmentId": "LIN",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"EN",
"SA"
]
}
],
"loopId": "SG17",
"loopMaxRepeat": 9999,
"parentLoopId": "SG10"
},
{
"segmentId": "QTY",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"12"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"PCE"
]
}
],
"loopId": "SG17",
"loopMaxRepeat": 9999,
"parentLoopId": "SG10"
},
{
"segmentId": "UNS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"S"
]
}
]
},
{
"segmentId": "CNT",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"2"
]
},
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,410 @@
{
"name": "QQQ baseline DESADV D96A",
"standard": "EDIFACT",
"version": "D96A",
"transactionSet": "DESADV",
"functionalIdCode": "DESADV",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"DESADV"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"96A"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "BGM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"351"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"9",
"29"
]
}
]
},
{
"segmentId": "DTM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 5,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"137",
"11"
]
},
{
"position": 1,
"dataType": "DT",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"102"
]
}
]
},
{
"segmentId": "RFF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ON"
]
},
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG1",
"loopMaxRepeat": 10
},
{
"segmentId": "NAD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"BY",
"SU",
"DP"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 3,
"codes": [
"9"
]
}
],
"loopId": "SG2",
"loopMaxRepeat": 10
},
{
"segmentId": "CPS",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 12,
"usage": "REQUIRED"
}
],
"loopId": "SG10",
"loopMaxRepeat": 999
},
{
"segmentId": "PAC",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED"
}
],
"loopId": "SG10",
"loopMaxRepeat": 999
},
{
"segmentId": "LIN",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"EN",
"SA"
]
}
],
"loopId": "SG17",
"loopMaxRepeat": 9999,
"parentLoopId": "SG10"
},
{
"segmentId": "QTY",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"12"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"PCE"
]
}
],
"loopId": "SG17",
"loopMaxRepeat": 9999,
"parentLoopId": "SG10"
},
{
"segmentId": "UNS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"S"
]
}
]
},
{
"segmentId": "CNT",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"2"
]
},
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,430 @@
{
"name": "QQQ baseline INVOIC D01B",
"standard": "EDIFACT",
"version": "D01B",
"transactionSet": "INVOIC",
"functionalIdCode": "INVOIC",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"INVOIC"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"01B"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "BGM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"380"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"9",
"29"
]
}
]
},
{
"segmentId": "DTM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 5,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"137",
"11"
]
},
{
"position": 1,
"dataType": "DT",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"102"
]
}
]
},
{
"segmentId": "RFF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ON"
]
},
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG1",
"loopMaxRepeat": 10
},
{
"segmentId": "NAD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"BY",
"SU",
"DP"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 3,
"codes": [
"9"
]
}
],
"loopId": "SG2",
"loopMaxRepeat": 10
},
{
"segmentId": "LIN",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"EN",
"SA"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "QTY",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"47"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"PCE"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "PRI",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"AAA"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "UNS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"S"
]
}
]
},
{
"segmentId": "CNT",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"2"
]
},
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "MOA",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"77"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 18,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,430 @@
{
"name": "QQQ baseline INVOIC D96A",
"standard": "EDIFACT",
"version": "D96A",
"transactionSet": "INVOIC",
"functionalIdCode": "INVOIC",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"INVOIC"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"96A"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "BGM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"380"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"9",
"29"
]
}
]
},
{
"segmentId": "DTM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 5,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"137",
"11"
]
},
{
"position": 1,
"dataType": "DT",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"102"
]
}
]
},
{
"segmentId": "RFF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ON"
]
},
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG1",
"loopMaxRepeat": 10
},
{
"segmentId": "NAD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"BY",
"SU",
"DP"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 3,
"codes": [
"9"
]
}
],
"loopId": "SG2",
"loopMaxRepeat": 10
},
{
"segmentId": "LIN",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"EN",
"SA"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "QTY",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"47"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"PCE"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "PRI",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"AAA"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "UNS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"S"
]
}
]
},
{
"segmentId": "CNT",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"2"
]
},
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "MOA",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"77"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 18,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,403 @@
{
"name": "QQQ baseline ORDERS D01B",
"standard": "EDIFACT",
"version": "D01B",
"transactionSet": "ORDERS",
"functionalIdCode": "ORDERS",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ORDERS"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"01B"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "BGM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"220"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"9",
"29"
]
}
]
},
{
"segmentId": "DTM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 5,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"137",
"11"
]
},
{
"position": 1,
"dataType": "DT",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"102"
]
}
]
},
{
"segmentId": "RFF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ON"
]
},
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG1",
"loopMaxRepeat": 10
},
{
"segmentId": "NAD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"BY",
"SU",
"DP"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 3,
"codes": [
"9"
]
}
],
"loopId": "SG2",
"loopMaxRepeat": 10
},
{
"segmentId": "LIN",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"EN",
"SA"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "QTY",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"21"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"PCE"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "PRI",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"AAA"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "UNS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"S"
]
}
]
},
{
"segmentId": "CNT",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"2"
]
},
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,403 @@
{
"name": "QQQ baseline ORDERS D96A",
"standard": "EDIFACT",
"version": "D96A",
"transactionSet": "ORDERS",
"functionalIdCode": "ORDERS",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ORDERS"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"96A"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "BGM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"220"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"9",
"29"
]
}
]
},
{
"segmentId": "DTM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 5,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"137",
"11"
]
},
{
"position": 1,
"dataType": "DT",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"102"
]
}
]
},
{
"segmentId": "RFF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ON"
]
},
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG1",
"loopMaxRepeat": 10
},
{
"segmentId": "NAD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"BY",
"SU",
"DP"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 3,
"codes": [
"9"
]
}
],
"loopId": "SG2",
"loopMaxRepeat": 10
},
{
"segmentId": "LIN",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"EN",
"SA"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "QTY",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"21"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"PCE"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "PRI",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"AAA"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "UNS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"S"
]
}
]
},
{
"segmentId": "CNT",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"2"
]
},
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,403 @@
{
"name": "QQQ baseline ORDRSP D01B",
"standard": "EDIFACT",
"version": "D01B",
"transactionSet": "ORDRSP",
"functionalIdCode": "ORDRSP",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ORDRSP"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"01B"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "BGM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"231"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"9",
"29"
]
}
]
},
{
"segmentId": "DTM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 5,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"137",
"11"
]
},
{
"position": 1,
"dataType": "DT",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"102"
]
}
]
},
{
"segmentId": "RFF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ON"
]
},
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG1",
"loopMaxRepeat": 10
},
{
"segmentId": "NAD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"BY",
"SU",
"DP"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 3,
"codes": [
"9"
]
}
],
"loopId": "SG2",
"loopMaxRepeat": 10
},
{
"segmentId": "LIN",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"EN",
"SA"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "QTY",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"21"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"PCE"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "PRI",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"AAA"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "UNS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"S"
]
}
]
},
{
"segmentId": "CNT",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"2"
]
},
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,403 @@
{
"name": "QQQ baseline ORDRSP D96A",
"standard": "EDIFACT",
"version": "D96A",
"transactionSet": "ORDRSP",
"functionalIdCode": "ORDRSP",
"segments": [
{
"segmentId": "UNH",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ORDRSP"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"D"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"96A"
]
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 2,
"usage": "REQUIRED",
"componentPosition": 4,
"codes": [
"UN"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 6,
"usage": "SITUATIONAL",
"componentPosition": 5
}
]
},
{
"segmentId": "BGM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"231"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"9",
"29"
]
}
]
},
{
"segmentId": "DTM",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 5,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"137",
"11"
]
},
{
"position": 1,
"dataType": "DT",
"minLength": 1,
"maxLength": 8,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"102"
]
}
]
},
{
"segmentId": "RFF",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"ON"
]
},
{
"position": 1,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG1",
"loopMaxRepeat": 10
},
{
"segmentId": "NAD",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"codes": [
"BY",
"SU",
"DP"
]
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 2
},
{
"position": 2,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "SITUATIONAL",
"componentPosition": 3,
"codes": [
"9"
]
}
],
"loopId": "SG2",
"loopMaxRepeat": 10
},
{
"segmentId": "LIN",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 6,
"usage": "REQUIRED"
},
{
"position": 3,
"dataType": "AN",
"minLength": 1,
"maxLength": 35,
"usage": "REQUIRED",
"componentPosition": 1
},
{
"position": 3,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 2,
"codes": [
"EN",
"SA"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "QTY",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"21"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
},
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 3,
"codes": [
"PCE"
]
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "PRI",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"AAA"
]
},
{
"position": 1,
"dataType": "R",
"minLength": 1,
"maxLength": 15,
"usage": "REQUIRED",
"componentPosition": 2
}
],
"loopId": "SG25",
"loopMaxRepeat": 9999
},
{
"segmentId": "UNS",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 1,
"usage": "REQUIRED",
"codes": [
"S"
]
}
]
},
{
"segmentId": "CNT",
"usage": "SITUATIONAL",
"minUse": 0,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "ID",
"minLength": 1,
"maxLength": 3,
"usage": "REQUIRED",
"componentPosition": 1,
"codes": [
"2"
]
},
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED",
"componentPosition": 2
}
]
},
{
"segmentId": "UNT",
"usage": "REQUIRED",
"minUse": 1,
"maxUse": 1,
"elements": [
{
"position": 1,
"dataType": "N0",
"minLength": 1,
"maxLength": 10,
"usage": "REQUIRED"
},
{
"position": 2,
"dataType": "AN",
"minLength": 1,
"maxLength": 14,
"usage": "REQUIRED"
}
]
}
]
}
@@ -0,0 +1,128 @@
{
"map": {
"name": "DESADV D01B retail sample",
"standard": "EDIFACT",
"version": "D01B",
"transactionSet": "DESADV",
"direction": "OUTBOUND",
"status": "ACTIVE",
"revision": 1,
"tableName": "sample_shipment",
"businessKeyPath": "BGM02",
"businessKeyType": "SHIPMENT"
},
"rules": [
{
"sequence": 1,
"ediPath": "BGM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"351\"}"
},
{
"sequence": 2,
"ediPath": "BGM02",
"qqqPath": "shipmentNumber",
"required": true
},
{
"sequence": 3,
"ediPath": "BGM03",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"9\"}"
},
{
"sequence": 4,
"ediPath": "DTM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"137\"}"
},
{
"sequence": 5,
"ediPath": "DTM01-2",
"qqqPath": "shipDate",
"required": true,
"transform": "DATE"
},
{
"sequence": 6,
"ediPath": "DTM01-3",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"102\"}"
},
{
"sequence": 7,
"ediPath": "SG10/CPS01",
"qqqPath": "cartons.sscc",
"required": true,
"transform": "SEQUENCE"
},
{
"sequence": 8,
"ediPath": "SG10/PAC01",
"qqqPath": "cartons.sscc",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 9,
"ediPath": "SG10/SG17/LIN01",
"qqqPath": "cartons.items.sku",
"required": true,
"transform": "SEQUENCE"
},
{
"sequence": 10,
"ediPath": "SG10/SG17/LIN03-1",
"qqqPath": "cartons.items.sku",
"required": true
},
{
"sequence": 11,
"ediPath": "SG10/SG17/LIN03-2",
"qqqPath": "cartons.items.sku",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"SA\"}"
},
{
"sequence": 12,
"ediPath": "SG10/SG17/QTY01-1",
"qqqPath": "cartons.items.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"12\"}"
},
{
"sequence": 13,
"ediPath": "SG10/SG17/QTY01-2",
"qqqPath": "cartons.items.quantity",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 14,
"ediPath": "SG10/SG17/QTY01-3",
"qqqPath": "cartons.items.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PCE\"}"
},
{
"sequence": 15,
"ediPath": "UNS01",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"S\"}"
}
]
}
@@ -0,0 +1,128 @@
{
"map": {
"name": "DESADV D96A retail sample",
"standard": "EDIFACT",
"version": "D96A",
"transactionSet": "DESADV",
"direction": "OUTBOUND",
"status": "ACTIVE",
"revision": 1,
"tableName": "sample_shipment",
"businessKeyPath": "BGM02",
"businessKeyType": "SHIPMENT"
},
"rules": [
{
"sequence": 1,
"ediPath": "BGM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"351\"}"
},
{
"sequence": 2,
"ediPath": "BGM02",
"qqqPath": "shipmentNumber",
"required": true
},
{
"sequence": 3,
"ediPath": "BGM03",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"9\"}"
},
{
"sequence": 4,
"ediPath": "DTM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"137\"}"
},
{
"sequence": 5,
"ediPath": "DTM01-2",
"qqqPath": "shipDate",
"required": true,
"transform": "DATE"
},
{
"sequence": 6,
"ediPath": "DTM01-3",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"102\"}"
},
{
"sequence": 7,
"ediPath": "SG10/CPS01",
"qqqPath": "cartons.sscc",
"required": true,
"transform": "SEQUENCE"
},
{
"sequence": 8,
"ediPath": "SG10/PAC01",
"qqqPath": "cartons.sscc",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 9,
"ediPath": "SG10/SG17/LIN01",
"qqqPath": "cartons.items.sku",
"required": true,
"transform": "SEQUENCE"
},
{
"sequence": 10,
"ediPath": "SG10/SG17/LIN03-1",
"qqqPath": "cartons.items.sku",
"required": true
},
{
"sequence": 11,
"ediPath": "SG10/SG17/LIN03-2",
"qqqPath": "cartons.items.sku",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"SA\"}"
},
{
"sequence": 12,
"ediPath": "SG10/SG17/QTY01-1",
"qqqPath": "cartons.items.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"12\"}"
},
{
"sequence": 13,
"ediPath": "SG10/SG17/QTY01-2",
"qqqPath": "cartons.items.quantity",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 14,
"ediPath": "SG10/SG17/QTY01-3",
"qqqPath": "cartons.items.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PCE\"}"
},
{
"sequence": 15,
"ediPath": "UNS01",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"S\"}"
}
]
}
@@ -0,0 +1,143 @@
{
"map": {
"name": "INVOIC D01B retail sample",
"standard": "EDIFACT",
"version": "D01B",
"transactionSet": "INVOIC",
"direction": "OUTBOUND",
"status": "ACTIVE",
"revision": 1,
"tableName": "sample_invoice",
"businessKeyPath": "BGM02",
"businessKeyType": "INVOICE"
},
"rules": [
{
"sequence": 1,
"ediPath": "BGM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"380\"}"
},
{
"sequence": 2,
"ediPath": "BGM02",
"qqqPath": "invoiceNumber",
"required": true
},
{
"sequence": 3,
"ediPath": "BGM03",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"9\"}"
},
{
"sequence": 4,
"ediPath": "DTM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"137\"}"
},
{
"sequence": 5,
"ediPath": "DTM01-2",
"qqqPath": "invoiceDate",
"required": true,
"transform": "DATE"
},
{
"sequence": 6,
"ediPath": "DTM01-3",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"102\"}"
},
{
"sequence": 7,
"ediPath": "SG25/LIN01",
"qqqPath": "lines.sku",
"required": true,
"transform": "SEQUENCE"
},
{
"sequence": 8,
"ediPath": "SG25/LIN03-1",
"qqqPath": "lines.sku",
"required": true
},
{
"sequence": 9,
"ediPath": "SG25/LIN03-2",
"qqqPath": "lines.sku",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"SA\"}"
},
{
"sequence": 10,
"ediPath": "SG25/QTY01-1",
"qqqPath": "lines.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"47\"}"
},
{
"sequence": 11,
"ediPath": "SG25/QTY01-2",
"qqqPath": "lines.quantity",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 12,
"ediPath": "SG25/QTY01-3",
"qqqPath": "lines.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PCE\"}"
},
{
"sequence": 13,
"ediPath": "SG25/PRI01-1",
"qqqPath": "lines.unitPrice",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"AAA\"}"
},
{
"sequence": 14,
"ediPath": "SG25/PRI01-2",
"qqqPath": "lines.unitPrice",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 15,
"ediPath": "UNS01",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"S\"}"
},
{
"sequence": 16,
"ediPath": "MOA01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"77\"}"
},
{
"sequence": 17,
"ediPath": "MOA01-2",
"qqqPath": "total",
"required": true,
"transform": "DECIMAL"
}
]
}
@@ -0,0 +1,143 @@
{
"map": {
"name": "INVOIC D96A retail sample",
"standard": "EDIFACT",
"version": "D96A",
"transactionSet": "INVOIC",
"direction": "OUTBOUND",
"status": "ACTIVE",
"revision": 1,
"tableName": "sample_invoice",
"businessKeyPath": "BGM02",
"businessKeyType": "INVOICE"
},
"rules": [
{
"sequence": 1,
"ediPath": "BGM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"380\"}"
},
{
"sequence": 2,
"ediPath": "BGM02",
"qqqPath": "invoiceNumber",
"required": true
},
{
"sequence": 3,
"ediPath": "BGM03",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"9\"}"
},
{
"sequence": 4,
"ediPath": "DTM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"137\"}"
},
{
"sequence": 5,
"ediPath": "DTM01-2",
"qqqPath": "invoiceDate",
"required": true,
"transform": "DATE"
},
{
"sequence": 6,
"ediPath": "DTM01-3",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"102\"}"
},
{
"sequence": 7,
"ediPath": "SG25/LIN01",
"qqqPath": "lines.sku",
"required": true,
"transform": "SEQUENCE"
},
{
"sequence": 8,
"ediPath": "SG25/LIN03-1",
"qqqPath": "lines.sku",
"required": true
},
{
"sequence": 9,
"ediPath": "SG25/LIN03-2",
"qqqPath": "lines.sku",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"SA\"}"
},
{
"sequence": 10,
"ediPath": "SG25/QTY01-1",
"qqqPath": "lines.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"47\"}"
},
{
"sequence": 11,
"ediPath": "SG25/QTY01-2",
"qqqPath": "lines.quantity",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 12,
"ediPath": "SG25/QTY01-3",
"qqqPath": "lines.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PCE\"}"
},
{
"sequence": 13,
"ediPath": "SG25/PRI01-1",
"qqqPath": "lines.unitPrice",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"AAA\"}"
},
{
"sequence": 14,
"ediPath": "SG25/PRI01-2",
"qqqPath": "lines.unitPrice",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 15,
"ediPath": "UNS01",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"S\"}"
},
{
"sequence": 16,
"ediPath": "MOA01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"77\"}"
},
{
"sequence": 17,
"ediPath": "MOA01-2",
"qqqPath": "total",
"required": true,
"transform": "DECIMAL"
}
]
}
@@ -0,0 +1,57 @@
{
"map": {
"name": "ORDERS D01B retail sample",
"standard": "EDIFACT",
"version": "D01B",
"transactionSet": "ORDERS",
"direction": "INBOUND",
"status": "ACTIVE",
"revision": 1,
"tableName": "sample_order",
"businessKeyPath": "BGM02",
"businessKeyType": "PO",
"matchKeyFields": "poNumber"
},
"rules": [
{
"sequence": 1,
"ediPath": "BGM02",
"qqqPath": "poNumber",
"required": true
},
{
"sequence": 2,
"ediPath": "DTM[DTM01-1=137]/DTM01-2",
"qqqPath": "orderDate",
"required": true,
"transform": "DATE"
},
{
"sequence": 3,
"ediPath": "NAD[NAD01=BY]/NAD02-1",
"qqqPath": "buyerId",
"required": false
},
{
"sequence": 4,
"ediPath": "SG25[*]/LIN/LIN03-1",
"qqqPath": "lines.sku",
"required": true
},
{
"sequence": 5,
"ediPath": "SG25[*]/QTY/QTY01-2",
"qqqPath": "lines.quantity",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 6,
"ediPath": "SG25[*]/PRI/PRI01-2",
"qqqPath": "lines.unitPrice",
"required": false,
"transform": "DECIMAL",
"condition": "present(PRI01-2)"
}
]
}
@@ -0,0 +1,57 @@
{
"map": {
"name": "ORDERS D96A retail sample",
"standard": "EDIFACT",
"version": "D96A",
"transactionSet": "ORDERS",
"direction": "INBOUND",
"status": "ACTIVE",
"revision": 1,
"tableName": "sample_order",
"businessKeyPath": "BGM02",
"businessKeyType": "PO",
"matchKeyFields": "poNumber"
},
"rules": [
{
"sequence": 1,
"ediPath": "BGM02",
"qqqPath": "poNumber",
"required": true
},
{
"sequence": 2,
"ediPath": "DTM[DTM01-1=137]/DTM01-2",
"qqqPath": "orderDate",
"required": true,
"transform": "DATE"
},
{
"sequence": 3,
"ediPath": "NAD[NAD01=BY]/NAD02-1",
"qqqPath": "buyerId",
"required": false
},
{
"sequence": 4,
"ediPath": "SG25[*]/LIN/LIN03-1",
"qqqPath": "lines.sku",
"required": true
},
{
"sequence": 5,
"ediPath": "SG25[*]/QTY/QTY01-2",
"qqqPath": "lines.quantity",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 6,
"ediPath": "SG25[*]/PRI/PRI01-2",
"qqqPath": "lines.unitPrice",
"required": false,
"transform": "DECIMAL",
"condition": "present(PRI01-2)"
}
]
}
@@ -0,0 +1,128 @@
{
"map": {
"name": "ORDRSP D01B retail sample",
"standard": "EDIFACT",
"version": "D01B",
"transactionSet": "ORDRSP",
"direction": "OUTBOUND",
"status": "ACTIVE",
"revision": 1,
"tableName": "sample_order",
"businessKeyPath": "BGM02",
"businessKeyType": "PO"
},
"rules": [
{
"sequence": 1,
"ediPath": "BGM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"231\"}"
},
{
"sequence": 2,
"ediPath": "BGM02",
"qqqPath": "poNumber",
"required": true
},
{
"sequence": 3,
"ediPath": "BGM03",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"29\"}"
},
{
"sequence": 4,
"ediPath": "DTM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"137\"}"
},
{
"sequence": 5,
"ediPath": "DTM01-2",
"qqqPath": "orderDate",
"required": true,
"transform": "DATE"
},
{
"sequence": 6,
"ediPath": "DTM01-3",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"102\"}"
},
{
"sequence": 7,
"ediPath": "SG25/LIN01",
"qqqPath": "lines.sku",
"required": true,
"transform": "SEQUENCE"
},
{
"sequence": 8,
"ediPath": "SG25/LIN03-1",
"qqqPath": "lines.sku",
"required": true
},
{
"sequence": 9,
"ediPath": "SG25/LIN03-2",
"qqqPath": "lines.sku",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"SA\"}"
},
{
"sequence": 10,
"ediPath": "SG25/QTY01-1",
"qqqPath": "lines.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"21\"}"
},
{
"sequence": 11,
"ediPath": "SG25/QTY01-2",
"qqqPath": "lines.quantity",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 12,
"ediPath": "SG25/QTY01-3",
"qqqPath": "lines.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PCE\"}"
},
{
"sequence": 13,
"ediPath": "SG25/PRI01-1",
"qqqPath": "lines.unitPrice",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"AAA\"}"
},
{
"sequence": 14,
"ediPath": "SG25/PRI01-2",
"qqqPath": "lines.unitPrice",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 15,
"ediPath": "UNS01",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"S\"}"
}
]
}
@@ -0,0 +1,128 @@
{
"map": {
"name": "ORDRSP D96A retail sample",
"standard": "EDIFACT",
"version": "D96A",
"transactionSet": "ORDRSP",
"direction": "OUTBOUND",
"status": "ACTIVE",
"revision": 1,
"tableName": "sample_order",
"businessKeyPath": "BGM02",
"businessKeyType": "PO"
},
"rules": [
{
"sequence": 1,
"ediPath": "BGM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"231\"}"
},
{
"sequence": 2,
"ediPath": "BGM02",
"qqqPath": "poNumber",
"required": true
},
{
"sequence": 3,
"ediPath": "BGM03",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"29\"}"
},
{
"sequence": 4,
"ediPath": "DTM01-1",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"137\"}"
},
{
"sequence": 5,
"ediPath": "DTM01-2",
"qqqPath": "orderDate",
"required": true,
"transform": "DATE"
},
{
"sequence": 6,
"ediPath": "DTM01-3",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"102\"}"
},
{
"sequence": 7,
"ediPath": "SG25/LIN01",
"qqqPath": "lines.sku",
"required": true,
"transform": "SEQUENCE"
},
{
"sequence": 8,
"ediPath": "SG25/LIN03-1",
"qqqPath": "lines.sku",
"required": true
},
{
"sequence": 9,
"ediPath": "SG25/LIN03-2",
"qqqPath": "lines.sku",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"SA\"}"
},
{
"sequence": 10,
"ediPath": "SG25/QTY01-1",
"qqqPath": "lines.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"21\"}"
},
{
"sequence": 11,
"ediPath": "SG25/QTY01-2",
"qqqPath": "lines.quantity",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 12,
"ediPath": "SG25/QTY01-3",
"qqqPath": "lines.quantity",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PCE\"}"
},
{
"sequence": 13,
"ediPath": "SG25/PRI01-1",
"qqqPath": "lines.unitPrice",
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"AAA\"}"
},
{
"sequence": 14,
"ediPath": "SG25/PRI01-2",
"qqqPath": "lines.unitPrice",
"required": true,
"transform": "DECIMAL"
},
{
"sequence": 15,
"ediPath": "UNS01",
"qqqPath": null,
"required": true,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"S\"}"
}
]
}
@@ -0,0 +1,97 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader;
import com.kingsrook.qbits.edi.edifact.EdifactInterchange;
import com.kingsrook.qbits.edi.flow.InboundPayload;
import com.kingsrook.qbits.edi.flow.InboundSink;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Synthetic Lindqvist retail partner shared by tests and the standalone demo.
*******************************************************************************/
public class EdifactSupport
{
/*******************************************************************************
** Configure both CONTRL directions and the four retail documents.
*******************************************************************************/
public static QRecord install(String version) throws Exception
{
QRecord partner = SystemWrites.insert("edi_partner", new QRecord().withValue("name", "Lindqvist Retail AB").withValue("code", "LINDQVIST")
.withValue("status", "ACTIVE").withValue("contactEmail", "lindqvist@example.invalid").withValue("defaultTimezone", "Europe/Stockholm"));
QRecord local = SystemWrites.insert("edi_identity", new QRecord().withValue("standard", "EDIFACT").withValue("interchangeQualifier", "14").withValue("interchangeId", "QQQTEST"));
QRecord remote = SystemWrites.insert("edi_identity", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("standard", "EDIFACT")
.withValue("interchangeQualifier", "14").withValue("interchangeId", "LINDQVIST"));
QRecord transport = SystemWrites.insert("edi_transport", new QRecord().withValue("name", "lindqvist-outbox").withValue("type", "GATEWAY")
.withValue("direction", "BOTH").withValue("gatewayEndpointName", "lindqvist-outbox").withValue("isActive", true));
QRecord agreement = SystemWrites.insert("edi_agreement", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("localIdentityId", local.getValue("id"))
.withValue("partnerIdentityId", remote.getValue("id")).withValue("standard", "EDIFACT").withValue("usageIndicator", "T").withValue("interchangeVersion", "3")
.withValue("elementSeparator", "+").withValue("componentSeparator", ":").withValue("segmentTerminator", "'").withValue("releaseCharacter", "?")
.withValue("applyTestDocuments", true).withValue("outboundTransportId", transport.getValue("id")));
for(String doc : List.of("ORDERS", "ORDRSP", "DESADV", "INVOIC", "CONTRL"))
{
QRecord map = doc.equals("CONTRL") ? null : RetailSupport.installMap(doc, version);
QRecord guide = EdiRecords.query("edi_guide", EdiRecords.equal("transactionSet", doc).withCriteria("version", QCriteriaOperator.EQUALS, version)).getFirst();
for(String direction : doc.equals("CONTRL") ? List.of("INBOUND", "OUTBOUND") : List.of(doc.equals("ORDERS") ? "INBOUND" : "OUTBOUND"))
{
SystemWrites.insert("edi_agreement_document", new QRecord().withValue("agreementId", agreement.getValue("id")).withValue("direction", direction)
.withValue("transactionSet", doc).withValue("version", version).withValue("guideId", guide.getValue("id")).withValue("mapId", map == null ? null : map.getValue("id"))
.withValue("sourceTableName", map == null ? null : map.getValue("tableName")).withValue("targetTableName", map == null ? null : map.getValue("tableName"))
.withValue("ackExpectation", doc.equals("CONTRL") ? "NONE" : "CONTRL").withValue("ackLevel", "ALL_TRANSACTIONS").withValue("ackDueMinutes", 1)
.withValue("outboundTrigger", "MANUAL").withValue("batchMode", "IMMEDIATE").withValue("isActive", true));
}
}
return (agreement);
}
/*******************************************************************************
** Two synthetic lines, released delimiters and an EANCOM association identifier.
*******************************************************************************/
public static String orders(String reference, String version)
{
return ("UNA:+.? 'UNB+UNOC:3+LINDQVIST:14+QQQTEST:14+261014:1200+" + reference + "++++++1'"
+ "UNH+M1+ORDERS:D:" + version.substring(1) + ":UN:EAN008'BGM+220+TEST-" + reference + "+9'DTM+137:20261014:102'RFF+ON:PO-1'NAD+BY+BUY?+ER::9'"
+ "LIN+1++SKU?:ONE:SA'QTY+21:2:PCE'PRI+AAA:7.5'LIN+2++SKU?':SA'QTY+21:3:PCE'PRI+AAA:2'UNS+S'CNT+2:2'UNT+14+M1'UNZ+1+" + reference + "'");
}
/*******************************************************************************
** Parse one synthetic interchange in memory.
*******************************************************************************/
public static EdifactInterchange read(String text) throws Exception
{
return (new EdifactEnvelopeReader(100000).read(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))).getFirst());
}
/*******************************************************************************
** Exercise the durable receive boundary without a real network transport.
*******************************************************************************/
public static void receive(String text, String external) throws Exception
{
new InboundSink(EdiRuntime.current().payloads()).receive(new InboundPayload("GATEWAY", external, "synthetic.edi", null, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))));
}
}
@@ -0,0 +1,500 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.edi.ack.AckLevel;
import com.kingsrook.qbits.edi.edifact.ContrlBuilder;
import com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader;
import com.kingsrook.qbits.edi.edifact.EdifactInterchange;
import com.kingsrook.qbits.edi.edifact.EdifactMapper;
import com.kingsrook.qbits.edi.edifact.EdifactOutbound;
import com.kingsrook.qbits.edi.edifact.EdifactSeparators;
import com.kingsrook.qbits.edi.edifact.EdifactWriter;
import com.kingsrook.qbits.edi.flow.InboxWorker;
import com.kingsrook.qbits.edi.flow.OutboundEnqueue;
import com.kingsrook.qbits.edi.flow.OutboundWorker;
import com.kingsrook.qbits.edi.guide.BuiltInGuideLoader;
import com.kingsrook.qbits.edi.guide.GuideValidator;
import com.kingsrook.qbits.edi.guide.RuleEvaluator;
import com.kingsrook.qbits.edi.guide.ValidationFinding;
import com.kingsrook.qbits.edi.map.InboundMapper;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.storage.PayloadRef;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
** Native envelope, guide, map and durable pipeline acceptance with synthetic data.
*******************************************************************************/
class EdifactTest
{
/*******************************************************************************
** Restore test context.
*******************************************************************************/
@AfterEach
void clear()
{
QContext.clear();
}
/*******************************************************************************
** Directory baselines validate and mapping retains segment groups and components.
*******************************************************************************/
@ParameterizedTest
@ValueSource(strings = {"D96A", "D01B"})
void orders(String version) throws Exception
{
H2Support.create();
var input = EdifactSupport.read(EdifactSupport.orders("100", version));
assertThat(input.findings()).isEmpty();
var message = input.allMessages().getFirst();
assertThat(message.findings()).isEmpty();
var guide = new BuiltInGuideLoader().resource("ORDERS-" + version);
assertThat(new GuideValidator().validate(message, guide, false)).isEmpty();
var sample = RetailSupport.sample("ORDERS", version);
List<QRecord> rules = new ArrayList<>();
sample.get("rules").forEach(r -> rules.add(RetailSupport.record(r)));
var mapped = new InboundMapper(EdiRuntime.current().transforms()).map(message, guide, RetailSupport.record(sample.get("map")), rules);
assertThat(mapped.record().getValueString("buyerId")).isEqualTo("BUY+ER");
assertThat(mapped.record().getAssociatedRecords().get("lines")).extracting(r -> r.getValueString("sku")).containsExactly("SKU:ONE", "SKU'");
assertThat(new RuleEvaluator(message.segments(), new GuideValidator().loops(message, guide)).values("SG25/LIN/LIN03-1")).containsExactly("SKU:ONE", "SKU'");
assertThat(EdifactSupport.read(new EdifactWriter().write(input))).isEqualTo(input);
}
/*******************************************************************************
** Each interchange can change UNA; released terminators never split the payload.
*******************************************************************************/
@Test
void multiInterchangeAndSyntaxFour() throws Exception
{
var first = EdifactSupport.read(EdifactSupport.orders("100", "D96A"));
var custom = new EdifactInterchange(new EdifactSeparators('>', '|', '.', '!', ' ', '~'), first.unb(), first.unz(), first.groups(), first.messages(), List.of());
String text = new EdifactWriter().write(first) + "\r\n" + new EdifactWriter().write(custom, "\r\n");
var parsed = new EdifactEnvelopeReader(1000).read(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
assertThat(parsed).containsExactly(first, custom);
var four = EdifactSupport.read(EdifactSupport.orders("101", "D01B").replace("UNA:+.? '", "UNA:+.?*'").replace("UNOC:3", "UNOC:4").replace("261014:1200", "20261014:1200"));
assertThat(four.findings()).isEmpty();
assertThat(four.allMessages().getFirst().findings()).isEmpty();
assertThat(EdifactSupport.read(new EdifactWriter().write(four))).isEqualTo(four);
assertThat(EdifactSupport.read(EdifactSupport.orders("100", "D96A").substring(9)).findings()).isEmpty();
}
/*******************************************************************************
** Native group counts, references, message limits and incomplete envelopes fail safely.
*******************************************************************************/
@Test
void findingsAndGroups() throws Exception
{
String grouped = EdifactSupport.orders("100", "D96A").replace("UNH+", "UNG+ORDERS+LINDQVIST+QQQTEST+261014:1200+G1+UN+D:96A'UNH+").replace("UNZ+", "UNE+1+G1'UNZ+");
var valid = EdifactSupport.read(grouped);
assertThat(valid.findings()).isEmpty();
assertThat(valid.groups()).hasSize(1);
assertThat(valid.groups().getFirst().findings()).isEmpty();
assertThat(EdifactSupport.read(new EdifactWriter().write(valid))).isEqualTo(valid);
var groupAck = new ContrlBuilder().build(valid, Map.of(), AckLevel.ALL_TRANSACTIONS, "A1", "D96A");
assertThat(new EdifactWriter().write(groupAck.segments(), EdifactSeparators.DEFAULT, "")).contains("UCF+G1+LINDQVIST+QQQTEST+7'");
assertThat(new GuideValidator().validate(groupAck, new BuiltInGuideLoader().resource("CONTRL-D96A"), false)).isEmpty();
var invalid = EdifactSupport.read(grouped.replace("UNT+14+M1", "UNT+13+M2").replace("UNE+1+G1", "UNE+2+G2").replace("UNZ+1+100", "UNZ+2+101"));
assertThat(invalid.findings()).extracting(EnvelopeFinding::code).contains("29", "28");
assertThat(invalid.groups().getFirst().findings()).extracting(EnvelopeFinding::code).containsExactly("29", "28");
assertThat(invalid.allMessages().getFirst().findings()).extracting(EnvelopeFinding::code).containsExactly("29", "28");
assertThat(EdifactSupport.read(grouped.replace("UNZ+1+100'", "")).findings()).isNotEmpty();
var bounded = new EdifactEnvelopeReader(3).read(new ByteArrayInputStream(grouped.getBytes(StandardCharsets.UTF_8))).getFirst();
assertThat(bounded.allMessages().getFirst().segments()).hasSize(3);
assertThat(bounded.allMessages().getFirst().findings()).extracting(EnvelopeFinding::code).contains("35");
assertThatThrownBy(() -> new EdifactEnvelopeReader(1)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> EdifactSeparators.fromUna("bad")).isInstanceOf(IllegalArgumentException.class);
}
/*******************************************************************************
** Exact CONTRL output for acceptance, rejected data and envelope syntax errors.
*******************************************************************************/
@Test
void acknowledgements() throws Exception
{
var input = EdifactSupport.read(EdifactSupport.orders("100", "D96A"));
var builder = new ContrlBuilder();
var accepted = builder.build(input, Map.of(), AckLevel.ALL_TRANSACTIONS, "A1", "D96A");
assertThat(new EdifactWriter().write(accepted.segments(), EdifactSeparators.DEFAULT, "")).isEqualTo("UNH+A1+CONTRL:D:96A:UN'UCI+100+LINDQVIST:14+QQQTEST:14+7'UCM+M1+ORDERS:D:96A:UN:EAN008+7'UNT+4+A1'");
var error = new ValidationFinding(7, "QTY", "SG25", 1, 2, 2, "8", "6", "8", "6", "ERROR", "Invalid number", null);
var rejected = builder.build(input, Map.of("M1", List.of(error)), AckLevel.ERRORS_ONLY, "A2", "D96A");
assertThat(new EdifactWriter().write(rejected.segments(), EdifactSeparators.DEFAULT, "")).isEqualTo("UNH+A2+CONTRL:D:96A:UN'UCI+100+LINDQVIST:14+QQQTEST:14+7'UCM+M1+ORDERS:D:96A:UN:EAN008+4'UCS+7'UCD+12+1:2'UNT+6+A2'");
var broken = EdifactSupport.read(EdifactSupport.orders("100", "D96A").replace("UNZ+1+100", "UNZ+2+100"));
assertThat(new EdifactWriter().write(builder.build(broken, Map.of(), AckLevel.GROUP_ONLY, "A3", "D96A").segments(), EdifactSeparators.DEFAULT, ""))
.isEqualTo("UNH+A3+CONTRL:D:96A:UN'UCI+100+LINDQVIST:14+QQQTEST:14+4+29'UNT+3+A3'");
for(var ack : List.of(accepted, rejected))
{
assertThat(new GuideValidator().validate(ack, new BuiltInGuideLoader().resource("CONTRL-D96A"), false)).isEmpty();
}
}
/*******************************************************************************
** All outbound retail documents serialize, validate and parse with two line occurrences.
*******************************************************************************/
@ParameterizedTest
@ValueSource(strings = {"D96A", "D01B"})
void outboundRoundTrips(String version) throws Exception
{
H2Support.create();
for(String doc : List.of("ORDRSP", "DESADV", "INVOIC"))
{
var sample = RetailSupport.sample(doc, version);
List<QRecord> rules = new ArrayList<>();
sample.get("rules").forEach(r -> rules.add(RetailSupport.record(r)));
var message = new EdifactMapper().message(RetailSupport.source(), RetailSupport.record(sample.get("map")), rules, "1");
assertThat(new GuideValidator().validate(message, new BuiltInGuideLoader().resource(doc + "-" + version), false)).as(doc).isEmpty();
var envelope = EdifactOutbound.wrap(new QRecord().withValue("usageIndicator", "T"), new QRecord().withValue("interchangeId", "LOCAL").withValue("interchangeQualifier", "14"),
new QRecord().withValue("interchangeId", "REMOTE").withValue("interchangeQualifier", "14"), List.of(message), "100", java.time.LocalDateTime.of(2026, 10, 14, 12, 0));
var parsed = EdifactSupport.read(new EdifactWriter().write(envelope));
assertThat(parsed.findings()).isEmpty();
assertThat(parsed.allMessages().getFirst().findings()).isEmpty();
assertThat(parsed.allMessages().getFirst().segments()).isEqualTo(message.segments());
}
}
/*******************************************************************************
** ORDERS is applied through the durable inbox; repeat controls cannot reapply it.
*******************************************************************************/
@Test
void inboxAndDuplicates() throws Exception
{
setup();
EdifactSupport.receive(EdifactSupport.orders("100", "D96A"), "orders-1");
assertThat(new InboxWorker().runOnce()).isEqualTo(1);
assertThat(rows("edi_inbox")).extracting(r -> r.getValueString("status")).containsExactly("PROCESSED");
assertThat(rows("edi_transaction")).anyMatch(r -> "ORDERS".equals(r.getValueString("transactionSet")) && "APPLIED".equals(r.getValueString("status")));
assertThat(rows("sample_order")).hasSize(1);
assertThat(rows("sample_order_line")).hasSize(2);
assertThat(rows("edi_outbound_message")).hasSize(1);
EdifactSupport.receive(EdifactSupport.orders("100", "D96A").replace("TEST-100", "TEST-OTHER"), "orders-2");
new InboxWorker().runOnce();
assertThat(rows("sample_order")).hasSize(1);
assertThat(rows("edi_exception")).anyMatch(r -> "DUPLICATE".equals(r.getValueString("type")));
}
/*******************************************************************************
** Worker maps persisted retail records, including nested shipment associations.
*******************************************************************************/
@Test
void outboundWorker() throws Exception
{
setup();
SystemWrites.call(() ->
{
for(String table : List.of("sample_order", "sample_shipment", "sample_invoice"))
{
QRecord source = SystemWrites.transaction(() -> RetailSupport.saveSource(table, RetailSupport.source()));
OutboundEnqueue.enqueue(table, source, "MANUAL", null);
}
return (null);
});
assertThat(new OutboundWorker().runOnce()).isEqualTo(3);
assertThat(rows("edi_outbound_message")).hasSize(3);
for(var row : rows("edi_outbound_message"))
{
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(row.getValueString("payloadRef"), null, 0)))
{
var parsed = new EdifactEnvelopeReader(10000).read(stream).getFirst();
assertThat(parsed.findings()).isEmpty();
assertThat(parsed.allMessages().getFirst().findings()).isEmpty();
}
}
}
/*******************************************************************************
** CONTRL resolves deadlines and reconciles accepted and rejected outbound messages.
*******************************************************************************/
@ParameterizedTest
@ValueSource(booleans = {false, true})
void reconcile(boolean reject) throws Exception
{
QRecord agreement = setup();
SystemWrites.call(() ->
{
QRecord source = SystemWrites.transaction(() -> RetailSupport.saveSource("sample_order", RetailSupport.source()));
OutboundEnqueue.enqueue("sample_order", source, "MANUAL", null);
return (null);
});
new OutboundWorker().runOnce();
QRecord outgoing = rows("edi_outbound_message").getFirst();
com.kingsrook.qbits.edi.transport.gateway.standin.FileGatewayOutbox.register((endpoint, name, stream, size) ->
new com.kingsrook.qbits.edi.transport.gateway.standin.OutboxFileRef(1, endpoint, name, name, size, null, java.time.Instant.now()));
try
{
new com.kingsrook.qbits.edi.flow.SendWorker().runOnce();
assertThat(rows("edi_functional_group").getFirst().getValueInstant("ackDueAt")).isNotNull();
EdiRuntime.current().config().withClock(java.time.Clock.fixed(java.time.Instant.now().plusSeconds(120), java.time.ZoneOffset.UTC));
assertThat(new com.kingsrook.qbits.edi.flow.AckMonitor().runOnce()).isEqualTo(1);
EdifactInterchange original;
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(outgoing.getValueString("payloadRef"), null, 0)))
{
original = new EdifactEnvelopeReader(10000).read(stream).getFirst();
}
var message = original.allMessages().getFirst();
var error = new ValidationFinding(2, "BGM", null, 2, null, 2, "8", "1", "8", "1", "ERROR", "Missing number", null);
var ack = new ContrlBuilder().build(original, reject ? Map.of(message.reference(), List.of(error)) : Map.of(), AckLevel.ALL_TRANSACTIONS, "A1", "D96A");
var reply = EdifactOutbound.wrap(agreement, SystemWrites.call(() -> OutboundWorker.row("edi_identity", agreement.getValue("partnerIdentityId"))),
SystemWrites.call(() -> OutboundWorker.row("edi_identity", agreement.getValue("localIdentityId"))), List.of(ack), "ACK100", java.time.LocalDateTime.of(2026, 10, 14, 12, 0));
EdifactSupport.receive(new EdifactWriter().write(reply), "ack-1");
assertThat(new InboxWorker().runOnce()).isEqualTo(1);
assertThat(rows("edi_interchange").stream().filter(r -> r.getValue("id").equals(outgoing.getValue("interchangeId"))).findFirst().orElseThrow().getValueString("status"))
.isEqualTo(reject ? "REJECTED" : "ACKNOWLEDGED");
assertThat(rows("edi_transaction").stream().filter(r -> "OUTBOUND".equals(r.getValueString("direction"))).findFirst().orElseThrow().getValueString("ackStatus")).isEqualTo(reject ? "R" : "A");
assertThat(rows("edi_exception")).anyMatch(r -> "OVERDUE_ACK".equals(r.getValueString("type")) && "RESOLVED".equals(r.getValueString("status")));
assertThat(rows("edi_exception").stream().anyMatch(r -> "REJECTED_ACK".equals(r.getValueString("type")))).isEqualTo(reject);
assertThat(rows("edi_outbound_message")).hasSize(1);
}
finally
{
com.kingsrook.qbits.edi.transport.gateway.standin.FileGatewayOutbox.register(null);
}
}
/*******************************************************************************
** A single payload can contain X12 and EDIFACT and route each independently.
*******************************************************************************/
@Test
void mixedStandards() throws Exception
{
setup();
SystemWrites.call(() ->
{
QRecord partner = SystemWrites.insert("edi_partner", new QRecord().withValue("name", "X12 mixed").withValue("code", "MIXED"));
QRecord local = SystemWrites.insert("edi_identity", new QRecord().withValue("standard", "X12").withValue("interchangeQualifier", "ZZ").withValue("interchangeId", "QQQTEST"));
QRecord remote = SystemWrites.insert("edi_identity", new QRecord().withValue("standard", "X12").withValue("interchangeQualifier", "ZZ").withValue("interchangeId", "SYNTHETIC").withValue("partnerId", partner.getValue("id")));
QRecord agreement = SystemWrites.insert("edi_agreement", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("localIdentityId", local.getValue("id"))
.withValue("partnerIdentityId", remote.getValue("id")).withValue("standard", "X12").withValue("usageIndicator", "T").withValue("applyTestDocuments", true));
QRecord map = RetailSupport.installMap("850", "004010");
SystemWrites.insert("edi_agreement_document", new QRecord().withValue("agreementId", agreement.getValue("id")).withValue("direction", "INBOUND").withValue("transactionSet", "850")
.withValue("version", "004010").withValue("guideId", map.getValue("guideId")).withValue("mapId", map.getValue("id")).withValue("ackExpectation", "NONE"));
return (null);
});
String x12 = Synthetic850.generate(1, "004010", com.kingsrook.qbits.edi.x12.X12Separators.DEFAULT);
EdifactSupport.receive(x12 + "\n" + EdifactSupport.orders("200", "D96A"), "mixed");
assertThat(new InboxWorker().runOnce()).isEqualTo(1);
assertThat(rows("edi_transaction").stream().filter(r -> "APPLIED".equals(r.getValueString("status"))).map(r -> r.getValueString("transactionSet"))).containsExactlyInAnyOrder("850", "ORDERS");
assertThat(rows("sample_order")).hasSize(2);
}
/*******************************************************************************
** All service characters in data survive custom UNA and release escaping.
*******************************************************************************/
@Test
void releasedCharactersAndRepetition() throws Exception
{
var input = EdifactSupport.read(EdifactSupport.orders("100", "D96A"));
var message = input.allMessages().getFirst();
var segments = new ArrayList<>(message.segments());
segments.set(1, com.kingsrook.qbits.edi.x12.X12Segment.of("BGM", 2, "220", "A|B>C!D~E?F", "9"));
var changed = new com.kingsrook.qbits.edi.edifact.EdifactMessage(message.header(), message.unt(), segments, List.of());
var custom = new EdifactInterchange(new EdifactSeparators('>', '|', '.', '!', ' ', '~'), input.unb(), input.unz(), List.of(), List.of(changed), List.of());
String encoded = new EdifactWriter().write(custom);
assertThat(encoded).contains("A!|B!>C!!D!~E?F");
assertThat(EdifactSupport.read(encoded).allMessages().getFirst().segments()).isEqualTo(segments);
var standard = new EdifactInterchange(EdifactSeparators.DEFAULT, input.unb(), input.unz(), List.of(), List.of(changed), List.of());
assertThat(new EdifactWriter().write(standard)).contains("E??F");
String four = EdifactSupport.orders("100", "D01B").replace("UNA:+.? '", "UNA:+.?*'").replace("UNOC:3", "UNOC:4").replace("261014:1200", "20261014:1200").replace("TEST-100", "FIRST*SECOND");
var parsed = EdifactSupport.read(four);
assertThat(parsed.allMessages().getFirst().segments().get(1).value(2, 2, 1)).isEqualTo("SECOND");
assertThat(EdifactSupport.read(new EdifactWriter().write(parsed))).isEqualTo(parsed);
var withoutAdvice = EdifactSupport.read(four.substring(9));
assertThat(withoutAdvice.separators().repetition()).isEqualTo('*');
assertThat(EdifactSupport.read(new EdifactWriter().write(withoutAdvice))).isEqualTo(withoutAdvice);
}
/*******************************************************************************
** Resend retains bytes, while resend-as-new preserves archived content and changes controls.
*******************************************************************************/
@Test
void archivedResendAndPreview() throws Exception
{
setup();
SystemWrites.call(() ->
{
var source = SystemWrites.transaction(() -> RetailSupport.saveSource("sample_order", RetailSupport.source()));
OutboundEnqueue.enqueue("sample_order", source, "MANUAL", null);
return (null);
});
new OutboundWorker().runOnce();
QRecord original = rows("edi_outbound_message").getFirst();
SystemWrites.call(() -> SystemWrites.transaction(() ->
{
com.kingsrook.qbits.edi.flow.OutboundActions.execute("edi_outbound_message", original, "resend");
SystemWrites.update("sample_order", new QRecord().withValue("id", rows("sample_order").getFirst().getValue("id")).withValue("poNumber", "CHANGED-AFTER-SEND"));
com.kingsrook.qbits.edi.flow.OutboundActions.execute("edi_outbound_message", original, "new");
return (null);
}));
assertThat(rows("edi_outbound_message")).hasSize(2);
assertThat(rows("edi_outbound_message").getFirst().getValueString("payloadRef")).isEqualTo(original.getValueString("payloadRef"));
var rebuilt = rows("edi_outbound_message").getLast();
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(rebuilt.getValueString("payloadRef"), null, 0)))
{
var parsed = new EdifactEnvelopeReader(10000).read(stream).getFirst();
assertThat(parsed.allMessages().getFirst().segments().get(1).value(2)).isEqualTo("DEMO-1001");
assertThat(parsed.allMessages().getFirst().reference()).isEqualTo("2");
assertThat(parsed.unb().value(5)).isEqualTo("2");
}
SystemWrites.call(() ->
{
QRecord map = rows("edi_map").stream().filter(r -> "ORDERS".equals(r.getValueString("transactionSet"))).findFirst().orElseThrow();
var payload = EdiRuntime.current().payloads().put(com.kingsrook.qbits.edi.storage.PayloadKind.SAMPLE, new ByteArrayInputStream(EdifactSupport.orders("300", "D96A").getBytes(StandardCharsets.UTF_8)));
var lines = new com.kingsrook.qbits.edi.map.MapPreview().preview(map, new QRecord().withValue("payloadRef", payload.value()));
assertThat(lines).isNotEmpty();
assertThat(rows("sample_order")).hasSize(1);
return (null);
});
}
/*******************************************************************************
** UN control levels allow fourteen digits without relaxing X12 bounds.
*******************************************************************************/
@Test
void controlLevels() throws Exception
{
QRecord agreement = setup();
var allocator = EdiRuntime.current().controls();
assertThat(allocator.allocate(agreement.getValueInteger("id"), null, "UNB", 99999999999998L, 99999999999999L, false)).isEqualTo(99999999999998L);
assertThat(allocator.allocate(agreement.getValueInteger("id"), null, "UNB", 1, 99999999999999L, false)).isEqualTo(99999999999999L);
assertThatThrownBy(() -> allocator.allocate(agreement.getValueInteger("id"), null, "UNB", 1, 99999999999999L, false)).isInstanceOf(IllegalStateException.class);
assertThat(allocator.allocate(agreement.getValueInteger("id"), null, "UNH", 1, 99999999999999L, true)).isEqualTo(1);
assertThatThrownBy(() -> allocator.allocate(agreement.getValueInteger("id"), null, "ISA", 1, 99999999999999L, true)).isInstanceOf(IllegalArgumentException.class);
}
/*******************************************************************************
** Decimal commas follow UNA in inbound mapping and outbound numeric serialization.
*******************************************************************************/
@Test
void decimalComma() throws Exception
{
H2Support.create();
var input = EdifactSupport.read(EdifactSupport.orders("400", "D96A").replace("UNA:+.? '", "UNA:+,? '").replace("AAA:7.5", "AAA:7,5"));
var guide = new BuiltInGuideLoader().resource("ORDERS-D96A");
var message = input.allMessages().getFirst();
assertThat(new GuideValidator().validate(message, guide, false)).isEmpty();
var sample = RetailSupport.sample("ORDERS", "D96A");
List<QRecord> rules = new ArrayList<>();
sample.get("rules").forEach(r -> rules.add(RetailSupport.record(r)));
var result = new InboundMapper(EdiRuntime.current().transforms()).map(message, guide, RetailSupport.record(sample.get("map")), rules);
assertThat(result.record().getAssociatedRecords().get("lines").getFirst().getValueBigDecimal("unitPrice")).isEqualByComparingTo("7.5");
var outbound = RetailSupport.sample("INVOIC", "D96A");
rules.clear();
outbound.get("rules").forEach(r -> rules.add(RetailSupport.record(r)));
var invoice = new EdifactMapper(',').message(RetailSupport.source(), RetailSupport.record(outbound.get("map")), rules, "1");
assertThat(new EdifactWriter().write(invoice.segments(), new EdifactSeparators(':', '+', ',', '?', ' ', '\''), "")).contains("PRI+AAA:7,5'");
}
/*******************************************************************************
** Invalid data and message counts are rejected; unknown senders cannot trigger replies.
*******************************************************************************/
@Test
void rejectsInvalidAndUnknownDocuments() throws Exception
{
setup();
EdifactSupport.receive(EdifactSupport.orders("500", "D96A").replace("QTY+21:2:PCE", "QTY+21:BAD:PCE"), "bad-value");
EdifactSupport.receive(EdifactSupport.orders("501", "D96A").replace("UNT+14", "UNT+13"), "bad-count");
EdifactSupport.receive(EdifactSupport.orders("502", "D96A").replace("LINDQVIST:14", "UNKNOWN:14"), "unknown");
assertThat(new InboxWorker().runOnce()).isEqualTo(3);
assertThat(rows("sample_order")).isEmpty();
assertThat(rows("edi_transaction").stream().filter(r -> "ORDERS".equals(r.getValueString("transactionSet")))).allMatch(r -> "INVALID".equals(r.getValueString("status")));
assertThat(rows("edi_outbound_message")).hasSize(2);
assertThat(rows("edi_exception")).anyMatch(r -> "UNKNOWN_PARTNER".equals(r.getValueString("type")));
for(var row : rows("edi_outbound_message"))
{
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(row.getValueString("payloadRef"), null, 0)))
{
String ack = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
assertThat(ack).contains("+4").doesNotContain("BAD");
if(ack.contains("UCI+501+"))
{
assertThat(ack).contains("UCM+M1+ORDERS:D:96A:UN:EAN008+4+29'").doesNotContain("UCS+");
}
}
}
}
/*******************************************************************************
** Invalid CONTRL references roll back reconciliation; receipt-only responses leave it pending.
*******************************************************************************/
@Test
void invalidAcknowledgements() throws Exception
{
QRecord agreement = setup();
SystemWrites.call(() ->
{
var source = SystemWrites.transaction(() -> RetailSupport.saveSource("sample_order", RetailSupport.source()));
OutboundEnqueue.enqueue("sample_order", source, "MANUAL", null);
return (null);
});
new OutboundWorker().runOnce();
QRecord sent = rows("edi_outbound_message").getFirst();
String ackText = "UNA:+.? 'UNB+UNOC:3+LINDQVIST:14+QQQTEST:14+261014:1200+ACK1++++++1'UNH+A1+CONTRL:D:96A:UN'UCI+1+QQQTEST:14+LINDQVIST:14+7'UCM+BOGUS+ORDRSP:D:96A:UN+7'UNT+4+A1'UNZ+1+ACK1'";
var invalid = EdifactSupport.read(ackText).allMessages().getFirst();
QRecord received = new QRecord().withValue("id", rows("edi_transaction").getFirst().getValue("id"));
assertThatThrownBy(() -> SystemWrites.call(() -> SystemWrites.transaction(() ->
{
new com.kingsrook.qbits.edi.edifact.ContrlReader().reconcile(invalid, agreement, received);
return (null);
}))).isNotNull();
assertThat(rows("edi_functional_group").getFirst().getValueString("ackStatus")).isEqualTo("PENDING");
assertThat(rows("edi_transaction").getFirst().getValueString("status")).isEqualTo("QUEUED");
var receipt = EdifactSupport.read(ackText.replace("LINDQVIST:14+7", "LINDQVIST:14+8")).allMessages().getFirst();
SystemWrites.call(() ->
{
new com.kingsrook.qbits.edi.edifact.ContrlReader().reconcile(receipt, agreement, received);
return (null);
});
assertThat(rows("edi_interchange").getFirst().getValueString("status")).isEqualTo("ENVELOPED");
assertThat(rows("edi_outbound_message").getFirst().getValueString("payloadRef")).isEqualTo(sent.getValueString("payloadRef"));
}
/*******************************************************************************
** Build actual QQQ tables and install the same definitions used in the demo.
*******************************************************************************/
private QRecord setup() throws Exception
{
H2Support.create();
RetailSupport.tables();
EdiRuntime.current().initialize();
return (SystemWrites.call(() -> EdifactSupport.install("D96A")));
}
/*******************************************************************************
** Read test evidence with the service boundary.
*******************************************************************************/
private List<QRecord> rows(String table) throws Exception
{
return (SystemWrites.call(() -> EdiRecords.query(table, new QQueryFilter())));
}
}
@@ -58,7 +58,7 @@ public final class RetailSupport
{
var table = new QTableMetaData().withName(name).withLabel(name).withBackendName(backend).withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withBackendName("id"));
for(String field : List.of("poNumber", "sku", "shipmentNumber", "shipTime", "sscc", "invoiceNumber", "reportNumber"))
for(String field : List.of("buyerId", "poNumber", "sku", "shipmentNumber", "shipTime", "sscc", "invoiceNumber", "reportNumber"))
{
table.withField(new QFieldMetaData(field, QFieldType.STRING).withBackendName(com.kingsrook.qqq.backend.core.instances.QInstanceEnricher.inferBackendName(field)));
}
@@ -247,10 +247,50 @@ public class EdiDemoServer extends AbstractQQQApplication implements AutoCloseab
logistics(local, agreements.get(1));
seedAs2(local);
healthcare();
edifact();
return (null);
});
}
/*******************************************************************************
** Synthetic Swedish retailer exchanges ORDERS, three responses and CONTRL.
*******************************************************************************/
private void edifact() throws Exception
{
var agreement = com.kingsrook.qbits.edi.EdifactSupport.install("D96A");
for(int i = 0; i < 3; i++)
{
com.kingsrook.qbits.edi.EdifactSupport.receive(com.kingsrook.qbits.edi.EdifactSupport.orders("L" + i, "D96A"), "lindqvist-orders-" + i);
}
new InboxWorker().runOnce();
for(String table : List.of("sample_order", "sample_shipment", "sample_invoice"))
{
enqueue(table, RetailSupport.source().withValue("poNumber", "LINDQVIST-DEMO"));
}
new OutboundWorker().runOnce();
new SendWorker().runOnce();
var messages = EdiRecords.query("edi_outbound_message", EdiRecords.equal("agreementId", agreement.getValue("id")));
for(var message : messages)
{
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(message.getValueString("payloadRef"), null, 0)))
{
var original = new com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader(10000).read(stream).getFirst();
if(original.allMessages().stream().anyMatch(m -> "CONTRL".equals(m.type())))
{
continue;
}
var ack = new com.kingsrook.qbits.edi.edifact.ContrlBuilder().build(original, Map.of(), com.kingsrook.qbits.edi.ack.AckLevel.ALL_TRANSACTIONS, "ACK1", "D96A");
var reply = com.kingsrook.qbits.edi.edifact.EdifactOutbound.wrap(agreement,
OutboundWorker.row("edi_identity", agreement.getValue("partnerIdentityId")), OutboundWorker.row("edi_identity", agreement.getValue("localIdentityId")), List.of(ack),
"REPLY" + message.getValue("id"), java.time.LocalDateTime.now(ZoneOffset.UTC));
receive(new com.kingsrook.qbits.edi.edifact.EdifactWriter().write(reply), "lindqvist-contrl-" + message.getValue("id"));
}
}
new InboxWorker().runOnce();
}
/*******************************************************************************
** Synthetic clearinghouse claims, remittance and batch eligibility exchange.
*******************************************************************************/
@@ -86,7 +86,7 @@ class EdiDemoServerTest
demo.seed();
demo.seed();
assertThat(demo.defineQInstance().getHomeAppName()).isEqualTo("edi");
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_partner", new QQueryFilter()))).hasSize(6);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_partner", new QQueryFilter()))).hasSize(7);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_certificate", new QQueryFilter()))).hasSize(5);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_inbox", new QQueryFilter()))).hasSizeGreaterThanOrEqualTo(40);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_transaction", new QQueryFilter()))).anyMatch(row -> "OUTBOUND".equals(row.getValueString("direction")));
@@ -94,11 +94,16 @@ class EdiDemoServerTest
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_functional_group", new QQueryFilter()))).anyMatch(row -> row.getValueInstant("ackReceivedAt") != null);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_exception", new QQueryFilter()))).anyMatch(row -> "OVERDUE_ACK".equals(row.getValueString("type")));
var partners = SystemWrites.call(() -> EdiRecords.query("edi_partner", new QQueryFilter()));
assertThat(partners).extracting(row -> row.getValueString("code")).containsExactlyInAnyOrder("NORTHWIND", "HARBOR", "SUMMIT", "RIDGELINE", "PACIFIC", "CEDAR");
assertThat(partners).extracting(row -> row.getValueString("code")).containsExactlyInAnyOrder("NORTHWIND", "HARBOR", "SUMMIT", "RIDGELINE", "LINDQVIST", "PACIFIC", "CEDAR");
assertThat(partners).allSatisfy(row -> assertThat(row.getValueString("contactEmail")).endsWith("@example.invalid"));
assertThat(SystemWrites.call(() -> EdiRecords.query("sample_wms_order", new QQueryFilter()))).hasSize(3);
assertThat(SystemWrites.call(() -> EdiRecords.query("sample_shipment_status", new QQueryFilter()))).hasSize(3);
var transactions = SystemWrites.call(() -> EdiRecords.query("edi_transaction", new QQueryFilter()));
assertThat(transactions).anyMatch(row -> "ORDERS".equals(row.getValueString("transactionSet")) && "APPLIED".equals(row.getValueString("status")));
for(String document : java.util.List.of("ORDRSP", "DESADV", "INVOIC"))
{
assertThat(transactions).anyMatch(row -> document.equals(row.getValueString("transactionSet")) && "ACKNOWLEDGED".equals(row.getValueString("status")));
}
assertThat(transactions).anyMatch(row -> "945".equals(row.getValueString("transactionSet")) && "SENT".equals(row.getValueString("status")));
assertThat(transactions).anyMatch(row -> "204".equals(row.getValueString("transactionSet")) && "SENT".equals(row.getValueString("status")));
assertThat(transactions).anyMatch(row -> "940".equals(row.getValueString("transactionSet")) && "INVALID".equals(row.getValueString("status")));