Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47c3aac1b2
|
||
|
|
bf8f40959a | ||
|
|
f7c031b793 | ||
|
|
cd9423e966 | ||
|
|
d97418447b
|
||
|
|
c15872a373
|
||
|
|
fc800b8124 | ||
|
|
30ac598952
|
||
|
|
21b0e61cbb | ||
|
|
5afeefb1c2
|
||
|
|
b9ea954e1f | ||
|
|
ea4c48ad66
|
||
|
|
09b77ec122 | ||
|
|
4f8916246a
|
No files matched your search
@@ -1,7 +1,7 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.8
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.9
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
|
||||
@@ -52,7 +52,7 @@ The package layout per phase is in `docs/DESIGN.md`.
|
||||
## Dependencies
|
||||
|
||||
Java 21; parent `com.kingsrook:qbit-build-parent:2.0.0`. Default-active `qqq-snapshot` imports QQQ
|
||||
`4.1.0-RC.5` ahead of the parent's 4.0.0 BOM (ADR-0007); move to each new RC within a week (G7).
|
||||
`4.1.0-RC.6` ahead of the parent's 4.0.0 BOM (ADR-0007); move to each new RC within a week (G7).
|
||||
Middleware Javalin is optional. Also StAEDI (X12) and as2-lib with BouncyCastle (AS2), all Apache-2.0/MIT-style licensed.
|
||||
|
||||
## Release
|
||||
|
||||
+29
-1
@@ -6,8 +6,20 @@ Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic
|
||||
|
||||
### Added
|
||||
|
||||
- Phase f1 `edi` 1.0.0 spec'd UI API: guide tree, map target/transform metadata, encrypted base64 sample preview with unsaved rule overrides, and per-rule validation under `/qqq/v1/qbit/edi`.
|
||||
- Versioned `Edi...V1` schemas and generated OpenAPI fragment with the RC.6 contract/compatibility checks; real HTTP strict-mode permission, PHI, upload, isolation and validation tests.
|
||||
- Shared sandbox/record-action preview service, current/original guide selection, caller-session transforms, structured revision/expected diffs and value-free PHI diagnostics.
|
||||
- Transform registry argument schemas, including host-supplied schemas. Preview Map record actions now enforce the documented exact `edi.map.preview` permission through core's full-name permission checker.
|
||||
|
||||
- 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.
|
||||
|
||||
- Phase d warehouse and logistics baseline guides for 940, 945, 214 and 204 in 004010 and 005010 (syntax notes, situational control-total and stop rules, AT7 pairing), inbound/outbound sample maps (940 in, 945 out/in, 214 in, 204 in/out), DATEPART/TIMEPART outbound transforms and `+`-joined inbound paths for date/time pairs, and Harbor Logistics and Ridgeline Freight demo traffic.
|
||||
||||||| parent of bfb63ce (feat: phase c - AS2 transport)
|
||||
|
||||
- Phase c AS2: `As2Transport` send and receive (RFC 4130) with S/MIME signing (SHA-256/384/512; SHA1 flagged), encryption (AES-128/256 CBC or GCM; 3DES flagged), optional compression, synchronous and asynchronous signed MDNs with MIC verification, Test Connection, optional TLS pinning and VAN transports with no partner. Built on as2-lib 5.1.8 and BouncyCastle, entirely in memory (no keystore or temporary files).
|
||||
- `POST /qqq/hooks/edi/as2/{transport}` and `/{transport}/mdn` route provider: identity, decryption (ACTIVE or NEXT keys), signature and MIC checks, Message-ID dedupe returning the original MDN, error MDNs with no inbox row, and a queued, retried asynchronous MDN sender.
|
||||
@@ -38,6 +50,22 @@ Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic
|
||||
- Repository scaffold to the QRun qbit repository standard: `qbit-build-parent` 2.0.0, QQQ 4.1.0-RC.5, Maven wrapper, Checkstyle, JaCoCo gate, CircleCI qqq-orb 0.6.8, release-candidate workflow, Dependabot, community files.
|
||||
- `EdiQBitProducer` and `EdiQBitConfig` (table prefix `edi_`, permission base `edi`).
|
||||
|
||||
### Changed
|
||||
|
||||
- QQQ baseline is now 4.1.0-RC.6. EDI tables/fields and healthcare test fixtures use core `DataClassification` through `AiTableMetaData`/`AiFieldMetaData`, preserving PHI, PII, CONFIDENTIAL and PHI AI-exposure restrictions.
|
||||
- Exception Assign and new/reopened partner rejection/dead-letter exceptions send value-free core notifications after commit, with capturing-sink and PHI canary coverage.
|
||||
- Retain `PhiLogScope` for core #1147 until RC.7: RC.6 still logs RDBMS query filters and has no redaction hook or `FieldAccessHelper`. Contingent dependency-behavior APIs are also absent; shared-choice filters remain tested.
|
||||
|
||||
### Removed
|
||||
|
||||
- Reflective `EdiClassifications` RC.5 stand-in. File-gateway stand-ins remain until its API is published; the object-storage API integration is unchanged.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Shared-choice filters group partner-or-shared alternatives beneath an AND root, so core search criteria cannot admit another partner's transports.
|
||||
|
||||
- 999 now accepts SNIP 3+ findings with `E` instead of rejecting every ERROR. HIPAA acknowledgement selection follows the implementation version and enabled agreement expectation; outbound HIPAA ST carries its implementation identifier.
|
||||
- Qualified HL/NM1 anchors distinguish healthcare hierarchy levels; repeated loop names under different parents retain separate occurrences. Situational rules inherit direct ancestor segments without borrowing values from sibling occurrences.
|
||||
- Review/Result records expose identifiers only. A marked RC.5 PHI logging bridge prevents core upsert query diagnostics from copying claim/member values into logs.
|
||||
|
||||
- Inbound apply no longer fails with a null-pointer error on a target table that has no associations (flat tables such as the 214 status sample).
|
||||
@@ -52,7 +52,7 @@ The package layout per phase is in `docs/DESIGN.md`.
|
||||
## Dependencies
|
||||
|
||||
Java 21; parent `com.kingsrook:qbit-build-parent:2.0.0`. Default-active `qqq-snapshot` imports QQQ
|
||||
`4.1.0-RC.5` ahead of the parent's 4.0.0 BOM (ADR-0007); move to each new RC within a week (G7).
|
||||
`4.1.0-RC.6` ahead of the parent's 4.0.0 BOM (ADR-0007); move to each new RC within a week (G7).
|
||||
Middleware Javalin is optional. Also StAEDI (X12) and as2-lib with BouncyCastle (AS2), all Apache-2.0/MIT-style licensed.
|
||||
|
||||
## Release
|
||||
|
||||
@@ -1,20 +1,40 @@
|
||||
# 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`.
|
||||
Java 21, QQQ **4.1.0-RC.6+**, an RDBMS backend, and `com.kingsrook.qbits:qbit-edi:0.1.0-SNAPSHOT`.
|
||||
Apply [the PostgreSQL schema](docs/ddl/edi-postgres.sql). Targets and their associations must
|
||||
share the EDI backend so each transaction set can commit or roll back atomically.
|
||||
|
||||
@@ -37,7 +57,7 @@ a least-privilege `QSession` with target and association permissions. Applying w
|
||||
produces an APPLY exception: `no service session configured`.
|
||||
|
||||
The `edi` app has Activity/Setup sections, typed home widgets, record screens and standard
|
||||
Review/Result actions. Main tables have RC.5 search-field metadata; `EdiIntegrations.mainTables()`
|
||||
Review/Result actions. Main tables have RC.6 search-field metadata; `EdiIntegrations.mainTables()`
|
||||
also exposes the list for host search registration.
|
||||
|
||||
## Tables
|
||||
@@ -53,9 +73,60 @@ There are **26 persisted tables**, whose SQL names equal their QQQ names.
|
||||
| Storage/queue | `edi_payload`, `edi_outbound_request`, `edi_outbound_message` |
|
||||
|
||||
`edi_preview_download` is a **storage-only metadata alias**, not a SQL table. Supporting tables
|
||||
appear as child lists. Partner-scoped form choices use RC.5 contingent filters, including shared
|
||||
appear as child lists. Partner-scoped form choices use RC.6 contingent filters, including shared
|
||||
transports/guides and local certificates/identities.
|
||||
|
||||
## UI API
|
||||
|
||||
With optional Javalin present, the producer registers the `edi` extension, version `1.0.0`,
|
||||
under `/qqq/v1/qbit/edi`. All routes are endpoint specs and pass strict UI-route auditing.
|
||||
|
||||
| Method | Path after `/qqq/v1/qbit/edi` | Result |
|
||||
|---|---|---|
|
||||
| GET | `/guides/{guideId}/tree` | Nested loops, ordered segments, elements and up to 200 active codes per element |
|
||||
| GET | `/maps/{mapId}/target` | Target fields, direct associations, current rules and registered transform argument schemas |
|
||||
| POST | `/sandbox/preview` | Parse counts, positional findings, acknowledgement, mapped records and revision/expected diffs |
|
||||
| POST | `/maps/{mapId}/validate` | Per-rule path, transform and required-argument errors |
|
||||
|
||||
Preview accepts `{mapId, sampleId?, sampleBase64?, sampleName?, ruleOverrides?, guideRevision?}`.
|
||||
Supply exactly one sample source. Uploads are base64 JSON, at most **5 MiB decoded**, archived as
|
||||
encrypted `edi_map_sample` records through the same storage as Preview Map. No runtime files are
|
||||
created. Configure the host's Javalin `http.maxRequestSize` to at least 8 MiB to allow base64
|
||||
overhead and request metadata. Oversized decoded samples return 400; a smaller host limit returns 413.
|
||||
|
||||
`ruleOverrides` replaces the entire rule list in memory, including an empty list; it never saves
|
||||
rules. `CURRENT` (default) selects the newest active revision of the pinned guide's name, standard,
|
||||
version, transaction set and partner; `ORIGINAL` uses the map's pinned guide. Validate accepts
|
||||
`{rules: [...]}` and checks the pinned guide. Rule fields and `transformArgs` have the same shape
|
||||
as core table CRUD (`transformArgs` is a JSON object encoded as a string).
|
||||
|
||||
Every read enforces table permissions and partner locks with the caller's session, regardless of
|
||||
the host's default core-enforcement setting. Preview additionally requires **`edi.map.preview`**,
|
||||
shared with both Preview Map record actions. Reading target metadata requires target/association
|
||||
table read permissions; guide definitions, code lists, map rules and stored samples require their
|
||||
own table reads. LOOKUP and host transforms run as the caller, without service-account elevation.
|
||||
Only the internal PHI classification check and encrypted sample storage use the EDI system writer.
|
||||
|
||||
For PHI, `result` and `ack.text` are null and both diff lists are empty; findings retain positions
|
||||
and codes with generic messages. Mapping failures are value-free `MAP` findings. Preview never
|
||||
writes target records. Diffs contain differing document records as `{documentIndex, before, after}`;
|
||||
acknowledgements for multiple groups are concatenated, with `type: MIXED` for different ack types.
|
||||
|
||||
The mapper saves `edi_map_rule` through the **core table API**, using its normal
|
||||
`edi.map_rule.insert|edit|delete` permissions and record locks. There are no custom rule-write routes.
|
||||
The Next mapper page is separate phase-f work.
|
||||
|
||||
The generated contract is `src/main/resources/openapi/qqq-v1/edi.yaml`; all component names are
|
||||
`Edi...V1`. Regenerate intentionally with:
|
||||
|
||||
```powershell
|
||||
./mvnw.cmd -B '-Dtest=EdiApiContractTest' '-Dedi.openapi.update=true' test
|
||||
```
|
||||
|
||||
RC.6 provides `UiApiContract.check`, used to compare generated and checked-in YAML and run
|
||||
`OpenApiCompatibilityChecker` against this initial fragment baseline. The extension also appears
|
||||
in the host's `/qqq/v1/openapi.yaml` and `/qqq/v1/version`.
|
||||
|
||||
## Inbound operation
|
||||
|
||||
`InboundSink.receive(InboundPayload)` stores encrypted bytes and SHA-256 before returning a durable
|
||||
@@ -66,10 +137,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.
|
||||
@@ -91,7 +162,7 @@ Outbound maps support nested association paths; inbound target paths retain the
|
||||
Preview shows parse counts, SNIP findings, the would-be ack, mapped JSON, expected-result and
|
||||
previous-revision comparisons. It writes no target records. PHI previews suppress document values.
|
||||
A session-authorized encrypted JSON report is downloadable. Select a stored sample or upload
|
||||
synthetic EDI on the standard Review step. Browser uploads require the host's RC.5 Javalin
|
||||
synthetic EDI on the standard Review step. Browser uploads require the host's RC.6 Javalin
|
||||
`uploadedFileArchiveTableName` to be `edi_preview_download` and multipart handling without disk
|
||||
staging. The alias encrypts streams and persists samples; this process accepts no filesystem archive.
|
||||
Samples must contain synthetic data only.
|
||||
@@ -145,29 +216,35 @@ in memory. Database storage remains the default for demos/tests.
|
||||
|
||||
For object storage, add/install optional `qbit-object-storage:0.1.0-RC.2`, configure its bucket
|
||||
alias and select `OBJECT_STORAGE`. EDI uses only its public API and system access, with keys
|
||||
`edi/yyyy/MM/dd/kind/uuid`. Its transitive QQQ dependencies are excluded to retain RC.5.
|
||||
`edi/yyyy/MM/dd/kind/uuid`. Its transitive QQQ dependencies are excluded to retain RC.6.
|
||||
|
||||
Optional `qqq-esb` publishes setup/activity events after commit. Document events contain only ID
|
||||
and status because RC.5's standard factory otherwise copies PHI fields. Alert names are
|
||||
and status because RC.6's standard factory otherwise copies PHI fields. Alert names are
|
||||
`edi.inbox.dead-letter`, `edi.ack.overdue`, `edi.ack.rejected`, `edi.send.failed`,
|
||||
`edi.certificate.expiring`, `edi.certificate.expired` and `edi.mdn.overdue` (RC.5 requires dotted names).
|
||||
`edi.certificate.expiring`, `edi.certificate.expired` and `edi.mdn.overdue` (RC.6 requires dotted names).
|
||||
|
||||
RC.5 lacks the planned core `DataClassification` API. `EdiClassifications` marks PHI/PII/confidential
|
||||
fields and PHI fields deny AI exposure. Hosts must supply secured-data protection, database
|
||||
encryption at rest and access policies for PHI columns; markers alone do not mask fields.
|
||||
Core `DataClassification` metadata marks PHI/PII/CONFIDENTIAL fields through `AiFieldMetaData`,
|
||||
with a CONFIDENTIAL table default through `AiTableMetaData`. PHI fields deny AI exposure.
|
||||
Hosts must supply secured-data protection, database encryption at rest and access policies for PHI columns; markers alone do not mask fields.
|
||||
The home reports installed integration and storage state.
|
||||
|
||||
Assign sends a value-free `QAlerts.notify` notification to the assignee after commit. Newly opened
|
||||
or reopened REJECTED_ACK/DEAD_LETTER exceptions notify distinct assignees of OPEN/IN_PROGRESS
|
||||
exceptions for the same partner; these existing assignments define the partner's recipients.
|
||||
Repeated occurrences do not send another notification. Titles and bodies contain no document,
|
||||
partner or exception-summary values. Without an alerts integration, core uses its logging sink.
|
||||
|
||||
## Outbound and transports
|
||||
|
||||
Active OUTBOUND agreement documents select the source table, map, guide and trigger. RECORD_CHANGE
|
||||
uses core's instance listener and the writer's transaction. Source and EDI tables must share the
|
||||
backend. `triggerFilter` is a serialized QQQ `QQueryFilter`; MANUAL also honors that filter.
|
||||
RC.5 catches listener errors without failing the source write: successful enqueues roll back with
|
||||
RC.6 catches listener errors without failing the source write: successful enqueues roll back with
|
||||
the source transaction, but hosts must monitor failed listener execution.
|
||||
|
||||
`edi_outbound_enqueue` is the ESB boundary. Configure an RC.5 `EsbTrigger` to invoke it with
|
||||
`edi_outbound_enqueue` is the ESB boundary. Configure an RC.6 `EsbTrigger` to invoke it with
|
||||
`sourceTableName`, `sourceRecordId` and `eventId`; broker setup belongs to the host. Event IDs are
|
||||
unique per agreement document. RC.5's consumer runner uses JMS, so broker-free acceptance calls
|
||||
unique per agreement document. RC.6's consumer runner uses JMS, so broker-free acceptance calls
|
||||
this process directly.
|
||||
|
||||
`edi_outbound_worker` and `edi_send_worker` run every 15 seconds when a scheduler is configured.
|
||||
@@ -256,6 +333,53 @@ the agreement document `ackDueMinutes` (60 when unset).
|
||||
Existing databases need the new `edi_as2_mdn` table, `edi_outbound_message.transport_mic` and
|
||||
`edi_transport.weak_crypto` (see `docs/ddl/edi-postgres.sql`).
|
||||
|
||||
## HIPAA and PHI
|
||||
|
||||
Original **QQQ baseline guides** cover 837P (005010X222A1), 837I (005010X223A2),
|
||||
835 (005010X221A1), 270/271 (005010X279A1), 276/277 (005010X212), 834 (005010X220A1)
|
||||
and 999 (005010X231A1). These are in-house structure and usage baselines, not licensed TR3s.
|
||||
No TR3 text or full licensed code lists ship. Hosts may import their licensed guides.
|
||||
|
||||
SNIP 1–2 covers envelopes and the active guide. SNIP 3 uses exact decimals for claim charges,
|
||||
claim/line CAS adjustments and payments less signed PLB adjustments (zero tolerance). SNIP 4
|
||||
rules run per loop occurrence, including dependent patients, claim/line dates, diagnoses,
|
||||
subscriber demographics and benefits-or-rejection responses. SNIP 3+ findings produce `E` and
|
||||
an exception; SNIP 1–2 errors produce `R`. HIPAA replies use 999 when acknowledgement is enabled.
|
||||
SNIP 5 includes ICD-10 diagnosis shape, NPI Luhn with the 80840 prefix, and US ZIP shape.
|
||||
`ExternalCodeSetProvider.isValid(codeSet, code, asOf)` supplies licensed membership checks for
|
||||
EXTERNAL code lists; without it, only formats are checked. The System widget reports that state.
|
||||
|
||||
Healthcare agreement documents default to PHI and SNIP 4. Clinical keys go only to
|
||||
`edi_transaction.phiBusinessKey`; validation excerpts, acknowledgements, events, exceptions,
|
||||
alerts and process summaries contain no submitted clinical values. Healthcare sample maps and
|
||||
host tables live **only in test sources/resources** and use seeded synthetic patients and claims.
|
||||
Their NPI `1234567893` is a checksum test fixture, not an asserted provider identity.
|
||||
|
||||
**The host must encrypt the database at rest and should install qbit-secured-data.** Payloads
|
||||
are always AES-GCM encrypted; PHI columns rely on host database encryption. RC.6 core
|
||||
classification metadata marks PHI/PII/CONFIDENTIAL and denies PHI AI exposure, but does not
|
||||
provide secured-data field tiers. Without secured-data policies, a user with table access can
|
||||
read PHI columns.
|
||||
The home explicitly reports this limitation. Use record-level audits on host PHI target tables
|
||||
until core classified-value audit protection is available.
|
||||
|
||||
RC.6 still includes query criteria in RDBMS failure logs and has no logging redaction hook.
|
||||
The marked `PhiLogScope` bridge suppresses synchronous logger diagnostics during PHI mapping/application and preview; ordinary value-free EDI events
|
||||
remain persisted. It changes no logger levels and does not suppress other threads. The bridge
|
||||
tracks core #1147 (secured-data logging), with a removal/review deadline of 4.1.0-RC.7.
|
||||
Map samples use record-level audits to avoid copying expected-result PHI into audit details. Host code outside these scopes must protect its
|
||||
own logging, audits and asynchronous work.
|
||||
|
||||
Download Raw Document uses a session-bound process grant, rechecks `edi.raw.download` and record
|
||||
read/partner access at download time, streams decrypted bytes without local files, and records
|
||||
`RAW_DOWNLOADED` with the user and row references, never content. Download filenames are generic.
|
||||
|
||||
`PhiCanaryTest` scans every five-character marker window through gateway/HTTPS intake, forced
|
||||
SNIP 2/3/4 errors, mapping/apply, 999, replay/resend, Review/Result, alerts/notifications, DEBUG logs, all EDI
|
||||
query exports and all home/child widgets. FULL exports exempt only PHI-classified columns;
|
||||
RC.6 HIDDEN field-lock exports have no exemptions. MASKED tiers and the optional Quick Search
|
||||
qbit require the newer secured-data integration and are not claimed by these RC.6 tests.
|
||||
|
||||
## Build and verification
|
||||
|
||||
```bash
|
||||
|
||||
+23
-1
@@ -42,9 +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 alone has configured outbound documents. 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.
|
||||
@@ -58,3 +66,17 @@ descending creation date and ID. The home exception queue puts OPEN before IN_PR
|
||||
first within each status. RC.5 has no table-level default-sort metadata to override Next's sort.
|
||||
Inbound message labels use external IDs because filenames are classified as PHI.
|
||||
Light/dark visual layout and production scheduler/gateway deployment remain host acceptance.
|
||||
|
||||
Phase e adds a fifth partner, **Cedar Health Clearinghouse** (CEDAR, PHI enabled), after the existing
|
||||
retail and logistics scenarios. It sends seeded synthetic professional and institutional claims,
|
||||
a remittance, a professional claim with a one-dollar balancing error (SNIP 3), and a professional
|
||||
claim missing its dependent patient loop (SNIP 4). Both business-rule errors are accepted with
|
||||
errors (`E`) and open exceptions. Value-free 999s go to the in-memory clearinghouse outbox.
|
||||
A synthetic eligibility request goes out as a 270 and a matching-trace 271 arrives. Clinical keys
|
||||
appear only in the classified PHI key column, not the ordinary document key or record labels.
|
||||
|
||||
The healthcare sample maps and tables are test-only. Patients are seeded `Testpatient Alpha`
|
||||
fixtures, member IDs begin with `SYN`, and `1234567893` is a documented NPI checksum test value.
|
||||
The host database must be encrypted for real PHI; this disposable demo uses synthetic data only.
|
||||
The System widget shows format-only external code validation until a provider is registered.
|
||||
Inbox and interchange records offer the audited **Download Raw Document** action.
|
||||
+116
-27
@@ -130,7 +130,7 @@ Package layout (`com.kingsrook.qbits.edi`):
|
||||
|
||||
| Dependency | Scope | Why |
|
||||
|---|---|---|
|
||||
| QQQ `4.1.0-RC.5` (then each new RC, G7) | compile | core, `QAlerts.raise/resolve` (RC.5) |
|
||||
| QQQ `4.1.0-RC.6` (then each new RC, G7) | compile | core classification, `QAlerts.raise/resolve/notify` |
|
||||
| `qqq-middleware-javalin` | optional | hooks route provider; UI API extension (RC.6) |
|
||||
| `qqq-esb` | optional | outbound trigger from ESB table events |
|
||||
| `qqq-backend-module-filesystem` (SFTP) | compile | SFTP push and VAN polling. This is core's SFTP client, the one qbit-sftp-data-integration uses; EDI uses it directly instead of depending on that qbit (G1). Remote SFTP only; nothing local. |
|
||||
@@ -139,13 +139,20 @@ Package layout (`com.kingsrook.qbits.edi`):
|
||||
| `qbit-object-storage` (its `api` package only) | optional | raw payload storage (section 10) |
|
||||
| `qbit-file-gateway` (its `api` package only) | optional | inbound SFTP and partner-pull OUTBOX (section 5.3) |
|
||||
|
||||
Core features that arrive in RC.6, and how EDI bridges until then:
|
||||
Core features checked against the `v4.1.0-RC.6` tag:
|
||||
|
||||
| Need | Core | Until RC.6 |
|
||||
| Need | Core | RC.6 status |
|
||||
|---|---|---|
|
||||
| `DataClassification` on fields (PHI) | `actions.ai.DataClassification` (#1142) | `EdiClassifications` stand-in, `STAND-IN(core=QRun-IO/qqq#1142, removeBy=4.1.0-RC.6)`, sets the classification reflectively when the class is present |
|
||||
| User notifications | `QAlerts.notify` (#1125) | alerts only via `QAlerts.raise` (RC.5); notifications added with the RC.6 bump |
|
||||
| UI API extension point | `MiddlewareSpecExtension` (#1128) | the sandbox is a record action (section 8.3); spec'd endpoints come in phase f |
|
||||
| `DataClassification` on fields (PHI) | `actions.ai.DataClassification` (#1142) | Uses `AiFieldMetaData.withClassification` and CONFIDENTIAL `AiTableMetaData` defaults; classification stand-in removed, including healthcare test fixtures |
|
||||
| User notifications | `QAlerts.notify` (#1125) | Assign notifies the assignee; new/reopened rejection and dead-letter exceptions notify current partner assignees (section 4.4) |
|
||||
| UI API extension point | `MiddlewareSpecExtension` (#1128) | Available in core; sandbox remains a record action until phase f |
|
||||
| Contingent choice dependency behavior | `PossibleValueDependencyBehavior`, `possibleValueSourceDependsOn` | Neither API exists in the tag; existing filters retain shared choices before a partner is selected |
|
||||
| Secured-data tiers and log redaction | `FieldAccessHelper`, logging hook (#1147) | Spec only; no implementation in the tag. Retain `PhiLogScope`, `STAND-IN(core=QRun-IO/qqq#1147, removeBy=4.1.0-RC.7)` |
|
||||
|
||||
**RC.6 baseline:** classifications and notifications now use core APIs. The PHI logging bridge
|
||||
remains because RDBMS query failures still log filters and QLogger has no classified-value
|
||||
redaction hook; review/remove it by RC.7 under #1147. FULL and HIDDEN field-lock canaries run;
|
||||
MASKED tiers await `FieldAccessHelper`. Host logging/audits outside PHI scopes still need protection.
|
||||
|
||||
The file-gateway `api` ships in that qbit's phase (a). Until it is on a published SNAPSHOT, EDI uses
|
||||
`GatewayStandIn` with the identical shape, marked `STAND-IN(core=QRun-IO/qbit-file-gateway,
|
||||
@@ -247,7 +254,11 @@ One row per document, version and direction that the agreement carries.
|
||||
- When the ack arrives, `QAlerts.resolve("edi.ack.overdue", dedupKey, note)`.
|
||||
- A rejecting ack (AK9/IK9 `R`, or partial `P`) raises `edi.ack.rejected` (ERROR) and opens a
|
||||
`REJECTED_ACK` exception.
|
||||
- From RC.6, assignees of exceptions also get a `QAlerts.notify` notification.
|
||||
- Assign sends the assignee a value-free `QAlerts.notify` notification after the update commits.
|
||||
New or reopened `REJECTED_ACK`/`DEAD_LETTER` exceptions notify distinct assignees of
|
||||
OPEN/IN_PROGRESS exceptions for the same partner (including the reopened row's assignee).
|
||||
Existing active exception assignments define partner recipients; no new assignment model is
|
||||
introduced. Coalesced repeat occurrences do not notify again. Rollbacks discard notifications.
|
||||
|
||||
## 5. Transports
|
||||
|
||||
@@ -420,10 +431,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 +444,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
|
||||
|
||||
@@ -559,16 +576,41 @@ upload is stored as a sample), and the standard Review/Result lines show:
|
||||
5. the diff against the previous map revision's result and against `expectedResultJson`.
|
||||
|
||||
Nothing is written to target tables: the preview runs the whole pipeline with a no-op applier.
|
||||
Phase f adds spec'd endpoints (`POST /qqq/v1/qbit/edi/sandbox/preview` and related) for the mapper
|
||||
UI.
|
||||
Phase f1 provides the `edi` 1.0.0 `MiddlewareSpecExtension`, with component schemas named
|
||||
`Edi...V1` and a generated, contract-tested fragment at `openapi/qqq-v1/edi.yaml`:
|
||||
|
||||
### 8.4 No-code mapper UI (phase f; specified now, built later)
|
||||
- `GET /qqq/v1/qbit/edi/guides/{guideId}/tree`
|
||||
- `GET /qqq/v1/qbit/edi/maps/{mapId}/target`
|
||||
- `POST /qqq/v1/qbit/edi/sandbox/preview`
|
||||
- `POST /qqq/v1/qbit/edi/maps/{mapId}/validate`
|
||||
|
||||
Preview Map and the HTTP preview share `MapPreviewService`. HTTP preview takes `mapId`, exactly
|
||||
one of `sampleId`/`sampleBase64`, optional `sampleName`, a complete in-memory `ruleOverrides`
|
||||
replacement, and `guideRevision`. Uploads are limited to 5 MiB decoded and stored encrypted as
|
||||
samples; the host must allow at least 8 MiB HTTP bodies for base64 overhead. `CURRENT` (HTTP default)
|
||||
selects the newest active revision in the pinned guide family and partner; `ORIGINAL` uses the
|
||||
map's pinned guide, as the record action does.
|
||||
|
||||
The response contains parse counts, positional findings, acknowledgement type/text, mapped records
|
||||
and associations, structured per-document diffs, and `phiSuppressed`. PHI returns null result/ack
|
||||
text, empty diffs and generic finding messages. Mapping errors are value-free `MAP` findings.
|
||||
The preview action permission is exactly `edi.map.preview`, with no `.hasAccess` suffix.
|
||||
Guide, sample and rule reads explicitly enforce permissions and record locks; transforms run in
|
||||
the caller session. Only classification and encrypted sample storage use the internal system writer.
|
||||
|
||||
### 8.4 No-code mapper UI (phase f1 API built; Next page follows)
|
||||
A custom Next UI page (custom UIs may be nav entries):
|
||||
- left, the guide tree (loops, segments, elements with usage and code lists); right, the target
|
||||
table's fields and associations; drag to create a rule, then choose a transform;
|
||||
- a live preview pane on a chosen sample (the sandbox endpoint), with errors inline;
|
||||
- revisions: edit a DRAFT, activate, compare revisions;
|
||||
- reads and writes only through `/qqq/v1` (CRUD on `edi_map_rule` plus the extension endpoints).
|
||||
- reads and writes only through `/qqq/v1`: the guide-tree and map-target GETs above, the sandbox
|
||||
preview POST and the map validate POST; writes remain core CRUD on `edi_map_rule`, enforcing
|
||||
`edi.map_rule.insert|edit|delete` and record locks.
|
||||
- target metadata includes direct associations and transform argument JSON Schemas from the
|
||||
registry. Validate accepts `{rules: [...]}`, returns indexed per-rule errors against the pinned
|
||||
guide/target and never executes transforms or saves rules. Guide code lists return at most
|
||||
200 active codes per element.
|
||||
|
||||
## 9. Flows
|
||||
|
||||
@@ -750,6 +792,13 @@ Non-HIPAA documents validate at level 2 by default and HIPAA documents at level
|
||||
document can raise or lower its level.
|
||||
|
||||
### 11.4 External code sets
|
||||
|
||||
JSON element constraints support `source: "EXTERNAL"` and `externalCodeSet`. Normalized code-list
|
||||
rows preserve both properties. Register the provider with
|
||||
`EdiQBitConfig.withExternalCodeSetProvider(...)`; it is excluded from configuration JSON.
|
||||
Membership failures are positional SNIP 5 findings. The effective date uses a service date,
|
||||
then a payment date, otherwise the configured clock. Provider failures disclose no values.
|
||||
|
||||
`ExternalCodeSetProvider` (SPI: `boolean isValid(String codeSet, String code, LocalDate asOf)`),
|
||||
registered by the host, for example backed by a licensed table the host loads. Code lists with
|
||||
`source = EXTERNAL` delegate to it. Without a provider they check the format only, and the home says
|
||||
@@ -757,7 +806,7 @@ so.
|
||||
|
||||
## 12. HIPAA and PHI
|
||||
|
||||
- **Classification** (`DataClassification`, core #1142; the stand-in until RC.6):
|
||||
- **Classification** (`DataClassification`, core #1142; native RC.6 metadata):
|
||||
- PHI: `edi_transaction.phiBusinessKey`, `edi_transaction_error.valueExcerpt` (never written for
|
||||
PHI documents, but classified anyway), `edi_map_sample.expectedResultJson`, `edi_inbox.fileName`
|
||||
(file names often carry member or claim ids), and the payloads themselves (encrypted, never in a
|
||||
@@ -778,13 +827,17 @@ so.
|
||||
- **Minimum necessary:** PHI maps write only the mapped fields. The raw document stays encrypted and
|
||||
can be downloaded only with the record action "Download Raw Document" (permission
|
||||
`edi.raw.download`), which is audited and, with secured-data, written to its access log.
|
||||
- **Canary egress tests** (phase e, the core SECURED-DATA procedure): synthetic 837P and 835
|
||||
documents carry `CANARY-PHI-<random>` in the patient name, member id, claim number and a
|
||||
diagnosis. They run through receive, validate (with forced errors), 999, map, apply, exception,
|
||||
reprocess, resend, the home widgets, exports of every `edi_` table, Quick Search, alerts and
|
||||
captured logs, as a FULL, a MASKED and a HIDDEN user. The test fails on any substring of the canary
|
||||
longer than 4 characters outside the encrypted payloads and the PHI-classified columns, and for the
|
||||
MASKED and HIDDEN users anywhere at all.
|
||||
- **Canary egress tests** (phase e, the core SECURED-DATA procedure): two randomized markers per
|
||||
patient-name, member-id, claim-number and diagnosis field run through gateway and HTTPS hooks,
|
||||
validation with SNIP 2/3/4 failures, 999, map/apply, exceptions, reprocess/resend, every home and
|
||||
child widget, every persisted `edi_` table's QueryAction export, Review/Result, captured alerts and notifications
|
||||
and an in-memory DEBUG log appender. Every five-character window is scanned, detecting all
|
||||
longer substrings. Only explicit PHI value/display-value columns are exempt for FULL exports;
|
||||
ciphertext bytes are checked for plaintext. RC.6 HIDDEN field locks are separately exercised
|
||||
without exemptions. The storage-only download alias has no query surface. MASKED tiers and
|
||||
the optional Quick Search index await their host/core integration; they are not simulated.
|
||||
Long diagnosis markers use a widened test-only guide element so accepted-with-error fixtures
|
||||
can also reach mapping/apply; separate malformed monetary elements force structural rejection.
|
||||
- No real PHI anywhere in the repo: samples are synthetic, produced by a seeded generator.
|
||||
|
||||
## 13. App home and navigation
|
||||
@@ -925,7 +978,6 @@ queued acknowledgements are implemented. These RC.5 adaptations refine the execu
|
||||
Object storage uses only its published API.
|
||||
- Standard optional ESB publication follows RC.5. Document events use the core envelope/publisher
|
||||
with ID/status only because the standard factory copies every field, including PHI keys.
|
||||
- The a1 classification stand-in and host responsibility for PHI field protection remain unchanged.
|
||||
|
||||
Synthetic H2 verification covers permissions, association rollback, retry/stale claims, gateway
|
||||
storage failures, original/current replay, 997/999, ack reconciliation, previews and home renderers.
|
||||
@@ -955,7 +1007,7 @@ RC.5 adaptations implemented in this worktree:
|
||||
- `InboundApplier` assumed every target table has associations; a flat table (the 214 status sample) hit a null pointer. It is now null-safe.
|
||||
- The 214 sample map records the first LX event per transaction into a flat status table and always inserts (no match key), so repeated statuses form a history; a host that wants every LX event maps an association.
|
||||
- Demo: Harbor Logistics gets a gateway OUTBOX transport for 945s (the partner pulls over SFTP, per 5.3) after the retail seed has run, so its earlier acknowledgements keep their visible retries; the fourth partner Ridgeline Freight is added, so `EdiDemoServerTest` now expects four partners.
|
||||
||||||| parent of bfb63ce (feat: phase c - AS2 transport)
|
||||
|
||||
|
||||
## Phase c implementation notes (2026-10-14)
|
||||
|
||||
@@ -977,3 +1029,40 @@ RC.5 adaptations implemented in this worktree:
|
||||
`lastError` instead of a generic text.
|
||||
- Not implemented: EC and RSA-PSS signing keys (RSA only), MIC comparison under a different algorithm than
|
||||
the one requested, CAQH/Peppol profiles, and AS2 over a host-mounted client certificate (`MTLS`).
|
||||
|
||||
|
||||
## Phase e implementation notes (2026-10-14)
|
||||
|
||||
- See the RC.6 baseline in section 3. Map samples use record-level audits because expected-result
|
||||
JSON can contain PHI. Raw downloads use a session-bound storage grant and an `edi_event` audit;
|
||||
a secured-data access log remains host integration work.
|
||||
- Situational rules bind to their occurrence and inherit only direct ancestor segments, so a
|
||||
sibling line's service date cannot satisfy another line. Fixed loop-head qualifiers distinguish
|
||||
HL/NM1 roles; the same 2300/2400 names can occur under subscriber and patient parents.
|
||||
- The 271 baseline requires benefits or an AAA rejection for a traced response; it does not
|
||||
perform cross-document 270/271 correlation. Provider differences that cannot be inferred from
|
||||
the submitted document belong in a companion guide or host rule; no rendering provider is
|
||||
invented from an absent identifier.
|
||||
- Healthcare maps and sample target tables are test-only, per the phase-e scope. The 837 sample
|
||||
map is a single-claim example with service-line associations; the validator supports multiple
|
||||
claims and checks each independently. The 835 and 834 samples map multiple claim/member
|
||||
associations. These are baseline examples, not a complete licensed TR3 implementation.
|
||||
- Child-list widgets explicitly enforce their nested queries on RC.5; the generic renderer
|
||||
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.6
|
||||
test classpath. FULL and actual HIDDEN-field-lock scans run now; no simulated masking is used.
|
||||
|
||||
## Phase g implementation notes (2026-10-14)
|
||||
|
||||
- Outbound EDIFACT is ungrouped. Readers/writers preserve optional UNG/UNE groups. Ungrouped
|
||||
interchanges have a logical `edi_functional_group` tracking row, keyed by UNB reference, so
|
||||
the existing send-time deadline/overdue monitor also covers CONTRL. `groupCount` remains zero
|
||||
for those wire envelopes. GROUP_ONLY means an interchange-level CONTRL in this case.
|
||||
- Payload framing buffers one interchange (hard cap 50 MiB) before StAEDI; retained message
|
||||
segments respect `maxSegmentsPerTransaction`. It uses no filesystem. This follows the existing
|
||||
buffered storage adapters rather than the original bounded-memory streaming aspiration.
|
||||
- Retail SG labels are local guide identifiers shared by the two baseline versions, as documented
|
||||
in GUIDE-FORMAT.md; the baselines are deliberately small, not complete directory content.
|
||||
- Existing databases require the three agreement columns listed in README.md. Existing X12
|
||||
separator defaults remain unchanged; EDIFACT agreements explicitly select their outbound UNA
|
||||
characters. CONTRL action 8 is receipt-only and does not clear a functional acknowledgement deadline.
|
||||
+28
-1
@@ -1,6 +1,6 @@
|
||||
# EDI guide JSON
|
||||
|
||||
The five files in `src/main/resources/edi/guides/` are original QQQ baseline guides, not licensed
|
||||
The files in `src/main/resources/edi/guides/` are original QQQ baseline guides, not licensed
|
||||
X12/TR3 documentation or complete code sets. JSON loads into `guide.Guide` with
|
||||
`BuiltInGuideLoader.read(InputStream)`. `GuideRepository.load(id)` reconstructs the same model from
|
||||
the normalized `edi_guide*` and code-list tables.
|
||||
@@ -49,3 +49,30 @@ historical rows and their children remain addressable by id. A reverted resource
|
||||
new revision. Built-in guide writes require the loader's security key (`edi.guide.loader`), or a
|
||||
system service session. Configuration editors can receive `CUSTOM`/`IMPORTED` values of the
|
||||
`edi_guide_source` key without receiving permission to edit `BUILT_IN` rows.
|
||||
|
||||
## EDIFACT baselines
|
||||
|
||||
Set `standard` to `EDIFACT`, `transactionSet` to the UNH message type, and `version` to the
|
||||
concatenated UNH version/release (`D96A`, `D01B`). The ten bundled resources are in-house retail
|
||||
subsets with short field names and only the code values needed by the maps. They contain no
|
||||
copied directory descriptions and are not complete UNECE directories or EANCOM certifications.
|
||||
|
||||
UNH and UNT appear as actual guide segments. UNB/UNZ and UNG/UNE are checked by the envelope
|
||||
reader. UNH association codes, such as `EAN008`, remain available as component 5 of UNH02.
|
||||
EDIFACT mandatory (`M`) maps to `REQUIRED`; conditional (`C`) maps to `SITUATIONAL` with
|
||||
`condition` or rules where needed. A component uses the existing `position` plus
|
||||
`componentPosition`, for example NAD02 component 1. EDIFACT decimal values accept `.` or `,`;
|
||||
DECIMAL/UNIT inbound transforms normalize them and outbound DECIMAL uses the agreement's mark.
|
||||
|
||||
Segment groups use the existing loop tree: SG1 starts with RFF, SG2 with NAD, and the retail
|
||||
line loop SG25 starts with LIN. These are stable local guide loop labels, not a claim that every
|
||||
message/directory assigns the same published group numbers. DESADV uses SG10 (CPS) containing
|
||||
SG17 (LIN), matching carton/item associations. Map examples are `NAD[NAD01=BY]/NAD02-1`,
|
||||
`SG25/LIN/LIN03-1` and `SG25[*]/QTY/QTY01-2`. Named loops take precedence over element parsing,
|
||||
so `SG25` is a loop rather than an element of a hypothetical `SG` segment.
|
||||
|
||||
CONTRL baselines include UCI, optional UCF group responses, UCM message responses and nested
|
||||
UCS/UCD positional errors. Shared validator diagnostics are translated to EDIFACT 0085 codes;
|
||||
submitted erroneous values are not copied into the acknowledgement. `GROUP_ONLY` reports the
|
||||
interchange outcome, `ALL_TRANSACTIONS` includes accepted messages, and `ERRORS_ONLY` includes
|
||||
rejected message details. A CONTRL is never itself acknowledged. APERAK is deferred.
|
||||
+16
-3
@@ -52,9 +52,9 @@ for Shipment/Order/Pack/Item loops and parent numbering.
|
||||
DATE/TIME format QQQ values as X12 dates/times; DECIMAL multiplies by the implied `scale`.
|
||||
Other registered transforms operate on the source value. Conditions use the existing EDI rule
|
||||
language over segments already emitted. A missing required mapping input fails mapping;
|
||||
structurally invalid mapped output opens VALIDATION and is never sent. ST/SE are generated by
|
||||
the mapper and group-local controls are assigned by the worker. Component-target paths are
|
||||
currently rejected explicitly; the bundled retail maps use simple elements.
|
||||
structurally invalid mapped output opens VALIDATION and is never sent. ST/SE or UNH/UNT are generated by
|
||||
the mapper; the worker allocates controls. Component-target paths use `-`, for example
|
||||
`LIN03-1` and `LIN03-2`, and retain components separately so the EDIFACT writer can escape data.
|
||||
|
||||
## Warehouse and logistics samples
|
||||
|
||||
@@ -76,3 +76,16 @@ collide with the header N1 loop) and the stop sequence is mapped from S501. A 21
|
||||
events; the sample map records the first (`LX[1]/...`) and a host that wants all of them maps an
|
||||
association instead. Optional outbound qualifiers such as MAN01 and L1102 are constants, so the
|
||||
sample source rows must carry the value they qualify (the carton SSCC, the bill of lading).
|
||||
|
||||
## EDIFACT retail samples
|
||||
|
||||
Each of D96A and D01B includes `ORDERS-<version>-sample.json` inbound to `sample_order` and
|
||||
`lines`, and outbound `ORDRSP` from `sample_order`, `DESADV` from `sample_shipment` with
|
||||
`cartons.items`, and `INVOIC` from `sample_invoice` with `lines`. Test sources define these
|
||||
synthetic tables. ORDERS carries its business key in BGM02, date in qualified DTM01 component 2,
|
||||
buyer in qualified NAD02 component 1, and items in SG25. BGM's document codes are 220, 231, 351
|
||||
and 380 respectively. These subsets intentionally carry only the fields exercised by the samples.
|
||||
|
||||
Composite paths address values, not delimiter-joined strings. An identifier containing `+`, `:`
|
||||
or `'` remains a single value; the writer supplies release escapes. The outbound agreement owns
|
||||
UNA settings. Separate map resources per directory keep future version-specific changes isolated.
|
||||
@@ -3,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)
|
||||
);
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.5</qqq.snapshot.version>
|
||||
<qqq.snapshot.version>4.1.0-RC.6</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -55,6 +55,26 @@ public class EdiQBitConfig implements QBitConfig
|
||||
return (java.util.Set.copyOf(sendAsEdiTables));
|
||||
}
|
||||
|
||||
private com.kingsrook.qbits.edi.guide.ExternalCodeSetProvider externalCodeSetProvider;
|
||||
|
||||
/*******************************************************************************
|
||||
** Optional host provider for licensed external code sets.
|
||||
*******************************************************************************/
|
||||
@JsonIgnore
|
||||
public com.kingsrook.qbits.edi.guide.ExternalCodeSetProvider getExternalCodeSetProvider()
|
||||
{
|
||||
return (externalCodeSetProvider);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Register a provider; null retains format-only validation.
|
||||
*******************************************************************************/
|
||||
public EdiQBitConfig withExternalCodeSetProvider(com.kingsrook.qbits.edi.guide.ExternalCodeSetProvider value)
|
||||
{
|
||||
externalCodeSetProvider = value;
|
||||
return (this);
|
||||
}
|
||||
|
||||
private String payloadBucketAlias = "edi";
|
||||
private java.util.function.Supplier<com.kingsrook.qqq.backend.core.model.session.QSession> serviceSessionSupplier = null;
|
||||
private java.time.Clock clock = java.time.Clock.systemUTC();
|
||||
|
||||
@@ -138,6 +138,7 @@ public class EdiQBitProducer implements QBitMetaDataProducer<EdiQBitConfig>
|
||||
.withPlugin(new com.kingsrook.qbits.edi.as2.As2Transport()).withPlugin(new com.kingsrook.qbits.edi.transport.UnavailableTransport("EMAIL")));
|
||||
try
|
||||
{
|
||||
com.kingsrook.qbits.edi.api.EdiApi.register(instance);
|
||||
com.kingsrook.qqq.middleware.javalin.QJavalinMetaData.ofOrWithNew(instance).withAdditionalRouteProviderReference(
|
||||
new com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference(com.kingsrook.qbits.edi.transport.HttpsRouteProvider.class))
|
||||
.withAdditionalRouteProviderReference(new com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference(com.kingsrook.qbits.edi.as2.As2RouteProvider.class));
|
||||
|
||||
@@ -53,7 +53,7 @@ public final class EdiRuntime
|
||||
private static final Map<QInstance, EdiRuntime> INSTANCES = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private final X12EnvelopeReader reader;
|
||||
private final X12Writer writer = new X12Writer();
|
||||
private final GuideValidator validator = new GuideValidator();
|
||||
private final GuideValidator validator;
|
||||
private final BuiltInGuideLoader guides = new BuiltInGuideLoader();
|
||||
private final com.kingsrook.qbits.edi.storage.PayloadStore payloads;
|
||||
private final EdiQBitConfig config;
|
||||
@@ -71,6 +71,7 @@ public final class EdiRuntime
|
||||
private EdiRuntime(QInstance instance, EdiQBitConfig config)
|
||||
{
|
||||
this.config = config;
|
||||
validator = new GuideValidator(config);
|
||||
reader = new X12EnvelopeReader(config.getMaxSegmentsPerTransaction());
|
||||
payloads = "OBJECT_STORAGE".equals(config.getPayloadStore())
|
||||
? new com.kingsrook.qbits.edi.storage.ObjectStoragePayloadStore(new PayloadCrypto(config.getSecretsResolver()), config.getPayloadBucketAlias(), config.getClock())
|
||||
|
||||
@@ -57,7 +57,7 @@ final class FunctionalAckBuilder
|
||||
for(X12TransactionSet transaction : group.transactions())
|
||||
{
|
||||
List<ValidationFinding> errors = findings.getOrDefault(transaction.st().value(2), List.of());
|
||||
boolean rejected = !transaction.findings().isEmpty() || errors.stream().anyMatch(f -> "ERROR".equals(f.severity()));
|
||||
boolean rejected = !transaction.findings().isEmpty() || errors.stream().anyMatch(f -> "ERROR".equals(f.severity()) && f.snipLevel() <= 2);
|
||||
String status = rejected ? "R" : errors.isEmpty() ? "A" : "E";
|
||||
if(!rejected)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.cfg.CoercionAction;
|
||||
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.fasterxml.jackson.databind.type.LogicalType;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.extensions.AbstractJsonEndpointSpec;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtension;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtensionsMetaData;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.In;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
import com.kingsrook.qqq.openapi.model.Type;
|
||||
import io.javalin.http.HttpStatus;
|
||||
|
||||
/*******************************************************************************
|
||||
** The EDI 1.0.0 UI API: four spec'd operations in the default qbit namespace.
|
||||
*******************************************************************************/
|
||||
public final class EdiApi
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Static registration only.
|
||||
*******************************************************************************/
|
||||
private EdiApi()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Install with optional Javalin from the qbit producer.
|
||||
*******************************************************************************/
|
||||
public static void register(QInstance instance)
|
||||
{
|
||||
MiddlewareSpecExtensionsMetaData.ofOrWithNew(instance).withExtension(extension());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Shared descriptor for route binding and the committed contract.
|
||||
*******************************************************************************/
|
||||
public static MiddlewareSpecExtension extension()
|
||||
{
|
||||
return (new MiddlewareSpecExtension().withName("edi").withSemanticVersion("1.0.0")
|
||||
.withDescription("EDI sandbox and mapper, using the signed-in user's permissions.")
|
||||
.withSpec(GuideTreeSpec.class).withSpec(MapTargetSpec.class).withSpec(PreviewSpec.class).withSpec(ValidateSpec.class));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Common typed JSON contract; core owns authentication and error mapping.
|
||||
*******************************************************************************/
|
||||
public abstract static class EdiSpec extends AbstractJsonEndpointSpec
|
||||
{
|
||||
private final String path;
|
||||
private final String response;
|
||||
private final String request;
|
||||
private final String summary;
|
||||
|
||||
/*******************************************************************************
|
||||
** Immutable operation metadata.
|
||||
*******************************************************************************/
|
||||
protected EdiSpec(String path, String response, String request, String summary)
|
||||
{
|
||||
this.path = path;
|
||||
this.response = response;
|
||||
this.request = request;
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Publish the method, path and permission expectations.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (new BasicOperation().withPath("/qbit/edi" + path).withHttpMethod(request == null ? HttpMethod.GET : HttpMethod.POST)
|
||||
.withShortSummary(summary).withLongDescription(summary + ". Uses the caller session and table read permissions. Preview also requires edi.map.preview. Rule writes use core table CRUD."));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Declare positive integer record identifiers.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
String name = path.contains("{guideId}") ? "guideId" : path.contains("{mapId}") ? "mapId" : null;
|
||||
return (name == null ? List.of() : List.of(new Parameter().withName(name).withIn(In.PATH).withRequired(true)
|
||||
.withDescription("Positive record identifier.").withSchema(new Schema().withType(Type.INTEGER))));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** All schemas use the Edi prefix and V1 suffix.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Map<String, Schema> defineComponentSchemas()
|
||||
{
|
||||
return (path.startsWith("/guides/") ? EdiSchemas.all() : Map.of());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Response component name.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return (response);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Optional request body contract.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (request == null ? null : EdiSchemas.ref(request));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Core middleware errors are explicitly documented.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<BasicResponse> defineAdditionalBasicResponses()
|
||||
{
|
||||
return (List.of(new BasicResponse(HttpStatus.BAD_REQUEST, "Invalid request or sample exceeds 5 MiB", "EdiErrorV1"),
|
||||
new BasicResponse(HttpStatus.UNAUTHORIZED, "Sign in required", "EdiErrorV1"),
|
||||
new BasicResponse(HttpStatus.FORBIDDEN, "Permission denied", "EdiErrorV1"),
|
||||
new BasicResponse(HttpStatus.NOT_FOUND, "Missing or invisible record", "EdiErrorV1"),
|
||||
new BasicResponse(HttpStatus.CONTENT_TOO_LARGE, "Host HTTP request size limit exceeded", "EdiErrorV1")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Reject malformed or overflowing identifiers without echoing request content.
|
||||
*******************************************************************************/
|
||||
protected Integer id(JsonInput input, String name) throws QBadRequestException
|
||||
{
|
||||
try
|
||||
{
|
||||
int value = Integer.parseInt(input.getPathParam(name));
|
||||
if(value < 1)
|
||||
{
|
||||
throw (new NumberFormatException());
|
||||
}
|
||||
return (value);
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw (new QBadRequestException("A positive integer identifier is required"));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Strict scalar typing prevents coercion of unsaved edits and sample identifiers.
|
||||
*******************************************************************************/
|
||||
protected <T> T body(JsonInput input, Class<T> type) throws QBadRequestException
|
||||
{
|
||||
ObjectMapper mapper = JsonMapper.builder().disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
|
||||
.disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT).build();
|
||||
mapper.coercionConfigFor(LogicalType.Textual).setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail).setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail);
|
||||
try
|
||||
{
|
||||
if(input.getBody() == null)
|
||||
{
|
||||
throw (new IllegalArgumentException("Object body required"));
|
||||
}
|
||||
return (mapper.convertValue(input.getBody(), type));
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
throw (new QBadRequestException("Invalid body; check property names and types"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Read the guide tree.
|
||||
*******************************************************************************/
|
||||
public static class GuideTreeSpec extends EdiSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Public constructor for core spec discovery.
|
||||
*******************************************************************************/
|
||||
public GuideTreeSpec()
|
||||
{
|
||||
super("/guides/{guideId}/tree", "EdiGuideTreeV1", null, "Read the guide tree");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
return (new EdiApiService().tree(id(input, "guideId")));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Read target metadata and map rules.
|
||||
*******************************************************************************/
|
||||
public static class MapTargetSpec extends EdiSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Public constructor for core spec discovery.
|
||||
*******************************************************************************/
|
||||
public MapTargetSpec()
|
||||
{
|
||||
super("/maps/{mapId}/target", "EdiTargetV1", null, "Read target metadata and map rules");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
return (new EdiApiService().target(id(input, "mapId")));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Preview an encrypted sample with optional unsaved rules.
|
||||
*******************************************************************************/
|
||||
public static class PreviewSpec extends EdiSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Public constructor for core spec discovery.
|
||||
*******************************************************************************/
|
||||
public PreviewSpec()
|
||||
{
|
||||
super("/sandbox/preview", "EdiPreviewV1", "EdiPreviewRequestV1", "Preview an encrypted sample with optional unsaved rules");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
return (new EdiApiService().preview(body(input, EdiSchemas.EdiPreviewRequestV1.class)));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate unsaved map rules.
|
||||
*******************************************************************************/
|
||||
public static class ValidateSpec extends EdiSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Public constructor for core spec discovery.
|
||||
*******************************************************************************/
|
||||
public ValidateSpec()
|
||||
{
|
||||
super("/maps/{mapId}/validate", "EdiValidationV1", "EdiValidateRequestV1", "Validate unsaved map rules");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
return (new EdiApiService().validate(id(input, "mapId"), body(input, EdiSchemas.EdiValidateRequestV1.class)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kingsrook.qbits.edi.EdiRuntime;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiAssociationV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiElementV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiFieldV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiGuideTreeV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiLoopV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiPreviewRequestV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiPreviewV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiRuleErrorsV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiRuleV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiSegmentV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiTargetV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiTransformV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiValidateRequestV1;
|
||||
import com.kingsrook.qbits.edi.api.EdiSchemas.EdiValidationV1;
|
||||
import com.kingsrook.qbits.edi.map.MapPreviewService;
|
||||
import com.kingsrook.qbits.edi.map.MapRuleValidator;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.storage.PayloadKind;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
/*******************************************************************************
|
||||
** Caller-authorized projections and sandbox orchestration, using normal QQQ actions.
|
||||
*******************************************************************************/
|
||||
public class EdiApiService
|
||||
{
|
||||
public static final int MAX_SAMPLE_BYTES = 5 * 1024 * 1024;
|
||||
private static final ObjectMapper JSON = new ObjectMapper().findAndRegisterModules();
|
||||
|
||||
/*******************************************************************************
|
||||
** Enforced reads preserve table permissions, field locks and partner record locks.
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> query(String table, QQueryFilter filter) throws Exception
|
||||
{
|
||||
return (EdiRecords.queryEnforced(table, filter));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** A missing or invisible record has the same response.
|
||||
*******************************************************************************/
|
||||
private static QRecord row(String table, Integer id) throws Exception
|
||||
{
|
||||
if(id == null || id < 1)
|
||||
{
|
||||
throw (new QBadRequestException("A positive identifier is required"));
|
||||
}
|
||||
var records = query(table, EdiRecords.equal("id", id));
|
||||
if(records.isEmpty())
|
||||
{
|
||||
throw (new QNotFoundException("Not found"));
|
||||
}
|
||||
return (records.getFirst());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Read a guide as a nested tree, preserving duplicate loop ids in separate parents.
|
||||
*******************************************************************************/
|
||||
public EdiGuideTreeV1 tree(Integer id) throws Exception
|
||||
{
|
||||
row("edi_guide", id);
|
||||
List<EdiLoopV1> roots = new ArrayList<>();
|
||||
List<EdiSegmentV1> rootSegments = new ArrayList<>();
|
||||
Map<String, EdiLoopV1> loops = new LinkedHashMap<>();
|
||||
Map<String, String> latestPaths = new LinkedHashMap<>();
|
||||
var segments = query("edi_guide_segment", EdiRecords.equal("guideId", id));
|
||||
segments.sort(Comparator.comparing(r -> r.getValueInteger("sequence")));
|
||||
for(var segment : segments)
|
||||
{
|
||||
List<EdiElementV1> elements = new ArrayList<>();
|
||||
var rows = query("edi_guide_element", EdiRecords.equal("guideSegmentId", segment.getValue("id")));
|
||||
rows.sort(Comparator.comparing((QRecord r) -> r.getValueInteger("position")).thenComparing(r -> Objects.requireNonNullElse(r.getValueInteger("componentPosition"), 0)));
|
||||
for(var element : rows)
|
||||
{
|
||||
List<String> codes = List.of();
|
||||
Integer codeList = element.getValueInteger("codeListId");
|
||||
if(codeList != null)
|
||||
{
|
||||
row("edi_code_list", codeList);
|
||||
codes = query("edi_code_list_value", EdiRecords.equal("codeListId", codeList)
|
||||
.withCriteria("isActive", com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS, true))
|
||||
.stream().map(r -> r.getValueString("code")).sorted().limit(200).toList();
|
||||
}
|
||||
elements.add(new EdiElementV1(element.getValueInteger("position"), element.getValueInteger("componentPosition"),
|
||||
element.getValueString("elementId"), element.getValueString("name"), element.getValueString("dataType"),
|
||||
element.getValueInteger("minLength"), element.getValueInteger("maxLength"), element.getValueString("usage"), codeList, codes));
|
||||
}
|
||||
EdiSegmentV1 node = new EdiSegmentV1(segment.getValueString("segmentId"), segment.getValueString("position"), segment.getValueString("usage"),
|
||||
segment.getValueInteger("minUse"), segment.getValueInteger("maxUse"), segment.getValueString("name"), elements);
|
||||
String loopId = segment.getValueString("loopId");
|
||||
if(loopId == null || loopId.isBlank())
|
||||
{
|
||||
rootSegments.add(node);
|
||||
}
|
||||
else
|
||||
{
|
||||
String parent = latestPaths.get(segment.getValueString("parentLoopId"));
|
||||
String path = (parent == null ? "" : parent + "/") + loopId;
|
||||
EdiLoopV1 loop = loops.get(path);
|
||||
if(loop == null)
|
||||
{
|
||||
loop = new EdiLoopV1(loopId, segment.getValueInteger("loopMaxRepeat"), new ArrayList<>(), new ArrayList<>());
|
||||
loops.put(path, loop);
|
||||
(parent == null ? roots : loops.get(parent).loops()).add(loop);
|
||||
}
|
||||
latestPaths.put(loopId, path);
|
||||
loop.segments().add(node);
|
||||
}
|
||||
}
|
||||
return (new EdiGuideTreeV1(roots, rootSegments));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Return visible target metadata, rules and the same registry used by mapping.
|
||||
*******************************************************************************/
|
||||
public EdiTargetV1 target(Integer id) throws Exception
|
||||
{
|
||||
QRecord map = row("edi_map", id);
|
||||
QTableMetaData table = targetTable(map);
|
||||
List<EdiAssociationV1> associations = new ArrayList<>();
|
||||
if(table.getAssociations() != null)
|
||||
{
|
||||
for(var association : table.getAssociations())
|
||||
{
|
||||
QTableMetaData child = QContext.getQInstance().getTable(association.getAssociatedTableName());
|
||||
associations.add(new EdiAssociationV1(association.getName(), child.getName(), Objects.requireNonNullElse(child.getLabel(), association.getName()), fields(child)));
|
||||
}
|
||||
}
|
||||
List<EdiRuleV1> rules = new ArrayList<>();
|
||||
for(QRecord rule : query("edi_map_rule", EdiRecords.equal("mapId", id)))
|
||||
{
|
||||
rules.add(rule(rule));
|
||||
}
|
||||
rules.sort(Comparator.comparing(r -> Objects.requireNonNullElse(r.sequence(), 0)));
|
||||
return (new EdiTargetV1(table.getName(), fields(table), associations, rules,
|
||||
EdiRuntime.current().transforms().argumentSchemas().entrySet().stream().map(e -> new EdiTransformV1(e.getKey(), e.getValue())).toList()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve the configured target without accepting a client-supplied table.
|
||||
*******************************************************************************/
|
||||
private QTableMetaData targetTable(QRecord map) throws Exception
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(map.getValueString("tableName"));
|
||||
if(table == null)
|
||||
{
|
||||
throw (new QBadRequestException("The map target table is unavailable"));
|
||||
}
|
||||
return (table);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Table metadata is only exposed with the table's read permission.
|
||||
*******************************************************************************/
|
||||
private List<EdiFieldV1> fields(QTableMetaData table) throws Exception
|
||||
{
|
||||
QueryInput input = new QueryInput(table.getName());
|
||||
input.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
|
||||
PermissionsHelper.checkTablePermissionThrowing(input, TablePermissionSubType.READ);
|
||||
return (table.getFields().values().stream().map(f -> new EdiFieldV1(f.getName(), f.getLabel(), f.getType().name(), f.getIsRequired(), f.getPossibleValueSourceName())).toList());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Copy only documented rule fields; do not leak persistence or diagnostic fields.
|
||||
*******************************************************************************/
|
||||
private EdiRuleV1 rule(QRecord r)
|
||||
{
|
||||
return (new EdiRuleV1(r.getValueInteger("id"), r.getValueInteger("sequence"), r.getValueString("ediPath"), r.getValueString("qqqPath"),
|
||||
r.getValueString("transform"), r.getValueString("transformArgs"), r.getValueBoolean("required"), r.getValueString("condition"), r.getValueString("defaultValue")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Convert unsaved edits to detached records; mapId is never accepted from a rule.
|
||||
*******************************************************************************/
|
||||
private List<QRecord> records(List<EdiRuleV1> rules) throws Exception
|
||||
{
|
||||
if(rules == null || rules.size() > 10000 || rules.stream().anyMatch(Objects::isNull))
|
||||
{
|
||||
throw (new QBadRequestException("Supply an array of at most 10000 rules"));
|
||||
}
|
||||
List<QRecord> result = new ArrayList<>();
|
||||
for(EdiRuleV1 rule : rules)
|
||||
{
|
||||
result.add(new QRecord().withValue("id", rule.id()).withValue("sequence", rule.sequence()).withValue("ediPath", rule.ediPath())
|
||||
.withValue("qqqPath", rule.qqqPath()).withValue("transform", rule.transform()).withValue("transformArgs", rule.transformArgs())
|
||||
.withValue("required", rule.required()).withValue("condition", rule.condition()).withValue("defaultValue", rule.defaultValue()));
|
||||
}
|
||||
return (result);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate all submitted rules, with stable index correlation and no persistence.
|
||||
*******************************************************************************/
|
||||
public EdiValidationV1 validate(Integer id, EdiValidateRequestV1 request) throws Exception
|
||||
{
|
||||
QRecord map = row("edi_map", id);
|
||||
targetTable(map);
|
||||
var guide = MapPreviewService.guide(map, "ORIGINAL");
|
||||
List<QRecord> rules = records(request.rules());
|
||||
List<EdiRuleErrorsV1> results = new ArrayList<>();
|
||||
for(int i = 0; i < rules.size(); i++)
|
||||
{
|
||||
QRecord rule = rules.get(i);
|
||||
results.add(new EdiRuleErrorsV1(i, rule.getValueInteger("id"), rule.getValueInteger("sequence"),
|
||||
new MapRuleValidator().validate(rule, map, guide, EdiRuntime.current().transforms())));
|
||||
}
|
||||
return (new EdiValidationV1(results));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Authorize before decoding or saving an upload; previews never change map rules.
|
||||
*******************************************************************************/
|
||||
public EdiPreviewV1 preview(EdiPreviewRequestV1 request) throws Exception
|
||||
{
|
||||
QRecord map = row("edi_map", request.mapId());
|
||||
AbstractActionInput permission = new AbstractActionInput();
|
||||
permission.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
|
||||
PermissionsHelper.checkProcessPermissionThrowing(permission, "edi_map_preview");
|
||||
String revision = Objects.requireNonNullElse(request.guideRevision(), "CURRENT");
|
||||
if(!List.of("CURRENT", "ORIGINAL").contains(revision))
|
||||
{
|
||||
throw (new QBadRequestException("guideRevision must be CURRENT or ORIGINAL"));
|
||||
}
|
||||
if((request.sampleId() == null) == (request.sampleBase64() == null))
|
||||
{
|
||||
throw (new QBadRequestException("Supply exactly one of sampleId and sampleBase64"));
|
||||
}
|
||||
List<QRecord> overrides = request.ruleOverrides() == null ? null : records(request.ruleOverrides());
|
||||
QRecord sample;
|
||||
if(request.sampleId() != null)
|
||||
{
|
||||
sample = row("edi_map_sample", request.sampleId());
|
||||
if(!Objects.equals(sample.getValueInteger("mapId"), request.mapId()))
|
||||
{
|
||||
throw (new QNotFoundException("Not found"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String encoded = request.sampleBase64();
|
||||
if(encoded.length() > ((MAX_SAMPLE_BYTES + 2) / 3) * 4)
|
||||
{
|
||||
throw (new QBadRequestException("Sample exceeds the 5 MiB limit"));
|
||||
}
|
||||
byte[] bytes;
|
||||
try
|
||||
{
|
||||
bytes = Base64.getDecoder().decode(encoded);
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
throw (new QBadRequestException("sampleBase64 must be valid base64"));
|
||||
}
|
||||
if(bytes.length == 0 || bytes.length > MAX_SAMPLE_BYTES)
|
||||
{
|
||||
throw (new QBadRequestException("Sample must contain 1 byte through 5 MiB"));
|
||||
}
|
||||
if(request.sampleName() != null && request.sampleName().length() > 255)
|
||||
{
|
||||
throw (new QBadRequestException("sampleName must be at most 255 characters"));
|
||||
}
|
||||
// As in the record action, only encrypted sample archival uses the EDI writer.
|
||||
sample = SystemWrites.call(() ->
|
||||
{
|
||||
var ref = EdiRuntime.current().payloads().put(PayloadKind.SAMPLE, new ByteArrayInputStream(bytes));
|
||||
try
|
||||
{
|
||||
return (SystemWrites.insert("edi_map_sample", new QRecord().withValue("mapId", request.mapId())
|
||||
.withValue("name", Objects.requireNonNullElse(request.sampleName(), "Synthetic upload")).withValue("payloadRef", ref.value())));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
EdiRuntime.current().payloads().delete(ref);
|
||||
throw (e);
|
||||
}
|
||||
});
|
||||
}
|
||||
return (JSON.convertValue(new MapPreviewService().preview(map, sample, overrides, revision), EdiPreviewV1.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import com.kingsrook.qqq.middleware.javalin.schemabuilder.ToSchema;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
import com.kingsrook.qqq.openapi.model.Type;
|
||||
|
||||
/*******************************************************************************
|
||||
** Versioned wire types; schemas describe values without exposing QRecord internals.
|
||||
*******************************************************************************/
|
||||
public final class EdiSchemas
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Static schema catalog.
|
||||
*******************************************************************************/
|
||||
private EdiSchemas()
|
||||
{
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** A reference to another versioned wire type.
|
||||
*******************************************************************************/
|
||||
public static Schema ref(String name)
|
||||
{
|
||||
return (new Schema().withRefToSchema(name));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Array schema with an explicit item shape.
|
||||
*******************************************************************************/
|
||||
private static Schema array(Schema item)
|
||||
{
|
||||
return (new Schema().withType(Type.ARRAY).withItems(item));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Ordered guide tree. Root segments are outside loops; nested loops retain parent scope.
|
||||
*******************************************************************************/
|
||||
public record EdiGuideTreeV1(List<EdiLoopV1> loops, List<EdiSegmentV1> segments) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Ordered guide tree. Root segments are outside loops; nested loops retain parent scope.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("loops", array(ref("EdiLoopV1"))),
|
||||
Map.entry("segments", array(ref("EdiSegmentV1")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Guide loop, nested within its parent; repeat is the maximum occurrence count.
|
||||
*******************************************************************************/
|
||||
public record EdiLoopV1(String id, Integer repeat, List<EdiLoopV1> loops, List<EdiSegmentV1> segments) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Guide loop, nested within its parent; repeat is the maximum occurrence count.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("id", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("repeat", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("loops", array(ref("EdiLoopV1"))),
|
||||
Map.entry("segments", array(ref("EdiSegmentV1")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Segment definition in guide sequence order.
|
||||
*******************************************************************************/
|
||||
public record EdiSegmentV1(String id, String position, String usage, Integer min, Integer max, String name, List<EdiElementV1> elements) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Segment definition in guide sequence order.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("id", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("position", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("usage", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("min", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("max", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("name", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("elements", array(ref("EdiElementV1")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Element or composite component; codes contain at most 200 active codes in lexical order.
|
||||
*******************************************************************************/
|
||||
public record EdiElementV1(Integer position, Integer component, String id, String name, String type, Integer minLength, Integer maxLength, String usage, Integer codeListId, List<String> codes) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Element or composite component; codes contain at most 200 active codes in lexical order.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("position", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("component", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("id", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("name", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("type", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("minLength", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("maxLength", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("usage", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("codeListId", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("codes", array(new Schema().withType(Type.STRING).withNullable(true)))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Target metadata visible to the caller, current rules and registered transforms.
|
||||
*******************************************************************************/
|
||||
public record EdiTargetV1(String table, List<EdiFieldV1> fields, List<EdiAssociationV1> associations, List<EdiRuleV1> rules, List<EdiTransformV1> transforms) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Target metadata visible to the caller, current rules and registered transforms.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("table", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("fields", array(ref("EdiFieldV1"))),
|
||||
Map.entry("associations", array(ref("EdiAssociationV1"))),
|
||||
Map.entry("rules", array(ref("EdiRuleV1"))),
|
||||
Map.entry("transforms", array(ref("EdiTransformV1")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** QQQ target field metadata.
|
||||
*******************************************************************************/
|
||||
public record EdiFieldV1(String name, String label, String type, Boolean required, String possibleValueSourceName) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("QQQ target field metadata.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("name", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("label", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("type", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("required", new Schema().withType(Type.BOOLEAN)),
|
||||
Map.entry("possibleValueSourceName", new Schema().withType(Type.STRING).withNullable(true))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Direct QQQ association and its target fields.
|
||||
*******************************************************************************/
|
||||
public record EdiAssociationV1(String name, String table, String label, List<EdiFieldV1> fields) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Direct QQQ association and its target fields.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("name", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("table", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("label", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("fields", array(ref("EdiFieldV1")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Map rule. id is optional for unsaved rules. transformArgs is a JSON object encoded as a string, as in core table CRUD.
|
||||
*******************************************************************************/
|
||||
public record EdiRuleV1(Integer id, Integer sequence, String ediPath, String qqqPath, String transform, String transformArgs, Boolean required, String condition, String defaultValue) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Map rule. id is optional for unsaved rules. transformArgs is a JSON object encoded as a string, as in core table CRUD.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("id", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("sequence", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("ediPath", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("qqqPath", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("transform", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("transformArgs", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("required", new Schema().withType(Type.BOOLEAN).withNullable(true)),
|
||||
Map.entry("condition", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("defaultValue", new Schema().withType(Type.STRING).withNullable(true))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Registered transform and JSON Schema describing its string-valued arguments.
|
||||
*******************************************************************************/
|
||||
public record EdiTransformV1(String name, Map<String, Object> argumentSchema) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Registered transform and JSON Schema describing its string-valued arguments.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("name", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("argumentSchema", new Schema().withType(Type.OBJECT).withAdditionalProperties(true).withDescription("JSON Schema: type, properties, required and additionalProperties. All transform arguments are strings."))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** mapId is required. Supply exactly one of sampleId or sampleBase64. Uploads are at most 5 MiB decoded, stored encrypted. ruleOverrides replaces the whole rule set in memory. CURRENT (default) uses the newest active guide in the pinned guide family; ORIGINAL uses the pinned guide.
|
||||
*******************************************************************************/
|
||||
public record EdiPreviewRequestV1(Integer mapId, Integer sampleId, String sampleBase64, String sampleName, List<EdiRuleV1> ruleOverrides, String guideRevision) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("mapId is required. Supply exactly one of sampleId or sampleBase64. Uploads are at most 5 MiB decoded, stored encrypted. ruleOverrides replaces the whole rule set in memory. CURRENT (default) uses the newest active guide in the pinned guide family; ORIGINAL uses the pinned guide.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("mapId", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("sampleId", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("sampleBase64", new Schema().withType(Type.STRING).withNullable(true).withFormat("byte").withMaxLength(6990508)),
|
||||
Map.entry("sampleName", new Schema().withType(Type.STRING).withNullable(true).withMaxLength(255)),
|
||||
Map.entry("ruleOverrides", array(ref("EdiRuleV1")).withNullable(true)),
|
||||
Map.entry("guideRevision", new Schema().withType(Type.STRING).withNullable(true).withEnumValues(List.of("CURRENT", "ORIGINAL")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Parse, validate, acknowledge and map without target writes. PHI suppresses result, ack text, and both diffs.
|
||||
*******************************************************************************/
|
||||
public record EdiPreviewV1(EdiParseV1 parse, List<EdiFindingV1> findings, EdiAckV1 ack, EdiResultV1 result, EdiDiffV1 diff, Boolean phiSuppressed) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Parse, validate, acknowledge and map without target writes. PHI suppresses result, ack text, and both diffs.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("parse", ref("EdiParseV1")),
|
||||
Map.entry("findings", array(ref("EdiFindingV1"))),
|
||||
Map.entry("ack", ref("EdiAckV1")),
|
||||
Map.entry("result", ref("EdiResultV1").withNullable(true)),
|
||||
Map.entry("diff", ref("EdiDiffV1")),
|
||||
Map.entry("phiSuppressed", new Schema().withType(Type.BOOLEAN))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Counts of wire interchanges, groups and documents.
|
||||
*******************************************************************************/
|
||||
public record EdiParseV1(Integer interchanges, Integer groups, Integer documents) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Counts of wire interchanges, groups and documents.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("interchanges", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("groups", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("documents", new Schema().withType(Type.INTEGER).withNullable(true))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Value-free diagnostic. PHI messages are generic; only positions and codes are retained.
|
||||
*******************************************************************************/
|
||||
public record EdiFindingV1(Integer snipLevel, String severity, Integer segmentPosition, String segmentId, String loopId, Integer elementPosition, String code, String message) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Value-free diagnostic. PHI messages are generic; only positions and codes are retained.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("snipLevel", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("severity", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("segmentPosition", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("segmentId", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("loopId", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("elementPosition", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("code", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("message", new Schema().withType(Type.STRING).withNullable(true))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Concatenated acknowledgement previews; type is 997, 999, TA1, CONTRL, NONE or MIXED. text is null for PHI.
|
||||
*******************************************************************************/
|
||||
public record EdiAckV1(String type, String text) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Concatenated acknowledgement previews; type is 997, 999, TA1, CONTRL, NONE or MIXED. text is null for PHI.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("type", new Schema().withType(Type.STRING).withNullable(true)),
|
||||
Map.entry("text", new Schema().withType(Type.STRING).withNullable(true))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Mapped records in document order; no target writes.
|
||||
*******************************************************************************/
|
||||
public record EdiResultV1(List<EdiRecordV1> records) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Mapped records in document order; no target writes.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("records", array(ref("EdiRecordV1")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Field values and recursively associated records. Values use the target table field types.
|
||||
*******************************************************************************/
|
||||
public record EdiRecordV1(Map<String, Object> values, Map<String, List<EdiRecordV1>> associations) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Field values and recursively associated records. Values use the target table field types.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("values", new Schema().withType(Type.OBJECT).withAdditionalProperties(true)),
|
||||
Map.entry("associations", new Schema().withType(Type.OBJECT).withAdditionalProperties(array(ref("EdiRecordV1"))))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Only differing records are returned. Both lists are empty for PHI.
|
||||
*******************************************************************************/
|
||||
public record EdiDiffV1(List<EdiDifferenceV1> againstPreviousRevision, List<EdiDifferenceV1> againstExpected) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Only differing records are returned. Both lists are empty for PHI.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("againstPreviousRevision", array(ref("EdiDifferenceV1"))),
|
||||
Map.entry("againstExpected", array(ref("EdiDifferenceV1")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** One document comparison; documentIndex is zero based.
|
||||
*******************************************************************************/
|
||||
public record EdiDifferenceV1(Integer documentIndex, Object before, Object after) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("One document comparison; documentIndex is zero based.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("documentIndex", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("before", new Schema().withNullable(true).withDescription("Expected JSON or a previous mapped record.")),
|
||||
Map.entry("after", new Schema().withType(Type.OBJECT).withAdditionalProperties(true).withNullable(true))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** rules is required and may be empty; rules are never persisted.
|
||||
*******************************************************************************/
|
||||
public record EdiValidateRequestV1(List<EdiRuleV1> rules) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("rules is required and may be empty; rules are never persisted.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("rules", array(ref("EdiRuleV1")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** One result per submitted rule, including rules with no errors.
|
||||
*******************************************************************************/
|
||||
public record EdiValidationV1(List<EdiRuleErrorsV1> rules) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("One result per submitted rule, including rules with no errors.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("rules", array(ref("EdiRuleErrorsV1")))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Correlate errors using zero-based index, optional id and sequence.
|
||||
*******************************************************************************/
|
||||
public record EdiRuleErrorsV1(Integer index, Integer id, Integer sequence, List<String> errors) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Correlate errors using zero-based index, optional id and sequence.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("index", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("id", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("sequence", new Schema().withType(Type.INTEGER).withNullable(true)),
|
||||
Map.entry("errors", array(new Schema().withType(Type.STRING).withNullable(true)))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Core middleware error body.
|
||||
*******************************************************************************/
|
||||
public record EdiErrorV1(String error) implements ToSchema
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe this wire object for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Schema toSchema()
|
||||
{
|
||||
return (new Schema().withType(Type.OBJECT).withDescription("Core middleware error body.")
|
||||
.withProperties(new TreeMap<>(Map.ofEntries(
|
||||
Map.entry("error", new Schema().withType(Type.STRING).withNullable(true))))));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Complete deterministic schema catalog for all four operations.
|
||||
*******************************************************************************/
|
||||
public static Map<String, Schema> all()
|
||||
{
|
||||
Map<String, Schema> result = new TreeMap<>();
|
||||
result.put("EdiGuideTreeV1", new EdiGuideTreeV1(null, null).toSchema());
|
||||
result.put("EdiLoopV1", new EdiLoopV1(null, null, null, null).toSchema());
|
||||
result.put("EdiSegmentV1", new EdiSegmentV1(null, null, null, null, null, null, null).toSchema());
|
||||
result.put("EdiElementV1", new EdiElementV1(null, null, null, null, null, null, null, null, null, null).toSchema());
|
||||
result.put("EdiTargetV1", new EdiTargetV1(null, null, null, null, null).toSchema());
|
||||
result.put("EdiFieldV1", new EdiFieldV1(null, null, null, null, null).toSchema());
|
||||
result.put("EdiAssociationV1", new EdiAssociationV1(null, null, null, null).toSchema());
|
||||
result.put("EdiRuleV1", new EdiRuleV1(null, null, null, null, null, null, null, null, null).toSchema());
|
||||
result.put("EdiTransformV1", new EdiTransformV1(null, null).toSchema());
|
||||
result.put("EdiPreviewRequestV1", new EdiPreviewRequestV1(null, null, null, null, null, null).toSchema());
|
||||
result.put("EdiPreviewV1", new EdiPreviewV1(null, null, null, null, null, null).toSchema());
|
||||
result.put("EdiParseV1", new EdiParseV1(null, null, null).toSchema());
|
||||
result.put("EdiFindingV1", new EdiFindingV1(null, null, null, null, null, null, null, null).toSchema());
|
||||
result.put("EdiAckV1", new EdiAckV1(null, null).toSchema());
|
||||
result.put("EdiResultV1", new EdiResultV1(null).toSchema());
|
||||
result.put("EdiRecordV1", new EdiRecordV1(null, null).toSchema());
|
||||
result.put("EdiDiffV1", new EdiDiffV1(null, null).toSchema());
|
||||
result.put("EdiDifferenceV1", new EdiDifferenceV1(null, null, null).toSchema());
|
||||
result.put("EdiValidateRequestV1", new EdiValidateRequestV1(null).toSchema());
|
||||
result.put("EdiValidationV1", new EdiValidationV1(null).toSchema());
|
||||
result.put("EdiRuleErrorsV1", new EdiRuleErrorsV1(null, null, null, null).toSchema());
|
||||
result.put("EdiErrorV1", new EdiErrorV1(null).toSchema());
|
||||
return (result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.app;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Explicitly enforce table and field permissions inside the widget's action scope.
|
||||
** RC.5 treats nested DEFAULT queries as trusted, so the generic renderer alone
|
||||
** does not remove fields denied by a FieldSecurityLock.
|
||||
*******************************************************************************/
|
||||
public class EdiChildRecordListRenderer extends ChildRecordListRenderer
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Use the standard typed child list with an enforced query at its public boundary.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected QueryOutput executeQuery(QueryInput input) throws QException
|
||||
{
|
||||
input.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
|
||||
return (super.executeQuery(input));
|
||||
}
|
||||
}
|
||||
@@ -312,7 +312,7 @@ public class EdiHome extends AbstractWidgetRenderer
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
rows.add(row("Item", "Payload storage", "Status", "DATABASE".equals(store) ? "Database, encrypted (demos and tests)" : "Object storage, encrypted"));
|
||||
rows.add(row("Item", "Secured data", "Status", secured ? "Installed" : "Not installed: PHI fields are not masked"));
|
||||
rows.add(row("Item", "External code sets", "Status", "Not configured"));
|
||||
rows.add(row("Item", "External code sets", "Status", EdiRuntime.current().config().getExternalCodeSetProvider() == null ? "Format only: no provider configured" : "Provider configured"));
|
||||
rows.add(row("Item", "Inbound backlog", "Status", countOf("edi_inbox", inboxBacklog())
|
||||
+ (oldestIn.isEmpty() ? "" : ", oldest " + humanize(Duration.between(oldestIn.getFirst().getValueInstant("receivedAt"), now)))));
|
||||
rows.add(row("Item", "Outbound queue", "Status", countOf("edi_outbound_message", outboundQueue())
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.document;
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
|
||||
/*******************************************************************************
|
||||
** Standard-neutral document view over the existing immutable segment value model.
|
||||
*******************************************************************************/
|
||||
public interface EdiDocument
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Return the actual message header, including its original composite values.
|
||||
*******************************************************************************/
|
||||
X12Segment header();
|
||||
|
||||
/*******************************************************************************
|
||||
** Return ordered segments including the message envelope.
|
||||
*******************************************************************************/
|
||||
List<X12Segment> segments();
|
||||
|
||||
/*******************************************************************************
|
||||
** Return envelope diagnostics without submitted values.
|
||||
*******************************************************************************/
|
||||
List<EnvelopeFinding> findings();
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.document;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactSeparators;
|
||||
|
||||
/*******************************************************************************
|
||||
** Frame concatenated X12 and EDIFACT envelopes without delimiter guessing in data.
|
||||
*******************************************************************************/
|
||||
public class EnvelopeReader
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Read one envelope, respecting UNA and released terminators; null means EOF.
|
||||
*******************************************************************************/
|
||||
public byte[] next(InputStream input) throws IOException
|
||||
{
|
||||
int first;
|
||||
do
|
||||
{
|
||||
first = input.read();
|
||||
}
|
||||
while(first != -1 && Character.isWhitespace(first));
|
||||
if(first == -1)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write(first);
|
||||
bytes.write(input.readNBytes(2));
|
||||
String tag = bytes.toString(StandardCharsets.US_ASCII);
|
||||
char terminator;
|
||||
char release = 0;
|
||||
String end;
|
||||
StringBuilder prefix = new StringBuilder();
|
||||
if("ISA".equals(tag))
|
||||
{
|
||||
bytes.write(input.readNBytes(103));
|
||||
if(bytes.size() != 106)
|
||||
{
|
||||
return (bytes.toByteArray());
|
||||
}
|
||||
terminator = (char) bytes.toByteArray()[105];
|
||||
end = "IEA";
|
||||
}
|
||||
else if("UNA".equals(tag) || "UNB".equals(tag))
|
||||
{
|
||||
EdifactSeparators separators = EdifactSeparators.DEFAULT;
|
||||
if("UNA".equals(tag))
|
||||
{
|
||||
bytes.write(input.readNBytes(6));
|
||||
separators = EdifactSeparators.fromUna(bytes.toString(StandardCharsets.US_ASCII));
|
||||
}
|
||||
else
|
||||
{
|
||||
prefix.append("UNB");
|
||||
}
|
||||
terminator = separators.segment();
|
||||
release = separators.release();
|
||||
end = "UNZ";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException("Unknown EDI envelope");
|
||||
}
|
||||
boolean escaped = false;
|
||||
int value;
|
||||
while((value = input.read()) != -1)
|
||||
{
|
||||
bytes.write(value);
|
||||
if(bytes.size() > 50 * 1024 * 1024)
|
||||
{
|
||||
throw new IOException("EDI envelope exceeds size limit");
|
||||
}
|
||||
if(escaped)
|
||||
{
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if(value == release)
|
||||
{
|
||||
escaped = true;
|
||||
}
|
||||
else if(value == terminator)
|
||||
{
|
||||
if(prefix.toString().equals(end))
|
||||
{
|
||||
break;
|
||||
}
|
||||
prefix.setLength(0);
|
||||
}
|
||||
else if(prefix.length() < 3 && !(prefix.isEmpty() && Character.isWhitespace(value)))
|
||||
{
|
||||
prefix.append((char) value);
|
||||
}
|
||||
}
|
||||
return (bytes.toByteArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.edi.ack.AckLevel;
|
||||
import com.kingsrook.qbits.edi.guide.ValidationFinding;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
|
||||
/*******************************************************************************
|
||||
** Positional CONTRL syntax responses; business application failures are not syntax errors.
|
||||
*******************************************************************************/
|
||||
public class ContrlBuilder
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Build directory-specific acknowledgement details without echoing data values.
|
||||
*******************************************************************************/
|
||||
public EdifactMessage build(EdifactInterchange input, Map<String, List<ValidationFinding>> findings, AckLevel level, String reference, String version)
|
||||
{
|
||||
List<X12Segment> segments = new ArrayList<>();
|
||||
segments.add(EdifactWriter.segment("UNH", 1, List.of(reference), List.of("CONTRL", version.substring(0, 1), version.substring(1), "UN")));
|
||||
boolean rejected = !input.findings().isEmpty() || level == AckLevel.GROUP_ONLY && (input.groups().stream().anyMatch(g -> !g.findings().isEmpty())
|
||||
|| input.allMessages().stream().anyMatch(m -> rejected(m, findings)));
|
||||
var uci = new ArrayList<List<String>>();
|
||||
uci.add(List.of(input.unb().value(5)));
|
||||
uci.add(input.unb().elements().get(1).getFirst());
|
||||
uci.add(input.unb().elements().get(2).getFirst());
|
||||
uci.add(List.of(rejected ? "4" : "7"));
|
||||
if(!input.findings().isEmpty())
|
||||
{
|
||||
uci.add(List.of(input.findings().getFirst().code()));
|
||||
}
|
||||
segments.add(new X12Segment("UCI", 2, uci.stream().map(List::of).toList()));
|
||||
if(level != AckLevel.GROUP_ONLY && input.findings().isEmpty())
|
||||
{
|
||||
messages(input.messages(), findings, level, segments);
|
||||
for(var group : input.groups())
|
||||
{
|
||||
var response = EdifactWriter.segment("UCF", segments.size() + 1, List.of(group.ung().value(5)), group.ung().elements().get(1).getFirst(),
|
||||
group.ung().elements().get(2).getFirst(), List.of(group.findings().isEmpty() ? "7" : "4"));
|
||||
if(!group.findings().isEmpty())
|
||||
{
|
||||
var fields = new ArrayList<>(response.elements());
|
||||
fields.add(List.of(List.of(group.findings().getFirst().code())));
|
||||
response = new X12Segment("UCF", response.position(), fields);
|
||||
}
|
||||
segments.add(response);
|
||||
messages(group.messages(), findings, level, segments);
|
||||
}
|
||||
}
|
||||
segments.add(X12Segment.of("UNT", segments.size() + 1, String.valueOf(segments.size() + 1), reference));
|
||||
return (new EdifactMessage(segments.getFirst(), segments.getLast(), List.copyOf(segments), List.of()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Suppress accepted message detail only under ERRORS_ONLY.
|
||||
*******************************************************************************/
|
||||
private void messages(List<EdifactMessage> messages, Map<String, List<ValidationFinding>> findings, AckLevel level, List<X12Segment> segments)
|
||||
{
|
||||
for(var message : messages)
|
||||
{
|
||||
if("CONTRL".equals(message.type()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
boolean rejected = rejected(message, findings);
|
||||
if(level == AckLevel.ERRORS_ONLY && !rejected)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var fields = new ArrayList<List<List<String>>>();
|
||||
fields.add(List.of(List.of(message.reference())));
|
||||
fields.add(message.header().elements().get(1));
|
||||
fields.add(List.of(List.of(rejected ? "4" : "7")));
|
||||
if(!message.findings().isEmpty())
|
||||
{
|
||||
fields.add(List.of(List.of(message.findings().getFirst().code())));
|
||||
}
|
||||
segments.add(new X12Segment("UCM", segments.size() + 1, fields));
|
||||
for(var finding : findings.getOrDefault(message.reference(), List.of()))
|
||||
{
|
||||
if(!"ERROR".equals(finding.severity()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String code = syntaxCode(finding);
|
||||
segments.add(finding.elementPosition() == null
|
||||
? X12Segment.of("UCS", segments.size() + 1, String.valueOf(finding.segmentPosition()), code)
|
||||
: X12Segment.of("UCS", segments.size() + 1, String.valueOf(finding.segmentPosition())));
|
||||
if(finding.elementPosition() != null)
|
||||
{
|
||||
List<String> position = finding.componentPosition() == null ? List.of(finding.elementPosition().toString())
|
||||
: List.of(finding.elementPosition().toString(), finding.componentPosition().toString());
|
||||
segments.add(EdifactWriter.segment("UCD", segments.size() + 1, List.of(code), position));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Envelope and structural errors reject the message, warnings do not.
|
||||
*******************************************************************************/
|
||||
private boolean rejected(EdifactMessage message, Map<String, List<ValidationFinding>> findings)
|
||||
{
|
||||
return (!message.findings().isEmpty() || findings.getOrDefault(message.reference(), List.of()).stream().anyMatch(f -> "ERROR".equals(f.severity())));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Translate shared validator diagnostics to service syntax codes (0085).
|
||||
*******************************************************************************/
|
||||
public static String syntaxCode(ValidationFinding finding)
|
||||
{
|
||||
String code = finding.elementPosition() == null ? finding.ak3Code() : finding.ak4Code();
|
||||
if(code == null)
|
||||
{
|
||||
return ("12");
|
||||
}
|
||||
return (finding.elementPosition() == null ? switch(code)
|
||||
{
|
||||
case "3" -> "13";
|
||||
case "4", "5" -> "35";
|
||||
case "2", "6", "7" -> "33";
|
||||
default -> "12";
|
||||
} : switch(code)
|
||||
{
|
||||
case "1", "2" -> "13";
|
||||
case "4" -> "14";
|
||||
case "5" -> "15";
|
||||
default -> "12";
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.edi.EdiRuntime;
|
||||
import com.kingsrook.qbits.edi.flow.EdiJournal;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlerts;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
/*******************************************************************************
|
||||
** Reconcile addressed CONTRL controls only within the routed outbound agreement.
|
||||
*******************************************************************************/
|
||||
public class ContrlReader
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Apply functional acceptance/rejection; receipt-only code 8 leaves deadlines open.
|
||||
*******************************************************************************/
|
||||
public void reconcile(EdifactMessage message, QRecord agreement, QRecord received) throws Exception
|
||||
{
|
||||
X12Segment uci = message.segments().stream().filter(s -> "UCI".equals(s.id())).findFirst().orElseThrow();
|
||||
if("8".equals(uci.value(4)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String overall = status(uci.value(4));
|
||||
var candidates = EdiRecords.query("edi_interchange", EdiRecords.equal("agreementId", agreement.getValue("id")).withCriteria("direction", QCriteriaOperator.EQUALS, "OUTBOUND")
|
||||
.withCriteria("standard", QCriteriaOperator.EQUALS, "EDIFACT").withCriteria("controlNumber", QCriteriaOperator.EQUALS, uci.value(1))
|
||||
.withCriteria("senderId", QCriteriaOperator.EQUALS, uci.value(2)).withCriteria("senderQualifier", QCriteriaOperator.EQUALS, uci.value(2, 1, 2))
|
||||
.withCriteria("receiverId", QCriteriaOperator.EQUALS, uci.value(3)).withCriteria("receiverQualifier", QCriteriaOperator.EQUALS, uci.value(3, 1, 2)));
|
||||
if(candidates.size() != 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Acknowledgement reference is unknown or ambiguous");
|
||||
}
|
||||
QRecord interchange = candidates.getFirst();
|
||||
Map<String, X12Segment> details = new LinkedHashMap<>();
|
||||
Map<String, String> groupStatuses = new LinkedHashMap<>();
|
||||
String currentGroup = "";
|
||||
for(var segment : message.segments())
|
||||
{
|
||||
if("UCF".equals(segment.id()))
|
||||
{
|
||||
currentGroup = segment.value(1);
|
||||
groupStatuses.put(currentGroup, status(segment.value(4)));
|
||||
}
|
||||
if("UCM".equals(segment.id()))
|
||||
{
|
||||
String key = currentGroup + ":" + segment.value(1);
|
||||
if(details.put(key, segment) != null)
|
||||
{
|
||||
throw new IllegalArgumentException("Duplicate acknowledgement message reference");
|
||||
}
|
||||
status(segment.value(3));
|
||||
}
|
||||
}
|
||||
boolean rejected = "R".equals(overall);
|
||||
var groups = EdiRecords.query("edi_functional_group", EdiRecords.equal("interchangeId", interchange.getValue("id")));
|
||||
for(var group : groups)
|
||||
{
|
||||
String scope = interchange.getValueInteger("groupCount") == 0 ? "" : group.getValueString("controlNumber");
|
||||
String groupStatus = "R".equals(overall) ? "R" : groupStatuses.getOrDefault(scope, overall);
|
||||
boolean groupRejected = "R".equals(groupStatus);
|
||||
for(var tx : EdiRecords.query("edi_transaction", EdiRecords.equal("functionalGroupId", group.getValue("id"))))
|
||||
{
|
||||
X12Segment detail = details.remove(scope + ":" + tx.getValueString("controlNumber"));
|
||||
if(detail != null && (!detail.value(2).equals(tx.getValueString("transactionSet"))
|
||||
|| !(detail.value(2, 1, 2) + detail.value(2, 1, 3)).equals(tx.getValueString("version"))))
|
||||
{
|
||||
throw new IllegalArgumentException("Acknowledgement message identification mismatch");
|
||||
}
|
||||
String result = "R".equals(groupStatus) || detail != null && "R".equals(status(detail.value(3))) ? "R" : "A";
|
||||
groupRejected |= "R".equals(result);
|
||||
SystemWrites.update("edi_transaction", new QRecord().withValue("id", tx.getValue("id")).withValue("ackStatus", result)
|
||||
.withValue("status", "R".equals(result) ? "REJECTED" : "ACKNOWLEDGED"));
|
||||
}
|
||||
rejected |= groupRejected;
|
||||
SystemWrites.update("edi_functional_group", new QRecord().withValue("id", group.getValue("id")).withValue("ackStatus", groupRejected ? "R" : "A")
|
||||
.withValue("ackReceivedAt", EdiRuntime.current().config().getClock().instant()).withValue("ackTransactionId", received.getValue("id")));
|
||||
}
|
||||
if(!details.isEmpty() || groupStatuses.keySet().stream().anyMatch(c -> groups.stream().noneMatch(g -> c.equals(g.getValueString("controlNumber")))))
|
||||
{
|
||||
throw new IllegalArgumentException("Acknowledgement contains an unknown message or group reference");
|
||||
}
|
||||
SystemWrites.update("edi_interchange", new QRecord().withValue("id", interchange.getValue("id")).withValue("status", rejected ? "REJECTED" : "ACKNOWLEDGED"));
|
||||
SystemWrites.update("edi_transaction", new QRecord().withValue("id", received.getValue("id")).withValue("status", "ACKNOWLEDGED").withValue("ackStatus", rejected ? "R" : "A"));
|
||||
for(var group : groups)
|
||||
{
|
||||
QAlerts.resolve("edi.ack.overdue", "edi_functional_group:" + group.getValue("id"), "CONTRL received");
|
||||
}
|
||||
for(var exception : EdiRecords.query("edi_exception", EdiRecords.equal("interchangeId", interchange.getValue("id")).withCriteria("type", QCriteriaOperator.EQUALS, "OVERDUE_ACK")
|
||||
.withCriteria("status", QCriteriaOperator.IN, "OPEN", "IN_PROGRESS")))
|
||||
{
|
||||
SystemWrites.update("edi_exception", new QRecord().withValue("id", exception.getValue("id")).withValue("status", "RESOLVED")
|
||||
.withValue("resolvedAt", EdiRuntime.current().config().getClock().instant()).withValue("resolution", "CONTRL received"));
|
||||
}
|
||||
if(rejected)
|
||||
{
|
||||
EdiJournal.exception(null, interchange.getValueInteger("id"), null, agreement.getValueInteger("partnerId"), "REJECTED_ACK", "Partner rejected an outbound EDIFACT document");
|
||||
QAlerts.raise(new AlertSignal("edi.ack.rejected").withSeverity(AlertSeverity.ERROR).withDedupKey("edi_interchange:" + interchange.getValue("id"))
|
||||
.withTitle("EDI acknowledgement rejected").withSummary("Review the outbound interchange"));
|
||||
}
|
||||
EdiJournal.event(null, interchange.getValueInteger("id"), received.getValueInteger("id"), "ACK_RECEIVED");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Only functional acceptance and rejection can complete an expected response.
|
||||
*******************************************************************************/
|
||||
private String status(String action)
|
||||
{
|
||||
return (switch(action)
|
||||
{
|
||||
case "7" -> "A";
|
||||
case "4" -> "R";
|
||||
default -> throw new IllegalArgumentException("Unsupported CONTRL action");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.edi.document.EnvelopeReader;
|
||||
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
import io.xlate.edi.stream.EDIInputFactory;
|
||||
import io.xlate.edi.stream.EDIStreamEvent;
|
||||
import io.xlate.edi.stream.EDIStreamException;
|
||||
import io.xlate.edi.stream.EDIStreamReader;
|
||||
import io.xlate.edi.stream.EDIStreamValidationError;
|
||||
|
||||
/*******************************************************************************
|
||||
** StAEDI tokenizer with explicit UN envelope checks and bounded message retention.
|
||||
*******************************************************************************/
|
||||
public class EdifactEnvelopeReader
|
||||
{
|
||||
private final int maxSegments;
|
||||
|
||||
/*******************************************************************************
|
||||
** Bound retained message segments, including UNH and UNT.
|
||||
*******************************************************************************/
|
||||
public EdifactEnvelopeReader(int maxSegments)
|
||||
{
|
||||
if(maxSegments < 2)
|
||||
{
|
||||
throw new IllegalArgumentException("At least two segments are required");
|
||||
}
|
||||
this.maxSegments = maxSegments;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Read concatenated EDIFACT interchanges without closing the caller stream.
|
||||
*******************************************************************************/
|
||||
public List<EdifactInterchange> read(InputStream input) throws IOException
|
||||
{
|
||||
List<EdifactInterchange> result = new ArrayList<>();
|
||||
EnvelopeReader framing = new EnvelopeReader();
|
||||
byte[] bytes;
|
||||
while((bytes = framing.next(input)) != null)
|
||||
{
|
||||
result.add(parse(bytes));
|
||||
}
|
||||
return (result);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Parse one framed interchange; diagnostics never include submitted values.
|
||||
*******************************************************************************/
|
||||
private EdifactInterchange parse(byte[] bytes) throws IOException
|
||||
{
|
||||
String prefix = new String(bytes, 0, Math.min(9, bytes.length), StandardCharsets.US_ASCII);
|
||||
EdifactSeparators separators = prefix.startsWith("UNA") ? EdifactSeparators.fromUna(prefix) : EdifactSeparators.DEFAULT;
|
||||
List<EnvelopeFinding> errors = new ArrayList<>();
|
||||
List<EnvelopeFinding> groupErrors = new ArrayList<>();
|
||||
List<EnvelopeFinding> messageErrors = new ArrayList<>();
|
||||
List<EdifactGroup> groups = new ArrayList<>();
|
||||
List<EdifactMessage> messages = new ArrayList<>();
|
||||
List<EdifactMessage> grouped = new ArrayList<>();
|
||||
List<X12Segment> segments = new ArrayList<>();
|
||||
var references = new HashSet<String>();
|
||||
X12Segment unb = null;
|
||||
X12Segment unz = null;
|
||||
X12Segment ung = null;
|
||||
X12Segment unh = null;
|
||||
String tag = "";
|
||||
int count = 0;
|
||||
List<List<List<String>>> values = new ArrayList<>();
|
||||
EDIInputFactory factory = EDIInputFactory.newFactory();
|
||||
factory.setProperty(EDIInputFactory.EDI_IGNORE_EXTRANEOUS_CHARACTERS, true);
|
||||
factory.setProperty(EDIInputFactory.EDI_VALIDATE_CONTROL_CODE_VALUES, false);
|
||||
try(EDIStreamReader reader = factory.createEDIStreamReader(new ByteArrayInputStream(bytes)))
|
||||
{
|
||||
while(reader.hasNext())
|
||||
{
|
||||
EDIStreamEvent event = reader.next();
|
||||
if(event == EDIStreamEvent.START_SEGMENT)
|
||||
{
|
||||
tag = reader.getText();
|
||||
values = new ArrayList<>();
|
||||
}
|
||||
else if(event == EDIStreamEvent.ELEMENT_DATA)
|
||||
{
|
||||
int e = reader.getLocation().getElementPosition();
|
||||
int r = Math.max(1, reader.getLocation().getElementOccurrence());
|
||||
int c = Math.max(1, reader.getLocation().getComponentPosition());
|
||||
while(values.size() < e)
|
||||
{
|
||||
values.add(new ArrayList<>());
|
||||
}
|
||||
while(values.get(e - 1).size() < r)
|
||||
{
|
||||
values.get(e - 1).add(new ArrayList<>());
|
||||
}
|
||||
var components = values.get(e - 1).get(r - 1);
|
||||
while(components.size() < c)
|
||||
{
|
||||
components.add("");
|
||||
}
|
||||
components.set(c - 1, reader.getText());
|
||||
}
|
||||
else if(event.isError() && reader.getErrorType() != EDIStreamValidationError.CONTROL_REFERENCE_MISMATCH
|
||||
&& reader.getErrorType() != EDIStreamValidationError.CONTROL_COUNT_DOES_NOT_MATCH_ACTUAL_COUNT)
|
||||
{
|
||||
if(unh != null)
|
||||
{
|
||||
messageErrors.add(error("UNH", Math.max(1, count), "12"));
|
||||
}
|
||||
else if(ung != null)
|
||||
{
|
||||
groupErrors.add(error("UNG", count, "12"));
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.add(error("UNB", count, "12"));
|
||||
}
|
||||
}
|
||||
else if(event == EDIStreamEvent.END_SEGMENT)
|
||||
{
|
||||
if("UNA".equals(tag))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
count = "UNH".equals(tag) ? 1 : count + 1;
|
||||
X12Segment segment = new X12Segment(tag, count, values);
|
||||
switch(tag)
|
||||
{
|
||||
case "UNB" ->
|
||||
{
|
||||
unb = segment;
|
||||
if("4".equals(unb.value(1, 1, 2)) && !prefix.startsWith("UNA"))
|
||||
{
|
||||
separators = new EdifactSeparators(':', '+', '.', '?', '*', '\'');
|
||||
}
|
||||
if(!List.of("3", "4").contains(unb.value(1, 1, 2)) || !List.of("", "1").contains(unb.value(11))
|
||||
|| unb.value(5).isEmpty() || unb.value(5).length() > 14)
|
||||
{
|
||||
errors.add(error("UNB", 1, "12"));
|
||||
}
|
||||
}
|
||||
case "UNG" ->
|
||||
{
|
||||
ung = segment;
|
||||
grouped = new ArrayList<>();
|
||||
groupErrors = new ArrayList<>();
|
||||
}
|
||||
case "UNH" ->
|
||||
{
|
||||
unh = segment;
|
||||
segments = new ArrayList<>();
|
||||
segments.add(segment);
|
||||
messageErrors = new ArrayList<>();
|
||||
if(!references.add(unh.value(1)))
|
||||
{
|
||||
messageErrors.add(error("UNH", 1, "19"));
|
||||
}
|
||||
}
|
||||
case "UNT" ->
|
||||
{
|
||||
if(unh == null)
|
||||
{
|
||||
errors.add(error("UNB", count, "33"));
|
||||
break;
|
||||
}
|
||||
check(messageErrors, "UNH", segment, segment.value(1), String.valueOf(count), "29");
|
||||
check(messageErrors, "UNH", segment, segment.value(2), unh.value(1), "28");
|
||||
if(segments.size() < maxSegments)
|
||||
{
|
||||
segments.add(segment);
|
||||
}
|
||||
(ung == null ? messages : grouped).add(new EdifactMessage(unh, segment, List.copyOf(segments), List.copyOf(messageErrors)));
|
||||
unh = null;
|
||||
}
|
||||
case "UNE" ->
|
||||
{
|
||||
if(ung == null)
|
||||
{
|
||||
errors.add(error("UNB", count, "33"));
|
||||
break;
|
||||
}
|
||||
check(groupErrors, "UNG", segment, segment.value(1), String.valueOf(grouped.size()), "29");
|
||||
check(groupErrors, "UNG", segment, segment.value(2), ung.value(5), "28");
|
||||
groups.add(new EdifactGroup(ung, segment, List.copyOf(grouped), List.copyOf(groupErrors)));
|
||||
ung = null;
|
||||
}
|
||||
case "UNZ" ->
|
||||
{
|
||||
unz = segment;
|
||||
check(errors, "UNB", segment, segment.value(1), String.valueOf(groups.isEmpty() ? messages.size() : groups.size()), "29");
|
||||
check(errors, "UNB", segment, segment.value(2), unb == null ? "" : unb.value(5), "28");
|
||||
}
|
||||
default ->
|
||||
{
|
||||
if(unh == null)
|
||||
{
|
||||
errors.add(error("UNB", count, "33"));
|
||||
}
|
||||
else if(segments.size() < maxSegments - 1)
|
||||
{
|
||||
segments.add(segment);
|
||||
}
|
||||
else if(messageErrors.stream().noneMatch(f -> "35".equals(f.code())))
|
||||
{
|
||||
messageErrors.add(error("UNH", count, "35"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(EDIStreamException e)
|
||||
{
|
||||
errors.add(error("UNB", 0, "12"));
|
||||
}
|
||||
if(unb == null || unz == null || ung != null || unh != null || !groups.isEmpty() && !messages.isEmpty())
|
||||
{
|
||||
errors.add(error("UNB", 0, "13"));
|
||||
}
|
||||
return (new EdifactInterchange(separators, unb, unz, List.copyOf(groups), List.copyOf(messages), List.copyOf(errors)));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Count fields accept zero padding; references retain their exact spelling.
|
||||
*******************************************************************************/
|
||||
private static void check(List<EnvelopeFinding> findings, String level, X12Segment segment, String actual, String expected, String code)
|
||||
{
|
||||
boolean equal = "29".equals(code) ? actual.matches("[0-9]+") && new java.math.BigInteger(actual).toString().equals(expected) : actual.equals(expected);
|
||||
if(!equal)
|
||||
{
|
||||
findings.add(error(level, segment.position(), code));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Service syntax error code 0085 with a value-free diagnostic.
|
||||
*******************************************************************************/
|
||||
private static EnvelopeFinding error(String level, int position, String code)
|
||||
{
|
||||
return (new EnvelopeFinding(level, position, code, "EDIFACT envelope failed validation"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
|
||||
/*******************************************************************************
|
||||
** Optional UNG/UNE functional group; ungrouped messages belong to the interchange.
|
||||
*******************************************************************************/
|
||||
public record EdifactGroup(X12Segment ung, X12Segment une, List<EdifactMessage> messages, List<EnvelopeFinding> findings)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qbits.edi.EdiRuntime;
|
||||
import com.kingsrook.qbits.edi.ack.AckLevel;
|
||||
import com.kingsrook.qbits.edi.flow.EdiJournal;
|
||||
import com.kingsrook.qbits.edi.flow.InboundProcessor;
|
||||
import com.kingsrook.qbits.edi.flow.InboxWorker;
|
||||
import com.kingsrook.qbits.edi.flow.OutboundWorker;
|
||||
import com.kingsrook.qbits.edi.guide.GuideRepository;
|
||||
import com.kingsrook.qbits.edi.guide.ValidationFinding;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.storage.PayloadStore;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
/*******************************************************************************
|
||||
** Native UNB routing and CONTRL with the shared revision, validation and apply checkpoints.
|
||||
*******************************************************************************/
|
||||
public class EdifactInbound extends InboundProcessor
|
||||
{
|
||||
private final QRecord inbox;
|
||||
private final Instant lease;
|
||||
|
||||
/*******************************************************************************
|
||||
** Bind every write to the same inbox lease used by the X12 worker.
|
||||
*******************************************************************************/
|
||||
public EdifactInbound(QRecord inbox, Instant lease, PayloadStore store)
|
||||
{
|
||||
super(inbox, lease, store);
|
||||
this.inbox = inbox;
|
||||
this.lease = lease;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Route UNB identities before validating, acknowledging or applying messages.
|
||||
*******************************************************************************/
|
||||
public void process(EdifactInterchange input, int position) throws Exception
|
||||
{
|
||||
if(input.unb() == null)
|
||||
{
|
||||
InboxWorker.fenced(inbox, lease, () ->
|
||||
{
|
||||
EdiJournal.exception(inbox.getValueInteger("id"), null, null, null, "ENVELOPE", "Invalid EDIFACT header");
|
||||
return (null);
|
||||
});
|
||||
return;
|
||||
}
|
||||
QRecord agreement = route(input);
|
||||
QRecord interchange = InboxWorker.fenced(inbox, lease, () -> interchange(input, agreement, position));
|
||||
if(agreement == null)
|
||||
{
|
||||
problem(interchange, "UNKNOWN_PARTNER");
|
||||
return;
|
||||
}
|
||||
if(inbox.getValueString("username") != null && EdiRecords.query("edi_partner", EdiRecords.equal("gatewayUsername", inbox.getValue("username"))).stream()
|
||||
.anyMatch(p -> !p.getValue("id").equals(agreement.getValue("partnerId"))))
|
||||
{
|
||||
problem(interchange, "PARTNER_MISMATCH");
|
||||
return;
|
||||
}
|
||||
if(inbox.getValueLong("sizeBytes") != null && inbox.getValueLong("sizeBytes") > Objects.requireNonNullElse(agreement.getValueLong("maxInterchangeBytes"), 52428800L))
|
||||
{
|
||||
problem(interchange, "ENVELOPE");
|
||||
return;
|
||||
}
|
||||
boolean duplicate = "DUPLICATE".equals(interchange.getValueString("status"));
|
||||
boolean ackInput = !input.allMessages().isEmpty() && input.allMessages().stream().allMatch(m -> "CONTRL".equals(m.type()));
|
||||
if(duplicate)
|
||||
{
|
||||
problem(interchange, "DUPLICATE");
|
||||
if(!"PROCESS".equals(agreement.getValueString("onDuplicate")))
|
||||
{
|
||||
if(!ackInput && "REJECT_TA1".equals(agreement.getValueString("onDuplicate")))
|
||||
{
|
||||
var rejected = new EdifactInterchange(input.separators(), input.unb(), input.unz(), input.groups(), input.messages(),
|
||||
List.of(new com.kingsrook.qbits.edi.x12.EnvelopeFinding("UNB", 1, "19", "Duplicate interchange")));
|
||||
acknowledge(rejected, agreement, interchange, Map.of(), AckLevel.GROUP_ONLY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!input.findings().isEmpty())
|
||||
{
|
||||
problem(interchange, "ENVELOPE");
|
||||
if(!ackInput)
|
||||
{
|
||||
acknowledge(input, agreement, interchange, Map.of(), AckLevel.GROUP_ONLY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Map<String, List<ValidationFinding>> findings = new LinkedHashMap<>();
|
||||
AckLevel level = AckLevel.ERRORS_ONLY;
|
||||
boolean sendAck = false;
|
||||
List<EdifactGroup> batches = input.groups().isEmpty() ? List.of(new EdifactGroup(null, null, input.messages(), List.of())) : input.groups();
|
||||
int groupPosition = 0;
|
||||
for(var batch : batches)
|
||||
{
|
||||
int gp = ++groupPosition;
|
||||
QRecord group = InboxWorker.fenced(inbox, lease, () ->
|
||||
{
|
||||
var prior = EdiRecords.query("edi_functional_group", EdiRecords.equal("interchangeId", interchange.getValue("id")).withCriteria("inboundPosition", QCriteriaOperator.EQUALS, gp));
|
||||
return (prior.isEmpty() ? SystemWrites.insert("edi_functional_group", new QRecord().withValue("interchangeId", interchange.getValue("id")).withValue("inboundPosition", gp)
|
||||
.withValue("senderId", input.unb().value(2)).withValue("controlNumber", batch.ung() == null ? input.unb().value(5) : batch.ung().value(5))
|
||||
.withValue("version", batch.messages().isEmpty() ? null : batch.messages().getFirst().version())
|
||||
.withValue("functionalIdCode", batch.ung() == null ? null : batch.ung().value(1)).withValue("transactionCount", batch.messages().size()).withValue("ackStatus", "NONE")) : prior.getFirst());
|
||||
});
|
||||
int messagePosition = 0;
|
||||
for(var message : batch.messages())
|
||||
{
|
||||
QRecord document = document(agreement, message);
|
||||
int mp = ++messagePosition;
|
||||
QRecord configured = document;
|
||||
QRecord tx = InboxWorker.fenced(inbox, lease, () ->
|
||||
{
|
||||
var prior = EdiRecords.query("edi_transaction", EdiRecords.equal("functionalGroupId", group.getValue("id")).withCriteria("inboundPosition", QCriteriaOperator.EQUALS, mp));
|
||||
return (prior.isEmpty() ? SystemWrites.insert("edi_transaction", new QRecord().withValue("interchangeId", interchange.getValue("id")).withValue("functionalGroupId", group.getValue("id"))
|
||||
.withValue("inboundPosition", mp).withValue("partnerId", agreement.getValue("partnerId")).withValue("direction", "INBOUND").withValue("transactionSet", message.type())
|
||||
.withValue("version", message.version()).withValue("controlNumber", message.reference()).withValue("segmentCount", message.segments().size()).withValue("status", "RECEIVED")
|
||||
.withValue("agreementDocumentId", configured == null ? null : configured.getValue("id"))
|
||||
.withValue("containsPhi", configured != null && Boolean.TRUE.equals(configured.getValueBoolean("containsPhi")))) : prior.getFirst());
|
||||
});
|
||||
document = revisions(document, tx);
|
||||
List<ValidationFinding> result = new ArrayList<>();
|
||||
if(document == null || document.getValue("guideId") == null)
|
||||
{
|
||||
result.add(finding("No implementation guide is configured"));
|
||||
problem(interchange, "NOT_CONFIGURED");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.addAll(EdiRuntime.current().validator().validate(message, new GuideRepository().load(document.getValueInteger("guideId")), Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))));
|
||||
}
|
||||
findings.put(message.reference(), List.copyOf(result));
|
||||
if(!batch.findings().isEmpty() || !message.findings().isEmpty())
|
||||
{
|
||||
result.add(finding("Envelope counts or references failed validation"));
|
||||
}
|
||||
QRecord effective = document;
|
||||
InboxWorker.fenced(inbox, lease, () ->
|
||||
{
|
||||
validation(tx, result, effective);
|
||||
return (null);
|
||||
});
|
||||
if("CONTRL".equals(message.type()))
|
||||
{
|
||||
if(result.isEmpty())
|
||||
{
|
||||
InboxWorker.fenced(inbox, lease, () ->
|
||||
{
|
||||
new ContrlReader().reconcile(message, agreement, tx);
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if(document != null && "CONTRL".equals(document.getValueString("ackExpectation")))
|
||||
{
|
||||
sendAck = true;
|
||||
AckLevel selected = AckLevel.valueOf(Objects.requireNonNullElse(document.getValueString("ackLevel"), "ERRORS_ONLY"));
|
||||
if(selected == AckLevel.ALL_TRANSACTIONS || level != AckLevel.ALL_TRANSACTIONS && selected == AckLevel.GROUP_ONLY)
|
||||
{
|
||||
level = selected;
|
||||
}
|
||||
}
|
||||
if(document != null && result.stream().noneMatch(f -> "ERROR".equals(f.severity()))
|
||||
&& (!"T".equals(input.usage()) || Boolean.TRUE.equals(agreement.getValueBoolean("applyTestDocuments"))))
|
||||
{
|
||||
apply(message, tx, document, agreement);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!ackInput && (sendAck || "1".equals(input.unb().value(9))))
|
||||
{
|
||||
acknowledge(input, agreement, interchange, findings, level);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Match qualifier, identifier, standard, usage and identity ownership exactly.
|
||||
*******************************************************************************/
|
||||
private QRecord route(EdifactInterchange input) throws Exception
|
||||
{
|
||||
for(var agreement : EdiRecords.query("edi_agreement", EdiRecords.equal("isActive", true).withCriteria("standard", QCriteriaOperator.EQUALS, "EDIFACT")
|
||||
.withCriteria("usageIndicator", QCriteriaOperator.EQUALS, input.usage())))
|
||||
{
|
||||
QRecord sender = OutboundWorker.row("edi_identity", agreement.getValue("partnerIdentityId"));
|
||||
QRecord receiver = OutboundWorker.row("edi_identity", agreement.getValue("localIdentityId"));
|
||||
if(Boolean.TRUE.equals(sender.getValueBoolean("isActive")) && Boolean.TRUE.equals(receiver.getValueBoolean("isActive"))
|
||||
&& "EDIFACT".equals(sender.getValueString("standard")) && "EDIFACT".equals(receiver.getValueString("standard"))
|
||||
&& Objects.equals(sender.getValue("partnerId"), agreement.getValue("partnerId")) && receiver.getValue("partnerId") == null
|
||||
&& input.unb().value(2).equals(sender.getValueString("interchangeId")) && input.unb().value(3).equals(receiver.getValueString("interchangeId"))
|
||||
&& input.unb().value(2, 1, 2).equals(Objects.requireNonNullElse(sender.getValueString("interchangeQualifier"), ""))
|
||||
&& input.unb().value(3, 1, 2).equals(Objects.requireNonNullElse(receiver.getValueString("interchangeQualifier"), "")))
|
||||
{
|
||||
return (agreement);
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Preserve retries and serialize reference duplicate checks on the agreement.
|
||||
*******************************************************************************/
|
||||
private QRecord interchange(EdifactInterchange input, QRecord agreement, int position) throws Exception
|
||||
{
|
||||
var prior = EdiRecords.query("edi_interchange", EdiRecords.equal("inboxId", inbox.getValue("id")).withCriteria("inboundPosition", QCriteriaOperator.EQUALS, position));
|
||||
if(!prior.isEmpty())
|
||||
{
|
||||
QRecord existing = prior.getFirst();
|
||||
if(agreement != null && existing.getValue("agreementId") == null)
|
||||
{
|
||||
existing.withValue("agreementId", agreement.getValue("id")).withValue("partnerId", agreement.getValue("partnerId"));
|
||||
SystemWrites.update("edi_interchange", existing);
|
||||
}
|
||||
return (existing);
|
||||
}
|
||||
boolean duplicate = false;
|
||||
if(agreement != null)
|
||||
{
|
||||
SystemWrites.guard("UPDATE edi_agreement SET id=id WHERE id=?", agreement.getValue("id"));
|
||||
duplicate = EdiRuntime.current().duplicates().edifact(agreement.getValueInteger("id"), input, Objects.requireNonNullElse(agreement.getValueInteger("duplicateWindowDays"), 365), EdiRuntime.current().config().getClock().instant());
|
||||
SystemWrites.update("edi_inbox", new QRecord().withValue("id", inbox.getValue("id")).withValue("partnerId", agreement.getValue("partnerId")));
|
||||
}
|
||||
return (SystemWrites.insert("edi_interchange", new QRecord().withValue("inboxId", inbox.getValue("id")).withValue("inboundPosition", position).withValue("direction", "INBOUND")
|
||||
.withValue("agreementId", agreement == null ? null : agreement.getValue("id")).withValue("partnerId", agreement == null ? null : agreement.getValue("partnerId"))
|
||||
.withValue("standard", "EDIFACT").withValue("senderId", input.unb().value(2)).withValue("senderQualifier", input.unb().value(2, 1, 2))
|
||||
.withValue("receiverId", input.unb().value(3)).withValue("receiverQualifier", input.unb().value(3, 1, 2)).withValue("controlNumber", input.unb().value(5))
|
||||
.withValue("interchangeVersion", input.unb().value(1, 1, 2)).withValue("usageIndicator", input.usage()).withValue("groupCount", input.groups().size())
|
||||
.withValue("payloadRef", inbox.getValue("payloadRef")).withValue("payloadSha256", inbox.getValue("payloadSha256")).withValue("sizeBytes", inbox.getValue("sizeBytes"))
|
||||
.withValue("status", duplicate ? "DUPLICATE" : input.findings().isEmpty() ? "ACCEPTED" : "REJECTED")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Match message type and directory independently for every UNH.
|
||||
*******************************************************************************/
|
||||
private QRecord document(QRecord agreement, EdifactMessage message) throws Exception
|
||||
{
|
||||
var rows = EdiRecords.query("edi_agreement_document", EdiRecords.equal("agreementId", agreement.getValue("id")).withCriteria("direction", QCriteriaOperator.EQUALS, "INBOUND")
|
||||
.withCriteria("transactionSet", QCriteriaOperator.EQUALS, message.type()).withCriteria("version", QCriteriaOperator.EQUALS, message.version()).withCriteria("isActive", QCriteriaOperator.EQUALS, true));
|
||||
return (rows.isEmpty() ? null : rows.getFirst());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Queue at most one CONTRL per received interchange, never acknowledge CONTRL.
|
||||
*******************************************************************************/
|
||||
private void acknowledge(EdifactInterchange input, QRecord agreement, QRecord received, Map<String, List<ValidationFinding>> findings, AckLevel level) throws Exception
|
||||
{
|
||||
InboxWorker.fenced(inbox, lease, () ->
|
||||
{
|
||||
if(EdiRecords.query("edi_outbound_message", EdiRecords.equal("ackForInterchangeId", received.getValue("id"))).isEmpty())
|
||||
{
|
||||
String version = input.allMessages().isEmpty() ? "D96A" : input.allMessages().getFirst().version();
|
||||
var message = new ContrlBuilder().build(input, findings, level, EdifactOutbound.allocate(agreement, "UNH"), version);
|
||||
EdifactOutbound.envelope(agreement, null, null, List.of(), List.of(message), null, received.getValueInteger("id"));
|
||||
EdiJournal.event(inbox.getValueInteger("id"), received.getValueInteger("id"), null, "ACK_QUEUED");
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Record a safe positional structural failure.
|
||||
*******************************************************************************/
|
||||
private ValidationFinding finding(String text)
|
||||
{
|
||||
return (new ValidationFinding(1, "UNH", null, null, null, 1, "3", null, "3", null, "ERROR", text, null));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Journal failures only while holding the current inbox claim.
|
||||
*******************************************************************************/
|
||||
private void problem(QRecord interchange, String type) throws Exception
|
||||
{
|
||||
InboxWorker.fenced(inbox, lease, () ->
|
||||
{
|
||||
EdiJournal.exception(inbox.getValueInteger("id"), interchange.getValueInteger("id"), null, interchange.getValueInteger("partnerId"), type, "EDIFACT processing requires attention: " + type);
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
|
||||
/*******************************************************************************
|
||||
** EDIFACT service envelope with optional groups and ungrouped messages.
|
||||
*******************************************************************************/
|
||||
public record EdifactInterchange(EdifactSeparators separators, X12Segment unb, X12Segment unz,
|
||||
List<EdifactGroup> groups, List<EdifactMessage> messages, List<EnvelopeFinding> findings)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Visit every message without inventing a functional group on the wire.
|
||||
*******************************************************************************/
|
||||
public List<EdifactMessage> allMessages()
|
||||
{
|
||||
return (Stream.concat(messages.stream(), groups.stream().flatMap(g -> g.messages().stream())).toList());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Normalize the UNB test flag to the agreement's usage vocabulary.
|
||||
*******************************************************************************/
|
||||
public String usage()
|
||||
{
|
||||
return ("1".equals(unb.value(11)) ? "T" : "P");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.edi.map.OutboundMapper;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
/*******************************************************************************
|
||||
** Reuse mapping transforms and associations with native UNH/UNT envelopes.
|
||||
*******************************************************************************/
|
||||
public class EdifactMapper extends OutboundMapper
|
||||
{
|
||||
private final char decimalMark;
|
||||
|
||||
/*******************************************************************************
|
||||
** Default numeric formatting follows the default UNA decimal mark.
|
||||
*******************************************************************************/
|
||||
public EdifactMapper()
|
||||
{
|
||||
this('.');
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Use the agreement's decimal service character for outbound numeric transforms.
|
||||
*******************************************************************************/
|
||||
public EdifactMapper(char decimalMark)
|
||||
{
|
||||
this.decimalMark = decimalMark;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Render numeric values using the negotiated decimal mark.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String decimal(String value)
|
||||
{
|
||||
return (value.replace('.', decimalMark));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Build one directory-specific message with an allocated UNH reference.
|
||||
*******************************************************************************/
|
||||
public EdifactMessage message(QRecord source, QRecord map, List<QRecord> rules, String reference) throws Exception
|
||||
{
|
||||
if(!"OUTBOUND".equals(map.getValueString("direction")))
|
||||
{
|
||||
throw new IllegalArgumentException("An outbound map is required");
|
||||
}
|
||||
String version = map.getValueString("version");
|
||||
List<X12Segment> segments = new ArrayList<>();
|
||||
segments.add(EdifactWriter.segment("UNH", 1, List.of(reference), List.of(map.getValueString("transactionSet"), version.substring(0, 1), version.substring(1), "UN")));
|
||||
emit(source, rules.stream().sorted(Comparator.comparing(r -> r.getValueInteger("sequence"))).toList(), "", "", 1, "", new int[] {0}, segments);
|
||||
segments.add(X12Segment.of("UNT", segments.size() + 1, String.valueOf(segments.size() + 1), reference));
|
||||
return (new EdifactMessage(segments.getFirst(), segments.getLast(), List.copyOf(segments), List.of()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.edi.document.EdiDocument;
|
||||
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
|
||||
/*******************************************************************************
|
||||
** UNH/UNT message with its complete directory and association identification.
|
||||
*******************************************************************************/
|
||||
public record EdifactMessage(X12Segment header, X12Segment unt, List<X12Segment> segments, List<EnvelopeFinding> findings) implements EdiDocument
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Agreement document key, for example ORDERS.
|
||||
*******************************************************************************/
|
||||
public String type()
|
||||
{
|
||||
return (header.value(2));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Agreement directory key, for example D96A.
|
||||
*******************************************************************************/
|
||||
public String version()
|
||||
{
|
||||
return (header.value(2, 1, 2) + header.value(2, 1, 3));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Message reference, unique within its interchange.
|
||||
*******************************************************************************/
|
||||
public String reference()
|
||||
{
|
||||
return (header.value(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qbits.edi.EdiRuntime;
|
||||
import com.kingsrook.qbits.edi.flow.EdiJournal;
|
||||
import com.kingsrook.qbits.edi.flow.OutboundWorker;
|
||||
import com.kingsrook.qbits.edi.guide.RuleEvaluator;
|
||||
import com.kingsrook.qbits.edi.storage.PayloadKind;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
/*******************************************************************************
|
||||
** EDIFACT outbound envelopes and durable tracking through the existing send queue.
|
||||
*******************************************************************************/
|
||||
public class EdifactOutbound
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Allocate independent UNB/UNH sequences within the configured control scope.
|
||||
*******************************************************************************/
|
||||
public static String allocate(QRecord agreement, String level) throws Exception
|
||||
{
|
||||
boolean local = "LOCAL_IDENTITY".equals(agreement.getValueString("controlNumberScope"));
|
||||
return (String.valueOf(EdiRuntime.current().controls().allocate(local ? null : agreement.getValueInteger("id"),
|
||||
local ? agreement.getValueInteger("localIdentityId") : null, level, 1, 99999999999999L, true)));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Build an ungrouped interchange; UNZ counts messages when UNG is absent.
|
||||
*******************************************************************************/
|
||||
public static EdifactInterchange wrap(QRecord agreement, QRecord sender, QRecord recipient, List<EdifactMessage> messages, String control, LocalDateTime at)
|
||||
{
|
||||
String syntax = Objects.requireNonNullElse(agreement.getValueString("interchangeVersion"), "3");
|
||||
if(!List.of("3", "4").contains(syntax))
|
||||
{
|
||||
throw new IllegalArgumentException("EDIFACT syntax version must be 3 or 4");
|
||||
}
|
||||
X12Segment unb = EdifactWriter.segment("UNB", 1, List.of(Objects.requireNonNullElse(agreement.getValueString("syntaxIdentifier"), "UNOC"), syntax),
|
||||
List.of(sender.getValueString("interchangeId"), Objects.requireNonNullElse(sender.getValueString("interchangeQualifier"), "")),
|
||||
List.of(recipient.getValueString("interchangeId"), Objects.requireNonNullElse(recipient.getValueString("interchangeQualifier"), "")),
|
||||
List.of(at.format(DateTimeFormatter.ofPattern("4".equals(syntax) ? "yyyyMMdd" : "yyMMdd")), at.format(DateTimeFormatter.ofPattern("HHmm"))),
|
||||
List.of(control), List.of(""), List.of(""), List.of(""), List.of(""), List.of(""), List.of("T".equals(agreement.getValueString("usageIndicator")) ? "1" : ""));
|
||||
return (new EdifactInterchange(EdifactSeparators.fromAgreement(agreement), unb, X12Segment.of("UNZ", 0, String.valueOf(messages.size()), control), List.of(), List.copyOf(messages), List.of()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Store bytes and record checkpoints atomically with outbound requests or ack fencing.
|
||||
** A logical tracking group carries ack deadlines even when UNG is absent on the wire.
|
||||
*******************************************************************************/
|
||||
public static QRecord envelope(QRecord agreement, QRecord document, QRecord map, List<QRecord> requests, List<EdifactMessage> messages, Integer original, Integer ackFor) throws Exception
|
||||
{
|
||||
var runtime = EdiRuntime.current();
|
||||
var interchange = wrap(agreement, OutboundWorker.row("edi_identity", agreement.getValue("localIdentityId")), OutboundWorker.row("edi_identity", agreement.getValue("partnerIdentityId")),
|
||||
messages, allocate(agreement, "UNB"), LocalDateTime.ofInstant(runtime.config().getClock().instant(), ZoneOffset.UTC));
|
||||
String ending = switch(Objects.requireNonNullElse(agreement.getValueString("lineEnding"), "NONE"))
|
||||
{
|
||||
case "LF" -> "\n";
|
||||
case "CRLF" -> "\r\n";
|
||||
default -> "";
|
||||
};
|
||||
var payload = runtime.payloads().put(PayloadKind.OUTBOUND, new ByteArrayInputStream(new EdifactWriter().write(interchange, ending).getBytes(StandardCharsets.UTF_8)));
|
||||
QRecord persisted = SystemWrites.insert("edi_interchange", new QRecord().withValue("direction", "OUTBOUND").withValue("partnerId", agreement.getValue("partnerId"))
|
||||
.withValue("agreementId", agreement.getValue("id")).withValue("transportId", agreement.getValue("outboundTransportId")).withValue("standard", "EDIFACT")
|
||||
.withValue("senderQualifier", interchange.unb().value(2, 1, 2)).withValue("senderId", interchange.unb().value(2))
|
||||
.withValue("receiverQualifier", interchange.unb().value(3, 1, 2)).withValue("receiverId", interchange.unb().value(3))
|
||||
.withValue("controlNumber", interchange.unb().value(5)).withValue("interchangeVersion", interchange.unb().value(1, 1, 2)).withValue("usageIndicator", interchange.usage())
|
||||
.withValue("interchangeDate", runtime.config().getClock().instant()).withValue("groupCount", 0).withValue("payloadRef", payload.value()).withValue("payloadSha256", payload.sha256())
|
||||
.withValue("sizeBytes", payload.sizeBytes()).withValue("status", "ENVELOPED").withValue("originalInterchangeId", original)
|
||||
.withValue("containsPhi", document != null && Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))));
|
||||
boolean expects = document != null && "CONTRL".equals(document.getValueString("ackExpectation"));
|
||||
QRecord group = SystemWrites.insert("edi_functional_group", new QRecord().withValue("interchangeId", persisted.getValue("id")).withValue("senderId", interchange.unb().value(2))
|
||||
.withValue("controlNumber", interchange.unb().value(5)).withValue("version", messages.getFirst().version()).withValue("transactionCount", messages.size()).withValue("ackStatus", expects ? "PENDING" : "NONE"));
|
||||
for(int i = 0; i < messages.size(); i++)
|
||||
{
|
||||
var message = messages.get(i);
|
||||
String key = map == null || map.getValueString("businessKeyPath") == null ? null : new RuleEvaluator(message.segments(), Map.of()).values(map.getValueString("businessKeyPath")).stream().findFirst().orElse(null);
|
||||
SystemWrites.insert("edi_transaction", new QRecord().withValue("interchangeId", persisted.getValue("id")).withValue("functionalGroupId", group.getValue("id"))
|
||||
.withValue("direction", "OUTBOUND").withValue("partnerId", agreement.getValue("partnerId")).withValue("agreementDocumentId", document == null ? null : document.getValue("id"))
|
||||
.withValue("transactionSet", message.type()).withValue("version", message.version()).withValue("controlNumber", message.reference()).withValue("status", "QUEUED")
|
||||
.withValue("guideId", document == null ? null : document.getValue("guideId"))
|
||||
.withValue("guideRevision", document == null || document.getValue("guideId") == null ? null : OutboundWorker.row("edi_guide", document.getValue("guideId")).getValue("revision")).withValue("mapId", map == null ? null : map.getValue("id"))
|
||||
.withValue("mapRevision", map == null ? null : map.getValue("revision"))
|
||||
.withValue(document != null && Boolean.TRUE.equals(document.getValueBoolean("containsPhi")) ? "phiBusinessKey" : "businessKey", key)
|
||||
.withValue("businessKeyType", map == null ? null : map.getValue("businessKeyType"))
|
||||
.withValue("containsPhi", document != null && Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))).withValue("segmentCount", message.segments().size())
|
||||
.withValue("targetTableName", i < requests.size() ? requests.get(i).getValue("sourceTableName") : null).withValue("targetRecordId", i < requests.size() ? requests.get(i).getValue("sourceRecordId") : null));
|
||||
if(i < requests.size())
|
||||
{
|
||||
SystemWrites.update("edi_outbound_request", new QRecord().withValue("id", requests.get(i).getValue("id")).withValue("status", "ENVELOPED"));
|
||||
}
|
||||
}
|
||||
QRecord queued = SystemWrites.insert("edi_outbound_message", new QRecord().withValue("interchangeId", persisted.getValue("id")).withValue("agreementId", agreement.getValue("id"))
|
||||
.withValue("partnerId", agreement.getValue("partnerId")).withValue("transportId", agreement.getValue("outboundTransportId")).withValue("status", "QUEUED").withValue("attempts", 0)
|
||||
.withValue("ackForInterchangeId", ackFor).withValue("payloadRef", payload.value()).withValue("payloadSha256", payload.sha256()).withValue("sizeBytes", payload.sizeBytes()));
|
||||
SystemWrites.update("edi_interchange", new QRecord().withValue("id", persisted.getValue("id")).withValue("outboundMessageId", queued.getValue("id")));
|
||||
EdiJournal.event(null, persisted.getValueInteger("id"), null, "ENVELOPED");
|
||||
return (queued);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Re-envelope archived outbound messages without reading changed source records.
|
||||
*******************************************************************************/
|
||||
public static void resendAsNew(QRecord original) throws Exception
|
||||
{
|
||||
QRecord agreement = OutboundWorker.row("edi_agreement", original.getValue("agreementId"));
|
||||
QRecord oldInterchange = OutboundWorker.row("edi_interchange", original.getValue("interchangeId"));
|
||||
EdifactInterchange archive;
|
||||
try(var stream = EdiRuntime.current().payloads().open(new com.kingsrook.qbits.edi.storage.PayloadRef(original.getValueString("payloadRef"), null, 0)))
|
||||
{
|
||||
archive = new EdifactEnvelopeReader(EdiRuntime.current().config().getMaxSegmentsPerTransaction()).read(stream).getFirst();
|
||||
}
|
||||
List<EdifactMessage> messages = new java.util.ArrayList<>();
|
||||
for(var message : archive.allMessages())
|
||||
{
|
||||
String reference = allocate(agreement, "UNH");
|
||||
var segments = new java.util.ArrayList<>(message.segments());
|
||||
var elements = new java.util.ArrayList<>(message.header().elements());
|
||||
elements.set(0, List.of(List.of(reference)));
|
||||
segments.set(0, new X12Segment("UNH", 1, elements));
|
||||
segments.set(segments.size() - 1, X12Segment.of("UNT", segments.size(), String.valueOf(segments.size()), reference));
|
||||
messages.add(new EdifactMessage(segments.getFirst(), segments.getLast(), List.copyOf(segments), List.of()));
|
||||
}
|
||||
var oldTransactions = com.kingsrook.qbits.edi.storage.EdiRecords.query("edi_transaction", com.kingsrook.qbits.edi.storage.EdiRecords.equal("interchangeId", oldInterchange.getValue("id")));
|
||||
QRecord first = oldTransactions.getFirst();
|
||||
QRecord document = first.getValue("agreementDocumentId") == null ? null : OutboundWorker.row("edi_agreement_document", first.getValue("agreementDocumentId"));
|
||||
QRecord queued = envelope(agreement, document, null, List.of(), messages, oldInterchange.getValueInteger("id"), null);
|
||||
var newTransactions = com.kingsrook.qbits.edi.storage.EdiRecords.query("edi_transaction", com.kingsrook.qbits.edi.storage.EdiRecords.equal("interchangeId", queued.getValue("interchangeId")));
|
||||
for(int i = 0; i < archive.allMessages().size(); i++)
|
||||
{
|
||||
String reference = archive.allMessages().get(i).reference();
|
||||
QRecord old = oldTransactions.stream().filter(t -> reference.equals(t.getValueString("controlNumber"))).findFirst().orElseThrow();
|
||||
QRecord update = new QRecord().withValue("id", newTransactions.get(i).getValue("id"));
|
||||
for(String field : List.of("guideId", "guideRevision", "mapId", "mapRevision", "businessKey", "phiBusinessKey", "containsPhi", "businessKeyType", "secondaryKey", "targetTableName", "targetRecordId", "agreementDocumentId"))
|
||||
{
|
||||
update.withValue(field, old.getValue(field));
|
||||
}
|
||||
SystemWrites.update("edi_transaction", update);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
/*******************************************************************************
|
||||
** UNA service characters; the fifth character is reserved in syntax version 3.
|
||||
*******************************************************************************/
|
||||
public record EdifactSeparators(char component, char element, char decimal, char release, char repetition, char segment)
|
||||
{
|
||||
public static final EdifactSeparators DEFAULT = new EdifactSeparators(':', '+', '.', '?', ' ', '\'');
|
||||
|
||||
/*******************************************************************************
|
||||
** Ambiguous service characters cannot be safely parsed or escaped.
|
||||
*******************************************************************************/
|
||||
public EdifactSeparators
|
||||
{
|
||||
var values = List.of(component, element, decimal, release, repetition, segment);
|
||||
if(!List.of('.', ',').contains(decimal) || new HashSet<>(values).size() != 6 || values.stream().anyMatch(c -> c > 127 || Character.isISOControl(c)))
|
||||
{
|
||||
throw new IllegalArgumentException("Invalid EDIFACT service characters");
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Read exactly six UNA service characters.
|
||||
*******************************************************************************/
|
||||
public static EdifactSeparators fromUna(String una)
|
||||
{
|
||||
if(una.length() != 9 || !una.startsWith("UNA"))
|
||||
{
|
||||
throw new IllegalArgumentException("Invalid UNA");
|
||||
}
|
||||
return (new EdifactSeparators(una.charAt(3), una.charAt(4), una.charAt(5), una.charAt(6), una.charAt(7), una.charAt(8)));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Use the agreement's EDIFACT service characters.
|
||||
*******************************************************************************/
|
||||
public static EdifactSeparators fromAgreement(QRecord agreement)
|
||||
{
|
||||
return (new EdifactSeparators(character(agreement, "componentSeparator", ":"), character(agreement, "elementSeparator", "+"),
|
||||
character(agreement, "decimalMark", "."), character(agreement, "releaseCharacter", "?"),
|
||||
"4".equals(agreement.getValueString("interchangeVersion")) ? character(agreement, "repetitionSeparator", "*") : ' ', character(agreement, "segmentTerminator", "'")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** A separator setting must contain exactly one character.
|
||||
*******************************************************************************/
|
||||
private static char character(QRecord agreement, String field, String fallback)
|
||||
{
|
||||
String value = Objects.requireNonNullElse(agreement.getValueString(field), fallback);
|
||||
if(value.length() != 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Invalid service character setting");
|
||||
}
|
||||
return (value.charAt(0));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Render service string advice, including its segment terminator.
|
||||
*******************************************************************************/
|
||||
public String una()
|
||||
{
|
||||
return ("UNA" + component + element + decimal + release + repetition + segment);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi.edifact;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
|
||||
/*******************************************************************************
|
||||
** In-memory EDIFACT serialization; escape values, never composite structure.
|
||||
*******************************************************************************/
|
||||
public class EdifactWriter
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Render a full interchange with explicit UNA advice.
|
||||
*******************************************************************************/
|
||||
public String write(EdifactInterchange interchange)
|
||||
{
|
||||
return (write(interchange, ""));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Retain optional groups and caller-selected line endings.
|
||||
*******************************************************************************/
|
||||
public String write(EdifactInterchange interchange, String ending)
|
||||
{
|
||||
List<X12Segment> segments = new ArrayList<>();
|
||||
segments.add(interchange.unb());
|
||||
interchange.messages().forEach(m -> segments.addAll(m.segments()));
|
||||
for(var group : interchange.groups())
|
||||
{
|
||||
segments.add(group.ung());
|
||||
group.messages().forEach(m -> segments.addAll(m.segments()));
|
||||
segments.add(group.une());
|
||||
}
|
||||
segments.add(interchange.unz());
|
||||
return (interchange.separators().una() + ending + write(segments, interchange.separators(), ending));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Serialize component and repetition lists without losing released delimiters.
|
||||
*******************************************************************************/
|
||||
public String write(List<X12Segment> segments, EdifactSeparators separators, String ending)
|
||||
{
|
||||
if(!List.of("", "\n", "\r\n").contains(ending))
|
||||
{
|
||||
throw new IllegalArgumentException("Unsupported line ending");
|
||||
}
|
||||
StringBuilder out = new StringBuilder();
|
||||
for(var segment : segments)
|
||||
{
|
||||
out.append(segment.id());
|
||||
for(var element : segment.elements())
|
||||
{
|
||||
out.append(separators.element());
|
||||
for(int r = 0; r < element.size(); r++)
|
||||
{
|
||||
if(r > 0)
|
||||
{
|
||||
if(separators.repetition() == ' ')
|
||||
{
|
||||
throw new IllegalArgumentException("Repetition requires syntax version 4");
|
||||
}
|
||||
out.append(separators.repetition());
|
||||
}
|
||||
for(int c = 0; c < element.get(r).size(); c++)
|
||||
{
|
||||
if(c > 0)
|
||||
{
|
||||
out.append(separators.component());
|
||||
}
|
||||
for(char value : element.get(r).get(c).toCharArray())
|
||||
{
|
||||
if(value == separators.release() || value == separators.element() || value == separators.component()
|
||||
|| value == separators.segment() || separators.repetition() != ' ' && value == separators.repetition())
|
||||
{
|
||||
out.append(separators.release());
|
||||
}
|
||||
out.append(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.append(separators.segment()).append(ending);
|
||||
}
|
||||
return (out.toString());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Construct a segment from explicit component lists, with no delimiter parsing.
|
||||
*******************************************************************************/
|
||||
@SafeVarargs
|
||||
public static X12Segment segment(String tag, int position, List<String>... elements)
|
||||
{
|
||||
return (new X12Segment(tag, position, java.util.Arrays.stream(elements).map(List::of).toList()));
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ import java.util.List;
|
||||
import com.kingsrook.qbits.edi.EdiRuntime;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qqq.backend.core.alerts.NotificationAudience;
|
||||
import com.kingsrook.qqq.backend.core.alerts.NotificationSignal;
|
||||
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.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@@ -54,16 +57,33 @@ public final class EdiJournal
|
||||
filter.withCriteria(pair[0].toString(), pair[1] == null ? QCriteriaOperator.IS_BLANK : QCriteriaOperator.EQUALS, (Integer) pair[1]);
|
||||
}
|
||||
List<QRecord> matches = EdiRecords.query("edi_exception", filter);
|
||||
if(matches.isEmpty())
|
||||
QRecord exception;
|
||||
boolean opened = matches.isEmpty();
|
||||
if(opened)
|
||||
{
|
||||
SystemWrites.insert("edi_exception", new QRecord().withValue("inboxId", inbox).withValue("interchangeId", interchange).withValue("transactionId", transaction)
|
||||
exception = SystemWrites.insert("edi_exception", new QRecord().withValue("inboxId", inbox).withValue("interchangeId", interchange).withValue("transactionId", transaction)
|
||||
.withValue("partnerId", partner).withValue("type", type).withValue("summary", summary).withValue("status", "OPEN").withValue("occurrences", 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
QRecord row = matches.getFirst();
|
||||
exception = row;
|
||||
opened = List.of("RESOLVED", "IGNORED").contains(row.getValueString("status"));
|
||||
SystemWrites.update("edi_exception", new QRecord().withValue("id", row.getValue("id")).withValue("occurrences", row.getValueInteger("occurrences") + 1).withValue("status", "OPEN"));
|
||||
}
|
||||
if(opened && partner != null && List.of("REJECTED_ACK", "DEAD_LETTER").contains(type))
|
||||
{
|
||||
String[] assignees = EdiRecords.query("edi_exception", EdiRecords.equal("partnerId", partner)
|
||||
.withCriteria("status", QCriteriaOperator.IN, "OPEN", "IN_PROGRESS")).stream()
|
||||
.map(row -> row.getValueString("assignedToUserId")).filter(user -> user != null && !user.isBlank()).distinct().toArray(String[]::new);
|
||||
if(assignees.length > 0)
|
||||
{
|
||||
QAlerts.notify(new NotificationSignal("EDI exception opened")
|
||||
.withBody("An acknowledgement rejection or dead-letter exception needs attention.")
|
||||
.withAudience(NotificationAudience.users(assignees))
|
||||
.withSourceName("edi").withSourceTableName("edi_exception").withSourceRecordId(exception.getValueString("id")));
|
||||
}
|
||||
}
|
||||
event(inbox, interchange, transaction, "EXCEPTION");
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,7 @@ public class InboundProcessor
|
||||
if(EdiRecords.query("edi_outbound_message", EdiRecords.equal("ackForGroupId", groupRow.getValue("id"))).isEmpty())
|
||||
{
|
||||
AckLevel level = AckLevel.valueOf(Objects.requireNonNullElse(config.getValueString("ackLevel"), "ERRORS_ONLY"));
|
||||
boolean hipaa = Boolean.TRUE.equals(config.getValueBoolean("containsPhi")) && group.gs().value(8).startsWith("005010X");
|
||||
boolean hipaa = group.gs().value(8).startsWith("005010X") || "999".equals(config.getValueString("ackExpectation"));
|
||||
var ack = hipaa ? runtime.ack999().build(group, findings, level, "0001") : runtime.ack997().build(group, findings, level, "0001");
|
||||
queue(AckEnvelope.wrap(x12, group, ack, control(agreement, "ISA"), control(agreement, "GS"), LocalDateTime.now(runtime.config().getClock())),
|
||||
agreement, id, groupRow.getValueInteger("id"), hipaa ? "999" : "997");
|
||||
@@ -261,6 +261,7 @@ public class InboundProcessor
|
||||
.withValue("receiverQualifier", x12.isa().value(7)).withValue("receiverId", x12.isa().value(8).trim()).withValue("controlNumber", x12.isa().value(13))
|
||||
.withValue("interchangeVersion", x12.isa().value(12)).withValue("usageIndicator", x12.isa().value(15)).withValue("groupCount", x12.groups().size())
|
||||
.withValue("payloadRef", inbox.getValue("payloadRef")).withValue("payloadSha256", inbox.getValue("payloadSha256")).withValue("sizeBytes", inbox.getValue("sizeBytes"))
|
||||
.withValue("containsPhi", x12.groups().stream().anyMatch(g -> g.gs().value(8).startsWith("005010X")))
|
||||
.withValue("status", duplicate ? "DUPLICATE" : x12.findings().isEmpty() ? "ACCEPTED" : "REJECTED"));
|
||||
EdiJournal.event(inbox.getValueInteger("id"), row.getValueInteger("id"), null, "PARSED");
|
||||
return (row);
|
||||
@@ -307,14 +308,14 @@ public class InboundProcessor
|
||||
return (SystemWrites.insert("edi_transaction", new QRecord().withValue("inboundPosition", position).withValue("interchangeId", interchange.getValue("id")).withValue("functionalGroupId", group.getValue("id"))
|
||||
.withValue("direction", "INBOUND").withValue("partnerId", interchange.getValue("partnerId")).withValue("transactionSet", tx.st().value(1))
|
||||
.withValue("version", group.getValue("version")).withValue("controlNumber", tx.st().value(2)).withValue("segmentCount", tx.segments().size())
|
||||
.withValue("agreementDocumentId", document == null ? null : document.getValue("id")).withValue("containsPhi", document != null && Boolean.TRUE.equals(document.getValueBoolean("containsPhi")))
|
||||
.withValue("agreementDocumentId", document == null ? null : document.getValue("id")).withValue("containsPhi", group.getValueString("version").startsWith("005010X") || document != null && Boolean.TRUE.equals(document.getValueBoolean("containsPhi")))
|
||||
.withValue("status", "RECEIVED")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** 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)
|
||||
{
|
||||
@@ -352,7 +353,7 @@ public class InboundProcessor
|
||||
List<ValidationFinding> findings = new ArrayList<>();
|
||||
if(document != null && document.getValueInteger("guideId") != null)
|
||||
{
|
||||
findings.addAll(runtime.validator().validate(tx, new GuideRepository().load(document.getValueInteger("guideId")), Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))));
|
||||
findings.addAll(runtime.validator().validate(tx, new GuideRepository().load(document.getValueInteger("guideId")), Boolean.TRUE.equals(document.getValueBoolean("containsPhi")) || group.gs().value(8).startsWith("005010X")));
|
||||
int snip = Objects.requireNonNullElse(document.getValueInteger("snipLevel"), 2);
|
||||
findings.removeIf(f -> f.snipLevel() > snip);
|
||||
}
|
||||
@@ -382,7 +383,7 @@ public class InboundProcessor
|
||||
/*******************************************************************************
|
||||
** Persist findings without repeatedly adding error rows on storage retries.
|
||||
*******************************************************************************/
|
||||
private void validation(QRecord tx, List<ValidationFinding> findings, QRecord document) throws Exception
|
||||
protected void validation(QRecord tx, List<ValidationFinding> findings, QRecord document) throws Exception
|
||||
{
|
||||
boolean rejected = findings.stream().anyMatch(f -> "ERROR".equals(f.severity()) && f.snipLevel() <= 2);
|
||||
if(!"APPLIED".equals(tx.getValueString("status")) || Boolean.TRUE.equals(inbox.getValueBoolean("reprocess")))
|
||||
@@ -420,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")))
|
||||
{
|
||||
@@ -432,7 +433,7 @@ public class InboundProcessor
|
||||
return;
|
||||
}
|
||||
String[] stage = {"MAPPING"};
|
||||
try
|
||||
try(var logging = com.kingsrook.qbits.edi.security.PhiLogScope.open(Boolean.TRUE.equals(tx.getValueBoolean("containsPhi"))))
|
||||
{
|
||||
InboxWorker.fenced(inbox, lease, () ->
|
||||
{
|
||||
@@ -447,7 +448,7 @@ public class InboundProcessor
|
||||
stage[0] = "APPLY";
|
||||
QRecord target = InboundApplier.asPartner(runtime.config().getServiceSessionSupplier(), agreement.getValueInteger("partnerId"), () -> new InboundApplier().apply(map, mapped.record()));
|
||||
String primaryKey = com.kingsrook.qqq.backend.core.context.QContext.getQInstance().getTable(map.getValueString("tableName")).getPrimaryKeyField();
|
||||
boolean phi = Boolean.TRUE.equals(document.getValueBoolean("containsPhi"));
|
||||
boolean phi = Boolean.TRUE.equals(tx.getValueBoolean("containsPhi"));
|
||||
SystemWrites.update("edi_transaction", new QRecord().withValue("id", tx.getValue("id")).withValue("status", "APPLIED")
|
||||
.withValue("mapId", map.getValue("id")).withValue("mapRevision", map.getValue("revision")).withValue("businessKeyType", map.getValue("businessKeyType"))
|
||||
.withValue(phi ? "phiBusinessKey" : "businessKey", mapped.businessKey()).withValue("secondaryKey", phi ? null : mapped.secondaryKey())
|
||||
|
||||
@@ -88,11 +88,22 @@ public class InboxWorker
|
||||
}
|
||||
try(InputStream stream = store.open(new PayloadRef(row.getValueString("payloadRef"), row.getValueString("payloadSha256"), row.getValueLong("sizeBytes"))))
|
||||
{
|
||||
var interchanges = EdiRuntime.current().reader().read(stream);
|
||||
var framing = new com.kingsrook.qbits.edi.document.EnvelopeReader();
|
||||
byte[] envelope;
|
||||
int position = 0;
|
||||
for(var interchange : interchanges)
|
||||
while((envelope = framing.next(stream)) != null)
|
||||
{
|
||||
new InboundProcessor(row, lease, store).process(interchange, ++position);
|
||||
position++;
|
||||
if(envelope[0] == 'U')
|
||||
{
|
||||
var parsed = new com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader(EdiRuntime.current().config().getMaxSegmentsPerTransaction()).read(new java.io.ByteArrayInputStream(envelope)).getFirst();
|
||||
new com.kingsrook.qbits.edi.edifact.EdifactInbound(row, lease, store).process(parsed, position);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parsed = EdiRuntime.current().reader().read(new java.io.ByteArrayInputStream(envelope)).getFirst();
|
||||
new InboundProcessor(row, lease, store).process(parsed, position);
|
||||
}
|
||||
}
|
||||
fenced(row, lease, () ->
|
||||
{
|
||||
|
||||
@@ -73,6 +73,11 @@ public final class OutboundActions
|
||||
*******************************************************************************/
|
||||
private static void reEnvelope(QRecord message) throws Exception
|
||||
{
|
||||
if("EDIFACT".equals(OutboundWorker.row("edi_agreement", message.getValue("agreementId")).getValueString("standard")))
|
||||
{
|
||||
com.kingsrook.qbits.edi.edifact.EdifactOutbound.resendAsNew(message);
|
||||
return;
|
||||
}
|
||||
var runtime = EdiRuntime.current();
|
||||
QRecord originalRow = OutboundWorker.row("edi_interchange", message.getValue("interchangeId"));
|
||||
QRecord agreement = OutboundWorker.row("edi_agreement", message.getValue("agreementId"));
|
||||
|
||||
@@ -93,7 +93,7 @@ public class OutboundWorker
|
||||
return (0);
|
||||
}
|
||||
List<QRecord> accepted = new ArrayList<>();
|
||||
List<X12TransactionSet> sets = new ArrayList<>();
|
||||
List<com.kingsrook.qbits.edi.document.EdiDocument> sets = new ArrayList<>();
|
||||
QRecord map = row("edi_map", document.getValueInteger("mapId"));
|
||||
var rules = EdiRecords.query("edi_map_rule", EdiRecords.equal("mapId", map.getValue("id")));
|
||||
var associations = new java.util.LinkedHashSet<String>();
|
||||
@@ -115,9 +115,9 @@ public class OutboundWorker
|
||||
{
|
||||
continue;
|
||||
}
|
||||
try
|
||||
try(var logging = com.kingsrook.qbits.edi.security.PhiLogScope.open(Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))))
|
||||
{
|
||||
X12TransactionSet set = InboundApplier.asPartner(EdiRuntime.current().config().getServiceSessionSupplier(), agreement.getValueInteger("partnerId"), () ->
|
||||
com.kingsrook.qbits.edi.document.EdiDocument set = InboundApplier.asPartner(EdiRuntime.current().config().getServiceSessionSupplier(), agreement.getValueInteger("partnerId"), () ->
|
||||
{
|
||||
String table = request.getValueString("sourceTableName");
|
||||
String primaryKey = QContext.getQInstance().getTable(table).getPrimaryKeyField();
|
||||
@@ -129,7 +129,9 @@ public class OutboundWorker
|
||||
{
|
||||
throw new IllegalArgumentException("Source record or active map unavailable");
|
||||
}
|
||||
return (new OutboundMapper().build(records.getFirst(), map, rules, String.format("%04d", sets.size() + 1)));
|
||||
return ("EDIFACT".equals(agreement.getValueString("standard"))
|
||||
? new com.kingsrook.qbits.edi.edifact.EdifactMapper(com.kingsrook.qbits.edi.edifact.EdifactSeparators.fromAgreement(agreement).decimal()).message(records.getFirst(), map, rules, com.kingsrook.qbits.edi.edifact.EdifactOutbound.allocate(agreement, "UNH"))
|
||||
: new OutboundMapper().build(records.getFirst(), map, rules, String.format("%04d", sets.size() + 1)));
|
||||
});
|
||||
if(EdiRuntime.current().validator().validate(set, guide, Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))).stream().anyMatch(f -> "ERROR".equals(f.severity())))
|
||||
{
|
||||
@@ -170,8 +172,13 @@ public class OutboundWorker
|
||||
/*******************************************************************************
|
||||
** Allocate controls once, store exact bytes, then persist all tracking records.
|
||||
*******************************************************************************/
|
||||
public static QRecord envelope(QRecord agreement, QRecord document, QRecord map, List<QRecord> requests, List<X12TransactionSet> sets, Integer original) throws Exception
|
||||
public static QRecord envelope(QRecord agreement, QRecord document, QRecord map, List<QRecord> requests, List<? extends com.kingsrook.qbits.edi.document.EdiDocument> sets, Integer original) throws Exception
|
||||
{
|
||||
if("EDIFACT".equals(agreement.getValueString("standard")))
|
||||
{
|
||||
return (com.kingsrook.qbits.edi.edifact.EdifactOutbound.envelope(agreement, document, map, requests,
|
||||
sets.stream().map(s -> (com.kingsrook.qbits.edi.edifact.EdifactMessage) s).toList(), original, null));
|
||||
}
|
||||
var runtime = EdiRuntime.current();
|
||||
QRecord local = row("edi_identity", agreement.getValueInteger("localIdentityId"));
|
||||
QRecord partner = row("edi_identity", agreement.getValueInteger("partnerIdentityId"));
|
||||
@@ -187,7 +194,7 @@ public class OutboundWorker
|
||||
X12Segment gs = X12Segment.of("GS", 2, document.getValueString("functionalIdCode"), local.getValueString("applicationCode"), partner.getValueString("applicationCode"), at.format(DateTimeFormatter.BASIC_ISO_DATE),
|
||||
at.format(DateTimeFormatter.ofPattern("HHmm")), gsControl, "X", document.getValueString("version"));
|
||||
var interchange = new X12Interchange(new X12Separators(value(agreement, "elementSeparator", "*").charAt(0), value(agreement, "segmentTerminator", "~").charAt(0), component.charAt(0), version.compareTo("00501") >= 0 ? repetition.charAt(0) : null),
|
||||
isa, X12Segment.of("IEA", 0, "1", isaControl), List.of(new X12FunctionalGroup(gs, X12Segment.of("GE", 0, String.valueOf(sets.size()), gsControl), List.copyOf(sets), List.of())), List.of(), List.of());
|
||||
isa, X12Segment.of("IEA", 0, "1", isaControl), List.of(new X12FunctionalGroup(gs, X12Segment.of("GE", 0, String.valueOf(sets.size()), gsControl), sets.stream().map(s -> (X12TransactionSet) s).toList(), List.of())), List.of(), List.of());
|
||||
String ending = switch(value(agreement, "lineEnding", "NONE"))
|
||||
{
|
||||
case "LF" -> "\n";
|
||||
@@ -211,7 +218,7 @@ public class OutboundWorker
|
||||
String businessKey = map.getValueString("businessKeyPath") == null ? null : new com.kingsrook.qbits.edi.guide.RuleEvaluator(set.segments(), java.util.Map.of()).values(map.getValueString("businessKeyPath")).stream().findFirst().orElse(null);
|
||||
SystemWrites.insert("edi_transaction", new QRecord().withValue("interchangeId", persisted.getValue("id")).withValue("functionalGroupId", group.getValue("id"))
|
||||
.withValue("direction", "OUTBOUND").withValue("partnerId", agreement.getValue("partnerId")).withValue("agreementDocumentId", document.getValue("id"))
|
||||
.withValue("transactionSet", set.st().value(1)).withValue("version", gs.value(8)).withValue("controlNumber", set.st().value(2)).withValue("status", "QUEUED")
|
||||
.withValue("transactionSet", set.header().value(1)).withValue("version", gs.value(8)).withValue("controlNumber", set.header().value(2)).withValue("status", "QUEUED")
|
||||
.withValue("guideId", document.getValue("guideId")).withValue("guideRevision", document.getValue("guideId") == null ? null : row("edi_guide", document.getValue("guideId")).getValue("revision"))
|
||||
.withValue("targetTableName", i < requests.size() ? requests.get(i).getValue("sourceTableName") : null).withValue("targetRecordId", i < requests.size() ? requests.get(i).getValue("sourceRecordId") : null).withValue("mapId", map.getValue("id")).withValue("mapRevision", map.getValue("revision"))
|
||||
.withValue("businessKeyType", map.getValue("businessKeyType")).withValue(Boolean.TRUE.equals(document.getValueBoolean("containsPhi")) ? "phiBusinessKey" : "businessKey", businessKey)
|
||||
|
||||
@@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
*******************************************************************************/
|
||||
public class BuiltInGuideLoader
|
||||
{
|
||||
public static final List<String> RESOURCES = List.of("850-004010", "850-005010", "997-004010", "997-005010", "999-005010X231A1", "855-004010", "855-005010", "810-004010", "810-005010", "846-004010", "846-005010", "860-004010", "860-005010", "856-004010", "856-005010", "940-004010", "940-005010", "945-004010", "945-005010", "214-004010", "214-005010", "204-004010", "204-005010");
|
||||
public static final List<String> RESOURCES = List.of("850-004010", "850-005010", "997-004010", "997-005010", "999-005010X231A1", "855-004010", "855-005010", "810-004010", "810-005010", "846-004010", "846-005010", "860-004010", "860-005010", "856-004010", "856-005010", "940-004010", "940-005010", "945-004010", "945-005010", "214-004010", "214-005010", "204-004010", "204-005010", "ORDERS-D96A", "ORDRSP-D96A", "DESADV-D96A", "INVOIC-D96A", "CONTRL-D96A", "ORDERS-D01B", "ORDRSP-D01B", "DESADV-D01B", "INVOIC-D01B", "CONTRL-D01B", "837P-005010X222A1", "837I-005010X223A2", "835-005010X221A1", "270-005010X279A1", "271-005010X279A1", "276-005010X212", "277-005010X212", "834-005010X220A1");
|
||||
|
||||
/*******************************************************************************
|
||||
** Read the documented JSON format in memory.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.guide;
|
||||
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Host-owned, licensed code sets. Implementations must not log submitted values.
|
||||
*******************************************************************************/
|
||||
@FunctionalInterface
|
||||
public interface ExternalCodeSetProvider
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Check membership on the document's effective service or payment date.
|
||||
*******************************************************************************/
|
||||
boolean isValid(String codeSet, String code, LocalDate asOf);
|
||||
}
|
||||
@@ -27,6 +27,13 @@ import java.util.List;
|
||||
/*******************************************************************************
|
||||
** An element or composite component constraint.
|
||||
*******************************************************************************/
|
||||
public record GuideElement(Integer position, Integer componentPosition, String elementId, String dataType, Integer minLength, Integer maxLength, String usage, List<String> codes, String fixedValue, Integer repeatMax, String condition)
|
||||
public record GuideElement(Integer position, Integer componentPosition, String elementId, String dataType, Integer minLength, Integer maxLength, String usage, List<String> codes, String fixedValue, Integer repeatMax, String condition, String source, String externalCodeSet)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Preserve the original constructor for host-authored inline constraints.
|
||||
*******************************************************************************/
|
||||
public GuideElement(Integer position, Integer componentPosition, String elementId, String dataType, Integer minLength, Integer maxLength, String usage, List<String> codes, String fixedValue, Integer repeatMax, String condition)
|
||||
{
|
||||
this(position, componentPosition, elementId, dataType, minLength, maxLength, usage, codes, fixedValue, repeatMax, condition, null, null);
|
||||
}
|
||||
}
|
||||
@@ -47,22 +47,37 @@ public class GuideRepository
|
||||
*******************************************************************************/
|
||||
public Guide load(Integer id) throws Exception
|
||||
{
|
||||
QRecord guide = rows("edi_guide", "id", id).getFirst();
|
||||
List<QRecord> definitions = new ArrayList<>(rows("edi_guide_segment", "guideId", id));
|
||||
return (load(id, false));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** UI preview explicitly enforces every guide read, independently of host defaults.
|
||||
*******************************************************************************/
|
||||
public Guide load(Integer id, boolean enforcePermissions) throws Exception
|
||||
{
|
||||
var guideRows = rows("edi_guide", "id", id, enforcePermissions);
|
||||
if(guideRows.isEmpty())
|
||||
{
|
||||
throw (new com.kingsrook.qqq.backend.core.exceptions.QNotFoundException("Not found"));
|
||||
}
|
||||
QRecord guide = guideRows.getFirst();
|
||||
List<QRecord> definitions = new ArrayList<>(rows("edi_guide_segment", "guideId", id, enforcePermissions));
|
||||
definitions.sort(Comparator.comparing(r -> r.getValueInteger("sequence")));
|
||||
List<GuideSegment> segments = new ArrayList<>();
|
||||
List<QRecord> rules = rows("edi_guide_rule", "guideId", id);
|
||||
List<QRecord> rules = rows("edi_guide_rule", "guideId", id, enforcePermissions);
|
||||
for(QRecord definition : definitions)
|
||||
{
|
||||
Integer segmentId = definition.getValueInteger("id");
|
||||
List<GuideElement> elements = new ArrayList<>();
|
||||
for(QRecord element : rows("edi_guide_element", "guideSegmentId", segmentId))
|
||||
for(QRecord element : rows("edi_guide_element", "guideSegmentId", segmentId, enforcePermissions))
|
||||
{
|
||||
List<String> codes = null;
|
||||
QRecord codeList = null;
|
||||
if(element.getValueInteger("codeListId") != null)
|
||||
{
|
||||
codeList = rows("edi_code_list", "id", element.getValueInteger("codeListId"), enforcePermissions).getFirst();
|
||||
codes = new ArrayList<>();
|
||||
for(QRecord code : rows("edi_code_list_value", "codeListId", element.getValueInteger("codeListId")))
|
||||
for(QRecord code : rows("edi_code_list_value", "codeListId", element.getValueInteger("codeListId"), enforcePermissions))
|
||||
{
|
||||
if(Boolean.TRUE.equals(code.getValueBoolean("isActive")))
|
||||
{
|
||||
@@ -72,7 +87,7 @@ public class GuideRepository
|
||||
}
|
||||
elements.add(new GuideElement(element.getValueInteger("position"), element.getValueInteger("componentPosition"), element.getValueString("elementId"),
|
||||
element.getValueString("dataType"), element.getValueInteger("minLength"), element.getValueInteger("maxLength"), element.getValueString("usage"), codes,
|
||||
element.getValueString("fixedValue"), element.getValueInteger("repeatMax"), condition(rules, element.getValueInteger("situationalRuleId"))));
|
||||
element.getValueString("fixedValue"), element.getValueInteger("repeatMax"), condition(rules, element.getValueInteger("situationalRuleId")), codeList == null ? null : codeList.getValueString("source"), codeList == null ? null : codeList.getValueString("externalCodeSet")));
|
||||
}
|
||||
List<GuideRule> segmentRules = rules.stream().filter(r -> segmentId.equals(r.getValueInteger("guideSegmentId")) && !"condition".equals(r.getValueString("message"))).map(GuideRepository::rule).toList();
|
||||
segments.add(new GuideSegment(definition.getValueString("segmentId"), definition.getValueString("loopId"), definition.getValueString("parentLoopId"),
|
||||
@@ -107,11 +122,11 @@ public class GuideRepository
|
||||
for(GuideElement element : list(segment.elements()))
|
||||
{
|
||||
Integer codeListId = null;
|
||||
if(element.codes() != null)
|
||||
if(element.codes() != null || "EXTERNAL".equals(element.source()))
|
||||
{
|
||||
codeListId = insert("edi_code_list", new QRecord().withValue("name", guide.name() + "/" + revision + "/" + sequence + "/" + element.position())
|
||||
.withValue("elementId", element.elementId()).withValue("source", "BUILT_IN"), transaction).getValueInteger("id");
|
||||
for(String code : element.codes())
|
||||
.withValue("elementId", element.elementId()).withValue("source", element.source() == null ? "BUILT_IN" : element.source()).withValue("externalCodeSet", element.externalCodeSet()), transaction).getValueInteger("id");
|
||||
for(String code : list(element.codes()))
|
||||
{
|
||||
insert("edi_code_list_value", new QRecord().withValue("codeListId", codeListId).withValue("code", code).withValue("isActive", true), transaction);
|
||||
}
|
||||
@@ -204,9 +219,9 @@ public class GuideRepository
|
||||
/*******************************************************************************
|
||||
** Query normalized child records.
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> rows(String table, String field, Integer id) throws Exception
|
||||
private static List<QRecord> rows(String table, String field, Integer id, boolean enforcePermissions) throws Exception
|
||||
{
|
||||
return (EdiRecords.query(table, EdiRecords.equal(field, id)));
|
||||
return (enforcePermissions ? EdiRecords.queryEnforced(table, EdiRecords.equal(field, id)) : EdiRecords.query(table, EdiRecords.equal(field, id)));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -34,31 +34,31 @@ import com.kingsrook.qbits.edi.x12.X12TransactionSet;
|
||||
*******************************************************************************/
|
||||
public class GuideValidator
|
||||
{
|
||||
private final com.kingsrook.qbits.edi.EdiQBitConfig config;
|
||||
|
||||
/*******************************************************************************
|
||||
** Standalone validation uses built-in formats without a licensed provider.
|
||||
*******************************************************************************/
|
||||
public GuideValidator()
|
||||
{
|
||||
this(new com.kingsrook.qbits.edi.EdiQBitConfig());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Read the host provider at validation time, allowing runtime registration.
|
||||
*******************************************************************************/
|
||||
public GuideValidator(com.kingsrook.qbits.edi.EdiQBitConfig config)
|
||||
{
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate one transaction; diagnostic text never contains submitted values.
|
||||
*******************************************************************************/
|
||||
public List<ValidationFinding> validate(X12TransactionSet transaction, Guide guide, boolean containsPhi)
|
||||
public List<ValidationFinding> validate(EdiDocument transaction, Guide guide, boolean containsPhi)
|
||||
{
|
||||
State state = new State(transaction.segments(), containsPhi);
|
||||
List<Node> roots = new ArrayList<>();
|
||||
Map<String, Node> nodes = new LinkedHashMap<>();
|
||||
for(GuideSegment segment : guide.segments())
|
||||
{
|
||||
List<Node> destination = roots;
|
||||
if(segment.loopId() != null && !segment.loopId().isBlank())
|
||||
{
|
||||
Node loop = nodes.get(segment.loopId());
|
||||
if(loop == null)
|
||||
{
|
||||
loop = new Node(segment.loopId(), null);
|
||||
nodes.put(segment.loopId(), loop);
|
||||
Node parent = nodes.get(segment.parentLoopId());
|
||||
(parent == null ? roots : parent.children).add(loop);
|
||||
}
|
||||
destination = loop.children;
|
||||
}
|
||||
destination.add(new Node(null, segment));
|
||||
}
|
||||
State state = new State(transaction.segments(), containsPhi, "EDIFACT".equals(guide.standard()));
|
||||
List<Node> roots = tree(guide);
|
||||
consume(roots, state, false);
|
||||
while(state.index < state.segments.size())
|
||||
{
|
||||
@@ -86,48 +86,90 @@ 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)
|
||||
{
|
||||
GuideSegment definition = binding.definition();
|
||||
X12Segment segment = binding.segment();
|
||||
elements(segment, definition, evaluator, state);
|
||||
external(segment, definition, state);
|
||||
for(GuideRule rule : list(definition.rules()))
|
||||
{
|
||||
rule(rule, segment, definition.loopId(), evaluator, state);
|
||||
rule(rule, segment, definition.loopId(), "SITUATIONAL".equals(rule.type()) && definition.loopId() != null ? scoped(binding, state) : evaluator, state);
|
||||
}
|
||||
}
|
||||
if(guide.version().startsWith("005010X"))
|
||||
{
|
||||
state.findings.addAll(new HealthcareValidation().validate(transaction, guide, state.loops));
|
||||
}
|
||||
return (List.copyOf(state.findings));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve guide-defined loop occurrences for the mapper using the validator grammar.
|
||||
*******************************************************************************/
|
||||
public Map<String, List<List<X12Segment>>> loops(X12TransactionSet transaction, Guide guide)
|
||||
public Map<String, List<List<X12Segment>>> loops(EdiDocument transaction, Guide guide)
|
||||
{
|
||||
State state = new State(transaction.segments(), true, "EDIFACT".equals(guide.standard()));
|
||||
List<Node> roots = tree(guide);
|
||||
consume(roots, state, false);
|
||||
return (state.loops);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** The same loop name can occur under subscriber and dependent patient parents.
|
||||
*******************************************************************************/
|
||||
private static List<Node> tree(Guide guide)
|
||||
{
|
||||
State state = new State(transaction.segments(), true);
|
||||
List<Node> roots = new ArrayList<>();
|
||||
Map<String, Node> nodes = new LinkedHashMap<>();
|
||||
Map<String, Node> latest = new LinkedHashMap<>();
|
||||
for(GuideSegment segment : guide.segments())
|
||||
{
|
||||
List<Node> destination = roots;
|
||||
if(segment.loopId() != null && !segment.loopId().isBlank())
|
||||
{
|
||||
Node loop = nodes.get(segment.loopId());
|
||||
Node parent = latest.get(segment.parentLoopId());
|
||||
List<Node> siblings = parent == null ? roots : parent.children;
|
||||
Node loop = siblings.stream().filter(n -> segment.loopId().equals(n.loopId)).findFirst().orElse(null);
|
||||
if(loop == null)
|
||||
{
|
||||
loop = new Node(segment.loopId(), null);
|
||||
nodes.put(segment.loopId(), loop);
|
||||
Node parent = nodes.get(segment.parentLoopId());
|
||||
(parent == null ? roots : parent.children).add(loop);
|
||||
siblings.add(loop);
|
||||
}
|
||||
latest.put(segment.loopId(), loop);
|
||||
destination = loop.children;
|
||||
}
|
||||
destination.add(new Node(null, segment));
|
||||
}
|
||||
consume(roots, state, false);
|
||||
return (state.loops);
|
||||
return (roots);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Situational rules see their occurrence plus direct ancestor segments, never siblings.
|
||||
*******************************************************************************/
|
||||
private static RuleEvaluator scoped(Binding binding, State state)
|
||||
{
|
||||
List<X12Segment> scope = state.loops.getOrDefault(binding.definition().loopId(), List.of()).stream()
|
||||
.filter(o -> o.contains(binding.segment())).min(java.util.Comparator.comparingInt(List::size)).orElse(state.segments);
|
||||
List<X12Segment> visible = new ArrayList<>(scope);
|
||||
for(List<X12Segment> ancestor : state.loops.values().stream().flatMap(List::stream).toList())
|
||||
{
|
||||
if(ancestor.size() > scope.size() && ancestor.contains(scope.getFirst()))
|
||||
{
|
||||
for(X12Segment segment : ancestor)
|
||||
{
|
||||
boolean nested = state.loops.values().stream().flatMap(List::stream)
|
||||
.anyMatch(o -> o.size() < ancestor.size() && ancestor.contains(o.getFirst()) && o.contains(segment));
|
||||
if(!nested && !visible.contains(segment))
|
||||
{
|
||||
visible.add(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visible.sort(java.util.Comparator.comparingInt(X12Segment::position));
|
||||
return (new RuleEvaluator(visible, state.loops));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -141,7 +183,7 @@ public class GuideValidator
|
||||
{
|
||||
GuideSegment head = node.children.getFirst().definition;
|
||||
int count = 0;
|
||||
while(state.matches(head.segmentId()))
|
||||
while(state.matches(head))
|
||||
{
|
||||
int start = state.index;
|
||||
consume(node.children, state, true);
|
||||
@@ -165,7 +207,7 @@ public class GuideValidator
|
||||
GuideSegment definition = node.definition;
|
||||
int count = 0;
|
||||
int maximum = number(definition.maxUse(), 1);
|
||||
while(state.matches(definition.segmentId()) && (count == 0 || !loopOccurrence || node != nodes.getFirst()))
|
||||
while(state.matches(definition.segmentId()) && (node != nodes.getFirst() || !loopOccurrence || state.matches(definition)) && (count == 0 || !loopOccurrence || node != nodes.getFirst()))
|
||||
{
|
||||
X12Segment segment = state.segments.get(state.index++);
|
||||
state.bindings.add(new Binding(segment, definition));
|
||||
@@ -229,13 +271,13 @@ 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);
|
||||
}
|
||||
if(element.fixedValue() != null && !element.fixedValue().equals(value)
|
||||
|| "ID".equals(element.dataType()) && element.codes() != null && !element.codes().contains(value))
|
||||
|| !"EXTERNAL".equals(element.source()) && "ID".equals(element.dataType()) && element.codes() != null && !element.codes().contains(value))
|
||||
{
|
||||
state.add(segment, definition.loopId(), element, "8", "7", "Invalid code or fixed value", value);
|
||||
}
|
||||
@@ -243,6 +285,47 @@ public class GuideValidator
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** External membership is SNIP 5 and is optional; formats are always built in.
|
||||
*******************************************************************************/
|
||||
private void external(X12Segment segment, GuideSegment definition, State state)
|
||||
{
|
||||
if(config.getExternalCodeSetProvider() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for(GuideElement element : list(definition.elements()))
|
||||
{
|
||||
if(!"EXTERNAL".equals(element.source()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int repetitions = element.position() <= segment.elements().size() ? segment.elements().get(element.position() - 1).size() : 1;
|
||||
for(int repetition = 1; repetition <= repetitions; repetition++)
|
||||
{
|
||||
String value = segment.value(element.position(), repetition, number(element.componentPosition(), 1));
|
||||
if(!value.isEmpty())
|
||||
{
|
||||
boolean valid;
|
||||
try(var logging = com.kingsrook.qbits.edi.security.PhiLogScope.open(state.phi))
|
||||
{
|
||||
valid = config.getExternalCodeSetProvider().isValid(element.externalCodeSet(), value,
|
||||
HealthcareValidation.asOf(state.segments, config.getClock()));
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
if(!valid)
|
||||
{
|
||||
state.findings.add(new ValidationFinding(segment.position(), segment.id(), definition.loopId(), element.position(), element.componentPosition(),
|
||||
5, "8", "7", "8", "7", "ERROR", "External code validation failed", null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate X12 lexical data types without coercing bad values.
|
||||
*******************************************************************************/
|
||||
@@ -370,6 +453,7 @@ public class GuideValidator
|
||||
{
|
||||
private final List<X12Segment> segments;
|
||||
private final boolean phi;
|
||||
private final boolean edifact;
|
||||
private final List<ValidationFinding> findings = new ArrayList<>();
|
||||
private final List<Binding> bindings = new ArrayList<>();
|
||||
private final Map<String, List<List<X12Segment>>> loops = new LinkedHashMap<>();
|
||||
@@ -378,10 +462,11 @@ public class GuideValidator
|
||||
/*******************************************************************************
|
||||
** Initialize a transaction validation.
|
||||
*******************************************************************************/
|
||||
private State(List<X12Segment> segments, boolean phi)
|
||||
private State(List<X12Segment> segments, boolean phi, boolean edifact)
|
||||
{
|
||||
this.segments = segments;
|
||||
this.phi = phi;
|
||||
this.edifact = edifact;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -392,6 +477,15 @@ public class GuideValidator
|
||||
return (index < segments.size() && segments.get(index).id().equals(id));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Fixed qualifiers distinguish adjacent HL and NM1 loop anchors.
|
||||
*******************************************************************************/
|
||||
private boolean matches(GuideSegment definition)
|
||||
{
|
||||
return (matches(definition.segmentId()) && list(definition.elements()).stream().filter(e -> e.fixedValue() != null)
|
||||
.allMatch(e -> e.fixedValue().equals(segments.get(index).value(e.position(), 1, number(e.componentPosition(), 1)))));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Locate a missing segment relative to the buffered transaction.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* 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.guide;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Original healthcare balancing and format checks, without licensed code content.
|
||||
*******************************************************************************/
|
||||
public class HealthcareValidation
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Exact decimal arithmetic; every diagnostic contains only positions and codes.
|
||||
*******************************************************************************/
|
||||
public List<ValidationFinding> validate(EdiDocument transaction, Guide guide, Map<String, List<List<X12Segment>>> loops)
|
||||
{
|
||||
List<ValidationFinding> findings = new ArrayList<>();
|
||||
List<X12Segment> segments = transaction.segments();
|
||||
if("837".equals(guide.transactionSet()))
|
||||
{
|
||||
for(List<X12Segment> claim : blocks(segments, "CLM"))
|
||||
{
|
||||
X12Segment head = claim.getFirst();
|
||||
try
|
||||
{
|
||||
BigDecimal lines = BigDecimal.ZERO;
|
||||
for(X12Segment line : claim)
|
||||
{
|
||||
if(line.id().equals("SV1") || line.id().equals("SV2"))
|
||||
{
|
||||
lines = lines.add(amount(line, line.id().equals("SV1") ? 2 : 3));
|
||||
}
|
||||
}
|
||||
if(amount(head, 2).compareTo(lines) != 0)
|
||||
{
|
||||
findings.add(finding(head, "2300", 2, null, 3, "Claim charges do not balance"));
|
||||
}
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
findings.add(finding(head, "2300", 2, null, 3, "Claim balance cannot be calculated"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if("835".equals(guide.transactionSet()))
|
||||
{
|
||||
remittance(segments, findings);
|
||||
}
|
||||
for(X12Segment segment : segments)
|
||||
{
|
||||
String loop = loops.entrySet().stream().filter(e -> e.getValue().stream().anyMatch(o -> o.contains(segment)))
|
||||
.min(java.util.Comparator.comparingInt(e -> e.getValue().stream().filter(o -> o.contains(segment)).mapToInt(List::size).min().orElse(Integer.MAX_VALUE)))
|
||||
.map(Map.Entry::getKey).orElse(null);
|
||||
if(segment.id().equals("NM1") && segment.value(8).equals("XX") && !validNpi(segment.value(9)))
|
||||
{
|
||||
findings.add(finding(segment, loop, 9, null, 5, "Invalid NPI format or check digit"));
|
||||
}
|
||||
if(segment.id().equals("N4") && !segment.value(3).isEmpty() && (segment.value(4).isEmpty() || segment.value(4).equals("US"))
|
||||
&& !segment.value(3).matches("[0-9]{5}([0-9]{4}|-[0-9]{4})?"))
|
||||
{
|
||||
findings.add(finding(segment, loop, 3, null, 5, "Invalid postal format"));
|
||||
}
|
||||
if(segment.id().equals("HI"))
|
||||
{
|
||||
for(int position = 1; position <= segment.elements().size(); position++)
|
||||
{
|
||||
for(int repetition = 1; repetition <= segment.elements().get(position - 1).size(); repetition++)
|
||||
{
|
||||
if(List.of("ABK", "ABF", "ABJ", "APR").contains(segment.value(position, repetition, 1))
|
||||
&& !segment.value(position, repetition, 2).matches("[A-Z][0-9][A-Z0-9](?:[A-Z0-9]{0,4}|\\.[A-Z0-9]{1,4})"))
|
||||
{
|
||||
findings.add(finding(segment, loop, position, 2, 5, "Invalid ICD-10 diagnosis format"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (findings);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Claim and line CAS monetary positions are 3, 6, 9, 12, 15 and 18.
|
||||
*******************************************************************************/
|
||||
private static void remittance(List<X12Segment> segments, List<ValidationFinding> findings)
|
||||
{
|
||||
for(List<X12Segment> claim : blocks(segments, "CLP"))
|
||||
{
|
||||
X12Segment head = claim.getFirst();
|
||||
try
|
||||
{
|
||||
BigDecimal adjustments = BigDecimal.ZERO;
|
||||
for(X12Segment segment : claim)
|
||||
{
|
||||
if(segment.id().equals("CAS"))
|
||||
{
|
||||
for(int p = 3; p <= segment.elements().size(); p += 3)
|
||||
{
|
||||
adjustments = adjustments.add(amount(segment, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
if(amount(head, 3).subtract(amount(head, 4)).compareTo(adjustments) != 0)
|
||||
{
|
||||
findings.add(finding(head, "2100", 3, null, 3, "Claim adjustments do not balance"));
|
||||
}
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
findings.add(finding(head, "2100", 3, null, 3, "Claim balance cannot be calculated"));
|
||||
}
|
||||
}
|
||||
for(X12Segment payment : segments.stream().filter(s -> s.id().equals("BPR")).toList())
|
||||
{
|
||||
try
|
||||
{
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for(X12Segment segment : segments)
|
||||
{
|
||||
if(segment.id().equals("CLP"))
|
||||
{
|
||||
total = total.add(amount(segment, 4));
|
||||
}
|
||||
if(segment.id().equals("PLB"))
|
||||
{
|
||||
for(int p = 4; p <= segment.elements().size(); p += 2)
|
||||
{
|
||||
total = total.subtract(amount(segment, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
if(amount(payment, 2).compareTo(total) != 0)
|
||||
{
|
||||
findings.add(finding(payment, null, 2, null, 3, "Payment does not balance"));
|
||||
}
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
findings.add(finding(payment, null, 2, null, 3, "Payment balance cannot be calculated"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Isolate each claim so one claim cannot compensate for another's imbalance.
|
||||
*******************************************************************************/
|
||||
private static List<List<X12Segment>> blocks(List<X12Segment> segments, String anchor)
|
||||
{
|
||||
List<List<X12Segment>> blocks = new ArrayList<>();
|
||||
List<X12Segment> current = null;
|
||||
for(X12Segment segment : segments)
|
||||
{
|
||||
if(segment.id().equals(anchor))
|
||||
{
|
||||
current = new ArrayList<>();
|
||||
blocks.add(current);
|
||||
}
|
||||
if(current != null)
|
||||
{
|
||||
current.add(segment);
|
||||
}
|
||||
}
|
||||
return (blocks);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Missing monetary elements fail balancing rather than silently becoming zero.
|
||||
*******************************************************************************/
|
||||
private static BigDecimal amount(X12Segment segment, int position)
|
||||
{
|
||||
return (new BigDecimal(segment.value(position)));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Position-only finding with a positional trigger for the implementation ack.
|
||||
*******************************************************************************/
|
||||
private static ValidationFinding finding(X12Segment segment, String loop, Integer element, Integer component, int level, String message)
|
||||
{
|
||||
return (new ValidationFinding(segment.position(), segment.id(), loop, element, component, level,
|
||||
"8", "7", "8", "7", "ERROR", message, null)
|
||||
.withContext(level == 3 ? new AckContext(segment.id(), segment.position(), loop, element, component) : null));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** NPI uses the Luhn checksum over the 80840 prefix and all ten digits.
|
||||
*******************************************************************************/
|
||||
public static boolean validNpi(String value)
|
||||
{
|
||||
if(!value.matches("[12][0-9]{9}"))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
String digits = "80840" + value;
|
||||
int sum = 0;
|
||||
for(int i = digits.length() - 1, n = 0; i >= 0; i--, n++)
|
||||
{
|
||||
int digit = digits.charAt(i) - '0';
|
||||
if(n % 2 == 1)
|
||||
{
|
||||
digit *= 2;
|
||||
}
|
||||
sum += digit > 9 ? digit - 9 : digit;
|
||||
}
|
||||
return (sum % 10 == 0);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Service date, then payment date, then the configured clock for undated documents.
|
||||
*******************************************************************************/
|
||||
public static LocalDate asOf(List<X12Segment> segments, Clock clock)
|
||||
{
|
||||
for(X12Segment segment : segments)
|
||||
{
|
||||
String date = segment.id().equals("DTP") && segment.value(1).equals("472") ? segment.value(3)
|
||||
: segment.id().equals("BPR") ? segment.value(16) : "";
|
||||
if(RuleEvaluator.validDate(date) && date.length() == 8)
|
||||
{
|
||||
return (LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE));
|
||||
}
|
||||
}
|
||||
return (LocalDate.now(clock));
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ public class RuleEvaluator
|
||||
String name = selector.group(1);
|
||||
String qualifier = selector.group(2);
|
||||
Matcher element = Pattern.compile("([A-Z][A-Z0-9]{1,2}?)([0-9]{2})(?:-([0-9]+))?").matcher(name);
|
||||
if(element.matches())
|
||||
if(element.matches() && !loops.containsKey(name))
|
||||
{
|
||||
List<X12Segment> selected = select(scope, element.group(1), qualifier);
|
||||
int position = Integer.parseInt(element.group(2));
|
||||
|
||||
@@ -25,12 +25,12 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.edi.document.EdiDocument;
|
||||
import com.kingsrook.qbits.edi.guide.Guide;
|
||||
import com.kingsrook.qbits.edi.guide.GuideValidator;
|
||||
import com.kingsrook.qbits.edi.guide.RuleEvaluator;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
import com.kingsrook.qbits.edi.x12.X12TransactionSet;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -58,7 +58,7 @@ public class InboundMapper
|
||||
/*******************************************************************************
|
||||
** Load ordered rules for a persisted map.
|
||||
*******************************************************************************/
|
||||
public Result map(X12TransactionSet transaction, Guide guide, QRecord map) throws Exception
|
||||
public Result map(EdiDocument transaction, Guide guide, QRecord map) throws Exception
|
||||
{
|
||||
List<QRecord> rules = new ArrayList<>(EdiRecords.query("edi_map_rule", EdiRecords.equal("mapId", map.getValue("id"))));
|
||||
rules.sort(Comparator.comparing(r -> r.getValueInteger("sequence")));
|
||||
@@ -68,7 +68,7 @@ public class InboundMapper
|
||||
/*******************************************************************************
|
||||
** Map in memory; wildcard rules sharing an association use the same loop scope.
|
||||
*******************************************************************************/
|
||||
public Result map(X12TransactionSet transaction, Guide guide, QRecord map, List<QRecord> rules) throws Exception
|
||||
public Result map(EdiDocument transaction, Guide guide, QRecord map, List<QRecord> rules) throws Exception
|
||||
{
|
||||
Map<String, List<List<X12Segment>>> loops = new GuideValidator().loops(transaction, guide);
|
||||
RuleEvaluator evaluator = new RuleEvaluator(transaction.segments(), loops);
|
||||
@@ -86,7 +86,7 @@ public class InboundMapper
|
||||
}
|
||||
if(fields.length == 1)
|
||||
{
|
||||
apply(rule, evaluator, result, fields[0], path);
|
||||
apply(rule, evaluator, result, fields[0], path, "EDIFACT".equals(guide.standard()));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -117,10 +117,14 @@ 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException e)
|
||||
{
|
||||
throw (e);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new IllegalArgumentException("Mapping failed at rule " + rule.getValueInteger("sequence") + " (" + rule.getValueString("ediPath") + " -> " + rule.getValueString("qqqPath") + ")");
|
||||
@@ -132,7 +136,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 +148,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()))
|
||||
{
|
||||
|
||||
@@ -20,138 +20,47 @@
|
||||
|
||||
package com.kingsrook.qbits.edi.map;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kingsrook.qbits.edi.EdiRuntime;
|
||||
import com.kingsrook.qbits.edi.ack.AckLevel;
|
||||
import com.kingsrook.qbits.edi.guide.GuideRepository;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.storage.PayloadRef;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
/*******************************************************************************
|
||||
** No-op apply preview with ordinary Result lines; PHI never enters process output.
|
||||
** Standard record-action presentation of the shared sandbox preview.
|
||||
*******************************************************************************/
|
||||
public class MapPreview
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Parse, validate, acknowledge and map a stored synthetic sample without target writes.
|
||||
** Render the same pipeline as HTTP using ordinary Review/Result lines.
|
||||
*******************************************************************************/
|
||||
public ArrayList<ProcessSummaryLineInterface> preview(QRecord map, QRecord sample) throws Exception
|
||||
{
|
||||
EdiRuntime runtime = EdiRuntime.current();
|
||||
boolean phi = List.of("837", "835", "270", "271", "276", "277", "834").contains(map.getValueString("transactionSet"));
|
||||
phi = phi || com.kingsrook.qbits.edi.support.SystemWrites.call(() -> !EdiRecords.query("edi_agreement_document", EdiRecords.equal("mapId", map.getValue("id"))
|
||||
.withCriteria("containsPhi", com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS, true)).isEmpty());
|
||||
var guide = new GuideRepository().load(map.getValueInteger("guideId"));
|
||||
Map<String, Object> preview = new MapPreviewService().preview(map, sample, null, "ORIGINAL");
|
||||
var json = new ObjectMapper().findAndRegisterModules();
|
||||
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
|
||||
try(InputStream stream = runtime.payloads().open(new PayloadRef(sample.getValueString("payloadRef"), null, 0)))
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, "Parsed: " + json.writeValueAsString(preview.get("parse"))));
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, "SNIP findings: " + json.writeValueAsString(preview.get("findings"))));
|
||||
if(Boolean.TRUE.equals(preview.get("phiSuppressed")))
|
||||
{
|
||||
var interchanges = runtime.reader().read(stream);
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, interchanges.size(), "interchanges parsed"));
|
||||
for(var interchange : interchanges)
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, "PHI map result suppressed; no target records written"));
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, "Acknowledgement preview suppressed for PHI document"));
|
||||
}
|
||||
else
|
||||
{
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, json.writeValueAsString(preview.get("result"))));
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, json.writeValueAsString(preview.get("ack"))));
|
||||
Map<?, ?> diff = (Map<?, ?>) preview.get("diff");
|
||||
for(String key : List.of("againstExpected", "againstPreviousRevision"))
|
||||
{
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, interchange.groups().size(), "functional groups parsed"));
|
||||
for(var group : interchange.groups())
|
||||
{
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, group.transactions().size(), "transaction sets parsed"));
|
||||
Map<String, List<com.kingsrook.qbits.edi.guide.ValidationFinding>> all = new java.util.LinkedHashMap<>();
|
||||
for(var transaction : group.transactions())
|
||||
{
|
||||
var findings = runtime.validator().validate(transaction, guide, phi);
|
||||
all.put(transaction.st().value(2), findings);
|
||||
for(int snip = 1; snip <= 7; snip++)
|
||||
{
|
||||
int level = snip;
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, (int) findings.stream().filter(f -> f.snipLevel() == level).count(), "findings at SNIP " + snip));
|
||||
}
|
||||
List<QRecord> rules = EdiRecords.query("edi_map_rule", EdiRecords.equal("mapId", map.getValue("id")));
|
||||
rules.sort(java.util.Comparator.comparing(r -> r.getValueInteger("sequence")));
|
||||
var mapped = InboundApplier.asService(runtime.config().getServiceSessionSupplier(), () -> new InboundMapper(runtime.transforms()).map(transaction, guide, map, rules));
|
||||
if(phi)
|
||||
{
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, "PHI map result suppressed; no target records written"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ObjectMapper json = new ObjectMapper().findAndRegisterModules();
|
||||
String actual = resultJson(mapped.record());
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, actual));
|
||||
String expected = sample.getValueString("expectedResultJson");
|
||||
if(expected != null)
|
||||
{
|
||||
diff(lines, "Expected result", json.readTree(expected), json.readTree(actual));
|
||||
}
|
||||
Integer revision = map.getValueInteger("revision");
|
||||
var previous = EdiRecords.query("edi_map", EdiRecords.equal("name", map.getValue("name")));
|
||||
previous.removeIf(r -> !java.util.Objects.equals(r.getValue("partnerId"), map.getValue("partnerId"))
|
||||
|| revision == null || r.getValueInteger("revision") == null || r.getValueInteger("revision") >= revision);
|
||||
previous.sort(java.util.Comparator.comparing((QRecord r) -> r.getValueInteger("revision")).reversed());
|
||||
if(!previous.isEmpty())
|
||||
{
|
||||
QRecord prior = previous.getFirst();
|
||||
var priorGuide = new GuideRepository().load(prior.getValueInteger("guideId"));
|
||||
List<QRecord> priorRules = EdiRecords.query("edi_map_rule", EdiRecords.equal("mapId", prior.getValue("id")));
|
||||
priorRules.sort(java.util.Comparator.comparing(r -> r.getValueInteger("sequence")));
|
||||
var priorResult = InboundApplier.asService(runtime.config().getServiceSessionSupplier(),
|
||||
() -> new InboundMapper(runtime.transforms()).map(transaction, priorGuide, prior, priorRules));
|
||||
diff(lines, "Previous revision " + prior.getValueInteger("revision"), json.readTree(resultJson(priorResult.record())), json.readTree(actual));
|
||||
}
|
||||
}
|
||||
}
|
||||
var ack = phi && group.gs().value(8).startsWith("005010X") ? runtime.ack999().build(group, all, AckLevel.ALL_TRANSACTIONS, "0001")
|
||||
: runtime.ack997().build(group, all, AckLevel.ALL_TRANSACTIONS, "0001");
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, phi ? "Acknowledgement preview suppressed for PHI document"
|
||||
: runtime.writer().write(com.kingsrook.qbits.edi.ack.AckEnvelope.wrap(interchange, group, ack, "1", "1", java.time.LocalDateTime.now(runtime.config().getClock())))));
|
||||
}
|
||||
String label = key.equals("againstExpected") ? "Expected result" : "Previous revision";
|
||||
List<?> differences = (List<?>) diff.get(key);
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, label + (differences.isEmpty() ? " matches" : " differs: " + json.writeValueAsString(differences))));
|
||||
}
|
||||
}
|
||||
return (lines);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Stable record JSON excludes QRecord diagnostics and implementation metadata.
|
||||
*******************************************************************************/
|
||||
private static String resultJson(QRecord record) throws Exception
|
||||
{
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
result.put("values", record.getValues());
|
||||
Map<String, Object> associations = new java.util.LinkedHashMap<>();
|
||||
if(record.getAssociatedRecords() != null)
|
||||
{
|
||||
for(var entry : record.getAssociatedRecords().entrySet())
|
||||
{
|
||||
List<Object> children = new ArrayList<>();
|
||||
for(QRecord child : entry.getValue())
|
||||
{
|
||||
children.add(new ObjectMapper().readTree(resultJson(child)));
|
||||
}
|
||||
associations.put(entry.getKey(), children);
|
||||
}
|
||||
}
|
||||
result.put("associations", associations);
|
||||
return (new ObjectMapper().findAndRegisterModules().writeValueAsString(result));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Show a structured before/after diff for synthetic samples only.
|
||||
*******************************************************************************/
|
||||
private static void diff(ArrayList<ProcessSummaryLineInterface> lines, String label, com.fasterxml.jackson.databind.JsonNode before, com.fasterxml.jackson.databind.JsonNode after) throws Exception
|
||||
{
|
||||
if(before.equals(after))
|
||||
{
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, label + " matches"));
|
||||
}
|
||||
else
|
||||
{
|
||||
Map<String, Object> difference = Map.of("comparison", label, "before", before, "after", after);
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, label + " differs: " + new ObjectMapper().writeValueAsString(difference)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* 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.map;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kingsrook.qbits.edi.EdiRuntime;
|
||||
import com.kingsrook.qbits.edi.ack.AckEnvelope;
|
||||
import com.kingsrook.qbits.edi.ack.AckLevel;
|
||||
import com.kingsrook.qbits.edi.document.EdiDocument;
|
||||
import com.kingsrook.qbits.edi.edifact.ContrlBuilder;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactWriter;
|
||||
import com.kingsrook.qbits.edi.guide.Guide;
|
||||
import com.kingsrook.qbits.edi.guide.GuideRepository;
|
||||
import com.kingsrook.qbits.edi.guide.ValidationFinding;
|
||||
import com.kingsrook.qbits.edi.security.PhiLogScope;
|
||||
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.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
/*******************************************************************************
|
||||
** Shared no-apply preview pipeline. All mapping, including LOOKUP, uses the caller.
|
||||
** This service has no optional middleware dependency.
|
||||
*******************************************************************************/
|
||||
public class MapPreviewService
|
||||
{
|
||||
private static final ObjectMapper JSON = new ObjectMapper().findAndRegisterModules();
|
||||
|
||||
/*******************************************************************************
|
||||
** Read ordered persisted rules, or order a copy of the unsaved replacement set.
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> rules(QRecord map, List<QRecord> overrides) throws Exception
|
||||
{
|
||||
List<QRecord> result = new ArrayList<>(overrides == null ? EdiRecords.queryEnforced("edi_map_rule", EdiRecords.equal("mapId", map.getValue("id"))) : overrides);
|
||||
result.sort(Comparator.comparing(r -> Objects.requireNonNullElse(r.getValueInteger("sequence"), 0)));
|
||||
return (result);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** PHI classification cannot be weakened by the caller or by hidden agreements.
|
||||
*******************************************************************************/
|
||||
public static boolean containsPhi(QRecord map) throws Exception
|
||||
{
|
||||
return (healthcare(map.getValueString("transactionSet")) || SystemWrites.call(() -> !EdiRecords.query("edi_agreement_document",
|
||||
EdiRecords.equal("mapId", map.getValue("id")).withCriteria("containsPhi", QCriteriaOperator.EQUALS, true)).isEmpty()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Healthcare document types always suppress values, even with a mismatched map.
|
||||
*******************************************************************************/
|
||||
private static boolean healthcare(String type)
|
||||
{
|
||||
return (type != null && List.of("837", "835", "270", "271", "276", "277", "834").contains(type));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** ORIGINAL is the pinned row; CURRENT follows only the same guide family.
|
||||
*******************************************************************************/
|
||||
public static Guide guide(QRecord map, String revision) throws Exception
|
||||
{
|
||||
Integer id = map.getValueInteger("guideId");
|
||||
if("CURRENT".equals(revision))
|
||||
{
|
||||
var pinnedRows = EdiRecords.queryEnforced("edi_guide", EdiRecords.equal("id", id));
|
||||
if(pinnedRows.isEmpty())
|
||||
{
|
||||
throw (new com.kingsrook.qqq.backend.core.exceptions.QNotFoundException("Not found"));
|
||||
}
|
||||
QRecord pinned = pinnedRows.getFirst();
|
||||
var candidates = EdiRecords.queryEnforced("edi_guide", EdiRecords.equal("name", pinned.getValue("name")).withCriteria("status", QCriteriaOperator.EQUALS, "ACTIVE"));
|
||||
candidates.removeIf(r -> !Objects.equals(r.getValue("partnerId"), pinned.getValue("partnerId"))
|
||||
|| !Objects.equals(r.getValue("standard"), pinned.getValue("standard"))
|
||||
|| !Objects.equals(r.getValue("version"), pinned.getValue("version"))
|
||||
|| !Objects.equals(r.getValue("transactionSet"), pinned.getValue("transactionSet")));
|
||||
candidates.sort(Comparator.comparing((QRecord r) -> Objects.requireNonNullElse(r.getValueInteger("revision"), 0)).reversed());
|
||||
if(!candidates.isEmpty())
|
||||
{
|
||||
id = candidates.getFirst().getValueInteger("id");
|
||||
}
|
||||
}
|
||||
return (new GuideRepository().load(id, true));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Parse and inspect the complete sample before exposing any values.
|
||||
*******************************************************************************/
|
||||
public Map<String, Object> preview(QRecord map, QRecord sample, List<QRecord> overrides, String revision) throws Exception
|
||||
{
|
||||
EdiRuntime runtime = EdiRuntime.current();
|
||||
boolean phi = containsPhi(map);
|
||||
Guide guide = guide(map, revision);
|
||||
List<QRecord> rules = rules(map, overrides);
|
||||
State state = new State();
|
||||
try(var logging = PhiLogScope.open(true); InputStream stream = SystemWrites.call(() -> runtime.payloads().open(new PayloadRef(sample.getValueString("payloadRef"), null, 0))))
|
||||
{
|
||||
if("EDIFACT".equals(map.getValueString("standard")))
|
||||
{
|
||||
var interchanges = new EdifactEnvelopeReader(runtime.config().getMaxSegmentsPerTransaction()).read(stream);
|
||||
state.interchanges = interchanges.size();
|
||||
for(var interchange : interchanges)
|
||||
{
|
||||
state.groups += interchange.groups().size();
|
||||
state.envelope(interchange.findings());
|
||||
interchange.groups().forEach(group -> state.envelope(group.findings()));
|
||||
Map<String, List<ValidationFinding>> all = new LinkedHashMap<>();
|
||||
for(var message : interchange.allMessages())
|
||||
{
|
||||
all.put(message.reference(), document(state, map, sample, rules, guide, message, phi));
|
||||
}
|
||||
if(interchange.unb() != null)
|
||||
{
|
||||
var ack = new ContrlBuilder().build(interchange, all, AckLevel.ALL_TRANSACTIONS, "1", map.getValueString("version"));
|
||||
state.ack("CONTRL", phi ? null : new EdifactWriter().write(ack.segments(), interchange.separators(), ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var interchanges = runtime.reader().read(stream);
|
||||
phi = phi || interchanges.stream().flatMap(i -> i.groups().stream()).flatMap(g -> g.transactions().stream()).anyMatch(t -> healthcare(t.st().value(1)));
|
||||
state.interchanges = interchanges.size();
|
||||
for(var interchange : interchanges)
|
||||
{
|
||||
state.groups += interchange.groups().size();
|
||||
state.envelope(interchange.findings());
|
||||
if(!interchange.findings().isEmpty())
|
||||
{
|
||||
var ta1 = runtime.ta1().build(interchange.isa(), interchange.findings());
|
||||
var isa = AckEnvelope.replyIsa(interchange.isa(), "1", java.time.LocalDateTime.now(runtime.config().getClock()));
|
||||
var reply = new com.kingsrook.qbits.edi.x12.X12Interchange(interchange.separators(), isa,
|
||||
com.kingsrook.qbits.edi.x12.X12Segment.of("IEA", 3, "0", isa.value(13)), List.of(), List.of(ta1), List.of());
|
||||
state.ack("TA1", phi ? null : runtime.writer().write(reply));
|
||||
}
|
||||
for(var group : interchange.groups())
|
||||
{
|
||||
state.envelope(group.findings());
|
||||
Map<String, List<ValidationFinding>> all = new LinkedHashMap<>();
|
||||
for(var transaction : group.transactions())
|
||||
{
|
||||
all.put(transaction.st().value(2), document(state, map, sample, rules, guide, transaction, phi));
|
||||
}
|
||||
boolean use999 = phi && group.gs().value(8).startsWith("005010X");
|
||||
var ack = use999 ? runtime.ack999().build(group, all, AckLevel.ALL_TRANSACTIONS, "0001") : runtime.ack997().build(group, all, AckLevel.ALL_TRANSACTIONS, "0001");
|
||||
state.ack(use999 ? "999" : "997", phi ? null : runtime.writer().write(AckEnvelope.wrap(interchange, group, ack, "1", "1", java.time.LocalDateTime.now(runtime.config().getClock()))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException e)
|
||||
{
|
||||
throw (e);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
// Parser, custom transform and expected-JSON diagnostics may contain payload values.
|
||||
throw (new QBadRequestException("Sample could not be previewed; check the document, guide and rules."));
|
||||
}
|
||||
return (object("parse", Map.of("interchanges", state.interchanges, "groups", state.groups, "documents", state.documents),
|
||||
"findings", state.findings, "ack", object("type", state.ackType, "text", phi ? null : state.ackText.toString()),
|
||||
"result", phi ? null : Map.of("records", state.records), "diff", Map.of("againstPreviousRevision", state.previous, "againstExpected", state.expected), "phiSuppressed", phi));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate and map one document, collecting only value-free findings for PHI.
|
||||
*******************************************************************************/
|
||||
private List<ValidationFinding> document(State state, QRecord map, QRecord sample, List<QRecord> rules, Guide guide, EdiDocument document, boolean phi) throws Exception
|
||||
{
|
||||
int index = state.documents++;
|
||||
state.envelope(document.findings());
|
||||
var findings = EdiRuntime.current().validator().validate(document, guide, phi);
|
||||
for(var finding : findings)
|
||||
{
|
||||
state.findings.add(object("snipLevel", finding.snipLevel(), "severity", finding.severity(), "segmentPosition", finding.segmentPosition(),
|
||||
"segmentId", finding.segmentId(), "loopId", finding.loopId(), "elementPosition", finding.elementPosition(),
|
||||
"code", finding.elementPosition() == null ? finding.ak3Code() : finding.ak4Code(), "message", phi ? "Validation finding" : finding.message()));
|
||||
}
|
||||
QRecord mapped;
|
||||
try
|
||||
{
|
||||
mapped = new InboundMapper(EdiRuntime.current().transforms()).map(document, guide, map, rules).record();
|
||||
}
|
||||
catch(com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException e)
|
||||
{
|
||||
throw (e);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
state.findings.add(object("snipLevel", 0, "severity", "ERROR", "segmentPosition", null, "segmentId", null, "loopId", null,
|
||||
"elementPosition", null, "code", "MAP", "message", "Mapping failed; check the rule paths and transform arguments"));
|
||||
return (findings);
|
||||
}
|
||||
if(!phi)
|
||||
{
|
||||
Map<String, Object> actual = record(mapped);
|
||||
state.records.add(actual);
|
||||
String expected = sample.getValueString("expectedResultJson");
|
||||
if(expected != null)
|
||||
{
|
||||
difference(state.expected, index, JSON.readValue(expected, Object.class), actual);
|
||||
}
|
||||
Integer revision = map.getValueInteger("revision");
|
||||
var previous = EdiRecords.queryEnforced("edi_map", EdiRecords.equal("name", map.getValue("name")));
|
||||
previous.removeIf(r -> !Objects.equals(r.getValue("partnerId"), map.getValue("partnerId"))
|
||||
|| !Objects.equals(r.getValue("standard"), map.getValue("standard")) || !Objects.equals(r.getValue("transactionSet"), map.getValue("transactionSet"))
|
||||
|| !Objects.equals(r.getValue("direction"), map.getValue("direction")) || !Objects.equals(r.getValue("version"), map.getValue("version"))
|
||||
|| !Objects.equals(r.getValue("tableName"), map.getValue("tableName"))
|
||||
|| revision == null || r.getValueInteger("revision") == null || r.getValueInteger("revision") >= revision);
|
||||
previous.sort(Comparator.comparing((QRecord r) -> r.getValueInteger("revision")).reversed());
|
||||
if(!previous.isEmpty() && !containsPhi(previous.getFirst()))
|
||||
{
|
||||
QRecord prior = previous.getFirst();
|
||||
QRecord priorResult = new InboundMapper(EdiRuntime.current().transforms()).map(document, guide(prior, "ORIGINAL"), prior, rules(prior, null)).record();
|
||||
difference(state.previous, index, record(priorResult), actual);
|
||||
}
|
||||
}
|
||||
return (findings);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Stable record values and recursive associations, without QQQ diagnostics.
|
||||
*******************************************************************************/
|
||||
private static Map<String, Object> record(QRecord record)
|
||||
{
|
||||
Map<String, Object> associations = new LinkedHashMap<>();
|
||||
if(record.getAssociatedRecords() != null)
|
||||
{
|
||||
record.getAssociatedRecords().forEach((name, children) -> associations.put(name, children.stream().map(MapPreviewService::record).toList()));
|
||||
}
|
||||
return (object("values", record.getValues(), "associations", associations));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Compare normalized JSON so temporal values and numeric types match wire output.
|
||||
*******************************************************************************/
|
||||
private static void difference(List<Map<String, Object>> result, int index, Object before, Object after)
|
||||
{
|
||||
if(!JSON.valueToTree(before).equals(JSON.valueToTree(after)))
|
||||
{
|
||||
result.add(object("documentIndex", index, "before", before, "after", after));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Ordered objects allowing explicit nulls on the wire.
|
||||
*******************************************************************************/
|
||||
private static Map<String, Object> object(Object... pairs)
|
||||
{
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for(int i = 0; i < pairs.length; i += 2)
|
||||
{
|
||||
result.put((String) pairs[i], pairs[i + 1]);
|
||||
}
|
||||
return (result);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** One invocation's accumulation; never shared across requests.
|
||||
*******************************************************************************/
|
||||
private static class State
|
||||
{
|
||||
private int interchanges;
|
||||
private int groups;
|
||||
private int documents;
|
||||
private final List<Map<String, Object>> findings = new ArrayList<>();
|
||||
private final List<Map<String, Object>> records = new ArrayList<>();
|
||||
private final List<Map<String, Object>> previous = new ArrayList<>();
|
||||
private final List<Map<String, Object>> expected = new ArrayList<>();
|
||||
private String ackType = "NONE";
|
||||
private final StringBuilder ackText = new StringBuilder();
|
||||
|
||||
/*******************************************************************************
|
||||
** Envelope diagnostics carry no raw envelope identifiers.
|
||||
*******************************************************************************/
|
||||
private void envelope(List<EnvelopeFinding> values)
|
||||
{
|
||||
values.forEach(f -> findings.add(object("snipLevel", 1, "severity", "ERROR", "segmentPosition", f.segmentPosition(),
|
||||
"segmentId", null, "loopId", null, "elementPosition", null, "code", f.code(), "message", "Envelope validation finding")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Aggregate acknowledgements in wire order.
|
||||
*******************************************************************************/
|
||||
private void ack(String type, String text)
|
||||
{
|
||||
ackType = "NONE".equals(ackType) || type.equals(ackType) ? type : "MIXED";
|
||||
if(text != null)
|
||||
{
|
||||
ackText.append(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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.map;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kingsrook.qbits.edi.guide.Guide;
|
||||
import com.kingsrook.qbits.edi.guide.GuideSegment;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
/*******************************************************************************
|
||||
** Static validation of unsaved rules; never runs transforms or writes records.
|
||||
*******************************************************************************/
|
||||
public class MapRuleValidator
|
||||
{
|
||||
private static final Pattern ELEMENT = Pattern.compile("([A-Z][A-Z0-9]{1,2}?)([0-9]{2})(?:-([0-9]+))?");
|
||||
private static final Pattern STEP = Pattern.compile("([^\\[]+)(?:\\[(\\*|[1-9][0-9]*|[^\\[\\]]+=[^\\[\\]]+)])?");
|
||||
|
||||
/*******************************************************************************
|
||||
** Report independent errors without echoing rule literals or default values.
|
||||
*******************************************************************************/
|
||||
public List<String> validate(QRecord rule, QRecord map, Guide guide, TransformRegistry registry)
|
||||
{
|
||||
List<String> errors = new ArrayList<>();
|
||||
if(rule.getValueInteger("sequence") == null || rule.getValueInteger("sequence") < 1)
|
||||
{
|
||||
errors.add("sequence must be a positive integer");
|
||||
}
|
||||
boolean outbound = "OUTBOUND".equals(map.getValueString("direction"));
|
||||
String ediPath = rule.getValueString("ediPath");
|
||||
String transform = Objects.requireNonNullElse(rule.getValueString("transform"), "").toUpperCase(Locale.ROOT);
|
||||
if(ediPath == null || ediPath.isBlank())
|
||||
{
|
||||
if(outbound || !transform.equals("CONSTANT") && rule.getValueString("defaultValue") == null)
|
||||
{
|
||||
errors.add("ediPath is required");
|
||||
}
|
||||
}
|
||||
else if(!resolves(ediPath, guide))
|
||||
{
|
||||
errors.add("ediPath does not resolve in the guide");
|
||||
}
|
||||
String target = rule.getValueString("qqqPath");
|
||||
QTableMetaData table = QContext.getQInstance().getTable(map.getValueString("tableName"));
|
||||
String[] fields = target == null ? new String[0] : target.split("\\.", -1);
|
||||
for(int i = 0; table != null && i < fields.length - 1; i++)
|
||||
{
|
||||
var associations = table.getAssociations();
|
||||
String name = fields[i];
|
||||
var association = associations == null ? null : associations.stream().filter(a -> a.getName().equals(name)).findFirst().orElse(null);
|
||||
table = association == null ? null : QContext.getQInstance().getTable(association.getAssociatedTableName());
|
||||
}
|
||||
boolean generated = outbound && (target == null || target.isBlank()) && List.of("CONSTANT", "SEQUENCE", "HL").contains(transform);
|
||||
if(!generated && (table == null || fields.length == 0 || !table.getFields().containsKey(fields[fields.length - 1])
|
||||
|| !outbound && fields.length > 2))
|
||||
{
|
||||
errors.add("qqqPath does not resolve in the target table");
|
||||
}
|
||||
if(fields.length > 1 && !"OUTBOUND".equals(map.getValueString("direction")) && (ediPath == null || !ediPath.contains("[*]/")))
|
||||
{
|
||||
errors.add("An inbound association requires a wildcard loop");
|
||||
}
|
||||
Map<String, Object> schema = registry.argumentSchemas().get(transform);
|
||||
if(!transform.isEmpty() && schema == null)
|
||||
{
|
||||
errors.add("Unknown transform");
|
||||
}
|
||||
try
|
||||
{
|
||||
String raw = rule.getValueString("transformArgs");
|
||||
var args = new ObjectMapper().readTree(raw == null || raw.isBlank() ? "{}" : raw);
|
||||
if(!args.isObject() || java.util.stream.StreamSupport.stream(java.util.Spliterators.spliteratorUnknownSize(args.elements(), 0), false).anyMatch(v -> !v.isTextual()))
|
||||
{
|
||||
errors.add("transformArgs must be a JSON object of strings");
|
||||
}
|
||||
else if(schema != null && schema.get("required") instanceof List<?> required)
|
||||
{
|
||||
for(Object key : required)
|
||||
{
|
||||
if(!args.hasNonNull(key.toString()))
|
||||
{
|
||||
errors.add("Missing required transform argument: " + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
errors.add("transformArgs must be a JSON object of strings");
|
||||
}
|
||||
return (errors);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve every joined element path, including selectors and composite positions.
|
||||
*******************************************************************************/
|
||||
private boolean resolves(String path, Guide guide)
|
||||
{
|
||||
for(String part : path.split("\\+(?![^\\[]*])", -1))
|
||||
{
|
||||
List<GuideSegment> scope = guide.segments();
|
||||
String[] steps = part.trim().split("/", -1);
|
||||
for(int index = 0; index < steps.length; index++)
|
||||
{
|
||||
var step = STEP.matcher(steps[index]);
|
||||
if(!step.matches())
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
String name = step.group(1);
|
||||
String qualifier = step.group(2);
|
||||
var element = ELEMENT.matcher(name);
|
||||
if(index == steps.length - 1)
|
||||
{
|
||||
if(!element.matches() || !element(scope, name))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
scope = scope.stream().filter(s -> s.segmentId().equals(element.group(1))).toList();
|
||||
}
|
||||
else
|
||||
{
|
||||
List<GuideSegment> current = scope;
|
||||
scope = current.stream().filter(s -> s.segmentId().equals(name) || inLoop(s, name, guide, 0)).toList();
|
||||
if(scope.isEmpty())
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
if(qualifier != null && qualifier.contains("=") && !element(scope, qualifier.substring(0, qualifier.indexOf('='))))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (true);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Ancestor scope matters when different loops contain the same segment.
|
||||
*******************************************************************************/
|
||||
private boolean inLoop(GuideSegment segment, String loop, Guide guide, int depth)
|
||||
{
|
||||
if(loop.equals(segment.loopId()))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
if(segment.parentLoopId() == null || depth > guide.segments().size())
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
return (loop.equals(segment.parentLoopId()) || guide.segments().stream().filter(s -> segment.parentLoopId().equals(s.loopId()))
|
||||
.anyMatch(parent -> inLoop(parent, loop, guide, depth + 1)));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Match an element and its optional composite component.
|
||||
*******************************************************************************/
|
||||
private boolean element(List<GuideSegment> segments, String path)
|
||||
{
|
||||
var match = ELEMENT.matcher(path);
|
||||
if(!match.matches())
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
int position = Integer.parseInt(match.group(2));
|
||||
Integer component;
|
||||
try
|
||||
{
|
||||
component = match.group(3) == null ? null : Integer.valueOf(match.group(3));
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
return (segments.stream().filter(s -> s.segmentId().equals(match.group(1))).flatMap(s -> s.elements().stream())
|
||||
.anyMatch(e -> e.position() == position && (component == null || component.equals(e.componentPosition()))));
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,10 @@ public class OutboundMapper
|
||||
throw new IllegalArgumentException("An outbound map is required");
|
||||
}
|
||||
List<X12Segment> segments = new ArrayList<>();
|
||||
segments.add(X12Segment.of("ST", 1, map.getValueString("transactionSet"), control));
|
||||
String version = map.getValueString("version");
|
||||
segments.add(version != null && version.startsWith("005010X")
|
||||
? X12Segment.of("ST", 1, map.getValueString("transactionSet"), control, version)
|
||||
: X12Segment.of("ST", 1, map.getValueString("transactionSet"), control));
|
||||
List<QRecord> sorted = rules.stream().sorted(Comparator.comparing(r -> r.getValueInteger("sequence"))).toList();
|
||||
emit(source, sorted, "", "", 1, "", new int[] {0}, segments);
|
||||
segments.add(X12Segment.of("SE", segments.size() + 1, String.valueOf(segments.size() + 1), control));
|
||||
@@ -72,7 +75,7 @@ public class OutboundMapper
|
||||
/*******************************************************************************
|
||||
** Walk each loop once per associated record, retaining the nearest HL parent.
|
||||
*******************************************************************************/
|
||||
private void emit(QRecord source, List<QRecord> rules, String prefix, String qPrefix, int sequence, String parent,
|
||||
protected void emit(QRecord source, List<QRecord> rules, String prefix, String qPrefix, int sequence, String parent,
|
||||
int[] hierarchy, List<X12Segment> output) throws Exception
|
||||
{
|
||||
Map<String, List<QRecord>> nodes = new LinkedHashMap<>();
|
||||
@@ -109,13 +112,13 @@ public class OutboundMapper
|
||||
}
|
||||
continue;
|
||||
}
|
||||
List<String> elements = new ArrayList<>();
|
||||
List<List<String>> elements = new ArrayList<>();
|
||||
String segmentId = null;
|
||||
for(QRecord rule : node.getValue())
|
||||
{
|
||||
String path = rule.getValueString("ediPath");
|
||||
var match = ELEMENT.matcher(path.substring(path.lastIndexOf('/') + 1));
|
||||
if(!match.matches() || match.group(3) != null)
|
||||
if(!match.matches())
|
||||
{
|
||||
throw new IllegalArgumentException("Unsupported outbound element path");
|
||||
}
|
||||
@@ -127,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))
|
||||
@@ -169,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
|
||||
{
|
||||
@@ -180,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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
public class TransformRegistry
|
||||
{
|
||||
private final Map<String, Transform> transforms = new ConcurrentHashMap<>();
|
||||
private final Map<String, Map<String, Object>> argumentSchemas = new ConcurrentHashMap<>();
|
||||
|
||||
/*******************************************************************************
|
||||
** Transform a selected path's values in the service session.
|
||||
@@ -118,11 +119,59 @@ public class TransformRegistry
|
||||
** Register or replace a named host transform.
|
||||
*******************************************************************************/
|
||||
public TransformRegistry register(String name, Transform transform)
|
||||
{
|
||||
return (register(name, transform, schema(name)));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Host transforms publish their argument contract alongside executable behavior.
|
||||
*******************************************************************************/
|
||||
public TransformRegistry register(String name, Transform transform, Map<String, Object> argumentSchema)
|
||||
{
|
||||
transforms.put(name.toUpperCase(Locale.ROOT), transform);
|
||||
argumentSchemas.put(name.toUpperCase(Locale.ROOT), Map.copyOf(argumentSchema));
|
||||
return (this);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Sorted snapshot for mapper clients and static validation.
|
||||
*******************************************************************************/
|
||||
public Map<String, Map<String, Object>> argumentSchemas()
|
||||
{
|
||||
return (java.util.Collections.unmodifiableMap(new java.util.TreeMap<>(argumentSchemas)));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Built-ins accept strings, matching the persisted transformArgs representation.
|
||||
*******************************************************************************/
|
||||
private static Map<String, Object> schema(String name)
|
||||
{
|
||||
List<String> keys = switch(name.toUpperCase(Locale.ROOT))
|
||||
{
|
||||
case "CONSTANT" -> List.of("value");
|
||||
case "LOOKUP" -> List.of("table", "key", "result");
|
||||
case "DATETIME" -> List.of("time", "timezone");
|
||||
case "DATEPART", "TIMEPART" -> List.of("timezone");
|
||||
case "DECIMAL" -> List.of("scale");
|
||||
case "CONCAT" -> List.of("separator");
|
||||
case "SPLIT" -> List.of("separator", "index");
|
||||
case "SUBSTRING" -> List.of("start", "end");
|
||||
case "UNIT" -> List.of("factor", "divisor", "scale");
|
||||
case "SEQUENCE" -> List.of("start");
|
||||
case "HL" -> List.of("code");
|
||||
default -> List.of();
|
||||
};
|
||||
Map<String, Object> properties = new java.util.TreeMap<>();
|
||||
keys.forEach(key -> properties.put(key, Map.of("type", "string")));
|
||||
List<String> required = switch(name.toUpperCase(Locale.ROOT))
|
||||
{
|
||||
case "CONSTANT" -> List.of("value");
|
||||
case "LOOKUP" -> List.of("table", "key");
|
||||
default -> List.of();
|
||||
};
|
||||
return (Map.of("type", "object", "properties", properties, "required", required, "additionalProperties", Map.of("type", "string")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute a closed registry entry; unknown names fail explicitly.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -27,7 +27,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
/*******************************************************************************
|
||||
** RC.5 form filters; no dependency on later dependency-behavior metadata.
|
||||
** RC.6 form filters; no dependency on later dependency-behavior metadata.
|
||||
*******************************************************************************/
|
||||
public final class EdiContingencies
|
||||
{
|
||||
@@ -75,7 +75,9 @@ public final class EdiContingencies
|
||||
}
|
||||
if(filter != null)
|
||||
{
|
||||
field.withPossibleValueSourceFilter(filter);
|
||||
// Core appends its search criteria to the root; keep them ANDed with partner scope.
|
||||
field.withPossibleValueSourceFilter(filter.getBooleanOperator() == QQueryFilter.BooleanOperator.OR
|
||||
? new QQueryFilter().withSubFilter(filter) : filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +51,10 @@ import com.kingsrook.qbits.edi.model.EdiPayload;
|
||||
import com.kingsrook.qbits.edi.model.EdiTransaction;
|
||||
import com.kingsrook.qbits.edi.model.EdiTransactionError;
|
||||
import com.kingsrook.qbits.edi.model.EdiTransport;
|
||||
import com.kingsrook.qbits.edi.security.EdiClassifications;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiExposure;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@@ -232,11 +232,12 @@ public final class EdiTables
|
||||
QTableMetaData table = new QTableMetaData().withName(name).withBackendName(config.getBackendName())
|
||||
.withPrimaryKeyField("id").withFieldsFromEntity(entity).withRecordLabelFormatAndFields("%s", label)
|
||||
.withPermissionRules(new QPermissionRules().withPermissionBaseName("edi." + name.substring(4)).withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS))
|
||||
.withAuditRules(new QAuditRules().withAuditLevel(readOnly ? AuditLevel.RECORD : AuditLevel.FIELD));
|
||||
.withAuditRules(new QAuditRules().withAuditLevel(readOnly || name.equals("edi_map_sample") ? AuditLevel.RECORD : AuditLevel.FIELD));
|
||||
QRecordEntity.registerTable(entity, table);
|
||||
AiTableMetaData.ofOrWithNew(table).withClassification(DataClassification.CONFIDENTIAL);
|
||||
if(List.of("edi_payload", "edi_certificate", "edi_inbox").contains(name))
|
||||
{
|
||||
table.withSupplementalMetaData(new AiTableMetaData().withExposure(AiExposure.NONE));
|
||||
AiTableMetaData.ofOrWithNew(table).withExposure(AiExposure.NONE);
|
||||
}
|
||||
if(name.equals("edi_exception"))
|
||||
{
|
||||
@@ -253,13 +254,14 @@ public final class EdiTables
|
||||
}
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
String classification = List.of("phiBusinessKey", "fileName", "ciphertext", "expectedResultJson", "valueExcerpt").contains(field.getName()) ? "PHI"
|
||||
: List.of("contactName", "contactEmail", "contactPhone").contains(field.getName()) ? "PII" : "CONFIDENTIAL";
|
||||
field.withSupplementalMetaData(new EdiClassifications(classification));
|
||||
if(classification.equals("PHI"))
|
||||
DataClassification classification = List.of("phiBusinessKey", "fileName", "ciphertext", "expectedResultJson", "valueExcerpt").contains(field.getName()) ? DataClassification.PHI
|
||||
: List.of("contactName", "contactEmail", "contactPhone").contains(field.getName()) ? DataClassification.PII : DataClassification.CONFIDENTIAL;
|
||||
AiFieldMetaData metadata = new AiFieldMetaData().withClassification(classification);
|
||||
if(classification == DataClassification.PHI)
|
||||
{
|
||||
field.withSupplementalMetaData(new AiFieldMetaData().withExposure(AiExposure.NONE));
|
||||
metadata.withExposure(AiExposure.NONE);
|
||||
}
|
||||
field.withSupplementalMetaData(metadata);
|
||||
}
|
||||
EdiTablePresentation.apply(table);
|
||||
EdiRecordLabels.configure(table);
|
||||
@@ -281,11 +283,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"));
|
||||
@@ -305,7 +307,7 @@ public final class EdiTables
|
||||
sources.add(enumSource("edi_interchange_status", "RECEIVED", "ACCEPTED", "ACCEPTED_WITH_ERRORS", "REJECTED", "DUPLICATE", "ENVELOPED", "SENT", "DELIVERED", "FAILED"));
|
||||
sources.add(enumSource("edi_ack_status", "A", "E", "P", "R", "NONE", "PENDING"));
|
||||
sources.add(enumSource("edi_transaction_status", "RECEIVED", "VALID", "INVALID", "MAPPED", "APPLIED", "EXCEPTION", "QUEUED", "SENT", "ACKNOWLEDGED", "REJECTED", "IGNORED"));
|
||||
sources.add(enumSource("edi_event_type", "RECEIVED", "STORED", "PARSED", "VALIDATED", "ACK_SENT", "ACK_QUEUED", "MAPPED", "APPLIED", "ENVELOPED", "SENT", "MDN", "DELIVERED", "ACK_RECEIVED", "EXCEPTION", "REPROCESSED", "RESENT"));
|
||||
sources.add(enumSource("edi_event_type", "RECEIVED", "STORED", "PARSED", "VALIDATED", "ACK_SENT", "ACK_QUEUED", "MAPPED", "APPLIED", "ENVELOPED", "SENT", "MDN", "DELIVERED", "ACK_RECEIVED", "EXCEPTION", "REPROCESSED", "RESENT", "RAW_DOWNLOADED"));
|
||||
sources.add(enumSource("edi_exception_type", "UNKNOWN_PARTNER", "PARTNER_MISMATCH", "NOT_CONFIGURED", "DUPLICATE", "ENVELOPE", "VALIDATION", "MAPPING", "APPLY", "TRANSPORT", "REJECTED_ACK", "OVERDUE_ACK", "DEAD_LETTER"));
|
||||
sources.add(enumSource("edi_exception_status", "OPEN", "IN_PROGRESS", "RESOLVED", "IGNORED"));
|
||||
sources.add(enumSource("edi_inbox_status", "PENDING", "PROCESSING", "PROCESSED", "RETRY_WAIT", "DEAD_LETTER", "DUPLICATE", "IGNORED"));
|
||||
@@ -422,7 +424,8 @@ public final class EdiTables
|
||||
{
|
||||
return (joins().stream().map(join -> ChildRecordListRenderer.widgetMetaDataBuilder(join).withName(join.getName())
|
||||
.withLabel(joinLabel(join)).withCanAddChildRecord(false)
|
||||
.withOrderBys(childOrder(join.getRightTable())).getWidgetMetaData().withGridColumns(12)).toList());
|
||||
.withOrderBys(childOrder(join.getRightTable())).getWidgetMetaData()
|
||||
.withCodeReference(new QCodeReference(com.kingsrook.qbits.edi.app.EdiChildRecordListRenderer.class)).withGridColumns(12)).toList());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@ import com.kingsrook.qbits.edi.map.MapPreview;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.alerts.NotificationAudience;
|
||||
import com.kingsrook.qqq.backend.core.alerts.NotificationSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlerts;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
@@ -75,6 +78,10 @@ public final class EdiProcesses
|
||||
{
|
||||
result.add(action(table, "preview", "Preview Map", "edi.map.preview"));
|
||||
}
|
||||
for(String table : List.of("edi_inbox", "edi_interchange"))
|
||||
{
|
||||
result.add(action(table, "download", "Download Raw Document", "edi.raw.download").withMaxInputRecords(1));
|
||||
}
|
||||
result.add(action("edi_exception", "assign", "Assign", "edi.exception.assign"));
|
||||
result.add(action("edi_exception", "ignore", "Ignore", "edi.exception.ignore"));
|
||||
result.add(action("edi_exception", "resolve", "Mark Resolved", "edi.exception.resolve"));
|
||||
@@ -130,6 +137,8 @@ public final class EdiProcesses
|
||||
.withName(table + "_" + operation).withLabel(label).withTableName(table).withIsHidden(true).withMinInputRecords(1).withPermissionRules(permission(permission));
|
||||
if(operation.equals("preview"))
|
||||
{
|
||||
process.getPermissionRules().withCustomPermissionChecker(
|
||||
com.kingsrook.qqq.backend.core.actions.permissions.UseOtherPermissionNameCustomPermissionChecker.build("edi.map.preview"));
|
||||
if(table.equals("edi_map"))
|
||||
{
|
||||
process.getFrontendStep("review").withFormField(new QFieldMetaData("sampleId", QFieldType.INTEGER)
|
||||
@@ -148,6 +157,16 @@ public final class EdiProcesses
|
||||
process.getFrontendStep("result").withViewField(new QFieldMetaData(field, QFieldType.STRING));
|
||||
}
|
||||
}
|
||||
if(operation.equals("download"))
|
||||
{
|
||||
process.getFrontendStep("result").withComponent(new com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData()
|
||||
.withType(com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType.DOWNLOAD_FORM));
|
||||
for(String field : List.of("downloadFileName", "storageTableName", "storageReference"))
|
||||
{
|
||||
process.getBackendStep("execute").getOutputMetaData().withField(new QFieldMetaData(field, QFieldType.STRING));
|
||||
process.getFrontendStep("result").withViewField(new QFieldMetaData(field, QFieldType.STRING));
|
||||
}
|
||||
}
|
||||
if(operation.equals("assign"))
|
||||
{
|
||||
process.getFrontendStep("review").withFormField(new QFieldMetaData("assignedToUserId", QFieldType.STRING).withLabel("Assign to user ID").withIsRequired(true));
|
||||
@@ -173,7 +192,7 @@ public final class EdiProcesses
|
||||
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output)
|
||||
{
|
||||
count += input.getRecords().size();
|
||||
output.getRecords().addAll(input.getRecords());
|
||||
input.getRecords().forEach(record -> output.getRecords().add(new QRecord().withValue("id", record.getValue("id"))));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -218,6 +237,20 @@ public final class EdiProcesses
|
||||
{
|
||||
lines.add(new ProcessSummaryLine(Status.OK, 1, "public certificate exported"));
|
||||
}
|
||||
else if(operation.equals("download"))
|
||||
{
|
||||
if(!QContext.getQSession().hasPermission("edi.raw.download"))
|
||||
{
|
||||
throw new QException("Raw document download is not authorized");
|
||||
}
|
||||
var storage = new com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput(com.kingsrook.qbits.edi.storage.PreviewDownloadBackend.TABLE)
|
||||
.withReference("raw:" + table + ":" + row.getValueInteger("id"));
|
||||
com.kingsrook.qqq.backend.core.actions.processes.ProcessFileDownload.registerStorage(storage);
|
||||
output.addValue("downloadFileName", "edi-document.edi");
|
||||
output.addValue("storageTableName", storage.getTableName());
|
||||
output.addValue("storageReference", storage.getReference());
|
||||
lines.add(new ProcessSummaryLine(Status.OK, 1, "Document ready for authorized download"));
|
||||
}
|
||||
else if(operation.equals("preview"))
|
||||
{
|
||||
QRecord map = table.equals("edi_map") ? row : EdiRecords.query("edi_map", EdiRecords.equal("id", row.getValue("mapId"))).getFirst();
|
||||
@@ -288,13 +321,25 @@ public final class EdiProcesses
|
||||
QRecord update = new QRecord().withValue("id", row.getValue("id")).withValue("status", operation.equals("assign") ? "IN_PROGRESS" : operation.equals("ignore") ? "IGNORED" : "RESOLVED");
|
||||
if(operation.equals("assign"))
|
||||
{
|
||||
update.withValue("assignedToUserId", input.getValue("assignedToUserId"));
|
||||
String assignee = input.getValueString("assignedToUserId");
|
||||
if(assignee == null || assignee.isBlank())
|
||||
{
|
||||
throw new IllegalArgumentException("An assignee is required");
|
||||
}
|
||||
update.withValue("assignedToUserId", assignee);
|
||||
}
|
||||
else
|
||||
{
|
||||
update.withValue("resolvedBy", user).withValue("resolvedAt", EdiRuntime.current().config().getClock().instant());
|
||||
}
|
||||
SystemWrites.update(table, update);
|
||||
if(operation.equals("assign"))
|
||||
{
|
||||
QAlerts.notify(new NotificationSignal("EDI exception assigned")
|
||||
.withBody("An EDI exception has been assigned to you.")
|
||||
.withAudience(NotificationAudience.user(update.getValueString("assignedToUserId")))
|
||||
.withSourceName("edi").withSourceTableName(table).withSourceRecordId(row.getValueString("id")));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -316,7 +361,7 @@ public final class EdiProcesses
|
||||
output.addValue("storageTableName", storage.getTableName());
|
||||
output.addValue("storageReference", ref.value());
|
||||
}
|
||||
output.getRecords().add(selected);
|
||||
output.getRecords().add(new QRecord().withValue("id", selected.getValue("id")));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
|
||||
@@ -61,7 +61,11 @@ public class OutboundProcesses implements BackendStep
|
||||
.withStep(new QBackendStepMetaData().withName("execute").withCode(new QCodeReference(OutboundProcesses.class)));
|
||||
if(config.getSchedulerName() != null && (seconds != null || cron != null))
|
||||
{
|
||||
process.withSchedule(new QScheduleMetaData().withSchedulerName(config.getSchedulerName()).withRepeatSeconds(seconds).withCronExpression(cron).withCronTimeZoneId("UTC"));
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// core validation refuses a cron time zone on a repeat schedule //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
process.withSchedule(new QScheduleMetaData().withSchedulerName(config.getSchedulerName()).withRepeatSeconds(seconds).withCronExpression(cron)
|
||||
.withCronTimeZoneId(cron == null ? null : "UTC"));
|
||||
}
|
||||
return (process);
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* 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.security;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1142, removeBy=4.1.0-RC.6). Field classifications on RC.5.
|
||||
*******************************************************************************/
|
||||
public class EdiClassifications extends QSupplementalFieldMetaData
|
||||
{
|
||||
private final String classification;
|
||||
|
||||
/*******************************************************************************
|
||||
** Retain classification as metadata until the core enum is available.
|
||||
*******************************************************************************/
|
||||
public EdiClassifications(String classification)
|
||||
{
|
||||
this.classification = classification;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Stable supplemental metadata key.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getType()
|
||||
{
|
||||
return ("edi.classification");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Read the classification without requiring an RC.6 class.
|
||||
*******************************************************************************/
|
||||
public String getClassification()
|
||||
{
|
||||
return (classification);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Bridge when a host supplies the newer field classification API.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void enrich(QInstance instance, QFieldMetaData field)
|
||||
{
|
||||
try
|
||||
{
|
||||
Class<?> type = Class.forName("com.kingsrook.qqq.backend.core.actions.ai.DataClassification");
|
||||
Object value = type.getField(classification).get(null);
|
||||
field.getClass().getMethod("setDataClassification", type).invoke(field, value);
|
||||
}
|
||||
catch(ClassNotFoundException e)
|
||||
{
|
||||
// RC.5 keeps the explicit supplemental classification below.
|
||||
}
|
||||
catch(ReflectiveOperationException e)
|
||||
{
|
||||
throw new IllegalStateException("Update the EDI classification bridge for this core version", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.security;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Marker;
|
||||
import org.apache.logging.log4j.core.LogEvent;
|
||||
import org.apache.logging.log4j.core.Logger;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.filter.AbstractFilter;
|
||||
import org.apache.logging.log4j.message.Message;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1147, removeBy=4.1.0-RC.7).
|
||||
** RC.6 logs query criteria verbatim. Suppress synchronous diagnostic logging only
|
||||
** while PHI mapping/application runs; value-free EDI events remain in the database.
|
||||
** This does not replace host masking or protection of host code outside this scope.
|
||||
*******************************************************************************/
|
||||
public final class PhiLogScope implements AutoCloseable
|
||||
{
|
||||
private static final ThreadLocal<Integer> DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||
private static final Map<Configuration, Boolean> INSTALLED = new WeakHashMap<>();
|
||||
private final int previous;
|
||||
|
||||
/*******************************************************************************
|
||||
** Protect nested PHI work without changing logger levels or unrelated threads.
|
||||
*******************************************************************************/
|
||||
private PhiLogScope(boolean enabled)
|
||||
{
|
||||
previous = DEPTH.get();
|
||||
if(enabled)
|
||||
{
|
||||
install();
|
||||
DEPTH.set(previous + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Open a scope around code that may pass clinical values to RC.6 core actions.
|
||||
*******************************************************************************/
|
||||
public static PhiLogScope open(boolean enabled)
|
||||
{
|
||||
return (new PhiLogScope(enabled));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Install once per logger configuration, including after a host reconfiguration.
|
||||
*******************************************************************************/
|
||||
private static synchronized void install()
|
||||
{
|
||||
if(!(LogManager.getContext(false) instanceof LoggerContext context))
|
||||
{
|
||||
throw new IllegalStateException("PHI processing requires the RC.6 Log4j protection bridge");
|
||||
}
|
||||
Configuration configuration = context.getConfiguration();
|
||||
if(!INSTALLED.containsKey(configuration))
|
||||
{
|
||||
ScopeFilter filter = new ScopeFilter();
|
||||
filter.start();
|
||||
configuration.addFilter(filter);
|
||||
INSTALLED.put(configuration, true);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Always restore nesting, including failed mapping or target transactions.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
if(previous == 0)
|
||||
{
|
||||
DEPTH.remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
DEPTH.set(previous);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Pre-event filter prevents sensitive diagnostics from reaching any appender.
|
||||
*******************************************************************************/
|
||||
private static class ScopeFilter extends AbstractFilter
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Decide before Log4j creates or formats a data-bearing event.
|
||||
*******************************************************************************/
|
||||
private Result decision()
|
||||
{
|
||||
return (DEPTH.get() > 0 ? Result.DENY : Result.NEUTRAL);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** QLogger uses structured Log4j messages.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, Message message, Throwable error)
|
||||
{
|
||||
return (decision());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Object diagnostics from third-party mapping or target customizers.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, Object message, Throwable error)
|
||||
{
|
||||
return (decision());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Parameterized diagnostics from registered host extensions.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object... parameters)
|
||||
{
|
||||
return (decision());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Synchronous event filtering for configurations invoking this filter later.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Result filter(LogEvent event)
|
||||
{
|
||||
return (decision());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,17 @@ public final class EdiRecords
|
||||
return (new QueryAction().execute(new QueryInput(table).withFilter(filter).withShouldFetchHeavyFields(true).withTransaction(com.kingsrook.qqq.backend.core.context.QContext.getQBackendTransaction())).getRecords());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** UI-facing reads enforce permissions even when the host disables default enforcement.
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> queryEnforced(String table, QQueryFilter filter) throws QException
|
||||
{
|
||||
QueryInput input = new QueryInput(table).withFilter(filter).withShouldFetchHeavyFields(true)
|
||||
.withTransaction(com.kingsrook.qqq.backend.core.context.QContext.getQBackendTransaction());
|
||||
input.setPermissionEnforcement(com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement.ENFORCE);
|
||||
return (new QueryAction().execute(input).getRecords());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Match one field by exact value.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -71,6 +71,10 @@ public class PreviewDownloadBackend implements QBackendModuleInterface, QStorage
|
||||
{
|
||||
throw new QException("Preview download is not authorized for this session");
|
||||
}
|
||||
if(input.getReference().startsWith("raw:"))
|
||||
{
|
||||
return (RawDocumentDownload.open(input.getReference()));
|
||||
}
|
||||
return (SystemWrites.call(() -> EdiRuntime.current().payloads().open(new PayloadRef(input.getReference(), null, 0))));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.storage;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.edi.EdiRuntime;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Stream an authorized archived document, with a value-free access event per download.
|
||||
*******************************************************************************/
|
||||
public final class RawDocumentDownload
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Recheck permission and partner locks on every access, including registered URLs.
|
||||
*******************************************************************************/
|
||||
public static InputStream open(String reference) throws QException
|
||||
{
|
||||
String[] parts = reference.split(":");
|
||||
if(parts.length != 3 || !parts[0].equals("raw") || !List.of("edi_inbox", "edi_interchange").contains(parts[1])
|
||||
|| QContext.getQSession() == null || !QContext.getQSession().hasPermission("edi.raw.download"))
|
||||
{
|
||||
throw new QException("Raw document download is not authorized");
|
||||
}
|
||||
Integer id;
|
||||
try
|
||||
{
|
||||
id = Integer.valueOf(parts[2]);
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw new QException("Invalid raw document reference");
|
||||
}
|
||||
QueryInput query = new QueryInput(parts[1]).withFilter(EdiRecords.equal("id", id));
|
||||
query.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
|
||||
List<QRecord> rows = new QueryAction().execute(query).getRecords();
|
||||
if(rows.size() != 1 || rows.getFirst().getValueString("payloadRef") == null)
|
||||
{
|
||||
throw new QException("Raw document is unavailable");
|
||||
}
|
||||
QRecord row = rows.getFirst();
|
||||
String user = QContext.getQSession().getUser() == null ? "system" : java.util.Objects.toString(QContext.getQSession().getUser().getIdReference(), "system");
|
||||
return (SystemWrites.call(() ->
|
||||
{
|
||||
SystemWrites.insert("edi_event", new QRecord().withValue("type", "RAW_DOWNLOADED").withValue("userName", user)
|
||||
.withValue("inboxId", parts[1].equals("edi_inbox") ? id : row.getValueInteger("inboxId"))
|
||||
.withValue("interchangeId", parts[1].equals("edi_interchange") ? id : null)
|
||||
.withValue("at", EdiRuntime.current().config().getClock().instant()).withValue("detail", "Raw document download authorized"));
|
||||
return (EdiRuntime.current().payloads().open(new PayloadRef(row.getValueString("payloadRef"), null, 0)));
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ public class HttpsTransport implements EdiTransportPlugin
|
||||
bytes = stream.readAllBytes();
|
||||
}
|
||||
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(transport.getHttpsUrl())).timeout(Duration.ofSeconds(60))
|
||||
.header("Content-Type", "application/edi-x12").POST(HttpRequest.BodyPublishers.ofByteArray(bytes));
|
||||
.header("Content-Type", new String(bytes, java.nio.charset.StandardCharsets.US_ASCII).stripLeading().startsWith("UN") ? "application/EDIFACT" : "application/edi-x12").POST(HttpRequest.BodyPublishers.ofByteArray(bytes));
|
||||
if(transport.getHttpsHeadersJson() != null)
|
||||
{
|
||||
Map<String, String> headers = new ObjectMapper().readValue(transport.getHttpsHeadersJson(), new TypeReference<Map<String, String>>() {});
|
||||
|
||||
@@ -50,7 +50,8 @@ public class ControlNumberAllocator
|
||||
*******************************************************************************/
|
||||
public long allocate(Integer agreementId, Integer localIdentityId, String level, long min, long max, boolean wrap) throws SQLException
|
||||
{
|
||||
if((agreementId == null) == (localIdentityId == null) || !List.of("ISA", "GS").contains(level) || min < 1 || max < min || max > 999999999)
|
||||
long ceiling = List.of("UNB", "UNG", "UNH").contains(level) ? 99999999999999L : 999999999;
|
||||
if((agreementId == null) == (localIdentityId == null) || !List.of("ISA", "GS", "UNB", "UNG", "UNH").contains(level) || min < 1 || max < min || max > ceiling)
|
||||
{
|
||||
throw new IllegalArgumentException("Invalid control number scope or bounds");
|
||||
}
|
||||
@@ -100,7 +101,7 @@ public class ControlNumberAllocator
|
||||
return (min);
|
||||
}
|
||||
}
|
||||
if(minimum < 1 || maximum < minimum || maximum > 999999999 || previous < minimum - 1 || previous > maximum)
|
||||
if(minimum < 1 || maximum < minimum || maximum > ceiling || previous < minimum - 1 || previous > maximum)
|
||||
{
|
||||
throw new IllegalStateException("Stored control number range is invalid");
|
||||
}
|
||||
|
||||
@@ -55,6 +55,18 @@ public class DuplicateDetector
|
||||
return (!EdiRecords.query("edi_interchange", filter).isEmpty());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** EDIFACT duplicates use complete UNB identities, reference and test indicator.
|
||||
*******************************************************************************/
|
||||
public boolean edifact(Integer agreementId, com.kingsrook.qbits.edi.edifact.EdifactInterchange input, int days, Instant now) throws QException
|
||||
{
|
||||
return (!EdiRecords.query("edi_interchange", EdiRecords.equal("agreementId", agreementId).withCriteria("direction", QCriteriaOperator.EQUALS, "INBOUND")
|
||||
.withCriteria("standard", QCriteriaOperator.EQUALS, "EDIFACT").withCriteria("senderId", QCriteriaOperator.EQUALS, input.unb().value(2))
|
||||
.withCriteria("senderQualifier", QCriteriaOperator.EQUALS, input.unb().value(2, 1, 2)).withCriteria("receiverId", QCriteriaOperator.EQUALS, input.unb().value(3))
|
||||
.withCriteria("receiverQualifier", QCriteriaOperator.EQUALS, input.unb().value(3, 1, 2)).withCriteria("controlNumber", QCriteriaOperator.EQUALS, input.unb().value(5))
|
||||
.withCriteria("usageIndicator", QCriteriaOperator.EQUALS, input.usage()).withCriteria("createDate", QCriteriaOperator.GREATER_THAN_OR_EQUALS, now.minus(days, ChronoUnit.DAYS))).isEmpty());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Check GS06 for a sender in the duplicate window.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -27,6 +27,15 @@ import java.util.List;
|
||||
/*******************************************************************************
|
||||
** Buffered transaction set, including its ST and SE.
|
||||
*******************************************************************************/
|
||||
public record X12TransactionSet(X12Segment st, X12Segment se, List<X12Segment> segments, List<EnvelopeFinding> findings)
|
||||
public record X12TransactionSet(X12Segment st, X12Segment se, List<X12Segment> segments, List<EnvelopeFinding> findings) implements com.kingsrook.qbits.edi.document.EdiDocument
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Expose the header to standard-neutral validators and maps.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public X12Segment header()
|
||||
{
|
||||
return (st);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
{
|
||||
"name": "QQQ baseline guide 270 005010X279A1",
|
||||
"standard": "X12",
|
||||
"version": "005010X279A1",
|
||||
"transactionSet": "270",
|
||||
"functionalIdCode": "HS",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "ST",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "270"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"fixedValue": "005010X279A1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BHT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 4
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "DT",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000A",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "20"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100A",
|
||||
"parentLoopId": "2000A",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "PR"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000B",
|
||||
"parentLoopId": "2000A",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "21"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100B",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "1P"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000C",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "22"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "TRN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 2,
|
||||
"loopId": "2000C",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "IL"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N3",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 55
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N4",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 30
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DMG",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "DT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "EQ",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2110C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"codes": [
|
||||
"30"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "SE",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 10
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
{
|
||||
"name": "QQQ baseline guide 271 005010X279A1",
|
||||
"standard": "X12",
|
||||
"version": "005010X279A1",
|
||||
"transactionSet": "271",
|
||||
"functionalIdCode": "HB",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "ST",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "271"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"fixedValue": "005010X279A1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BHT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 4
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "DT",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000A",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "20"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100A",
|
||||
"parentLoopId": "2000A",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "PR"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000B",
|
||||
"parentLoopId": "2000A",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "21"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100B",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "1P"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000C",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "22"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "TRN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 2,
|
||||
"loopId": "2000C",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "IL"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N3",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 55
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N4",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 30
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DMG",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "DT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "AAA",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "EB",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2110C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 7,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HSD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2110C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2110C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2110C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "AAA",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2110C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "SE",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 10
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"type": "SITUATIONAL",
|
||||
"expression": "when(present(TRN02), or(present(EB), present(AAA)))",
|
||||
"snipLevel": 4,
|
||||
"severity": "ERROR",
|
||||
"message": "Benefits or rejection reason required for inquiry response"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,762 @@
|
||||
{
|
||||
"name": "QQQ baseline guide 276 005010X212",
|
||||
"standard": "X12",
|
||||
"version": "005010X212",
|
||||
"transactionSet": "276",
|
||||
"functionalIdCode": "HR",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "ST",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "276"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"fixedValue": "005010X212"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BHT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 4
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "DT",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000A",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "20"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100A",
|
||||
"parentLoopId": "2000A",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "PR"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000B",
|
||||
"parentLoopId": "2000A",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "21"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100B",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "1P"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000C",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "22"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "TRN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 2,
|
||||
"loopId": "2000C",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "IL"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N3",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 55
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N4",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 30
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DMG",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "DT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "TRN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "STC",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "DT",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "AMT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "SVC",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2220C",
|
||||
"parentLoopId": "2200C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2220C",
|
||||
"parentLoopId": "2200C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "SE",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 10
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,762 @@
|
||||
{
|
||||
"name": "QQQ baseline guide 277 005010X212",
|
||||
"standard": "X12",
|
||||
"version": "005010X212",
|
||||
"transactionSet": "277",
|
||||
"functionalIdCode": "HN",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "ST",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "277"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"fixedValue": "005010X212"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BHT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 4
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "DT",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000A",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "20"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100A",
|
||||
"parentLoopId": "2000A",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "PR"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000B",
|
||||
"parentLoopId": "2000A",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "21"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100B",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "1P"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HL",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000C",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 12
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "22"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"0",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "TRN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 2,
|
||||
"loopId": "2000C",
|
||||
"parentLoopId": "2000B",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "IL"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N3",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 55
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N4",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 30
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DMG",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "DT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2100C",
|
||||
"parentLoopId": "2000C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "TRN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "STC",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "DT",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "AMT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2200C",
|
||||
"parentLoopId": "2100C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "SVC",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2220C",
|
||||
"parentLoopId": "2200C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2220C",
|
||||
"parentLoopId": "2200C",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "SE",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 10
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
{
|
||||
"name": "QQQ baseline guide 834 005010X220A1",
|
||||
"standard": "X12",
|
||||
"version": "005010X220A1",
|
||||
"transactionSet": "834",
|
||||
"functionalIdCode": "BE",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "ST",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "834"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"fixedValue": "005010X220A1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BGN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "DT",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "TM",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N1",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "1000A",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "P5"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N1",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "1000B",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "IN"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "INS",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"Y",
|
||||
"N"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"type": "SITUATIONAL",
|
||||
"expression": "when(eq(INS01, 'Y'), present(DMG))",
|
||||
"snipLevel": 4,
|
||||
"severity": "ERROR",
|
||||
"message": "Subscriber demographics required",
|
||||
"ik3Code": "8",
|
||||
"ik4Code": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 10,
|
||||
"loopId": "2000",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2000",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100A",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "IL"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"codes": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N3",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100A",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 55
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N4",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100A",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 30
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2100A",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DMG",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100A",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "DT",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "HD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2300",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTP",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2300",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"codes": [
|
||||
"D8",
|
||||
"RD8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2300",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "SE",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 10
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,951 @@
|
||||
{
|
||||
"name": "QQQ baseline guide 835 005010X221A1",
|
||||
"standard": "X12",
|
||||
"version": "005010X221A1",
|
||||
"transactionSet": "835",
|
||||
"functionalIdCode": "HP",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "ST",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"fixedValue": "835"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"fixedValue": "005010X221A1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BPR",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "R",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 1
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 16,
|
||||
"dataType": "DT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "TRN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5
|
||||
},
|
||||
{
|
||||
"segmentId": "N1",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "1000A",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "PR"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N3",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "1000A",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 55
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N4",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "1000A",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 30
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "1000A",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N1",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "1000B",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"fixedValue": "PE"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N3",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "1000B",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 55
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "N4",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "1000B",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 30
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "1000B",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "LX",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2000",
|
||||
"parentLoopId": null,
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CLP",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 38
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "R",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "R",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 5,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CAS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 99,
|
||||
"loopId": "2100",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "R",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 5,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 6,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 7,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 10,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 11,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 12,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 13,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 14,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 15,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 16,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 17,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 18,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 19,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "NM1",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 10,
|
||||
"loopId": "2100",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 3
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 60
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "AN",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "MIA",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "MOA",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2100",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 10,
|
||||
"loopId": "2100",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 10,
|
||||
"loopId": "2100",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "AMT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 10,
|
||||
"loopId": "2100",
|
||||
"parentLoopId": "2000",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "SVC",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"loopId": "2110",
|
||||
"parentLoopId": "2100",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"componentPosition": 2,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "HCPCS"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "R",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "R",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2110",
|
||||
"parentLoopId": "2100",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "CAS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 99,
|
||||
"loopId": "2110",
|
||||
"parentLoopId": "2100",
|
||||
"loopMaxRepeat": 999,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "R",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 5,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 6,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 7,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 9,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 10,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 11,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 12,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 13,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 14,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 15,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 16,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
},
|
||||
{
|
||||
"position": 17,
|
||||
"dataType": "ID",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 5,
|
||||
"source": "EXTERNAL",
|
||||
"externalCodeSet": "CARC"
|
||||
},
|
||||
{
|
||||
"position": 18,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 19,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "REF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 10,
|
||||
"loopId": "2110",
|
||||
"parentLoopId": "2100",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "AMT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 5,
|
||||
"loopId": "2110",
|
||||
"parentLoopId": "2100",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "LQ",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 20,
|
||||
"loopId": "2110",
|
||||
"parentLoopId": "2100",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "PLB",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 99,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 50
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "DT",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 8
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "R",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 6,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 8,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 10,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 12,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
},
|
||||
{
|
||||
"position": 14,
|
||||
"dataType": "R",
|
||||
"usage": "SITUATIONAL",
|
||||
"minLength": 1,
|
||||
"maxLength": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "SE",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 10
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"usage": "REQUIRED",
|
||||
"minLength": 1,
|
||||
"maxLength": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "QQQ baseline 999 005010X231A1",
|
||||
"name": "QQQ baseline guide 999 005010X231A1",
|
||||
"standard": "X12",
|
||||
"version": "005010X231A1",
|
||||
"transactionSet": "999",
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
{
|
||||
"name": "QQQ baseline CONTRL D01B",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D01B",
|
||||
"transactionSet": "CONTRL",
|
||||
"functionalIdCode": "CONTRL",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"CONTRL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"01B"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UCI",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 4,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 4,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 5,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UCM",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "UCS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG1"
|
||||
},
|
||||
{
|
||||
"segmentId": "UCD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 99,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"12",
|
||||
"13",
|
||||
"14",
|
||||
"15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG1"
|
||||
},
|
||||
{
|
||||
"segmentId": "UCF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG3",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "UCM",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
],
|
||||
"loopId": "SG4",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG3"
|
||||
},
|
||||
{
|
||||
"segmentId": "UCS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
],
|
||||
"loopId": "SG5",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG4"
|
||||
},
|
||||
{
|
||||
"segmentId": "UCD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 99,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"12",
|
||||
"13",
|
||||
"14",
|
||||
"15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG5",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG4"
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
{
|
||||
"name": "QQQ baseline CONTRL D96A",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D96A",
|
||||
"transactionSet": "CONTRL",
|
||||
"functionalIdCode": "CONTRL",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"CONTRL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"96A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UCI",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 4,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 4,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 5,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UCM",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "UCS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG1"
|
||||
},
|
||||
{
|
||||
"segmentId": "UCD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 99,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"12",
|
||||
"13",
|
||||
"14",
|
||||
"15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG1"
|
||||
},
|
||||
{
|
||||
"segmentId": "UCF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG3",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "UCM",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
],
|
||||
"loopId": "SG4",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG3"
|
||||
},
|
||||
{
|
||||
"segmentId": "UCS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL"
|
||||
}
|
||||
],
|
||||
"loopId": "SG5",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG4"
|
||||
},
|
||||
{
|
||||
"segmentId": "UCD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 99,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"12",
|
||||
"13",
|
||||
"14",
|
||||
"15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG5",
|
||||
"loopMaxRepeat": 999,
|
||||
"parentLoopId": "SG4"
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
{
|
||||
"name": "QQQ baseline DESADV D01B",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D01B",
|
||||
"transactionSet": "DESADV",
|
||||
"functionalIdCode": "DESADV",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"DESADV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"01B"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BGM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"351"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"9",
|
||||
"29"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 5,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"137",
|
||||
"11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "DT",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"102"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "RFF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "NAD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"BY",
|
||||
"SU",
|
||||
"DP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"9"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "CPS",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 12,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
],
|
||||
"loopId": "SG10",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "PAC",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
],
|
||||
"loopId": "SG10",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "LIN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"EN",
|
||||
"SA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG17",
|
||||
"loopMaxRepeat": 9999,
|
||||
"parentLoopId": "SG10"
|
||||
},
|
||||
{
|
||||
"segmentId": "QTY",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"12"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"PCE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG17",
|
||||
"loopMaxRepeat": 9999,
|
||||
"parentLoopId": "SG10"
|
||||
},
|
||||
{
|
||||
"segmentId": "UNS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"S"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CNT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
{
|
||||
"name": "QQQ baseline DESADV D96A",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D96A",
|
||||
"transactionSet": "DESADV",
|
||||
"functionalIdCode": "DESADV",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"DESADV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"96A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BGM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"351"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"9",
|
||||
"29"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 5,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"137",
|
||||
"11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "DT",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"102"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "RFF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "NAD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"BY",
|
||||
"SU",
|
||||
"DP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"9"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "CPS",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 12,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
],
|
||||
"loopId": "SG10",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "PAC",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
],
|
||||
"loopId": "SG10",
|
||||
"loopMaxRepeat": 999
|
||||
},
|
||||
{
|
||||
"segmentId": "LIN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"EN",
|
||||
"SA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG17",
|
||||
"loopMaxRepeat": 9999,
|
||||
"parentLoopId": "SG10"
|
||||
},
|
||||
{
|
||||
"segmentId": "QTY",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"12"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"PCE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG17",
|
||||
"loopMaxRepeat": 9999,
|
||||
"parentLoopId": "SG10"
|
||||
},
|
||||
{
|
||||
"segmentId": "UNS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"S"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CNT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
{
|
||||
"name": "QQQ baseline INVOIC D01B",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D01B",
|
||||
"transactionSet": "INVOIC",
|
||||
"functionalIdCode": "INVOIC",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"INVOIC"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"01B"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BGM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"380"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"9",
|
||||
"29"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 5,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"137",
|
||||
"11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "DT",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"102"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "RFF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "NAD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"BY",
|
||||
"SU",
|
||||
"DP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"9"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "LIN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"EN",
|
||||
"SA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "QTY",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"47"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"PCE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "PRI",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"AAA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "UNS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"S"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CNT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "MOA",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"77"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 18,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
{
|
||||
"name": "QQQ baseline INVOIC D96A",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D96A",
|
||||
"transactionSet": "INVOIC",
|
||||
"functionalIdCode": "INVOIC",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"INVOIC"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"96A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BGM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"380"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"9",
|
||||
"29"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 5,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"137",
|
||||
"11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "DT",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"102"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "RFF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "NAD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"BY",
|
||||
"SU",
|
||||
"DP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"9"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "LIN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"EN",
|
||||
"SA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "QTY",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"47"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"PCE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "PRI",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"AAA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "UNS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"S"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CNT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "MOA",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"77"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 18,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
{
|
||||
"name": "QQQ baseline ORDERS D01B",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D01B",
|
||||
"transactionSet": "ORDERS",
|
||||
"functionalIdCode": "ORDERS",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ORDERS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"01B"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BGM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"220"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"9",
|
||||
"29"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 5,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"137",
|
||||
"11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "DT",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"102"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "RFF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "NAD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"BY",
|
||||
"SU",
|
||||
"DP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"9"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "LIN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"EN",
|
||||
"SA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "QTY",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"21"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"PCE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "PRI",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"AAA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "UNS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"S"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CNT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
{
|
||||
"name": "QQQ baseline ORDERS D96A",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D96A",
|
||||
"transactionSet": "ORDERS",
|
||||
"functionalIdCode": "ORDERS",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ORDERS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"96A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BGM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"220"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"9",
|
||||
"29"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 5,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"137",
|
||||
"11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "DT",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"102"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "RFF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "NAD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"BY",
|
||||
"SU",
|
||||
"DP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"9"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "LIN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"EN",
|
||||
"SA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "QTY",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"21"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"PCE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "PRI",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"AAA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "UNS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"S"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CNT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
{
|
||||
"name": "QQQ baseline ORDRSP D01B",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D01B",
|
||||
"transactionSet": "ORDRSP",
|
||||
"functionalIdCode": "ORDRSP",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ORDRSP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"01B"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BGM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"231"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"9",
|
||||
"29"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 5,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"137",
|
||||
"11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "DT",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"102"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "RFF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "NAD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"BY",
|
||||
"SU",
|
||||
"DP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"9"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "LIN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"EN",
|
||||
"SA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "QTY",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"21"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"PCE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "PRI",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"AAA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "UNS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"S"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CNT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
{
|
||||
"name": "QQQ baseline ORDRSP D96A",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D96A",
|
||||
"transactionSet": "ORDRSP",
|
||||
"functionalIdCode": "ORDRSP",
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "UNH",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ORDRSP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"96A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 4,
|
||||
"codes": [
|
||||
"UN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "BGM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"231"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"9",
|
||||
"29"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "DTM",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 5,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"137",
|
||||
"11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "DT",
|
||||
"minLength": 1,
|
||||
"maxLength": 8,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"102"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "RFF",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"ON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG1",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "NAD",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"BY",
|
||||
"SU",
|
||||
"DP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "SITUATIONAL",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"9"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG2",
|
||||
"loopMaxRepeat": 10
|
||||
},
|
||||
{
|
||||
"segmentId": "LIN",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 6,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 35,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2,
|
||||
"codes": [
|
||||
"EN",
|
||||
"SA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "QTY",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"21"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 3,
|
||||
"codes": [
|
||||
"PCE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "PRI",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"AAA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "R",
|
||||
"minLength": 1,
|
||||
"maxLength": 15,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
],
|
||||
"loopId": "SG25",
|
||||
"loopMaxRepeat": 9999
|
||||
},
|
||||
{
|
||||
"segmentId": "UNS",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 1,
|
||||
"usage": "REQUIRED",
|
||||
"codes": [
|
||||
"S"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "CNT",
|
||||
"usage": "SITUATIONAL",
|
||||
"minUse": 0,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "ID",
|
||||
"minLength": 1,
|
||||
"maxLength": 3,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 1,
|
||||
"codes": [
|
||||
"2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED",
|
||||
"componentPosition": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"segmentId": "UNT",
|
||||
"usage": "REQUIRED",
|
||||
"minUse": 1,
|
||||
"maxUse": 1,
|
||||
"elements": [
|
||||
{
|
||||
"position": 1,
|
||||
"dataType": "N0",
|
||||
"minLength": 1,
|
||||
"maxLength": 10,
|
||||
"usage": "REQUIRED"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"dataType": "AN",
|
||||
"minLength": 1,
|
||||
"maxLength": 14,
|
||||
"usage": "REQUIRED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"map": {
|
||||
"name": "DESADV D01B retail sample",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D01B",
|
||||
"transactionSet": "DESADV",
|
||||
"direction": "OUTBOUND",
|
||||
"status": "ACTIVE",
|
||||
"revision": 1,
|
||||
"tableName": "sample_shipment",
|
||||
"businessKeyPath": "BGM02",
|
||||
"businessKeyType": "SHIPMENT"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"ediPath": "BGM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"351\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"ediPath": "BGM02",
|
||||
"qqqPath": "shipmentNumber",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"ediPath": "BGM03",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"9\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"ediPath": "DTM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"137\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"ediPath": "DTM01-2",
|
||||
"qqqPath": "shipDate",
|
||||
"required": true,
|
||||
"transform": "DATE"
|
||||
},
|
||||
{
|
||||
"sequence": 6,
|
||||
"ediPath": "DTM01-3",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"102\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 7,
|
||||
"ediPath": "SG10/CPS01",
|
||||
"qqqPath": "cartons.sscc",
|
||||
"required": true,
|
||||
"transform": "SEQUENCE"
|
||||
},
|
||||
{
|
||||
"sequence": 8,
|
||||
"ediPath": "SG10/PAC01",
|
||||
"qqqPath": "cartons.sscc",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"1\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 9,
|
||||
"ediPath": "SG10/SG17/LIN01",
|
||||
"qqqPath": "cartons.items.sku",
|
||||
"required": true,
|
||||
"transform": "SEQUENCE"
|
||||
},
|
||||
{
|
||||
"sequence": 10,
|
||||
"ediPath": "SG10/SG17/LIN03-1",
|
||||
"qqqPath": "cartons.items.sku",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 11,
|
||||
"ediPath": "SG10/SG17/LIN03-2",
|
||||
"qqqPath": "cartons.items.sku",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"SA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 12,
|
||||
"ediPath": "SG10/SG17/QTY01-1",
|
||||
"qqqPath": "cartons.items.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"12\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 13,
|
||||
"ediPath": "SG10/SG17/QTY01-2",
|
||||
"qqqPath": "cartons.items.quantity",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 14,
|
||||
"ediPath": "SG10/SG17/QTY01-3",
|
||||
"qqqPath": "cartons.items.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"PCE\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 15,
|
||||
"ediPath": "UNS01",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"S\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"map": {
|
||||
"name": "DESADV D96A retail sample",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D96A",
|
||||
"transactionSet": "DESADV",
|
||||
"direction": "OUTBOUND",
|
||||
"status": "ACTIVE",
|
||||
"revision": 1,
|
||||
"tableName": "sample_shipment",
|
||||
"businessKeyPath": "BGM02",
|
||||
"businessKeyType": "SHIPMENT"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"ediPath": "BGM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"351\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"ediPath": "BGM02",
|
||||
"qqqPath": "shipmentNumber",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"ediPath": "BGM03",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"9\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"ediPath": "DTM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"137\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"ediPath": "DTM01-2",
|
||||
"qqqPath": "shipDate",
|
||||
"required": true,
|
||||
"transform": "DATE"
|
||||
},
|
||||
{
|
||||
"sequence": 6,
|
||||
"ediPath": "DTM01-3",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"102\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 7,
|
||||
"ediPath": "SG10/CPS01",
|
||||
"qqqPath": "cartons.sscc",
|
||||
"required": true,
|
||||
"transform": "SEQUENCE"
|
||||
},
|
||||
{
|
||||
"sequence": 8,
|
||||
"ediPath": "SG10/PAC01",
|
||||
"qqqPath": "cartons.sscc",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"1\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 9,
|
||||
"ediPath": "SG10/SG17/LIN01",
|
||||
"qqqPath": "cartons.items.sku",
|
||||
"required": true,
|
||||
"transform": "SEQUENCE"
|
||||
},
|
||||
{
|
||||
"sequence": 10,
|
||||
"ediPath": "SG10/SG17/LIN03-1",
|
||||
"qqqPath": "cartons.items.sku",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 11,
|
||||
"ediPath": "SG10/SG17/LIN03-2",
|
||||
"qqqPath": "cartons.items.sku",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"SA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 12,
|
||||
"ediPath": "SG10/SG17/QTY01-1",
|
||||
"qqqPath": "cartons.items.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"12\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 13,
|
||||
"ediPath": "SG10/SG17/QTY01-2",
|
||||
"qqqPath": "cartons.items.quantity",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 14,
|
||||
"ediPath": "SG10/SG17/QTY01-3",
|
||||
"qqqPath": "cartons.items.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"PCE\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 15,
|
||||
"ediPath": "UNS01",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"S\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"map": {
|
||||
"name": "INVOIC D01B retail sample",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D01B",
|
||||
"transactionSet": "INVOIC",
|
||||
"direction": "OUTBOUND",
|
||||
"status": "ACTIVE",
|
||||
"revision": 1,
|
||||
"tableName": "sample_invoice",
|
||||
"businessKeyPath": "BGM02",
|
||||
"businessKeyType": "INVOICE"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"ediPath": "BGM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"380\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"ediPath": "BGM02",
|
||||
"qqqPath": "invoiceNumber",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"ediPath": "BGM03",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"9\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"ediPath": "DTM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"137\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"ediPath": "DTM01-2",
|
||||
"qqqPath": "invoiceDate",
|
||||
"required": true,
|
||||
"transform": "DATE"
|
||||
},
|
||||
{
|
||||
"sequence": 6,
|
||||
"ediPath": "DTM01-3",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"102\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 7,
|
||||
"ediPath": "SG25/LIN01",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true,
|
||||
"transform": "SEQUENCE"
|
||||
},
|
||||
{
|
||||
"sequence": 8,
|
||||
"ediPath": "SG25/LIN03-1",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 9,
|
||||
"ediPath": "SG25/LIN03-2",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"SA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 10,
|
||||
"ediPath": "SG25/QTY01-1",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"47\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 11,
|
||||
"ediPath": "SG25/QTY01-2",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 12,
|
||||
"ediPath": "SG25/QTY01-3",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"PCE\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 13,
|
||||
"ediPath": "SG25/PRI01-1",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"AAA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 14,
|
||||
"ediPath": "SG25/PRI01-2",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 15,
|
||||
"ediPath": "UNS01",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"S\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 16,
|
||||
"ediPath": "MOA01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"77\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 17,
|
||||
"ediPath": "MOA01-2",
|
||||
"qqqPath": "total",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"map": {
|
||||
"name": "INVOIC D96A retail sample",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D96A",
|
||||
"transactionSet": "INVOIC",
|
||||
"direction": "OUTBOUND",
|
||||
"status": "ACTIVE",
|
||||
"revision": 1,
|
||||
"tableName": "sample_invoice",
|
||||
"businessKeyPath": "BGM02",
|
||||
"businessKeyType": "INVOICE"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"ediPath": "BGM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"380\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"ediPath": "BGM02",
|
||||
"qqqPath": "invoiceNumber",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"ediPath": "BGM03",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"9\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"ediPath": "DTM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"137\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"ediPath": "DTM01-2",
|
||||
"qqqPath": "invoiceDate",
|
||||
"required": true,
|
||||
"transform": "DATE"
|
||||
},
|
||||
{
|
||||
"sequence": 6,
|
||||
"ediPath": "DTM01-3",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"102\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 7,
|
||||
"ediPath": "SG25/LIN01",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true,
|
||||
"transform": "SEQUENCE"
|
||||
},
|
||||
{
|
||||
"sequence": 8,
|
||||
"ediPath": "SG25/LIN03-1",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 9,
|
||||
"ediPath": "SG25/LIN03-2",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"SA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 10,
|
||||
"ediPath": "SG25/QTY01-1",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"47\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 11,
|
||||
"ediPath": "SG25/QTY01-2",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 12,
|
||||
"ediPath": "SG25/QTY01-3",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"PCE\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 13,
|
||||
"ediPath": "SG25/PRI01-1",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"AAA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 14,
|
||||
"ediPath": "SG25/PRI01-2",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 15,
|
||||
"ediPath": "UNS01",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"S\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 16,
|
||||
"ediPath": "MOA01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"77\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 17,
|
||||
"ediPath": "MOA01-2",
|
||||
"qqqPath": "total",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"map": {
|
||||
"name": "ORDERS D01B retail sample",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D01B",
|
||||
"transactionSet": "ORDERS",
|
||||
"direction": "INBOUND",
|
||||
"status": "ACTIVE",
|
||||
"revision": 1,
|
||||
"tableName": "sample_order",
|
||||
"businessKeyPath": "BGM02",
|
||||
"businessKeyType": "PO",
|
||||
"matchKeyFields": "poNumber"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"ediPath": "BGM02",
|
||||
"qqqPath": "poNumber",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"ediPath": "DTM[DTM01-1=137]/DTM01-2",
|
||||
"qqqPath": "orderDate",
|
||||
"required": true,
|
||||
"transform": "DATE"
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"ediPath": "NAD[NAD01=BY]/NAD02-1",
|
||||
"qqqPath": "buyerId",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"ediPath": "SG25[*]/LIN/LIN03-1",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"ediPath": "SG25[*]/QTY/QTY01-2",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 6,
|
||||
"ediPath": "SG25[*]/PRI/PRI01-2",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": false,
|
||||
"transform": "DECIMAL",
|
||||
"condition": "present(PRI01-2)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"map": {
|
||||
"name": "ORDERS D96A retail sample",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D96A",
|
||||
"transactionSet": "ORDERS",
|
||||
"direction": "INBOUND",
|
||||
"status": "ACTIVE",
|
||||
"revision": 1,
|
||||
"tableName": "sample_order",
|
||||
"businessKeyPath": "BGM02",
|
||||
"businessKeyType": "PO",
|
||||
"matchKeyFields": "poNumber"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"ediPath": "BGM02",
|
||||
"qqqPath": "poNumber",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"ediPath": "DTM[DTM01-1=137]/DTM01-2",
|
||||
"qqqPath": "orderDate",
|
||||
"required": true,
|
||||
"transform": "DATE"
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"ediPath": "NAD[NAD01=BY]/NAD02-1",
|
||||
"qqqPath": "buyerId",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"ediPath": "SG25[*]/LIN/LIN03-1",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"ediPath": "SG25[*]/QTY/QTY01-2",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 6,
|
||||
"ediPath": "SG25[*]/PRI/PRI01-2",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": false,
|
||||
"transform": "DECIMAL",
|
||||
"condition": "present(PRI01-2)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"map": {
|
||||
"name": "ORDRSP D01B retail sample",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D01B",
|
||||
"transactionSet": "ORDRSP",
|
||||
"direction": "OUTBOUND",
|
||||
"status": "ACTIVE",
|
||||
"revision": 1,
|
||||
"tableName": "sample_order",
|
||||
"businessKeyPath": "BGM02",
|
||||
"businessKeyType": "PO"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"ediPath": "BGM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"231\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"ediPath": "BGM02",
|
||||
"qqqPath": "poNumber",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"ediPath": "BGM03",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"29\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"ediPath": "DTM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"137\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"ediPath": "DTM01-2",
|
||||
"qqqPath": "orderDate",
|
||||
"required": true,
|
||||
"transform": "DATE"
|
||||
},
|
||||
{
|
||||
"sequence": 6,
|
||||
"ediPath": "DTM01-3",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"102\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 7,
|
||||
"ediPath": "SG25/LIN01",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true,
|
||||
"transform": "SEQUENCE"
|
||||
},
|
||||
{
|
||||
"sequence": 8,
|
||||
"ediPath": "SG25/LIN03-1",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 9,
|
||||
"ediPath": "SG25/LIN03-2",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"SA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 10,
|
||||
"ediPath": "SG25/QTY01-1",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"21\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 11,
|
||||
"ediPath": "SG25/QTY01-2",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 12,
|
||||
"ediPath": "SG25/QTY01-3",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"PCE\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 13,
|
||||
"ediPath": "SG25/PRI01-1",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"AAA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 14,
|
||||
"ediPath": "SG25/PRI01-2",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 15,
|
||||
"ediPath": "UNS01",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"S\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"map": {
|
||||
"name": "ORDRSP D96A retail sample",
|
||||
"standard": "EDIFACT",
|
||||
"version": "D96A",
|
||||
"transactionSet": "ORDRSP",
|
||||
"direction": "OUTBOUND",
|
||||
"status": "ACTIVE",
|
||||
"revision": 1,
|
||||
"tableName": "sample_order",
|
||||
"businessKeyPath": "BGM02",
|
||||
"businessKeyType": "PO"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"ediPath": "BGM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"231\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 2,
|
||||
"ediPath": "BGM02",
|
||||
"qqqPath": "poNumber",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 3,
|
||||
"ediPath": "BGM03",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"29\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 4,
|
||||
"ediPath": "DTM01-1",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"137\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 5,
|
||||
"ediPath": "DTM01-2",
|
||||
"qqqPath": "orderDate",
|
||||
"required": true,
|
||||
"transform": "DATE"
|
||||
},
|
||||
{
|
||||
"sequence": 6,
|
||||
"ediPath": "DTM01-3",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"102\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 7,
|
||||
"ediPath": "SG25/LIN01",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true,
|
||||
"transform": "SEQUENCE"
|
||||
},
|
||||
{
|
||||
"sequence": 8,
|
||||
"ediPath": "SG25/LIN03-1",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"sequence": 9,
|
||||
"ediPath": "SG25/LIN03-2",
|
||||
"qqqPath": "lines.sku",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"SA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 10,
|
||||
"ediPath": "SG25/QTY01-1",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"21\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 11,
|
||||
"ediPath": "SG25/QTY01-2",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 12,
|
||||
"ediPath": "SG25/QTY01-3",
|
||||
"qqqPath": "lines.quantity",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"PCE\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 13,
|
||||
"ediPath": "SG25/PRI01-1",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"AAA\"}"
|
||||
},
|
||||
{
|
||||
"sequence": 14,
|
||||
"ediPath": "SG25/PRI01-2",
|
||||
"qqqPath": "lines.unitPrice",
|
||||
"required": true,
|
||||
"transform": "DECIMAL"
|
||||
},
|
||||
{
|
||||
"sequence": 15,
|
||||
"ediPath": "UNS01",
|
||||
"qqqPath": null,
|
||||
"required": true,
|
||||
"transform": "CONSTANT",
|
||||
"transformArgs": "{\"value\": \"S\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
components:
|
||||
schemas:
|
||||
EdiAckV1:
|
||||
description: "Concatenated acknowledgement previews; type is 997, 999, TA1,\
|
||||
\ CONTRL, NONE or MIXED. text is null for PHI."
|
||||
properties:
|
||||
text:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "object"
|
||||
EdiAssociationV1:
|
||||
description: "Direct QQQ association and its target fields."
|
||||
properties:
|
||||
fields:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiFieldV1"
|
||||
type: "array"
|
||||
label:
|
||||
nullable: true
|
||||
type: "string"
|
||||
name:
|
||||
nullable: true
|
||||
type: "string"
|
||||
table:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "object"
|
||||
EdiDiffV1:
|
||||
description: "Only differing records are returned. Both lists are empty for\
|
||||
\ PHI."
|
||||
properties:
|
||||
againstExpected:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiDifferenceV1"
|
||||
type: "array"
|
||||
againstPreviousRevision:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiDifferenceV1"
|
||||
type: "array"
|
||||
type: "object"
|
||||
EdiDifferenceV1:
|
||||
description: "One document comparison; documentIndex is zero based."
|
||||
properties:
|
||||
after:
|
||||
additionalProperties: true
|
||||
nullable: true
|
||||
type: "object"
|
||||
before:
|
||||
description: "Expected JSON or a previous mapped record."
|
||||
nullable: true
|
||||
documentIndex:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
type: "object"
|
||||
EdiElementV1:
|
||||
description: "Element or composite component; codes contain at most 200 active\
|
||||
\ codes in lexical order."
|
||||
properties:
|
||||
codeListId:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
codes:
|
||||
items:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "array"
|
||||
component:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
id:
|
||||
nullable: true
|
||||
type: "string"
|
||||
maxLength:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
minLength:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
name:
|
||||
nullable: true
|
||||
type: "string"
|
||||
position:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
type:
|
||||
nullable: true
|
||||
type: "string"
|
||||
usage:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "object"
|
||||
EdiErrorV1:
|
||||
description: "Core middleware error body."
|
||||
properties:
|
||||
error:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "object"
|
||||
EdiFieldV1:
|
||||
description: "QQQ target field metadata."
|
||||
properties:
|
||||
label:
|
||||
nullable: true
|
||||
type: "string"
|
||||
name:
|
||||
nullable: true
|
||||
type: "string"
|
||||
possibleValueSourceName:
|
||||
nullable: true
|
||||
type: "string"
|
||||
required:
|
||||
type: "boolean"
|
||||
type:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "object"
|
||||
EdiFindingV1:
|
||||
description: "Value-free diagnostic. PHI messages are generic; only positions\
|
||||
\ and codes are retained."
|
||||
properties:
|
||||
code:
|
||||
nullable: true
|
||||
type: "string"
|
||||
elementPosition:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
loopId:
|
||||
nullable: true
|
||||
type: "string"
|
||||
message:
|
||||
nullable: true
|
||||
type: "string"
|
||||
segmentId:
|
||||
nullable: true
|
||||
type: "string"
|
||||
segmentPosition:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
severity:
|
||||
nullable: true
|
||||
type: "string"
|
||||
snipLevel:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
type: "object"
|
||||
EdiGuideTreeV1:
|
||||
description: "Ordered guide tree. Root segments are outside loops; nested loops\
|
||||
\ retain parent scope."
|
||||
properties:
|
||||
loops:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiLoopV1"
|
||||
type: "array"
|
||||
segments:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiSegmentV1"
|
||||
type: "array"
|
||||
type: "object"
|
||||
EdiLoopV1:
|
||||
description: "Guide loop, nested within its parent; repeat is the maximum occurrence\
|
||||
\ count."
|
||||
properties:
|
||||
id:
|
||||
nullable: true
|
||||
type: "string"
|
||||
loops:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiLoopV1"
|
||||
type: "array"
|
||||
repeat:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
segments:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiSegmentV1"
|
||||
type: "array"
|
||||
type: "object"
|
||||
EdiParseV1:
|
||||
description: "Counts of wire interchanges, groups and documents."
|
||||
properties:
|
||||
documents:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
groups:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
interchanges:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
type: "object"
|
||||
EdiPreviewRequestV1:
|
||||
description: "mapId is required. Supply exactly one of sampleId or sampleBase64.\
|
||||
\ Uploads are at most 5 MiB decoded, stored encrypted. ruleOverrides replaces\
|
||||
\ the whole rule set in memory. CURRENT (default) uses the newest active guide\
|
||||
\ in the pinned guide family; ORIGINAL uses the pinned guide."
|
||||
properties:
|
||||
guideRevision:
|
||||
enum:
|
||||
- "CURRENT"
|
||||
- "ORIGINAL"
|
||||
nullable: true
|
||||
type: "string"
|
||||
mapId:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
ruleOverrides:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiRuleV1"
|
||||
nullable: true
|
||||
type: "array"
|
||||
sampleBase64:
|
||||
format: "byte"
|
||||
maxLength: 6990508
|
||||
nullable: true
|
||||
type: "string"
|
||||
sampleId:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
sampleName:
|
||||
maxLength: 255
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "object"
|
||||
EdiPreviewV1:
|
||||
description: "Parse, validate, acknowledge and map without target writes. PHI\
|
||||
\ suppresses result, ack text, and both diffs."
|
||||
properties:
|
||||
ack:
|
||||
$ref: "#/components/schemas/EdiAckV1"
|
||||
diff:
|
||||
$ref: "#/components/schemas/EdiDiffV1"
|
||||
findings:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiFindingV1"
|
||||
type: "array"
|
||||
parse:
|
||||
$ref: "#/components/schemas/EdiParseV1"
|
||||
phiSuppressed:
|
||||
type: "boolean"
|
||||
result:
|
||||
$ref: "#/components/schemas/EdiResultV1"
|
||||
nullable: true
|
||||
type: "object"
|
||||
EdiRecordV1:
|
||||
description: "Field values and recursively associated records. Values use the\
|
||||
\ target table field types."
|
||||
properties:
|
||||
associations:
|
||||
additionalProperties:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiRecordV1"
|
||||
type: "array"
|
||||
type: "object"
|
||||
values:
|
||||
additionalProperties: true
|
||||
type: "object"
|
||||
type: "object"
|
||||
EdiResultV1:
|
||||
description: "Mapped records in document order; no target writes."
|
||||
properties:
|
||||
records:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiRecordV1"
|
||||
type: "array"
|
||||
type: "object"
|
||||
EdiRuleErrorsV1:
|
||||
description: "Correlate errors using zero-based index, optional id and sequence."
|
||||
properties:
|
||||
errors:
|
||||
items:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "array"
|
||||
id:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
index:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
sequence:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
type: "object"
|
||||
EdiRuleV1:
|
||||
description: "Map rule. id is optional for unsaved rules. transformArgs is a\
|
||||
\ JSON object encoded as a string, as in core table CRUD."
|
||||
properties:
|
||||
condition:
|
||||
nullable: true
|
||||
type: "string"
|
||||
defaultValue:
|
||||
nullable: true
|
||||
type: "string"
|
||||
ediPath:
|
||||
nullable: true
|
||||
type: "string"
|
||||
id:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
qqqPath:
|
||||
nullable: true
|
||||
type: "string"
|
||||
required:
|
||||
nullable: true
|
||||
type: "boolean"
|
||||
sequence:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
transform:
|
||||
nullable: true
|
||||
type: "string"
|
||||
transformArgs:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "object"
|
||||
EdiSegmentV1:
|
||||
description: "Segment definition in guide sequence order."
|
||||
properties:
|
||||
elements:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiElementV1"
|
||||
type: "array"
|
||||
id:
|
||||
nullable: true
|
||||
type: "string"
|
||||
max:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
min:
|
||||
nullable: true
|
||||
type: "integer"
|
||||
name:
|
||||
nullable: true
|
||||
type: "string"
|
||||
position:
|
||||
nullable: true
|
||||
type: "string"
|
||||
usage:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "object"
|
||||
EdiTargetV1:
|
||||
description: "Target metadata visible to the caller, current rules and registered\
|
||||
\ transforms."
|
||||
properties:
|
||||
associations:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiAssociationV1"
|
||||
type: "array"
|
||||
fields:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiFieldV1"
|
||||
type: "array"
|
||||
rules:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiRuleV1"
|
||||
type: "array"
|
||||
table:
|
||||
nullable: true
|
||||
type: "string"
|
||||
transforms:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiTransformV1"
|
||||
type: "array"
|
||||
type: "object"
|
||||
EdiTransformV1:
|
||||
description: "Registered transform and JSON Schema describing its string-valued\
|
||||
\ arguments."
|
||||
properties:
|
||||
argumentSchema:
|
||||
additionalProperties: true
|
||||
description: "JSON Schema: type, properties, required and additionalProperties.\
|
||||
\ All transform arguments are strings."
|
||||
type: "object"
|
||||
name:
|
||||
nullable: true
|
||||
type: "string"
|
||||
type: "object"
|
||||
EdiValidateRequestV1:
|
||||
description: "rules is required and may be empty; rules are never persisted."
|
||||
properties:
|
||||
rules:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiRuleV1"
|
||||
type: "array"
|
||||
type: "object"
|
||||
EdiValidationV1:
|
||||
description: "One result per submitted rule, including rules with no errors."
|
||||
properties:
|
||||
rules:
|
||||
items:
|
||||
$ref: "#/components/schemas/EdiRuleErrorsV1"
|
||||
type: "array"
|
||||
type: "object"
|
||||
securitySchemes:
|
||||
sessionUuidCookie:
|
||||
in: "cookie"
|
||||
name: "sessionUUID"
|
||||
type: "apiKey"
|
||||
info:
|
||||
contact:
|
||||
email: "contact@kingsrook.com"
|
||||
description: "EDI sandbox and mapper, using the signed-in user's permissions."
|
||||
title: "QQQ Middleware API extension: edi"
|
||||
version: "1.0.0"
|
||||
openapi: "3.0.3"
|
||||
paths:
|
||||
/qqq/v1/qbit/edi/guides/{guideId}/tree:
|
||||
get:
|
||||
description: "Read the guide tree. Uses the caller session and table read permissions.\
|
||||
\ Preview also requires edi.map.preview. Rule writes use core table CRUD."
|
||||
parameters:
|
||||
- description: "Positive record identifier."
|
||||
in: "path"
|
||||
name: "guideId"
|
||||
required: true
|
||||
schema:
|
||||
type: "integer"
|
||||
responses:
|
||||
400:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Invalid request or sample exceeds 5 MiB"
|
||||
401:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Sign in required"
|
||||
403:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Permission denied"
|
||||
404:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Missing or invisible record"
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiGuideTreeV1"
|
||||
description: "Successful Response"
|
||||
413:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Host HTTP request size limit exceeded"
|
||||
security:
|
||||
- sessionUuidCookie:
|
||||
- "N/A"
|
||||
summary: "Read the guide tree"
|
||||
/qqq/v1/qbit/edi/maps/{mapId}/target:
|
||||
get:
|
||||
description: "Read target metadata and map rules. Uses the caller session and\
|
||||
\ table read permissions. Preview also requires edi.map.preview. Rule writes\
|
||||
\ use core table CRUD."
|
||||
parameters:
|
||||
- description: "Positive record identifier."
|
||||
in: "path"
|
||||
name: "mapId"
|
||||
required: true
|
||||
schema:
|
||||
type: "integer"
|
||||
responses:
|
||||
400:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Invalid request or sample exceeds 5 MiB"
|
||||
401:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Sign in required"
|
||||
403:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Permission denied"
|
||||
404:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Missing or invisible record"
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiTargetV1"
|
||||
description: "Successful Response"
|
||||
413:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Host HTTP request size limit exceeded"
|
||||
security:
|
||||
- sessionUuidCookie:
|
||||
- "N/A"
|
||||
summary: "Read target metadata and map rules"
|
||||
/qqq/v1/qbit/edi/sandbox/preview:
|
||||
post:
|
||||
description: "Preview an encrypted sample with optional unsaved rules. Uses\
|
||||
\ the caller session and table read permissions. Preview also requires edi.map.preview.\
|
||||
\ Rule writes use core table CRUD."
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiPreviewRequestV1"
|
||||
required: true
|
||||
responses:
|
||||
400:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Invalid request or sample exceeds 5 MiB"
|
||||
401:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Sign in required"
|
||||
403:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Permission denied"
|
||||
404:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Missing or invisible record"
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiPreviewV1"
|
||||
description: "Successful Response"
|
||||
413:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Host HTTP request size limit exceeded"
|
||||
security:
|
||||
- sessionUuidCookie:
|
||||
- "N/A"
|
||||
summary: "Preview an encrypted sample with optional unsaved rules"
|
||||
/qqq/v1/qbit/edi/maps/{mapId}/validate:
|
||||
post:
|
||||
description: "Validate unsaved map rules. Uses the caller session and table\
|
||||
\ read permissions. Preview also requires edi.map.preview. Rule writes use\
|
||||
\ core table CRUD."
|
||||
parameters:
|
||||
- description: "Positive record identifier."
|
||||
in: "path"
|
||||
name: "mapId"
|
||||
required: true
|
||||
schema:
|
||||
type: "integer"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiValidateRequestV1"
|
||||
required: true
|
||||
responses:
|
||||
400:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Invalid request or sample exceeds 5 MiB"
|
||||
401:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Sign in required"
|
||||
403:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Permission denied"
|
||||
404:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Missing or invisible record"
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiValidationV1"
|
||||
description: "Successful Response"
|
||||
413:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EdiErrorV1"
|
||||
description: "Host HTTP request size limit exceeded"
|
||||
security:
|
||||
- sessionUuidCookie:
|
||||
- "N/A"
|
||||
summary: "Validate unsaved map rules"
|
||||
@@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueSearchFilterUseCase;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
@@ -39,7 +40,7 @@ import org.junit.jupiter.params.provider.CsvSource;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/*******************************************************************************
|
||||
** Contingent metadata and real RC.5 possible-value searches with form values.
|
||||
** Contingent metadata and real RC.6 possible-value searches with form values.
|
||||
*******************************************************************************/
|
||||
class ContingenciesTest
|
||||
{
|
||||
@@ -123,7 +124,7 @@ class ContingenciesTest
|
||||
{
|
||||
var field = instance.getTable(pair[0]).getField(pair[1]);
|
||||
QQueryFilter interpreted = field.getPossibleValueSourceFilter().clone();
|
||||
// RC.5 middleware interprets the field filter before calling the search action.
|
||||
// RC.6 middleware interprets the field filter before calling the search action.
|
||||
interpreted.interpretValues(Map.of("partnerId", 1));
|
||||
var result = new SearchPossibleValueSourceAction().execute(new SearchPossibleValueSourceInput().withPossibleValueSourceName(field.getPossibleValueSourceName())
|
||||
.withDefaultQueryFilter(interpreted).withOtherValues(Map.of("partnerId", 1)).withSearchTerm("").withLimit(100));
|
||||
@@ -133,6 +134,41 @@ class ContingenciesTest
|
||||
assertThat(instance.getTable("edi_exception").getField("assignedToUserId").getPossibleValueSourceFilter()).isNull();
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** RC.6 lacks dependency behavior metadata; shared transports still work with no partner.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void sharedTransportBeforeAndAfterPartnerSelection() throws Exception
|
||||
{
|
||||
var instance = H2Support.create();
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
for(Integer partner : java.util.Arrays.asList(null, 1, 2))
|
||||
{
|
||||
SystemWrites.insert("edi_transport", new QRecord().withValue("name", "van-" + partner).withValue("partnerId", partner)
|
||||
.withValue("type", "GATEWAY").withValue("direction", "BOTH").withValue("gatewayEndpointName", "synthetic-outbox"));
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
for(var values : List.of(Map.<String, java.io.Serializable>of(), Map.<String, java.io.Serializable>of("partnerId", 1)))
|
||||
{
|
||||
var field = instance.getTable("edi_agreement").getField("outboundTransportId");
|
||||
var filter = field.getPossibleValueSourceFilter().clone();
|
||||
filter.interpretValues(Map.of("input", values), PossibleValueSearchFilterUseCase.FORM);
|
||||
var result = new SearchPossibleValueSourceAction().execute(new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName(field.getPossibleValueSourceName()).withDefaultQueryFilter(filter)
|
||||
.withOtherValues(values).withSearchTerm("").withLimit(100));
|
||||
assertThat(result.getResults()).extracting(value -> value.getId().toString()).contains("1").doesNotContain("3");
|
||||
assertThat(result.getResults()).hasSize(values.isEmpty() ? 1 : 2);
|
||||
filter = field.getPossibleValueSourceFilter().clone();
|
||||
filter.interpretValues(Map.of("input", values), PossibleValueSearchFilterUseCase.FORM);
|
||||
var unrelated = new SearchPossibleValueSourceAction().execute(new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName(field.getPossibleValueSourceName()).withDefaultQueryFilter(filter)
|
||||
.withOtherValues(values).withSearchTerm("van-2").withLimit(100));
|
||||
assertThat(unrelated.getResults()).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Test fixture operation.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import com.kingsrook.qbits.edi.api.EdiApi;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtensionsMetaData;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
|
||||
import com.kingsrook.qqq.middleware.javalin.tools.UiApiContract;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/*******************************************************************************
|
||||
** Generated extension fragment is the exact versioned contract shipped to clients.
|
||||
*******************************************************************************/
|
||||
class EdiApiContractTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Use RC.6's real helper, including compatibility comparison against this first baseline.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void contract() throws Exception
|
||||
{
|
||||
var version = new MiddlewareVersionV1();
|
||||
var extension = EdiApi.extension();
|
||||
String generated = UiApiContract.generateExtensionYaml(version, extension);
|
||||
Path path = Path.of("src/main/resources/openapi/qqq-v1/edi.yaml").toAbsolutePath();
|
||||
if(Boolean.getBoolean("edi.openapi.update"))
|
||||
{
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, generated);
|
||||
}
|
||||
String committed = Files.readString(path);
|
||||
assertThat(UiApiContract.check(generated, committed, committed, 1,
|
||||
"mvnw.cmd -B -Dtest=EdiApiContractTest -Dedi.openapi.update=true test")).isEmpty();
|
||||
var model = version.generateExtensionOpenAPIModel("qqq", extension);
|
||||
assertThat(model.getComponents().getSchemas().keySet()).allMatch(n -> n.startsWith("Edi") && n.endsWith("V1"));
|
||||
assertThat(model.getPaths()).hasSize(4);
|
||||
assertThat(model.getPaths().keySet()).allMatch(n -> n.startsWith("/qqq/v1/qbit/edi/"));
|
||||
assertThat(extension.getEffectivePathPrefix()).isEqualTo("/qbit/edi");
|
||||
assertThat(extension.getSemanticVersion()).isEqualTo("1.0.0");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Producer registers the extension and all metadata validates with RC.6.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void registration() throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
var instance = H2Support.create();
|
||||
assertThat(MiddlewareSpecExtensionsMetaData.ofOrWithNew(instance).getExtensions()).hasSize(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
com.kingsrook.qqq.backend.core.context.QContext.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
/*
|
||||
* 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.net.ServerSocket;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kingsrook.qbits.edi.api.EdiApiService;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.storage.PayloadKind;
|
||||
import com.kingsrook.qbits.edi.storage.PayloadRef;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
|
||||
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
|
||||
import com.kingsrook.qqq.middleware.javalin.QJavalinImplementation;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/*******************************************************************************
|
||||
** Real HTTP against strict-mode QQQ, real SQL, real encrypted samples and caller sessions.
|
||||
*******************************************************************************/
|
||||
class EdiApiHttpTest
|
||||
{
|
||||
private static final ObjectMapper JSON = new ObjectMapper();
|
||||
private QApplicationJavalinServer server;
|
||||
private HttpClient client;
|
||||
private QInstance instance;
|
||||
private QRecord map;
|
||||
private QRecord sample;
|
||||
private String baseUrl;
|
||||
private String suffix;
|
||||
|
||||
/*******************************************************************************
|
||||
** The existing synthetic vertical-slice fixture supplies tables, guide and map.
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
void start() throws Exception
|
||||
{
|
||||
new InboundTest().setup("004010");
|
||||
instance = QContext.getQInstance();
|
||||
map = EdiRecords.query("edi_map", new QQueryFilter()).getFirst();
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
SystemWrites.update("edi_map", new QRecord().withValue("id", map.getValue("id")).withValue("partnerId", 1));
|
||||
SystemWrites.update("edi_guide", new QRecord().withValue("id", map.getValue("guideId")).withValue("partnerId", 1));
|
||||
return (null);
|
||||
});
|
||||
map.withValue("partnerId", 1);
|
||||
sample = saveSample(Synthetic850.generate(17, "004010", com.kingsrook.qbits.edi.x12.X12Separators.DEFAULT), map.getValueInteger("id"));
|
||||
instance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withName("mock").withType(QAuthenticationType.MOCK)
|
||||
.withCustomizer(new QCodeReference(Sessions.class)));
|
||||
int port;
|
||||
try(ServerSocket socket = new ServerSocket(0))
|
||||
{
|
||||
port = socket.getLocalPort();
|
||||
}
|
||||
baseUrl = "http://127.0.0.1:" + port + "/qqq/v1/qbit/edi";
|
||||
suffix = "-" + UUID.randomUUID();
|
||||
client = HttpClient.newHttpClient();
|
||||
server = new QApplicationJavalinServer(new AbstractQQQApplication()
|
||||
{
|
||||
@Override
|
||||
public QInstance defineQInstance()
|
||||
{
|
||||
return (instance);
|
||||
}
|
||||
}).withPort(port).withServeFrontendNext(false).withServeFrontendMaterialDashboard(false)
|
||||
.withServeLegacyUnversionedMiddlewareAPI(false).withStrictUiApiRoutes(true)
|
||||
.withJavalinConfigCustomizer(config -> config.http.maxRequestSize = 8 * 1024 * 1024);
|
||||
server.start();
|
||||
QContext.init(instance, new QSystemUserSession(instance));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Release server globals and thread context.
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void stop()
|
||||
{
|
||||
if(server != null)
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
if(client != null)
|
||||
{
|
||||
client.close();
|
||||
}
|
||||
QJavalinImplementation.setSessionCookieHttpOnly(false);
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Ordinary users receive selected reads; none receives raw-payload access.
|
||||
*******************************************************************************/
|
||||
public static class Sessions implements QAuthenticationModuleCustomizerInterface
|
||||
{
|
||||
@Override
|
||||
public void customizeSession(QInstance instance, QSession session, Map<String, Object> context)
|
||||
{
|
||||
String role = session.getUuid().split("-")[0];
|
||||
session.withUser(new QUser().withIdReference(role));
|
||||
session.withSecurityKeyValue("edi_partner", 1);
|
||||
if(!role.equals("denied"))
|
||||
{
|
||||
for(String table : instance.getTables().keySet())
|
||||
{
|
||||
if(!table.equals("edi_payload") && !table.equals("edi_preview_download")
|
||||
&& !(role.equals("noguide") && table.equals("edi_guide"))
|
||||
&& !(role.equals("norules") && table.equals("edi_map_rule"))
|
||||
&& !(role.equals("notarget") && table.equals("sample_order")))
|
||||
{
|
||||
String permission = table.startsWith("edi_") ? "edi." + table.substring(4) + ".read"
|
||||
: table.equals("sample_order") ? "sample.order.read" : "sample.line.read";
|
||||
session.withPermissions(permission);
|
||||
}
|
||||
}
|
||||
if(!role.equals("reader"))
|
||||
{
|
||||
session.withPermissions("edi.map.preview");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Transport helper retains the exact response for egress scans.
|
||||
*******************************************************************************/
|
||||
private HttpResponse<String> request(String method, String path, Object body, String role) throws Exception
|
||||
{
|
||||
String text = body == null ? "" : body instanceof String s ? s : JSON.writeValueAsString(body);
|
||||
return (client.send(HttpRequest.newBuilder(URI.create(baseUrl + path)).header("Cookie", "sessionUUID=" + role + suffix)
|
||||
.header("Content-Type", "application/json").method(method, HttpRequest.BodyPublishers.ofString(text)).build(), HttpResponse.BodyHandlers.ofString()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Assert success before parsing the wire JSON.
|
||||
*******************************************************************************/
|
||||
private JsonNode ok(String method, String path, Object body) throws Exception
|
||||
{
|
||||
var response = request(method, path, body, "user");
|
||||
assertThat(response.statusCode()).as(response.body()).isEqualTo(200);
|
||||
return (JSON.readTree(response.body()));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** An authorized fixture writer archives synthetic bytes only.
|
||||
*******************************************************************************/
|
||||
private QRecord saveSample(String text, Integer mapId) throws Exception
|
||||
{
|
||||
return (SystemWrites.call(() ->
|
||||
{
|
||||
var ref = EdiRuntime.current().payloads().put(PayloadKind.SAMPLE, InboundTest.bytes(text));
|
||||
return (EdiRecords.insert("edi_map_sample", new QRecord().withValue("name", "Synthetic HTTP sample").withValue("mapId", mapId).withValue("payloadRef", ref.value())));
|
||||
}));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Shared stored-sample request.
|
||||
*******************************************************************************/
|
||||
private Map<String, Object> preview()
|
||||
{
|
||||
return (Map.of("mapId", map.getValueInteger("id"), "sampleId", sample.getValueInteger("id")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Guide structure and target schema include associations, rules and argument contracts.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void guideAndTarget() throws Exception
|
||||
{
|
||||
var tree = ok("GET", "/guides/" + map.getValue("guideId") + "/tree", null);
|
||||
assertThat(tree.get("loops")).isNotEmpty();
|
||||
assertThat(tree.toString()).contains("\"id\":\"BEG\"", "\"position\":\"0030\"", "codeListId", "minLength");
|
||||
var target = ok("GET", "/maps/" + map.getValue("id") + "/target", null);
|
||||
assertThat(target.get("table").asText()).isEqualTo("sample_order");
|
||||
assertThat(target.get("fields").toString()).contains("poNumber", "possibleValueSourceName");
|
||||
assertThat(target.get("associations").toString()).contains("lines", "sample_order_line", "quantity");
|
||||
assertThat(target.get("rules")).hasSize(5);
|
||||
assertThat(target.get("transforms").toString()).contains("LOOKUP", "argumentSchema", "required", "table", "key");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Each operation returns real 403 responses for denied table or action permissions.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void permissions() throws Exception
|
||||
{
|
||||
String target = "/maps/" + map.getValue("id") + "/target";
|
||||
String validate = "/maps/" + map.getValue("id") + "/validate";
|
||||
assertThat(request("GET", "/guides/" + map.getValue("guideId") + "/tree", null, "denied").statusCode()).isEqualTo(403);
|
||||
assertThat(request("GET", target, null, "denied").statusCode()).isEqualTo(403);
|
||||
assertThat(request("POST", validate, Map.of("rules", List.of()), "denied").statusCode()).isEqualTo(403);
|
||||
assertThat(request("POST", "/sandbox/preview", preview(), "denied").statusCode()).isEqualTo(403);
|
||||
assertThat(request("POST", "/sandbox/preview", preview(), "reader").statusCode()).isEqualTo(403);
|
||||
assertThat(request("GET", target, null, "notarget").statusCode()).isEqualTo(403);
|
||||
assertThat(request("GET", target, null, "norules").statusCode()).isEqualTo(403);
|
||||
assertThat(request("GET", "/guides/" + map.getValue("guideId") + "/tree", null, "noguide").statusCode()).isEqualTo(403);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Preview returns the mapped structure and ack while leaving target tables untouched.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void storedPreview() throws Exception
|
||||
{
|
||||
var result = ok("POST", "/sandbox/preview", preview());
|
||||
assertThat(result.at("/parse/interchanges").asInt()).isEqualTo(1);
|
||||
assertThat(result.at("/parse/groups").asInt()).isEqualTo(1);
|
||||
assertThat(result.at("/parse/documents").asInt()).isEqualTo(1);
|
||||
assertThat(result.at("/result/records/0/values/poNumber").asText()).isNotBlank();
|
||||
assertThat(result.at("/result/records/0/associations/lines")).hasSize(1);
|
||||
assertThat(result.at("/ack/type").asText()).isEqualTo("997");
|
||||
assertThat(result.at("/ack/text").asText()).contains("AK9");
|
||||
assertThat(result.get("phiSuppressed").asBoolean()).isFalse();
|
||||
assertThat(EdiRecords.query("sample_order", new QQueryFilter())).isEmpty();
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Unsaved overrides replace rules in memory and custom transforms see the caller.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void unsavedOverridesAndCallerSession() throws Exception
|
||||
{
|
||||
EdiRuntime.current().transforms().register("SESSION", (values, args) -> QContext.getQSession().getUser().getIdReference(),
|
||||
Map.of("type", "object", "properties", Map.of(), "required", List.of()));
|
||||
var rule = Map.of("sequence", 1, "ediPath", "BEG03", "qqqPath", "poNumber", "transform", "SESSION");
|
||||
var body = new java.util.LinkedHashMap<>(preview());
|
||||
body.put("ruleOverrides", List.of(rule));
|
||||
var result = ok("POST", "/sandbox/preview", body);
|
||||
assertThat(result.at("/result/records/0/values/poNumber").asText()).isEqualTo("user");
|
||||
assertThat(EdiRecords.query("edi_map_rule", EdiRecords.equal("mapId", map.getValue("id")))).hasSize(5);
|
||||
assertThat(ok("POST", "/sandbox/preview", preview()).at("/result/records/0/values/poNumber").asText()).isNotEqualTo("user");
|
||||
body.put("ruleOverrides", List.of());
|
||||
assertThat(ok("POST", "/sandbox/preview", body).at("/result/records/0/values").size()).isZero();
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Base64 uploads become encrypted samples, with no plaintext in the payload table.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void upload() throws Exception
|
||||
{
|
||||
String raw = Synthetic850.generate(18, "004010", com.kingsrook.qbits.edi.x12.X12Separators.DEFAULT);
|
||||
int count = EdiRecords.query("edi_map_sample", new QQueryFilter()).size();
|
||||
var result = ok("POST", "/sandbox/preview", Map.of("mapId", map.getValueInteger("id"), "sampleBase64",
|
||||
Base64.getEncoder().encodeToString(raw.getBytes(StandardCharsets.US_ASCII)), "sampleName", "Uploaded sample"));
|
||||
assertThat(result.at("/parse/documents").asInt()).isEqualTo(1);
|
||||
var samples = EdiRecords.query("edi_map_sample", new QQueryFilter());
|
||||
assertThat(samples).hasSize(count + 1);
|
||||
var uploaded = samples.stream().filter(r -> "Uploaded sample".equals(r.getValueString("name"))).findFirst().orElseThrow();
|
||||
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(uploaded.getValueString("payloadRef"), null, 0)))
|
||||
{
|
||||
assertThat(new String(stream.readAllBytes(), StandardCharsets.US_ASCII)).isEqualTo(raw);
|
||||
}
|
||||
for(var payload : EdiRecords.query("edi_payload", new QQueryFilter()))
|
||||
{
|
||||
assertThat(new String((byte[]) payload.getValue("ciphertext"), StandardCharsets.ISO_8859_1)).doesNotContain("SYNTH-1", raw);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Reject oversized decoded bytes as well as encoded lengths before persistence.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void uploadLimitAndBadRequests() throws Exception
|
||||
{
|
||||
int count = EdiRecords.query("edi_map_sample", new QQueryFilter()).size();
|
||||
for(int size : List.of(EdiApiService.MAX_SAMPLE_BYTES + 1, EdiApiService.MAX_SAMPLE_BYTES + 4))
|
||||
{
|
||||
var response = request("POST", "/sandbox/preview", Map.of("mapId", map.getValueInteger("id"), "sampleBase64",
|
||||
Base64.getEncoder().encodeToString(new byte[size])), "user");
|
||||
assertThat(response.statusCode()).as(response.body()).isEqualTo(400);
|
||||
}
|
||||
for(Object body : List.of(Map.of("mapId", map.getValueInteger("id"), "sampleBase64", "!invalid!"),
|
||||
Map.of("mapId", map.getValueInteger("id")), Map.of("mapId", map.getValueInteger("id"), "sampleId", sample.getValueInteger("id"), "sampleBase64", "AAAA"),
|
||||
Map.of("mapId", map.getValueInteger("id"), "sampleId", sample.getValueInteger("id"), "guideRevision", "BAD"),
|
||||
"{\"mapId\":1.2}", "{\"mapId\":\"1\"}", "{\"mapId\":1,\"ruleOverrides\":[null]}", "[]", "null", "invalid"))
|
||||
{
|
||||
assertThat(request("POST", "/sandbox/preview", body, "user").statusCode()).isEqualTo(400);
|
||||
}
|
||||
assertThat(EdiRecords.query("edi_map_sample", new QQueryFilter())).hasSize(count);
|
||||
assertThat(request("GET", "/maps/invalid/target", null, "user").statusCode()).isEqualTo(400);
|
||||
assertThat(request("GET", "/guides/999999/tree", null, "user").statusCode()).isEqualTo(404);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** PHI is detected through hidden agreement data, and canaries never reach responses.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void phiSuppression() throws Exception
|
||||
{
|
||||
String canary = "CANARY_PRIVATE_VALUE";
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
var document = EdiRecords.query("edi_agreement_document", EdiRecords.equal("mapId", map.getValue("id"))).getFirst();
|
||||
SystemWrites.update("edi_agreement_document", new QRecord().withValue("id", document.getValue("id")).withValue("containsPhi", true));
|
||||
return (null);
|
||||
});
|
||||
var body = new java.util.LinkedHashMap<>(preview());
|
||||
body.put("ruleOverrides", List.of(Map.of("sequence", 1, "qqqPath", "poNumber", "transform", "CONSTANT", "transformArgs", "{\"value\":\"" + canary + "\"}")));
|
||||
var response = request("POST", "/sandbox/preview", body, "user");
|
||||
assertThat(response.statusCode()).as(response.body()).isEqualTo(200);
|
||||
assertThat(response.body()).doesNotContain(canary, "SYNTH-1", "poNumber");
|
||||
var json = JSON.readTree(response.body());
|
||||
assertThat(json.get("phiSuppressed").asBoolean()).isTrue();
|
||||
assertThat(json.get("result").isNull()).isTrue();
|
||||
assertThat(json.at("/ack/text").isNull()).isTrue();
|
||||
assertThat(json.at("/diff/againstExpected")).isEmpty();
|
||||
assertThat(json.at("/diff/againstPreviousRevision")).isEmpty();
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Rule errors are independently correlated; valid paths and transforms succeed.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void validateRules() throws Exception
|
||||
{
|
||||
var rules = ok("GET", "/maps/" + map.getValue("id") + "/target", null).get("rules");
|
||||
var valid = ok("POST", "/maps/" + map.getValue("id") + "/validate", Map.of("rules", rules));
|
||||
assertThat(valid.get("rules")).hasSize(5);
|
||||
valid.get("rules").forEach(rule -> assertThat(rule.get("errors")).as(rule.toString()).isEmpty());
|
||||
var invalid = List.of(
|
||||
Map.of("sequence", 1, "ediPath", "BAD99", "qqqPath", "unknown", "transform", "NO_SUCH_TRANSFORM"),
|
||||
Map.of("sequence", 2, "ediPath", "BEG03", "qqqPath", "poNumber", "transform", "LOOKUP", "transformArgs", "{}"),
|
||||
Map.of("sequence", 3, "ediPath", "PO1[*]/PO199", "qqqPath", "lines.sku", "transformArgs", "[]"),
|
||||
Map.of("sequence", 4, "ediPath", "BEG03", "qqqPath", "lines.sku", "transformArgs", "{\"index\":1}"),
|
||||
Map.of("sequence", 0, "qqqPath", "poNumber"));
|
||||
var errors = ok("POST", "/maps/" + map.getValue("id") + "/validate", Map.of("rules", invalid));
|
||||
assertThat(errors.at("/rules/0/errors")).hasSize(3);
|
||||
assertThat(errors.at("/rules/1/errors").toString()).contains("table", "key");
|
||||
assertThat(errors.at("/rules/2/errors").toString()).contains("ediPath", "transformArgs");
|
||||
assertThat(errors.at("/rules/3/errors").toString()).contains("wildcard", "transformArgs");
|
||||
assertThat(errors.at("/rules/4/errors").toString()).contains("sequence", "ediPath");
|
||||
assertThat(request("POST", "/maps/" + map.getValue("id") + "/validate", Map.of(), "user").statusCode()).isEqualTo(400);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Existing samples must belong to the selected map, and partner locks apply first.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void sampleAndPartnerIsolation() throws Exception
|
||||
{
|
||||
QRecord another = SystemWrites.call(() -> EdiRecords.insert("edi_map", new QRecord().withValue("name", "Other map").withValue("partnerId", 1)
|
||||
.withValue("guideId", map.getValue("guideId")).withValue("tableName", "sample_order").withValue("direction", "INBOUND").withValue("transactionSet", "850")));
|
||||
assertThat(request("POST", "/sandbox/preview", Map.of("mapId", another.getValueInteger("id"), "sampleId", sample.getValueInteger("id")), "user").statusCode()).isEqualTo(404);
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
SystemWrites.update("edi_map", new QRecord().withValue("id", map.getValue("id")).withValue("partnerId", 2));
|
||||
return (null);
|
||||
});
|
||||
assertThat(request("GET", "/maps/" + map.getValue("id") + "/target", null, "user").statusCode()).isEqualTo(404);
|
||||
assertThat(request("POST", "/sandbox/preview", preview(), "user").statusCode()).isEqualTo(404);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** A runtime transform failure returns a safe diagnostic instead of an exception value.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void mappingFailureIsValueFree() throws Exception
|
||||
{
|
||||
EdiRuntime.current().transforms().register("FAIL", (values, args) ->
|
||||
{
|
||||
throw new IllegalArgumentException("SECRET_CANARY");
|
||||
});
|
||||
var body = new java.util.LinkedHashMap<>(preview());
|
||||
body.put("ruleOverrides", List.of(Map.of("sequence", 1, "ediPath", "BEG03", "qqqPath", "poNumber", "transform", "FAIL")));
|
||||
var response = request("POST", "/sandbox/preview", body, "user");
|
||||
assertThat(response.statusCode()).as(response.body()).isEqualTo(200);
|
||||
assertThat(response.body()).doesNotContain("SECRET_CANARY").contains("\"code\":\"MAP\"");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** The exact decoded limit is accepted, including base64 expansion.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void exactUploadLimit() throws Exception
|
||||
{
|
||||
String raw = Synthetic850.generate(19, "004010", com.kingsrook.qbits.edi.x12.X12Separators.DEFAULT);
|
||||
String padded = raw + " ".repeat(EdiApiService.MAX_SAMPLE_BYTES - raw.length());
|
||||
var result = ok("POST", "/sandbox/preview", Map.of("mapId", map.getValueInteger("id"), "sampleBase64",
|
||||
Base64.getEncoder().encodeToString(padded.getBytes(StandardCharsets.US_ASCII))));
|
||||
assertThat(result.at("/parse/documents").asInt()).isEqualTo(1);
|
||||
assertThat(EdiRecords.query("edi_payload", new QQueryFilter())).anyMatch(r -> r.getValueLong("sizeBytes") == EdiApiService.MAX_SAMPLE_BYTES);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Required LOOKUP permissions are not borrowed from the configured service session.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void lookupCannotElevate() throws Exception
|
||||
{
|
||||
var body = new java.util.LinkedHashMap<>(preview());
|
||||
body.put("ruleOverrides", List.of(Map.of("sequence", 1, "ediPath", "BEG03", "qqqPath", "poNumber", "transform", "LOOKUP",
|
||||
"transformArgs", "{\"table\":\"sample_order\",\"key\":\"poNumber\"}")));
|
||||
var response = request("POST", "/sandbox/preview", body, "notarget");
|
||||
assertThat(response.statusCode()).as(response.body()).isEqualTo(403);
|
||||
assertThat(request("POST", "/sandbox/preview", preview(), "norules").statusCode()).isEqualTo(403);
|
||||
assertThat(request("POST", "/maps/" + map.getValue("id") + "/validate", Map.of("rules", List.of()), "noguide").statusCode()).isEqualTo(403);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** A later active guide changes CURRENT findings while ORIGINAL keeps the pinned guide.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void guideRevisionSelection() throws Exception
|
||||
{
|
||||
var repository = new com.kingsrook.qbits.edi.guide.GuideRepository();
|
||||
var guide = repository.load(map.getValueInteger("guideId"));
|
||||
var segments = new ArrayList<>(guide.segments());
|
||||
segments.add(segments.size() - 1, new com.kingsrook.qbits.edi.guide.GuideSegment("XYZ", null, null, 1, "REQUIRED", 1, 1, List.of(), List.of(), null));
|
||||
var changed = new com.kingsrook.qbits.edi.guide.Guide(guide.name(), guide.standard(), guide.version(), guide.transactionSet(), guide.functionalIdCode(), segments, guide.rules());
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
Integer id = repository.save(changed, "synthetic-http-revision", 2);
|
||||
SystemWrites.update("edi_guide", new QRecord().withValue("id", id).withValue("partnerId", 1));
|
||||
return (null);
|
||||
});
|
||||
var body = new java.util.LinkedHashMap<>(preview());
|
||||
body.put("guideRevision", "ORIGINAL");
|
||||
var original = ok("POST", "/sandbox/preview", body);
|
||||
body.put("guideRevision", "CURRENT");
|
||||
var current = ok("POST", "/sandbox/preview", body);
|
||||
assertThat(current.get("findings").size()).isGreaterThan(original.get("findings").size());
|
||||
assertThat(current.get("findings").toString()).contains("XYZ");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Previous-revision and expected diffs identify the changed document and values.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void revisionAndExpectedDiffs() throws Exception
|
||||
{
|
||||
QRecord revised = SystemWrites.call(() ->
|
||||
{
|
||||
QRecord next = new QRecord().withValue("revision", 2).withValue("partnerId", 1).withValue("guideId", map.getValue("guideId"));
|
||||
for(String field : List.of("name", "direction", "standard", "version", "transactionSet", "tableName", "status"))
|
||||
{
|
||||
next.withValue(field, map.getValue(field));
|
||||
}
|
||||
return (EdiRecords.insert("edi_map", next));
|
||||
});
|
||||
var nextSample = saveSample(Synthetic850.generate(17, "004010", com.kingsrook.qbits.edi.x12.X12Separators.DEFAULT), revised.getValueInteger("id"));
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
SystemWrites.update("edi_map_sample", new QRecord().withValue("id", nextSample.getValue("id")).withValue("expectedResultJson", "{\"values\":{\"poNumber\":\"EXPECTED\"},\"associations\":{}}"));
|
||||
return (null);
|
||||
});
|
||||
var result = ok("POST", "/sandbox/preview", Map.of("mapId", revised.getValueInteger("id"), "sampleId", nextSample.getValueInteger("id"),
|
||||
"ruleOverrides", List.of(Map.of("sequence", 1, "qqqPath", "poNumber", "transform", "CONSTANT", "transformArgs", "{\"value\":\"EDITED\"}"))));
|
||||
assertThat(result.at("/diff/againstPreviousRevision")).hasSize(1);
|
||||
assertThat(result.at("/diff/againstExpected")).hasSize(1);
|
||||
assertThat(result.at("/diff/againstPreviousRevision/0/after/values/poNumber").asText()).isEqualTo("EDITED");
|
||||
assertThat(result.at("/diff/againstExpected/0/before/values/poNumber").asText()).isEqualTo("EXPECTED");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Large code lists are deterministically capped in the guide-tree projection.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void codeListLimit() throws Exception
|
||||
{
|
||||
var definitions = EdiRecords.query("edi_guide_segment", EdiRecords.equal("guideId", map.getValue("guideId")));
|
||||
Integer codeListId = null;
|
||||
for(var definition : definitions)
|
||||
{
|
||||
for(var element : EdiRecords.query("edi_guide_element", EdiRecords.equal("guideSegmentId", definition.getValue("id"))))
|
||||
{
|
||||
if(element.getValueInteger("codeListId") != null)
|
||||
{
|
||||
codeListId = element.getValueInteger("codeListId");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(codeListId != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
final Integer listId = codeListId;
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
for(int i = 0; i < 205; i++)
|
||||
{
|
||||
EdiRecords.insert("edi_code_list_value", new QRecord().withValue("codeListId", listId).withValue("code", "SYNTH" + i).withValue("isActive", true));
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
var tree = ok("GET", "/guides/" + map.getValue("guideId") + "/tree", null);
|
||||
assertThat(tree.findValues("codes")).allSatisfy(codes -> assertThat(codes.size()).isLessThanOrEqualTo(200));
|
||||
assertThat(tree.findValues("codes")).anyMatch(codes -> codes.size() == 200);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Transaction and interchange envelope errors are represented in findings and acknowledgements.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void envelopeFindings() throws Exception
|
||||
{
|
||||
String raw = Synthetic850.generate(20, "004010", com.kingsrook.qbits.edi.x12.X12Separators.DEFAULT);
|
||||
var invalid = saveSample(raw.replace("SE*9*", "SE*99*").replace("IEA*1*", "IEA*2*"), map.getValueInteger("id"));
|
||||
var response = ok("POST", "/sandbox/preview", Map.of("mapId", map.getValueInteger("id"), "sampleId", invalid.getValueInteger("id")));
|
||||
assertThat(response.get("findings")).hasSizeGreaterThanOrEqualTo(2);
|
||||
assertThat(response.get("findings").toString()).contains("\"snipLevel\":1");
|
||||
assertThat(response.at("/ack/type").asText()).isEqualTo("MIXED");
|
||||
assertThat(response.at("/ack/text").asText()).contains("TA1*", "ISA*", "AK9*");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** A caller cannot inspect another partner's guide via a visible map.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void hiddenGuide() throws Exception
|
||||
{
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
SystemWrites.update("edi_guide", new QRecord().withValue("id", map.getValue("guideId")).withValue("partnerId", 2));
|
||||
return (null);
|
||||
});
|
||||
assertThat(request("GET", "/guides/" + map.getValue("guideId") + "/tree", null, "user").statusCode()).isEqualTo(404);
|
||||
assertThat(request("POST", "/sandbox/preview", preview(), "user").statusCode()).isEqualTo(404);
|
||||
assertThat(request("POST", "/maps/" + map.getValue("id") + "/validate", Map.of("rules", List.of()), "user").statusCode()).isEqualTo(404);
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.edi.metadata.EdiTables;
|
||||
import com.kingsrook.qbits.edi.security.EdiClassifications;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
@@ -52,6 +53,34 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*******************************************************************************/
|
||||
class EdiPresentationTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Core classification resolution preserves PHI/PII and the confidential default.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void coreClassifications() throws Exception
|
||||
{
|
||||
var instance = H2Support.create();
|
||||
for(var table : EdiTables.tables(new EdiQBitConfig().withBackendName("edi")))
|
||||
{
|
||||
assertThat(com.kingsrook.qqq.backend.core.actions.ai.AiDataClassifications.ofTable(table)).isEqualTo(DataClassification.CONFIDENTIAL);
|
||||
}
|
||||
for(String[] pair : List.of(new String[] {"edi_transaction", "phiBusinessKey"}, new String[] {"edi_transaction_error", "valueExcerpt"},
|
||||
new String[] {"edi_map_sample", "expectedResultJson"}, new String[] {"edi_inbox", "fileName"}, new String[] {"edi_payload", "ciphertext"}))
|
||||
{
|
||||
var table = instance.getTable(pair[0]);
|
||||
assertThat(com.kingsrook.qqq.backend.core.actions.ai.AiDataClassifications.ofField(table, pair[1])).isEqualTo(DataClassification.PHI);
|
||||
assertThat(AiFieldMetaData.of(table.getField(pair[1])).getExposure()).isEqualTo(com.kingsrook.qqq.backend.core.actions.ai.AiExposure.NONE);
|
||||
}
|
||||
for(String field : List.of("contactName", "contactEmail", "contactPhone"))
|
||||
{
|
||||
assertThat(com.kingsrook.qqq.backend.core.actions.ai.AiDataClassifications.ofField(instance.getTable("edi_partner"), field)).isEqualTo(DataClassification.PII);
|
||||
}
|
||||
assertThat(com.kingsrook.qqq.backend.core.actions.ai.AiDataClassifications.ofField(instance.getTable("edi_partner"), "code")).isEqualTo(DataClassification.CONFIDENTIAL);
|
||||
HealthcareSupport.tables();
|
||||
assertThat(com.kingsrook.qqq.backend.core.actions.ai.AiDataClassifications.ofField(instance.getTable("sample_claim"), "patientName")).isEqualTo(DataClassification.PHI);
|
||||
assertThat(com.kingsrook.qqq.backend.core.actions.ai.AiDataClassifications.ofField(instance.getTable("sample_claim_line"), "parentId")).isEqualTo(DataClassification.CONFIDENTIAL);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** All registered tables have readable labels and linked internal references.
|
||||
*******************************************************************************/
|
||||
@@ -79,8 +108,8 @@ class EdiPresentationTest
|
||||
}
|
||||
for(String name : table.getRecordLabelFields() == null ? List.<String>of() : table.getRecordLabelFields())
|
||||
{
|
||||
var classification = (EdiClassifications) table.getField(name).getSupplementalMetaData().get("edi.classification");
|
||||
assertThat(classification.getClassification()).as(table.getName() + "." + name).isNotEqualTo("PHI");
|
||||
var classification = AiFieldMetaData.of(table.getField(name));
|
||||
assertThat(classification.getClassification()).as(table.getName() + "." + name).isNotEqualTo(DataClassification.PHI);
|
||||
}
|
||||
}
|
||||
assertThat(instance.getTable("edi_exception").getField("assignedToUserId").getLabel()).isEqualTo("Assigned To");
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactInterchange;
|
||||
import com.kingsrook.qbits.edi.flow.InboundPayload;
|
||||
import com.kingsrook.qbits.edi.flow.InboundSink;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
/*******************************************************************************
|
||||
** Synthetic Lindqvist retail partner shared by tests and the standalone demo.
|
||||
*******************************************************************************/
|
||||
public class EdifactSupport
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Configure both CONTRL directions and the four retail documents.
|
||||
*******************************************************************************/
|
||||
public static QRecord install(String version) throws Exception
|
||||
{
|
||||
QRecord partner = SystemWrites.insert("edi_partner", new QRecord().withValue("name", "Lindqvist Retail AB").withValue("code", "LINDQVIST")
|
||||
.withValue("status", "ACTIVE").withValue("contactEmail", "lindqvist@example.invalid").withValue("defaultTimezone", "Europe/Stockholm"));
|
||||
QRecord local = SystemWrites.insert("edi_identity", new QRecord().withValue("standard", "EDIFACT").withValue("interchangeQualifier", "14").withValue("interchangeId", "QQQTEST"));
|
||||
QRecord remote = SystemWrites.insert("edi_identity", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("standard", "EDIFACT")
|
||||
.withValue("interchangeQualifier", "14").withValue("interchangeId", "LINDQVIST"));
|
||||
QRecord transport = SystemWrites.insert("edi_transport", new QRecord().withValue("name", "lindqvist-outbox").withValue("type", "GATEWAY")
|
||||
.withValue("direction", "BOTH").withValue("gatewayEndpointName", "lindqvist-outbox").withValue("isActive", true));
|
||||
QRecord agreement = SystemWrites.insert("edi_agreement", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("localIdentityId", local.getValue("id"))
|
||||
.withValue("partnerIdentityId", remote.getValue("id")).withValue("standard", "EDIFACT").withValue("usageIndicator", "T").withValue("interchangeVersion", "3")
|
||||
.withValue("elementSeparator", "+").withValue("componentSeparator", ":").withValue("segmentTerminator", "'").withValue("releaseCharacter", "?")
|
||||
.withValue("applyTestDocuments", true).withValue("outboundTransportId", transport.getValue("id")));
|
||||
for(String doc : List.of("ORDERS", "ORDRSP", "DESADV", "INVOIC", "CONTRL"))
|
||||
{
|
||||
QRecord map = doc.equals("CONTRL") ? null : RetailSupport.installMap(doc, version);
|
||||
QRecord guide = EdiRecords.query("edi_guide", EdiRecords.equal("transactionSet", doc).withCriteria("version", QCriteriaOperator.EQUALS, version)).getFirst();
|
||||
for(String direction : doc.equals("CONTRL") ? List.of("INBOUND", "OUTBOUND") : List.of(doc.equals("ORDERS") ? "INBOUND" : "OUTBOUND"))
|
||||
{
|
||||
SystemWrites.insert("edi_agreement_document", new QRecord().withValue("agreementId", agreement.getValue("id")).withValue("direction", direction)
|
||||
.withValue("transactionSet", doc).withValue("version", version).withValue("guideId", guide.getValue("id")).withValue("mapId", map == null ? null : map.getValue("id"))
|
||||
.withValue("sourceTableName", map == null ? null : map.getValue("tableName")).withValue("targetTableName", map == null ? null : map.getValue("tableName"))
|
||||
.withValue("ackExpectation", doc.equals("CONTRL") ? "NONE" : "CONTRL").withValue("ackLevel", "ALL_TRANSACTIONS").withValue("ackDueMinutes", 1)
|
||||
.withValue("outboundTrigger", "MANUAL").withValue("batchMode", "IMMEDIATE").withValue("isActive", true));
|
||||
}
|
||||
}
|
||||
return (agreement);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Two synthetic lines, released delimiters and an EANCOM association identifier.
|
||||
*******************************************************************************/
|
||||
public static String orders(String reference, String version)
|
||||
{
|
||||
return ("UNA:+.? 'UNB+UNOC:3+LINDQVIST:14+QQQTEST:14+261014:1200+" + reference + "++++++1'"
|
||||
+ "UNH+M1+ORDERS:D:" + version.substring(1) + ":UN:EAN008'BGM+220+TEST-" + reference + "+9'DTM+137:20261014:102'RFF+ON:PO-1'NAD+BY+BUY?+ER::9'"
|
||||
+ "LIN+1++SKU?:ONE:SA'QTY+21:2:PCE'PRI+AAA:7.5'LIN+2++SKU?':SA'QTY+21:3:PCE'PRI+AAA:2'UNS+S'CNT+2:2'UNT+14+M1'UNZ+1+" + reference + "'");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Parse one synthetic interchange in memory.
|
||||
*******************************************************************************/
|
||||
public static EdifactInterchange read(String text) throws Exception
|
||||
{
|
||||
return (new EdifactEnvelopeReader(100000).read(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))).getFirst());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Exercise the durable receive boundary without a real network transport.
|
||||
*******************************************************************************/
|
||||
public static void receive(String text, String external) throws Exception
|
||||
{
|
||||
new InboundSink(EdiRuntime.current().payloads()).receive(new InboundPayload("GATEWAY", external, "synthetic.edi", null, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.edi;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.edi.ack.AckLevel;
|
||||
import com.kingsrook.qbits.edi.edifact.ContrlBuilder;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactEnvelopeReader;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactInterchange;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactMapper;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactOutbound;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactSeparators;
|
||||
import com.kingsrook.qbits.edi.edifact.EdifactWriter;
|
||||
import com.kingsrook.qbits.edi.flow.InboxWorker;
|
||||
import com.kingsrook.qbits.edi.flow.OutboundEnqueue;
|
||||
import com.kingsrook.qbits.edi.flow.OutboundWorker;
|
||||
import com.kingsrook.qbits.edi.guide.BuiltInGuideLoader;
|
||||
import com.kingsrook.qbits.edi.guide.GuideValidator;
|
||||
import com.kingsrook.qbits.edi.guide.RuleEvaluator;
|
||||
import com.kingsrook.qbits.edi.guide.ValidationFinding;
|
||||
import com.kingsrook.qbits.edi.map.InboundMapper;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.storage.PayloadRef;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qbits.edi.x12.EnvelopeFinding;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/*******************************************************************************
|
||||
** Native envelope, guide, map and durable pipeline acceptance with synthetic data.
|
||||
*******************************************************************************/
|
||||
class EdifactTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Restore test context.
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void clear()
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Directory baselines validate and mapping retains segment groups and components.
|
||||
*******************************************************************************/
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"D96A", "D01B"})
|
||||
void orders(String version) throws Exception
|
||||
{
|
||||
H2Support.create();
|
||||
var input = EdifactSupport.read(EdifactSupport.orders("100", version));
|
||||
assertThat(input.findings()).isEmpty();
|
||||
var message = input.allMessages().getFirst();
|
||||
assertThat(message.findings()).isEmpty();
|
||||
var guide = new BuiltInGuideLoader().resource("ORDERS-" + version);
|
||||
assertThat(new GuideValidator().validate(message, guide, false)).isEmpty();
|
||||
var sample = RetailSupport.sample("ORDERS", version);
|
||||
List<QRecord> rules = new ArrayList<>();
|
||||
sample.get("rules").forEach(r -> rules.add(RetailSupport.record(r)));
|
||||
var mapped = new InboundMapper(EdiRuntime.current().transforms()).map(message, guide, RetailSupport.record(sample.get("map")), rules);
|
||||
assertThat(mapped.record().getValueString("buyerId")).isEqualTo("BUY+ER");
|
||||
assertThat(mapped.record().getAssociatedRecords().get("lines")).extracting(r -> r.getValueString("sku")).containsExactly("SKU:ONE", "SKU'");
|
||||
assertThat(new RuleEvaluator(message.segments(), new GuideValidator().loops(message, guide)).values("SG25/LIN/LIN03-1")).containsExactly("SKU:ONE", "SKU'");
|
||||
assertThat(EdifactSupport.read(new EdifactWriter().write(input))).isEqualTo(input);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Each interchange can change UNA; released terminators never split the payload.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void multiInterchangeAndSyntaxFour() throws Exception
|
||||
{
|
||||
var first = EdifactSupport.read(EdifactSupport.orders("100", "D96A"));
|
||||
var custom = new EdifactInterchange(new EdifactSeparators('>', '|', '.', '!', ' ', '~'), first.unb(), first.unz(), first.groups(), first.messages(), List.of());
|
||||
String text = new EdifactWriter().write(first) + "\r\n" + new EdifactWriter().write(custom, "\r\n");
|
||||
var parsed = new EdifactEnvelopeReader(1000).read(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
|
||||
assertThat(parsed).containsExactly(first, custom);
|
||||
var four = EdifactSupport.read(EdifactSupport.orders("101", "D01B").replace("UNA:+.? '", "UNA:+.?*'").replace("UNOC:3", "UNOC:4").replace("261014:1200", "20261014:1200"));
|
||||
assertThat(four.findings()).isEmpty();
|
||||
assertThat(four.allMessages().getFirst().findings()).isEmpty();
|
||||
assertThat(EdifactSupport.read(new EdifactWriter().write(four))).isEqualTo(four);
|
||||
assertThat(EdifactSupport.read(EdifactSupport.orders("100", "D96A").substring(9)).findings()).isEmpty();
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Native group counts, references, message limits and incomplete envelopes fail safely.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void findingsAndGroups() throws Exception
|
||||
{
|
||||
String grouped = EdifactSupport.orders("100", "D96A").replace("UNH+", "UNG+ORDERS+LINDQVIST+QQQTEST+261014:1200+G1+UN+D:96A'UNH+").replace("UNZ+", "UNE+1+G1'UNZ+");
|
||||
var valid = EdifactSupport.read(grouped);
|
||||
assertThat(valid.findings()).isEmpty();
|
||||
assertThat(valid.groups()).hasSize(1);
|
||||
assertThat(valid.groups().getFirst().findings()).isEmpty();
|
||||
assertThat(EdifactSupport.read(new EdifactWriter().write(valid))).isEqualTo(valid);
|
||||
var groupAck = new ContrlBuilder().build(valid, Map.of(), AckLevel.ALL_TRANSACTIONS, "A1", "D96A");
|
||||
assertThat(new EdifactWriter().write(groupAck.segments(), EdifactSeparators.DEFAULT, "")).contains("UCF+G1+LINDQVIST+QQQTEST+7'");
|
||||
assertThat(new GuideValidator().validate(groupAck, new BuiltInGuideLoader().resource("CONTRL-D96A"), false)).isEmpty();
|
||||
var invalid = EdifactSupport.read(grouped.replace("UNT+14+M1", "UNT+13+M2").replace("UNE+1+G1", "UNE+2+G2").replace("UNZ+1+100", "UNZ+2+101"));
|
||||
assertThat(invalid.findings()).extracting(EnvelopeFinding::code).contains("29", "28");
|
||||
assertThat(invalid.groups().getFirst().findings()).extracting(EnvelopeFinding::code).containsExactly("29", "28");
|
||||
assertThat(invalid.allMessages().getFirst().findings()).extracting(EnvelopeFinding::code).containsExactly("29", "28");
|
||||
assertThat(EdifactSupport.read(grouped.replace("UNZ+1+100'", "")).findings()).isNotEmpty();
|
||||
var bounded = new EdifactEnvelopeReader(3).read(new ByteArrayInputStream(grouped.getBytes(StandardCharsets.UTF_8))).getFirst();
|
||||
assertThat(bounded.allMessages().getFirst().segments()).hasSize(3);
|
||||
assertThat(bounded.allMessages().getFirst().findings()).extracting(EnvelopeFinding::code).contains("35");
|
||||
assertThatThrownBy(() -> new EdifactEnvelopeReader(1)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> EdifactSeparators.fromUna("bad")).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Exact CONTRL output for acceptance, rejected data and envelope syntax errors.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void acknowledgements() throws Exception
|
||||
{
|
||||
var input = EdifactSupport.read(EdifactSupport.orders("100", "D96A"));
|
||||
var builder = new ContrlBuilder();
|
||||
var accepted = builder.build(input, Map.of(), AckLevel.ALL_TRANSACTIONS, "A1", "D96A");
|
||||
assertThat(new EdifactWriter().write(accepted.segments(), EdifactSeparators.DEFAULT, "")).isEqualTo("UNH+A1+CONTRL:D:96A:UN'UCI+100+LINDQVIST:14+QQQTEST:14+7'UCM+M1+ORDERS:D:96A:UN:EAN008+7'UNT+4+A1'");
|
||||
var error = new ValidationFinding(7, "QTY", "SG25", 1, 2, 2, "8", "6", "8", "6", "ERROR", "Invalid number", null);
|
||||
var rejected = builder.build(input, Map.of("M1", List.of(error)), AckLevel.ERRORS_ONLY, "A2", "D96A");
|
||||
assertThat(new EdifactWriter().write(rejected.segments(), EdifactSeparators.DEFAULT, "")).isEqualTo("UNH+A2+CONTRL:D:96A:UN'UCI+100+LINDQVIST:14+QQQTEST:14+7'UCM+M1+ORDERS:D:96A:UN:EAN008+4'UCS+7'UCD+12+1:2'UNT+6+A2'");
|
||||
var broken = EdifactSupport.read(EdifactSupport.orders("100", "D96A").replace("UNZ+1+100", "UNZ+2+100"));
|
||||
assertThat(new EdifactWriter().write(builder.build(broken, Map.of(), AckLevel.GROUP_ONLY, "A3", "D96A").segments(), EdifactSeparators.DEFAULT, ""))
|
||||
.isEqualTo("UNH+A3+CONTRL:D:96A:UN'UCI+100+LINDQVIST:14+QQQTEST:14+4+29'UNT+3+A3'");
|
||||
for(var ack : List.of(accepted, rejected))
|
||||
{
|
||||
assertThat(new GuideValidator().validate(ack, new BuiltInGuideLoader().resource("CONTRL-D96A"), false)).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** All outbound retail documents serialize, validate and parse with two line occurrences.
|
||||
*******************************************************************************/
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"D96A", "D01B"})
|
||||
void outboundRoundTrips(String version) throws Exception
|
||||
{
|
||||
H2Support.create();
|
||||
for(String doc : List.of("ORDRSP", "DESADV", "INVOIC"))
|
||||
{
|
||||
var sample = RetailSupport.sample(doc, version);
|
||||
List<QRecord> rules = new ArrayList<>();
|
||||
sample.get("rules").forEach(r -> rules.add(RetailSupport.record(r)));
|
||||
var message = new EdifactMapper().message(RetailSupport.source(), RetailSupport.record(sample.get("map")), rules, "1");
|
||||
assertThat(new GuideValidator().validate(message, new BuiltInGuideLoader().resource(doc + "-" + version), false)).as(doc).isEmpty();
|
||||
var envelope = EdifactOutbound.wrap(new QRecord().withValue("usageIndicator", "T"), new QRecord().withValue("interchangeId", "LOCAL").withValue("interchangeQualifier", "14"),
|
||||
new QRecord().withValue("interchangeId", "REMOTE").withValue("interchangeQualifier", "14"), List.of(message), "100", java.time.LocalDateTime.of(2026, 10, 14, 12, 0));
|
||||
var parsed = EdifactSupport.read(new EdifactWriter().write(envelope));
|
||||
assertThat(parsed.findings()).isEmpty();
|
||||
assertThat(parsed.allMessages().getFirst().findings()).isEmpty();
|
||||
assertThat(parsed.allMessages().getFirst().segments()).isEqualTo(message.segments());
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** ORDERS is applied through the durable inbox; repeat controls cannot reapply it.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void inboxAndDuplicates() throws Exception
|
||||
{
|
||||
setup();
|
||||
EdifactSupport.receive(EdifactSupport.orders("100", "D96A"), "orders-1");
|
||||
assertThat(new InboxWorker().runOnce()).isEqualTo(1);
|
||||
assertThat(rows("edi_inbox")).extracting(r -> r.getValueString("status")).containsExactly("PROCESSED");
|
||||
assertThat(rows("edi_transaction")).anyMatch(r -> "ORDERS".equals(r.getValueString("transactionSet")) && "APPLIED".equals(r.getValueString("status")));
|
||||
assertThat(rows("sample_order")).hasSize(1);
|
||||
assertThat(rows("sample_order_line")).hasSize(2);
|
||||
assertThat(rows("edi_outbound_message")).hasSize(1);
|
||||
EdifactSupport.receive(EdifactSupport.orders("100", "D96A").replace("TEST-100", "TEST-OTHER"), "orders-2");
|
||||
new InboxWorker().runOnce();
|
||||
assertThat(rows("sample_order")).hasSize(1);
|
||||
assertThat(rows("edi_exception")).anyMatch(r -> "DUPLICATE".equals(r.getValueString("type")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Worker maps persisted retail records, including nested shipment associations.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void outboundWorker() throws Exception
|
||||
{
|
||||
setup();
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
for(String table : List.of("sample_order", "sample_shipment", "sample_invoice"))
|
||||
{
|
||||
QRecord source = SystemWrites.transaction(() -> RetailSupport.saveSource(table, RetailSupport.source()));
|
||||
OutboundEnqueue.enqueue(table, source, "MANUAL", null);
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
assertThat(new OutboundWorker().runOnce()).isEqualTo(3);
|
||||
assertThat(rows("edi_outbound_message")).hasSize(3);
|
||||
for(var row : rows("edi_outbound_message"))
|
||||
{
|
||||
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(row.getValueString("payloadRef"), null, 0)))
|
||||
{
|
||||
var parsed = new EdifactEnvelopeReader(10000).read(stream).getFirst();
|
||||
assertThat(parsed.findings()).isEmpty();
|
||||
assertThat(parsed.allMessages().getFirst().findings()).isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** CONTRL resolves deadlines and reconciles accepted and rejected outbound messages.
|
||||
*******************************************************************************/
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void reconcile(boolean reject) throws Exception
|
||||
{
|
||||
QRecord agreement = setup();
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
QRecord source = SystemWrites.transaction(() -> RetailSupport.saveSource("sample_order", RetailSupport.source()));
|
||||
OutboundEnqueue.enqueue("sample_order", source, "MANUAL", null);
|
||||
return (null);
|
||||
});
|
||||
new OutboundWorker().runOnce();
|
||||
QRecord outgoing = rows("edi_outbound_message").getFirst();
|
||||
com.kingsrook.qbits.edi.transport.gateway.standin.FileGatewayOutbox.register((endpoint, name, stream, size) ->
|
||||
new com.kingsrook.qbits.edi.transport.gateway.standin.OutboxFileRef(1, endpoint, name, name, size, null, java.time.Instant.now()));
|
||||
try
|
||||
{
|
||||
new com.kingsrook.qbits.edi.flow.SendWorker().runOnce();
|
||||
assertThat(rows("edi_functional_group").getFirst().getValueInstant("ackDueAt")).isNotNull();
|
||||
EdiRuntime.current().config().withClock(java.time.Clock.fixed(java.time.Instant.now().plusSeconds(120), java.time.ZoneOffset.UTC));
|
||||
assertThat(new com.kingsrook.qbits.edi.flow.AckMonitor().runOnce()).isEqualTo(1);
|
||||
EdifactInterchange original;
|
||||
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(outgoing.getValueString("payloadRef"), null, 0)))
|
||||
{
|
||||
original = new EdifactEnvelopeReader(10000).read(stream).getFirst();
|
||||
}
|
||||
var message = original.allMessages().getFirst();
|
||||
var error = new ValidationFinding(2, "BGM", null, 2, null, 2, "8", "1", "8", "1", "ERROR", "Missing number", null);
|
||||
var ack = new ContrlBuilder().build(original, reject ? Map.of(message.reference(), List.of(error)) : Map.of(), AckLevel.ALL_TRANSACTIONS, "A1", "D96A");
|
||||
var reply = EdifactOutbound.wrap(agreement, SystemWrites.call(() -> OutboundWorker.row("edi_identity", agreement.getValue("partnerIdentityId"))),
|
||||
SystemWrites.call(() -> OutboundWorker.row("edi_identity", agreement.getValue("localIdentityId"))), List.of(ack), "ACK100", java.time.LocalDateTime.of(2026, 10, 14, 12, 0));
|
||||
EdifactSupport.receive(new EdifactWriter().write(reply), "ack-1");
|
||||
assertThat(new InboxWorker().runOnce()).isEqualTo(1);
|
||||
assertThat(rows("edi_interchange").stream().filter(r -> r.getValue("id").equals(outgoing.getValue("interchangeId"))).findFirst().orElseThrow().getValueString("status"))
|
||||
.isEqualTo(reject ? "REJECTED" : "ACKNOWLEDGED");
|
||||
assertThat(rows("edi_transaction").stream().filter(r -> "OUTBOUND".equals(r.getValueString("direction"))).findFirst().orElseThrow().getValueString("ackStatus")).isEqualTo(reject ? "R" : "A");
|
||||
assertThat(rows("edi_exception")).anyMatch(r -> "OVERDUE_ACK".equals(r.getValueString("type")) && "RESOLVED".equals(r.getValueString("status")));
|
||||
assertThat(rows("edi_exception").stream().anyMatch(r -> "REJECTED_ACK".equals(r.getValueString("type")))).isEqualTo(reject);
|
||||
assertThat(rows("edi_outbound_message")).hasSize(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
com.kingsrook.qbits.edi.transport.gateway.standin.FileGatewayOutbox.register(null);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** A single payload can contain X12 and EDIFACT and route each independently.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void mixedStandards() throws Exception
|
||||
{
|
||||
setup();
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
QRecord partner = SystemWrites.insert("edi_partner", new QRecord().withValue("name", "X12 mixed").withValue("code", "MIXED"));
|
||||
QRecord local = SystemWrites.insert("edi_identity", new QRecord().withValue("standard", "X12").withValue("interchangeQualifier", "ZZ").withValue("interchangeId", "QQQTEST"));
|
||||
QRecord remote = SystemWrites.insert("edi_identity", new QRecord().withValue("standard", "X12").withValue("interchangeQualifier", "ZZ").withValue("interchangeId", "SYNTHETIC").withValue("partnerId", partner.getValue("id")));
|
||||
QRecord agreement = SystemWrites.insert("edi_agreement", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("localIdentityId", local.getValue("id"))
|
||||
.withValue("partnerIdentityId", remote.getValue("id")).withValue("standard", "X12").withValue("usageIndicator", "T").withValue("applyTestDocuments", true));
|
||||
QRecord map = RetailSupport.installMap("850", "004010");
|
||||
SystemWrites.insert("edi_agreement_document", new QRecord().withValue("agreementId", agreement.getValue("id")).withValue("direction", "INBOUND").withValue("transactionSet", "850")
|
||||
.withValue("version", "004010").withValue("guideId", map.getValue("guideId")).withValue("mapId", map.getValue("id")).withValue("ackExpectation", "NONE"));
|
||||
return (null);
|
||||
});
|
||||
String x12 = Synthetic850.generate(1, "004010", com.kingsrook.qbits.edi.x12.X12Separators.DEFAULT);
|
||||
EdifactSupport.receive(x12 + "\n" + EdifactSupport.orders("200", "D96A"), "mixed");
|
||||
assertThat(new InboxWorker().runOnce()).isEqualTo(1);
|
||||
assertThat(rows("edi_transaction").stream().filter(r -> "APPLIED".equals(r.getValueString("status"))).map(r -> r.getValueString("transactionSet"))).containsExactlyInAnyOrder("850", "ORDERS");
|
||||
assertThat(rows("sample_order")).hasSize(2);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** All service characters in data survive custom UNA and release escaping.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void releasedCharactersAndRepetition() throws Exception
|
||||
{
|
||||
var input = EdifactSupport.read(EdifactSupport.orders("100", "D96A"));
|
||||
var message = input.allMessages().getFirst();
|
||||
var segments = new ArrayList<>(message.segments());
|
||||
segments.set(1, com.kingsrook.qbits.edi.x12.X12Segment.of("BGM", 2, "220", "A|B>C!D~E?F", "9"));
|
||||
var changed = new com.kingsrook.qbits.edi.edifact.EdifactMessage(message.header(), message.unt(), segments, List.of());
|
||||
var custom = new EdifactInterchange(new EdifactSeparators('>', '|', '.', '!', ' ', '~'), input.unb(), input.unz(), List.of(), List.of(changed), List.of());
|
||||
String encoded = new EdifactWriter().write(custom);
|
||||
assertThat(encoded).contains("A!|B!>C!!D!~E?F");
|
||||
assertThat(EdifactSupport.read(encoded).allMessages().getFirst().segments()).isEqualTo(segments);
|
||||
var standard = new EdifactInterchange(EdifactSeparators.DEFAULT, input.unb(), input.unz(), List.of(), List.of(changed), List.of());
|
||||
assertThat(new EdifactWriter().write(standard)).contains("E??F");
|
||||
String four = EdifactSupport.orders("100", "D01B").replace("UNA:+.? '", "UNA:+.?*'").replace("UNOC:3", "UNOC:4").replace("261014:1200", "20261014:1200").replace("TEST-100", "FIRST*SECOND");
|
||||
var parsed = EdifactSupport.read(four);
|
||||
assertThat(parsed.allMessages().getFirst().segments().get(1).value(2, 2, 1)).isEqualTo("SECOND");
|
||||
assertThat(EdifactSupport.read(new EdifactWriter().write(parsed))).isEqualTo(parsed);
|
||||
var withoutAdvice = EdifactSupport.read(four.substring(9));
|
||||
assertThat(withoutAdvice.separators().repetition()).isEqualTo('*');
|
||||
assertThat(EdifactSupport.read(new EdifactWriter().write(withoutAdvice))).isEqualTo(withoutAdvice);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Resend retains bytes, while resend-as-new preserves archived content and changes controls.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void archivedResendAndPreview() throws Exception
|
||||
{
|
||||
setup();
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
var source = SystemWrites.transaction(() -> RetailSupport.saveSource("sample_order", RetailSupport.source()));
|
||||
OutboundEnqueue.enqueue("sample_order", source, "MANUAL", null);
|
||||
return (null);
|
||||
});
|
||||
new OutboundWorker().runOnce();
|
||||
QRecord original = rows("edi_outbound_message").getFirst();
|
||||
SystemWrites.call(() -> SystemWrites.transaction(() ->
|
||||
{
|
||||
com.kingsrook.qbits.edi.flow.OutboundActions.execute("edi_outbound_message", original, "resend");
|
||||
SystemWrites.update("sample_order", new QRecord().withValue("id", rows("sample_order").getFirst().getValue("id")).withValue("poNumber", "CHANGED-AFTER-SEND"));
|
||||
com.kingsrook.qbits.edi.flow.OutboundActions.execute("edi_outbound_message", original, "new");
|
||||
return (null);
|
||||
}));
|
||||
assertThat(rows("edi_outbound_message")).hasSize(2);
|
||||
assertThat(rows("edi_outbound_message").getFirst().getValueString("payloadRef")).isEqualTo(original.getValueString("payloadRef"));
|
||||
var rebuilt = rows("edi_outbound_message").getLast();
|
||||
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(rebuilt.getValueString("payloadRef"), null, 0)))
|
||||
{
|
||||
var parsed = new EdifactEnvelopeReader(10000).read(stream).getFirst();
|
||||
assertThat(parsed.allMessages().getFirst().segments().get(1).value(2)).isEqualTo("DEMO-1001");
|
||||
assertThat(parsed.allMessages().getFirst().reference()).isEqualTo("2");
|
||||
assertThat(parsed.unb().value(5)).isEqualTo("2");
|
||||
}
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
QRecord map = rows("edi_map").stream().filter(r -> "ORDERS".equals(r.getValueString("transactionSet"))).findFirst().orElseThrow();
|
||||
var payload = EdiRuntime.current().payloads().put(com.kingsrook.qbits.edi.storage.PayloadKind.SAMPLE, new ByteArrayInputStream(EdifactSupport.orders("300", "D96A").getBytes(StandardCharsets.UTF_8)));
|
||||
var lines = new com.kingsrook.qbits.edi.map.MapPreview().preview(map, new QRecord().withValue("payloadRef", payload.value()));
|
||||
assertThat(lines).isNotEmpty();
|
||||
assertThat(rows("sample_order")).hasSize(1);
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** UN control levels allow fourteen digits without relaxing X12 bounds.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void controlLevels() throws Exception
|
||||
{
|
||||
QRecord agreement = setup();
|
||||
var allocator = EdiRuntime.current().controls();
|
||||
assertThat(allocator.allocate(agreement.getValueInteger("id"), null, "UNB", 99999999999998L, 99999999999999L, false)).isEqualTo(99999999999998L);
|
||||
assertThat(allocator.allocate(agreement.getValueInteger("id"), null, "UNB", 1, 99999999999999L, false)).isEqualTo(99999999999999L);
|
||||
assertThatThrownBy(() -> allocator.allocate(agreement.getValueInteger("id"), null, "UNB", 1, 99999999999999L, false)).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(allocator.allocate(agreement.getValueInteger("id"), null, "UNH", 1, 99999999999999L, true)).isEqualTo(1);
|
||||
assertThatThrownBy(() -> allocator.allocate(agreement.getValueInteger("id"), null, "ISA", 1, 99999999999999L, true)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Decimal commas follow UNA in inbound mapping and outbound numeric serialization.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void decimalComma() throws Exception
|
||||
{
|
||||
H2Support.create();
|
||||
var input = EdifactSupport.read(EdifactSupport.orders("400", "D96A").replace("UNA:+.? '", "UNA:+,? '").replace("AAA:7.5", "AAA:7,5"));
|
||||
var guide = new BuiltInGuideLoader().resource("ORDERS-D96A");
|
||||
var message = input.allMessages().getFirst();
|
||||
assertThat(new GuideValidator().validate(message, guide, false)).isEmpty();
|
||||
var sample = RetailSupport.sample("ORDERS", "D96A");
|
||||
List<QRecord> rules = new ArrayList<>();
|
||||
sample.get("rules").forEach(r -> rules.add(RetailSupport.record(r)));
|
||||
var result = new InboundMapper(EdiRuntime.current().transforms()).map(message, guide, RetailSupport.record(sample.get("map")), rules);
|
||||
assertThat(result.record().getAssociatedRecords().get("lines").getFirst().getValueBigDecimal("unitPrice")).isEqualByComparingTo("7.5");
|
||||
var outbound = RetailSupport.sample("INVOIC", "D96A");
|
||||
rules.clear();
|
||||
outbound.get("rules").forEach(r -> rules.add(RetailSupport.record(r)));
|
||||
var invoice = new EdifactMapper(',').message(RetailSupport.source(), RetailSupport.record(outbound.get("map")), rules, "1");
|
||||
assertThat(new EdifactWriter().write(invoice.segments(), new EdifactSeparators(':', '+', ',', '?', ' ', '\''), "")).contains("PRI+AAA:7,5'");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Invalid data and message counts are rejected; unknown senders cannot trigger replies.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void rejectsInvalidAndUnknownDocuments() throws Exception
|
||||
{
|
||||
setup();
|
||||
EdifactSupport.receive(EdifactSupport.orders("500", "D96A").replace("QTY+21:2:PCE", "QTY+21:BAD:PCE"), "bad-value");
|
||||
EdifactSupport.receive(EdifactSupport.orders("501", "D96A").replace("UNT+14", "UNT+13"), "bad-count");
|
||||
EdifactSupport.receive(EdifactSupport.orders("502", "D96A").replace("LINDQVIST:14", "UNKNOWN:14"), "unknown");
|
||||
assertThat(new InboxWorker().runOnce()).isEqualTo(3);
|
||||
assertThat(rows("sample_order")).isEmpty();
|
||||
assertThat(rows("edi_transaction").stream().filter(r -> "ORDERS".equals(r.getValueString("transactionSet")))).allMatch(r -> "INVALID".equals(r.getValueString("status")));
|
||||
assertThat(rows("edi_outbound_message")).hasSize(2);
|
||||
assertThat(rows("edi_exception")).anyMatch(r -> "UNKNOWN_PARTNER".equals(r.getValueString("type")));
|
||||
for(var row : rows("edi_outbound_message"))
|
||||
{
|
||||
try(var stream = EdiRuntime.current().payloads().open(new PayloadRef(row.getValueString("payloadRef"), null, 0)))
|
||||
{
|
||||
String ack = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertThat(ack).contains("+4").doesNotContain("BAD");
|
||||
if(ack.contains("UCI+501+"))
|
||||
{
|
||||
assertThat(ack).contains("UCM+M1+ORDERS:D:96A:UN:EAN008+4+29'").doesNotContain("UCS+");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Invalid CONTRL references roll back reconciliation; receipt-only responses leave it pending.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void invalidAcknowledgements() throws Exception
|
||||
{
|
||||
QRecord agreement = setup();
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
var source = SystemWrites.transaction(() -> RetailSupport.saveSource("sample_order", RetailSupport.source()));
|
||||
OutboundEnqueue.enqueue("sample_order", source, "MANUAL", null);
|
||||
return (null);
|
||||
});
|
||||
new OutboundWorker().runOnce();
|
||||
QRecord sent = rows("edi_outbound_message").getFirst();
|
||||
String ackText = "UNA:+.? 'UNB+UNOC:3+LINDQVIST:14+QQQTEST:14+261014:1200+ACK1++++++1'UNH+A1+CONTRL:D:96A:UN'UCI+1+QQQTEST:14+LINDQVIST:14+7'UCM+BOGUS+ORDRSP:D:96A:UN+7'UNT+4+A1'UNZ+1+ACK1'";
|
||||
var invalid = EdifactSupport.read(ackText).allMessages().getFirst();
|
||||
QRecord received = new QRecord().withValue("id", rows("edi_transaction").getFirst().getValue("id"));
|
||||
assertThatThrownBy(() -> SystemWrites.call(() -> SystemWrites.transaction(() ->
|
||||
{
|
||||
new com.kingsrook.qbits.edi.edifact.ContrlReader().reconcile(invalid, agreement, received);
|
||||
return (null);
|
||||
}))).isNotNull();
|
||||
assertThat(rows("edi_functional_group").getFirst().getValueString("ackStatus")).isEqualTo("PENDING");
|
||||
assertThat(rows("edi_transaction").getFirst().getValueString("status")).isEqualTo("QUEUED");
|
||||
var receipt = EdifactSupport.read(ackText.replace("LINDQVIST:14+7", "LINDQVIST:14+8")).allMessages().getFirst();
|
||||
SystemWrites.call(() ->
|
||||
{
|
||||
new com.kingsrook.qbits.edi.edifact.ContrlReader().reconcile(receipt, agreement, received);
|
||||
return (null);
|
||||
});
|
||||
assertThat(rows("edi_interchange").getFirst().getValueString("status")).isEqualTo("ENVELOPED");
|
||||
assertThat(rows("edi_outbound_message").getFirst().getValueString("payloadRef")).isEqualTo(sent.getValueString("payloadRef"));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Build actual QQQ tables and install the same definitions used in the demo.
|
||||
*******************************************************************************/
|
||||
private QRecord setup() throws Exception
|
||||
{
|
||||
H2Support.create();
|
||||
RetailSupport.tables();
|
||||
EdiRuntime.current().initialize();
|
||||
return (SystemWrites.call(() -> EdifactSupport.install("D96A")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Read test evidence with the service boundary.
|
||||
*******************************************************************************/
|
||||
private List<QRecord> rows(String table) throws Exception
|
||||
{
|
||||
return (SystemWrites.call(() -> EdiRecords.query(table, new QQueryFilter())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.kingsrook.qbits.edi.storage.EdiRecords;
|
||||
import com.kingsrook.qbits.edi.support.SystemWrites;
|
||||
import com.kingsrook.qbits.edi.x12.X12EnvelopeReader;
|
||||
import com.kingsrook.qbits.edi.x12.X12Interchange;
|
||||
import com.kingsrook.qbits.edi.x12.X12Segment;
|
||||
import com.kingsrook.qbits.edi.x12.X12Separators;
|
||||
import com.kingsrook.qbits.edi.x12.X12TransactionSet;
|
||||
import com.kingsrook.qbits.edi.x12.X12Writer;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Seeded synthetic healthcare fixtures and host tables; never used in main code.
|
||||
** NPI 1234567893 is a documented checksum test value, not a provider identity.
|
||||
*******************************************************************************/
|
||||
public final class HealthcareSupport
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Test-only schema with classified target fields and actual QQQ associations.
|
||||
*******************************************************************************/
|
||||
public static void tables() throws Exception
|
||||
{
|
||||
table("sample_claim", "claimNumber", "memberId", "patientName", "diagnosis", "totalCharge:DECIMAL");
|
||||
table("sample_claim_line", "parentId:INTEGER", "lineNumber:INTEGER", "procedureCode", "charge:DECIMAL");
|
||||
table("sample_remittance", "trace", "payment:DECIMAL");
|
||||
table("sample_remittance_claim", "parentId:INTEGER", "claimNumber", "patientName", "memberId", "charge:DECIMAL", "paid:DECIMAL");
|
||||
table("sample_eligibility_request", "trace", "memberId", "patientName");
|
||||
table("sample_eligibility_response", "trace", "memberId", "patientName", "benefit", "serviceType");
|
||||
table("sample_claim_status", "trace", "memberId", "patientName", "claimNumber", "category", "statusCode");
|
||||
table("sample_enrollment", "reference");
|
||||
table("sample_enrollment_member", "parentId:INTEGER", "memberId", "patientName", "birthDate:DATE", "coverage");
|
||||
association("sample_claim", "sample_claim_line", "lines");
|
||||
association("sample_remittance", "sample_remittance_claim", "claims");
|
||||
association("sample_enrollment", "sample_enrollment_member", "members");
|
||||
var instance = QContext.getQInstance();
|
||||
EdiRuntime.current().config().withServiceSessionSupplier(() -> new QSystemUserSession(instance));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Define one table and its H2 schema; fields are STRING unless suffixed with a type.
|
||||
*******************************************************************************/
|
||||
private static void table(String name, String... fields) throws Exception
|
||||
{
|
||||
var instance = QContext.getQInstance();
|
||||
String backend = EdiRuntime.current().config().getBackendName();
|
||||
var table = new QTableMetaData().withName(name).withLabel(name).withBackendName(backend).withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withBackendName("id"));
|
||||
for(String field : fields)
|
||||
{
|
||||
String[] parts = field.split(":");
|
||||
table.withField(new QFieldMetaData(parts[0], parts.length > 1 ? QFieldType.valueOf(parts[1]) : QFieldType.STRING).withBackendName(QInstanceEnricher.inferBackendName(parts[0]))
|
||||
.withSupplementalMetaData(new AiFieldMetaData().withClassification(parts[0].equals("parentId") ? DataClassification.CONFIDENTIAL : DataClassification.PHI)));
|
||||
}
|
||||
instance.addTable(table);
|
||||
try(Connection connection = ConnectionManager.getConnection((RDBMSBackendMetaData) instance.getBackend(backend)); var statement = connection.createStatement())
|
||||
{
|
||||
statement.execute(H2Support.ddl(table, false));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Join-backed child records, as the retail samples do.
|
||||
*******************************************************************************/
|
||||
private static void association(String parent, String child, String name)
|
||||
{
|
||||
var instance = QContext.getQInstance();
|
||||
String join = parent + "_" + name;
|
||||
instance.getTable(parent).withAssociation(new Association().withName(name).withAssociatedTableName(child).withJoinName(join));
|
||||
instance.addJoin(new QJoinMetaData().withName(join).withType(JoinType.ONE_TO_MANY).withLeftTable(parent).withRightTable(child).withJoinOn(new JoinOn("id", "parentId")));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Version and group identifiers for the eight original baselines.
|
||||
*******************************************************************************/
|
||||
public static String version(String key)
|
||||
{
|
||||
return (switch(key)
|
||||
{
|
||||
case "837P" -> "005010X222A1";
|
||||
case "837I" -> "005010X223A2";
|
||||
case "835" -> "005010X221A1";
|
||||
case "270", "271" -> "005010X279A1";
|
||||
case "276", "277" -> "005010X212";
|
||||
default -> "005010X220A1";
|
||||
});
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Functional identifiers are routing metadata, never patient information.
|
||||
*******************************************************************************/
|
||||
public static String group(String key)
|
||||
{
|
||||
return (switch(key)
|
||||
{
|
||||
case "837P", "837I" -> "HC";
|
||||
case "835" -> "HP";
|
||||
case "270" -> "HS";
|
||||
case "271" -> "HB";
|
||||
case "276" -> "HR";
|
||||
case "277" -> "HN";
|
||||
default -> "BE";
|
||||
});
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Reproducible member and claim references; no production names or identifiers.
|
||||
*******************************************************************************/
|
||||
public static String body(String key, long seed)
|
||||
{
|
||||
var random = new java.util.Random(seed);
|
||||
String member = String.format("SYN%06d", random.nextInt(1000000));
|
||||
String claim = String.format("SYNCLM%06d", random.nextInt(1000000));
|
||||
if(key.startsWith("837"))
|
||||
{
|
||||
boolean inpatient = key.equals("837I");
|
||||
return ("BHT*0019*00*SYNBATCH*20261015*1200*CH~"
|
||||
+ "NM1*41*2*Synthetic Submitter*****46*SYN~NM1*40*2*Synthetic Receiver*****46*SYN~"
|
||||
+ "HL*1**20*1~NM1*85*2*Synthetic Practice*****XX*1234567893~N3*1 Synthetic Way~N4*Testville*IL*60000~"
|
||||
+ "HL*2*1*22*0~SBR*P*18*******CI~NM1*IL*1*Testpatient Alpha*****MI*" + member + "~DMG*D8*19800101*U~NM1*PR*2*Synthetic Plan*****PI*SYNPLAN~"
|
||||
+ "CLM*" + claim + "*150***" + (inpatient ? "11:A:1" : "11:B:1") + "~"
|
||||
+ (inpatient ? "DTP*434*RD8*20261014-20261015~" : "") + "HI*ABK:A123~"
|
||||
+ "LX*1~" + (inpatient ? "SV2*0300*HC:S0001*100*UN*1~" : "SV1*HC:S0001*100*UN*1~") + "DTP*472*D8*20261015~"
|
||||
+ "LX*2~" + (inpatient ? "SV2*0300*HC:S0002*50*UN*1~" : "SV1*HC:S0002*50*UN*1~") + "DTP*472*D8*20261015~");
|
||||
}
|
||||
if(key.equals("835"))
|
||||
{
|
||||
return ("BPR*I*115*C*CHK************20261015~TRN*1*SYNTRACE" + seed + "~N1*PR*Synthetic Plan~N1*PE*Synthetic Practice~"
|
||||
+ "LX*1~CLP*" + claim + "*1*150*120*30~CAS*PR*1*10~NM1*QC*1*Testpatient Alpha~NM1*IL*1*Testpatient Alpha*****MI*" + member + "~"
|
||||
+ "SVC*HC:S0001*100*80~DTM*472*20261015~CAS*CO*45*20~SVC*HC:S0002*50*40~DTM*472*20261015~"
|
||||
+ "PLB*1234567893*20261231*WO:SYNADJ*5~");
|
||||
}
|
||||
if(key.equals("834"))
|
||||
{
|
||||
return ("BGN*00*SYNENROLL" + seed + "*20261015*1200~N1*P5*Synthetic Sponsor~N1*IN*Synthetic Plan~"
|
||||
+ "INS*Y*18*021~REF*0F*" + member + "~NM1*IL*1*Testpatient Alpha~DMG*D8*19800101*U~HD*021**HLT~DTP*348*D8*20261015~");
|
||||
}
|
||||
String head = "BHT*0022*13*SYNBATCH*20261015*1200~HL*1**20*1~NM1*PR*2*Synthetic Plan*****PI*SYNPLAN~"
|
||||
+ "HL*2*1*21*1~NM1*1P*2*Synthetic Practice*****XX*1234567893~HL*3*2*22*0~";
|
||||
return (head + (key.equals("270") || key.equals("271") ? "TRN*1*SYNTRACE" + seed + "~" : "")
|
||||
+ "NM1*IL*1*Testpatient Alpha*****MI*" + member + "~"
|
||||
+ (key.equals("270") ? "EQ*30~" : key.equals("271") ? "EB*1**30~" : "TRN*1*SYNTRACE" + seed + "~"
|
||||
+ (key.equals("277") ? "STC*A1:19*20261015~" : "") + "REF*EJ*" + claim + "~"));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Wrap raw synthetic segments using real ISA widths and counted ST/SE boundaries.
|
||||
*******************************************************************************/
|
||||
public static String interchange(String key, int control, String body)
|
||||
{
|
||||
String version = version(key);
|
||||
String set = key.startsWith("837") ? "837" : key;
|
||||
String isa = new X12Writer().write(List.of(X12Segment.of("ISA", 0, "00", "", "00", "", "ZZ", "CEDAR", "ZZ", "QQQTEST", "261015", "1200", "^", "00501", String.format("%09d", control), "0", "T", ":")), X12Separators.DEFAULT, "");
|
||||
return (isa + "GS*" + group(key) + "*CEDAR*QQQTEST*20261015*1200*" + control + "*X*" + version + "~ST*" + set + "*0001*" + version + "~"
|
||||
+ body + "SE*" + (body.split("~").length + 2) + "*0001~GE*1*" + control + "~IEA*1*" + String.format("%09d", control) + "~");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Seed the healthcare partner and sample agreement documents as normal records.
|
||||
*******************************************************************************/
|
||||
public static QRecord configure() throws Exception
|
||||
{
|
||||
return (SystemWrites.call(() ->
|
||||
{
|
||||
QRecord partner = SystemWrites.insert("edi_partner", new QRecord().withValue("name", "Cedar Health Clearinghouse").withValue("code", "CEDAR")
|
||||
.withValue("status", "ACTIVE").withValue("containsPhi", true).withValue("contactEmail", "cedar@example.invalid"));
|
||||
var locals = EdiRecords.query("edi_identity", EdiRecords.equal("interchangeId", "QQQTEST"));
|
||||
QRecord local = locals.isEmpty() ? SystemWrites.insert("edi_identity", new QRecord().withValue("standard", "X12").withValue("interchangeQualifier", "ZZ").withValue("interchangeId", "QQQTEST").withValue("applicationCode", "QQQTEST")) : locals.getFirst();
|
||||
QRecord remote = SystemWrites.insert("edi_identity", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("standard", "X12").withValue("interchangeQualifier", "ZZ").withValue("interchangeId", "CEDAR").withValue("applicationCode", "CEDAR"));
|
||||
QRecord transport = SystemWrites.insert("edi_transport", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("name", "cedar-gateway").withValue("type", "GATEWAY")
|
||||
.withValue("direction", "BOTH").withValue("gatewayEndpointName", "cedar-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", "X12").withValue("usageIndicator", "T").withValue("applyTestDocuments", true).withValue("isActive", true).withValue("interchangeVersion", "00501")
|
||||
.withValue("inboundTransportId", transport.getValue("id")).withValue("outboundTransportId", transport.getValue("id")));
|
||||
for(String key : List.of("837P", "837I", "835", "270", "271", "276", "277", "834"))
|
||||
{
|
||||
QRecord map = LogisticsSupport.installMap(key, version(key));
|
||||
SystemWrites.insert("edi_agreement_document", new QRecord().withValue("agreementId", agreement.getValue("id")).withValue("direction", map.getValue("direction"))
|
||||
.withValue("transactionSet", map.getValue("transactionSet")).withValue("version", version(key)).withValue("functionalIdCode", group(key))
|
||||
.withValue("guideId", map.getValue("guideId")).withValue("mapId", map.getValue("id")).withValue("ackExpectation", "999").withValue("ackLevel", "ALL_TRANSACTIONS")
|
||||
.withValue("sourceTableName", map.getValueString("direction").equals("OUTBOUND") ? map.getValue("tableName") : null).withValue("isActive", true));
|
||||
}
|
||||
return (agreement);
|
||||
}));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Normal durable intake and parsing, shared with the demo.
|
||||
*******************************************************************************/
|
||||
public static void receive(String key, int control, String body) throws Exception
|
||||
{
|
||||
new com.kingsrook.qbits.edi.flow.InboundSink(EdiRuntime.current().payloads()).receive(new com.kingsrook.qbits.edi.flow.InboundPayload("GATEWAY", "health-" + control,
|
||||
"synthetic-healthcare.edi", "127.0.0.1", new ByteArrayInputStream(interchange(key, control, body).getBytes(StandardCharsets.US_ASCII))));
|
||||
new com.kingsrook.qbits.edi.flow.InboxWorker().runOnce();
|
||||
}
|
||||
}
|
||||
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user