feat: phase e - HIPAA healthcare documents, SNIP 3-5, PHI handling, canary tests #11

Merged
KofTwentyTwo merged 1 commits from feature/phase-e-healthcare into develop 2026-10-14 13:46:36 +00:00
46 changed files with 11992 additions and 65 deletions

No files matched your search

+8 -1
View File
@@ -6,8 +6,11 @@ Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic
### Added
- 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.
@@ -40,4 +43,8 @@ Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic
### Fixed
- 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).
+46
View File
@@ -256,6 +256,52 @@ 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.5's
`EdiClassifications` stand-in marks PHI/PII/CONFIDENTIAL and denies PHI AI exposure, but does not
mask values. 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.5 also logs query criteria verbatim. 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. Replace both
bridges with core secured-data support at 4.1.0-RC.6. 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, DEBUG logs, all EDI
query exports and all home/child widgets. FULL exports exempt only PHI-classified columns;
RC.5 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.5 tests.
## Build and verification
```bash
+16 -1
View File
@@ -44,7 +44,8 @@ The in-memory gateway accepts every send, and the rejected documents produce rej
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 has the retail outbound documents; the logistics and healthcare
scenarios add their own outbound configurations. 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 +59,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.
+45 -8
View File
@@ -750,6 +750,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
@@ -778,13 +785,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 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.5 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
@@ -955,7 +966,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 +988,29 @@ 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)
- Core RC.5 logs full query criteria, including PHI upsert keys. `PhiLogScope` is a scoped,
marked bridge to suppress synchronous diagnostic logging during PHI map/apply and previews;
it leaves unrelated threads and persisted value-free events intact. Remove with the core
classification/logging bridge at RC.6. Map samples use record-level audits on RC.5 because their
expected-result column can contain PHI. Host asynchronous code, target audits and queries outside
these boundaries still need host secured-data protection. Raw downloads use RC.5's existing
session-bound storage grant and an `edi_event` audit rather than a not-yet-available access log.
- 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.5
test classpath. FULL and actual HIDDEN-field-lock scans run now; no simulated masking is used.
@@ -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();
@@ -53,7 +53,7 @@ public final class EdiRuntime
private static final Map<QInstance, EdiRuntime> INSTANCES = Collections.synchronizedMap(new WeakHashMap<>());
private final X12EnvelopeReader reader;
private final X12Writer writer = new X12Writer();
private final GuideValidator validator = new GuideValidator();
private final GuideValidator validator;
private final BuiltInGuideLoader guides = new BuiltInGuideLoader();
private final com.kingsrook.qbits.edi.storage.PayloadStore payloads;
private final EdiQBitConfig config;
@@ -71,6 +71,7 @@ public final class EdiRuntime
private EdiRuntime(QInstance instance, EdiQBitConfig config)
{
this.config = config;
validator = new GuideValidator(config);
reader = new X12EnvelopeReader(config.getMaxSegmentsPerTransaction());
payloads = "OBJECT_STORAGE".equals(config.getPayloadStore())
? new com.kingsrook.qbits.edi.storage.ObjectStoragePayloadStore(new PayloadCrypto(config.getSecretsResolver()), config.getPayloadBucketAlias(), config.getClock())
@@ -57,7 +57,7 @@ final class FunctionalAckBuilder
for(X12TransactionSet transaction : group.transactions())
{
List<ValidationFinding> errors = findings.getOrDefault(transaction.st().value(2), List.of());
boolean rejected = !transaction.findings().isEmpty() || errors.stream().anyMatch(f -> "ERROR".equals(f.severity()));
boolean rejected = !transaction.findings().isEmpty() || errors.stream().anyMatch(f -> "ERROR".equals(f.severity()) && f.snipLevel() <= 2);
String status = rejected ? "R" : errors.isEmpty() ? "A" : "E";
if(!rejected)
{
@@ -0,0 +1,47 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi.app;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
/*******************************************************************************
** Explicitly enforce table and field permissions inside the widget's action scope.
** RC.5 treats nested DEFAULT queries as trusted, so the generic renderer alone
** does not remove fields denied by a FieldSecurityLock.
*******************************************************************************/
public class EdiChildRecordListRenderer extends ChildRecordListRenderer
{
/*******************************************************************************
** Use the standard typed child list with an enforced query at its public boundary.
*******************************************************************************/
@Override
protected QueryOutput executeQuery(QueryInput input) throws QException
{
input.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
return (super.executeQuery(input));
}
}
@@ -312,7 +312,7 @@ public class EdiHome extends AbstractWidgetRenderer
List<Map<String, Object>> rows = new ArrayList<>();
rows.add(row("Item", "Payload storage", "Status", "DATABASE".equals(store) ? "Database, encrypted (demos and tests)" : "Object storage, encrypted"));
rows.add(row("Item", "Secured data", "Status", secured ? "Installed" : "Not installed: PHI fields are not masked"));
rows.add(row("Item", "External code sets", "Status", "Not configured"));
rows.add(row("Item", "External code sets", "Status", EdiRuntime.current().config().getExternalCodeSetProvider() == null ? "Format only: no provider configured" : "Provider configured"));
rows.add(row("Item", "Inbound backlog", "Status", countOf("edi_inbox", inboxBacklog())
+ (oldestIn.isEmpty() ? "" : ", oldest " + humanize(Duration.between(oldestIn.getFirst().getValueInstant("receivedAt"), now)))));
rows.add(row("Item", "Outbound queue", "Status", countOf("edi_outbound_message", outboundQueue())
@@ -185,7 +185,7 @@ public class InboundProcessor
if(EdiRecords.query("edi_outbound_message", EdiRecords.equal("ackForGroupId", groupRow.getValue("id"))).isEmpty())
{
AckLevel level = AckLevel.valueOf(Objects.requireNonNullElse(config.getValueString("ackLevel"), "ERRORS_ONLY"));
boolean hipaa = Boolean.TRUE.equals(config.getValueBoolean("containsPhi")) && group.gs().value(8).startsWith("005010X");
boolean hipaa = group.gs().value(8).startsWith("005010X") || "999".equals(config.getValueString("ackExpectation"));
var ack = hipaa ? runtime.ack999().build(group, findings, level, "0001") : runtime.ack997().build(group, findings, level, "0001");
queue(AckEnvelope.wrap(x12, group, ack, control(agreement, "ISA"), control(agreement, "GS"), LocalDateTime.now(runtime.config().getClock())),
agreement, id, groupRow.getValueInteger("id"), hipaa ? "999" : "997");
@@ -261,6 +261,7 @@ public class InboundProcessor
.withValue("receiverQualifier", x12.isa().value(7)).withValue("receiverId", x12.isa().value(8).trim()).withValue("controlNumber", x12.isa().value(13))
.withValue("interchangeVersion", x12.isa().value(12)).withValue("usageIndicator", x12.isa().value(15)).withValue("groupCount", x12.groups().size())
.withValue("payloadRef", inbox.getValue("payloadRef")).withValue("payloadSha256", inbox.getValue("payloadSha256")).withValue("sizeBytes", inbox.getValue("sizeBytes"))
.withValue("containsPhi", x12.groups().stream().anyMatch(g -> g.gs().value(8).startsWith("005010X")))
.withValue("status", duplicate ? "DUPLICATE" : x12.findings().isEmpty() ? "ACCEPTED" : "REJECTED"));
EdiJournal.event(inbox.getValueInteger("id"), row.getValueInteger("id"), null, "PARSED");
return (row);
@@ -307,7 +308,7 @@ 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")));
}
@@ -352,7 +353,7 @@ public class InboundProcessor
List<ValidationFinding> findings = new ArrayList<>();
if(document != null && document.getValueInteger("guideId") != null)
{
findings.addAll(runtime.validator().validate(tx, new GuideRepository().load(document.getValueInteger("guideId")), Boolean.TRUE.equals(document.getValueBoolean("containsPhi"))));
findings.addAll(runtime.validator().validate(tx, new GuideRepository().load(document.getValueInteger("guideId")), Boolean.TRUE.equals(document.getValueBoolean("containsPhi")) || group.gs().value(8).startsWith("005010X")));
int snip = Objects.requireNonNullElse(document.getValueInteger("snipLevel"), 2);
findings.removeIf(f -> f.snipLevel() > snip);
}
@@ -432,7 +433,7 @@ public class InboundProcessor
return;
}
String[] stage = {"MAPPING"};
try
try(var logging = com.kingsrook.qbits.edi.security.PhiLogScope.open(Boolean.TRUE.equals(tx.getValueBoolean("containsPhi"))))
{
InboxWorker.fenced(inbox, lease, () ->
{
@@ -447,7 +448,7 @@ public class InboundProcessor
stage[0] = "APPLY";
QRecord target = InboundApplier.asPartner(runtime.config().getServiceSessionSupplier(), agreement.getValueInteger("partnerId"), () -> new InboundApplier().apply(map, mapped.record()));
String primaryKey = com.kingsrook.qqq.backend.core.context.QContext.getQInstance().getTable(map.getValueString("tableName")).getPrimaryKeyField();
boolean phi = Boolean.TRUE.equals(document.getValueBoolean("containsPhi"));
boolean phi = Boolean.TRUE.equals(tx.getValueBoolean("containsPhi"));
SystemWrites.update("edi_transaction", new QRecord().withValue("id", tx.getValue("id")).withValue("status", "APPLIED")
.withValue("mapId", map.getValue("id")).withValue("mapRevision", map.getValue("revision")).withValue("businessKeyType", map.getValue("businessKeyType"))
.withValue(phi ? "phiBusinessKey" : "businessKey", mapped.businessKey()).withValue("secondaryKey", phi ? null : mapped.secondaryKey())
@@ -115,7 +115,7 @@ 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"), () ->
{
@@ -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", "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);
}
}
@@ -59,8 +59,10 @@ public class GuideRepository
for(QRecord element : rows("edi_guide_element", "guideSegmentId", segmentId))
{
List<String> codes = null;
QRecord codeList = null;
if(element.getValueInteger("codeListId") != null)
{
codeList = rows("edi_code_list", "id", element.getValueInteger("codeListId")).getFirst();
codes = new ArrayList<>();
for(QRecord code : rows("edi_code_list_value", "codeListId", element.getValueInteger("codeListId")))
{
@@ -72,7 +74,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 +109,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);
}
@@ -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)
{
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));
}
List<Node> roots = tree(guide);
consume(roots, state, false);
while(state.index < state.segments.size())
{
@@ -93,11 +93,16 @@ public class GuideValidator
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));
}
@@ -107,27 +112,64 @@ public class GuideValidator
public Map<String, List<List<X12Segment>>> loops(X12TransactionSet transaction, Guide guide)
{
State state = new State(transaction.segments(), true);
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)
{
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));
@@ -235,7 +277,7 @@ public class GuideValidator
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.
*******************************************************************************/
@@ -392,6 +475,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.x12.X12Segment;
import com.kingsrook.qbits.edi.x12.X12TransactionSet;
/*******************************************************************************
** 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(X12TransactionSet 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));
}
}
@@ -51,7 +51,7 @@ public class MapPreview
.withCriteria("containsPhi", com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS, true)).isEmpty());
var guide = new GuideRepository().load(map.getValueInteger("guideId"));
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
try(InputStream stream = runtime.payloads().open(new PayloadRef(sample.getValueString("payloadRef"), null, 0)))
try(var logging = com.kingsrook.qbits.edi.security.PhiLogScope.open(phi); InputStream stream = runtime.payloads().open(new PayloadRef(sample.getValueString("payloadRef"), null, 0)))
{
var interchanges = runtime.reader().read(stream);
lines.add(new ProcessSummaryLine(Status.INFO, interchanges.size(), "interchanges parsed"));
@@ -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));
@@ -232,7 +232,7 @@ 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);
if(List.of("edi_payload", "edi_certificate", "edi_inbox").contains(name))
{
@@ -305,7 +305,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 +422,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());
}
/*******************************************************************************
@@ -75,6 +75,10 @@ public final class EdiProcesses
{
result.add(action(table, "preview", "Preview Map", "edi.map.preview"));
}
for(String table : List.of("edi_inbox", "edi_interchange"))
{
result.add(action(table, "download", "Download Raw Document", "edi.raw.download").withMaxInputRecords(1));
}
result.add(action("edi_exception", "assign", "Assign", "edi.exception.assign"));
result.add(action("edi_exception", "ignore", "Ignore", "edi.exception.ignore"));
result.add(action("edi_exception", "resolve", "Mark Resolved", "edi.exception.resolve"));
@@ -148,6 +152,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 +187,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 +232,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();
@@ -316,7 +344,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,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#1142, removeBy=4.1.0-RC.6).
** RC.5 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.5 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.5 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());
}
}
}
@@ -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)));
}));
}
}
@@ -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,239 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi;
import java.io.ByteArrayInputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qbits.edi.x12.X12EnvelopeReader;
import com.kingsrook.qbits.edi.x12.X12Interchange;
import com.kingsrook.qbits.edi.x12.X12Segment;
import com.kingsrook.qbits.edi.x12.X12Separators;
import com.kingsrook.qbits.edi.x12.X12TransactionSet;
import com.kingsrook.qbits.edi.x12.X12Writer;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
/*******************************************************************************
** Seeded synthetic healthcare fixtures and host tables; never used in main code.
** NPI 1234567893 is a documented checksum test value, not a provider identity.
*******************************************************************************/
public final class HealthcareSupport
{
/*******************************************************************************
** Test-only schema with classified target fields and actual QQQ associations.
*******************************************************************************/
public static void tables() throws Exception
{
table("sample_claim", "claimNumber", "memberId", "patientName", "diagnosis", "totalCharge:DECIMAL");
table("sample_claim_line", "parentId:INTEGER", "lineNumber:INTEGER", "procedureCode", "charge:DECIMAL");
table("sample_remittance", "trace", "payment:DECIMAL");
table("sample_remittance_claim", "parentId:INTEGER", "claimNumber", "patientName", "memberId", "charge:DECIMAL", "paid:DECIMAL");
table("sample_eligibility_request", "trace", "memberId", "patientName");
table("sample_eligibility_response", "trace", "memberId", "patientName", "benefit", "serviceType");
table("sample_claim_status", "trace", "memberId", "patientName", "claimNumber", "category", "statusCode");
table("sample_enrollment", "reference");
table("sample_enrollment_member", "parentId:INTEGER", "memberId", "patientName", "birthDate:DATE", "coverage");
association("sample_claim", "sample_claim_line", "lines");
association("sample_remittance", "sample_remittance_claim", "claims");
association("sample_enrollment", "sample_enrollment_member", "members");
var instance = QContext.getQInstance();
EdiRuntime.current().config().withServiceSessionSupplier(() -> new QSystemUserSession(instance));
}
/*******************************************************************************
** Define one table and its H2 schema; fields are STRING unless suffixed with a type.
*******************************************************************************/
private static void table(String name, String... fields) throws Exception
{
var instance = QContext.getQInstance();
String backend = EdiRuntime.current().config().getBackendName();
var table = new QTableMetaData().withName(name).withLabel(name).withBackendName(backend).withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withBackendName("id"));
for(String field : fields)
{
String[] parts = field.split(":");
table.withField(new QFieldMetaData(parts[0], parts.length > 1 ? QFieldType.valueOf(parts[1]) : QFieldType.STRING).withBackendName(QInstanceEnricher.inferBackendName(parts[0]))
.withSupplementalMetaData(new com.kingsrook.qbits.edi.security.EdiClassifications(parts[0].equals("parentId") ? "CONFIDENTIAL" : "PHI")));
}
instance.addTable(table);
try(Connection connection = ConnectionManager.getConnection((RDBMSBackendMetaData) instance.getBackend(backend)); var statement = connection.createStatement())
{
statement.execute(H2Support.ddl(table, false));
}
}
/*******************************************************************************
** Join-backed child records, as the retail samples do.
*******************************************************************************/
private static void association(String parent, String child, String name)
{
var instance = QContext.getQInstance();
String join = parent + "_" + name;
instance.getTable(parent).withAssociation(new Association().withName(name).withAssociatedTableName(child).withJoinName(join));
instance.addJoin(new QJoinMetaData().withName(join).withType(JoinType.ONE_TO_MANY).withLeftTable(parent).withRightTable(child).withJoinOn(new JoinOn("id", "parentId")));
}
/*******************************************************************************
** Version and group identifiers for the eight original baselines.
*******************************************************************************/
public static String version(String key)
{
return (switch(key)
{
case "837P" -> "005010X222A1";
case "837I" -> "005010X223A2";
case "835" -> "005010X221A1";
case "270", "271" -> "005010X279A1";
case "276", "277" -> "005010X212";
default -> "005010X220A1";
});
}
/*******************************************************************************
** Functional identifiers are routing metadata, never patient information.
*******************************************************************************/
public static String group(String key)
{
return (switch(key)
{
case "837P", "837I" -> "HC";
case "835" -> "HP";
case "270" -> "HS";
case "271" -> "HB";
case "276" -> "HR";
case "277" -> "HN";
default -> "BE";
});
}
/*******************************************************************************
** Reproducible member and claim references; no production names or identifiers.
*******************************************************************************/
public static String body(String key, long seed)
{
var random = new java.util.Random(seed);
String member = String.format("SYN%06d", random.nextInt(1000000));
String claim = String.format("SYNCLM%06d", random.nextInt(1000000));
if(key.startsWith("837"))
{
boolean inpatient = key.equals("837I");
return ("BHT*0019*00*SYNBATCH*20261015*1200*CH~"
+ "NM1*41*2*Synthetic Submitter*****46*SYN~NM1*40*2*Synthetic Receiver*****46*SYN~"
+ "HL*1**20*1~NM1*85*2*Synthetic Practice*****XX*1234567893~N3*1 Synthetic Way~N4*Testville*IL*60000~"
+ "HL*2*1*22*0~SBR*P*18*******CI~NM1*IL*1*Testpatient Alpha*****MI*" + member + "~DMG*D8*19800101*U~NM1*PR*2*Synthetic Plan*****PI*SYNPLAN~"
+ "CLM*" + claim + "*150***" + (inpatient ? "11:A:1" : "11:B:1") + "~"
+ (inpatient ? "DTP*434*RD8*20261014-20261015~" : "") + "HI*ABK:A123~"
+ "LX*1~" + (inpatient ? "SV2*0300*HC:S0001*100*UN*1~" : "SV1*HC:S0001*100*UN*1~") + "DTP*472*D8*20261015~"
+ "LX*2~" + (inpatient ? "SV2*0300*HC:S0002*50*UN*1~" : "SV1*HC:S0002*50*UN*1~") + "DTP*472*D8*20261015~");
}
if(key.equals("835"))
{
return ("BPR*I*115*C*CHK************20261015~TRN*1*SYNTRACE" + seed + "~N1*PR*Synthetic Plan~N1*PE*Synthetic Practice~"
+ "LX*1~CLP*" + claim + "*1*150*120*30~CAS*PR*1*10~NM1*QC*1*Testpatient Alpha~NM1*IL*1*Testpatient Alpha*****MI*" + member + "~"
+ "SVC*HC:S0001*100*80~DTM*472*20261015~CAS*CO*45*20~SVC*HC:S0002*50*40~DTM*472*20261015~"
+ "PLB*1234567893*20261231*WO:SYNADJ*5~");
}
if(key.equals("834"))
{
return ("BGN*00*SYNENROLL" + seed + "*20261015*1200~N1*P5*Synthetic Sponsor~N1*IN*Synthetic Plan~"
+ "INS*Y*18*021~REF*0F*" + member + "~NM1*IL*1*Testpatient Alpha~DMG*D8*19800101*U~HD*021**HLT~DTP*348*D8*20261015~");
}
String head = "BHT*0022*13*SYNBATCH*20261015*1200~HL*1**20*1~NM1*PR*2*Synthetic Plan*****PI*SYNPLAN~"
+ "HL*2*1*21*1~NM1*1P*2*Synthetic Practice*****XX*1234567893~HL*3*2*22*0~";
return (head + (key.equals("270") || key.equals("271") ? "TRN*1*SYNTRACE" + seed + "~" : "")
+ "NM1*IL*1*Testpatient Alpha*****MI*" + member + "~"
+ (key.equals("270") ? "EQ*30~" : key.equals("271") ? "EB*1**30~" : "TRN*1*SYNTRACE" + seed + "~"
+ (key.equals("277") ? "STC*A1:19*20261015~" : "") + "REF*EJ*" + claim + "~"));
}
/*******************************************************************************
** Wrap raw synthetic segments using real ISA widths and counted ST/SE boundaries.
*******************************************************************************/
public static String interchange(String key, int control, String body)
{
String version = version(key);
String set = key.startsWith("837") ? "837" : key;
String isa = new X12Writer().write(List.of(X12Segment.of("ISA", 0, "00", "", "00", "", "ZZ", "CEDAR", "ZZ", "QQQTEST", "261015", "1200", "^", "00501", String.format("%09d", control), "0", "T", ":")), X12Separators.DEFAULT, "");
return (isa + "GS*" + group(key) + "*CEDAR*QQQTEST*20261015*1200*" + control + "*X*" + version + "~ST*" + set + "*0001*" + version + "~"
+ body + "SE*" + (body.split("~").length + 2) + "*0001~GE*1*" + control + "~IEA*1*" + String.format("%09d", control) + "~");
}
/*******************************************************************************
** Seed the healthcare partner and sample agreement documents as normal records.
*******************************************************************************/
public static QRecord configure() throws Exception
{
return (SystemWrites.call(() ->
{
QRecord partner = SystemWrites.insert("edi_partner", new QRecord().withValue("name", "Cedar Health Clearinghouse").withValue("code", "CEDAR")
.withValue("status", "ACTIVE").withValue("containsPhi", true).withValue("contactEmail", "cedar@example.invalid"));
var locals = EdiRecords.query("edi_identity", EdiRecords.equal("interchangeId", "QQQTEST"));
QRecord local = locals.isEmpty() ? SystemWrites.insert("edi_identity", new QRecord().withValue("standard", "X12").withValue("interchangeQualifier", "ZZ").withValue("interchangeId", "QQQTEST").withValue("applicationCode", "QQQTEST")) : locals.getFirst();
QRecord remote = SystemWrites.insert("edi_identity", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("standard", "X12").withValue("interchangeQualifier", "ZZ").withValue("interchangeId", "CEDAR").withValue("applicationCode", "CEDAR"));
QRecord transport = SystemWrites.insert("edi_transport", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("name", "cedar-gateway").withValue("type", "GATEWAY")
.withValue("direction", "BOTH").withValue("gatewayEndpointName", "cedar-outbox").withValue("isActive", true));
QRecord agreement = SystemWrites.insert("edi_agreement", new QRecord().withValue("partnerId", partner.getValue("id")).withValue("localIdentityId", local.getValue("id")).withValue("partnerIdentityId", remote.getValue("id"))
.withValue("standard", "X12").withValue("usageIndicator", "T").withValue("applyTestDocuments", true).withValue("isActive", true).withValue("interchangeVersion", "00501")
.withValue("inboundTransportId", transport.getValue("id")).withValue("outboundTransportId", transport.getValue("id")));
for(String key : List.of("837P", "837I", "835", "270", "271", "276", "277", "834"))
{
QRecord map = LogisticsSupport.installMap(key, version(key));
SystemWrites.insert("edi_agreement_document", new QRecord().withValue("agreementId", agreement.getValue("id")).withValue("direction", map.getValue("direction"))
.withValue("transactionSet", map.getValue("transactionSet")).withValue("version", version(key)).withValue("functionalIdCode", group(key))
.withValue("guideId", map.getValue("guideId")).withValue("mapId", map.getValue("id")).withValue("ackExpectation", "999").withValue("ackLevel", "ALL_TRANSACTIONS")
.withValue("sourceTableName", map.getValueString("direction").equals("OUTBOUND") ? map.getValue("tableName") : null).withValue("isActive", true));
}
return (agreement);
}));
}
/*******************************************************************************
** Normal durable intake and parsing, shared with the demo.
*******************************************************************************/
public static void receive(String key, int control, String body) throws Exception
{
new com.kingsrook.qbits.edi.flow.InboundSink(EdiRuntime.current().payloads()).receive(new com.kingsrook.qbits.edi.flow.InboundPayload("GATEWAY", "health-" + control,
"synthetic-healthcare.edi", "127.0.0.1", new ByteArrayInputStream(interchange(key, control, body).getBytes(StandardCharsets.US_ASCII))));
new com.kingsrook.qbits.edi.flow.InboxWorker().runOnce();
}
}
@@ -0,0 +1,279 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.edi.ack.AckLevel;
import com.kingsrook.qbits.edi.ack.Ack999Builder;
import com.kingsrook.qbits.edi.guide.BuiltInGuideLoader;
import com.kingsrook.qbits.edi.guide.GuideValidator;
import com.kingsrook.qbits.edi.guide.HealthcareValidation;
import com.kingsrook.qbits.edi.guide.ValidationFinding;
import com.kingsrook.qbits.edi.map.InboundMapper;
import com.kingsrook.qbits.edi.map.OutboundMapper;
import com.kingsrook.qbits.edi.map.TransformRegistry;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qbits.edi.x12.X12Separators;
import com.kingsrook.qbits.edi.x12.X12Writer;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Original healthcare baselines, balances, situational failures and classified flows.
*******************************************************************************/
class HealthcareTest
{
/*******************************************************************************
** Restore the calling thread after database tests.
*******************************************************************************/
@AfterEach
void clear()
{
QContext.clear();
}
/*******************************************************************************
** Validate a synthetic body including its real envelopes and transaction controls.
*******************************************************************************/
private static List<ValidationFinding> validate(String key, String body) throws Exception
{
var tx = LogisticsSupport.read(HealthcareSupport.interchange(key, 1, body)).groups().getFirst().transactions().getFirst();
assertThat(tx.findings()).isEmpty();
return (new GuideValidator().validate(tx, new BuiltInGuideLoader().resource(key + "-" + HealthcareSupport.version(key)), true));
}
/*******************************************************************************
** All eight baseline guides accept their seeded fixtures, including format checks.
*******************************************************************************/
@ParameterizedTest
@ValueSource(strings = {"837P", "837I", "835", "270", "271", "276", "277", "834"})
void baseline(String key) throws Exception
{
assertThat(validate(key, HealthcareSupport.body(key, 123))).isEmpty();
}
/*******************************************************************************
** Every inbound map writes the synthetic identifiers and associated records.
*******************************************************************************/
@ParameterizedTest
@ValueSource(strings = {"837P", "837I", "835", "271", "277", "834"})
void inboundMap(String key) throws Exception
{
var tx = LogisticsSupport.read(HealthcareSupport.interchange(key, 1, HealthcareSupport.body(key, 123))).groups().getFirst().transactions().getFirst();
List<QRecord> rules = new ArrayList<>();
var map = LogisticsSupport.definition(key, HealthcareSupport.version(key), rules);
var result = new InboundMapper(new TransformRegistry()).map(tx, new BuiltInGuideLoader().resource(key + "-" + HealthcareSupport.version(key)), map, rules);
assertThat(result.businessKey()).startsWith("SYN");
if(key.startsWith("837"))
{
assertThat(result.record().getValueString("patientName")).isEqualTo("Testpatient Alpha");
assertThat(result.record().getValueBigDecimal("totalCharge")).isEqualByComparingTo("150");
assertThat(result.record().getAssociatedRecords().get("lines")).hasSize(2);
assertThat(result.record().getAssociatedRecords().get("lines").getLast().getValueBigDecimal("charge")).isEqualByComparingTo("50");
}
if(key.equals("835"))
{
assertThat(result.record().getAssociatedRecords().get("claims")).hasSize(1);
}
if(key.equals("834"))
{
assertThat(result.record().getAssociatedRecords().get("members")).hasSize(1);
}
}
/*******************************************************************************
** Missing situational data produces SNIP 4 findings, never submitted values.
*******************************************************************************/
@Test
void situationalFailures() throws Exception
{
String body = HealthcareSupport.body("837P", 123);
String dependent = body.replace("SBR*P*18", "SBR*P*19").replace("~CLM*", "~HL*3*2*23*0~PAT*19~NM1*QC*1*Testpatient Beta~CLM*");
assertThat(validate("837P", dependent)).isEmpty();
List<QRecord> dependentRules = new ArrayList<>();
var fixture = LogisticsSupport.definition("837P", "005010X222A1", dependentRules);
var dependentTx = LogisticsSupport.read(HealthcareSupport.interchange("837P", 1, dependent)).groups().getFirst().transactions().getFirst();
var dependentMap = new InboundMapper(new TransformRegistry()).map(dependentTx, new BuiltInGuideLoader().resource("837P-005010X222A1"), fixture, dependentRules);
assertThat(dependentMap.record().getValueString("patientName")).isEqualTo("Testpatient Beta");
for(String invalid : List.of(body.replace("SBR*P*18", "SBR*P*19"), dependent.replace("SBR*P*19", "SBR*P*18"),
dependent.replace("PAT*19~", ""), body.replace("HI*ABK:A123~", ""), body.replaceFirst("DTP\\*472\\*D8\\*20261015~", "")))
{
assertThat(validate("837P", invalid)).anyMatch(f -> f.snipLevel() == 4);
}
assertThat(validate("837I", HealthcareSupport.body("837I", 123).replace("DTP*434*RD8*20261014-20261015~", ""))).anyMatch(f -> f.snipLevel() == 4);
assertThat(validate("834", HealthcareSupport.body("834", 123).replace("DMG*D8*19800101*U~", ""))).anyMatch(f -> f.snipLevel() == 4);
String claimDate = body.replace("HI*", "DTP*472*D8*20261015~HI*").replace("~DTP*472*D8*20261015~LX", "~LX");
assertThat(validate("837P", claimDate)).isEmpty();
}
/*******************************************************************************
** Exact decimal balancing and implementation acknowledgement text with no values.
*******************************************************************************/
@Test
void balanceAndExact999() throws Exception
{
String body = HealthcareSupport.body("837P", 123).replace("*150***", "*150.01***");
var findings = validate("837P", body);
assertThat(findings).singleElement().satisfies(f -> assertThat(f.snipLevel()).isEqualTo(3));
var group = LogisticsSupport.read(HealthcareSupport.interchange("837P", 1, body)).groups().getFirst();
var ack = new Ack999Builder().build(group, Map.of("0001", findings), AckLevel.ALL_TRANSACTIONS, "0002");
assertThat(new X12Writer().write(ack.segments(), X12Separators.DEFAULT, "")).isEqualTo(
"ST*999*0002*005010X231A1~AK1*HC*1*005010X222A1~AK2*837*0001*005010X222A1~IK3*CLM*14*2300*8~IK4*2**7~CTX*SITUATIONAL TRIGGER*CLM*14*2300*2~IK5*E~AK9*E*1*1*1~SE*9*0002~");
assertThat(new GuideValidator().validate(ack, new BuiltInGuideLoader().resource("999-005010X231A1"), false)).isEmpty();
assertThat(validate("837I", HealthcareSupport.body("837I", 123).replace("*150***", "*149.99***"))).anyMatch(f -> f.snipLevel() == 3);
String remit = HealthcareSupport.body("835", 123);
assertThat(validate("835", remit.replace("BPR*I*115", "BPR*I*115.01"))).anyMatch(f -> f.snipLevel() == 3 && f.segmentId().equals("BPR"));
assertThat(validate("835", remit.replace("CAS*CO*45*20", "CAS*CO*45*19.99"))).anyMatch(f -> f.snipLevel() == 3 && f.segmentId().equals("CLP"));
assertThat(validate("835", remit.replace("BPR*I*115", "BPR*I*125").replace("WO:SYNADJ*5", "WO:SYNADJ*-5"))).isEmpty();
assertThat(validate("835", remit.replace("CAS*CO*45*20", "CAS*CO*45*10*1*94*10"))).isEmpty();
}
/*******************************************************************************
** Licensed membership is optional, format checks and NPI checksum are deterministic.
*******************************************************************************/
@Test
void formatsAndExternalCodes() throws Exception
{
assertThat(HealthcareValidation.validNpi("1234567893")).isTrue();
assertThat(HealthcareValidation.validNpi("1234567890")).isFalse();
String body = HealthcareSupport.body("837P", 123);
for(String invalid : List.of(body.replace("1234567893", "1234567890"), body.replace("60000", "ZIPBAD"), body.replace("ABK:A123", "ABK:BAD!")))
{
assertThat(validate("837P", invalid)).anyMatch(f -> f.snipLevel() == 5);
}
var calls = new ArrayList<String>();
var config = new EdiQBitConfig().withExternalCodeSetProvider((set, code, date) ->
{
calls.add(set + ":" + date);
return (!set.equals("ICD10CM"));
});
var tx = LogisticsSupport.read(HealthcareSupport.interchange("837P", 1, body)).groups().getFirst().transactions().getFirst();
var result = new GuideValidator(config).validate(tx, new BuiltInGuideLoader().resource("837P-005010X222A1"), true);
assertThat(result).anyMatch(f -> f.snipLevel() == 5 && f.segmentId().equals("HI"));
assertThat(calls).contains("ICD10CM:2026-10-15", "HCPCS:2026-10-15");
}
/*******************************************************************************
** Outbound eligibility and claim status requests use only their classified sources.
*******************************************************************************/
@ParameterizedTest
@ValueSource(strings = {"270", "276"})
void outboundMap(String key) throws Exception
{
H2Support.create();
List<QRecord> rules = new ArrayList<>();
QRecord map = LogisticsSupport.definition(key, HealthcareSupport.version(key), rules);
var tx = new OutboundMapper().build(new QRecord().withValue("trace", "SYNTRACE123").withValue("memberId", "SYN000123")
.withValue("patientName", "Testpatient Alpha").withValue("claimNumber", "SYNCLM123"), map, rules, "0001");
assertThat(new GuideValidator().validate(tx, new BuiltInGuideLoader().resource(key + "-" + HealthcareSupport.version(key)), true)).isEmpty();
}
/*******************************************************************************
** A claim cannot borrow charges, dates or patient loops from a neighboring claim.
*******************************************************************************/
@Test
void independentClaimsAndSituational999() throws Exception
{
String body = HealthcareSupport.body("837P", 123);
String claim = body.substring(body.indexOf("CLM*"));
String two = body + claim.replace("*150***", "*151***");
assertThat(validate("837P", two)).singleElement().satisfies(f -> assertThat(f.snipLevel()).isEqualTo(3));
String offsetting = body.replace("*150***", "*149***") + claim.replace("*150***", "*151***");
assertThat(validate("837P", offsetting).stream().filter(f -> f.snipLevel() == 3)).hasSize(2);
String missingDate = body + claim.replace("DTP*472*D8*20261015~", "");
assertThat(validate("837P", missingDate).stream().filter(f -> f.snipLevel() == 4)).hasSize(2);
String missingPatient = body.replace("SBR*P*18", "SBR*P*19");
var group = LogisticsSupport.read(HealthcareSupport.interchange("837P", 1, missingPatient)).groups().getFirst();
String ack = new X12Writer().write(new Ack999Builder().build(group, Map.of("0001", validate("837P", missingPatient)), AckLevel.ALL_TRANSACTIONS, "0002").segments(), X12Separators.DEFAULT, "");
assertThat(ack).isEqualTo("ST*999*0002*005010X231A1~AK1*HC*1*005010X222A1~AK2*837*0001*005010X222A1~IK3*SBR*10*2000B*8~CTX*SITUATIONAL TRIGGER*SBR*10*2000B*2~IK5*E~AK9*E*1*1*1~SE*8*0002~");
assertThat(validate("271", HealthcareSupport.body("271", 123).replace("EB*1**30~", ""))).anyMatch(f -> f.snipLevel() == 4);
assertThat(validate("271", HealthcareSupport.body("271", 123).replace("EB*1**30~", "AAA*Y**72*C~"))).isEmpty();
}
/*******************************************************************************
** External code-list source and name survive normalization; the home reflects the SPI.
*******************************************************************************/
@Test
void externalRepositoryAndHome() throws Exception
{
H2Support.create();
var source = new BuiltInGuideLoader().resource("837P-005010X222A1");
Integer id = new com.kingsrook.qbits.edi.guide.GuideRepository().save(source, "synthetic", 1);
var guide = new com.kingsrook.qbits.edi.guide.GuideRepository().load(id);
assertThat(guide.segments().stream().flatMap(s -> s.elements().stream()).filter(e -> "EXTERNAL".equals(e.source())))
.allMatch(e -> e.externalCodeSet() != null && e.codes().isEmpty());
var runtime = EdiRuntime.current();
var tx = LogisticsSupport.read(HealthcareSupport.interchange("837P", 1, HealthcareSupport.body("837P", 123))).groups().getFirst().transactions().getFirst();
assertThat(runtime.validator().validate(tx, guide, true)).isEmpty();
runtime.config().withExternalCodeSetProvider((set, code, date) -> false);
assertThat(runtime.validator().validate(tx, guide, true)).anyMatch(f -> f.snipLevel() == 5);
var widget = QContext.getQInstance().getWidget("edi_home_system");
String json = com.kingsrook.qqq.backend.core.utils.JsonUtils.toJson(new com.kingsrook.qbits.edi.app.EdiHome()
.render(new com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput().withWidgetMetaData(widget)));
assertThat(json).contains("Provider configured");
runtime.config().withExternalCodeSetProvider(null);
json = com.kingsrook.qqq.backend.core.utils.JsonUtils.toJson(new com.kingsrook.qbits.edi.app.EdiHome()
.render(new com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput().withWidgetMetaData(widget)));
assertThat(json).contains("Format only");
}
/*******************************************************************************
** Real persistence honors defaults, PHI keys, 999 routing and accepted-with-error apply.
*******************************************************************************/
@Test
void healthcarePipeline() throws Exception
{
H2Support.create();
HealthcareSupport.tables();
EdiRuntime.current().initialize();
HealthcareSupport.configure();
int control = 100;
for(String key : List.of("837P", "837I", "835", "271", "277", "834"))
{
HealthcareSupport.receive(key, ++control, HealthcareSupport.body(key, control));
}
HealthcareSupport.receive("837P", ++control, HealthcareSupport.body("837P", control).replace("*150***", "*151***"));
var rows = EdiRecords.query("edi_transaction", new QQueryFilter());
assertThat(rows.stream().filter(r -> "INBOUND".equals(r.getValueString("direction"))).toList()).hasSize(7).allSatisfy(r ->
{
assertThat(r.getValueString("status")).isEqualTo("APPLIED");
assertThat(r.getValueString("phiBusinessKey")).startsWith("SYN");
assertThat(r.getValueString("businessKey")).isNull();
assertThat(r.getValueBoolean("containsPhi")).isTrue();
});
assertThat(rows).anyMatch(r -> "E".equals(r.getValueString("ackStatus")));
assertThat(rows.stream().filter(r -> "OUTBOUND".equals(r.getValueString("direction"))).toList()).hasSize(7).allMatch(r -> "999".equals(r.getValueString("transactionSet")));
assertThat(EdiRecords.query("sample_claim_line", new QQueryFilter())).hasSize(6);
assertThat(EdiRecords.query("edi_transaction_error", new QQueryFilter())).allMatch(r -> r.getValue("valueExcerpt") == null);
assertThat(EdiRecords.query("edi_agreement_document", new QQueryFilter())).allMatch(r -> Boolean.TRUE.equals(r.getValueBoolean("containsPhi")));
}
}
@@ -0,0 +1,481 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kingsrook.qbits.edi.demo.EdiDemoServer;
import com.kingsrook.qbits.edi.flow.AckMonitor;
import com.kingsrook.qbits.edi.flow.InboundSink;
import com.kingsrook.qbits.edi.flow.InboxWorker;
import com.kingsrook.qbits.edi.flow.SendWorker;
import com.kingsrook.qbits.edi.metadata.EdiTables;
import com.kingsrook.qbits.edi.processes.EdiProcesses;
import com.kingsrook.qbits.edi.security.EdiClassifications;
import com.kingsrook.qbits.edi.storage.EdiRecords;
import com.kingsrook.qbits.edi.storage.PayloadCrypto;
import com.kingsrook.qbits.edi.storage.PreviewDownloadBackend;
import com.kingsrook.qbits.edi.storage.RawDocumentDownload;
import com.kingsrook.qbits.edi.support.SystemWrites;
import com.kingsrook.qbits.edi.transport.HttpsRouteProvider;
import com.kingsrook.qbits.edi.transport.gateway.EdiFileGatewayConsumer;
import com.kingsrook.qbits.edi.transport.gateway.standin.FileGatewayOutbox;
import com.kingsrook.qbits.edi.transport.gateway.standin.InboundFile;
import com.kingsrook.qbits.edi.transport.gateway.standin.OutboxFileRef;
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
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.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import io.javalin.Javalin;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
** End-to-end PHI egress scanner. Only explicit PHI columns may retain sentinels.
** RC.5 has no secured-data tier resolver; FULL exports and positional egress run here.
*******************************************************************************/
class PhiCanaryTest
{
private final List<String> markers = new ArrayList<>();
private final ObjectMapper json = new ObjectMapper();
/*******************************************************************************
** Every five-character window is enough to detect any longer leaked substring.
*******************************************************************************/
private void scan(String output, String channel)
{
for(String marker : markers)
{
for(int i = 0; i <= marker.length() - 5; i++)
{
String window = marker.substring(i, i + 5);
int at = output.indexOf(window);
assertThat(at).as("PHI substring in " + channel + (at < 0 ? "" : ": " + output.substring(0, Math.min(120, output.length())) + " ... " + output.substring(Math.max(0, at - 60), Math.min(output.length(), at + 80)))).isNegative();
}
}
}
/*******************************************************************************
** Exempt only values and display values of metadata-classified PHI record columns.
** Labels, messages, metadata, context and every other JSON property remain scanned.
*******************************************************************************/
private void exemptColumns(JsonNode node, String table)
{
if(node.isObject())
{
JsonNode records = node.get("records");
if(records != null && records.isArray())
{
for(JsonNode record : records)
{
for(String key : List.of("values", "displayValues"))
{
if(record.get(key) instanceof ObjectNode values)
{
for(var field : QContext.getQInstance().getTable(table).getFields().values())
{
var classification = field.getSupplementalMetaData("edi.classification");
if(classification instanceof EdiClassifications classified && classified.getClassification().equals("PHI"))
{
values.remove(field.getName());
}
}
}
}
}
}
}
node.elements().forEachRemaining(child -> exemptColumns(child, table));
}
/*******************************************************************************
** Scan all properties of the real serialized response, with narrow column exceptions.
*******************************************************************************/
private void response(Object output, String table, String channel) throws Exception
{
JsonNode node = json.readTree(JsonUtils.toJson(output));
if(table != null)
{
exemptColumns(node, table);
}
scan(node.toString(), channel);
}
/*******************************************************************************
** Force distinct field sentinels twice, across both ingress contracts and replays.
*******************************************************************************/
@Test
void allAvailableEgress() throws Exception
{
try(EdiDemoServer demo = new EdiDemoServer(); HttpClient client = HttpClient.newHttpClient())
{
demo.seed();
var instance = demo.defineQInstance();
CapturedAlerts.outputs.clear();
com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData.ofOrWithNew(instance).withSink(new com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference(CapturedAlerts.class));
// A test-only widened diagnosis field allows the long marker through mapping;
// its invalid code shape still exercises SNIP 5, and separate documents force SNIP 2.
SystemWrites.call(() ->
{
for(QRecord segment : EdiRecords.query("edi_guide_segment", EdiRecords.equal("segmentId", "HI")))
{
for(QRecord element : EdiRecords.query("edi_guide_element", EdiRecords.equal("guideSegmentId", segment.getValue("id"))))
{
SystemWrites.update("edi_guide_element", new QRecord().withValue("id", element.getValue("id")).withValue("maxLength", 80));
}
}
SystemWrites.insert("edi_transport", new QRecord().withValue("name", "canary-ingress").withValue("type", "HTTPS").withValue("direction", "INBOUND")
.withValue("isActive", true).withValue("httpsInboundAuth", "BEARER").withValue("httpsSecretEnvVar", "SYNTHETIC_HTTP_KEY"));
return (null);
});
EdiRuntime.current().config().withSecretsResolver(name -> name.equals("SYNTHETIC_HTTP_KEY") ? "synthetic-auth" : Base64.getEncoder().encodeToString(new byte[32]));
var provider = new HttpsRouteProvider();
provider.setQInstance(instance);
var server = Javalin.create(provider::acceptJavalinConfig).start(0);
var capture = new CapturedLogs();
var logger = (org.apache.logging.log4j.core.Logger) LogManager.getRootLogger();
capture.start();
logger.addAppender(capture);
List<String> transports = new ArrayList<>();
FileGatewayOutbox.register((endpoint, name, stream, size) ->
{
transports.add(new String(stream.readAllBytes(), StandardCharsets.US_ASCII));
return (new OutboxFileRef(transports.size(), endpoint, name, name, size, null, Instant.now()));
});
try
{
int control = 800;
List<QRecord> received = new ArrayList<>();
for(int variant = 0; variant < 2; variant++)
{
List<String> fields = new ArrayList<>();
for(int field = 0; field < 4; field++)
{
String marker = "CANARY-PHI-" + UUID.randomUUID().toString().substring(0, 8).replaceAll("(.)", "Z$1");
fields.add(marker);
markers.add(marker);
}
for(String key : List.of("837P", "835"))
{
String body = HealthcareSupport.body(key, 123).replace("Testpatient Alpha", fields.get(0))
.replaceAll("SYN[0-9]{6}", fields.get(1)).replaceAll("SYNCLM[0-9]{6}", fields.get(2));
if(key.equals("837P"))
{
body = body.replace("ABK:A123", "ABK:" + fields.get(3)).replace("*150***", "*150.01***").replace("SBR*P*18", "SBR*P*19");
}
else
{
body = body.replace("SVC*HC:S0001", "REF*DX*" + fields.get(3) + "~SVC*HC:S0001").replace("BPR*I*115", "BPR*I*115.01");
}
String raw = HealthcareSupport.interchange(key, ++control, body);
byte[] bytes = raw.getBytes(StandardCharsets.US_ASCII);
if(variant == 0)
{
var file = new InboundFile(control, "cedar-inbox", "synthetic-user", "synthetic-user", "127.0.0.1", "/", fields.get(1) + ".edi",
bytes.length, PayloadCrypto.digest(bytes), Instant.now(), null, null, () -> new ByteArrayInputStream(bytes));
response(new EdiFileGatewayConsumer(new InboundSink(EdiRuntime.current().payloads())).accept(file), null, "gateway reply");
}
else
{
var reply = client.send(HttpRequest.newBuilder(URI.create("http://localhost:" + server.port() + "/qqq/hooks/edi/https/canary-ingress"))
.header("Authorization", "Bearer synthetic-auth").POST(HttpRequest.BodyPublishers.ofByteArray(bytes)).build(), HttpResponse.BodyHandlers.ofString());
assertThat(reply.statusCode()).isEqualTo(202);
scan(reply.body() + reply.headers(), "HTTPS response");
}
assertThat(new InboxWorker().runOnce()).isEqualTo(1);
var transaction = rows("edi_transaction").stream().filter(r -> fields.get(2).equals(r.getValueString("phiBusinessKey")) || key.equals("835") && "SYNTRACE123".equals(r.getValueString("phiBusinessKey")))
.reduce((a, b) -> b).orElseThrow();
assertThat(transaction.getValueString("status")).isEqualTo("APPLIED");
received.add(transaction);
// Force a structural rejection with the same markers in the same fields.
HealthcareSupport.receive(key, ++control, body.replace(key.equals("837P") ? "SV1*HC:S0001*100" : "BPR*I*115.01", key.equals("837P") ? "SV1*HC:S0001*BAD" : "BPR*I*BAD"));
}
}
assertThat(rows("edi_transaction_error")).extracting(r -> r.getValueInteger("snipLevel")).contains(2, 3, 4);
QRecord first = received.getFirst();
QRecord archived = rows("edi_interchange").stream().filter(r -> first.getValue("interchangeId").equals(r.getValue("id"))).findFirst().orElseThrow();
QRecord sample = SystemWrites.call(() -> SystemWrites.insert("edi_map_sample", new QRecord().withValue("mapId", first.getValue("mapId"))
.withValue("name", "Synthetic PHI preview").withValue("payloadRef", archived.getValue("payloadRef")).withValue("expectedResultJson", "{\"clinicalKey\":\"" + markers.getFirst() + "\"}")));
process("edi_map_sample_preview", sample);
for(QRecord transaction : received)
{
process("edi_transaction_reprocess", transaction);
assertThat(new InboxWorker().runOnce()).isEqualTo(1);
}
for(QRecord exception : rows("edi_exception"))
{
process("edi_exception_resolve", exception);
}
while(new SendWorker().runOnce() > 0)
{
// Capture all eligible acknowledgement transports.
}
QRecord ack = rows("edi_outbound_message").stream().filter(r -> r.getValue("ackForGroupId") != null && "SENT".equals(r.getValueString("status"))).reduce((a, b) -> b).orElseThrow();
process("edi_outbound_message_resend", ack);
new SendWorker().runOnce();
assertThat(transports).isNotEmpty();
transports.forEach(payload -> scan(payload, "non-PHI acknowledgement transport"));
// Actual overdue alerts, with complete serialized signals captured by the sink.
QRecord group = rows("edi_functional_group").getFirst();
SystemWrites.call(() -> { SystemWrites.update("edi_functional_group", new QRecord().withValue("id", group.getValue("id")).withValue("ackStatus", "PENDING").withValue("ackDueAt", Instant.now().minusSeconds(60))); return (null); });
new AckMonitor().runOnce();
assertThat(CapturedAlerts.outputs).isNotEmpty();
CapturedAlerts.outputs.forEach(output -> scan(output, "alerts"));
for(var table : instance.getTables().values())
{
if(table.getName().startsWith("edi_") && !table.getName().equals(PreviewDownloadBackend.TABLE))
{
var output = new QueryAction().execute(new QueryInput(table.getName()).withFilter(new QQueryFilter()).withShouldFetchHeavyFields(true));
response(output, table.getName(), "export " + table.getName());
}
}
int rendered = 0;
for(var widget : instance.getWidgets().values())
{
var join = EdiTables.joins().stream().filter(j -> j.getName().equals(widget.getName())).findFirst();
if(join.isEmpty())
{
response(new RenderWidgetAction().execute(new RenderWidgetInput().withWidgetMetaData(widget)), null, widget.getName());
}
else
{
for(QRecord parent : rows(join.get().getLeftTable()))
{
var params = new HashMap<>(Map.of("tableName", join.get().getLeftTable(), "id", parent.getValueString("id")));
response(new RenderWidgetAction().execute(new RenderWidgetInput().withWidgetMetaData(widget).withUrlParams(params)), join.get().getRightTable(), widget.getName());
}
}
rendered++;
}
assertThat(rendered).isEqualTo(instance.getWidgets().size());
assertThat(capture.outputs).isNotEmpty();
capture.outputs.forEach(output -> scan(output, "logs"));
// Ciphertext contains no plaintext marker, including after decoding the BLOB.
for(QRecord payload : rows("edi_payload"))
{
if(payload.getValue("ciphertext") instanceof byte[] bytes)
{
scan(new String(bytes, StandardCharsets.ISO_8859_1), "ciphertext bytes");
}
}
// Raw access is the explicit authorized escape: bytes are streamed, event is value-free.
QRecord interchange = rows("edi_interchange").stream().filter(r -> received.getFirst().getValue("interchangeId").equals(r.getValue("id"))).findFirst().orElseThrow();
var download = process("edi_interchange_download", interchange);
var storage = new StorageInput(PreviewDownloadBackend.TABLE).withReference(download.getValueString("storageReference"));
try(var stream = new PreviewDownloadBackend().getInputStream(storage))
{
assertThat(new String(stream.readAllBytes(), StandardCharsets.US_ASCII)).contains(markers.getFirst());
}
assertThat(rows("edi_event")).anyMatch(r -> "RAW_DOWNLOADED".equals(r.getValueString("type")) && r.getValueString("userName") != null);
response(new QueryAction().execute(new QueryInput("edi_event").withFilter(new QQueryFilter())), "edi_event", "download audit");
assertThatThrownBy(() -> RawDocumentDownload.open("raw:edi_transaction:1")).hasMessageContaining("not authorized");
capture.outputs.forEach(output -> scan(output, "logs after raw download"));
var fullSession = QContext.getQSession();
try
{
QContext.setQSession(new com.kingsrook.qqq.backend.core.model.session.QSession());
assertThatThrownBy(() -> new PreviewDownloadBackend().getInputStream(storage)).hasMessageContaining("not authorized");
assertThatThrownBy(() -> RawDocumentDownload.open(storage.getReference())).hasMessageContaining("not authorized");
}
finally
{
QContext.setQSession(fullSession);
}
hiddenExportsAndWidgets();
capture.outputs.forEach(output -> scan(output, "logs after HIDDEN exports and widgets"));
}
finally
{
logger.removeAppender(capture);
capture.stop();
server.stop();
}
}
}
/*******************************************************************************
** RC.5 HIDDEN field locks are real enforcement, not test-side JSON redaction.
*******************************************************************************/
private void hiddenExportsAndWidgets() throws Exception
{
var instance = QContext.getQInstance();
var enforcement = instance.getEnforcePermissionsInCoreActions();
instance.setEnforcePermissionsInCoreActions(true);
var fullSession = QContext.getQSession();
var viewer = new com.kingsrook.qqq.backend.core.model.session.QSession().withPermissions("edi.admin").withSecurityKeyValue("edi.admin", true).withSecurityKeyValue("edi.guide.loader", true);
for(var table : instance.getTables().values())
{
if(!table.getName().startsWith("edi_") || table.getName().equals(PreviewDownloadBackend.TABLE))
{
continue;
}
viewer.withPermissions("edi." + table.getName().substring(4) + ".read");
for(var field : table.getFields().values())
{
if(field.getSupplementalMetaData("edi.classification") instanceof EdiClassifications classified && classified.getClassification().equals("PHI"))
{
field.withFieldSecurityLock(new com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock().withSecurityKeyType("synthetic_phi_access"));
}
}
}
try
{
QContext.setQSession(viewer);
for(var table : instance.getTables().values())
{
if(table.getName().startsWith("edi_") && !table.getName().equals(PreviewDownloadBackend.TABLE))
{
response(new QueryAction().execute(new QueryInput(table.getName()).withFilter(new QQueryFilter()).withShouldFetchHeavyFields(true)), null, "HIDDEN export " + table.getName());
}
}
for(var widget : instance.getWidgets().values())
{
var join = EdiTables.joins().stream().filter(j -> j.getName().equals(widget.getName())).findFirst();
var params = new HashMap<String, String>();
if(join.isPresent())
{
var parents = rows(join.get().getLeftTable());
assertThat(parents).as(join.get().getLeftTable()).isNotEmpty();
params.put("tableName", join.get().getLeftTable());
params.put("id", parents.getLast().getValueString("id"));
}
response(new RenderWidgetAction().execute(new RenderWidgetInput().withWidgetMetaData(widget).withUrlParams(params)), null, "HIDDEN " + widget.getName());
}
}
finally
{
QContext.setQSession(fullSession);
instance.setEnforcePermissionsInCoreActions(enforcement);
}
}
/*******************************************************************************
** Exercise standard Review and Result paths with serialized outputs and summaries.
*******************************************************************************/
private RunBackendStepOutput process(String name, QRecord record) throws Exception
{
var input = new RunBackendStepInput().withProcessName(name).withRecords(List.of(record));
var review = new EdiProcesses.Review();
var preview = new RunBackendStepOutput();
review.runOnePage(input, preview);
response(Map.of("records", preview.getRecords(), "values", preview.getValues()), null, "Review records");
response(review.getProcessSummary(preview, false), null, "Review summary");
var execute = new EdiProcesses.Execute();
var output = new RunBackendStepOutput();
execute.runOnePage(input, output);
response(Map.of("records", output.getRecords(), "values", output.getValues()), null, "Result records");
response(execute.getProcessSummary(output, true), null, "Result summary");
return (output);
}
/*******************************************************************************
** Actual QueryAction rows, including encrypted BLOBs.
*******************************************************************************/
private static List<QRecord> rows(String table) throws Exception
{
return (new QueryAction().execute(new QueryInput(table).withFilter(new QQueryFilter()).withShouldFetchHeavyFields(true)).getRecords());
}
/*******************************************************************************
** Capture complete alert payloads, not merely alert type names.
*******************************************************************************/
public static class CapturedAlerts implements com.kingsrook.qqq.backend.core.alerts.AlertSinkInterface
{
static final List<String> outputs = new ArrayList<>();
/*******************************************************************************
** Retain every serialized signal for the substring scanner.
*******************************************************************************/
public void raise(com.kingsrook.qqq.backend.core.alerts.AlertSignal signal)
{
outputs.add(JsonUtils.toJson(signal));
}
/*******************************************************************************
** Resolutions are egress too.
*******************************************************************************/
public void resolve(com.kingsrook.qqq.backend.core.alerts.AlertResolution resolution)
{
outputs.add(JsonUtils.toJson(resolution));
}
/*******************************************************************************
** Heartbeats contain only the operational key and time.
*******************************************************************************/
public void heartbeat(String key, Instant at)
{
outputs.add(key + at);
}
}
/*******************************************************************************
** In-memory Log4j appender includes rendered throwable stacks in the scan.
*******************************************************************************/
private static class CapturedLogs extends AbstractAppender
{
private final List<String> outputs = new java.util.concurrent.CopyOnWriteArrayList<>();
/*******************************************************************************
** No filesystem output or production logging reconfiguration.
*******************************************************************************/
private CapturedLogs()
{
super("phi-egress", null, PatternLayout.newBuilder().withPattern("%c %m%ex").build(), false, null);
}
/*******************************************************************************
** Capture the exact formatted event including parameter values and exceptions.
*******************************************************************************/
@Override
public void append(LogEvent event)
{
outputs.add(new String(getLayout().toByteArray(event), StandardCharsets.UTF_8));
}
}
}
@@ -87,6 +87,7 @@ public class EdiDemoServer extends AbstractQQQApplication implements AutoCloseab
EdiRuntime.current().config().withSecretsResolver(name -> secrets.getOrDefault(name, Base64.getEncoder().encodeToString(new byte[32])));
RetailSupport.tables();
LogisticsSupport.tables();
com.kingsrook.qbits.edi.HealthcareSupport.tables();
instance.setHomeAppName("edi");
for(String table : List.of("sample_order", "sample_shipment", "sample_invoice", "sample_inventory", "sample_wms_shipment", "sample_load"))
{
@@ -245,10 +246,34 @@ public class EdiDemoServer extends AbstractQQQApplication implements AutoCloseab
}
logistics(local, agreements.get(1));
seedAs2(local);
healthcare();
return (null);
});
}
/*******************************************************************************
** Synthetic clearinghouse claims, remittance and batch eligibility exchange.
*******************************************************************************/
private void healthcare() throws Exception
{
var agreement = com.kingsrook.qbits.edi.HealthcareSupport.configure();
int control = 500;
for(String key : List.of("837P", "837I", "835"))
{
com.kingsrook.qbits.edi.HealthcareSupport.receive(key, ++control, com.kingsrook.qbits.edi.HealthcareSupport.body(key, control));
}
com.kingsrook.qbits.edi.HealthcareSupport.receive("837P", ++control, com.kingsrook.qbits.edi.HealthcareSupport.body("837P", control).replace("*150***", "*151***"));
com.kingsrook.qbits.edi.HealthcareSupport.receive("837P", ++control, com.kingsrook.qbits.edi.HealthcareSupport.body("837P", control).replace("SBR*P*18", "SBR*P*19"));
QRecord source = SystemWrites.insert("sample_eligibility_request", new QRecord().withValue("trace", "SYNTRACE506").withValue("memberId", "SYN000123").withValue("patientName", "Testpatient Alpha"));
com.kingsrook.qbits.edi.flow.OutboundEnqueue.enqueue("sample_eligibility_request", source, "MANUAL", null);
new OutboundWorker().runOnce();
com.kingsrook.qbits.edi.HealthcareSupport.receive("271", ++control, com.kingsrook.qbits.edi.HealthcareSupport.body("271", control));
while(new SendWorker().runOnce() > 0)
{
// Drain only the configured in-memory outboxes.
}
}
/*******************************************************************************
** Warehouse and logistics traffic: Harbor Logistics (3PL) sends 940s and receives 945s,
** and Ridgeline Freight (carrier) receives 204 tenders and sends 214 statuses.
@@ -86,7 +86,7 @@ class EdiDemoServerTest
demo.seed();
demo.seed();
assertThat(demo.defineQInstance().getHomeAppName()).isEqualTo("edi");
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_partner", new QQueryFilter()))).hasSize(5);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_partner", new QQueryFilter()))).hasSize(6);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_certificate", new QQueryFilter()))).hasSize(5);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_inbox", new QQueryFilter()))).hasSizeGreaterThanOrEqualTo(40);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_transaction", new QQueryFilter()))).anyMatch(row -> "OUTBOUND".equals(row.getValueString("direction")));
@@ -94,7 +94,7 @@ class EdiDemoServerTest
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_functional_group", new QQueryFilter()))).anyMatch(row -> row.getValueInstant("ackReceivedAt") != null);
assertThat(SystemWrites.call(() -> EdiRecords.query("edi_exception", new QQueryFilter()))).anyMatch(row -> "OVERDUE_ACK".equals(row.getValueString("type")));
var partners = SystemWrites.call(() -> EdiRecords.query("edi_partner", new QQueryFilter()));
assertThat(partners).extracting(row -> row.getValueString("code")).containsExactlyInAnyOrder("NORTHWIND", "HARBOR", "SUMMIT", "RIDGELINE", "PACIFIC");
assertThat(partners).extracting(row -> row.getValueString("code")).containsExactlyInAnyOrder("NORTHWIND", "HARBOR", "SUMMIT", "RIDGELINE", "PACIFIC", "CEDAR");
assertThat(partners).allSatisfy(row -> assertThat(row.getValueString("contactEmail")).endsWith("@example.invalid"));
assertThat(SystemWrites.call(() -> EdiRecords.query("sample_wms_order", new QQueryFilter()))).hasSize(3);
assertThat(SystemWrites.call(() -> EdiRecords.query("sample_shipment_status", new QQueryFilter()))).hasSize(3);
@@ -0,0 +1,254 @@
{
"map": {
"name": "Synthetic 270 outbound",
"direction": "OUTBOUND",
"transactionSet": "270",
"version": "005010X279A1",
"tableName": "sample_eligibility_request",
"revision": 1,
"status": "ACTIVE",
"businessKeyType": "HEALTHCARE",
"businessKeyPath": "TRN02",
"standard": "X12",
"onMatch": "UPDATE"
},
"rules": [
{
"sequence": 2,
"ediPath": "BHT01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"0022\"}"
},
{
"sequence": 3,
"ediPath": "BHT02",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"13\"}"
},
{
"sequence": 4,
"ediPath": "BHT03",
"qqqPath": "trace"
},
{
"sequence": 5,
"ediPath": "BHT04",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"20261015\"}"
},
{
"sequence": 6,
"ediPath": "BHT05",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1200\"}"
},
{
"sequence": 7,
"ediPath": "2000A/HL01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 8,
"ediPath": "2000A/HL02",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"\"}"
},
{
"sequence": 9,
"ediPath": "2000A/HL03",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"20\"}"
},
{
"sequence": 10,
"ediPath": "2000A/HL04",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 11,
"ediPath": "2000A/2100A/NM101",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PR\"}"
},
{
"sequence": 12,
"ediPath": "2000A/2100A/NM102",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"2\"}"
},
{
"sequence": 13,
"ediPath": "2000A/2100A/NM103",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"Synthetic PR\"}"
},
{
"sequence": 14,
"ediPath": "2000A/2100A/NM108",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PI\"}"
},
{
"sequence": 15,
"ediPath": "2000A/2100A/NM109",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"SYNPLAN\"}"
},
{
"sequence": 16,
"ediPath": "2000B/HL01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"2\"}"
},
{
"sequence": 17,
"ediPath": "2000B/HL02",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 18,
"ediPath": "2000B/HL03",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"21\"}"
},
{
"sequence": 19,
"ediPath": "2000B/HL04",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 20,
"ediPath": "2000B/2100B/NM101",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1P\"}"
},
{
"sequence": 21,
"ediPath": "2000B/2100B/NM102",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"2\"}"
},
{
"sequence": 22,
"ediPath": "2000B/2100B/NM103",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"Synthetic 1P\"}"
},
{
"sequence": 23,
"ediPath": "2000B/2100B/NM108",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"XX\"}"
},
{
"sequence": 24,
"ediPath": "2000B/2100B/NM109",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1234567893\"}"
},
{
"sequence": 25,
"ediPath": "2000C/HL01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"3\"}"
},
{
"sequence": 26,
"ediPath": "2000C/HL02",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"2\"}"
},
{
"sequence": 27,
"ediPath": "2000C/HL03",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"22\"}"
},
{
"sequence": 28,
"ediPath": "2000C/HL04",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"0\"}"
},
{
"sequence": 29,
"ediPath": "2000C/TRN01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 30,
"ediPath": "2000C/TRN02",
"qqqPath": "trace"
},
{
"sequence": 31,
"ediPath": "2000C/2100C/NM101",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"IL\"}"
},
{
"sequence": 32,
"ediPath": "2000C/2100C/NM102",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 33,
"ediPath": "2000C/2100C/NM103",
"qqqPath": "patientName"
},
{
"sequence": 34,
"ediPath": "2000C/2100C/NM108",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"MI\"}"
},
{
"sequence": 35,
"ediPath": "2000C/2100C/NM109",
"qqqPath": "memberId"
},
{
"sequence": 36,
"ediPath": "2000C/2100C/2110C/EQ01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"30\"}"
}
]
}
@@ -0,0 +1,43 @@
{
"map": {
"name": "Synthetic 271 inbound",
"direction": "INBOUND",
"transactionSet": "271",
"version": "005010X279A1",
"tableName": "sample_eligibility_response",
"revision": 1,
"status": "ACTIVE",
"businessKeyType": "HEALTHCARE",
"businessKeyPath": "TRN02",
"standard": "X12",
"onMatch": "UPDATE",
"matchKeyFields": "trace"
},
"rules": [
{
"sequence": 1,
"ediPath": "TRN02",
"qqqPath": "trace"
},
{
"sequence": 2,
"ediPath": "2100C/NM109",
"qqqPath": "memberId"
},
{
"sequence": 3,
"ediPath": "2100C/NM103",
"qqqPath": "patientName"
},
{
"sequence": 4,
"ediPath": "EB01",
"qqqPath": "benefit"
},
{
"sequence": 5,
"ediPath": "EB03",
"qqqPath": "serviceType"
}
]
}
@@ -0,0 +1,259 @@
{
"map": {
"name": "Synthetic 276 outbound",
"direction": "OUTBOUND",
"transactionSet": "276",
"version": "005010X212",
"tableName": "sample_claim_status",
"revision": 1,
"status": "ACTIVE",
"businessKeyType": "HEALTHCARE",
"businessKeyPath": "TRN02",
"standard": "X12",
"onMatch": "UPDATE"
},
"rules": [
{
"sequence": 2,
"ediPath": "BHT01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"0010\"}"
},
{
"sequence": 3,
"ediPath": "BHT02",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"13\"}"
},
{
"sequence": 4,
"ediPath": "BHT03",
"qqqPath": "trace"
},
{
"sequence": 5,
"ediPath": "BHT04",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"20261015\"}"
},
{
"sequence": 6,
"ediPath": "BHT05",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1200\"}"
},
{
"sequence": 7,
"ediPath": "2000A/HL01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 8,
"ediPath": "2000A/HL02",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"\"}"
},
{
"sequence": 9,
"ediPath": "2000A/HL03",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"20\"}"
},
{
"sequence": 10,
"ediPath": "2000A/HL04",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 11,
"ediPath": "2000A/2100A/NM101",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PR\"}"
},
{
"sequence": 12,
"ediPath": "2000A/2100A/NM102",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"2\"}"
},
{
"sequence": 13,
"ediPath": "2000A/2100A/NM103",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"Synthetic PR\"}"
},
{
"sequence": 14,
"ediPath": "2000A/2100A/NM108",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"PI\"}"
},
{
"sequence": 15,
"ediPath": "2000A/2100A/NM109",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"SYNPLAN\"}"
},
{
"sequence": 16,
"ediPath": "2000B/HL01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"2\"}"
},
{
"sequence": 17,
"ediPath": "2000B/HL02",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 18,
"ediPath": "2000B/HL03",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"21\"}"
},
{
"sequence": 19,
"ediPath": "2000B/HL04",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 20,
"ediPath": "2000B/2100B/NM101",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1P\"}"
},
{
"sequence": 21,
"ediPath": "2000B/2100B/NM102",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"2\"}"
},
{
"sequence": 22,
"ediPath": "2000B/2100B/NM103",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"Synthetic 1P\"}"
},
{
"sequence": 23,
"ediPath": "2000B/2100B/NM108",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"XX\"}"
},
{
"sequence": 24,
"ediPath": "2000B/2100B/NM109",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1234567893\"}"
},
{
"sequence": 25,
"ediPath": "2000C/HL01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"3\"}"
},
{
"sequence": 26,
"ediPath": "2000C/HL02",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"2\"}"
},
{
"sequence": 27,
"ediPath": "2000C/HL03",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"22\"}"
},
{
"sequence": 28,
"ediPath": "2000C/HL04",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"0\"}"
},
{
"sequence": 29,
"ediPath": "2000C/2100C/NM101",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"IL\"}"
},
{
"sequence": 30,
"ediPath": "2000C/2100C/NM102",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 31,
"ediPath": "2000C/2100C/NM103",
"qqqPath": "patientName"
},
{
"sequence": 32,
"ediPath": "2000C/2100C/NM108",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"MI\"}"
},
{
"sequence": 33,
"ediPath": "2000C/2100C/NM109",
"qqqPath": "memberId"
},
{
"sequence": 34,
"ediPath": "2000C/2100C/2200C/TRN01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"1\"}"
},
{
"sequence": 35,
"ediPath": "2000C/2100C/2200C/TRN02",
"qqqPath": "trace"
},
{
"sequence": 36,
"ediPath": "2000C/2100C/2200C/REF01",
"qqqPath": null,
"transform": "CONSTANT",
"transformArgs": "{\"value\": \"EJ\"}"
},
{
"sequence": 37,
"ediPath": "2000C/2100C/2200C/REF02",
"qqqPath": "claimNumber"
}
]
}
@@ -0,0 +1,43 @@
{
"map": {
"name": "Synthetic 277 inbound",
"direction": "INBOUND",
"transactionSet": "277",
"version": "005010X212",
"tableName": "sample_claim_status",
"revision": 1,
"status": "ACTIVE",
"businessKeyType": "HEALTHCARE",
"businessKeyPath": "2200C/TRN02",
"standard": "X12",
"onMatch": "UPDATE",
"matchKeyFields": "trace"
},
"rules": [
{
"sequence": 1,
"ediPath": "2200C/TRN02",
"qqqPath": "trace"
},
{
"sequence": 2,
"ediPath": "2100C/NM109",
"qqqPath": "memberId"
},
{
"sequence": 3,
"ediPath": "STC01-1",
"qqqPath": "category"
},
{
"sequence": 4,
"ediPath": "STC01-2",
"qqqPath": "statusCode"
},
{
"sequence": 5,
"ediPath": "REF[REF01=EJ]/REF02",
"qqqPath": "claimNumber"
}
]
}
@@ -0,0 +1,45 @@
{
"map": {
"name": "Synthetic 834 inbound",
"direction": "INBOUND",
"transactionSet": "834",
"version": "005010X220A1",
"tableName": "sample_enrollment",
"revision": 1,
"status": "ACTIVE",
"businessKeyType": "HEALTHCARE",
"businessKeyPath": "BGN02",
"standard": "X12",
"onMatch": "UPDATE",
"matchKeyFields": "reference"
},
"rules": [
{
"sequence": 1,
"ediPath": "BGN02",
"qqqPath": "reference"
},
{
"sequence": 2,
"ediPath": "2000[*]/REF[REF01=0F]/REF02",
"qqqPath": "members.memberId"
},
{
"sequence": 3,
"ediPath": "2000[*]/2100A/NM103",
"qqqPath": "members.patientName"
},
{
"sequence": 4,
"ediPath": "2000[*]/2100A/DMG02",
"qqqPath": "members.birthDate",
"transform": "DATE",
"condition": "present(2100A/DMG02)"
},
{
"sequence": 5,
"ediPath": "2000[*]/2300/HD03",
"qqqPath": "members.coverage"
}
]
}
@@ -0,0 +1,59 @@
{
"map": {
"name": "Synthetic 835 inbound",
"direction": "INBOUND",
"transactionSet": "835",
"version": "005010X221A1",
"tableName": "sample_remittance",
"revision": 1,
"status": "ACTIVE",
"businessKeyType": "HEALTHCARE",
"businessKeyPath": "TRN02",
"standard": "X12",
"onMatch": "UPDATE",
"matchKeyFields": "trace"
},
"rules": [
{
"sequence": 1,
"ediPath": "TRN02",
"qqqPath": "trace"
},
{
"sequence": 2,
"ediPath": "BPR02",
"qqqPath": "payment",
"transform": "DECIMAL",
"condition": "present(BPR02)"
},
{
"sequence": 3,
"ediPath": "2100[*]/CLP01",
"qqqPath": "claims.claimNumber"
},
{
"sequence": 4,
"ediPath": "2100[*]/CLP03",
"qqqPath": "claims.charge",
"transform": "DECIMAL",
"condition": "present(CLP03)"
},
{
"sequence": 5,
"ediPath": "2100[*]/CLP04",
"qqqPath": "claims.paid",
"transform": "DECIMAL",
"condition": "present(CLP04)"
},
{
"sequence": 6,
"ediPath": "2100[*]/NM1[NM101=QC]/NM103",
"qqqPath": "claims.patientName"
},
{
"sequence": 7,
"ediPath": "2100[*]/NM1[NM101=IL]/NM109",
"qqqPath": "claims.memberId"
}
]
}
@@ -0,0 +1,68 @@
{
"map": {
"name": "Synthetic 837I inbound",
"direction": "INBOUND",
"transactionSet": "837",
"version": "005010X223A2",
"tableName": "sample_claim",
"revision": 1,
"status": "ACTIVE",
"businessKeyType": "HEALTHCARE",
"businessKeyPath": "CLM01",
"standard": "X12",
"onMatch": "UPDATE",
"matchKeyFields": "claimNumber"
},
"rules": [
{
"sequence": 1,
"ediPath": "CLM01",
"qqqPath": "claimNumber"
},
{
"sequence": 2,
"ediPath": "2010BA/NM109",
"qqqPath": "memberId"
},
{
"sequence": 3,
"ediPath": "2010BA/NM103",
"qqqPath": "patientName"
},
{
"sequence": 4,
"ediPath": "CLM02",
"qqqPath": "totalCharge",
"transform": "DECIMAL",
"condition": "present(CLM02)"
},
{
"sequence": 5,
"ediPath": "HI01-2",
"qqqPath": "diagnosis"
},
{
"sequence": 6,
"ediPath": "2400[*]/LX01",
"qqqPath": "lines.lineNumber"
},
{
"sequence": 7,
"ediPath": "2400[*]/SV202-2",
"qqqPath": "lines.procedureCode"
},
{
"sequence": 8,
"ediPath": "2400[*]/SV203",
"qqqPath": "lines.charge",
"transform": "DECIMAL",
"condition": "present(SV203)"
},
{
"sequence": 9,
"ediPath": "2010CA/NM103",
"qqqPath": "patientName",
"condition": "present(2010CA/NM103)"
}
]
}
@@ -0,0 +1,68 @@
{
"map": {
"name": "Synthetic 837P inbound",
"direction": "INBOUND",
"transactionSet": "837",
"version": "005010X222A1",
"tableName": "sample_claim",
"revision": 1,
"status": "ACTIVE",
"businessKeyType": "HEALTHCARE",
"businessKeyPath": "CLM01",
"standard": "X12",
"onMatch": "UPDATE",
"matchKeyFields": "claimNumber"
},
"rules": [
{
"sequence": 1,
"ediPath": "CLM01",
"qqqPath": "claimNumber"
},
{
"sequence": 2,
"ediPath": "2010BA/NM109",
"qqqPath": "memberId"
},
{
"sequence": 3,
"ediPath": "2010BA/NM103",
"qqqPath": "patientName"
},
{
"sequence": 4,
"ediPath": "CLM02",
"qqqPath": "totalCharge",
"transform": "DECIMAL",
"condition": "present(CLM02)"
},
{
"sequence": 5,
"ediPath": "HI01-2",
"qqqPath": "diagnosis"
},
{
"sequence": 6,
"ediPath": "2400[*]/LX01",
"qqqPath": "lines.lineNumber"
},
{
"sequence": 7,
"ediPath": "2400[*]/SV101-2",
"qqqPath": "lines.procedureCode"
},
{
"sequence": 8,
"ediPath": "2400[*]/SV102",
"qqqPath": "lines.charge",
"transform": "DECIMAL",
"condition": "present(SV102)"
},
{
"sequence": 9,
"ediPath": "2010CA/NM103",
"qqqPath": "patientName",
"condition": "present(2010CA/NM103)"
}
]
}