Compare commits

..
Author SHA1 Message Date
james.maesandCodex 18905b0067 ci: use the shared branch-aware CodeQL worker
Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 10:02:17 -05:00
james.maesandCodex 9db6d4bb59 ci: adopt central orb 0.7.12 and stable gates (#31)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 08:58:18 -05:00
james.maesandCodex 9f581996d0 ci: centralize rc release completion in qqq orb (#30)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 02:30:33 -05:00
james.maesandCodex c9d324acd4 ci: adopt shared orb 0.7.11 (#29)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 01:01:51 -05:00
james.maes cd2128f78f Merge pull request #27 from QRun-IO/feature/central-ci
ci: adopt verified central QQQ orb 0.7.1
2026-10-20 18:53:02 -05:00
james.maesandCodex 3d336c3dfe ci: adopt verified central QQQ orb 0.7.2
Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 18:42:43 -05:00
james.maes 73411423df Merge pull request #26 from QRun-IO/feature/edi-business-home
fix(app): business-only EDI home; system widgets for the Admin home
2026-10-16 10:19:39 -05:00
james.maesandClaude Opus 5.5 cccd0ac0ab fix(app): business-only EDI home; system widgets for the Admin home
Move System, Dead letters, Certificates expiring and Setup status off the
EDI home into EdiHome.ADMIN_WIDGET_NAMES, gated to edi.admin.hasAccess
(hidden when denied) and full width, for a host to place on its Admin
home. Every remaining home widget requires edi.hasAccess (hidden when
denied); none is NOT_PROTECTED.

Add "Inbound documents are delayed": an alert at the top of the home,
shown only when the oldest unprocessed inbound document is older than
inboundDelayedAfterMinutes (default 60), with its age and a link to the
filtered inbound documents.

Setup counts that fail show "Unavailable" and are logged, never -1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:28:46 -05:00
james.maes 2681447b07 Merge pull request #25 from QRun-IO/ci/release-rc-secrets-inherit
ci(release-rc): pass secrets so the shared workflow can sign the RC tag
2026-10-16 08:05:13 -05:00
james.maesandClaude Opus 5.5 55a28c7031 ci(release-rc): pass secrets to the shared workflow so it can sign the RC tag
QRun-IO/.github#2 adds a tag-and-release job that signs vX with the
QRUN_TAG_SIGNING_KEY secret after Maven Central confirms the artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:19:12 -05:00
james.maes 2491669f23 Merge pull request #24 from QRun-IO/feature/ui-api-1.6
feat(ui-api): extension for QQQ UI API 1.6.0 (operationIds, BasicErrorResponseV1, QQQ 4.1.0-RC.15)
2026-10-15 16:13:04 -05:00
james.maesandClaude Opus 5.5 36f1b939b4 chore: target QQQ 4.1.0-RC.15 (UI API 1.6.0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:27:06 -05:00
james.maesandClaude Opus 5.5 77d63135af feat(ui-api): extension for QQQ UI API 1.6.0 (operationIds, BasicErrorResponseV1, QQQ 4.1.0-RC.14)
Regenerated against UI API 1.6.0: every operation has an operationId and
the standard error responses (BasicErrorResponseV1: error, code, message,
fieldErrors, traceId); the extension's own error schema is kept but
deprecated; hand-written error bodies go through QJavalinUtils so they carry
code/message/traceId. Extension MINOR bumped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:43:38 -05:00
james.maes 603e518a34 Merge pull request #23 from QRun-IO/fix/outbound-enqueue-table-cache
fix(outbound): record-change listener only costs queries for configured source tables
2026-10-15 07:38:22 -05:00
james.maesandClaude Opus 5.5 d1eb78c1df fix(outbound): the record-change listener only costs queries for configured source tables
OutboundEnqueue applied to every non-edi_ table, and per record opened a
system session and queried edi_agreement_document, so every insert and
update of audit, auditDetail, worm_audit_chain, jobs, alerts and the
rest paid for it (and UPDATEs fetched old records for it).

- OutboundSourceTables (per EdiRuntime) caches the source tables that
  have an active RECORD_CHANGE outbound document on an active agreement,
  loaded from committed data. appliesTo answers from it, so other tables
  do no work at all.
- Changes to edi_agreement / edi_agreement_document invalidate it, and
  again after their transaction commits (a reload that raced the change
  is discarded); until then the changing transaction bypasses the cache
  and sees its own definitions. A 60s TTL covers other processes.
- onRecordsChanged resolves definitions (documents + active agreements)
  once per event, not once per record. MANUAL and ESB_EVENT enqueue keep
  their semantics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:24:33 -05:00
james.maes 2810e00e9c Merge pull request #22 from QRun-IO/fix/producer-version-from-build
fix: qbit metadata version comes from the build
2026-10-15 01:42:56 -05:00
james.maesandClaude Opus 5.5 89a95737a5 fix: the qbit metadata version comes from the build, not a hardcoded constant
The producer read a literal version, so released artifacts reported a stale
or -SNAPSHOT version. qbit-edi.properties (version=${project.version}) is
Maven-filtered and the producer loads it; tests assert producer == bundle ==
artifact version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:29:54 -05:00
james.maes 86b8dbf13d Merge pull request #21 from QRun-IO/fix/help-bundle-version-test
test(help): bundle version test compares with the artifact version
2026-10-14 23:18:42 -05:00
james.maesandClaude Opus 5.5 4471a13e78 test(help): compare the help bundle version with the artifact version, not a hardcoded SNAPSHOT
The bundle version is now Maven-filtered, so a literal SNAPSHOT in the test
fails every release build. Surefire passes the artifact version in
qbit.artifactVersion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:07:33 -05:00
james.maes c39d78901c Merge pull request #20 from QRun-IO/fix/help-bundle-version
fix(help): help bundle version comes from the build, not a hardcoded SNAPSHOT
2026-10-14 22:37:20 -05:00
james.maesandClaude Opus 5.5 1e25fe54d2 fix(help): the help bundle carries the artifact version (Maven-filtered) instead of a hardcoded SNAPSHOT
On release branches the bundle's version and the producer's differed, so the
bundle test that asserts they match failed, and released bundles recorded
SNAPSHOT as their provenance. help/*.yaml is now filtered:
version: ${project.version}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:09:48 -05:00
james.maes 0fe99db48d Merge pull request #19 from QRun-IO/feature/help-bundle
feat(help): qbit-edi help bundle via QQQ help bundles, with coverage gate
2026-10-14 20:45:30 -05:00
james.maesandClaude Opus 5.5 b08057180b feat(help): ship the qbit-edi help bundle through QQQ help bundles, with coverage gate
Host-neutral help (deployment-specific facts removed), registered with
HelpBundles, gated by HelpBundleAssertions. QQQ 4.1.0-RC.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:27:22 -05:00
james.maes 79b85a163d Merge pull request #18 from QRun-IO/feature/tables-populated-defaults
feat: every edi table explains itself; Setup status; Install sample maps
2026-10-14 17:42:45 -05:00
james.maesandClaude Opus 5.5 4e32aebc00 fix: X12 routing honors identity isActive; tests for draft maps and host help
- X12 inbound routing and outbound building ignore inactive identities, as
  EDIFACT does (UPGRADE note).
- Test that a draft map bound to an outbound document is never used (it
  already was not) and that inactive identities stop sending.
- hostHelpIsKept test; drop the unfounded concurrency claim on Install
  sample maps; plain wording in the help bundle header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:32:54 -05:00
james.maesandClaude Opus 5.5 7bafab62cc docs: upgrade note wording
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:17:54 -05:00
james.maesandClaude Opus 5.5 a1914f7553 feat: table help, Setup status, Install sample maps, tables guardrail
Every edi_* table and the app get help bundle text saying how rows get
created and the setup order. The home gets a Setup status section. A new
Install sample maps table action adds the bundled synthetic maps as
inactive, unbound drafts (insert-only by name).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:17:41 -05:00
james.maes 51a5eaef74 Merge pull request #17 from QRun-IO/fix/repeat-schedule-time-zone
fix: no cron time zone on repeat-seconds worker schedules
2026-10-14 15:10:02 -05:00
james.maesandClaude Opus 5.5 47c3aac1b2 fix: no cron time zone on repeat-seconds worker schedules
OutboundProcesses.process set cronTimeZoneId("UTC") on every schedule.
Core instance validation refuses a time zone on a non-cron schedule, so a
host that configures a scheduler (kof22) failed validation at start. Only
cron schedules get UTC now; OutboundTest covers both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:59:12 -05:00
james.maes bf8f40959a Merge pull request #16 from QRun-IO/feature/orb-0.6.9
ci: qqq-orb 0.6.9 (refresh SNAPSHOT dependencies)
2026-10-14 10:57:01 -05:00
james.maesandClaude Opus 5.5 f7c031b793 ci: qqq-orb 0.6.9 (refresh SNAPSHOT dependencies)
Moves qqq-orb@0.6.8 to qqq-orb@0.6.9, which passes --update-snapshots so cached SNAPSHOT dependencies are re-checked on every build (QRun-IO/qqq-orb#22).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:06:51 -05:00
james.maes cd9423e966 Merge pull request #15 from QRun-IO/feature/phase-f-mapper-api
feat: phase f1 - spec'd UI API extension for the EDI mapper and sandbox
2026-10-14 09:58:48 -05:00
james.maes d97418447b Merge develop into feature/phase-f-mapper-api 2026-10-14 09:49:11 -05:00
james.maesandClaude Opus 5.5 c15872a373 feat: phase f1 - spec'd UI API extension for the EDI mapper
- MiddlewareSpecExtension "edi" 1.0.0 under /qqq/v1/qbit/edi:
  GET guides/{guideId}/tree, GET maps/{mapId}/target,
  POST sandbox/preview (unsaved rule overrides, base64 uploads up to
  5 MB, PHI suppression) and POST maps/{mapId}/validate.
- MapPreviewService shared by the Preview Map action and the endpoint;
  MapRuleValidator for paths, transforms and arguments.
- Committed fragment src/main/resources/openapi/qqq-v1/edi.yaml with a
  contract test; HTTP tests in strict raw-route mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:48:59 -05:00
james.maes fc800b8124 Merge pull request #14 from QRun-IO/feature/qqq-rc6
build: move to QQQ 4.1.0-RC.6 and remove RC.5 stand-ins
2026-10-14 09:26:57 -05:00
james.maesandClaude Opus 5.5 30ac598952 build: move to QQQ 4.1.0-RC.6 and remove RC.5 stand-ins
- qqq-bom 4.1.0-RC.6 (guardrail G7).
- Replace the EdiClassifications stand-in with core DataClassification
  on edi_ tables and fields and on the healthcare sample tables.
- Value-free QAlerts.notify notifications for exception assignment and
  partner exceptions.
- Fix the shared-transport possible-value filter grouping.
- PhiLogScope kept (RC.6 has no query-log redaction), now marked for
  core #1147 by RC.7; file-gateway stand-ins kept until that api ships.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:17:37 -05:00
james.maes 21b0e61cbb Merge pull request #13 from QRun-IO/feature/phase-g-edifact
feat: phase g - EDIFACT
2026-10-14 08:59:26 -05:00
james.maesandClaude Opus 5.5 5afeefb1c2 Merge develop into feature/phase-g-edifact
Resolve guide loader, validator, demo and docs conflicts with phases c and e;
HealthcareValidation now takes the standard-neutral EdiDocument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:50:06 -05:00
james.maes b9ea954e1f Merge pull request #11 from QRun-IO/feature/phase-e-healthcare
feat: phase e - HIPAA healthcare documents, SNIP 3-5, PHI handling, canary tests
2026-10-14 08:46:36 -05:00
james.maesandClaude Opus 5.5 ea4c48ad66 feat: phase e - HIPAA healthcare documents, SNIP 3-5, PHI handling
- In-house 005010 baseline guides for 837P, 837I, 835, 270/271, 276/277
  and 834; 999 for HIPAA documents with IK3/IK4/CTX/IK5.
- SNIP 3 balancing (837 claim vs line charges, 835 payment vs claims and
  PLB, CAS adjustments), SNIP 4 situational rules, SNIP 5 format checks
  (ICD-10 shape, NPI check digit, ZIP) and an ExternalCodeSetProvider SPI.
- PHI classification via the RC.5 stand-in; healthcare keys go to
  phiBusinessKey; no element values in errors, events, alerts, logs or
  acks for PHI documents; audited Download Raw Document action.
- Healthcare sample maps, canary PHI egress tests, demo clearinghouse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:38:24 -05:00
james.maes 09b77ec122 Merge pull request #10 from QRun-IO/feature/phase-c-as2
feat: phase c - AS2 transport
2026-10-14 08:27:25 -05:00
james.maesandClaude Opus 5.5 4f8916246a feat: phase g - EDIFACT
- EDIFACT envelope reader and writer (UNA, UNB/UNZ, UNG/UNE, UNH/UNT,
  release-character escaping, syntax 3 and 4) with envelope findings.
- Routing by UNB identities, EDIFACT control references and duplicate
  detection; mixed X12 and EDIFACT inbox processing.
- In-house baseline guides (D96A, D01B) for ORDERS, ORDRSP, DESADV,
  INVOIC and CONTRL; CONTRL generation and inbound reconciliation.
- Sample maps (ORDERS in; ORDRSP, DESADV, INVOIC out) and demo partner
  Lindqvist Retail AB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:26:05 -05:00
134 changed files with 27697 additions and 493 deletions

No files matched your search

+11 -57
View File
@@ -1,60 +1,14 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.6.8
commands:
configure-package-install:
steps:
- run:
name: Keep package installation unattended
command: |
# These build machines are discarded after the job.
sudo install -d /etc/needrestart/conf.d
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
qqq-orb: kingsrook/qqq-orb@0.7.12
workflows:
test_only:
jobs:
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
filters:
branches:
ignore: /(develop|release\/.*)/
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: snapshot
filters:
branches:
only: [develop]
publish_release_candidate:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release_candidate
filters:
branches:
only: [/release\/.*/]
publish_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release
filters:
branches:
ignore: /.*/
tags:
only: [/^v\d+\.\d+\.\d+$/]
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: 0.7.12
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /^v\d+\.\d+\.\d+$/
+23
View File
@@ -0,0 +1,23 @@
name: CodeQL
'on':
push:
branches:
- main
- develop
- release/**
- hotfix/**
pull_request: {}
schedule:
- cron: 41 16 * * 0
workflow_dispatch: {}
permissions:
contents: read
actions: read
security-events: write
packages: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
+62 -13
View File
@@ -1,25 +1,74 @@
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
# the release line and the release/* push runs CircleCI publish_release_candidate.
name: Release candidate
on:
'on':
workflow_dispatch:
inputs:
dry_run:
description: Print the plan without merging or creating branches
description: Plan a cut without changing release state
type: boolean
default: false
finalize:
description: Retry finalization of an existing cut; do not cut another RC
type: boolean
default: false
recover:
description: Recover the same published RC after a deployment timeout; never
reupload
type: boolean
default: false
circle_build:
description: Original Circle publisher build number (recovery only)
type: string
default: ''
merge_sha:
description: Existing cut merge_sha (finalize retry only)
type: string
default: ''
bump_sha:
description: Existing cut bump_sha (finalize retry only)
type: string
default: ''
branch:
description: Existing cut branch (finalize retry only)
type: string
default: ''
version:
description: Existing cut version (finalize retry only)
type: string
default: ''
pull_request:
types: [closed]
branches: [develop]
types:
- closed
branches:
- develop
repository_dispatch:
types:
- qbit-rc-published
permissions:
contents: write
actions: read
checks: read
jobs:
release-rc:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
'release:rc'))
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
}}
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
== 'workflow_dispatch' && inputs.finalize) }}
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
}}
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|| '' }}
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|| inputs.merge_sha || '' }}
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|| inputs.bump_sha || '' }}
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|| inputs.branch || '' }}
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|| inputs.version || '' }}
secrets: inherit
+16
View File
@@ -0,0 +1,16 @@
profile: qbit
orb_version: 0.7.12
default_branch: develop
java_version: '21'
browsers: true
api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: stable
test_branch_policy: non-release
publish:
snapshot: true
release_candidate: true
release: true
asciidoc: false
release_completion: true
+1 -1
View File
@@ -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
+46 -1
View File
@@ -4,10 +4,39 @@ Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic
## [Unreleased]
### Changed
- EDI home is business-only: System, Dead letters, Certificates expiring and Setup status move to `EdiHome.ADMIN_WIDGET_NAMES` for the host's Admin home (gated to `edi.admin.hasAccess`, full width); every home widget now requires `edi.hasAccess` and is hidden without it. A new "Inbound documents are delayed" alert links to the delayed inbound documents once the oldest is older than `inboundDelayedAfterMinutes` (default 60). Setup counts that fail show "Unavailable" and are logged instead of -1.
- UI API extension edi 1.1.0 (requires QQQ 4.1.0-RC.15, UI API 1.6.0): operationIds, standard error responses with BasicErrorResponseV1, extension tags, and retained deprecated EdiErrorV1 schema.
- Derive qbit metadata version from the Maven artifact version, keeping release metadata and help bundle versions in sync.
- Require QQQ 4.1.0-RC.7 for core help-bundle registration and coverage validation.
- X12 routing and sending honor identity `isActive`, matching EDIFACT.
### Added
- Complete, host-neutral `help/qbit-edi.yaml` bundle registered with QQQ core: app, tables, fields, actions, widgets and possible-value sources, with a coverage gate and HOST overrides. Table/app fallback remains available without `helpContent`.
- Setup status section on the EDI home (partner, identities, agreement, agreement documents, transport, certificates, guides, maps) with the next step; it counts only the active rows the runtime uses.
- "Install sample maps" action on Maps: standard streamed ETL, insert-only by name, installs the 32 bundled synthetic maps as draft, unbound maps bound to the built-in guide.
- `EdiTablesGuardrailTest`: every table is used at runtime, seeded or listed as starting empty, and has help.
- 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 +67,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).
+1 -1
View File
@@ -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
+192 -20
View File
@@ -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.7+**, 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,9 +57,26 @@ 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.
## In-app help
The producer registers [help/qbit-edi.yaml](src/main/resources/help/qbit-edi.yaml), a `QBIT`
bundle versioned with this artifact. It covers the EDI app, tables, fields, actions, widgets,
and possible-value sources. Live `{{...}}` references follow the current metadata.
Hosts can register their own `layer: HOST` bundle with `HelpBundles.register(qInstance,
"help/my-host.yaml")`, using the same keys and roles to override selected entries. Enable
core's `helpContent` provider with provenance fields, then call
`QInstanceHelpContentManager.loadHelpContent(qInstance)` and `HelpBundles.syncOnStartup(qInstance)`
after defining metadata. Core preserves user edits during sync. If you configure a different
EDI app name, use that name in your host's app help key.
Registration is safe without `helpContent`; those hosts retain the table/app help fallback.
`EdiHelpBundleTest` runs the core `HelpBundleAssertions` coverage gate and checks widget and
possible-value-source coverage during `./mvnw verify`, without host-specific exclusions.
## Tables
There are **26 persisted tables**, whose SQL names equal their QQQ names.
@@ -53,9 +90,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.1.0`,
under `/qqq/v1/qbit/edi`, requiring QQQ 4.1.0-RC.15 (UI API 1.6.0). 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`; extension component names are
`Edi...V1`, alongside core `BasicErrorResponseV1`. `EdiErrorV1` remains deprecated for compatibility. Regenerate with the locally installed migration build:
```powershell
./mvnw.cmd -o -B '-Dqqq.snapshot.version=4.1.99-SNAPSHOT' '-Dtest=EdiApiContractTest' '-Dedi.openapi.update=true' test
```
Core provides `UiApiContract.check`, used to compare generated and checked-in YAML, validate
operation IDs and references, and run `OpenApiCompatibilityChecker` against the committed 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 +154,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,11 +179,35 @@ 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.
## Setup and starter data
No Liquibase changelog or host include is needed: the qbit seeds its own reference data. When the
host calls `EdiRuntime.initialize()` at startup (after the schema exists) the 41 built-in guides
load, with their segments, elements, rules, code lists and code values. It only changes a guide when
the bundled content changes, so edits to your own rows are kept.
Everything else is host or runtime data, and every table explains itself when empty (help bundle
`help/qbit-edi.yaml`). Set up in this order:
1. Partner.
2. Identities: one of yours (no partner) and one per partner.
3. Agreement: pairs your identity with the partner's, for a standard and test or production.
4. Agreement documents: one per transaction set and direction, with the guide and optionally a map.
5. Transport (AS2, SFTP, HTTPS), linked on the agreement. Certificates only for AS2 signing and encryption.
The **Setup status** widget (`edi_home_setup`, for the host's Admin home) shows each step, whether
it is done (counting only the active rows the runtime uses) and the next step. A count that fails
shows "Unavailable" and is logged.
Maps start empty. The **Install sample maps** action on Maps adds the 32 bundled synthetic examples
as draft maps with no partner and no agreement document, by name and never over an existing map. Point
a map at your own table, preview it, activate it, then select it on an agreement document.
## Permissions
Full table permissions include `edi.partner.read|insert|edit|delete`, `edi.map.*`,
@@ -118,7 +230,14 @@ Full table permissions include `edi.partner.read|insert|edit|delete`, `edi.map.*
| Scheduled workers / ESB enqueue | `edi.admin` |
Partner locks use `edi_partner` with `edi.admin` all-access. Home queries explicitly enforce
the viewing user's permissions. Processes are table/record actions, never navigation entries.
the viewing user's permissions.
The EDI home shows business work only. Its widgets (`EdiHome.HOME_WIDGET_NAMES`) need
`edi.hasAccess` and are hidden without it. "Inbound documents delayed" appears only when the oldest
unprocessed inbound document is older than `inboundDelayedAfterMinutes` (default 60), with a link to
those documents. System widgets (`EdiHome.ADMIN_WIDGET_NAMES`: System, Dead letters, Certificates
expiring, Setup status) are not on the EDI home; they need `edi.admin.hasAccess`, are hidden without
it, and a host places them on its Admin home. Processes are table/record actions, never navigation entries.
## Configuration
@@ -145,29 +264,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 +381,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
+11
View File
@@ -0,0 +1,11 @@
# Upgrade notes
## Unreleased
- No schema change and no Liquibase include.
- Behavior change: an inactive identity (`isActive` false) now stops X12 routing and sending, as it already did for EDIFACT. Inbound documents from or to it are rejected as an unknown partner, and outbound requests for its agreements wait until it is active. Identities default to active, so only identities someone switched off are affected.
- Hosts that already call `EdiRuntime.initialize()` at startup keep getting the 41 built-in guides and code lists. A host that never called it will now see "Needs setup" for Guides and code lists on the home.
- New widget "Setup status" (`edi_home_setup`).
- Behavior change: the EDI home shows business widgets only (`EdiHome.HOME_WIDGET_NAMES`), each now requiring `edi.hasAccess` (hidden without it). `edi_home_system`, `edi_home_dead`, `edi_home_certificates` and `edi_home_setup` left the EDI home: they are listed in `EdiHome.ADMIN_WIDGET_NAMES`, require `edi.admin.hasAccess` (hidden without it), and the host places them on its Admin home.
- New business widget `edi_home_delayed` ("Inbound documents are delayed"), shown only when the oldest unprocessed inbound document is older than the new config value `inboundDelayedAfterMinutes` (default 60).
- New table action "Install sample maps" on Maps, permission `edi.admin`. It is never run automatically and only adds draft maps whose name does not exist yet.
+23 -1
View File
@@ -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
View File
@@ -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
View File
@@ -1,6 +1,6 @@
# EDI guide JSON
The five files in `src/main/resources/edi/guides/` are original QQQ baseline guides, not licensed
The files in `src/main/resources/edi/guides/` are original QQQ baseline guides, not licensed
X12/TR3 documentation or complete code sets. JSON loads into `guide.Guide` with
`BuiltInGuideLoader.read(InputStream)`. `GuideRepository.load(id)` reconstructs the same model from
the normalized `edi_guide*` and code-list tables.
@@ -49,3 +49,30 @@ historical rows and their children remain addressable by id. A reverted resource
new revision. Built-in guide writes require the loader's security key (`edi.guide.loader`), or a
system service session. Configuration editors can receive `CUSTOM`/`IMPORTED` values of the
`edi_guide_source` key without receiving permission to edit `BUILT_IN` rows.
## EDIFACT baselines
Set `standard` to `EDIFACT`, `transactionSet` to the UNH message type, and `version` to the
concatenated UNH version/release (`D96A`, `D01B`). The ten bundled resources are in-house retail
subsets with short field names and only the code values needed by the maps. They contain no
copied directory descriptions and are not complete UNECE directories or EANCOM certifications.
UNH and UNT appear as actual guide segments. UNB/UNZ and UNG/UNE are checked by the envelope
reader. UNH association codes, such as `EAN008`, remain available as component 5 of UNH02.
EDIFACT mandatory (`M`) maps to `REQUIRED`; conditional (`C`) maps to `SITUATIONAL` with
`condition` or rules where needed. A component uses the existing `position` plus
`componentPosition`, for example NAD02 component 1. EDIFACT decimal values accept `.` or `,`;
DECIMAL/UNIT inbound transforms normalize them and outbound DECIMAL uses the agreement's mark.
Segment groups use the existing loop tree: SG1 starts with RFF, SG2 with NAD, and the retail
line loop SG25 starts with LIN. These are stable local guide loop labels, not a claim that every
message/directory assigns the same published group numbers. DESADV uses SG10 (CPS) containing
SG17 (LIN), matching carton/item associations. Map examples are `NAD[NAD01=BY]/NAD02-1`,
`SG25/LIN/LIN03-1` and `SG25[*]/QTY/QTY01-2`. Named loops take precedence over element parsing,
so `SG25` is a loop rather than an element of a hypothetical `SG` segment.
CONTRL baselines include UCI, optional UCF group responses, UCM message responses and nested
UCS/UCD positional errors. Shared validator diagnostics are translated to EDIFACT 0085 codes;
submitted erroneous values are not copied into the acknowledgement. `GROUP_ONLY` reports the
interchange outcome, `ALL_TRANSACTIONS` includes accepted messages, and `ERRORS_ONLY` includes
rejected message details. A CONTRL is never itself acknowledged. APERAK is deferred.
+16 -3
View File
@@ -52,9 +52,9 @@ for Shipment/Order/Pack/Item loops and parent numbering.
DATE/TIME format QQQ values as X12 dates/times; DECIMAL multiplies by the implied `scale`.
Other registered transforms operate on the source value. Conditions use the existing EDI rule
language over segments already emitted. A missing required mapping input fails mapping;
structurally invalid mapped output opens VALIDATION and is never sent. ST/SE are generated by
the mapper and group-local controls are assigned by the worker. Component-target paths are
currently rejected explicitly; the bundled retail maps use simple elements.
structurally invalid mapped output opens VALIDATION and is never sent. ST/SE or UNH/UNT are generated by
the mapper; the worker allocates controls. Component-target paths use `-`, for example
`LIN03-1` and `LIN03-2`, and retain components separately so the EDIFACT writer can escape data.
## Warehouse and logistics samples
@@ -76,3 +76,16 @@ collide with the header N1 loop) and the stop sequence is mapped from S501. A 21
events; the sample map records the first (`LX[1]/...`) and a host that wants all of them maps an
association instead. Optional outbound qualifiers such as MAN01 and L1102 are constants, so the
sample source rows must carry the value they qualify (the carton SSCC, the bill of lading).
## EDIFACT retail samples
Each of D96A and D01B includes `ORDERS-<version>-sample.json` inbound to `sample_order` and
`lines`, and outbound `ORDRSP` from `sample_order`, `DESADV` from `sample_shipment` with
`cartons.items`, and `INVOIC` from `sample_invoice` with `lines`. Test sources define these
synthetic tables. ORDERS carries its business key in BGM02, date in qualified DTM01 component 2,
buyer in qualified NAD02 component 1, and items in SG25. BGM's document codes are 220, 231, 351
and 380 respectively. These subsets intentionally carry only the fields exercised by the samples.
Composite paths address values, not delimiter-joined strings. An identifier containing `+`, `:`
or `'` remains a single value; the writer supplies release escapes. The outbound agreement owns
UNA settings. Separate map resources per directory keep future version-specific changes isolated.
+3
View File
@@ -3,6 +3,7 @@ CREATE TABLE edi_agreement (
component_separator VARCHAR(255),
control_number_scope VARCHAR(255),
create_date TIMESTAMP,
decimal_mark VARCHAR(255),
duplicate_window_days INTEGER,
element_separator VARCHAR(255),
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
@@ -19,11 +20,13 @@ CREATE TABLE edi_agreement (
outbound_transport_id INTEGER,
partner_id INTEGER,
partner_identity_id INTEGER,
release_character VARCHAR(255),
repetition_separator VARCHAR(255),
request_ta1 BOOLEAN,
segment_terminator VARCHAR(255),
send_ta1 VARCHAR(255),
standard VARCHAR(255),
syntax_identifier VARCHAR(255),
usage_indicator VARCHAR(255),
UNIQUE (local_identity_id, partner_identity_id, standard, usage_indicator)
);
+30 -1
View File
@@ -126,7 +126,36 @@
</dependencies>
<build>
<resources>
<!-- help bundles carry the artifact version (QQQ help bundle provenance) -->
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>help/*.yaml</include>
<include>qbit-edi.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>help/*.yaml</exclude>
<exclude>qbit-edi.properties</exclude>
</excludes>
</resource>
</resources>
<plugins>
<!-- the artifact version, for the help bundle version test -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables combine.children="append">
<qbit.artifactVersion>${project.version}</qbit.artifactVersion>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
@@ -213,7 +242,7 @@
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.5</qqq.snapshot.version>
<qqq.snapshot.version>4.1.0-RC.15</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();
@@ -69,6 +89,7 @@ public class EdiQBitConfig implements QBitConfig
private String appLabel = "EDI";
private String parentAppName;
private String publicBaseUrl;
private Integer inboundDelayedAfterMinutes = 60;
@@ -94,6 +115,10 @@ public class EdiQBitConfig implements QBitConfig
{
errors.add("secretsResolver and partnerLocks are required");
}
if(inboundDelayedAfterMinutes == null || inboundDelayedAfterMinutes < 1)
{
errors.add("inboundDelayedAfterMinutes must be at least 1");
}
if(appName == null || appName.isBlank())
{
errors.add("appName is required");
@@ -370,4 +395,22 @@ public class EdiQBitConfig implements QBitConfig
publicBaseUrl = value;
return (this);
}
/*******************************************************************************
** How old (in minutes) the oldest unprocessed inbound document may get before
** the EDI home says "Inbound documents are delayed". Default 60.
*******************************************************************************/
public Integer getInboundDelayedAfterMinutes()
{
return (inboundDelayedAfterMinutes);
}
/*******************************************************************************
** Set the inbound delay threshold in minutes.
*******************************************************************************/
public EdiQBitConfig withInboundDelayedAfterMinutes(Integer value)
{
inboundDelayedAfterMinutes = value;
return (this);
}
}
@@ -21,9 +21,13 @@
package com.kingsrook.qbits.edi;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.help.bundles.HelpBundles;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer;
@@ -36,13 +40,41 @@ public class EdiQBitProducer implements QBitMetaDataProducer<EdiQBitConfig>
{
public static final String GROUP_ID = "com.kingsrook.qbits";
public static final String ARTIFACT_ID = "qbit-edi";
public static final String VERSION = "0.1.0";
public static final String VERSION = loadVersion();
private EdiQBitConfig qBitConfig;
private String namespace;
/*******************************************************************************
** Read the artifact version from the Maven-filtered resource.
*******************************************************************************/
private static String loadVersion()
{
try(InputStream inputStream = EdiQBitProducer.class.getClassLoader().getResourceAsStream("qbit-edi.properties"))
{
if(inputStream == null)
{
throw new IllegalStateException("Missing qbit-edi.properties; build with Maven to supply the artifact version");
}
Properties properties = new Properties();
properties.load(inputStream);
String version = properties.getProperty("version");
if(version == null || version.isBlank() || version.contains("${"))
{
throw new IllegalStateException("Missing or unfiltered version in qbit-edi.properties; build with Maven");
}
return (version);
}
catch(IOException e)
{
throw new IllegalStateException("Could not read the artifact version from qbit-edi.properties", e);
}
}
/*******************************************************************************
** Fluent configuration setter.
*******************************************************************************/
@@ -130,6 +162,16 @@ public class EdiQBitProducer implements QBitMetaDataProducer<EdiQBitConfig>
MetaDataProducerMultiOutput output = QBitMetaDataProducer.super.produce(instance);
output.addSelfToInstance(instance);
EdiRuntime.install(instance, qBitConfig);
// Registration is metadata-only; core startup sync skips hosts without helpContent.
HelpBundles.register(instance, "help/qbit-edi.yaml");
// The app name is configurable; its bundle key must follow the installed app.
HelpBundles.of(instance).getBundles().getLast().getEntries().stream()
.filter(entry -> "app:edi;slot:help".equals(entry.getKey()))
.forEach(entry -> entry.setKey("app:" + qBitConfig.getAppName() + ";slot:help"));
if(instance.getTable("helpContent") == null)
{
com.kingsrook.qbits.edi.metadata.EdiHelp.apply(instance, qBitConfig.getAppName());
}
instance.withSupplementalMetaData(new com.kingsrook.qbits.edi.metadata.EdiRecordLabels());
instance.withRecordChangeListener(new com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference(com.kingsrook.qbits.edi.flow.OutboundEnqueue.class));
instance.withSupplementalMetaData(new com.kingsrook.qbits.edi.transport.EdiTransportPluginsMetaData()
@@ -138,6 +180,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;
@@ -64,6 +64,7 @@ public final class EdiRuntime
private final Ack999Builder ack999 = new Ack999Builder();
private final TA1Builder ta1 = new TA1Builder();
private final InboundAckReader acknowledgements = new InboundAckReader();
private final com.kingsrook.qbits.edi.flow.OutboundSourceTables outboundSources = new com.kingsrook.qbits.edi.flow.OutboundSourceTables();
/*******************************************************************************
** Construct services without resolving keys until content is stored or opened.
@@ -71,6 +72,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())
@@ -94,6 +96,22 @@ public final class EdiRuntime
return (Objects.requireNonNull(INSTANCES.get(QContext.getQInstance()), "EDI runtime has not been installed"));
}
/*******************************************************************************
** Services for the current QQQ context, or null when none are installed.
*******************************************************************************/
public static EdiRuntime find()
{
return (QContext.getQInstance() == null ? null : INSTANCES.get(QContext.getQInstance()));
}
/*******************************************************************************
** Cached source tables that have active RECORD_CHANGE outbound definitions.
*******************************************************************************/
public com.kingsrook.qbits.edi.flow.OutboundSourceTables outboundSources()
{
return (outboundSources);
}
/*******************************************************************************
** Call at host startup after DDL, with the service session established.
*******************************************************************************/
@@ -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,294 @@
/*
* 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.middleware.javalin.specs.v1.responses.BasicErrorResponseV1;
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.1.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.1.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", BasicErrorResponseV1.class.getSimpleName()),
new BasicResponse(HttpStatus.UNAUTHORIZED, "Sign in required", BasicErrorResponseV1.class.getSimpleName()),
new BasicResponse(HttpStatus.FORBIDDEN, "Permission denied", BasicErrorResponseV1.class.getSimpleName()),
new BasicResponse(HttpStatus.NOT_FOUND, "Missing or invisible record", BasicErrorResponseV1.class.getSimpleName()),
new BasicResponse(HttpStatus.CONTENT_TOO_LARGE, "Host HTTP request size limit exceeded", BasicErrorResponseV1.class.getSimpleName())));
}
/*******************************************************************************
** 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("Deprecated since 1.1.0: errors use BasicErrorResponseV1.")
.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));
}
}
@@ -40,6 +40,8 @@ import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
@@ -47,6 +49,7 @@ 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.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.AlertData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.StatisticsData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.TableData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
@@ -56,9 +59,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Typed EDI home. Every data query explicitly enforces the viewing user's access.
@@ -67,9 +72,31 @@ public class EdiHome extends AbstractWidgetRenderer
{
public static final List<String> ACTIVITY = List.of("edi_transaction", "edi_interchange", "edi_exception");
public static final List<String> SETUP = List.of("edi_partner", "edi_agreement", "edi_transport", "edi_certificate", "edi_guide", "edi_code_list", "edi_map");
private static final Map<String, String> LABELS = new LinkedHashMap<>();
/*******************************************************************************
** System-audience widgets (component and queue health, dead letters,
** certificate expiry, setup and configuration). They are not on the EDI
** business home: a host places them on its Admin home. Each is gated to
** ADMIN_PERMISSION_BASE_NAME and hidden from everyone else.
*******************************************************************************/
public static final List<String> ADMIN_WIDGET_NAMES = List.of("edi_home_system", "edi_home_dead", "edi_home_certificates", "edi_home_setup");
/*******************************************************************************
** Business widgets on the EDI home, in order. Each is gated to the EDI app
** permission (edi.hasAccess) and hidden from everyone else.
*******************************************************************************/
public static final List<String> HOME_WIDGET_NAMES = List.of("edi_home_delayed", "edi_home_in_today", "edi_home_out_today", "edi_home_exceptions",
"edi_home_overdue", "edi_home_queue", "edi_home_acks", "edi_home_errors", "edi_home_volume");
/*******************************************************************************
** The EDI administrator permission (edi.admin.hasAccess), as on the admin processes.
*******************************************************************************/
public static final String ADMIN_PERMISSION_BASE_NAME = EdiQBitConfig.PERMISSION_BASE_NAME + ".admin";
private static final QLogger LOG = QLogger.getLogger(EdiHome.class);
private static final Map<String, String> LABELS = new LinkedHashMap<>();
private static final Map<String, String> DOCUMENT_NAMES = new HashMap<>();
private static final List<String> TABLES = List.of("queue", "acks", "errors", "volume", "system");
private static final List<String> TABLES = List.of("setup", "queue", "acks", "errors", "volume", "system");
static
{
@@ -79,12 +106,14 @@ public class EdiHome extends AbstractWidgetRenderer
Map.entry("835", "Remittance"), Map.entry("270", "Eligibility Inquiry"), Map.entry("271", "Eligibility Response"), Map.entry("276", "Claim Status Inquiry"),
Map.entry("277", "Claim Status Response"), Map.entry("834", "Enrollment"), Map.entry("997", "Functional Ack"), Map.entry("999", "Implementation Ack"),
Map.entry("TA1", "Interchange Ack")));
LABELS.put("delayed", "Inbound documents delayed");
LABELS.put("in_today", "Inbound today");
LABELS.put("out_today", "Outbound today");
LABELS.put("exceptions", "Open exceptions");
LABELS.put("overdue", "Overdue acks");
LABELS.put("dead", "Dead letters");
LABELS.put("certificates", "Certificates expiring (30 days)");
LABELS.put("setup", "Setup status");
LABELS.put("queue", "Exception queue");
LABELS.put("acks", "Overdue acknowledgements");
LABELS.put("errors", "Recent errors (24 hours)");
@@ -98,7 +127,7 @@ public class EdiHome extends AbstractWidgetRenderer
public static QAppMetaData app(EdiQBitConfig config, List<QTableMetaData> tables)
{
QAppMetaData app = new QAppMetaData().withName(config.getAppName()).withLabel(config.getAppLabel()).withIcon(new QIcon("swap_horiz"))
.withWidgets(LABELS.keySet().stream().map(n -> "edi_home_" + n).toList())
.withWidgets(HOME_WIDGET_NAMES)
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
.withSection(new QAppSection().withName("activity").withLabel("Activity").withTables(ACTIVITY))
.withSection(new QAppSection().withName("setup").withLabel("Setup").withTables(SETUP));
@@ -108,15 +137,23 @@ public class EdiHome extends AbstractWidgetRenderer
}
/*******************************************************************************
** RC.5 typed statistics and tables; no raw markup in metadata or widget data.
** RC.5 typed statistics, tables and one alert. Business widgets need the EDI
** app permission; the ADMIN_WIDGET_NAMES need the EDI administrator
** permission. Denied widgets are hidden, never shown as locked shells.
*******************************************************************************/
public static List<QWidgetMetaData> widgets()
{
List<QWidgetMetaData> widgets = new ArrayList<>();
LABELS.forEach((name, label) -> widgets.add(new QWidgetMetaData().withName("edi_home_" + name).withLabel(label)
.withType((TABLES.contains(name) ? WidgetType.TABLE : WidgetType.STATISTICS).getType())
.withGridColumns(TABLES.contains(name) ? 12 : 2).withCodeReference(new QCodeReference(EdiHome.class))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))));
LABELS.forEach((name, label) ->
{
String widgetName = "edi_home_" + name;
boolean admin = ADMIN_WIDGET_NAMES.contains(widgetName);
String type = name.equals("delayed") ? WidgetType.ALERT.getType() : (TABLES.contains(name) ? WidgetType.TABLE : WidgetType.STATISTICS).getType();
widgets.add(new QWidgetMetaData().withName(widgetName).withLabel(label).withType(type)
.withGridColumns(TABLES.contains(name) || admin || name.equals("delayed") ? 12 : 2).withCodeReference(new QCodeReference(EdiHome.class))
.withPermissionRules(new QPermissionRules().withPermissionBaseName(admin ? ADMIN_PERMISSION_BASE_NAME : EdiQBitConfig.PERMISSION_BASE_NAME)
.withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withDenyBehavior(DenyBehavior.HIDDEN)));
});
return (widgets);
}
@@ -136,8 +173,10 @@ public class EdiHome extends AbstractWidgetRenderer
case "exceptions" -> count("edi_exception", openExceptions());
case "overdue" -> count("edi_functional_group", overdueAcks(now));
case "dead" -> count("edi_exception", openExceptions().withCriteria("type", QCriteriaOperator.IN, "DEAD_LETTER", "TRANSPORT"));
case "delayed" -> delayed(now);
case "certificates" -> count("edi_certificate", new QQueryFilter().withCriteria("notAfter", QCriteriaOperator.LESS_THAN_OR_EQUALS, now.plus(30, ChronoUnit.DAYS))
.withCriteria("status", QCriteriaOperator.EQUALS, "ACTIVE"));
case "setup" -> setup();
case "queue" -> queue(now);
case "acks" -> acks(now);
case "errors" -> errors(now);
@@ -191,6 +230,95 @@ public class EdiHome extends AbstractWidgetRenderer
/*******************************************************************************
** The setup steps in order, with their status and the next step. Counts are
** taken as the viewer; a table the viewer cannot read shows as not visible.
*******************************************************************************/
private static RenderWidgetOutput setup() throws QException
{
QQueryFilter active = new QQueryFilter().withCriteria("isActive", QCriteriaOperator.EQUALS, true);
EdiSetupStatus.Counts counts = new EdiSetupStatus.Counts(safeCount("edi_partner", new QQueryFilter()),
safeCount("edi_identity", new QQueryFilter().withCriteria("isActive", QCriteriaOperator.EQUALS, true).withCriteria("partnerId", QCriteriaOperator.IS_BLANK)),
safeCount("edi_identity", new QQueryFilter().withCriteria("isActive", QCriteriaOperator.EQUALS, true).withCriteria("partnerId", QCriteriaOperator.IS_NOT_BLANK)),
safeCount("edi_agreement", new QQueryFilter()), safeCount("edi_agreement", active),
safeCount("edi_agreement_document", new QQueryFilter()), safeCount("edi_agreement_document", active),
safeCount("edi_transport", new QQueryFilter()), safeCount("edi_transport", active),
safeCount("edi_certificate", new QQueryFilter()), safeCount("edi_guide", new QQueryFilter()), safeCount("edi_code_list", new QQueryFilter()),
safeCount("edi_map", new QQueryFilter()), safeCount("edi_map", new QQueryFilter().withCriteria("status", QCriteriaOperator.EQUALS, "ACTIVE")));
List<Map<String, Object>> rows = new ArrayList<>();
for(EdiSetupStatus.Item item : EdiSetupStatus.items(counts))
{
rows.add(row("Step", item.step(), "Status", item.state().label() + (item.detail().isEmpty() ? "" : ": " + item.detail()), "Next step", item.nextStep().isEmpty() ? "Nothing to do." : item.nextStep()));
}
return (table(List.of("Step", "Status", "Next step"), rows, "edi_partner", new QQueryFilter(), "Open partners"));
}
/*******************************************************************************
** A count that may fail (for example a table the viewer cannot read): null,
** shown as "Unavailable", and logged; never a made-up number.
*******************************************************************************/
static Integer safeCount(String table, QQueryFilter filter)
{
try
{
return (countOf(table, filter));
}
catch(QException | RuntimeException e)
{
LOG.warn("EDI setup count unavailable", e, logPair("table", table));
return (null);
}
}
/*******************************************************************************
** The business face of a stalled inbound flow: when the oldest unprocessed
** inbound document is older than the configured threshold, a warning with its
** age that links to those documents. Hidden when nothing is delayed, or when
** the viewer cannot read the inbound documents.
*******************************************************************************/
private static RenderWidgetOutput delayed(Instant now) throws QException
{
Integer minutes = EdiRuntime.current().config().getInboundDelayedAfterMinutes();
Instant cutoff = now.minus(minutes, ChronoUnit.MINUTES);
List<QRecord> oldest = List.of();
try
{
oldest = query("edi_inbox", delayedInbound(cutoff).withOrderBy(new QFilterOrderBy("receivedAt", true)).withLimit(1));
}
catch(QPermissionDeniedException e)
{
///////////////////////////////////////////////////////////
// omitted, not locked: the viewer cannot open the inbox //
///////////////////////////////////////////////////////////
}
if(oldest.isEmpty())
{
return (new RenderWidgetOutput(new AlertData().withHideWidget(true)));
}
int count = countOf("edi_inbox", delayedInbound(cutoff));
String age = humanize(Duration.between(oldest.getFirst().getValueInstant("receivedAt"), now));
String html = "<strong>Inbound documents are delayed.</strong> " + count + (count == 1 ? " document has" : " documents have")
+ " waited more than " + humanize(Duration.ofMinutes(minutes)) + " to be processed; the oldest has waited " + age + ". "
+ "<a href=\"" + link("edi_inbox", delayedInbound(cutoff)) + "\">View delayed documents</a>";
return (new RenderWidgetOutput(new AlertData(AlertData.AlertType.WARNING, html)));
}
/*******************************************************************************
** Unprocessed inbound documents received before the cutoff.
*******************************************************************************/
static QQueryFilter delayedInbound(Instant cutoff)
{
return (inboxBacklog().withCriteria("receivedAt", QCriteriaOperator.LESS_THAN, cutoff));
}
/*******************************************************************************
** Open exceptions, oldest first, in four narrow columns.
*******************************************************************************/
@@ -312,7 +440,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,159 @@
/*
* 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 java.util.ArrayList;
import java.util.List;
/*******************************************************************************
** The Setup status on the EDI home: each setup step in the order it has to be
** done (partner, identity, agreement, document, transport), whether it is
** done, and the next step. The counts use the same conditions the runtime
** uses to route documents (active rows), so the card never reports "set up"
** for rows the runtime ignores.
*******************************************************************************/
public final class EdiSetupStatus
{
/*******************************************************************************
** DONE: working. TODO: the exchange needs it. BUILT_IN: provided by the
** qbit. OPTIONAL: only some setups need it. UNKNOWN: its count failed (for
** example the viewer cannot read the table), shown as "Unavailable".
*******************************************************************************/
public enum State
{
DONE("Set up"), TODO("Needs setup"), BUILT_IN("Built in"), OPTIONAL("Optional"), UNKNOWN("Unavailable");
private final String label;
/*******************************************************************************
**
*******************************************************************************/
State(String label)
{
this.label = label;
}
/*******************************************************************************
** The label shown on the card.
*******************************************************************************/
public String label()
{
return (label);
}
}
/*******************************************************************************
** One line of the card.
*******************************************************************************/
public record Item(String step, String tableName, State state, String detail, String nextStep)
{
}
/*******************************************************************************
** The row counts the card is built from; null means the count failed and the
** step shows as unavailable. Active counts are rows with isActive true (what
** the runtime uses).
*******************************************************************************/
public record Counts(Integer partners, Integer ownIdentities, Integer partnerIdentities, Integer agreements, Integer activeAgreements, Integer agreementDocuments,
Integer activeAgreementDocuments, Integer transports, Integer activeTransports, Integer certificates, Integer guides, Integer codeLists,
Integer maps, Integer activeMaps)
{
}
/*******************************************************************************
** Utility class.
*******************************************************************************/
private EdiSetupStatus()
{
}
/*******************************************************************************
** The items, in setup order.
*******************************************************************************/
public static List<Item> items(Counts c)
{
List<Item> items = new ArrayList<>();
items.add(item("1. Partner", "edi_partner", c.partners(), c.partners(), "partner", "Add the trading partner with New on Partners."));
items.add(identities(c));
items.add(item("3. Agreement", "edi_agreement", c.agreements(), c.activeAgreements(), "agreement",
"Add an agreement on Agreements: your identity, the partner's identity, the standard and test or production. Make sure Active is on."));
items.add(item("4. Agreement documents", "edi_agreement_document", c.agreementDocuments(), c.activeAgreementDocuments(), "agreement document",
"Add one per transaction set and direction on Agreement Documents: pick the guide, and a map if it should become records. A document type with no row is not processed."));
items.add(item("5. Transport", "edi_transport", c.transports(), c.activeTransports(), "transport",
"Add the AS2, SFTP or HTTPS connection on Transports (secrets as variable names), link it on the agreement, then run Test Connection."));
items.add(c.certificates() == null ? new Item("Certificates", "edi_certificate", State.UNKNOWN, "", "")
: c.certificates() == 0
? new Item("Certificates", "edi_certificate", State.OPTIONAL, "None.", "Needed only for AS2 signing and encryption: add yours and the partner's on Certificates.")
: new Item("Certificates", "edi_certificate", State.DONE, c.certificates() + (c.certificates() == 1 ? " certificate." : " certificates."), ""));
items.add(c.guides() == null || c.codeLists() == null ? new Item("Guides and code lists", "edi_guide", State.UNKNOWN, "", "")
: c.guides() == 0 || c.codeLists() == 0
? new Item("Guides and code lists", "edi_guide", State.TODO, "The built-in guides are not loaded.", "The host must call EdiRuntime.initialize() at startup, after the schema exists.")
: new Item("Guides and code lists", "edi_guide", State.BUILT_IN, c.guides() + " guides and " + c.codeLists() + " code lists.", ""));
items.add(c.maps() == null || c.activeMaps() == null ? new Item("Maps", "edi_map", State.UNKNOWN, "", "")
: c.maps() == 0
? new Item("Maps", "edi_map", State.OPTIONAL, "None.", "Needed to turn documents into records. Run Install sample maps on Maps to start from an example.")
: c.activeMaps() == 0
? new Item("Maps", "edi_map", State.TODO, c.maps() + " maps, none active.", "Point a map at your table, preview it, then run Activate Map.")
: new Item("Maps", "edi_map", State.DONE, c.activeMaps() + " active of " + c.maps() + ".", ""));
return (items);
}
/*******************************************************************************
** Step 2: your own identity (no partner) and the partner's.
*******************************************************************************/
private static Item identities(Counts c)
{
String step = "2. Identities";
if(c.ownIdentities() == null || c.partnerIdentities() == null)
{
return (new Item(step, "edi_identity", State.UNKNOWN, "", ""));
}
if(c.ownIdentities() == 0)
{
return (new Item(step, "edi_identity", State.TODO, "None of yours.", "Add your own ISA or UNB qualifier and ID on Identities, with no partner."));
}
if(c.partnerIdentities() == 0)
{
return (new Item(step, "edi_identity", State.TODO, "Yours is set; no partner has one.", "Add the partner's qualifier and ID on Identities, with the partner selected."));
}
return (new Item(step, "edi_identity", State.DONE, c.ownIdentities() + " of yours, " + c.partnerIdentities() + " for partners.", ""));
}
/*******************************************************************************
** A step that needs at least one active row.
*******************************************************************************/
private static Item item(String step, String table, Integer total, Integer active, String noun, String next)
{
if(total == null || active == null)
{
return (new Item(step, table, State.UNKNOWN, "", ""));
}
if(total == 0)
{
return (new Item(step, table, State.TODO, "None.", next));
}
if(active == 0)
{
return (new Item(step, table, State.TODO, total + " " + noun + (total == 1 ? "" : "s") + ", none active.", "Turn Active on for the one to use."));
}
return (new Item(step, table, State.DONE, active + " active.", ""));
}
}
@@ -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");
@@ -220,7 +220,8 @@ public class InboundProcessor
{
QRecord sender = row("edi_identity", agreement.getValueInteger("partnerIdentityId"));
QRecord receiver = row("edi_identity", agreement.getValueInteger("localIdentityId"));
if(sender != null && receiver != null && Objects.equals(sender.getValueInteger("partnerId"), agreement.getValueInteger("partnerId"))
if(sender != null && receiver != null && !Boolean.FALSE.equals(sender.getValueBoolean("isActive")) && !Boolean.FALSE.equals(receiver.getValueBoolean("isActive"))
&& Objects.equals(sender.getValueInteger("partnerId"), agreement.getValueInteger("partnerId"))
&& receiver.getValue("partnerId") == null && x12.isa().value(5).equals(sender.getValueString("interchangeQualifier"))
&& x12.isa().value(6).trim().equals(sender.getValueString("interchangeId")) && x12.isa().value(7).equals(receiver.getValueString("interchangeQualifier"))
&& x12.isa().value(8).trim().equals(receiver.getValueString("interchangeId")))
@@ -261,6 +262,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 +309,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 +354,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 +384,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 +422,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 +434,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 +449,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"));
@@ -20,7 +20,9 @@
package com.kingsrook.qbits.edi.flow;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kingsrook.qbits.edi.EdiRuntime;
import com.kingsrook.qbits.edi.storage.EdiRecords;
@@ -30,36 +32,80 @@ import com.kingsrook.qqq.backend.core.actions.tables.listeners.RecordChangeListe
import com.kingsrook.qqq.backend.core.actions.tables.listeners.RecordChangeType;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Transactional source listener and explicit enqueue boundary for MANUAL/ESB.
** The listener applies only to tables with an active RECORD_CHANGE definition
** (cached per runtime, see {@link OutboundSourceTables}), and to the definition
** tables themselves, whose changes invalidate that cache.
*******************************************************************************/
public class OutboundEnqueue implements RecordChangeListenerInterface
{
private static final QLogger LOG = QLogger.getLogger(OutboundEnqueue.class);
/*******************************************************************************
** Ignore internal tables and deletes; definitions are read at event time.
** Definition changes (any type), and inserts/updates of configured sources.
*******************************************************************************/
public boolean appliesTo(String table, RecordChangeType type)
{
return (!table.startsWith("edi_") && type != RecordChangeType.DELETE);
EdiRuntime runtime = EdiRuntime.find();
if(runtime == null)
{
return (false);
}
if(OutboundSourceTables.DEFINITION_TABLES.contains(table))
{
return (true);
}
if(table.startsWith("edi_") || type == RecordChangeType.DELETE)
{
return (false);
}
try
{
return (runtime.outboundSources().contains(table, QContext.getQBackendTransaction()));
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////
// if the definitions can't be read, apply - so the event runs its //
// own queries and reports the failure, rather than silently //
// dropping outbound work. //
//////////////////////////////////////////////////////////////////////
LOG.warn("Unable to read EDI outbound source tables", e, logPair("table", table));
return (true);
}
}
/*******************************************************************************
** Use the event transaction, even when the caller did not put it in QContext.
** Definitions are read once per event, not once per record.
*******************************************************************************/
public void onRecordsChanged(RecordChangeEvent event) throws QException
{
if(OutboundSourceTables.DEFINITION_TABLES.contains(event.getTableName()))
{
EdiRuntime.current().outboundSources().invalidate(event.getTransaction());
return;
}
var previous = QContext.getQBackendTransaction();
QContext.setTransaction(event.getTransaction());
try
{
for(QRecord record : event.getRecords())
SystemWrites.call(() ->
{
enqueue(event.getTableName(), record, "RECORD_CHANGE", null);
}
List<QRecord> definitions = definitions(event.getTableName(), OutboundSourceTables.TRIGGER);
for(QRecord record : event.getRecords())
{
enqueue(event.getTableName(), record, null, definitions);
}
return (null);
});
}
finally
{
@@ -72,45 +118,60 @@ public class OutboundEnqueue implements RecordChangeListenerInterface
*******************************************************************************/
public static Integer enqueue(String table, QRecord source, String trigger, String eventId) throws QException
{
return (SystemWrites.call(() ->
return (SystemWrites.call(() -> enqueue(table, source, eventId, definitions(table, trigger))));
}
/*******************************************************************************
** Active outbound documents for the table whose trigger matches (MANUAL
** matches every trigger) and whose agreement is active. Called inside a
** {@link SystemWrites#call}.
*******************************************************************************/
static List<QRecord> definitions(String table, String trigger) throws QException
{
List<QRecord> matching = new ArrayList<>();
for(QRecord document : EdiRecords.query("edi_agreement_document", EdiRecords.equal("sourceTableName", table)
.withCriteria("direction", QCriteriaOperator.EQUALS, "OUTBOUND").withCriteria("isActive", QCriteriaOperator.EQUALS, true)))
{
int count = 0;
var definitions = EdiRecords.query("edi_agreement_document", EdiRecords.equal("sourceTableName", table)
.withCriteria("direction", QCriteriaOperator.EQUALS, "OUTBOUND").withCriteria("isActive", QCriteriaOperator.EQUALS, true));
for(QRecord document : definitions)
if("MANUAL".equals(trigger) || trigger.equals(document.getValueString("outboundTrigger")))
{
if(!"MANUAL".equals(trigger) && !trigger.equals(document.getValueString("outboundTrigger")))
{
continue;
}
QRecord agreement = OutboundWorker.row("edi_agreement", document.getValueInteger("agreementId"));
if(!Boolean.TRUE.equals(agreement.getValueBoolean("isActive")))
{
continue;
}
var metadata = QContext.getQInstance().getTable(table);
if(!metadata.getBackendName().equals(EdiRuntime.current().config().getBackendName()))
{
throw new QException("Outbound source and EDI must share the transactional backend");
}
String filter = document.getValueString("triggerFilter");
if(filter != null && !filter.isBlank() && EdiRecords.query(table, EdiRecords.equal(metadata.getPrimaryKeyField(), source.getValue(metadata.getPrimaryKeyField()))
.withSubFilter(new ObjectMapper().readValue(filter, QQueryFilter.class))).isEmpty())
{
continue;
}
if(eventId != null && !EdiRecords.query("edi_outbound_request", EdiRecords.equal("eventId", eventId)
.withCriteria("agreementDocumentId", QCriteriaOperator.EQUALS, document.getValue("id"))).isEmpty())
{
continue;
}
SystemWrites.insert("edi_outbound_request", new QRecord().withValue("agreementDocumentId", document.getValue("id"))
.withValue("sourceTableName", table).withValue("sourceRecordId", source.getValueString(metadata.getPrimaryKeyField()))
.withValue("eventId", eventId).withValue("status", "PENDING").withValue("attempts", 0)
.withValue("nextAttemptAt", EdiRuntime.current().config().getClock().instant()));
count++;
matching.add(document);
}
return (count);
}));
}
Set<Integer> activeAgreements = OutboundSourceTables.activeAgreementIds(matching);
matching.removeIf(document -> !activeAgreements.contains(document.getValueInteger("agreementId")));
return (matching);
}
/*******************************************************************************
** Enqueue one source record against already-resolved definitions.
*******************************************************************************/
private static int enqueue(String table, QRecord source, String eventId, List<QRecord> definitions) throws Exception
{
int count = 0;
for(QRecord document : definitions)
{
var metadata = QContext.getQInstance().getTable(table);
if(!metadata.getBackendName().equals(EdiRuntime.current().config().getBackendName()))
{
throw new QException("Outbound source and EDI must share the transactional backend");
}
String filter = document.getValueString("triggerFilter");
if(filter != null && !filter.isBlank() && EdiRecords.query(table, EdiRecords.equal(metadata.getPrimaryKeyField(), source.getValue(metadata.getPrimaryKeyField()))
.withSubFilter(new ObjectMapper().readValue(filter, QQueryFilter.class))).isEmpty())
{
continue;
}
if(eventId != null && !EdiRecords.query("edi_outbound_request", EdiRecords.equal("eventId", eventId)
.withCriteria("agreementDocumentId", QCriteriaOperator.EQUALS, document.getValue("id"))).isEmpty())
{
continue;
}
SystemWrites.insert("edi_outbound_request", new QRecord().withValue("agreementDocumentId", document.getValue("id"))
.withValue("sourceTableName", table).withValue("sourceRecordId", source.getValueString(metadata.getPrimaryKeyField()))
.withValue("eventId", eventId).withValue("status", "PENDING").withValue("attempts", 0)
.withValue("nextAttemptAt", EdiRuntime.current().config().getClock().instant()));
count++;
}
return (count);
}
}
@@ -0,0 +1,183 @@
/*
* 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.flow;
import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Per-runtime cache of the source tables that have an active RECORD_CHANGE
** outbound definition on an active agreement. The record-change listener
** consults it so writes to every other table (audits, jobs, alerts...) cost
** nothing. It is loaded from committed data only (never in a writer's
** transaction). A change to an agreement or document invalidates it, and
** again once that change commits (discarding any reload that raced it); until
** then the changing transaction bypasses the cache, so it sees its own
** definitions. A short TTL covers changes made by another process.
*******************************************************************************/
public final class OutboundSourceTables
{
static final String TRIGGER = "RECORD_CHANGE";
static final Set<String> DEFINITION_TABLES = Set.of("edi_agreement", "edi_agreement_document");
private static final Duration TTL = Duration.ofSeconds(60);
private final AtomicLong generation = new AtomicLong();
private final AtomicLong loads = new AtomicLong();
private final Set<QBackendTransaction> changingTransactions = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
private volatile Snapshot snapshot;
/*******************************************************************************
** Loaded table names, with the generation and time they were loaded at.
*******************************************************************************/
private record Snapshot(Set<String> tables, long generation, long loadedAtNanos)
{
}
/*******************************************************************************
** Whether record changes to this table, written in this transaction (may be
** null), can enqueue outbound work.
*******************************************************************************/
public boolean contains(String table, QBackendTransaction transaction) throws QException
{
if(transaction != null && changingTransactions.contains(transaction))
{
return (true);
}
Snapshot current = snapshot;
if(current == null || current.generation() != generation.get() || System.nanoTime() - current.loadedAtNanos() > TTL.toNanos())
{
current = load();
}
return (current.tables().contains(table));
}
/*******************************************************************************
** Drop the cache now, and again once the changing transaction commits.
*******************************************************************************/
public void invalidate(QBackendTransaction transaction)
{
generation.incrementAndGet();
if(transaction != null)
{
changingTransactions.add(transaction);
transaction.addAfterCommitCallback(() ->
{
changingTransactions.remove(transaction);
generation.incrementAndGet();
});
}
}
/*******************************************************************************
** How many times the definitions have been loaded (for tests and diagnostics).
*******************************************************************************/
public long loadCount()
{
return (loads.get());
}
/*******************************************************************************
** Read committed active definitions once. A load that raced an invalidation
** is not kept as current, because it carries the older generation.
*******************************************************************************/
private Snapshot load() throws QException
{
long loadGeneration = generation.get();
Set<String> tables = SystemWrites.call(() ->
{
List<QRecord> documents = committed("edi_agreement_document", new QQueryFilter()
.withCriteria("direction", QCriteriaOperator.EQUALS, "OUTBOUND").withCriteria("isActive", QCriteriaOperator.EQUALS, true)
.withCriteria("outboundTrigger", QCriteriaOperator.EQUALS, TRIGGER));
Set<Integer> agreementIds = agreementIds(documents);
Set<Integer> activeAgreements = new HashSet<>();
if(!agreementIds.isEmpty())
{
committed("edi_agreement", new QQueryFilter().withCriteria("isActive", QCriteriaOperator.EQUALS, true)
.withCriteria("id", QCriteriaOperator.IN, agreementIds)).forEach(agreement -> activeAgreements.add(agreement.getValueInteger("id")));
}
Set<String> result = new HashSet<>();
for(QRecord document : documents)
{
if(document.getValueString("sourceTableName") != null && activeAgreements.contains(document.getValueInteger("agreementId")))
{
result.add(document.getValueString("sourceTableName"));
}
}
return (Set.copyOf(result));
});
loads.incrementAndGet();
Snapshot loaded = new Snapshot(tables, loadGeneration, System.nanoTime());
snapshot = loaded;
return (loaded);
}
/*******************************************************************************
** Query committed rows - deliberately without the caller's transaction.
*******************************************************************************/
private static List<QRecord> committed(String table, QQueryFilter filter) throws QException
{
return (new QueryAction().execute(new QueryInput(table).withFilter(filter)).getRecords());
}
/*******************************************************************************
** The distinct non-null agreement ids the documents reference.
*******************************************************************************/
private static Set<Integer> agreementIds(List<QRecord> documents)
{
Set<Integer> ids = new HashSet<>();
documents.forEach(document -> ids.add(document.getValueInteger("agreementId")));
ids.remove(null);
return (ids);
}
/*******************************************************************************
** Active agreement ids among those the documents reference, in one query in
** the current transaction.
*******************************************************************************/
static Set<Integer> activeAgreementIds(List<QRecord> documents) throws QException
{
Set<Integer> ids = agreementIds(documents);
Set<Integer> active = new HashSet<>();
if(!ids.isEmpty())
{
for(QRecord agreement : EdiRecords.query("edi_agreement", EdiRecords.equal("isActive", true).withCriteria("id", QCriteriaOperator.IN, ids)))
{
active.add(agreement.getValueInteger("id"));
}
}
return (active);
}
}
@@ -87,13 +87,22 @@ public class OutboundWorker
{
return (0);
}
QRecord localIdentity = row("edi_identity", agreement.getValueInteger("localIdentityId"));
QRecord partnerIdentity = row("edi_identity", agreement.getValueInteger("partnerIdentityId"));
if(localIdentity == null || partnerIdentity == null || Boolean.FALSE.equals(localIdentity.getValueBoolean("isActive")) || Boolean.FALSE.equals(partnerIdentity.getValueBoolean("isActive")))
{
///////////////////////////////////////////////////////////////////////////
// an inactive identity stops sending, as it stops routing; requests wait //
///////////////////////////////////////////////////////////////////////////
return (0);
}
var requests = EdiRecords.query("edi_outbound_request", EdiRecords.equal("agreementDocumentId", id).withCriteria("status", QCriteriaOperator.EQUALS, "PENDING").withLimit(100));
if(requests.isEmpty())
{
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 +124,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 +138,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 +181,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 +203,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 +227,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);
}
}
@@ -0,0 +1,157 @@
/*
* 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.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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;
/*******************************************************************************
** The bundled sample maps (synthetic, one per built-in document and version)
** and their insert-only installation. Installed maps are drafts, bound to no
** partner and to no agreement document, so installing one never changes how
** any document is processed; the target table in each is an example name the
** host replaces before activating.
*******************************************************************************/
public final class SampleMaps
{
public static final List<String> RESOURCES = List.of("204-004010", "204-005010", "204-out-004010", "204-out-005010", "214-004010", "214-005010",
"810-004010", "810-005010", "846-004010", "846-005010", "850-004010", "850-005010", "855-004010", "855-005010", "856-004010", "856-005010",
"860-004010", "860-005010", "940-004010", "940-005010", "945-004010", "945-005010", "945-in-004010", "945-in-005010",
"DESADV-D01B", "DESADV-D96A", "INVOIC-D01B", "INVOIC-D96A", "ORDERS-D01B", "ORDERS-D96A", "ORDRSP-D01B", "ORDRSP-D96A");
/*******************************************************************************
** Utility class.
*******************************************************************************/
private SampleMaps()
{
}
/*******************************************************************************
** One bundled sample: the map's values and its rules.
*******************************************************************************/
public static JsonNode read(String resource) throws IOException
{
try(InputStream input = SampleMaps.class.getResourceAsStream("/edi/maps/" + resource + "-sample.json"))
{
if(input == null)
{
throw (new IOException("Unknown sample map " + resource));
}
return (new ObjectMapper().readTree(input));
}
}
/*******************************************************************************
** The bundled samples whose map name does not exist yet (any revision, any
** partner), in bundle order. Reads the map table as the processing user.
*******************************************************************************/
public static List<String> missing() throws QException, IOException
{
Set<String> existing = new HashSet<>();
for(QRecord map : SystemWrites.query("edi_map", new QQueryFilter()))
{
existing.add(map.getValueString("name"));
}
List<String> missing = new ArrayList<>();
for(String resource : RESOURCES)
{
if(!existing.contains(read(resource).path("map").path("name").asText()))
{
missing.add(resource);
}
}
return (missing);
}
/*******************************************************************************
** The map record a sample installs as: a draft, revision 0, no partner,
** bound to the built-in guide for its document and version when present.
*******************************************************************************/
public static QRecord mapRecord(String resource) throws IOException, QException
{
JsonNode map = read(resource).path("map");
QRecord record = new QRecord();
map.fields().forEachRemaining(e ->
{
if(!e.getValue().isNull() && !List.of("status", "revision", "partnerId", "guideId", "parentMapId").contains(e.getKey()))
{
record.withValue(e.getKey(), e.getValue().asText());
}
});
record.withValue("status", "DRAFT").withValue("revision", 0);
Integer guideId = null;
int revision = -1;
for(QRecord guide : SystemWrites.query("edi_guide", new QQueryFilter().withCriteria("transactionSet", QCriteriaOperator.EQUALS, map.path("transactionSet").asText())
.withCriteria("version", QCriteriaOperator.EQUALS, map.path("version").asText()).withCriteria("source", QCriteriaOperator.EQUALS, "BUILT_IN")
.withCriteria("status", QCriteriaOperator.EQUALS, "ACTIVE")))
{
int candidate = guide.getValueInteger("revision") == null ? 0 : guide.getValueInteger("revision");
if(candidate > revision)
{
revision = candidate;
guideId = guide.getValueInteger("id");
}
}
return (record.withValue("guideId", guideId));
}
/*******************************************************************************
** Insert one sample and its ordered rules. Skips (returns null) a sample
** whose map name now exists, so a repeated run adds nothing. It is an
** administrator action; two people running it at the same moment is not
** guarded against (edi_map has no unique name, since revisions share one).
*******************************************************************************/
public static Integer install(String resource) throws Exception
{
JsonNode sample = read(resource);
String name = sample.path("map").path("name").asText();
if(!EdiRecords.query("edi_map", EdiRecords.equal("name", name)).isEmpty())
{
return (null);
}
QRecord map = SystemWrites.insert("edi_map", mapRecord(resource));
for(JsonNode rule : sample.path("rules"))
{
QRecord row = new QRecord();
rule.fields().forEachRemaining(e ->
{
if(!e.getValue().isNull())
{
row.withValue(e.getKey(), e.getValue().isInt() ? (java.io.Serializable) e.getValue().intValue() : e.getValue().isBoolean() ? (java.io.Serializable) e.getValue().booleanValue() : e.getValue().asText());
}
});
SystemWrites.insert("edi_map_rule", row.withValue("mapId", map.getValue("id")));
}
return (map.getValueInteger("id"));
}
}
@@ -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);
}
}
}
@@ -0,0 +1,109 @@
/*
* 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.metadata;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.help.bundles.HelpBundles;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Applies help/qbit-edi.yaml (the QQQ help-bundle format) to the Edi app
** and its edi_* tables: what each table is for and how its rows get created,
** shown on the table screen and when it is empty.
** Fallback for hosts without the helpContent table. Hosts with that table use
** the core bundle registry and startup sync, including HOST and USER overrides.
*******************************************************************************/
public final class EdiHelp
{
public static final String RESOURCE = "/help/qbit-edi.yaml";
public static final String SLOT = "help";
public static final String APP_KEY = "edi";
/*******************************************************************************
** Utility class.
*******************************************************************************/
private EdiHelp()
{
}
/*******************************************************************************
** The bundle's entries by key, validated (fails startup if malformed).
*******************************************************************************/
public static Map<String, JsonNode> entries() throws QException
{
Map<String, JsonNode> entries = new LinkedHashMap<>();
ObjectMapper mapper = new ObjectMapper();
for(var entry : HelpBundles.load("help/qbit-edi.yaml", EdiHelp.class.getClassLoader()).getEntries())
{
entries.put(entry.getKey(), mapper.valueToTree(entry));
}
return (entries);
}
/*******************************************************************************
** Apply the help to the app (by its configured name) and every edi_* table. A
** host's own help on a table is kept (the bundle only fills the slot).
*******************************************************************************/
public static void apply(QInstance qInstance, String appName) throws QException
{
Map<String, JsonNode> entries = entries();
for(QTableMetaData table : qInstance.getTables().values())
{
JsonNode entry = entries.get("table:" + table.getName() + ";slot:" + SLOT);
if(entry != null && (table.getHelpContent() == null || !table.getHelpContent().containsKey(SLOT)))
{
table.withHelpContent(SLOT, content(entry));
}
}
QAppMetaData app = qInstance.getApp(appName);
JsonNode appEntry = entries.get("app:" + APP_KEY + ";slot:" + SLOT);
if(app != null && appEntry != null && (app.getHelpContent() == null || !app.getHelpContent().containsKey(SLOT)))
{
app.withHelpContent(SLOT, content(appEntry));
}
}
/*******************************************************************************
** Preserve the tooltip summary separately from the Markdown long help.
*******************************************************************************/
static QHelpContent content(JsonNode entry)
{
return (new QHelpContent().withSummary(entry.path("summary").asText().strip()).withContentAsMarkdown(entry.path("content").asText().strip()));
}
}
@@ -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,12 +78,17 @@ 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"));
result.add(action("edi_guide", "override", "Create Partner Override", "edi.guide.override"));
result.add(action("edi_guide", "activate", "Activate Guide", "edi.guide.activate"));
result.add(action("edi_map", "activate", "Activate Map", "edi.map.activate"));
result.add(InstallSampleMapsProcess.process());
QProcessMetaData scheduled = new QProcessMetaData().withName("edi_inbox_worker").withLabel("Process Inbox").withIsHidden(true)
.withPermissionRules(permission("edi.admin")).withStep(new QBackendStepMetaData().withName("run").withCode(new QCodeReference(WorkerStep.class)));
if(config.getSchedulerName() != null)
@@ -130,6 +138,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 +158,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 +193,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 +238,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 +322,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 +362,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)
{
@@ -0,0 +1,210 @@
/*
* 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.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.edi.map.SampleMaps;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractExtractStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractLoadStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
/*******************************************************************************
** "Install sample maps": a table action on edi_map that adds the bundled
** synthetic sample maps whose name does not exist yet. Standard streamed ETL:
** Review lists what will be added, Result what was added. Insert-only by map
** name, so existing maps and edits are never touched. Installed maps are
** drafts with no partner and no agreement document, so nothing changes how a
** document is processed until an administrator activates and binds one.
*******************************************************************************/
public final class InstallSampleMapsProcess
{
public static final String NAME = "edi_map_installSamples";
/*******************************************************************************
** Utility class.
*******************************************************************************/
private InstallSampleMapsProcess()
{
}
/*******************************************************************************
** The process: a table-level action (no records need to be selected).
*******************************************************************************/
public static QProcessMetaData process()
{
Map<String, Serializable> values = new LinkedHashMap<>();
values.put(StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE, "edi_map");
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, "edi_map");
values.put(StreamedETLWithFrontendProcess.FIELD_SUPPORTS_FULL_VALIDATION, false);
values.put(StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE,
"These sample maps will be added as drafts, with no partner and no agreement document. Maps that already exist are not changed.");
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(Extract.class, Transform.class, Load.class, values);
process.setName(NAME);
process.setLabel("Install sample maps");
process.setIcon(new QIcon("playlist_add"));
process.setTableName("edi_map");
process.setIsHidden(true);
process.setMinInputRecords(0);
process.setPermissionRules(new QPermissionRules().withPermissionBaseName("edi.admin").withLevel(PermissionLevel.HAS_ACCESS_PERMISSION));
return (process);
}
/*******************************************************************************
** The samples still to install, as the processing user.
*******************************************************************************/
static List<String> missing() throws QException
{
return (SystemWrites.call(SampleMaps::missing));
}
/*******************************************************************************
** A summary line with the standard four wordings.
*******************************************************************************/
private static ProcessSummaryLine line(Status status, String future, String futurePlural, String past, String pastPlural)
{
ProcessSummaryLine line = new ProcessSummaryLine(status, 0, null);
line.setSingularFutureMessage(future);
line.setPluralFutureMessage(futurePlural);
line.setSingularPastMessage(past);
line.setPluralPastMessage(pastPlural);
return (line);
}
/*******************************************************************************
** Extract: the missing samples, computed (no query of selected records).
*******************************************************************************/
public static class Extract extends AbstractExtractStep
{
/*******************************************************************************
** How many samples will be added.
*******************************************************************************/
@Override
public Integer doCount(RunBackendStepInput input) throws QException
{
return (missing().size());
}
/*******************************************************************************
** One record per missing sample.
*******************************************************************************/
@Override
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
for(String resource : missing())
{
QRecord map = SystemWrites.call(() -> SampleMaps.mapRecord(resource));
getRecordPipe().addRecord(map.withValue("sample", resource));
}
}
}
/*******************************************************************************
** Transform: forward the records and count them by standard and direction.
*******************************************************************************/
public static class Transform extends AbstractTransformStep implements ProcessSummaryProviderInterface
{
private final Map<String, ProcessSummaryLine> byKind = new LinkedHashMap<>();
/*******************************************************************************
** Count by standard and direction.
*******************************************************************************/
@Override
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
for(QRecord record : input.getRecords())
{
String kind = record.getValueString("standard") + " " + record.getValueString("direction").toLowerCase();
byKind.computeIfAbsent(kind, k -> line(Status.OK, k + " sample map will be added as a draft", k + " sample maps will be added as drafts",
k + " sample map was added as a draft", k + " sample maps were added as drafts")).incrementCount();
output.getRecords().add(record);
}
}
/*******************************************************************************
** Standard Review summary.
*******************************************************************************/
@Override
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen)
{
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
byKind.values().forEach(line -> line.addSelfToListIfAnyCount(lines));
if(lines.isEmpty())
{
lines.add(new ProcessSummaryLine(Status.INFO, null, "Every sample map is already installed."));
}
return (lines);
}
}
/*******************************************************************************
** Load: insert each map and its rules as the processing user.
*******************************************************************************/
public static class Load extends AbstractLoadStep implements ProcessSummaryProviderInterface
{
private final ProcessSummaryLine added = line(Status.OK, "map will be added", "maps will be added", "map was added as a draft", "maps were added as drafts");
/*******************************************************************************
** One transaction per map, so a map never lands without its rules.
*******************************************************************************/
@Override
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
for(QRecord record : input.getRecords())
{
Integer id = SystemWrites.call(() -> SystemWrites.transaction(() -> SampleMaps.install(record.getValueString("sample"))));
if(id != null)
{
added.incrementCountAndAddPrimaryKey(id);
output.getRecords().add(new QRecord().withValue("id", id));
}
}
}
/*******************************************************************************
** Standard Result summary.
*******************************************************************************/
@Override
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen)
{
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
added.addSelfToListIfAnyCount(lines);
return (lines);
}
}
}
@@ -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)"
}
]
}
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more