mako-markt
Market data library for German energy market communication (MaKo).
mako-markt is the domain library for Marktlokation (MaLo), Messlokation (MeLo),
VersorgungsStatus, NB network contracts, trading-partner, and process-correlation
management. It is the foundation for marktd, the production
Market Data Hub.
Key design choices:
mako_markt::bo4eis the workspace's BO4E boundary — the gate every payload crosses in either direction, the rules BO4E states but enforces nowhere, and the typed columns a stored document is indexed by. See The BO4E gate below.- Writes take the typed BO, reads return the stored document.
MaloRepository::upserttakes&rubo4e::current::Marktlokation— never aserde_json::Value— so an unvalidated payload cannot reach storage and a shadow column cannot disagree with the document it shadows. The record types (MaloRecord.data,MeloRecord.data,ZaehlerRecord.data, …) hand the JSONB back verbatim, because a stored document must round-trip exactly as the counterparty sent it. NbContractRecordcarries full BO4EVertragJSONB —data: serde_json::Valuestores the canonical BO4EVertragpayload alongside typed SQL columns (netzebene,bilanzierungsmethode,billing_schedule).vertragsartandvertragsstatusare extracted as indexed columns for fast SQL filtering.rubo4e::currentis the BO vocabulary —Marktlokation,Messlokation,Zaehler,Geraet,Vertrag,Energiemenge,Lastgang,Rechnungand the COMs and enums beneath them. BO4E schema version202607.1.0.
The BO4E gate
BO4E's schema constrains almost nothing — of the 35 Geschäftsobjekte, two
declare a required field and none declares a oneOf — so "it deserialises" is
not validation. mako_markt::bo4e::decode is the four decisions that make up
accepting a BO4E document, in one place:
use bo4e;
use Marktlokation;
let malo: Marktlokation = decode
.map_err?;
| Stage | Refuses | code |
|---|---|---|
| 1. Discriminator | a Zaehler posted to the Geraet endpoint |
bo4e.discriminator |
| 2. Schema | a value the type cannot hold | bo4e.schema |
| 3. Strict enums | "sparte": "STROMM", at any depth, by JSON-path |
bo4e.unknown_enum |
| 4. BO4E rules | a document the standard's prose forbids | bo4e.rule |
Stage 3 carries most of the weight: Unknown serialises back as the literal
"UNKNOWN", so at an endpoint that stores the canonical round-trip, skipping it
overwrites what the caller sent rather than merely accepting it.
| Variant | Use |
|---|---|
decode |
an endpoint. All four stages. |
decode_received |
a counterparty's document. Stages 1–3 refuse; a rule violation is returned alongside the value, to be disputed rather than dead-lettered. |
ensure_conformant |
outbound: stages 3 and 4 on a value mako built. |
Stage 4 is mostly rubo4e's .validate(), which descends the whole tree;
bo4e::conformance adds the two rules BO4E states and rubo4e does not check.
Both report as ValidationFailure { path, message }. A rule belongs there
only if BO4E asserts it — endpoint requirements are a separate layer.
Design principles
| Principle | Detail |
|---|---|
| Stateless library | No axum, no sqlx, no async runtime in this crate. All I/O lives in services/marktd. |
| Validated domain identifiers | [MaloId], [MeloId], and [MarktpartnerId] validate format and checksum at construction time — invalid IDs are rejected at the system boundary. |
| Temporal role assignments | rollenzuordnung entries carry valid_from/valid_to date ranges. Queries are always resolved against a reference date (German local time, CET/CEST). |
Generic AppState |
One generic type parameter per repository trait — fully static dispatch with no dyn Trait overhead. |
| AFIT | All repository traits use async fn in trait (stable since Rust 1.75, MSRV 1.94). |
Crate structure
mako_markt
├── domain Validated IDs (MaloId, MeloId, MarktpartnerId), Sparte, ProcessStatus
├── repository Repository traits + AppState + record types + PageResult
│ MaloRecord (data: Marktlokation JSONB, typed columns)
│ MeloRecord (data: Messlokation JSONB, standorteigenschaften JSONB)
│ NbContractRecord (data: Vertrag JSONB, vertragsart/vertragsstatus columns)
│ ZaehlerRecord (data: Zaehler JSONB), GeraetRecord (data: Geraet JSONB)
│ VersorgungsStatusRepository, LieferStatus, VersorgungsStatusRecord
│ GrundversorgerRepository, GrundversorgerRecord (§36 Abs. 2 EnWG)
│ PriCatRepository, PriCatVersion, PriCatDispatchState
├── error MdmError — RFC 7807-ready with status_u16, error_code, error_title
├── bo4e decode · decode_received · ensure_conformant · conformance rules
│ shadow columns (MaloShadowColumns, …)
├── cloudevents InboundMakoEvent, MarktEvent, EventExtensions
│ Emitted: de.markt.malo.updated, de.markt.nb-contract.updated,
│ de.markt.pricat.published, de.markt.versorgung.beliefert,
│ and 17 more `de.markt.*` types
├── commands typed makod command names/payloads
├── makod_client HTTP client for the makod admin API (feature `makod-client`)
├── marktd_client HTTP client for the marktd REST API (feature `marktd-client`)
└── testing InMemory* test doubles (feature = "testing")
includes: InMemoryPriCatRepository
Domain identifiers
MaloId — 11-digit BDEW Marktlokations-ID
Validated with the BDEW alternating-weight check digit algorithm (BDEW Identifikatoren AWH V1.2 §2.1):
use MaloId;
let id = new?; // validates checksum
println!; // "51238696012"
MeloId — 33-character Messlokations-ID
use MeloId;
let id = new?;
MarktpartnerId — 13-digit BDEW/DVGW/GS1 Codenummer
Derives the NAD DE3055 agency code from the prefix (99… → 293, 98… → 332, other → 9):
use MarktpartnerId;
let mp_id = "9900357000004".?;
assert_eq!; // BDEW Strom
assert_eq!;
Note: Only GS1-issued 13-digit codes are true GLNs (NAD DE3055
9). BDEW-Codenummern (99…,293) and DVGW-Codenummern (98…,332) are not GLNs. UseMarktpartnerIdfor all market-participant identifiers — neverString.
Repository traits
All traits use AFIT and return Result<_, MdmError>. Every trait has two implementations:
| Implementation | Use |
|---|---|
Pg*Repository in services/marktd/src/pg/ |
Production — PostgreSQL via sqlx 0.8+ |
InMemory* (behind the testing feature) |
Unit tests — no PostgreSQL required |
VersorgungsStatusRepository
Persists the current supply state for each MaLo. Records are automatically derived
from de.mako.process.completed events by marktd's EventBus pipeline.
LieferStatus values:
| Variant | Meaning |
|---|---|
Beliefert |
MaLo is actively supplied by a nominated LF |
Unbeliefert |
MaLo has no active supplier (Grundversorgungsfall) |
Grundversorgung |
§36 EnWG basic supply is active |
Ersatzversorgung |
§38 EnWG emergency supply is active (max 3 months) |
Ruhend |
Supply suspended, MaLo registered but dormant |
Stillgelegt |
MaLo decommissioned |
The at: Date parameter is always the current German local date (CET/CEST) so that
rollenzuordnung validity is evaluated against the correct calendar date, not UTC.
Temporal Rollenzuordnung
Each MaloRecord carries only the assignments valid at the requested reference date.
The storage layer uses a LEFT JOIN … AND valid_from <= $at AND (valid_to IS NULL OR valid_to >= $at).
CloudEvents
Outbound events emitted by marktd conform to CloudEvents 1.0 structured-mode JSON
(application/cloudevents+json). They carry markt* extension attributes and are
HMAC-SHA256 signed for delivery to ERP subscribers.
HMAC-SHA256 signing and verification are not in this crate: they live in
mako_service::webhook (sign / verify_hmac), the one canonical
implementation shared by every emitter and verifier.
use ;
let event = new
.with_extensions;
let body = to_vec?;
// sign with mako_service::webhook::sign(secret, &body) and send the result
// in the `X-Markt-Signature` header
Event source: urn:mako:marktd:tenant:{tenant}
Error handling
MdmError is a thiserror-derived enum. Every variant maps to a stable HTTP status,
machine-readable error_code, and a human-readable error_title for RFC 7807 Problem
Details responses:
| Variant | Status | Code |
|---|---|---|
InvalidMaloId |
422 | invalid_malo_id |
InvalidMeloId |
422 | invalid_melo_id |
InvalidMpId |
422 | invalid_gln |
NotFound |
404 | not_found |
VersionConflict |
412 | version_conflict |
MakodConflict |
409 | makod_conflict |
Forbidden |
403 | forbidden |
Unprocessable |
422 | unprocessable |
MakodSync |
500 | makod_sync_failed |
WebhookDelivery |
500 | webhook_delivery_failed |
Internal |
500 | internal_error |
Testing
Enable the testing feature to get InMemory* test doubles for every repository trait:
[]
= { = "../../crates/mako-markt", = ["testing"] }
use ;
use Arc;
let state = new;
Feature flags
| Flag | Enables |
|---|---|
| (default) | All domain types, repository traits, CloudEvents |
testing |
InMemory* test doubles — never enable in production builds |
makod-client |
HTTP client for the makod admin API (reqwest) |
marktd-client |
HTTP client for the marktd REST API (reqwest) |
PriCatRepository — PRICAT 27003 version history and dispatch
PriCatRepository stores versioned PRICAT snapshots and an audit log of every
outbound dispatch attempt.
Every PUT /api/v1/preisblaetter/{nb_mp_id} call in marktd:
- Writes to
preisblaetter(existing single-row store forinvoicd) - Inserts a versioned snapshot in
pricat_versions - Emits
de.markt.pricat.publishedvia the internal event channel
Dispatch states:
| State | Meaning |
|---|---|
Pending |
Stored; dispatch not yet started |
Queued |
Dispatch task picked this version up |
Done |
All active LF partners successfully reached |
Error |
Last attempt failed; retried on next background scan |
Auto-dispatch on LF partner registration: when a new LF partner is upserted
via PUT /api/v1/partners/{mp_id} in marktd, the latest PRICAT version for the
operator's NB GLN is automatically re-queued for dispatch to the new partner.
Relationship to makod and marktd
┌─────────────────────────────────────────────────┐
│ services/marktd (binary) │
│ axum 0.8+ · sqlx 0.8+ · utoipa 5 · jiff 0.2 │
│ Pg*Repository ←─── implements traits ─────┐ │
│ fan-out worker ←─── event_log outbox + │ │
│ event_delivery ledger │ │
│ OIDC/JWT auth ←─── Cedar not used here │ │
└───────────────────────────┬─────────────────┘ │
│ uses │
┌───────────────────────────▼─────────────────┐ │
│ crates/mako-markt (library — this crate) │ │
│ domain · repository traits · AppState │ │
│ error · cloudevents · makod_client │ │
│ testing (feature = "testing") │ │
└─────────────────────────────────────────────┘
│ makes HTTP calls to
┌───────────▼─────────────────────────────────┐
│ services/makod (production daemon) │
│ EDIFACT · AS4 · event-sourced workflows │
└─────────────────────────────────────────────┘
mako-markt depends on neither axum nor sqlx. Both are confined to services/marktd.
This keeps the library independently testable with zero framework overhead.
Related crates
| Crate | Role |
|---|---|
mako-markt ← this crate |
Domain types, repository traits, the BO4E gate, CloudEvents emission |
energy-api |
The REST/WebSocket API-Webdienste channel, which addresses partners by the same BDEW Codenummer |
mako-events |
CloudEvents type catalog — the shared event vocabulary |
mako-fristen |
When an answer is due — Werktage, the MaKo holiday calendar, the per-PID Antwortfristen |
mako-pruefung |
Reads Marktlokations- and Vertragsstammdaten to decide an Antwortcode |
grid-billing · energy-billing · eeg-billing |
Bill against these stammdaten |
marktd |
Production daemon — the PostgreSQL implementation of the repository traits |
Part of mako, an open-source Rust platform for German energy market communication (Marktkommunikation). Full documentation: https://hupe1980.github.io/mako/