Compare commits

..
Author SHA1 Message Date
james.maesandClaude Opus 5.5 6ddc8d5e50 docs: qbit-edi design spec
Trading partners and agreements, pluggable transports (AS2, SFTP via
qbit-file-gateway, SFTP push and VAN polling, HTTPS, email), StAEDI
parsing, implementation guides and maps as versioned data, inbound and
outbound flows with TA1/997/999, HIPAA PHI handling and SNIP levels,
database-and-S3-only payload storage, the app home, and phases a-g.

Owner decisions (2026-10-12): all recommendations accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:41:43 -05:00
james.maesandClaude Opus 5.5 f45e126a62 chore: bring the scaffold up to the QRun qbit repository standard
Replace the application template's example sources with the EDI producer
and config, and add the standard files: qbit-build-parent 2.0.0 POM on
QQQ 4.1.0-RC.5 with Checkstyle and the JaCoCo gate, Maven wrapper,
CircleCI qqq-orb 0.6.8 workflows, the release-candidate workflow,
Dependabot, community pointer files, CHANGELOG, CLAUDE.md/AGENTS.md and a
DESIGN.md placeholder for the spec PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:31:04 -05:00
41 changed files with 2506 additions and 1132 deletions

No files matched your search

+57 -11
View File
@@ -1,14 +1,60 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.8
qqq-orb: kingsrook/qqq-orb@0.6.8
commands:
configure-package-install:
steps:
- run:
name: Keep package installation unattended
command: |
# These build machines are discarded after the job.
sudo install -d /etc/needrestart/conf.d
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
workflows:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
ignore: /.*/
test_only:
jobs:
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
filters:
branches:
ignore: /(develop|release\/.*)/
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: snapshot
filters:
branches:
only: [develop]
publish_release_candidate:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release_candidate
filters:
branches:
only: [/release\/.*/]
publish_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release
filters:
branches:
ignore: /.*/
tags:
only: [/^v\d+\.\d+\.\d+$/]
+20
View File
@@ -0,0 +1,20 @@
# QRun-IO qbit .editorconfig: the Kingsrook layout (3-space indentation, never tabs).
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 3
tab_width = 3
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[*.{cmd,bat}]
end_of_line = crlf
+3
View File
@@ -0,0 +1,3 @@
* text=auto eol=lf
*.cmd text eol=crlf
mvnw text eol=lf
-24
View File
@@ -1,24 +0,0 @@
# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository.
name: qrun-release
on:
push: {}
workflow_dispatch: {}
permissions:
contents: read
actions: read
jobs:
release:
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-maven.yaml@qrun-release-v1
with:
runner: linux-small
browsers: true
browser-runner: browser-build
postpublish:
if: ${{ needs.release.outputs.capture == 'true' }}
needs: release
permissions:
contents: write
actions: write
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1
with:
runner: linux-small
+25
View File
@@ -0,0 +1,25 @@
# Weekly, grouped dependency updates. Minor and patch updates arrive as one PR per ecosystem;
# major updates stay separate so each gets its own review. At most three version-update PRs per
# ecosystem are open at once. Security updates are enabled in the repository settings and are
# not limited by this schedule.
version: 2
updates:
- package-ecosystem: maven
directory: /
target-branch: develop
schedule:
interval: weekly
open-pull-requests-limit: 3
groups:
maven-minor-and-patch:
update-types: [minor, patch]
- package-ecosystem: github-actions
directory: /
target-branch: develop
schedule:
interval: weekly
open-pull-requests-limit: 3
groups:
github-actions:
patterns: ['*']
-23
View File
@@ -1,23 +0,0 @@
name: CodeQL
'on':
push:
branches:
- main
- develop
- release/**
- hotfix/**
pull_request: {}
schedule:
- cron: 41 16 * * 0
workflow_dispatch: {}
permissions:
contents: read
actions: read
security-events: write
packages: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
+25
View File
@@ -0,0 +1,25 @@
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
# the release line and the release/* push runs CircleCI publish_release_candidate.
name: Release candidate
on:
workflow_dispatch:
inputs:
dry_run:
description: Print the plan without merging or creating branches
type: boolean
default: false
pull_request:
types: [closed]
branches: [develop]
permissions:
contents: write
jobs:
release-rc:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
+15 -25
View File
@@ -1,31 +1,21 @@
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
# IDE
.idea/
.env
.idea
*.iml
.project
.classpath
.settings/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Build
*.class
*.log
*.jar
*.war
*.nar
*.ear
# Logs
*.log
logs/
*.zip
*.tar.gz
*.rar
hs_err_pid*
.DS_Store
*.sw*
.flattened-pom.xml
dependency-reduced-pom.xml
/.env.local
/.cache/
.claude/settings.local.json
+3
View File
@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
-15
View File
@@ -1,15 +0,0 @@
profile: qbit
orb_version: '0.8'
default_branch: develop
java_version: '21'
browsers: true
api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: none
test_branch_policy: all
publish:
snapshot: false
release_candidate: false
release: false
asciidoc: false
+62
View File
@@ -0,0 +1,62 @@
# AGENTS.md
This file provides guidance to Codex when working with code in this repository.
## Project Overview
**QBit EDI**: a full EDI application qbit for QQQ. Trading partners and agreements, pluggable
transports (AS2, SFTP/FTPS, VAN, HTTPS, email), X12 (004010, 005010; EDIFACT later) parsing and
validation against data-driven implementation guides, 997/999/TA1, versioned maps to QQQ records,
inbound and outbound flows, and HIPAA documents with PHI classification. Design: `docs/DESIGN.md`.
## Build Commands
```bash
./mvnw compile
./mvnw test
./mvnw verify # Checkstyle, tests, JaCoCo: 70% instructions, 90% classes
```
Always use the wrapper (Maven 3.9.11); Maven 3.10 cannot read the parent POM.
## Architecture
Entry point: `EdiQBitProducer implements QBitMetaDataProducer<EdiQBitConfig>` in
`com.kingsrook.qbits.edi`. Component producers are scanned only in `com.kingsrook.qbits.edi.metadata`.
The package layout per phase is in `docs/DESIGN.md`.
## Invariants
- Data lives in standard visible QQQ tables with record screens, secured by permissions and record locks.
- Processes are table or record actions with plain labels, never in the nav; record actions use the
standard streamed-ETL Review/Result summary.
- One app home that owns the EDI data, built from typed widgets (no `RawHTML`, guardrail G5).
- Inbound transport endpoints are non-UI routes under `/qqq/hooks/edi/...`, never under `/qqq/v1`.
UI endpoints are spec'd `MiddlewareSpecExtension`s under `/qqq/v1/qbit/edi/...` with a committed fragment.
- Records are created and updated through normal QQQ actions as a service account.
- Raw documents and PHI fields are classified (`DataClassification`), stored encrypted.
- Secrets are referenced by environment-variable name only. No real PHI in tests or samples.
- No dependency on another qbit's internals (guardrail G1); notifications go through core `QAlerts`.
## Naming
- Tables: `edi_<name>`, lowercase snake_case; SQL names equal QQQ table names.
- Permissions: full name, `edi.*`.
## Code Style
- Copy the Apache header from `checkstyle/license.txt`; 3-space indent, Allman braces, `return (x);`.
- Flower-box Javadoc on every class and method; fluent `withX()` setters.
- Wrapper types in config and entities; JUnit 5 and AssertJ tests.
## Dependencies
Java 21; parent `com.kingsrook:qbit-build-parent:2.0.0`. Default-active `qqq-snapshot` imports QQQ
`4.1.0-RC.5` ahead of the parent's 4.0.0 BOM (ADR-0007); move to each new RC within a week (G7).
Middleware Javalin is optional. Planned: StAEDI and as2-lib (both Apache-2.0).
## Release
CircleCI qqq-orb 0.6.8: `develop` publishes SNAPSHOTs, `release/*` publishes RCs (orb version
bumps; cut by the `release-rc.yml` workflow), `vX.Y.Z` tags publish releases. No manual publishing.
Branches are `feature/*` or `fix/*` only; never push `hotfix/*` or `release/*`.
+10
View File
@@ -0,0 +1,10 @@
# Changelog
Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
- Repository scaffold to the QRun qbit repository standard: `qbit-build-parent` 2.0.0, QQQ 4.1.0-RC.5, Maven wrapper, Checkstyle, JaCoCo gate, CircleCI qqq-orb 0.6.8, release-candidate workflow, Dependabot, community files.
- `EdiQBitProducer` and `EdiQBitConfig` (table prefix `edi_`, permission base `edi`).
+54 -17
View File
@@ -1,25 +1,62 @@
# qbit-template-application
# CLAUDE.md
GitHub template repository for scaffolding Application QBits (complete mini-applications
with tables, processes, widgets, and UI navigation) on the QQQ platform.
This file provides guidance to Claude Code when working with code in this repository.
## QQQ 4.0 baseline — 2026-09-24
## Project Overview
The historical review below predates the 4.0 migration. This template targets Java 21 and the public QQQ 4.0.0 release from Maven Central. The corrected APIs, license references and actual template commands are in README.md and docs/00-getting-started.md. Preserve the documented multi-instance and scaffold limitations.
**QBit EDI**: a full EDI application qbit for QQQ. Trading partners and agreements, pluggable
transports (AS2, SFTP/FTPS, VAN, HTTPS, email), X12 (004010, 005010; EDIFACT later) parsing and
validation against data-driven implementation guides, 997/999/TA1, versioned maps to QQQ records,
inbound and outbound flows, and HIPAA documents with PHI classification. Design: `docs/DESIGN.md`.
## Knowledge base
## Build Commands
Reviewed dossier and platform knowledge live in the second-brain vault:
```bash
./mvnw compile
./mvnw test
./mvnw verify # Checkstyle, tests, JaCoCo: 70% instructions, 90% classes
```
- Hub: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/qqq-hub.md`
- This repo's dossier: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/repos/qbit-template-application.md`
- QBit mechanics refresher: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/architecture/metadata-model.md`
Always use the wrapper (Maven 3.9.11); Maven 3.10 cannot read the parent POM.
Reviewed at commit `fb9395ac0d07` (branch `main`, 2026-07-04).
## Architecture
Key caveat from that review: the scaffold in `src/main` references six qqq APIs that do not
exist in any qqq version (wrong packages for `MetaDataProducerInterface` /
`AbstractWidgetRenderer` / `RenderWidgetInput`, plus nonexistent `QInstance.addAppSection`,
`QProcessMetaData.withInputFields`, `QAppSection.withSourceQBitName`) — it has never compiled,
and CI on `main` is red. Verify against the qqq checkout (`R:/Git.Local/QRunIO/qqq`) before
trusting any API usage shown here.
Entry point: `EdiQBitProducer implements QBitMetaDataProducer<EdiQBitConfig>` in
`com.kingsrook.qbits.edi`. Component producers are scanned only in `com.kingsrook.qbits.edi.metadata`.
The package layout per phase is in `docs/DESIGN.md`.
## Invariants
- Data lives in standard visible QQQ tables with record screens, secured by permissions and record locks.
- Processes are table or record actions with plain labels, never in the nav; record actions use the
standard streamed-ETL Review/Result summary.
- One app home that owns the EDI data, built from typed widgets (no `RawHTML`, guardrail G5).
- Inbound transport endpoints are non-UI routes under `/qqq/hooks/edi/...`, never under `/qqq/v1`.
UI endpoints are spec'd `MiddlewareSpecExtension`s under `/qqq/v1/qbit/edi/...` with a committed fragment.
- Records are created and updated through normal QQQ actions as a service account.
- Raw documents and PHI fields are classified (`DataClassification`), stored encrypted.
- Secrets are referenced by environment-variable name only. No real PHI in tests or samples.
- No dependency on another qbit's internals (guardrail G1); notifications go through core `QAlerts`.
## Naming
- Tables: `edi_<name>`, lowercase snake_case; SQL names equal QQQ table names.
- Permissions: full name, `edi.*`.
## Code Style
- Copy the Apache header from `checkstyle/license.txt`; 3-space indent, Allman braces, `return (x);`.
- Flower-box Javadoc on every class and method; fluent `withX()` setters.
- Wrapper types in config and entities; JUnit 5 and AssertJ tests.
## Dependencies
Java 21; parent `com.kingsrook:qbit-build-parent:2.0.0`. Default-active `qqq-snapshot` imports QQQ
`4.1.0-RC.5` ahead of the parent's 4.0.0 BOM (ADR-0007); move to each new RC within a week (G7).
Middleware Javalin is optional. Planned: StAEDI and as2-lib (both Apache-2.0).
## Release
CircleCI qqq-orb 0.6.8: `develop` publishes SNAPSHOTs, `release/*` publishes RCs (orb version
bumps; cut by the `release-rc.yml` workflow), `vX.Y.Z` tags publish releases. No manual publishing.
Branches are `feature/*` or `fix/*` only; never push `hotfix/*` or `release/*`.
+3
View File
@@ -0,0 +1,3 @@
# Code of Conduct
See Project level [Code of Conduct](https://github.com/QRun-IO/qqq/blob/develop/CODE_OF_CONDUCT.md)
+3
View File
@@ -0,0 +1,3 @@
# Code Style Guide
* See Project [Code Style Guide](https://github.com/QRun-IO/qqq/blob/develop/CODE_STYLE.md)
+5
View File
@@ -0,0 +1,5 @@
# Contributing
Follow [QQQ contributing guidance](https://github.com/QRun-IO/qqq/blob/develop/CONTRIBUTING.md).
Use Java 21 and `./mvnw verify`, and follow local Checkstyle. Create feature branches from `develop` and target `develop` with PRs. See [AGENTS.md](AGENTS.md) for architecture, naming, security and release conventions, and [the design](https://github.com/QRun-IO/qqq/pull/1137) before extending the qbit.
+1 -1
View File
@@ -176,7 +176,7 @@
END OF TERMS AND CONDITIONS
Copyright 2022-2025 QRunIO, LLC
Copyright 2024 Kingsrook, LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+3 -3
View File
@@ -1,6 +1,6 @@
qBit Template - Application
Copyright 2022-2025 QRunIO, LLC
QBit EDI
Copyright 2026 Kingsrook, LLC
This product includes software developed at QRunIO, LLC (https://qrun.io/).
This product includes software developed at Kingsrook, LLC (https://kingsrook.com/).
Licensed under the Apache License, Version 2.0.
+51 -41
View File
@@ -1,55 +1,65 @@
# QBit Template - Application
# QBit EDI
Template repository for creating **Application QBits** - complete, self-contained applications or major functional modules for the QQQ framework.
A full EDI application for QQQ: trading partners and agreements, pluggable transports (AS2, SFTP
and FTPS, VAN mailboxes, HTTPS, email), X12 parsing and validation against data-driven
implementation guides, automatic 997/999/TA1 acknowledgements, versioned maps between EDI segments
and QQQ records, inbound and outbound flows with resend and reprocess, and HIPAA healthcare
documents with PHI classification. X12 004010 and 005010 first; EDIFACT is designed in and comes
later. The design is [docs/DESIGN.md](docs/DESIGN.md).
## When to Use This Template
## Status
Use this template when building:
- Complete mini-applications (WMS, OMS, CRM)
- Major functional modules with multiple tables
- QBits that require UI navigation (QAppSection)
- Self-contained features with business logic
Repository scaffold. The design spec PR and the phased build (partners and X12 core, retail, AS2,
logistics, healthcare, mapper UI, EDIFACT) follow; see the phase table in `docs/DESIGN.md`.
## Quick Start
## Requirements
Requires **Java 21**, **Maven 3.8+**, and QQQ **4.0.0**. Create a repository with **Use this template**, customize its Maven coordinates and Java package/classes, then run `mvn clean verify`. Follow the [Getting Started guide](docs/00-getting-started.md) for the full sequence.
- Java 21; QQQ **4.1.0-RC.5+**.
- A configured QQQ backend for the qbit's `edi_` tables.
## Structure
## Install
```
src/main/java/com/kingsrook/qbits/example/
├── ExampleAppQBitConfig.java
├── ExampleAppQBitProducer.java
├── model/
│ ├── ExampleEntity.java
│ └── ExampleChildEntity.java
├── processes/
│ ├── ExampleProcessStep.java
│ └── ExampleProcessMetaDataProducer.java
└── widgets/
├── ExampleDashboardWidgetRenderer.java
└── ExampleDashboardWidgetMetaDataProducer.java
Use `com.kingsrook.qbits:qbit-edi:0.1.0-SNAPSHOT` once published, or build locally. After adding
your backend to the instance, install the producer:
```java
import com.kingsrook.qbits.edi.EdiQBitConfig;
import com.kingsrook.qbits.edi.EdiQBitProducer;
new EdiQBitProducer()
.withQBitConfig(new EdiQBitConfig()
.withBackendName("rdbms"))
.produce(qInstance);
```
## Key Characteristics
The producer scans only `com.kingsrook.qbits.edi.metadata` for component producers. Set
`withParentAppName("admin")` to nest the EDI app under an existing parent app.
| Feature | Description |
|---------|-------------|
| QAppSection | Required - provides UI navigation |
| Tables | Multiple interconnected business tables |
| Processes | Full CRUD with business logic |
| Widgets | Dashboard components |
| Security | Permission key support |
| Integration | Hooks for other QBits |
## Tables and permissions
## Documentation
Tables are lowercase snake_case prefixed `edi_`, and the SQL table name equals the QQQ table name.
Permissions use the full name (`edi.*`). The table list arrives with phase (a); see
`docs/DESIGN.md`.
- [Getting Started](docs/00-getting-started.md)
- [Tables and Relationships](docs/01-tables-and-relationships.md)
- [Processes](docs/02-processes.md)
- [Widgets and UI](docs/03-widgets-and-ui.md)
## Configuration
## See Also
Code configuration (`EdiQBitConfig`) holds only the backend and app placement. Partners,
agreements, transports, implementation guides and maps are data in the `edi_` tables. Secrets
(AS2 keys, SFTP credentials) are referenced by environment-variable name, never stored.
- [qbit-template-extension](https://github.com/QRun-IO/qbit-template-extension) - For infrastructure extensions
- [qbit-template-data](https://github.com/QRun-IO/qbit-template-data) - For reference data providers
## Adopting in another app
Add the dependency, install the producer as above, and grant the `edi.*` permissions to the roles
that operate EDI. Inbound transports register non-UI routes under `/qqq/hooks/edi/...`.
## Build
```bash
./mvnw verify # Checkstyle, tests, JaCoCo gate (70% instructions, 90% classes)
```
Always use the wrapper (Maven 3.9.11); Maven 3.10 cannot read the parent POM.
## License
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
+4
View File
@@ -0,0 +1,4 @@
# Security Policy
* See Project [Security Policy](https://github.com/QRun-IO/qqq/blob/develop/SECURITY.md)
* Report a vulnerability privately through this repository's **Security** tab ("Report a vulnerability").
+246
View File
@@ -0,0 +1,246 @@
<?xml version="1.0"?>
<!--
~ QQQ - Low-code Application Framework for Engineers.
~ Copyright (C) 2021-2022. 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.
-->
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd">
<!--
See reference at https://checkstyle.sourceforge.io/checks.html
-->
<module name = "Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.sf.net/config_whitespace.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<!--
<module name="LineLength">
<property name="max" value="100"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
-->
<module name="TreeWalker">
<module name="SuppressWarningsHolder"/>
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format" value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message" value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens" value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="NeedBraces"/>
<module name="LeftCurly">
<property name="option" value="nl"/>
</module>
<module name="RightCurly">
<property name="option" value="alone"/>
</module>
<!--
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
-->
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[_a-zA-Z]([a-z0-9A-Z][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-zA-Z]([a-z0-9A-Z][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-zA-Z]([a-z0-9A-Z][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="tokens" value="VARIABLE_DEF"/>
<property name="format" value="^[a-zA-Z]([a-z0-9A-Z][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][_A-Z0-9]*$)"/>
<message key="name.invalidPattern" value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][_A-Z0-9]*$)"/>
<message key="name.invalidPattern" value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][_A-Z0-9]*$)"/>
<message key="name.invalidPattern" value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed" value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded" value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow" value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded" value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="3"/>
<property name="braceAdjustment" value="0"/>
<property name="caseIndent" value="3"/>
<property name="throwsIndent" value="6"/>
<property name="lineWrappingIndentation" value="3"/>
<property name="arrayInitIndent" value="6"/>
</module>
<!--
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
</module>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
</module>
-->
<module name="CustomImportOrder">
<property name="customImportOrderRules" value="SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE###THIRD_PARTY_PACKAGE###STATIC"/>
<property name="specialImportsRegExp" value="^javax\."/>
<property name="standardPackageRegExp" value="^java\."/>
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="false"/>
</module>
<module name="MethodParamPad"/>
<module name="NoWhitespaceBefore">
<property name="tokens" value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad"/>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens" value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="JavadocMethod">
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
</module>
<module name="MissingJavadocMethod">
<property name="scope" value="private"/>
</module>
<module name="MissingJavadocType">
<property name="scope" value="private"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation"/>
<!-- this would be nice, but requires the the @inheritDoc javadoc tag... -->
<module name="MissingOverride"/>
</module>
<module name="Header">
<property name="headerFile" value="checkstyle/license.txt"/>
<property name="fileExtensions" value="java"/>
<property name="ignoreLines" value="3"/>
</module>
<module name="SuppressWarningsFilter"/>
</module>
+19
View File
@@ -0,0 +1,19 @@
/*
* 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.
*/
-30
View File
@@ -1,30 +0,0 @@
# Getting Started
Targets Java 21, Maven 3.8 or later, and QQQ 4.0.0. Maven resolves the framework dependencies from Maven Central.
1. Choose **Use this template** on GitHub, then clone the repository you created.
2. In `pom.xml`, set your own `groupId`, `artifactId`, name, description, and version.
3. Use your IDE's package refactoring to rename `com.kingsrook.qbits.example` throughout `src/`. Rename the `Example*` classes and update their imports and references with the IDE's rename refactoring.
4. Update the QBit producer's `GROUP_ID`, `ARTIFACT_ID`, and `VERSION` constants to match your project. Give the metadata names their own stable names before combining the QBit with other examples.
5. Build the generated project:
```bash
mvn clean verify
```
The repository provides example Java sources; customization uses normal package/class refactoring. Keep example coordinates unpublished. After customizing, add tests for your QBit's behavior before setting up its publishing workflow.
## Register with a host application
The following uses the original example names; substitute the names chosen above:
```java
new ExampleAppQBitProducer()
.withConfig(new ExampleAppQBitConfig()
.withBackendName("rdbms"))
.produce(qInstance, "my-application");
```
Register the configured backend in the host first. If you add table prefixes or multiple QBit instances, update related table references, possible-value-source names, and process/widget names consistently. A table prefix alone does not namespace all metadata.
[Tables and Relationships](01-tables-and-relationships.md) describes the model; the `processes/` and `widgets/` directories contain the remaining examples.
-50
View File
@@ -1,50 +0,0 @@
# Tables and Relationships
Application QBits typically have multiple interconnected tables representing a business domain.
## Entity Design
Use `@QMetaDataProducingEntity` for automatic metadata generation:
```java
@QMetaDataProducingEntity(producePossibleValueSource = true)
public class Order extends QRecordEntity
{
public static final String TABLE_NAME = "order";
@QField(isPrimaryKey = true)
private Integer id;
@QField(isRequired = true, possibleValueSourceName = "customer")
private Integer customerId;
@QField(isRequired = true)
private String status;
}
```
## Relationships
Define foreign keys using `possibleValueSourceName`:
```java
@QField(isRequired = true, possibleValueSourceName = "exampleEntity")
private Integer exampleEntityId;
```
## Table Prefixing
Application QBits support optional table prefixing:
```java
// Without prefix: order, orderLine
// With prefix "sales": sales_order, sales_orderLine
new MyAppQBitProducer()
.withConfig(new MyAppQBitConfig()
.withBackendName("rdbms")
.withTableNamePrefix("sales"))
.produce(qInstance, "sales-app");
```
The producer applies the prefix to table names. When extending this pattern, also update possible-value-source names, relationship targets, process tables, and navigation references; table renaming alone does not provide complete multi-instance support.
-7
View File
@@ -1,7 +0,0 @@
# Processes
Start with [ExampleProcessMetaDataProducer](../src/main/java/com/kingsrook/qbits/example/processes/ExampleProcessMetaDataProducer.java) and [ExampleProcessStep](../src/main/java/com/kingsrook/qbits/example/processes/ExampleProcessStep.java). The producer defines the process, its table, inputs, and backend step; the step transforms `PENDING` records to `PROCESSED` and returns them in the output.
In QQQ 4.0, import `MetaDataProducerInterface` from `com.kingsrook.qqq.backend.core.model.metadata`. Define process inputs through `withInputMetaData(new QFunctionInputMetaData().withField(...))`. An `AbstractTransformStep` must implement both `runOnePage(RunBackendStepInput, RunBackendStepOutput)` and `getProcessSummary(RunBackendStepOutput, boolean)`; keep the example's implementations when renaming it.
To make the process discoverable in navigation, add its registered name to a `QAppSection` through `withProcesses(List.of(ExampleProcessMetaDataProducer.NAME))`, then include the section in a `QAppMetaData` registered with `qInstance.addApp(...)`. Use the actual registered name if you introduce a prefix. Add tests for record changes and failure cases when replacing the example business logic.
-23
View File
@@ -1,23 +0,0 @@
# Widgets and UI
The application producer registers a `QAppMetaData` containing a `QAppSection`. The section references metadata names already registered in the instance; it is not registered directly on `QInstance`.
```java
QAppSection section = new QAppSection()
.withName("exampleSection")
.withLabel("Example Application")
.withTables(List.of(ExampleEntity.TABLE_NAME, ExampleChildEntity.TABLE_NAME))
.withProcesses(List.of(ExampleProcessMetaDataProducer.NAME))
.withWidgets(List.of(ExampleDashboardWidgetMetaDataProducer.NAME));
qInstance.addApp(new QAppMetaData()
.withName("exampleApp")
.withLabel("Example Application")
.withSection(section));
```
Use [ExampleAppQBitProducer](../src/main/java/com/kingsrook/qbits/example/ExampleAppQBitProducer.java) as the starting point. Keep these references consistent when enabling/disabling tables or introducing prefixes.
The widget has two parts: [its metadata producer](../src/main/java/com/kingsrook/qbits/example/widgets/ExampleDashboardWidgetMetaDataProducer.java) declares a statistics widget and its renderer, and [its renderer](../src/main/java/com/kingsrook/qbits/example/widgets/ExampleDashboardWidgetRenderer.java) returns a `RenderWidgetOutput` containing `StatisticsData`. Replace the example count with a query or calculation. Choose a widget type that matches the returned data model.
For QQQ 4.0, `AbstractWidgetRenderer` is in `com.kingsrook.qqq.backend.core.actions.dashboard.widgets`; `RenderWidgetInput` and `RenderWidgetOutput` are in `com.kingsrook.qqq.backend.core.model.actions.widgets`.
+858
View File
@@ -0,0 +1,858 @@
# QBit EDI: design
Status: **Approved** (owner, 2026-10-13: spec first, then build) · drafted 2026-10-13 · Author:
James Maes (drafted with Claude)
`QRun-IO/qbit-edi` (Apache-2.0) is a full EDI application qbit for QQQ. It handles the transports
(AS2, SFTP, VAN mailboxes, HTTPS, email), carries the EDI documents and their implementation guides
as data, runs inbound and outbound flows with 997, 999 and TA1 acknowledgements, and lets each
trading partner's customizations be expressed as versioned data rather than code.
## Owner request (verbatim)
> For EDI, i guess that would be more of a normal application qbit that would just handle all of
> the transport methods (AS2, etc) and then have all of the edi documents built in, with handling
> for edi flows, 997s etc and handling customizations of them, etc.
## Owner decisions
> **Owner decisions (2026-10-12): all recommendations accepted.** Every open question in section 17
> is decided as this spec recommends ("they are approved. we will adjust as needed once we can view
> them working").
**Owner decisions (2026-10-13):**
- Spec first, then build, in the phases of section 16.
- **First documents (X12):**
- retail and supply chain: 850, 855, 856, 810, 846, 860;
- warehouse and logistics: 940, 945, 214, 204;
- healthcare (HIPAA): 837 (P and I), 835, 270/271, 276/277, 834;
- acknowledgements: 997, 999, TA1.
- **Versions:** X12 004010 and 005010, with the 005010 HIPAA TR3s for healthcare. EDIFACT is
designed in now and built last.
- **No files or filesystem: just the database and S3** ("there are no files or filesystem - just
the database and s3"). Raw EDI documents, AS2 messages and MDNs, and certificates live in object
storage and the database, never on local disk. Private keys come from secrets (environment
variables). Every service is stateless, so any replica can process any message.
- **Inbound SFTP** comes through qbit-file-gateway (SFTP only in v1, no FTPS, owner decision),
using its published `api` package (section 5.3).
- **No kof22 adoption.** kof22 does not need EDI. The demo runs in `qqq-sample-project` (section
15).
## Mandatory standards that shape this design
- Data lives in standard, visible QQQ tables with full record screens, secured by permissions and
record locks. Tables are `edi_*`; permissions use the full name `edi.*`.
- Processes are table or record actions with plain labels, never in the nav. Record actions use the
standard streamed-ETL Review and Result summary lines; no custom dry-run forms.
- One app home that owns the EDI data, built from typed widgets (no `RawHTML`, guardrail G5). The
nav lists only the main tables; sub-tables are child lists on their parents.
- The API matches the admin. Main tables publish ESB table events and are searchable in Quick
Search.
- UI endpoints are spec'd `MiddlewareSpecExtension`s under `/qqq/v1/qbit/edi/...` with a committed
fragment and a contract test (G3). Machine endpoints (AS2, HTTPS receive, MDNs) are non-UI routes
under `/qqq/hooks/edi/...`.
- No dependency on another qbit's internals (G1). EDI depends only on the published `api` packages
of qbit-object-storage and qbit-file-gateway, both optional. Alerts and notifications go through
core `QAlerts`.
- No stand-in without `STAND-IN(core=..., removeBy=...)` (G4). Pinned core baseline, no SNAPSHOT on
`develop` (G7).
- Readable at all widths, light and dark. Real data only on the home.
## 1. Goals and non-goals
**Goals**
1. Exchange X12 with any trading partner over AS2, SFTP, a VAN mailbox, HTTPS or email, configured
as data per partner.
2. Never lose a document: store the raw bytes first (encrypted), then process from a durable inbox
with retries and a dead letter. Every step is idempotent and replayable.
3. Validate against implementation guides that are data (segments, elements, usage, conditional
rules, code lists), per standard and version, with versioned per-partner overrides.
4. Acknowledge automatically (TA1, 997, 999), and track the acknowledgements we expect from
partners, alerting when they are overdue.
5. Map documents to and from QQQ records with versioned, data-driven maps, and apply inbound
documents through normal QQQ actions as a service account, so table permissions, validation,
customizers, audit and ESB events all apply.
6. Make operations visible: a document timeline, search by business keys (PO, ASN, invoice and
claim numbers), an exception work queue, resend and reprocess, and a home showing volumes,
overdue acks and errors.
7. Carry PHI safely: classify healthcare content as PHI so the secured-data tiers and the canary
egress tests apply, and encrypt raw documents at rest.
**Non-goals (v1)**
- A VAN service of our own. We connect to VANs as mailboxes.
- Business-level responses beyond acknowledgements (824 Application Advice, 864 text). The model
leaves room for them (section 9.6).
- Shipping licensed X12 or TR3 text (section 7.4).
- CAQH CORE real-time 270/271 (SOAP and MIME envelope). Batch 270/271 is in scope; CORE
connectivity is a later transport plugin (section 5.6).
## 2. Architecture
```
partner ──AS2──► /qqq/hooks/edi/as2/{transport} ─┐
partner ──SFTP─► qbit-file-gateway INBOX ("edi") ─┤
VAN mailbox ◄─poll── SFTP poller (scheduled) ─┤ 1. store raw bytes (object storage, encrypted)
partner ──HTTPS► /qqq/hooks/edi/https/{transport} ─┤ 2. insert edi_inbox row (PENDING), return
email (qbit-email inbound, later) ─┘ (MDN / 200 / FileAcceptance.accepted)
│
edi_inbox worker (scheduled, claims rows, retries, dead letter)
▼
split interchanges ─► envelope check, TA1 ─► partner + agreement lookup ─► duplicate check
─► per ST: guide validation (StAEDI + guide rules) ─► 997/999 ─► map ─► apply as service account
─► statuses, timeline, exceptions (edi_exception) ─► ack reconciliation for inbound 997/999/TA1
host record change ─► ESB table event / record-change listener ─► edi_outbound_request (same tx)
─► outbound worker: map ─► guide validate ─► envelope (control numbers) ─► store raw
─► edi_outbound_message ─► transport send (AS2 / SFTP push / gateway OUTBOX / HTTPS / email)
─► MDN or delivery ─► expected ack due date ─► 997/999 reconcile or overdue alert
```
Package layout (`com.kingsrook.qbits.edi`):
| Package | Holds |
|---|---|
| (root) | `EdiQBitProducer`, `EdiQBitConfig`, `EdiRuntime` (the configured services) |
| `metadata` | the only scanned package: tables, PVS, joins, widgets, processes, app |
| `model` | table entities (`EdiPartner`, `EdiAgreement`, ...) |
| `x12` | StAEDI reading and writing, envelopes, separators, control numbers, splitting |
| `guide` | guide model, compiler to StAEDI schema, rule engine, SNIP levels, built-in guide loader |
| `ack` | 997, 999 and TA1 builders, inbound ack reconciliation, overdue monitor |
| `map` | map model, path language, transforms, inbound apply, outbound build, sandbox preview |
| `flow` | inbox and outbound workers, state machines, reprocess and resend |
| `transport` | `EdiTransportPlugin` SPI and the plugins: `as2`, `sftp`, `gateway`, `https`, `email` |
| `storage` | `PayloadStore` (object storage with envelope encryption; database fallback) |
| `security` | secrets by environment-variable name, certificates, classification, service session |
| `hooks` | the `/qqq/hooks/edi/...` route provider |
| `uiapi` | spec'd `/qqq/v1/qbit/edi/...` extension (phase f) |
| `edifact` | EDIFACT envelopes and CONTRL (phase g) |
## 3. Platform and dependencies
| Dependency | Scope | Why |
|---|---|---|
| QQQ `4.1.0-RC.5` (then each new RC, G7) | compile | core, `QAlerts.raise/resolve` (RC.5) |
| `qqq-middleware-javalin` | optional | hooks route provider; UI API extension (RC.6) |
| `qqq-esb` | optional | outbound trigger from ESB table events |
| `qqq-backend-module-filesystem` (SFTP) | compile | SFTP push and VAN polling. This is core's SFTP client, the one qbit-sftp-data-integration uses; EDI uses it directly instead of depending on that qbit (G1). Remote SFTP only; nothing local. |
| `io.xlate:staedi` 1.26.x (Apache-2.0) | compile | streaming X12 and EDIFACT reader and writer with schema validation |
| `com.helger.as2:as2-lib` 5.1.x (Apache-2.0) | compile | AS2 send, receive, MDN, S/MIME via BouncyCastle |
| `qbit-object-storage` (its `api` package only) | optional | raw payload storage (section 10) |
| `qbit-file-gateway` (its `api` package only) | optional | inbound SFTP and partner-pull OUTBOX (section 5.3) |
Core features that arrive in RC.6, and how EDI bridges until then:
| Need | Core | Until RC.6 |
|---|---|---|
| `DataClassification` on fields (PHI) | `actions.ai.DataClassification` (#1142) | `EdiClassifications` stand-in, `STAND-IN(core=QRun-IO/qqq#1142, removeBy=4.1.0-RC.6)`, sets the classification reflectively when the class is present |
| User notifications | `QAlerts.notify` (#1125) | alerts only via `QAlerts.raise` (RC.5); notifications added with the RC.6 bump |
| UI API extension point | `MiddlewareSpecExtension` (#1128) | the sandbox is a record action (section 8.3); spec'd endpoints come in phase f |
The file-gateway `api` ships in that qbit's phase (a). Until it is on a published SNAPSHOT, EDI uses
`GatewayStandIn` with the identical shape, marked `STAND-IN(core=QRun-IO/qbit-file-gateway,
removeBy=qbit-file-gateway 0.1.0-RC.1)`.
## 4. Trading partners and agreements
### 4.1 Tables
All tables have `id`, `createDate`, `modifyDate` and, where people edit them, `modifiedBy`; audit is
on (field-level for configuration tables); record screens have sections. SQL table names equal QQQ
table names; fields are camelCase in QQQ and snake_case in SQL. Labels in brackets.
**`edi_partner`** [Trading Partner] (main, nav, Quick Search, ESB events)
- `name` (unique), `code` (short slug, unique), `status` (TESTING, ACTIVE, SUSPENDED, RETIRED),
`contactName`, `contactEmail`, `contactPhone`, `notes`.
- `defaultTimezone` (for date transforms), `containsPhi` (boolean; set for healthcare partners,
drives classification defaults and the home's PHI badge).
- Children: identities, agreements, transports, certificates, documents, exceptions.
**`edi_identity`** [EDI Identity] (child of partner; "ours" when `partnerId` is null)
- `partnerId` (null = one of our own identities), `standard` (X12, EDIFACT),
`interchangeQualifier` (ISA05/07, e.g. `ZZ`, `01`, `12`; EDIFACT UNB 0007),
`interchangeId` (ISA06/08, padded to 15 on write; UNB 0004/0010), `applicationCode` (GS02/03),
`description`, `isActive`.
- Unique (standard, interchangeQualifier, interchangeId, applicationCode). Inbound routing matches
on these.
**`edi_agreement`** [Agreement] (main, nav; child list on partner)
One agreement is one (our identity, partner identity, standard, usage) combination.
- `partnerId`, `localIdentityId`, `partnerIdentityId`, `standard` (X12, EDIFACT), `usageIndicator`
(T test, P production; ISA15). Test and production are separate agreements, so test traffic never
touches production records unless the agreement says so.
- `interchangeVersion` (ISA12: `00401`, `00501`), `elementSeparator` (default `*`),
`segmentTerminator` (default `~`), `componentSeparator` (ISA16, default `:`),
`repetitionSeparator` (ISA11 for 00501, default `^`), `lineEnding` (NONE, LF, CRLF; outbound).
Inbound separators are always read from the ISA itself; these settings are for outbound.
- `requestTa1` (ISA14), `sendTa1` (NEVER, ON_REQUEST, ON_ERROR, ALWAYS; default: on request and on
error).
- `inboundTransportId`, `outboundTransportId` (section 5).
- `controlNumberScope` (AGREEMENT, LOCAL_IDENTITY; default AGREEMENT; section 4.3).
- `duplicateWindowDays` (default 365), `onDuplicate` (REJECT_TA1, IGNORE, PROCESS; default
REJECT_TA1 with an exception row).
- `maxInterchangeBytes` (default 50 MB), `applyTestDocuments` (boolean, default false: T documents
are validated and acknowledged but not applied).
- `isActive`, `notes`.
**`edi_agreement_document`** [Agreement Document] (child of agreement)
One row per document, version and direction that the agreement carries.
- `agreementId`, `direction` (INBOUND, OUTBOUND), `transactionSet` (`850`, `837`, ...),
`functionalIdCode` (GS01: PO, PR, SH, IN, IB, PC, OW, SW, QM, SM, HC, HP, HS, HB, HR, HN, BE, FA),
`version` (GS08: `004010`, `005010`, `005010X222A1`, ...), `guideId` (a base or override guide),
`mapId`, `ackExpectation` (NONE, 997, 999), `ackDueMinutes` (default 1440), `ackLevel`
(GROUP_ONLY, ALL_TRANSACTIONS, ERRORS_ONLY; for the acks we send), `snipLevel` (1-7; section 11;
default 2 for non-HIPAA, 4 for HIPAA), `containsPhi` (defaulted from the version),
`targetTableName` (inbound) or `sourceTableName` (outbound), `outboundTrigger` and
`triggerFilter` (section 9.3), `batchMode` (IMMEDIATE, BATCH; outbound), `batchSchedule` (cron,
when BATCH), `isActive`.
- Unique (agreementId, direction, transactionSet, version).
**`edi_control_number`** [Control Number] (child of agreement)
- `agreementId` (or `localIdentityId` when the scope is LOCAL_IDENTITY), `level` (ISA, GS),
`lastValue`, `minValue` (1), `maxValue` (999999999), `wrapAllowed` (default true).
- ST02 is assigned per group (0001 upward), so it needs no row.
### 4.2 Inbound routing
1. Read the ISA: 106 fixed characters; the element separator is character 4, the repetition
separator ISA11 (00501), the component separator ISA16 and the segment terminator character 106.
2. Find the partner identity by (ISA05, ISA06) and our identity by (ISA07, ISA08), then the agreement
by both plus ISA15. No match: an `UNKNOWN_PARTNER` exception, and TA1 code `006` or `007` when
we can address a reply. The raw document is always kept.
3. Per GS, match GS02/GS03 when application codes are configured; per ST, find the agreement
document by direction INBOUND, ST01 and GS08 (exact, then by the base version `005010`). No
match: the group is rejected in the 997/999 (AK5/IK5 `R`, "transaction set not supported") and a
`NOT_CONFIGURED` exception is opened.
### 4.3 Control numbers
- Allocation is an atomic compare-and-set on `edi_control_number` (`UPDATE ... SET last_value = n+1
WHERE id = ? AND last_value = n`, retried) in its own short transaction, so concurrent replicas
never hand out the same number. Past `maxValue` it restarts at `minValue` when wrapping is
allowed; otherwise the outbound fails with a plain error.
- Numbers are allocated when an interchange is enveloped, not when it is sent, so a resend of the
same interchange keeps its numbers and the partner can detect the duplicate. "Resend As New"
re-envelopes with new numbers (section 9.5).
- Inbound duplicate detection per agreement, within `duplicateWindowDays`:
- interchange: the same (sender, receiver, ISA13, ISA15). TA1 note code `025`.
- group: the same GS06 from the same sender. AK9/IK9 `R`.
- transaction: the same ST02 within the group. AK5/IK5 `R`.
- Byte-identical payloads (the same sha256 within the window) are caught earlier, at the inbox,
and marked `DUPLICATE` without processing.
### 4.4 Acknowledgement expectations and overdue alerts
- Each outbound functional group whose agreement document expects a 997 or 999 gets `ackDueAt`
(sent time + `ackDueMinutes`).
- The overdue monitor (a scheduled process, every 5 minutes) calls `QAlerts.raise(AlertSignal)`
with alert type `edi_ack_overdue`, severity WARNING, dedup key `edi_functional_group:<id>`, a
record reference to the group and a link to the interchange. qbit-alerts delivers it (inbox,
channels, quiet hours); without qbit-alerts, core's logging sink records it.
- When the ack arrives, `QAlerts.resolve("edi_ack_overdue", dedupKey, note)`.
- A rejecting ack (AK9/IK9 `R`, or partial `P`) raises `edi_ack_rejected` (ERROR) and opens a
`REJECTED_ACK` exception.
- From RC.6, assignees of exceptions also get a `QAlerts.notify` notification.
## 5. Transports
### 5.1 Model
Transports are data. **`edi_transport`** [Transport] (main, nav):
- `name` (unique slug, used in hook URLs), `type`, `direction` (INBOUND, OUTBOUND, BOTH),
`partnerId` (null for a shared transport such as a VAN), `isActive`, `notes`.
- Typed settings in sections. Only the section for `type` is shown and validated (a pre-insert and
pre-update customizer calls the plugin's `validate`):
- **AS2:** `as2LocalId`, `as2PartnerId`, `as2Url`, `as2Subject`, `signAlgorithm` (SHA256, SHA384,
SHA512; SHA1 only when the partner requires it, flagged), `encryptAlgorithm` (AES128_CBC,
AES256_CBC, AES128_GCM, AES256_GCM; 3DES only when required, flagged), `compress`, `mdnMode`
(SYNC, ASYNC, NONE), `mdnSigned`, `localCertificateId`, `partnerCertificateId`,
`partnerTlsCertificateId` (optional pinning), `httpAuthUserEnvVar`, `httpAuthPasswordEnvVar`.
- **SFTP (push and poll):** `sftpHost`, `sftpPort`, `sftpUsername`, `sftpPasswordEnvVar` or
`sftpPrivateKeyEnvVar`, `sftpHostKeyFingerprint` (required; unknown host keys are refused),
`sftpOutboundPath`, `sftpInboundPath`, `sftpPollCron`, `sftpAfterPickup` (DELETE, MOVE),
`sftpArchivePath`, `sftpFilePattern`, `sftpTempSuffix` (upload as `.tmp`, then rename).
- **File gateway:** `gatewayEndpointName` (the `file_gateway_alias.name`: an INBOX endpoint for
inbound, an OUTBOX endpoint for partner-pull outbound).
- **HTTPS:** `httpsUrl`, `httpsAuthType` (NONE, BASIC, BEARER, HMAC, MTLS), `httpsSecretEnvVar`,
`httpsHeadersJson` (non-secret headers), `httpsInboundAuth` (for `/qqq/hooks/edi/https/{name}`:
BASIC, BEARER or HMAC, the secret by env var).
- **Email:** `emailProviderName` (qbit-email), `emailTo`, `emailFromAlias`,
`emailAttachmentName`.
- **VAN:** an AS2 or SFTP transport with `isVan` true and `vanName`. One VAN transport serves many
partners (`partnerId` null); routing is by ISA identities, never by transport.
- Retry policy: `maxAttempts` (default 8), backoff 1m, 5m, 15m, 1h, 3h, 6h, 12h, 24h.
**`edi_certificate`** [Certificate] (main, nav)
- `alias`, `partnerId` (null = ours), `usage` (SIGNING, ENCRYPTION, SIGNING_AND_ENCRYPTION, TLS),
`certificatePem` (the public certificate only), `subject`, `issuer`, `serialNumber`,
`fingerprintSha256`, `notBefore`, `notAfter` (all parsed from the PEM on save),
`privateKeyEnvVar` (ours only: the env var holding a PEM or base64 PKCS#12),
`privateKeyPasswordEnvVar`, `status` (ACTIVE, NEXT, RETIRED). `NEXT` supports rollover: inbound
accepts either key, outbound switches when "Promote Certificate" runs.
- Expiry: the monitor raises `edi_certificate_expiring` at 30, 14 and 7 days (dedup per
certificate and threshold), and `edi_certificate_expired` at expiry.
- Record actions: "Promote Certificate", "Export Public Certificate" (download the PEM to send to a
partner). Private keys never enter a table, a log or a response.
### 5.2 Plugin SPI
```java
public interface EdiTransportPlugin
{
String getType(); // "AS2", "SFTP", "GATEWAY", "HTTPS", "EMAIL"
void validate(EdiTransport transport, List<String> errors); // settings check on save
EdiSendResult send(EdiOutboundMessage message, PayloadRef payload, EdiTransport transport) throws EdiTransportException;
default void poll(EdiTransport transport, InboundSink sink) throws EdiTransportException { } // pull transports
default EdiTestResult test(EdiTransport transport) { ... } // the "Test Connection" record action
}
```
- Plugins register in `EdiTransportPluginsMetaData` (a `QSupplementalInstanceMetaData`). The
producer registers the built-in ones; a host can add its own (e.g. a VAN's proprietary API).
- `EdiSendResult`: SENT (with the transport message id), DELIVERED (sync MDN, or HTTP 2xx where the
transport confirms delivery), RETRY (with a reason), FAILED (permanent, with a reason).
- `InboundSink.receive(InboundPayload)` stores the bytes and inserts the inbox row (section 9.1).
Every inbound path, push or pull, ends in this one call.
- **Secrets:** settings name environment variables (`...EnvVar`). `EdiSecrets.resolve(name)` reads
them at use time; values are never stored, logged, cached beyond the call, or returned by any
API. Tests replace the resolver. A missing variable fails "Test Connection" and the send with
"environment variable X is not set" (the name only).
### 5.3 SFTP through qbit-file-gateway (inbound) and partner pull (outbound)
Settled with the qbit-file-gateway agent (its DESIGN.md section "Consumer and outbox API"):
- EDI registers `EdiFileGatewayConsumer` (name `edi`) with
`FileGatewayConsumersMetaData.ofOrWithNew(qInstance).withConsumer(new QCodeReference(...))`. An
INBOX endpoint (a `file_gateway_alias` row with `consumer_name = edi`) delivers each uploaded
file, at least once, after upload close and scan.
- `accept(InboundFile)` runs on the gateway worker as the SFTP account's user (channel `sftp`).
`InboundFile` carries `uploadJobId`, `endpointName`, `username`, `userId`, `remoteAddress`,
`path`, `fileName`, `sizeBytes`, `sha256`, `receivedAt`, `storageBucketAlias`, `storageKey` and
`openStream()` (always from object storage). EDI streams the bytes into its own encrypted payload
store, inserts an `edi_inbox` row keyed by (`GATEWAY`, `uploadJobId`) with the sha256, and returns
`FileAcceptance.accepted("edi_inbox:<id>")`; a storage failure returns `retryLater(reason,
duration)`. It never parses inside `accept()`.
- The inbox row records the endpoint, username, user id, file name and remote address. The partner
is still determined from the ISA. When the SFTP account is linked to a partner and the ISA sender
is a different partner, processing stops with a `PARTNER_MISMATCH` exception, so one partner
cannot send as another.
- Outbound to partners that pull from us: `FileGatewayOutbox.put(endpointName, fileName, stream,
sizeBytes)` writes to the partner's OUTBOX endpoint and returns an `OutboxFileRef`;
`onDownloaded(OutboxFileRef)` on our consumer marks the outbound message DELIVERED.
`FileGatewayOutbox.isAvailable()` false makes a GATEWAY transport fail validation with
"qbit-file-gateway is not installed".
### 5.4 SFTP push and VAN polling (remote servers)
For partners and VANs that host their own SFTP, EDI connects out with core's SFTP client
(`qqq-backend-module-filesystem`):
- **Push:** write `sftpOutboundPath/<fileName>.tmp`, then rename. File names follow a configurable
pattern, default `<partnerCode>_<transactionSet>_<isa13>_<yyyyMMddHHmmss>.edi`.
- **Poll:** a scheduled process per transport lists `sftpInboundPath` matching `sftpFilePattern`,
streams each file straight into `InboundSink` (no local file), and deletes or moves it only after
the inbox row commits. A crash between commit and delete re-reads the file on the next poll; the
inbox's sha256 dedupe absorbs it.
- qbit-sftp-data-integration stays the tool for generic file imports and report exports. EDI does
not depend on it (G1) but uses the same core client, so behaviour and host-key handling match.
### 5.5 AS2 (phase c)
Built on as2-lib (Apache-2.0):
- **Send:** `AS2Client` with partnership settings built from `edi_transport` and `edi_certificate`
(a custom partnership factory and certificate factory backed by our tables, keys from env vars, no
keystore file). Sign, then encrypt, optional compression; the `Message-ID` is recorded. Sync MDN:
verify the signature and the MIC against the sent content; a mismatch or a failed disposition
fails the send. Async MDN: `Receipt-Delivery-Option` is
`https://<host>/qqq/hooks/edi/as2/{transport}/mdn`; the message stays SENT until the MDN arrives,
then becomes DELIVERED or FAILED. An MDN not received within `ackDueMinutes` raises
`edi_mdn_overdue`.
- **Receive:** `POST /qqq/hooks/edi/as2/{transport}` (a route provider outside `/qqq/v1`). The
handler reads the body up to the agreement's `maxInterchangeBytes` (in memory up to 8 MB, then
streamed to object storage, never to a temp file), uses as2-lib's receive helpers to decrypt,
verify the signature against the partner's certificate, decompress and compute the MIC, then calls
`InboundSink.receive` and returns the signed sync MDN, or sends the async MDN after commit. A
message that cannot be stored gets a failure MDN (`processed/error:
unexpected-processing-error`) so the partner retries.
- The AS2 `Message-ID` is the inbox dedupe key: a retransmitted message gets the original MDN again
and is not reprocessed.
- MDNs, sent and received, are stored in **`edi_as2_mdn`** [AS2 MDN] (child of the outbound message
or the inbox row): `direction`, `messageId`, `originalMessageId`, `disposition`, `micAlgorithm`,
`micValue`, `signed`, `signatureValid`, `payloadRef` (the raw MDN in object storage),
`receivedAt`.
- TLS: outbound uses the JVM trust store plus optional pinning (`partnerTlsCertificateId`). Inbound
TLS terminates at the host's ingress.
### 5.6 HTTPS and email
- **HTTPS out:** POST the interchange (`application/edi-x12` or `application/EDIFACT`) with the
configured auth. 2xx is DELIVERED. Responses are not parsed as acks (acks arrive as documents).
- **HTTPS in:** `POST /qqq/hooks/edi/https/{transport}` with BASIC, BEARER or HMAC (Standard
Webhooks signatures, verified with core's webhook signing utilities), a body size limit, then
`InboundSink.receive`.
- **CAQH CORE** (real-time 270/271 and 276/277 over SOAP or MIME) is a later plugin that reuses the
same parser and map engine with a synchronous reply. Not in v1.
- **Email out:** qbit-email's `EmailService.send` with the interchange as an attachment, category
TRANSACTIONAL, linked record `edi_outbound_message`. Healthcare documents may only go through a
provider marked `phi_allowed` and are refused otherwise. EDI calls qbit-email through its
published API only; until that API ships, this transport is disabled.
- **Email in:** through qbit-email's phase 2 inbound (an `InboundEmailHandler` registered by EDI).
Designed here, built when qbit-email inbound ships.
## 6. Parsing (X12 now, EDIFACT later)
- StAEDI (`EDIInputFactory`, `EDIStreamReader`) reads X12 as a stream, with no whole-document tree,
so a 50 MB 837 batch is processed in bounded memory per transaction set.
- One payload may hold several interchanges; each ISA/IEA becomes an `edi_interchange` row. Each
ST/SE is buffered as its own segment list (bounded by the guide's maximum segment count, default
100,000) for validation, ack building and mapping.
- Envelope checks: ISA fixed width, IEA01 = group count, IEA02 = ISA13, GE01 = transaction count,
GE02 = GS06, SE01 = segment count, SE02 = ST02. Envelope errors produce TA1 or AK9/IK9 codes.
- Character set: the X12 basic and extended character sets, and UTF-8 where the agreement allows it
(005010 extended). Unrecognised bytes are an envelope error, never silently replaced.
- Writing uses `EDIStreamWriter` with the agreement's separators.
- EDIFACT: StAEDI reads UNA/UNB/UNG/UNH and validates against EDIFACT schemas. It is designed into
the model now (`standard` columns, UNB fields on `edi_identity`, CONTRL as an ack type) and built
in phase g.
## 7. Implementation guides as data
### 7.1 Tables
**`edi_guide`** [Implementation Guide] (main, nav)
- `name`, `standard`, `version` (`004010`, `005010`, `005010X222A1`, ...), `transactionSet`,
`functionalIdCode`, `partnerId` (null = a base guide), `parentGuideId` (an override's base),
`revision` (increments on activation), `status` (DRAFT, ACTIVE, RETIRED), `source` (BUILT_IN,
IMPORTED, CUSTOM), `description`, `compiledChecksum`.
- BUILT_IN rows are read-only (a record lock keyed on `source`; only the loader writes them).
**`edi_guide_segment`** [Guide Segment] (child of guide): `guideId`, `sequence`, `loopId` (e.g.
`2000A`, `N1`, `HL`), `parentLoopId`, `loopMaxRepeat`, `segmentId`, `position` (the X12 position,
e.g. `0100`), `usage` (REQUIRED, SITUATIONAL, NOT_USED), `minUse`, `maxUse`, `name`,
`situationalRuleId`, `notes`.
**`edi_guide_element`** [Guide Element] (child of guide segment): `guideSegmentId`, `position` (3
for BEG03), `componentPosition` (null or 1..n), `elementId` (the data element number), `name`,
`dataType` (AN, ID, N0-N9, R, DT, TM, B), `minLength`, `maxLength`, `usage`, `codeListId`,
`fixedValue`, `repeatMax`, `situationalRuleId`.
**`edi_guide_rule`** [Guide Rule] (child of guide)
- X12 syntax notes per segment: `type` PAIRED (P), REQUIRED (R), EXCLUSION (E), CONDITIONAL (C) or
LIST_CONDITIONAL (L), with the element positions.
- Situational and inter-segment rules: `type` SITUATIONAL with an `expression` in the rule language
(section 7.3), `snipLevel` (2, 4, 6 or 7), `severity` (ERROR, WARNING), `message`, and the
997/999 error codes to report.
**`edi_code_list`** [Code List] (main, nav) and **`edi_code_list_value`** [Code] (child): `name`,
`elementId`, `source` (BUILT_IN, CUSTOM, EXTERNAL), `externalCodeSet` (for licensed code sets,
section 11.4); values have `code`, `description`, `isActive`.
### 7.2 Compilation and validation
- Activating a guide compiles it into a StAEDI schema (XML generated in memory and cached by guide
id and revision, never written to disk). StAEDI then enforces SNIP level 1 and most of level 2
structurally: segment order, loops, minimum and maximum use, element types and lengths, code
lists and syntax notes.
- After StAEDI, the guide rule engine runs the situational and inter-segment rules on the buffered
transaction set.
- Every finding becomes an `edi_transaction_error` row: segment position, segment id, element
position, the X12 code for the ack (AK3/AK4 for 997, IK3/IK4/CTX for 999), the SNIP level and a
plain message. Element values go in `valueExcerpt` only for non-PHI documents (section 12).
### 7.3 Rule language
A deliberately small expression language over the buffered transaction set, evaluated without a
scripting engine:
- paths as in maps (section 8.2): `CLM02`, `2300/CLM05-1`, `HL[HL03=22]/NM1[NM101=IL]/NM109`;
- functions: `present(p)`, `absent(p)`, `count(p)`, `sum(p)`, `eq(a, b)`, `in(p, 'A', 'B')`,
`matches(p, regex)`, `date(p)`, `and(...)`, `or(...)`, `not(x)`, `when(condition, then)`.
- Example: "if the patient is not the subscriber, the 2000C loop is required" is
`when(eq(2000B/SBR02, '18'), absent(2000C))` together with its converse.
### 7.4 Built-in guides and licensing
X12 standards and the HIPAA TR3s are copyrighted and licensed by ASC X12. A public Apache-2.0 repo
must not ship their text. So:
- **Built-in guides are authored in-house** for the documents in scope: segment and element
structure, usage, lengths and the code values the maps use, with our own short names and no copied
notes, descriptions or complete code lists. They are enough to parse, validate structurally,
acknowledge and map typical traffic, and are labelled "QQQ baseline guide".
- **Imported guides:** the table action "Import Guide" on `edi_guide` loads a guide that a licensee
exports from their licensed tools (a documented JSON format, plus an adapter for a common X12
schema JSON). Imported guides live in the host's database, never in this repo.
- Partner companion-guide differences are **override guides** (section 7.5).
- Built-in guides ship as JSON resources in the jar and are upserted by the loader at startup when
their checksum changes (as a new revision; the previous revision stays for documents processed
under it).
### 7.5 Partner overrides
- "Create Partner Override" (record action on a guide) copies the guide into a CUSTOM guide with
`partnerId` and `parentGuideId`, in DRAFT.
- Edits are ordinary record edits of its segments, elements, rules and code lists.
- "Activate Guide" requires that the guide compiles, increments `revision`, and retires the previous
ACTIVE revision for that partner. Documents record the guide id and revision they were validated
with, so reprocessing an old document can use the original or the current revision (a choice on
the reprocess action).
- "Compare With Base" lists the differences as standard Result lines (segments, elements and rules
added, removed or changed).
## 8. Maps
### 8.1 Tables
**`edi_map`** [Map] (main, nav)
- `name`, `direction` (INBOUND, OUTBOUND), `standard`, `version`, `transactionSet`, `guideId`,
`partnerId` (null = shared), `tableName` (the target or source QQQ table), `matchKeyFields`
(comma list; the inbound upsert key, e.g. `partnerId,poNumber`), `onMatch` (UPDATE, SKIP,
EXCEPTION), `businessKeyPath`, `businessKeyType`, `secondaryKeyPath`, `revision`, `status`
(DRAFT, ACTIVE, RETIRED), `parentMapId`, `description`.
**`edi_map_rule`** [Map Rule] (child of map): `sequence`, `ediPath`, `qqqPath` (`field`,
`association.field`), `transform`, `transformArgs`, `required`, `condition` (rule language),
`defaultValue`, `notes`.
**`edi_map_sample`** [Map Sample] (child of map): `name`, `payloadRef` (stored like any payload,
encrypted), `expectedResultJson`, `lastPreviewAt`, `lastPreviewStatus`. Synthetic data only; the
record screen says so, and samples on PHI maps are classified PHI.
### 8.2 Paths and transforms
- **EDI paths:** segment plus element position (`BEG03`), components with `-` (`CLM05-1`), loops by
id (`2300/CLM01`) or by qualifier predicate (`N1[N101=ST]/N3/N301`), repeats with an index or `*`
(`PO1[*]/PO102`). Inbound, `*` loops map to associations; outbound, associations drive loops.
- **QQQ paths:** fields and associations of the map's table, checked against the table's metadata
on activation.
- **Transforms:** `DATE` (CCYYMMDD; YYMMDD with a century window), `TIME` (HHMM, HHMMSS and
fractions), `DATETIME` (date + time + the partner's timezone), `DECIMAL` (implied decimals for N
types), `QUALIFIER` (pick by a sibling qualifier), `CODE` (translate through a code list or a
map-local table), `UNIT` (unit-of-measure conversion, EA, CA, PK, LB, KG, ..., with pack sizes
from a host field), `CONSTANT`, `CONCAT`, `SPLIT`, `SUBSTRING`, `TRIM`, `UPPER`, `LOOKUP` (find a
QQQ record id by a key, such as an item by UPC or a ship-to by N104, read as the service session),
`SEQUENCE` (outbound line numbers), `HL` (outbound HL numbering for 856 hierarchies). Hosts can
register their own transforms by name.
- A rule failure is a mapping error naming the rule, the EDI path and the QQQ path (with no value
for PHI documents).
### 8.3 Sandbox (phase a as an action, phase f as UI)
"Preview Map" (record action on `edi_map` and `edi_map_sample`): pick a sample or upload one (the
upload is stored as a sample), and the standard Review/Result lines show:
1. the parse: interchanges, groups and transaction sets found;
2. validation findings by SNIP level;
3. the 997/999 that would be sent;
4. the map result (records and associations that would be written), downloadable as JSON;
5. the diff against the previous map revision's result and against `expectedResultJson`.
Nothing is written to target tables: the preview runs the whole pipeline with a no-op applier.
Phase f adds spec'd endpoints (`POST /qqq/v1/qbit/edi/sandbox/preview` and related) for the mapper
UI.
### 8.4 No-code mapper UI (phase f; specified now, built later)
A custom Next UI page (custom UIs may be nav entries):
- left, the guide tree (loops, segments, elements with usage and code lists); right, the target
table's fields and associations; drag to create a rule, then choose a transform;
- a live preview pane on a chosen sample (the sandbox endpoint), with errors inline;
- revisions: edit a DRAFT, activate, compare revisions;
- reads and writes only through `/qqq/v1` (CRUD on `edi_map_rule` plus the extension endpoints).
## 9. Flows
### 9.1 Inbox and inbound processing
**`edi_inbox`** [Inbound Message] (child list on the transport and the interchange; linked from the
home):
- `transportId`, `transportType`, `externalId` (AS2 Message-ID, gateway `uploadJobId`, poll path and
modification time, HTTPS request id, email message id), `payloadRef`, `payloadSha256`,
`sizeBytes`, `fileName`, `remoteAddress`, `receivedAt`, `status` (PENDING, PROCESSING, PROCESSED,
RETRY_WAIT, DEAD_LETTER, DUPLICATE, IGNORED), `attempts`, `nextAttemptAt`, `claimedAt`,
`processedAt`, `lastError` (no payload content), `partnerId` (once known).
- Unique (`transportType`, `externalId`); sha256 duplicates within the window become DUPLICATE.
- The worker (a scheduled process, every 15 seconds, batches of 20) claims rows with a
compare-and-set on `status` and `claimedAt`. Claims older than 5 minutes are reclaimed, so any
replica can process and a crashed one loses nothing. Backoff 1m, 5m, 15m, 1h, 6h; then
DEAD_LETTER, an `edi_inbox_dead_letter` alert and an exception.
Per interchange, group and transaction set (each transaction set in its own database transaction,
so one bad document does not block the others):
1. envelope checks and TA1 (section 6);
2. partner and agreement routing (4.2), duplicate checks (4.3);
3. validation (7.2) at the agreement document's SNIP level;
4. ack building: one 997 or 999 per received group, sent per the agreement (9.4);
5. if the transaction set is accepted (`A` or `E`): map and apply (9.2); a rejected set is not
applied;
6. statuses, timeline events and exceptions.
### 9.2 Apply as a service account
- The applier runs `InsertAction` and `UpdateAction` (with associations) on the map's table under a
**service session** from `EdiQBitConfig.withServiceSessionSupplier(Supplier<QSession>)`, which the
host provides (a user with exactly the permissions EDI should have). Permission enforcement stays
on, so a map cannot write to a table the service account cannot. When partner locks are on,
that partner's security key is added to the session for the call.
- Upsert by `matchKeyFields`: query for an existing record as the service session; `onMatch`
decides. The 860 (PO change) is a map onto the same order table with UPDATE.
- The target table's customizers, validation, audit and ESB events all run, because this is the
normal action path.
- The target record id is stored on `edi_transaction` (`targetTableName`, `targetRecordId`). A host
can show a record's EDI documents with a child-list widget on `edi_transaction` filtered by
target.
- A failure rolls back that transaction set only, sets status EXCEPTION, and opens an `APPLY`
exception with the error.
### 9.3 Outbound
- **Trigger** (per outbound agreement document, `outboundTrigger`):
- `ESB_EVENT` (preferred when qqq-esb is configured): the source table publishes ESB table
events; an `EsbTrigger` subscribed to them runs the process `edi_outbound_enqueue`, which
applies `triggerFilter` (a QQueryFilter as JSON, e.g. `status = SHIPPED`) and inserts an
**`edi_outbound_request`** row (`agreementDocumentId`, `sourceTableName`, `sourceRecordId`,
`eventId`, `status`, `attempts`, `nextAttemptAt`, `claimedAt`, `lastError`). It is idempotent on
(event id, agreement document).
- `RECORD_CHANGE` (no broker): a core `RecordChangeListenerInterface` registered by EDI inserts the
same row inside the writer's transaction (a transactional outbox: the request exists exactly
when the source change commits).
- `MANUAL`: the table action "Send As EDI" on the source table, added when the host opts in, with
the standard Review/Result summary.
- **Outbound worker** (claims requests like the inbox): loads the source record as the service
session, maps it (section 8), and validates it against the guide (an invalid outbound document is
never sent; it opens a `VALIDATION` exception). It then batches per agreement (IMMEDIATE or on
`batchSchedule`), envelopes (control numbers, 4.3), stores the raw interchange and creates an
**`edi_outbound_message`**.
- **`edi_outbound_message`** [Outbound Message]: `interchangeId`, `transportId`, `payloadRef`,
`status` (QUEUED, SENDING, SENT, DELIVERED, RETRY_WAIT, FAILED, DEAD_LETTER, CANCELLED),
`attempts`, `nextAttemptAt`, `transportMessageId`, `sentAt`, `deliveredAt`, `lastError`. The
sender claims rows the same way and retries per the transport's policy; DEAD_LETTER raises
`edi_send_failed` and opens a `TRANSPORT` exception.
- **Reconcile:** an MDN or delivery confirmation sets DELIVERED; the expected 997/999 sets the
functional group's `ackStatus` (4.4); the home and the timeline show both.
### 9.4 Acknowledgements we send
- **TA1:** per the agreement's `sendTa1`, with codes from the envelope checks (e.g. `001` control
number mismatch, `006`/`007` invalid sender or receiver, `022` invalid control structure, `025`
duplicate).
- **997** (non-HIPAA, and 004010) and **999** (005010 HIPAA, `005010X231A1`): AK1, AK2, AK3, AK4,
AK5 and AK9, or IK3, IK4, IK5 and CTX, at the agreement document's `ackLevel`, built from the
validation findings.
- Acks are outbound interchanges on the same agreement and transport, with their own control
numbers, and appear as `edi_transaction` rows (997, 999, TA1) linked to what they acknowledge.
- Acks are never themselves acknowledged.
### 9.5 Resend, reprocess, search and timeline
Record actions, all with the standard Review/Result summary:
- **Reprocess** (on `edi_inbox`, `edi_interchange`, `edi_transaction`, `edi_exception`): run again
from the stored raw payload, from the parse or from the map step, with the original or the current
guide and map revisions. Idempotent: an applied document is matched by its target record and
updated or skipped per the map's `onMatch`. Acks are re-sent only when chosen.
- **Resend** (on `edi_outbound_message`, `edi_interchange`): send the same bytes again (the same
control numbers). **Resend As New**: re-envelope with new control numbers (for partners that
rejected a duplicate).
- **Cancel** (queued outbound), **Assign**, **Ignore** and **Mark Resolved** (exceptions).
- **Search by business keys:** `edi_transaction.businessKey` (PO, ASN, invoice, load and shipment
numbers, from the map's `businessKeyPath`, e.g. BEG03, BSN02, BIG02, B204, W0506),
`businessKeyType`, and `secondaryKey` (e.g. the PO on an 856 or 810). Healthcare keys (claim and
patient control numbers, member ids) go to `phiBusinessKey` instead (section 12). All are in Quick
Search; the PHI field matches only for users with FULL access to it (secured-data).
- **Timeline:** `edi_event` [Event] (child of interchange and transaction): `at`, `type` (RECEIVED,
STORED, PARSED, VALIDATED, ACK_SENT, MAPPED, APPLIED, ENVELOPED, SENT, MDN, DELIVERED,
ACK_RECEIVED, EXCEPTION, REPROCESSED, RESENT), `detail` (no payload values), `userName`. The
interchange and transaction screens show it in time order next to the record audit.
### 9.6 Runtime tables
**`edi_interchange`** [Interchange] (main, nav, Quick Search on ISA13, ESB events): `direction`,
`partnerId`, `agreementId`, `transportId`, `inboxId` or `outboundMessageId`, `standard`,
`senderQualifier`, `senderId`, `receiverQualifier`, `receiverId`, `controlNumber` (ISA13),
`interchangeVersion`, `usageIndicator`, `interchangeDate`, `groupCount`, `payloadRef`,
`payloadSha256`, `sizeBytes`, `status` (RECEIVED, ACCEPTED, ACCEPTED_WITH_ERRORS, REJECTED,
DUPLICATE, ENVELOPED, SENT, DELIVERED, FAILED), `ta1Status`, `containsPhi`.
**`edi_functional_group`** [Functional Group] (child of interchange): `functionalIdCode`,
`controlNumber` (GS06), `version` (GS08), `transactionCount`, `ackStatus` (A, E, P, R, NONE,
PENDING), `ackDueAt`, `ackReceivedAt`, `ackTransactionId`.
**`edi_transaction`** [Document] (main, nav, Quick Search, ESB events): `interchangeId`,
`functionalGroupId`, `direction`, `partnerId`, `agreementDocumentId`, `transactionSet`, `version`,
`controlNumber` (ST02), `businessKeyType`, `businessKey`, `secondaryKey`, `phiBusinessKey`,
`status` (RECEIVED, VALID, INVALID, MAPPED, APPLIED, EXCEPTION, QUEUED, SENT, ACKNOWLEDGED,
REJECTED, IGNORED), `ackStatus`, `errorCount`, `warningCount`, `guideId`, `guideRevision`, `mapId`,
`mapRevision`, `targetTableName`, `targetRecordId`, `segmentCount`, `containsPhi`.
**`edi_transaction_error`** [Validation Error] (child of transaction): `segmentPosition`,
`segmentId`, `loopId`, `elementPosition`, `componentPosition`, `snipLevel`, `ackCode`, `severity`,
`message`, `valueExcerpt` (non-PHI documents only).
**`edi_exception`** [Exception] (main, nav; the work queue): `type` (UNKNOWN_PARTNER,
PARTNER_MISMATCH, NOT_CONFIGURED, DUPLICATE, ENVELOPE, VALIDATION, MAPPING, APPLY, TRANSPORT,
REJECTED_ACK, OVERDUE_ACK, DEAD_LETTER), `status` (OPEN, IN_PROGRESS, RESOLVED, IGNORED),
`partnerId`, `inboxId`, `interchangeId`, `transactionId`, `outboundMessageId`, `summary`, `detail`
(no payload values), `assignedToUserId`, `resolvedBy`, `resolvedAt`, `resolution`, `occurrences`
(repeated failures of the same item increment it instead of adding rows).
The 824 Application Advice and the 864 are later document types on the same model (outbound
documents generated from an exception), not new tables.
## 10. Payload storage (database and S3 only)
- **`PayloadStore`** writes raw payloads (inbound files, outbound interchanges, AS2 messages and
MDNs, samples) to qbit-object-storage: the bucket alias from
`EdiQBitConfig.withPayloadBucketAlias` (default `edi`), key
`edi/<yyyy>/<MM>/<dd>/<kind>/<uuid>`, through `ObjectStorage.asSystem("edi", runId)`.
- **Envelope encryption, always:** each payload is encrypted with AES-256-GCM under a random data
key; the data key is wrapped with the master key from env var `QQQ_EDI_PAYLOAD_KEY` (base64, 32
bytes) and stored with its key id in the object's metadata. `QQQ_EDI_PAYLOAD_KEY_PREVIOUS` allows
rotation (read with either, write with the current one); the table action "Rewrap Payloads"
re-wraps old payloads. Bucket-side SSE is recommended as well, not relied on.
- `payloadRef` columns hold `objectstorage:<bucket>:<key>`; the plaintext sha256 is stored next to
it.
- **Database fallback:** without qbit-object-storage, payloads go to **`edi_payload`** (`kind`,
`ciphertext` BLOB, `keyId`, `sha256`, `sizeBytes`, `createDate`), encrypted the same way. It suits
the demo and tests; production should use object storage (startup logs a WARN, and the home
shows it).
- Nothing touches local disk: parsing streams from the store, AS2 uses memory and object storage,
and certificates come from table rows and env vars. A test asserts that a full round trip creates
no file under the JVM temp directory.
- **Retention** (`EdiQBitConfig.withRetention...`; the nightly table action "Purge Payloads"): raw
payloads are kept 7 years for healthcare (HIPAA documentation retention is 6 years) and 2 years
otherwise, configurable. Rows and their sha256 stay after the payload is purged. The home shows the
policy.
## 11. Validation levels (SNIP) and code sets
### 11.1 What each level checks and what we support
| SNIP | Meaning | Support |
|---|---|---|
| 1 | EDI syntax integrity: envelopes, segment and element syntax, separators | **Full** (StAEDI and the envelope checks) |
| 2 | HIPAA syntax requirements: TR3 usage (R/S/N), repeats, lengths, TR3 code values | **Full** for the rules in the active guide (the built-in baseline or an imported TR3) |
| 3 | Balancing: totals across segments (837 CLM02 equals the sum of the SV1/SV2 line charges; 835 BPR02 equals the sum of CLP04 less PLB adjustments; claim and line adjustments balance) | **Full** for 837P, 837I and 835 (coded checks) |
| 4 | Situational inter-segment rules ("if X then Y") | **Full** through guide rules (7.3); baseline guides carry the common ones, imported TR3s the rest |
| 5 | External code sets: ICD-10, CPT, HCPCS, NDC, CARC/RARC, taxonomy, ZIP | **Partial:** a pluggable `ExternalCodeSetProvider` (11.4); no licensed code set ships (CPT is AMA-licensed). Format checks (ICD-10 shape, NPI check digit, ZIP format) are built in. |
| 6 | Product type or line of business (ambulance, chiropractic, ...) | **Through override guides and rules**; no built-in content |
| 7 | Trading-partner-specific requirements (companion guides) | **Through partner override guides** (7.5) |
### 11.2 Findings and acks
SNIP 1-2 errors reject the transaction set (AK5/IK5 `R`) unless the agreement document downgrades a
rule to WARNING. SNIP 3-7 findings default to `E` (accepted with errors) plus an exception, because
business rejection belongs in an 824 or a 277CA, not in a 999. Each rule can set its severity.
### 11.3 Defaults
Non-HIPAA documents validate at level 2 by default and HIPAA documents at level 4. An agreement
document can raise or lower its level.
### 11.4 External code sets
`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
so.
## 12. HIPAA and PHI
- **Classification** (`DataClassification`, core #1142; the stand-in until RC.6):
- PHI: `edi_transaction.phiBusinessKey`, `edi_transaction_error.valueExcerpt` (never written for
PHI documents, but classified anyway), `edi_map_sample.expectedResultJson`, `edi_inbox.fileName`
(file names often carry member or claim ids), and the payloads themselves (encrypted, never in a
table column).
- PII: partner contact fields. CONFIDENTIAL: everything else in the `edi_` tables.
- Host target tables classify their own fields. The map writes through normal actions, so their
classification applies to what it writes.
- With qbit-secured-data installed, PHI fields follow its tiers: a customer-service user sees
`phiBusinessKey` masked and can find a claim by exact match only (REVEALABLE), which is how claim
lookup is meant to work for non-FULL users. Without it, the fields are FULL to anyone with the
table permission, so a host carrying PHI should install it (the home warns).
- **No PHI outside the encrypted payloads and PHI-classified fields:** for documents with
`containsPhi`, errors, events, exceptions, alert texts, log lines and Result lines carry segment
and element positions and codes, never values. The 999 we send carries positions and element
references only.
- **Encryption at rest:** payloads always (section 10). PHI columns rely on the database's
encryption at rest (a host responsibility, stated in the README) plus secured-data masking.
- **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.
- No real PHI anywhere in the repo: samples are synthetic, produced by a seeded generator.
## 13. App home and navigation
App `edi` [EDI], optionally under the host's parent app. Nav (main tables only):
- **Activity:** Documents (`edi_transaction`), Interchanges, Exceptions.
- **Setup:** Trading Partners, Agreements, Transports, Certificates, Implementation Guides, Code
Lists, Maps.
Sub-tables (identities, agreement documents, control numbers, guide segments, elements and rules,
code list values, map rules and samples, functional groups, validation errors, events, inbox,
outbound requests and messages, MDNs) are child lists on their parents.
**Home** (typed widgets only, no HTML; every figure links to the filtered table):
1. Statistics: documents in and out today and over 7 days, open exceptions, overdue acks, dead
letters, certificates expiring within 30 days.
2. Table: volume by partner and document type over 7 days (in, out, rejected), each cell linking to
`edi_transaction` filtered.
3. Table: overdue acknowledgements (partner, group, sent, due, age).
4. Table: recent errors (validation and transport, last 24 hours).
5. Child-record list: the exception queue (OPEN and IN_PROGRESS, oldest first), with Assign and
Reprocess actions.
6. Statistics: system state (payload store: object storage or the database fallback; secured-data
present; external code-set provider present; inbox backlog and the oldest pending age).
All counts run as the viewing user, so permissions and partner record locks apply. Real data only.
## 14. Security and permissions
- Table permissions use the full name: `edi.partner.read|insert|edit|delete`, and likewise for each
table (`edi.agreement.*`, `edi.transport.*`, `edi.certificate.*`, `edi.guide.*`, `edi.map.*`,
`edi.interchange.read`, `edi.transaction.read`, `edi.exception.read|edit`, ...). Runtime tables
are read-only to people (no insert or delete permission is offered; the qbit writes them as the
system).
- Process permissions: `edi.reprocess`, `edi.resend`, `edi.exception.assign`, `edi.guide.activate`,
`edi.guide.import`, `edi.map.activate`, `edi.map.preview`, `edi.certificate.promote`,
`edi.transport.test`, `edi.raw.download`, `edi.payload.purge`, `edi.admin`.
- **Partner record locks** (optional, `EdiQBitConfig.withPartnerLocks(true)`): every partner-scoped
table carries a READ_AND_WRITE lock on `partnerId` with the security key type `edi_partner` and the
all-access key `edi.admin`, so a host can limit operators to some partners. The service session
gets the needed partner key per call.
- Hooks authenticate per transport (AS2 signature and certificate, HTTPS auth), not with user
sessions; they run as the system only to store and enqueue.
- Client channels: the producer registers its own denies (architecture review F17): the assistant
channel is denied `edi_payload`, `edi_certificate` and `edi_inbox`, and the PHI fields follow
secured-data.
## 15. Demo
No kof22 adoption. The demo is a module in `qqq-sample-project` (in QRun-IO/qqq): a small orders,
shipments and invoices schema; a retailer partner over the file gateway or SFTP and a 3PL over AS2
against a local as2-lib peer; seeded synthetic documents; and a healthcare clearinghouse partner with
synthetic 837/835 for the PHI walkthrough. It runs on H2 with the database payload fallback, or with
MinIO for object storage.
## 16. Phases and acceptance
| Phase | Scope | Acceptance |
|---|---|---|
| a | Partners, identities, agreements, agreement documents, control numbers, transports (model), payload store, inbox and worker, X12 parse and envelopes, guide model, compiler and rule engine, built-in guides for 850 (004010, 005010) and the acks, 997/999/TA1 generation, inbound SFTP through the file gateway (stand-in until its api ships), exceptions, events, home v1, Preview Map | An 850 from a gateway upload becomes an applied record and a 997; TA1 and 997 for envelope and syntax errors; duplicates caught at every level; control numbers unique under 8 concurrent allocators; every table has a record screen; home counts are real; `./mvnw verify` green |
| b | Retail 850, 855, 856, 810, 846, 860 (004010, 005010) guides and maps; outbound (ESB and record-change triggers, batching, envelope, send); SFTP push and VAN polling; gateway OUTBOX; HTTPS in and out; inbound ack reconciliation; overdue-ack alerts; resend, reprocess, cancel | 855/856/810 built from host records, validated, enveloped and sent; a 997 reconciles and resolves the overdue alert, and a missing one raises it; resend keeps control numbers, resend-as-new does not |
| c | AS2 send and receive, sync and async MDN, certificates and rollover, expiry alerts, VAN over AS2 | Round trip against a local as2-lib peer: signed and encrypted, sync and async MDN, MIC verified, a bad signature refused, a retransmit gets the original MDN |
| d | Logistics 940, 945, 214, 204 guides and maps | Round trips per document |
| e | Healthcare 837P, 837I, 835, 270/271, 276/277, 834 (005010 TR3 baselines), 999, SNIP 3-4, PHI classification, canary egress tests; the email transport when qbit-email's API ships | 837 batches validated to SNIP 4 with balancing errors reported; correct 999s; canary test green |
| f | Sandbox endpoints and the mapper UI (UI API extension and a Next page) | Fragment committed and contract-tested; the mapper edits a map and previews a sample |
| g | EDIFACT (UNA/UNB/UNH, ORDERS, ORDRSP, DESADV, INVOIC, CONTRL) | EDIFACT round trips and CONTRL |
Each phase is one or more PRs into `develop`, with tests and, for UI-visible changes, screenshots of
the home and key records in light and dark at 390 and 1440 px.
### 16.1 Tests (all phases)
- Synthetic sample documents per type and version from a seeded generator; no real data.
- Round trips: parse, map and apply, then build the outbound document from the applied record and
compare.
- 997, 999 and TA1 generation against expected acks for crafted errors at every level.
- AS2 against a local as2-lib peer, in-process, in both directions.
- Control-number concurrency, wrap and duplicate detection.
- Partner overrides change validation outcomes as intended; map revisions and the sandbox diff.
- No local files during a full round trip; no secret values in logs or responses.
- Canary PHI egress (phase e).
## 17. Open questions (decided as recommended, 2026-10-12)
| # | Question | Recommendation (accepted) |
|---|---|---|
| Q1 | Ship full X12 or TR3 guide content? | No. In-house baseline guides plus an "Import Guide" action for licensed content (7.4). |
| Q2 | Use qbit-sftp-data-integration for SFTP push and polling? | No code dependency (G1); use core's SFTP client directly, the same one it uses (5.4). |
| Q3 | Where do raw documents live without object storage? | Encrypted in `edi_payload` in the database, for the demo and tests; production uses object storage (10). |
| Q4 | Outbound trigger: ESB or a record-change listener? | Both: the ESB trigger when a broker is configured, otherwise the in-transaction record-change listener; MANUAL for one-offs (9.3). |
| Q5 | Which user applies inbound documents? | A host-supplied service session with enforcement on (9.2). |
| Q6 | Test (ISA15 = T) documents? | Separate agreements; validated and acknowledged, applied only with `applyTestDocuments` (4.1). |
| Q7 | Business rejections (824, 277CA)? | Later document types on the same model; v1 uses exceptions plus `E` acks (11.2). |
| Q8 | Are PHI business keys searchable? | Yes, in a PHI-classified field, by exact match for REVEALABLE users through secured-data (12). |
| Q9 | Payload retention? | 7 years for healthcare, 2 years otherwise, configurable (10). |
| Q10 | SHA-1 and 3DES for AS2? | Only when a partner requires them, flagged on the transport and the home (5.1). |
| Q11 | Partner record locks? | Optional (`withPartnerLocks`), off by default (14). |
| Q12 | Demo host? | `qqq-sample-project`, not kof22 (15). |
## 18. Owner steps (outside code)
- Protect `develop` and make `test_only` required (the coordinator).
- For production use: set `QQQ_EDI_PAYLOAD_KEY`, an object-storage bucket alias `edi`, the service
user for the applier, AS2 keys and passwords as environment variables, and load licensed TR3
guides or code sets if healthcare validation beyond the baseline is needed.
Vendored Executable
+295
View File
@@ -0,0 +1,295 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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
#
# http://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.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"
Vendored
+189
View File
@@ -0,0 +1,189 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.4
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+157 -38
View File
@@ -1,37 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--
~ Copyright 2026 Kingsrook, LLC
~
~ 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
~
~ http://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.
-->
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-example-app</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>QBit Example Application</name>
<description>Template for Application QBits - complete mini-applications with UI navigation</description>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<revision>0.1.0-SNAPSHOT</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<qqq.version>4.0.0</qqq.version>
<jacoco.version>0.8.15</jacoco.version>
</properties>
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.0.0</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-edi</artifactId>
<version>${revision}</version>
<packaging>jar</packaging>
<name>QBit EDI</name>
<description>QQQ QBit: EDI (X12 and EDIFACT) trading partners, transports (AS2, SFTP, VAN, HTTPS, email), acknowledgements, data-driven implementation guides and maps</description>
<url>https://github.com/QRun-IO/qbit-edi</url>
<modelVersion>4.0.0</modelVersion>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>kingsrook</id>
<name>Kingsrook Team</name>
<email>contact@kingsrook.com</email>
<organization>Kingsrook, LLC</organization>
<organizationUrl>https://kingsrook.com</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/QRun-IO/qbit-edi.git</connection>
<developerConnection>scm:git:ssh://github.com:QRun-IO/qbit-edi.git</developerConnection>
<url>https://github.com/QRun-IO/qbit-edi</url>
<tag>HEAD</tag>
</scm>
<dependencies>
<!-- QQQ Backend Core -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-javalin</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
<version>${qqq.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -40,51 +101,109 @@
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
<version>${qqq.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.3.1</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.12.6</version>
</dependency>
</dependencies>
<configuration>
<configLocation>checkstyle/config.xml</configLocation>
<headerLocation>checkstyle/license.txt</headerLocation>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>
<failsOnError>false</failsOnError>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
<includeTestSourceDirectory>false</includeTestSourceDirectory>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
<limit>
<counter>CLASS</counter>
<value>COVEREDRATIO</value>
<minimum>0.90</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>qqq-snapshot</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.5</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.snapshot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
</profiles>
</project>
@@ -0,0 +1,155 @@
/*
* 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.List;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
/*******************************************************************************
** Code configuration for the EDI qbit. Runtime settings (partners, agreements,
** transports, guides and maps) are data in the edi_ tables, not here.
*******************************************************************************/
public class EdiQBitConfig implements QBitConfig
{
public static final String TABLE_NAME_PREFIX = "edi_";
public static final String PERMISSION_BASE_NAME = "edi";
private String backendName;
private String appName = "edi";
private String appLabel = "EDI";
private String parentAppName;
/*******************************************************************************
** Validate the configuration, collecting all errors.
*******************************************************************************/
@Override
public void validate(QInstance instance, List<String> errors)
{
if(backendName == null || instance.getBackend(backendName) == null)
{
errors.add("backendName must name an existing backend");
}
if(appName == null || appName.isBlank())
{
errors.add("appName is required");
}
}
/*******************************************************************************
** The backend for the qbit's tables.
*******************************************************************************/
@Override
public String getDefaultBackendNameForTables()
{
return (backendName);
}
/*******************************************************************************
** Getter for backendName
*******************************************************************************/
public String getBackendName()
{
return (this.backendName);
}
/*******************************************************************************
** Fluent setter for backendName
*******************************************************************************/
public EdiQBitConfig withBackendName(String backendName)
{
this.backendName = backendName;
return (this);
}
/*******************************************************************************
** Getter for appName
*******************************************************************************/
public String getAppName()
{
return (this.appName);
}
/*******************************************************************************
** Fluent setter for appName
*******************************************************************************/
public EdiQBitConfig withAppName(String appName)
{
this.appName = appName;
return (this);
}
/*******************************************************************************
** Getter for appLabel
*******************************************************************************/
public String getAppLabel()
{
return (this.appLabel);
}
/*******************************************************************************
** Fluent setter for appLabel
*******************************************************************************/
public EdiQBitConfig withAppLabel(String appLabel)
{
this.appLabel = appLabel;
return (this);
}
/*******************************************************************************
** Getter for parentAppName
*******************************************************************************/
public String getParentAppName()
{
return (this.parentAppName);
}
/*******************************************************************************
** Fluent setter for parentAppName
*******************************************************************************/
public EdiQBitConfig withParentAppName(String parentAppName)
{
this.parentAppName = parentAppName;
return (this);
}
}
@@ -0,0 +1,134 @@
/*
* 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 com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer;
/*******************************************************************************
** Entry point for the EDI qbit; component producers are scanned only in the
** metadata package.
*******************************************************************************/
public class EdiQBitProducer implements QBitMetaDataProducer<EdiQBitConfig>
{
public static final String GROUP_ID = "com.kingsrook.qbits";
public static final String ARTIFACT_ID = "qbit-edi";
public static final String VERSION = "0.1.0";
private EdiQBitConfig qBitConfig;
private String namespace;
/*******************************************************************************
** Fluent configuration setter.
*******************************************************************************/
public EdiQBitProducer withQBitConfig(EdiQBitConfig config)
{
qBitConfig = config;
return (this);
}
/*******************************************************************************
** Configuration for component producers.
*******************************************************************************/
@Override
public EdiQBitConfig getQBitConfig()
{
return (qBitConfig);
}
/*******************************************************************************
** Fluent namespace setter.
*******************************************************************************/
public EdiQBitProducer withNamespace(String namespace)
{
this.namespace = namespace;
return (this);
}
/*******************************************************************************
** Qbit namespace.
*******************************************************************************/
@Override
public String getNamespace()
{
return (namespace);
}
/*******************************************************************************
** Qbit identity.
*******************************************************************************/
@Override
public QBitMetaData getQBitMetaData()
{
return (new QBitMetaData().withGroupId(GROUP_ID).withArtifactId(ARTIFACT_ID).withVersion(VERSION).withNamespace(namespace).withConfig(qBitConfig));
}
/*******************************************************************************
** Unconfigured producers discovered by host scanning are disabled.
*******************************************************************************/
@Override
public boolean isEnabled()
{
return (qBitConfig != null);
}
/*******************************************************************************
** Only the metadata package holds component producers.
*******************************************************************************/
@Override
public String getPackageNameForFindingMetaDataProducers()
{
return ("com.kingsrook.qbits.edi.metadata");
}
/*******************************************************************************
** Validate the configuration and install the qbit's metadata.
*******************************************************************************/
@Override
public MetaDataProducerMultiOutput produce(QInstance instance) throws QException
{
qBitConfig.validate(instance);
MetaDataProducerMultiOutput output = QBitMetaDataProducer.super.produce(instance);
output.addSelfToInstance(instance);
return (new MetaDataProducerMultiOutput());
}
}
@@ -1,164 +0,0 @@
/*******************************************************************************
** Configuration class for the Example Application QBit.
**
** Application QBits support:
** - Backend configuration for data storage
** - Optional table prefixing for multi-instance deployment
** - Feature toggles for optional modules
** - Security key prefix for permission scoping
** - Integration hooks for other QBits
*******************************************************************************/
package com.kingsrook.qbits.example;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
public class ExampleAppQBitConfig implements QBitConfig
{
private String backendName;
private String tableNamePrefix;
private Boolean enableChildModule = true;
private String securityKeyPrefix;
/////////////////////////////////////////////////////////////////////////
// Integration hooks - allow host app to inject custom behavior //
/////////////////////////////////////////////////////////////////////////
private QCodeReference onEntityCreatedHandler;
private QCodeReference customValidationProvider;
/*******************************************************************************
** Validate the configuration before the QBit is produced.
*******************************************************************************/
@Override
public void validate(QInstance qInstance, List<String> errors)
{
if(!StringUtils.hasContent(backendName))
{
errors.add("backendName is required for ExampleAppQBit");
}
if(qInstance.getBackend(backendName) == null)
{
errors.add("Backend not found: " + backendName);
}
}
/*******************************************************************************
** Apply prefix to a table name if configured.
*******************************************************************************/
public String applyPrefix(String tableName)
{
if(StringUtils.hasContent(tableNamePrefix))
{
return tableNamePrefix + "_" + tableName;
}
return tableName;
}
/*******************************************************************************
** Get the security key for permission checks.
*******************************************************************************/
public String getSecurityKey(String baseName)
{
if(StringUtils.hasContent(securityKeyPrefix))
{
return securityKeyPrefix + "." + baseName;
}
return baseName;
}
//////////////////////////////////////////////////////////////////////////////
// Getters and fluent setters //
//////////////////////////////////////////////////////////////////////////////
public String getBackendName()
{
return backendName;
}
public ExampleAppQBitConfig withBackendName(String backendName)
{
this.backendName = backendName;
return this;
}
public String getTableNamePrefix()
{
return tableNamePrefix;
}
public ExampleAppQBitConfig withTableNamePrefix(String tableNamePrefix)
{
this.tableNamePrefix = tableNamePrefix;
return this;
}
public Boolean getEnableChildModule()
{
return enableChildModule;
}
public ExampleAppQBitConfig withEnableChildModule(Boolean enableChildModule)
{
this.enableChildModule = enableChildModule;
return this;
}
public String getSecurityKeyPrefix()
{
return securityKeyPrefix;
}
public ExampleAppQBitConfig withSecurityKeyPrefix(String securityKeyPrefix)
{
this.securityKeyPrefix = securityKeyPrefix;
return this;
}
public QCodeReference getOnEntityCreatedHandler()
{
return onEntityCreatedHandler;
}
public ExampleAppQBitConfig withOnEntityCreatedHandler(QCodeReference onEntityCreatedHandler)
{
this.onEntityCreatedHandler = onEntityCreatedHandler;
return this;
}
public QCodeReference getCustomValidationProvider()
{
return customValidationProvider;
}
public ExampleAppQBitConfig withCustomValidationProvider(QCodeReference customValidationProvider)
{
this.customValidationProvider = customValidationProvider;
return this;
}
}
@@ -1,175 +0,0 @@
/*******************************************************************************
** Producer for the Example Application QBit.
**
** Application QBits produce:
** - QBitMetaData for identification
** - QAppSection for UI navigation (REQUIRED)
** - Tables, processes, widgets, and other metadata
** - Security configuration
*******************************************************************************/
package com.kingsrook.qbits.example;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProducer;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.SourceQBitAware;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qbits.example.model.ExampleEntity;
import com.kingsrook.qbits.example.model.ExampleChildEntity;
public class ExampleAppQBitProducer implements QBitProducer
{
public static final String GROUP_ID = "com.kingsrook.qbits";
public static final String ARTIFACT_ID = "qbit-example-app";
public static final String VERSION = "0.1.0";
private ExampleAppQBitConfig config;
/*******************************************************************************
** Produce the QBit metadata into the QInstance.
*******************************************************************************/
@Override
public void produce(QInstance qInstance, String namespace) throws QException
{
/////////////////////////////////////////////////////////////////////////
// Validate configuration //
/////////////////////////////////////////////////////////////////////////
List<String> errors = new ArrayList<>();
config.validate(qInstance, errors);
if(!errors.isEmpty())
{
throw new QException("Configuration errors: " + String.join(", ", errors));
}
/////////////////////////////////////////////////////////////////////////
// Create and register QBit identity //
/////////////////////////////////////////////////////////////////////////
QBitMetaData qBitMetaData = new QBitMetaData()
.withGroupId(GROUP_ID)
.withArtifactId(ARTIFACT_ID)
.withVersion(VERSION)
.withNamespace(namespace)
.withConfig(config);
qInstance.addQBit(qBitMetaData);
/////////////////////////////////////////////////////////////////////////
// Discover and produce all component metadata //
/////////////////////////////////////////////////////////////////////////
List<MetaDataProducerInterface<?>> producers =
MetaDataProducerHelper.findProducers(getClass().getPackageName());
for(MetaDataProducerInterface<?> producer : producers)
{
///////////////////////////////////////////////////////////////////////
// Skip disabled components //
///////////////////////////////////////////////////////////////////////
if(!isProducerEnabled(producer))
{
continue;
}
MetaDataProducerOutput output = producer.produce(qInstance);
///////////////////////////////////////////////////////////////////////
// Apply table prefix if configured //
///////////////////////////////////////////////////////////////////////
if(output instanceof QTableMetaData table)
{
String prefixedName = config.applyPrefix(table.getName());
table.setName(prefixedName);
table.setBackendName(config.getBackendName());
}
///////////////////////////////////////////////////////////////////////
// Mark scope for all SourceQBitAware outputs //
///////////////////////////////////////////////////////////////////////
if(output instanceof SourceQBitAware sqa)
{
sqa.setSourceQBitName(qBitMetaData.getName());
}
output.addSelfToInstance(qInstance);
}
/////////////////////////////////////////////////////////////////////////
// Register QAppMetaData for UI navigation (REQUIRED for app QBits) //
/////////////////////////////////////////////////////////////////////////
qInstance.addApp(produceApp(config, qBitMetaData.getName()));
}
/*******************************************************************************
** Produce the QAppMetaData for UI navigation.
**
** This is REQUIRED for Application QBits - it defines how users navigate
** to the tables, processes, and widgets in the UI.
*******************************************************************************/
private QAppMetaData produceApp(ExampleAppQBitConfig config, String qbitName)
{
List<String> tables = new ArrayList<>();
tables.add(config.applyPrefix(ExampleEntity.TABLE_NAME));
if(Boolean.TRUE.equals(config.getEnableChildModule()))
{
tables.add(config.applyPrefix(ExampleChildEntity.TABLE_NAME));
}
QAppSection section = new QAppSection()
.withName(config.applyPrefix("exampleSection"))
.withLabel("Example Application")
.withIcon(new QIcon().withName("dashboard"))
.withTables(tables);
return new QAppMetaData()
.withName(config.applyPrefix("exampleApp"))
.withLabel("Example Application")
.withIcon(new QIcon().withName("dashboard"))
.withSection(section);
}
/*******************************************************************************
** Check if a producer should be enabled based on configuration.
*******************************************************************************/
private boolean isProducerEnabled(MetaDataProducerInterface<?> producer)
{
/////////////////////////////////////////////////////////////////////////
// Skip child entity if module disabled //
/////////////////////////////////////////////////////////////////////////
if(!Boolean.TRUE.equals(config.getEnableChildModule()))
{
if(producer.getClass().getSimpleName().contains("ExampleChildEntity"))
{
return false;
}
}
return true;
}
//////////////////////////////////////////////////////////////////////////////
// Fluent setters //
//////////////////////////////////////////////////////////////////////////////
public ExampleAppQBitProducer withConfig(ExampleAppQBitConfig config)
{
this.config = config;
return this;
}
}
@@ -1,159 +0,0 @@
/*******************************************************************************
** Example child entity demonstrating parent-child relationships.
**
** This entity shows:
** - Foreign key relationship to parent entity
** - Conditional inclusion via config toggle
*******************************************************************************/
package com.kingsrook.qbits.example.model;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
@QMetaDataProducingEntity(producePossibleValueSource = true)
public class ExampleChildEntity extends QRecordEntity
{
public static final String TABLE_NAME = "exampleChildEntity";
@QField(isPrimaryKey = true)
private Integer id;
@QField(isRequired = true, possibleValueSourceName = "exampleEntity")
private Integer exampleEntityId;
@QField(isRequired = true, maxLength = 100)
private String name;
@QField(maxLength = 500)
private String description;
@QField
private Integer sortOrder;
@QField
private Boolean isActive = true;
@QField
private Instant createDate;
@QField
private Instant modifyDate;
//////////////////////////////////////////////////////////////////////////////
// Fluent setters //
//////////////////////////////////////////////////////////////////////////////
public ExampleChildEntity withId(Integer id)
{
this.id = id;
return this;
}
public ExampleChildEntity withExampleEntityId(Integer exampleEntityId)
{
this.exampleEntityId = exampleEntityId;
return this;
}
public ExampleChildEntity withName(String name)
{
this.name = name;
return this;
}
public ExampleChildEntity withDescription(String description)
{
this.description = description;
return this;
}
public ExampleChildEntity withSortOrder(Integer sortOrder)
{
this.sortOrder = sortOrder;
return this;
}
public ExampleChildEntity withIsActive(Boolean isActive)
{
this.isActive = isActive;
return this;
}
public ExampleChildEntity withCreateDate(Instant createDate)
{
this.createDate = createDate;
return this;
}
public ExampleChildEntity withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return this;
}
//////////////////////////////////////////////////////////////////////////////
// Getters //
//////////////////////////////////////////////////////////////////////////////
public Integer getId()
{
return id;
}
public Integer getExampleEntityId()
{
return exampleEntityId;
}
public String getName()
{
return name;
}
public String getDescription()
{
return description;
}
public Integer getSortOrder()
{
return sortOrder;
}
public Boolean getIsActive()
{
return isActive;
}
public Instant getCreateDate()
{
return createDate;
}
public Instant getModifyDate()
{
return modifyDate;
}
}
@@ -1,145 +0,0 @@
/*******************************************************************************
** Example entity for an Application QBit.
**
** This entity demonstrates:
** - Standard QQQ entity patterns
** - PossibleValueSource generation
** - Fields with various types and constraints
*******************************************************************************/
package com.kingsrook.qbits.example.model;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
@QMetaDataProducingEntity(producePossibleValueSource = true)
public class ExampleEntity extends QRecordEntity
{
public static final String TABLE_NAME = "exampleEntity";
@QField(isPrimaryKey = true)
private Integer id;
@QField(isRequired = true, maxLength = 100)
private String name;
@QField(maxLength = 500)
private String description;
@QField(label = "Status")
private String status;
@QField
private Boolean isActive = true;
@QField
private Instant createDate;
@QField
private Instant modifyDate;
//////////////////////////////////////////////////////////////////////////////
// Fluent setters //
//////////////////////////////////////////////////////////////////////////////
public ExampleEntity withId(Integer id)
{
this.id = id;
return this;
}
public ExampleEntity withName(String name)
{
this.name = name;
return this;
}
public ExampleEntity withDescription(String description)
{
this.description = description;
return this;
}
public ExampleEntity withStatus(String status)
{
this.status = status;
return this;
}
public ExampleEntity withIsActive(Boolean isActive)
{
this.isActive = isActive;
return this;
}
public ExampleEntity withCreateDate(Instant createDate)
{
this.createDate = createDate;
return this;
}
public ExampleEntity withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return this;
}
//////////////////////////////////////////////////////////////////////////////
// Getters //
//////////////////////////////////////////////////////////////////////////////
public Integer getId()
{
return id;
}
public String getName()
{
return name;
}
public String getDescription()
{
return description;
}
public String getStatus()
{
return status;
}
public Boolean getIsActive()
{
return isActive;
}
public Instant getCreateDate()
{
return createDate;
}
public Instant getModifyDate()
{
return modifyDate;
}
}
@@ -1,46 +0,0 @@
/*******************************************************************************
** MetaData producer for the example process.
*******************************************************************************/
package com.kingsrook.qbits.example.processes;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qbits.example.model.ExampleEntity;
public class ExampleProcessMetaDataProducer implements MetaDataProducerInterface<QProcessMetaData>
{
public static final String NAME = "exampleProcess";
/*******************************************************************************
** Produce the process metadata.
*******************************************************************************/
@Override
public QProcessMetaData produce(QInstance qInstance) throws QException
{
return new QProcessMetaData()
.withName(NAME)
.withLabel("Example Process")
.withTableName(ExampleEntity.TABLE_NAME)
.withStepList(List.of(
new QBackendStepMetaData()
.withName("process")
.withCode(new QCodeReference(ExampleProcessStep.class))
.withInputData(new QFunctionInputMetaData()
.withField(new QFieldMetaData("confirmAction", QFieldType.BOOLEAN)
.withLabel("Confirm Action")
.withDefaultValue(false)))
));
}
}
@@ -1,67 +0,0 @@
/*******************************************************************************
** Example process step demonstrating business logic implementation.
**
** This shows:
** - Standard process step patterns
** - Input/output handling
** - Logging conventions
*******************************************************************************/
package com.kingsrook.qbits.example.processes;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
public class ExampleProcessStep extends AbstractTransformStep
{
private static final QLogger LOG = QLogger.getLogger(ExampleProcessStep.class);
/*******************************************************************************
** Run the process step.
*******************************************************************************/
@Override
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
List<QRecord> records = input.getRecords();
LOG.info("Processing records", logPair("count", records.size()));
for(QRecord record : records)
{
///////////////////////////////////////////////////////////////////////
// Example business logic - update status //
///////////////////////////////////////////////////////////////////////
String currentStatus = record.getValueString("status");
if("PENDING".equals(currentStatus))
{
record.setValue("status", "PROCESSED");
}
output.addRecord(record);
}
LOG.info("Processing complete", logPair("processed", records.size()));
}
/*******************************************************************************
** Return the process summary. Override this method in subclasses to provide
** meaningful summary lines for the result screen.
*******************************************************************************/
@Override
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
return (new ArrayList<>());
}
}
@@ -1,35 +0,0 @@
/*******************************************************************************
** MetaData producer for an example dashboard widget.
**
** Widgets provide dashboard components for Application QBits.
*******************************************************************************/
package com.kingsrook.qbits.example.widgets;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
public class ExampleDashboardWidgetMetaDataProducer implements MetaDataProducerInterface<QWidgetMetaData>
{
public static final String NAME = "exampleDashboardWidget";
/*******************************************************************************
** Produce the widget metadata.
*******************************************************************************/
@Override
public QWidgetMetaData produce(QInstance qInstance) throws QException
{
return new QWidgetMetaData()
.withName(NAME)
.withLabel("Example Dashboard")
.withType(WidgetType.STATISTICS.getType())
.withCodeReference(new QCodeReference(ExampleDashboardWidgetRenderer.class));
}
}
@@ -1,33 +0,0 @@
/*******************************************************************************
** Renderer for the example dashboard widget.
**
** This class produces the data displayed in the widget.
*******************************************************************************/
package com.kingsrook.qbits.example.widgets;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.StatisticsData;
public class ExampleDashboardWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
** Render the widget data.
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
/////////////////////////////////////////////////////////////////////////
// Add chart data, counts, or other dashboard information here //
/////////////////////////////////////////////////////////////////////////
StatisticsData data = new StatisticsData()
.withCount(0)
.withCountContext("Total items");
return (new RenderWidgetOutput(data));
}
}
@@ -0,0 +1,109 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.edi;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfigValidationException;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
** Unit tests for the EDI qbit producer and configuration.
*******************************************************************************/
class EdiQBitProducerTest
{
/*******************************************************************************
** An instance with a memory backend.
*******************************************************************************/
private static QInstance instance()
{
QInstance instance = new QInstance();
instance.addBackend(new QBackendMetaData().withName("memory").withBackendType(MemoryBackendModule.class));
return (instance);
}
/*******************************************************************************
** Identity, defaults and the enabled flag.
*******************************************************************************/
@Test
void testIdentityAndDefaults()
{
EdiQBitProducer producer = new EdiQBitProducer().withNamespace("ns");
assertThat(producer.isEnabled()).isFalse();
EdiQBitConfig config = new EdiQBitConfig().withBackendName("memory").withParentAppName("parent");
producer.withQBitConfig(config);
assertThat(producer.isEnabled()).isTrue();
assertThat(producer.getQBitConfig()).isSameAs(config);
assertThat(producer.getNamespace()).isEqualTo("ns");
assertThat(producer.getQBitMetaData().getArtifactId()).isEqualTo("qbit-edi");
assertThat(producer.getPackageNameForFindingMetaDataProducers()).isEqualTo("com.kingsrook.qbits.edi.metadata");
assertThat(config.getAppName()).isEqualTo("edi");
assertThat(config.getAppLabel()).isEqualTo("EDI");
assertThat(config.getParentAppName()).isEqualTo("parent");
assertThat(config.getBackendName()).isEqualTo("memory");
assertThat(config.getDefaultBackendNameForTables()).isEqualTo("memory");
assertThat(config.withAppLabel("Trading").getAppLabel()).isEqualTo("Trading");
}
/*******************************************************************************
** Validation collects every error.
*******************************************************************************/
@Test
void testValidation()
{
List<String> errors = new ArrayList<>();
new EdiQBitConfig().withAppName(" ").validate(instance(), errors);
assertThat(errors).containsExactly("backendName must name an existing backend", "appName is required");
errors.clear();
new EdiQBitConfig().withAppName(null).withBackendName("memory").validate(instance(), errors);
assertThat(errors).containsExactly("appName is required");
}
/*******************************************************************************
** Produce installs the qbit on a valid instance and refuses a bad config.
*******************************************************************************/
@Test
void testProduce() throws Exception
{
QInstance instance = instance();
new EdiQBitProducer().withQBitConfig(new EdiQBitConfig().withBackendName("memory")).produce(instance);
assertThat(instance.getQBits().values()).anyMatch(q -> "qbit-edi".equals(q.getArtifactId()));
assertThatThrownBy(() -> new EdiQBitProducer().withQBitConfig(new EdiQBitConfig()).produce(instance()))
.isInstanceOf(QBitConfigValidationException.class);
}
}