use std::future::Future;
use mako_engine::types::{MaLo, MarktpartnerCode};
use crate::wertebestellung::WertebestellungCommand;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConsentPerspective {
#[default]
MsbInbound,
EsaOutbound,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConsentCode {
Active,
SelfAssertion,
NoConsent,
Revoked,
FrameworkRejected,
}
impl ConsentCode {
#[must_use]
pub const fn allowed(self) -> bool {
matches!(self, Self::Active | Self::SelfAssertion)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsentDecision {
pub allowed: bool,
pub code: ConsentCode,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsentGateError(pub String);
impl std::fmt::Display for ConsentGateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ConsentGateError {}
pub trait ConsentGate {
fn check(
&self,
esa_mp_id: &MarktpartnerCode,
msb_mp_id: &MarktpartnerCode,
location_id: &str,
perspective: ConsentPerspective,
) -> impl Future<Output = Result<ConsentDecision, ConsentGateError>> + Send;
}
pub async fn gate_inbound<G: ConsentGate>(
cmd: WertebestellungCommand,
esa: &MarktpartnerCode,
msb: &MarktpartnerCode,
location: &MaLo,
gate: &G,
) -> WertebestellungCommand {
use WertebestellungCommand as C;
if !matches!(cmd, C::ReceiveAnfrage { .. } | C::ReceiveBestellung { .. }) {
return cmd;
}
let location: &str = if location.as_str().is_empty() {
if let C::ReceiveAnfrage { lokations_id, .. } = &cmd {
lokations_id.as_str()
} else {
location.as_str()
}
} else {
location.as_str()
};
if esa.as_str().is_empty() || location.is_empty() {
return cmd;
}
let location = location.to_owned();
let decision = match gate
.check(esa, msb, &location, ConsentPerspective::MsbInbound)
.await
{
Ok(d) => d,
Err(_) => return cmd,
};
if decision.allowed {
return cmd;
}
match cmd {
C::ReceiveAnfrage {
pid,
esa,
msb,
ebene,
lokations_id,
gegenstand,
message_ref,
quittung,
..
} => C::ReceiveAnfrage {
pid,
esa,
msb,
ebene,
lokations_id,
gegenstand,
message_ref,
quittung,
consent_block: Some(decision.reason),
},
C::ReceiveBestellung {
pid,
message_ref,
abonnement,
quittung,
..
} => C::ReceiveBestellung {
pid,
message_ref,
abonnement,
quittung,
consent_block: Some(decision.reason),
},
other => other,
}
}
pub async fn gate_outbound<G: ConsentGate>(
esa: &MarktpartnerCode,
msb: &MarktpartnerCode,
location: &MaLo,
gate: &G,
) -> Result<(), String> {
match gate
.check(esa, msb, location.as_str(), ConsentPerspective::EsaOutbound)
.await
{
Ok(d) if d.allowed => Ok(()),
Ok(d) => Err(format!(
"ESA-Einwilligung fehlt für {location}: {} (Rechtsgrundlage nach GDPR Art. 7 \
erforderlich, bevor der ESA Werte anfragt)",
d.reason
)),
Err(e) => Err(format!(
"ESA-Einwilligung konnte nicht geprüft werden für {location}: {e}"
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wertebestellung::{Lokationsebene, Zustellquittung};
use mako_engine::types::{MessageRef, Pruefidentifikator};
use time::OffsetDateTime;
struct FakeGate(Result<ConsentDecision, ConsentGateError>);
impl FakeGate {
fn allowing() -> Self {
Self(Ok(ConsentDecision {
allowed: true,
code: ConsentCode::Active,
reason: "aktive Einwilligung".to_owned(),
}))
}
fn blocking(reason: &str) -> Self {
Self(Ok(ConsentDecision {
allowed: false,
code: ConsentCode::Revoked,
reason: reason.to_owned(),
}))
}
fn failing() -> Self {
Self(Err(ConsentGateError("marktd unreachable".to_owned())))
}
}
impl ConsentGate for FakeGate {
async fn check(
&self,
_esa: &MarktpartnerCode,
_msb: &MarktpartnerCode,
_location: &str,
_perspective: ConsentPerspective,
) -> Result<ConsentDecision, ConsentGateError> {
self.0.clone()
}
}
fn gegenstand() -> Box<crate::esa::Bestellgegenstand> {
Box::new(crate::esa::Bestellgegenstand {
messprodukt: "9991000003056".to_owned(),
wunschtermin: time::macros::date!(2026 - 03 - 01),
zeitraum_bis: None,
abonnement: crate::esa::Abonnement::StartAbo,
smgw: None,
})
}
fn anfrage() -> WertebestellungCommand {
WertebestellungCommand::ReceiveAnfrage {
pid: Pruefidentifikator::const_new(35003),
esa: MarktpartnerCode::new("9900000000001"),
msb: MarktpartnerCode::new("9900000000002"),
ebene: Lokationsebene::Marktlokation,
lokations_id: "57685676748".to_owned(),
gegenstand: gegenstand(),
message_ref: MessageRef::new("MSG-1"),
quittung: Zustellquittung::positive(OffsetDateTime::UNIX_EPOCH),
consent_block: None,
}
}
fn bestellung() -> WertebestellungCommand {
WertebestellungCommand::ReceiveBestellung {
pid: Pruefidentifikator::const_new(17007),
message_ref: MessageRef::new("MSG-2"),
abonnement: crate::esa::Abonnement::StartAbo,
quittung: Zustellquittung::positive(OffsetDateTime::UNIX_EPOCH),
consent_block: None,
}
}
fn ids() -> (MarktpartnerCode, MarktpartnerCode, MaLo) {
(
MarktpartnerCode::new("9900000000001"),
MarktpartnerCode::new("9900000000002"),
MaLo::new("57685676748"),
)
}
fn consent_block(cmd: &WertebestellungCommand) -> Option<&str> {
match cmd {
WertebestellungCommand::ReceiveAnfrage { consent_block, .. }
| WertebestellungCommand::ReceiveBestellung { consent_block, .. } => {
consent_block.as_deref()
}
_ => None,
}
}
#[tokio::test]
async fn inbound_active_consent_passes_untouched() {
let (esa, msb, loc) = ids();
let cmd = gate_inbound(anfrage(), &esa, &msb, &loc, &FakeGate::allowing()).await;
assert_eq!(consent_block(&cmd), None);
}
#[tokio::test]
async fn inbound_block_rewrites_consent_block_on_anfrage_and_bestellung() {
let (esa, msb, loc) = ids();
let gate = FakeGate::blocking("Einwilligung widerrufen");
let cmd = gate_inbound(anfrage(), &esa, &msb, &loc, &gate).await;
assert_eq!(consent_block(&cmd), Some("Einwilligung widerrufen"));
let cmd = gate_inbound(bestellung(), &esa, &msb, &loc, &gate).await;
assert_eq!(consent_block(&cmd), Some("Einwilligung widerrufen"));
}
#[tokio::test]
async fn inbound_lookup_error_fails_open() {
let (esa, msb, loc) = ids();
let cmd = gate_inbound(bestellung(), &esa, &msb, &loc, &FakeGate::failing()).await;
assert_eq!(consent_block(&cmd), None);
}
#[tokio::test]
async fn inbound_empty_location_falls_back_to_anfrage_lokations_id() {
let (esa, msb, _) = ids();
let cmd = gate_inbound(
anfrage(),
&esa,
&msb,
&MaLo::new(""),
&FakeGate::blocking("widerrufen"),
)
.await;
assert_eq!(consent_block(&cmd), Some("widerrufen"));
let cmd = gate_inbound(
bestellung(),
&esa,
&msb,
&MaLo::new(""),
&FakeGate::blocking("widerrufen"),
)
.await;
assert_eq!(consent_block(&cmd), None);
}
#[tokio::test]
async fn inbound_non_gated_variant_is_untouched_even_when_blocked() {
let (esa, msb, loc) = ids();
let storno = WertebestellungCommand::ReceiveStornierung {
pid: Pruefidentifikator::const_new(39002),
message_ref: MessageRef::new("MSG-3"),
quittung: Zustellquittung::positive(OffsetDateTime::UNIX_EPOCH),
};
let cmd = gate_inbound(
storno.clone(),
&esa,
&msb,
&loc,
&FakeGate::blocking("widerrufen"),
)
.await;
assert_eq!(cmd, storno);
}
#[tokio::test]
async fn outbound_active_consent_allows() {
let (esa, msb, loc) = ids();
assert_eq!(
gate_outbound(&esa, &msb, &loc, &FakeGate::allowing()).await,
Ok(())
);
}
#[tokio::test]
async fn outbound_block_rejects_with_begruendung() {
let (esa, msb, loc) = ids();
let err = gate_outbound(&esa, &msb, &loc, &FakeGate::blocking("keine Einwilligung"))
.await
.unwrap_err();
assert_eq!(
err,
"ESA-Einwilligung fehlt für 57685676748: keine Einwilligung (Rechtsgrundlage \
nach GDPR Art. 7 erforderlich, bevor der ESA Werte anfragt)"
);
}
#[tokio::test]
async fn outbound_lookup_error_fails_closed() {
let (esa, msb, loc) = ids();
let err = gate_outbound(&esa, &msb, &loc, &FakeGate::failing())
.await
.unwrap_err();
assert_eq!(
err,
"ESA-Einwilligung konnte nicht geprüft werden für 57685676748: marktd unreachable"
);
}
}