diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a21a1..dcd0b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 6ba91bf..9c835d3 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/DEMO.md b/docs/DEMO.md index 40e292f..0141147 100644 --- a/docs/DEMO.md +++ b/docs/DEMO.md @@ -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. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index ccff243..d1b2447 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -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. diff --git a/docs/GUIDE-FORMAT.md b/docs/GUIDE-FORMAT.md index 47213a4..09b7db7 100644 --- a/docs/GUIDE-FORMAT.md +++ b/docs/GUIDE-FORMAT.md @@ -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. diff --git a/docs/MAPS.md b/docs/MAPS.md index 365bd6e..bf5579d 100644 --- a/docs/MAPS.md +++ b/docs/MAPS.md @@ -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--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. diff --git a/docs/ddl/edi-postgres.sql b/docs/ddl/edi-postgres.sql index 744b721..43ab29a 100644 --- a/docs/ddl/edi-postgres.sql +++ b/docs/ddl/edi-postgres.sql @@ -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) ); diff --git a/src/main/java/com/kingsrook/qbits/edi/document/EdiDocument.java b/src/main/java/com/kingsrook/qbits/edi/document/EdiDocument.java new file mode 100644 index 0000000..59fc58e --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/document/EdiDocument.java @@ -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 segments(); + + /******************************************************************************* + ** Return envelope diagnostics without submitted values. + *******************************************************************************/ + List findings(); +} diff --git a/src/main/java/com/kingsrook/qbits/edi/document/EnvelopeReader.java b/src/main/java/com/kingsrook/qbits/edi/document/EnvelopeReader.java new file mode 100644 index 0000000..0314f8f --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/document/EnvelopeReader.java @@ -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()); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/ContrlBuilder.java b/src/main/java/com/kingsrook/qbits/edi/edifact/ContrlBuilder.java new file mode 100644 index 0000000..8a094aa --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/ContrlBuilder.java @@ -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> findings, AckLevel level, String reference, String version) + { + List 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>(); + 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 messages, Map> findings, AckLevel level, List 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>>(); + 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 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> 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"; + }); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/ContrlReader.java b/src/main/java/com/kingsrook/qbits/edi/edifact/ContrlReader.java new file mode 100644 index 0000000..37220d7 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/ContrlReader.java @@ -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 details = new LinkedHashMap<>(); + Map 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"); + }); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactEnvelopeReader.java b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactEnvelopeReader.java new file mode 100644 index 0000000..48a9b58 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactEnvelopeReader.java @@ -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 read(InputStream input) throws IOException + { + List 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 errors = new ArrayList<>(); + List groupErrors = new ArrayList<>(); + List messageErrors = new ArrayList<>(); + List groups = new ArrayList<>(); + List messages = new ArrayList<>(); + List grouped = new ArrayList<>(); + List segments = new ArrayList<>(); + var references = new HashSet(); + X12Segment unb = null; + X12Segment unz = null; + X12Segment ung = null; + X12Segment unh = null; + String tag = ""; + int count = 0; + List>> 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 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")); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactGroup.java b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactGroup.java new file mode 100644 index 0000000..9eb85c8 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactGroup.java @@ -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 messages, List findings) +{ +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactInbound.java b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactInbound.java new file mode 100644 index 0000000..36c76d1 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactInbound.java @@ -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> findings = new LinkedHashMap<>(); + AckLevel level = AckLevel.ERRORS_ONLY; + boolean sendAck = false; + List 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 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> 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); + }); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactInterchange.java b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactInterchange.java new file mode 100644 index 0000000..06afd61 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactInterchange.java @@ -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 groups, List messages, List findings) +{ + /******************************************************************************* + ** Visit every message without inventing a functional group on the wire. + *******************************************************************************/ + public List 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"); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactMapper.java b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactMapper.java new file mode 100644 index 0000000..958f5ed --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactMapper.java @@ -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 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 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())); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactMessage.java b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactMessage.java new file mode 100644 index 0000000..c96afa1 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactMessage.java @@ -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 segments, List 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)); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactOutbound.java b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactOutbound.java new file mode 100644 index 0000000..e0d7fdb --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactOutbound.java @@ -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 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 requests, List 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 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); + } + } + +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactSeparators.java b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactSeparators.java new file mode 100644 index 0000000..aec206b --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactSeparators.java @@ -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); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactWriter.java b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactWriter.java new file mode 100644 index 0000000..d31c892 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/edi/edifact/EdifactWriter.java @@ -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 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 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... elements) + { + return (new X12Segment(tag, position, java.util.Arrays.stream(elements).map(List::of).toList())); + } +} diff --git a/src/main/java/com/kingsrook/qbits/edi/flow/InboundProcessor.java b/src/main/java/com/kingsrook/qbits/edi/flow/InboundProcessor.java index 60a6360..900f5e8 100644 --- a/src/main/java/com/kingsrook/qbits/edi/flow/InboundProcessor.java +++ b/src/main/java/com/kingsrook/qbits/edi/flow/InboundProcessor.java @@ -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 findings, QRecord document) throws Exception + protected void validation(QRecord tx, List 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"))) { diff --git a/src/main/java/com/kingsrook/qbits/edi/flow/InboxWorker.java b/src/main/java/com/kingsrook/qbits/edi/flow/InboxWorker.java index d69cce7..4b4c402 100644 --- a/src/main/java/com/kingsrook/qbits/edi/flow/InboxWorker.java +++ b/src/main/java/com/kingsrook/qbits/edi/flow/InboxWorker.java @@ -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, () -> { diff --git a/src/main/java/com/kingsrook/qbits/edi/flow/OutboundActions.java b/src/main/java/com/kingsrook/qbits/edi/flow/OutboundActions.java index c203ffd..6127af1 100644 --- a/src/main/java/com/kingsrook/qbits/edi/flow/OutboundActions.java +++ b/src/main/java/com/kingsrook/qbits/edi/flow/OutboundActions.java @@ -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")); diff --git a/src/main/java/com/kingsrook/qbits/edi/flow/OutboundWorker.java b/src/main/java/com/kingsrook/qbits/edi/flow/OutboundWorker.java index cecd7ba..b816538 100644 --- a/src/main/java/com/kingsrook/qbits/edi/flow/OutboundWorker.java +++ b/src/main/java/com/kingsrook/qbits/edi/flow/OutboundWorker.java @@ -93,7 +93,7 @@ public class OutboundWorker return (0); } List accepted = new ArrayList<>(); - List sets = new ArrayList<>(); + List 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(); @@ -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 requests, List sets, Integer original) throws Exception + public static QRecord envelope(QRecord agreement, QRecord document, QRecord map, List requests, List 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) diff --git a/src/main/java/com/kingsrook/qbits/edi/guide/BuiltInGuideLoader.java b/src/main/java/com/kingsrook/qbits/edi/guide/BuiltInGuideLoader.java index 56cbfff..90fb0be 100644 --- a/src/main/java/com/kingsrook/qbits/edi/guide/BuiltInGuideLoader.java +++ b/src/main/java/com/kingsrook/qbits/edi/guide/BuiltInGuideLoader.java @@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; *******************************************************************************/ public class BuiltInGuideLoader { - public static final List 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 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. diff --git a/src/main/java/com/kingsrook/qbits/edi/guide/GuideValidator.java b/src/main/java/com/kingsrook/qbits/edi/guide/GuideValidator.java index 2a661c7..9972a39 100644 --- a/src/main/java/com/kingsrook/qbits/edi/guide/GuideValidator.java +++ b/src/main/java/com/kingsrook/qbits/edi/guide/GuideValidator.java @@ -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 validate(X12TransactionSet transaction, Guide guide, boolean containsPhi) + public List 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 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>> loops(X12TransactionSet transaction, Guide guide) + public Map>> loops(EdiDocument transaction, Guide guide) { - State state = new State(transaction.segments(), true); + State state = new State(transaction.segments(), true, "EDIFACT".equals(guide.standard())); List 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 segments; private final boolean phi; + private final boolean edifact; private final List findings = new ArrayList<>(); private final List bindings = new ArrayList<>(); private final Map>> loops = new LinkedHashMap<>(); @@ -461,10 +462,11 @@ public class GuideValidator /******************************************************************************* ** Initialize a transaction validation. *******************************************************************************/ - private State(List segments, boolean phi) + private State(List segments, boolean phi, boolean edifact) { this.segments = segments; this.phi = phi; + this.edifact = edifact; } /******************************************************************************* diff --git a/src/main/java/com/kingsrook/qbits/edi/guide/HealthcareValidation.java b/src/main/java/com/kingsrook/qbits/edi/guide/HealthcareValidation.java index b797229..430a168 100644 --- a/src/main/java/com/kingsrook/qbits/edi/guide/HealthcareValidation.java +++ b/src/main/java/com/kingsrook/qbits/edi/guide/HealthcareValidation.java @@ -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 validate(X12TransactionSet transaction, Guide guide, Map>> loops) + public List validate(EdiDocument transaction, Guide guide, Map>> loops) { List findings = new ArrayList<>(); List segments = transaction.segments(); diff --git a/src/main/java/com/kingsrook/qbits/edi/guide/RuleEvaluator.java b/src/main/java/com/kingsrook/qbits/edi/guide/RuleEvaluator.java index c9b8521..323012f 100644 --- a/src/main/java/com/kingsrook/qbits/edi/guide/RuleEvaluator.java +++ b/src/main/java/com/kingsrook/qbits/edi/guide/RuleEvaluator.java @@ -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 selected = select(scope, element.group(1), qualifier); int position = Integer.parseInt(element.group(2)); diff --git a/src/main/java/com/kingsrook/qbits/edi/map/InboundMapper.java b/src/main/java/com/kingsrook/qbits/edi/map/InboundMapper.java index 8d83ee8..e6f6baf 100644 --- a/src/main/java/com/kingsrook/qbits/edi/map/InboundMapper.java +++ b/src/main/java/com/kingsrook/qbits/edi/map/InboundMapper.java @@ -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 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 rules) throws Exception + public Result map(EdiDocument transaction, Guide guide, QRecord map, List rules) throws Exception { Map>> 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())) { diff --git a/src/main/java/com/kingsrook/qbits/edi/map/MapPreview.java b/src/main/java/com/kingsrook/qbits/edi/map/MapPreview.java index 83c000c..36830ba 100644 --- a/src/main/java/com/kingsrook/qbits/edi/map/MapPreview.java +++ b/src/main/java/com/kingsrook/qbits/edi/map/MapPreview.java @@ -53,6 +53,10 @@ public class MapPreview ArrayList 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 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 lines = new ArrayList<>(); + for(var interchange : new com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader(runtime.config().getMaxSegmentsPerTransaction()).read(stream)) + { + Map> 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. *******************************************************************************/ diff --git a/src/main/java/com/kingsrook/qbits/edi/map/OutboundMapper.java b/src/main/java/com/kingsrook/qbits/edi/map/OutboundMapper.java index dd5d027..ec8ea42 100644 --- a/src/main/java/com/kingsrook/qbits/edi/map/OutboundMapper.java +++ b/src/main/java/com/kingsrook/qbits/edi/map/OutboundMapper.java @@ -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 rules, String prefix, String qPrefix, int sequence, String parent, + protected void emit(QRecord source, List rules, String prefix, String qPrefix, int sequence, String parent, int[] hierarchy, List output) throws Exception { Map> nodes = new LinkedHashMap<>(); @@ -112,13 +112,13 @@ public class OutboundMapper } continue; } - List elements = new ArrayList<>(); + List> 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); + } + } diff --git a/src/main/java/com/kingsrook/qbits/edi/metadata/EdiTables.java b/src/main/java/com/kingsrook/qbits/edi/metadata/EdiTables.java index abc1320..6f6c475 100644 --- a/src/main/java/com/kingsrook/qbits/edi/metadata/EdiTables.java +++ b/src/main/java/com/kingsrook/qbits/edi/metadata/EdiTables.java @@ -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")); diff --git a/src/main/java/com/kingsrook/qbits/edi/model/EdiAgreement.java b/src/main/java/com/kingsrook/qbits/edi/model/EdiAgreement.java index 8e27828..206b351 100644 --- a/src/main/java/com/kingsrook/qbits/edi/model/EdiAgreement.java +++ b/src/main/java/com/kingsrook/qbits/edi/model/EdiAgreement.java @@ -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); + } } diff --git a/src/main/java/com/kingsrook/qbits/edi/transport/HttpsTransport.java b/src/main/java/com/kingsrook/qbits/edi/transport/HttpsTransport.java index cbe533d..706a5e9 100644 --- a/src/main/java/com/kingsrook/qbits/edi/transport/HttpsTransport.java +++ b/src/main/java/com/kingsrook/qbits/edi/transport/HttpsTransport.java @@ -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 headers = new ObjectMapper().readValue(transport.getHttpsHeadersJson(), new TypeReference>() {}); diff --git a/src/main/java/com/kingsrook/qbits/edi/x12/ControlNumberAllocator.java b/src/main/java/com/kingsrook/qbits/edi/x12/ControlNumberAllocator.java index f366814..9e9e629 100644 --- a/src/main/java/com/kingsrook/qbits/edi/x12/ControlNumberAllocator.java +++ b/src/main/java/com/kingsrook/qbits/edi/x12/ControlNumberAllocator.java @@ -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"); } diff --git a/src/main/java/com/kingsrook/qbits/edi/x12/DuplicateDetector.java b/src/main/java/com/kingsrook/qbits/edi/x12/DuplicateDetector.java index 9f19afb..954806d 100644 --- a/src/main/java/com/kingsrook/qbits/edi/x12/DuplicateDetector.java +++ b/src/main/java/com/kingsrook/qbits/edi/x12/DuplicateDetector.java @@ -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. *******************************************************************************/ diff --git a/src/main/java/com/kingsrook/qbits/edi/x12/X12TransactionSet.java b/src/main/java/com/kingsrook/qbits/edi/x12/X12TransactionSet.java index 158a568..8641ab9 100644 --- a/src/main/java/com/kingsrook/qbits/edi/x12/X12TransactionSet.java +++ b/src/main/java/com/kingsrook/qbits/edi/x12/X12TransactionSet.java @@ -27,6 +27,15 @@ import java.util.List; /******************************************************************************* ** Buffered transaction set, including its ST and SE. *******************************************************************************/ -public record X12TransactionSet(X12Segment st, X12Segment se, List segments, List findings) +public record X12TransactionSet(X12Segment st, X12Segment se, List segments, List findings) implements com.kingsrook.qbits.edi.document.EdiDocument { + /******************************************************************************* + ** Expose the header to standard-neutral validators and maps. + *******************************************************************************/ + @Override + public X12Segment header() + { + return (st); + } + } diff --git a/src/main/resources/edi/guides/CONTRL-D01B.json b/src/main/resources/edi/guides/CONTRL-D01B.json new file mode 100644 index 0000000..b2d3c6b --- /dev/null +++ b/src/main/resources/edi/guides/CONTRL-D01B.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/guides/CONTRL-D96A.json b/src/main/resources/edi/guides/CONTRL-D96A.json new file mode 100644 index 0000000..8118753 --- /dev/null +++ b/src/main/resources/edi/guides/CONTRL-D96A.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/guides/DESADV-D01B.json b/src/main/resources/edi/guides/DESADV-D01B.json new file mode 100644 index 0000000..e8ea440 --- /dev/null +++ b/src/main/resources/edi/guides/DESADV-D01B.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/guides/DESADV-D96A.json b/src/main/resources/edi/guides/DESADV-D96A.json new file mode 100644 index 0000000..7cff7aa --- /dev/null +++ b/src/main/resources/edi/guides/DESADV-D96A.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/guides/INVOIC-D01B.json b/src/main/resources/edi/guides/INVOIC-D01B.json new file mode 100644 index 0000000..55e9caa --- /dev/null +++ b/src/main/resources/edi/guides/INVOIC-D01B.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/guides/INVOIC-D96A.json b/src/main/resources/edi/guides/INVOIC-D96A.json new file mode 100644 index 0000000..f149daf --- /dev/null +++ b/src/main/resources/edi/guides/INVOIC-D96A.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/guides/ORDERS-D01B.json b/src/main/resources/edi/guides/ORDERS-D01B.json new file mode 100644 index 0000000..9d60081 --- /dev/null +++ b/src/main/resources/edi/guides/ORDERS-D01B.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/guides/ORDERS-D96A.json b/src/main/resources/edi/guides/ORDERS-D96A.json new file mode 100644 index 0000000..e4a81e3 --- /dev/null +++ b/src/main/resources/edi/guides/ORDERS-D96A.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/guides/ORDRSP-D01B.json b/src/main/resources/edi/guides/ORDRSP-D01B.json new file mode 100644 index 0000000..cb72494 --- /dev/null +++ b/src/main/resources/edi/guides/ORDRSP-D01B.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/guides/ORDRSP-D96A.json b/src/main/resources/edi/guides/ORDRSP-D96A.json new file mode 100644 index 0000000..355e7b9 --- /dev/null +++ b/src/main/resources/edi/guides/ORDRSP-D96A.json @@ -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" + } + ] + } + ] +} diff --git a/src/main/resources/edi/maps/DESADV-D01B-sample.json b/src/main/resources/edi/maps/DESADV-D01B-sample.json new file mode 100644 index 0000000..979af69 --- /dev/null +++ b/src/main/resources/edi/maps/DESADV-D01B-sample.json @@ -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\"}" + } + ] +} diff --git a/src/main/resources/edi/maps/DESADV-D96A-sample.json b/src/main/resources/edi/maps/DESADV-D96A-sample.json new file mode 100644 index 0000000..77acd3f --- /dev/null +++ b/src/main/resources/edi/maps/DESADV-D96A-sample.json @@ -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\"}" + } + ] +} diff --git a/src/main/resources/edi/maps/INVOIC-D01B-sample.json b/src/main/resources/edi/maps/INVOIC-D01B-sample.json new file mode 100644 index 0000000..754a339 --- /dev/null +++ b/src/main/resources/edi/maps/INVOIC-D01B-sample.json @@ -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" + } + ] +} diff --git a/src/main/resources/edi/maps/INVOIC-D96A-sample.json b/src/main/resources/edi/maps/INVOIC-D96A-sample.json new file mode 100644 index 0000000..d92a348 --- /dev/null +++ b/src/main/resources/edi/maps/INVOIC-D96A-sample.json @@ -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" + } + ] +} diff --git a/src/main/resources/edi/maps/ORDERS-D01B-sample.json b/src/main/resources/edi/maps/ORDERS-D01B-sample.json new file mode 100644 index 0000000..89d95ae --- /dev/null +++ b/src/main/resources/edi/maps/ORDERS-D01B-sample.json @@ -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)" + } + ] +} diff --git a/src/main/resources/edi/maps/ORDERS-D96A-sample.json b/src/main/resources/edi/maps/ORDERS-D96A-sample.json new file mode 100644 index 0000000..dda2800 --- /dev/null +++ b/src/main/resources/edi/maps/ORDERS-D96A-sample.json @@ -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)" + } + ] +} diff --git a/src/main/resources/edi/maps/ORDRSP-D01B-sample.json b/src/main/resources/edi/maps/ORDRSP-D01B-sample.json new file mode 100644 index 0000000..62232d2 --- /dev/null +++ b/src/main/resources/edi/maps/ORDRSP-D01B-sample.json @@ -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\"}" + } + ] +} diff --git a/src/main/resources/edi/maps/ORDRSP-D96A-sample.json b/src/main/resources/edi/maps/ORDRSP-D96A-sample.json new file mode 100644 index 0000000..9f09cf6 --- /dev/null +++ b/src/main/resources/edi/maps/ORDRSP-D96A-sample.json @@ -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\"}" + } + ] +} diff --git a/src/test/java/com/kingsrook/qbits/edi/EdifactSupport.java b/src/test/java/com/kingsrook/qbits/edi/EdifactSupport.java new file mode 100644 index 0000000..5bd6232 --- /dev/null +++ b/src/test/java/com/kingsrook/qbits/edi/EdifactSupport.java @@ -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)))); + } +} diff --git a/src/test/java/com/kingsrook/qbits/edi/EdifactTest.java b/src/test/java/com/kingsrook/qbits/edi/EdifactTest.java new file mode 100644 index 0000000..8415764 --- /dev/null +++ b/src/test/java/com/kingsrook/qbits/edi/EdifactTest.java @@ -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 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 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 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 rows(String table) throws Exception + { + return (SystemWrites.call(() -> EdiRecords.query(table, new QQueryFilter()))); + } +} diff --git a/src/test/java/com/kingsrook/qbits/edi/RetailSupport.java b/src/test/java/com/kingsrook/qbits/edi/RetailSupport.java index 4eaa93c..02dc343 100644 --- a/src/test/java/com/kingsrook/qbits/edi/RetailSupport.java +++ b/src/test/java/com/kingsrook/qbits/edi/RetailSupport.java @@ -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))); } diff --git a/src/test/java/com/kingsrook/qbits/edi/demo/EdiDemoServer.java b/src/test/java/com/kingsrook/qbits/edi/demo/EdiDemoServer.java index 09eb495..389e09c 100644 --- a/src/test/java/com/kingsrook/qbits/edi/demo/EdiDemoServer.java +++ b/src/test/java/com/kingsrook/qbits/edi/demo/EdiDemoServer.java @@ -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. *******************************************************************************/ diff --git a/src/test/java/com/kingsrook/qbits/edi/demo/EdiDemoServerTest.java b/src/test/java/com/kingsrook/qbits/edi/demo/EdiDemoServerTest.java index e5eb009..d0c9898 100644 --- a/src/test/java/com/kingsrook/qbits/edi/demo/EdiDemoServerTest.java +++ b/src/test/java/com/kingsrook/qbits/edi/demo/EdiDemoServerTest.java @@ -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")));