ADR-007: Connect integrations without deep data integration
| Date | Author | Status |
|---|---|---|
| 2026-07-30 | Fabian Beyerlein | Draft |
Context
The system landscape STREAM might have to integrate with is vast. And this vast system landscape comes with various levels of modernity, thus requiring different levels of integration. This usually comes down to available data. AMONDIS does not track data in the structure and level of detail we do - so we cannot reliably rebuild the data on our side properly.
Some integrations cannot become first-class STREAM master-data integrations. They provide enough data to render and operate electronic shelf labels, but not enough structured product, topology, packaging or inventory data to rebuild the STREAM domain model. For these integrations, STREAM should not invent incomplete products, locations or shop entities just to fit the existing model.
Instead, these integrations should be treated as display-driven external label integrations:
- The external system owns product identity, location identity, replacement logic, validity and requestability.
- STREAM owns ESL assignment, rendering, button press handling, assignment state and delivery to physical labels.
- STREAM does not own product master data, shop catalog data, topology hierarchy or the external order lifecycle for these integrations.
The first integration we will implement will be on the simpler end of the spectrum in the first version. UHB Software1 sells Medical e-Shop2 as part of their AMONDIS3 product suite.
The first version of the integration will consist of a JSON file exchange through an SMB share. We will receive a JSON document with labels that contains all data we need to display on the ESL.
Example data
[
{
"Brcnum": 32542,
"Otl": 466,
"OtlAnfmaw": "931540",
"OtlAbbr": "39 I ENTGIFT",
"OtlDes": "39 I ENTGIFTUNG",
"Abbrroom": "Gruppenraum",
"Desroom": "E.01",
"Abbrarck": "Disco",
"Tray": "1",
"Subtray": "1",
"Item": 70986,
"Itemnum": "9603919",
"Itemcode": "eSPR KETAMIN 50MG/ML ROLLE",
"ReplacementItemCode": null,
"Qtt": 25,
"Unit": "STK",
"Pcksize": 1000,
"Unitissue": "ROL",
"ValidFrom": "20210922",
"ValidUntil": "20991231"
},
{
"Brcnum": 32631,
"Otl": 324,
"OtlAnfmaw": "912505",
"OtlAbbr": "10 I LI ZI.0",
"OtlDes": "10 I LI ZI.027 UMW.- UND KH-HYGIENE",
"Abbrroom": "Test TL",
"Desroom": "Test TL",
"Abbrarck": "Test TL",
"Tray": "1",
"Subtray": "1",
"Item": 72840,
"Itemnum": "1000000.145",
"Itemcode": "1,2-DICHLORETHAN REINST EMPURA 1 L (PCK)",
"ReplacementItemCode": "eSPR KETAMIN 50MG/ML ROLLE",
"Qtt": 1,
"Unit": "PCK",
"Pcksize": 1,
"Unitissue": "PCK",
"ValidFrom": "20230127",
"ValidUntil": "20991231"
}
]
Field descriptions
| JSON field | Description |
|---|---|
Brcnum |
Unique ID of label; required for sending requests to Medical e-Shop |
Otl |
Supply Area ID |
OtlAnfmaw |
Supply Area Code |
OtlAbbr |
Supply Area short name |
OtlDes |
Supply Area long name |
Abbrroom |
Room short name |
Desroom |
Room long name |
Abbrarck |
Shelf short name |
Tray |
Compartment Code |
Subtray |
Sub Compartment Code |
Item |
Medical e-Shop internal product ID |
Itemnum |
Product ID |
Itemcode |
Product name |
ReplacementItemCode |
Product name of a potential replacement product; show instead of Itemcode if present |
Qtt |
Packaging unit packages |
Unit |
Packaging unit item descriptor |
Pcksize |
Packaging unit items |
Unitissue |
Packaging unit package descriptor |
ValidFrom |
Label valid/usable from |
ValidUntil |
Label valid/usable until |
Integration flow
Importing data from AMONDIS
- AMONDIS will drop JSON files into the SMB share
- Mercury reads them, validates the file and sends the imported labels to Solaris
- Solaris stores them as external labels
The import must be idempotent. A file from AMONDIS represents the complete
current snapshot for one organization and integration. Rows present in a
successfully imported file are upserted. Rows that were known before but are no
longer present in the latest successful full import are marked as BLOCKED; if
the integration provides validity dates, valid_until is retained from the
source data. A background job should mark external labels where valid_until
has passed as BLOCKED. External labels with status BLOCKED cannot be
requested. If assigned, they are displayed like products in BLOCKED state.
Failed imports must not partially apply missing-row blocking. Either the import is applied as a complete snapshot, or the previous successful import remains the source of truth.
Sending requests to AMONDIS
The labels from AMONDIS need to be connected to ESLs. The order flow is roughly:
- Person clicks ESL
- Solaris receives webhook
- Solaris resolves the external label assigned to the ESL
- Assignment state is set to
REQUESTED - Data sent to Mercury
- Mercury drops a JSON file
We need to prevent re-orders for this integration. If the assignment is
REQUESTED, do nothing. Refilling the label is always possible. There will need
to be an endpoint in Mercury to receive notifications on cancelled requests from
AMONDIS to properly reset labels.
For request button presses, Solaris must suppress requests when the linked
external label has status BLOCKED or when the assignment state is already
REQUESTED. Otherwise, it transitions the assignment state to REQUESTED and
sends the request to Mercury.
For refill button presses, Solaris transitions the assignment state back to
DEFAULT. This makes future requests possible again.
API definition (German)
Decision
Instead of trying to shoehorn the AMONDIS data into our structure, we introduce external labels. External labels allow us to repeat a similar pattern for other integrations that provide display-ready label data but no deep STREAM-compatible master data.
External labels and ESLs can be linked through product assignments. The assignment state remains separate from the external label's requestability:
DEFAULTmeans requests are possible if the external label is requestable.REQUESTEDmeans a request was already sent and duplicate requests must be suppressed.
The table shall have the following columns:
| Name | Data type | Notes |
|---|---|---|
id |
UUID | v7 |
org_id |
UUID | Edge to organizations |
integration |
TEXT | ID of integration responsible for this row |
external_id |
TEXT | Stable label/product ID from the external system |
status |
TEXT | Product status, e.g. ACTIVE or BLOCKED |
data |
JSONB | JSON blob with label data as received by external system |
data_hash |
TEXT | Hash of normalized data to avoid unnecessary updates |
valid_from |
TIMESTAMPTZ | Optional validity start |
valid_until |
TIMESTAMPTZ | Optional validity end |
last_seen_at |
TIMESTAMPTZ | Last successful full import containing this row |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
The natural deduplication key is (org_id, integration, external_id). For
AMONDIS Medical e-Shop, Brcnum is the external_id.
Each integration provides an adapter for the integration-specific behavior. The adapter is responsible for:
- identifying the integration
- validating and mapping imported data
- extracting the external ID
- deriving requestability
- rendering labels or providing the label rendering model
- generating outbound request payloads for Mercury
This way, integrations can define their own label templates and request payloads while the rest of the system does not have to understand integration-specific fields.
In the UI, we need a new section to manage external labels. Topology, Master Data and the Shop should eventually be hidden. The external labels section needs to allow seeing all synced external labels (read-only). It also needs to allow (bulk-)assigning external labels to ESLs through their label code as usual.
Analytics should continue to work for assignments backed by external labels.
Alternatives considered
- Map AMONDIS data into STREAM products, topology and shop entities
- Rejected because the source data is not structured enough for our model. Creating fake or incomplete domain entities would make the core model less reliable.
- Store AMONDIS fields directly on existing product assignment entities
- Rejected because it would pollute core assignment data with integration-specific fields and would not scale to other shallow integrations.
- Build AMONDIS-specific tables
- Rejected because future integrations with similar constraints would repeat the same pattern.
- Render directly from files without persisting external labels
- Rejected because assignment, search, analytics, idempotent imports and request suppression need persisted state.
Consequences
- Enables shallow integrations without corrupting STREAM's core product, topology and shop models.
- Keeps integration-specific data and behavior behind adapters.
- Reuses the existing ESL assignment, button press event and assignment state infrastructure.
- Adds a second label/product path next to the normal STREAM product model.
- UI and analytics need to understand assignments backed by external labels.
- Some STREAM features that require deep product, topology or shop data will not be available for these integrations.
Open decisions
- adapter shape with interface
- what does marcury do vs what does solaris do?
- do we parse to internal data in mercury? do we pass the blob to solaris and keep mercury light?