mako_engine/erp.rs
1//! ERP integration traits and reference implementations.
2//!
3//! ## Role
4//!
5//! `mako-engine` is a protocol processor — it handles EDIFACT parsing, BDEW
6//! process rules, AS4 delivery, and regulatory deadlines. All contract data,
7//! billing logic, and master data live in the operator's ERP.
8//!
9//! This module defines the **stable integration contract** between `mako-engine`
10//! and external ERP or backend systems. The payload contract is **BO4E**, not
11//! raw EDIFACT. ERP adapters never see EDIFACT segment codes or format-version
12//! identifiers — those are absorbed inside `mako-engine`.
13//!
14//! ## Outbound: mako → ERP
15//!
16//! Implement [`ErpAdapter`] and register it at startup. Every domain event
17//! that requires ERP action is delivered as an [`ErpEvent`]. The production
18//! `WebhookErpAdapter` (in `makod`) serialises events as
19//! **[CloudEvents 1.0](https://cloudevents.io) structured-mode JSON** and POSTs
20//! them to the configured ERP endpoint.
21//!
22//! ```text
23//! POST <erp_webhook_url>
24//! Content-Type: application/cloudevents+json
25//! X-Idempotency-Key: <event.idempotency_key>
26//! X-Mako-Signature: <hmac-sha256-hex> ← only when secret is configured
27//!
28//! {
29//! "specversion": "1.0",
30//! "id": "<idempotency_key>",
31//! "source": "urn:mako:tenant:<tenant_id>",
32//! "type": "de.mako.aperak.accepted",
33//! "time": "2026-10-01T10:15:00+02:00",
34//! "subject": "<process_id>",
35//! "dataschema": "https://.../Marktlokation.json",
36//! "datacontenttype": "application/json",
37//! "makoconvid": "<conversation_id>",
38//! "makocausationid": "<causation_id>",
39//! "makopid": 55001,
40//! "data": { "_typ": "MARKTLOKATION", ... }
41//! }
42//! ```
43//!
44//! See [`ErpEventType::cloud_event_type`] for the full type → CE type mapping.
45//! The BO4E payload is always in the `data` field; the `payload_schema` URL
46//! maps to the CloudEvents `dataschema` attribute.
47//!
48//! ## Inbound: ERP → mako (event-driven)
49//!
50//! For ERP systems with a message bus, implement [`ErpCommandSource`] to feed
51//! BO4E business objects into the engine without a synchronous REST round-trip.
52//!
53//! ```rust,ignore
54//! struct MyKafkaSource { consumer: KafkaConsumer }
55//!
56//! impl ErpCommandSource for MyKafkaSource {
57//! async fn next(&self) -> Result<Option<InboundErpCommand>, ErpAdapterError> {
58//! let msg = self.consumer.poll(Duration::from_millis(100)).await;
59//! Ok(msg.map(|m| InboundErpCommand {
60//! idempotency_key: m.offset().to_string(),
61//! tenant_id: TenantId::new(),
62//! payload_schema: "…/Marktlokation.json".into(),
63//! payload: serde_json::from_slice(m.payload()).unwrap(),
64//! }))
65//! }
66//!
67//! async fn ack(&self, id: &str) -> Result<(), ErpAdapterError> {
68//! self.consumer.commit_offset(id.parse().unwrap()).await
69//! .map_err(ErpAdapterError::transport)
70//! }
71//!
72//! async fn nack(&self, _id: &str, _reason: &str) -> Result<(), ErpAdapterError> {
73//! Ok(()) // Kafka auto-redelivers on next poll
74//! }
75//! }
76//! ```
77//!
78//! ## Reference implementations
79//!
80//! | Type | Feature | Use case |
81//! |------|---------|---------|
82//! | `NoopErpAdapter` | `testing` | Unit tests, CI |
83//! | [`LogErpAdapter`] | — | Structured log output; starting point for new integrations |
84//! | `NoopErpCommandSource` | `testing` | No-op inbound source for tests |
85//!
86//! For the production `WebhookErpAdapter` and `POST /api/v1/commands` endpoint,
87//! see `makod/src/erp_adapter.rs`.
88
89use std::sync::Arc;
90
91use serde::{Deserialize, Serialize};
92use time::OffsetDateTime;
93
94use crate::erc::ErcCode;
95use crate::ids::{ConversationId, EventId, ProcessId, TenantId};
96
97// ── ErpAdapterError ───────────────────────────────────────────────────────────
98
99/// Errors produced by [`ErpAdapter`] and [`ErpCommandSource`] implementations.
100#[derive(Debug, thiserror::Error)]
101pub enum ErpAdapterError {
102 /// The ERP response payload could not be deserialised or is semantically
103 /// invalid.
104 #[error("ERP payload error: {0}")]
105 Payload(String),
106
107 /// A transient transport error (network timeout, HTTP 5xx, broker
108 /// disconnect). The delivery worker will retry with exponential backoff.
109 #[error("ERP transport error: {0}")]
110 Transport(String),
111
112 /// A permanent, non-retryable error (e.g. invalid configuration,
113 /// authentication failure). The delivery worker will dead-letter the
114 /// message.
115 #[error("ERP permanent error: {0}")]
116 Permanent(String),
117}
118
119impl ErpAdapterError {
120 /// Construct a [`Payload`](ErpAdapterError::Payload) variant.
121 pub fn payload(e: impl std::fmt::Display) -> Self {
122 Self::Payload(e.to_string())
123 }
124
125 /// Construct a [`Transport`](ErpAdapterError::Transport) variant.
126 pub fn transport(e: impl std::fmt::Display) -> Self {
127 Self::Transport(e.to_string())
128 }
129
130 /// Construct a [`Permanent`](ErpAdapterError::Permanent) variant.
131 pub fn permanent(e: impl std::fmt::Display) -> Self {
132 Self::Permanent(e.to_string())
133 }
134
135 /// Returns `true` for transient errors that warrant a retry.
136 #[must_use]
137 pub fn is_retryable(&self) -> bool {
138 matches!(self, Self::Transport(_))
139 }
140}
141
142// ── ErpEventType ─────────────────────────────────────────────────────────────
143
144/// Semantic classification of an outbound ERP process event.
145///
146/// The ERP uses this to decide which action to take — update an order status,
147/// trigger a billing run, open a complaint ticket, etc.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub enum ErpEventType {
151 /// A new MaKo process was spawned (e.g. inbound UTILMD received).
152 ProcessInitiated,
153 /// The counterparty sent an APERAK accepting our UTILMD.
154 AperakAccepted,
155 /// The counterparty sent an APERAK rejecting our UTILMD.
156 ///
157 /// `erc_code` is `Some` when the APERAK carried a structured ERC segment
158 /// (BDEW APERAK AHB 1.0 §2.2). It is `None` for legacy outbox messages
159 /// that predate the typed ERC code field.
160 AperakRejected {
161 /// Structured BDEW ERC error code from the APERAK ERC segment.
162 ///
163 /// Use [`crate::erc::recommended_action`] to derive the
164 /// recommended automated ERP response.
165 #[serde(skip_serializing_if = "Option::is_none")]
166 erc_code: Option<ErcCode>,
167 },
168 /// No APERAK received within the regulatory SLA window (deadline expired).
169 AperakTimeout,
170 /// A CONTRL syntax acknowledgement was received.
171 ContrlReceived,
172 /// The process reached its terminal success state
173 /// (e.g. Lieferbeginn/Lieferende confirmed).
174 ProcessCompleted,
175 /// A MaLo identification request was successfully resolved: the MaLo was
176 /// found and the positive callback was delivered to the requesting LF.
177 ///
178 /// The `payload` field of the associated [`ErpEvent`] carries a BO4E
179 /// `Marktlokation` JSON object with the resolved MaLo data.
180 MaloIdentified,
181 /// The process failed permanently (regulatory timeout, data error, …).
182 ProcessFailed {
183 /// Human-readable failure description.
184 reason: Box<str>,
185 },
186}
187
188impl ErpEventType {
189 /// Short label for structured logging and metrics.
190 #[must_use]
191 pub fn label(&self) -> &'static str {
192 match self {
193 Self::ProcessInitiated => "process_initiated",
194 Self::AperakAccepted => "aperak_accepted",
195 Self::AperakRejected { .. } => "aperak_rejected",
196 Self::AperakTimeout => "aperak_timeout",
197 Self::ContrlReceived => "contrl_received",
198 Self::ProcessCompleted => "process_completed",
199 Self::MaloIdentified => "malo_identified",
200 Self::ProcessFailed { .. } => "process_failed",
201 }
202 }
203
204 /// CloudEvents 1.0 `type` attribute for this event.
205 ///
206 /// Follows the reverse-DNS prefix convention (`de.mako.<domain>.<action>`).
207 /// Used by the `WebhookErpAdapter` to populate the `type` field of the
208 /// CloudEvents envelope.
209 #[must_use]
210 pub fn cloud_event_type(&self) -> &'static str {
211 match self {
212 Self::ProcessInitiated => "de.mako.process.initiated",
213 Self::AperakAccepted => "de.mako.aperak.accepted",
214 Self::AperakRejected { .. } => "de.mako.aperak.rejected",
215 Self::AperakTimeout => "de.mako.aperak.timeout",
216 Self::ContrlReceived => "de.mako.contrl.received",
217 Self::ProcessCompleted => "de.mako.process.completed",
218 Self::MaloIdentified => "de.mako.malo.identified",
219 Self::ProcessFailed { .. } => "de.mako.process.failed",
220 }
221 }
222}
223
224// ── ErpEvent ──────────────────────────────────────────────────────────────────
225
226/// A structured process event delivered from `mako-engine` to the ERP.
227///
228/// The payload is always a **BO4E-typed JSON object** — the ERP adapter never
229/// receives raw EDIFACT bytes or EDIFACT format-version identifiers.
230///
231/// On the wire (via `WebhookErpAdapter`) this struct is serialised as a
232/// **[CloudEvents 1.0](https://cloudevents.io) structured-mode JSON** envelope
233/// with `Content-Type: application/cloudevents+json`. The BO4E payload lives
234/// in the CloudEvents `data` field; `payload_schema` maps to `dataschema`;
235/// `event_type` maps to the `type` attribute via [`ErpEventType::cloud_event_type`].
236///
237/// ## Idempotency
238///
239/// `idempotency_key` maps to the CloudEvents `id` attribute and is also sent
240/// as `X-Idempotency-Key` for ERP middleware that keys on headers. The ERP
241/// **must** persist this key and return `HTTP 200 OK` for duplicate deliveries.
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ErpEvent {
244 /// Stable dedup key — store in the ERP to reject duplicate deliveries.
245 ///
246 /// Derived from the outbox `message_id`; stable across retries.
247 pub idempotency_key: String,
248
249 /// Semantic classification of this event.
250 pub event_type: ErpEventType,
251
252 /// The mako process that generated this event.
253 pub process_id: ProcessId,
254
255 /// Tenant (operator GLN) that owns this process.
256 pub tenant_id: TenantId,
257
258 /// BDEW business conversation identifier.
259 pub conversation_id: ConversationId,
260
261 /// The mako domain event that directly caused this ERP notification.
262 pub causation_id: EventId,
263
264 /// Prüfidentifikator of the process.
265 pub pid: u32,
266
267 /// BO4E JSON Schema URL that validates [`payload`](ErpEvent::payload).
268 ///
269 /// Examples:
270 /// - `"https://raw.githubusercontent.com/BO4E/BO4E-Schemas/v202607.0.0/src/bo4e_schemas/bo/Marktlokation.json"`
271 /// - `"https://raw.githubusercontent.com/BO4E/BO4E-Schemas/v202607.0.0/src/bo4e_schemas/bo/Messlokation.json"`
272 ///
273 /// `None` for events where no primary BO4E object is applicable
274 /// (e.g. `ContrlReceived`).
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub payload_schema: Option<String>,
277
278 /// BO4E-typed payload.
279 ///
280 /// Deserialise using the ERP's own BO4E library. Raw EDIFACT structures
281 /// are never exposed here. `null` when no payload is applicable.
282 pub payload: serde_json::Value,
283
284 /// Wall-clock time when the domain event was persisted.
285 pub occurred_at: OffsetDateTime,
286
287 /// Workflow family name that produced this event (e.g. `"gpke-sperrung"`).
288 ///
289 /// Carried through from `OutboxMessage::workflow_name`. Emitted as the
290 /// `makoworkflow` CloudEvents extension attribute by `WebhookErpAdapter`.
291 /// `marktd` maps this to `marktrole` for role-scoped ERP subscriber fan-out.
292 ///
293 /// Empty string for events produced by legacy outbox messages that
294 /// predate this field.
295 pub workflow_name: Box<str>,
296}
297
298// ── ErpAdapter trait ──────────────────────────────────────────────────────────
299
300/// Outbound notification sink — `mako-engine` calls this when a process event
301/// should be reported to the ERP.
302///
303/// The payload is always a BO4E-typed JSON object; the adapter never receives
304/// raw EDIFACT bytes or format-version identifiers.
305///
306/// ## Contract
307///
308/// - Must be **idempotent** on `event.idempotency_key`. Called twice with the
309/// same key must succeed without double-posting.
310/// - Return [`ErpAdapterError::Transport`] for transient failures — the caller
311/// will retry with exponential backoff.
312/// - Return [`ErpAdapterError::Permanent`] for non-retryable failures — the
313/// caller will dead-letter the event.
314#[allow(async_fn_in_trait)]
315pub trait ErpAdapter: Send + Sync + 'static {
316 /// Deliver `event` to the ERP.
317 async fn notify(&self, event: ErpEvent) -> Result<(), ErpAdapterError>;
318}
319
320/// Blanket `Arc` implementation so `ErpAdapter` can be shared across tasks.
321impl<T: ErpAdapter> ErpAdapter for Arc<T> {
322 async fn notify(&self, event: ErpEvent) -> Result<(), ErpAdapterError> {
323 (**self).notify(event).await
324 }
325}
326
327// ── InboundErpCommand ─────────────────────────────────────────────────────────
328
329/// A BO4E business object received from the ERP, intended to trigger a mako
330/// process.
331///
332/// `mako-engine` maps the BO4E payload to an internal `Command` via the
333/// domain crate's command mapper.
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct InboundErpCommand {
336 /// Stable dedup key — forwarded to [`InboxStore::accept`].
337 ///
338 /// The ERP must supply a stable, unique identifier per command so that
339 /// retransmissions do not double-execute the workflow.
340 ///
341 /// [`InboxStore::accept`]: crate::inbox::InboxStore::accept
342 pub idempotency_key: String,
343
344 /// Tenant (operator GLN) that owns the target process.
345 pub tenant_id: TenantId,
346
347 /// BO4E JSON Schema URL — identifies the object type without inspecting
348 /// `payload`.
349 ///
350 /// Example:
351 /// `"https://raw.githubusercontent.com/BO4E/BO4E-Schemas/v202607.0.0/src/bo4e_schemas/bo/Vertrag.json"`
352 pub payload_schema: String,
353
354 /// BO4E-typed JSON payload. `mako-engine` maps this to an internal
355 /// `Command` via the registered domain command mapper.
356 pub payload: serde_json::Value,
357}
358
359// ── ErpCommandSource trait ────────────────────────────────────────────────────
360
361/// Inbound command source — `mako-engine` polls this for new BO4E objects
362/// from the ERP.
363///
364/// Implement this for broker-based inbound flows (Kafka consumer, SFTP poll,
365/// database change feed, …) to make the entire integration fully event-driven
366/// — no synchronous REST round-trip required.
367///
368/// ## Contract
369///
370/// - [`next`](ErpCommandSource::next) must be **non-blocking** when idle —
371/// return `Ok(None)` immediately when no command is available.
372/// - [`ack`](ErpCommandSource::ack) must suppress re-delivery of `id` after
373/// a successful ack (idempotent).
374/// - [`nack`](ErpCommandSource::nack) should allow re-delivery of `id` after
375/// an appropriate backoff.
376#[allow(async_fn_in_trait)]
377pub trait ErpCommandSource: Send + Sync + 'static {
378 /// Return the next pending BO4E command, or `None` when the source is idle.
379 async fn next(&self) -> Result<Option<InboundErpCommand>, ErpAdapterError>;
380
381 /// Acknowledge successful processing of `id`.
382 ///
383 /// After a successful ack the source must not re-deliver `id`.
384 async fn ack(&self, id: &str) -> Result<(), ErpAdapterError>;
385
386 /// Negative-acknowledge — allow re-delivery of `id` after backoff.
387 async fn nack(&self, id: &str, reason: &str) -> Result<(), ErpAdapterError>;
388}
389
390/// Blanket `Arc` implementation so `ErpCommandSource` can be shared across tasks.
391impl<S: ErpCommandSource> ErpCommandSource for Arc<S> {
392 async fn next(&self) -> Result<Option<InboundErpCommand>, ErpAdapterError> {
393 (**self).next().await
394 }
395 async fn ack(&self, id: &str) -> Result<(), ErpAdapterError> {
396 (**self).ack(id).await
397 }
398 async fn nack(&self, id: &str, reason: &str) -> Result<(), ErpAdapterError> {
399 (**self).nack(id, reason).await
400 }
401}
402
403// ── NoopErpAdapter ────────────────────────────────────────────────────────────
404
405/// An [`ErpAdapter`] that succeeds immediately without notifying anything.
406///
407/// Use in unit tests and CI where no real ERP endpoint is available.
408#[cfg(feature = "testing")]
409#[derive(Debug, Clone, Default)]
410pub struct NoopErpAdapter;
411
412#[cfg(feature = "testing")]
413impl ErpAdapter for NoopErpAdapter {
414 async fn notify(&self, _event: ErpEvent) -> Result<(), ErpAdapterError> {
415 Ok(())
416 }
417}
418
419// ── LogErpAdapter ─────────────────────────────────────────────────────────────
420
421/// An [`ErpAdapter`] that logs every event at `info` level without delivering
422/// it.
423///
424/// Useful as a development starting point — replace it with your concrete ERP
425/// adapter in production.
426#[derive(Debug, Clone, Default)]
427pub struct LogErpAdapter;
428
429impl ErpAdapter for LogErpAdapter {
430 async fn notify(&self, event: ErpEvent) -> Result<(), ErpAdapterError> {
431 tracing::info!(
432 idempotency_key = %event.idempotency_key,
433 event_type = event.event_type.label(),
434 process_id = %event.process_id,
435 tenant_id = %event.tenant_id,
436 pid = event.pid,
437 "ErpAdapter: event logged (no delivery configured)",
438 );
439 Ok(())
440 }
441}
442
443// ── NoopErpCommandSource ──────────────────────────────────────────────────────
444
445/// An [`ErpCommandSource`] that is always idle (returns `Ok(None)`).
446///
447/// Use in tests where no inbound ERP command flow is needed.
448#[cfg(feature = "testing")]
449#[derive(Debug, Clone, Default)]
450pub struct NoopErpCommandSource;
451
452#[cfg(feature = "testing")]
453impl ErpCommandSource for NoopErpCommandSource {
454 async fn next(&self) -> Result<Option<InboundErpCommand>, ErpAdapterError> {
455 Ok(None)
456 }
457 async fn ack(&self, _id: &str) -> Result<(), ErpAdapterError> {
458 Ok(())
459 }
460 async fn nack(&self, _id: &str, _reason: &str) -> Result<(), ErpAdapterError> {
461 Ok(())
462 }
463}
464
465// ── ErpAdapterTestHarness ─────────────────────────────────────────────────────
466
467/// A recording [`ErpAdapter`] for use in tests.
468///
469/// Records every [`ErpEvent`] delivered via [`notify`](ErpAdapter::notify) so
470/// tests can assert on event types, ordering, and BO4E payload shapes.
471///
472/// ```rust,ignore
473/// let harness = ErpAdapterTestHarness::new();
474/// my_workflow.run_with_adapter(harness.adapter()).await?;
475///
476/// let events = harness.events();
477/// assert_eq!(events[0].event_type, ErpEventType::ProcessInitiated);
478/// assert_eq!(events[1].event_type, ErpEventType::AperakAccepted);
479/// ```
480#[cfg(feature = "testing")]
481#[derive(Debug, Clone, Default)]
482pub struct ErpAdapterTestHarness {
483 events: Arc<tokio::sync::Mutex<Vec<ErpEvent>>>,
484}
485
486#[cfg(feature = "testing")]
487impl ErpAdapterTestHarness {
488 /// Create a new empty harness.
489 #[must_use]
490 pub fn new() -> Self {
491 Self::default()
492 }
493
494 /// Return a snapshot of all recorded events in delivery order.
495 pub async fn events(&self) -> Vec<ErpEvent> {
496 self.events.lock().await.clone()
497 }
498
499 /// Drain all recorded events, resetting the harness.
500 pub async fn drain(&self) -> Vec<ErpEvent> {
501 std::mem::take(&mut *self.events.lock().await)
502 }
503}
504
505#[cfg(feature = "testing")]
506impl ErpAdapter for ErpAdapterTestHarness {
507 async fn notify(&self, event: ErpEvent) -> Result<(), ErpAdapterError> {
508 self.events.lock().await.push(event);
509 Ok(())
510 }
511}
512
513// ── ErpCommandSourceTestHarness ───────────────────────────────────────────────
514
515/// A controllable [`ErpCommandSource`] for use in tests.
516///
517/// Inject canned [`InboundErpCommand`] payloads and verify that the engine
518/// processes them correctly.
519///
520/// ```text
521/// let source = ErpCommandSourceTestHarness::new();
522/// source.inject(InboundErpCommand {
523/// idempotency_key: "order-42".into(),
524/// tenant_id: TenantId::new(),
525/// payload_schema: ".../Vertrag.json".into(),
526/// payload: serde_json::json!({ "_typ": "VERTRAG", ... }),
527/// }).await;
528///
529/// // The engine picks up the command on the next poll.
530/// ```
531#[cfg(feature = "testing")]
532#[derive(Debug, Clone, Default)]
533pub struct ErpCommandSourceTestHarness {
534 queue: Arc<tokio::sync::Mutex<std::collections::VecDeque<InboundErpCommand>>>,
535 acked: Arc<tokio::sync::Mutex<Vec<String>>>,
536 nacked: Arc<tokio::sync::Mutex<Vec<(String, String)>>>,
537}
538
539#[cfg(feature = "testing")]
540impl ErpCommandSourceTestHarness {
541 /// Create a new empty harness.
542 #[must_use]
543 pub fn new() -> Self {
544 Self::default()
545 }
546
547 /// Enqueue a command to be returned by the next [`next`](ErpCommandSource::next) call.
548 pub async fn inject(&self, cmd: InboundErpCommand) {
549 self.queue.lock().await.push_back(cmd);
550 }
551
552 /// Return all acked command IDs.
553 pub async fn acked(&self) -> Vec<String> {
554 self.acked.lock().await.clone()
555 }
556
557 /// Return all nacked `(id, reason)` pairs.
558 pub async fn nacked(&self) -> Vec<(String, String)> {
559 self.nacked.lock().await.clone()
560 }
561}
562
563#[cfg(feature = "testing")]
564impl ErpCommandSource for ErpCommandSourceTestHarness {
565 async fn next(&self) -> Result<Option<InboundErpCommand>, ErpAdapterError> {
566 Ok(self.queue.lock().await.pop_front())
567 }
568
569 async fn ack(&self, id: &str) -> Result<(), ErpAdapterError> {
570 self.acked.lock().await.push(id.to_owned());
571 Ok(())
572 }
573
574 async fn nack(&self, id: &str, reason: &str) -> Result<(), ErpAdapterError> {
575 self.nacked
576 .lock()
577 .await
578 .push((id.to_owned(), reason.to_owned()));
579 Ok(())
580 }
581}
582
583// ── BO4E schema URL constants ─────────────────────────────────────────────────
584
585/// BO4E schema URL base for v202607.0.0.
586///
587/// Use `bo4e_schema_url!(Marktlokation)` to construct typed schema URLs at
588/// compile time.
589pub const BO4E_V202607_BASE: &str =
590 "https://raw.githubusercontent.com/BO4E/BO4E-Schemas/v202607.0.0/src/bo4e_schemas";
591
592/// Construct a BO4E v202607.0.0 JSON Schema URL for a Business Object.
593///
594/// ```rust
595/// use mako_engine::bo4e_schema_url;
596/// assert!(bo4e_schema_url!("bo", "Marktlokation").contains("Marktlokation"));
597/// ```
598#[macro_export]
599macro_rules! bo4e_schema_url {
600 ($category:literal, $name:literal) => {
601 concat!(
602 "https://raw.githubusercontent.com/BO4E/BO4E-Schemas/v202607.0.0/src/bo4e_schemas/",
603 $category,
604 "/",
605 $name,
606 ".json",
607 )
608 };
609}
610
611/// BO4E JSON Schema URL for `Marktlokation`.
612pub const BO4E_SCHEMA_MARKTLOKATION: &str = bo4e_schema_url!("bo", "Marktlokation");
613
614/// BO4E JSON Schema URL for `Messlokation`.
615pub const BO4E_SCHEMA_MESSLOKATION: &str = bo4e_schema_url!("bo", "Messlokation");
616
617/// BO4E JSON Schema URL for `Vertrag`.
618pub const BO4E_SCHEMA_VERTRAG: &str = bo4e_schema_url!("bo", "Vertrag");
619
620/// BO4E JSON Schema URL for `Energiemenge`.
621pub const BO4E_SCHEMA_ENERGIEMENGE: &str = bo4e_schema_url!("bo", "Energiemenge");
622
623/// BO4E JSON Schema URL for `Rechnung`.
624pub const BO4E_SCHEMA_RECHNUNG: &str = bo4e_schema_url!("bo", "Rechnung");
625
626/// BO4E JSON Schema URL for `Zaehler`.
627pub const BO4E_SCHEMA_ZAEHLER: &str = bo4e_schema_url!("bo", "Zaehler");
628
629/// BO4E JSON Schema URL for `Geschaeftspartner`.
630pub const BO4E_SCHEMA_GESCHAEFTSPARTNER: &str = bo4e_schema_url!("bo", "Geschaeftspartner");