use thiserror::Error;
#[derive(Debug)]
pub struct IoError(pub std::io::Error);
impl PartialEq for IoError {
fn eq(&self, other: &Self) -> bool {
self.0.kind() == other.0.kind()
}
}
impl std::fmt::Display for IoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::error::Error for IoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0.source()
}
}
impl From<std::io::Error> for IoError {
fn from(e: std::io::Error) -> Self {
Self(e)
}
}
#[derive(Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum EdifactError {
#[error("unexpected end of input at byte offset {offset}")]
UnexpectedEof {
offset: usize,
},
#[error("invalid delimiter byte 0x{byte:02X} at offset {offset}")]
InvalidDelimiter {
byte: u8,
offset: usize,
},
#[error("invalid EDIFACT text at byte offset {offset}")]
InvalidText {
offset: usize,
},
#[error("invalid release sequence at byte offset {offset}: dangling release character")]
InvalidReleaseSequence {
offset: usize,
},
#[error("interchange message count mismatch: UNZ declared {expected}, found {actual}")]
MessageCountMismatch {
expected: u32,
actual: u32,
},
#[error("segment count mismatch in message {message_ref}: UNT declared {expected}, found {actual}")]
SegmentCountMismatch {
expected: u32,
actual: u32,
message_ref: String,
},
#[error("invalid segment tag {0:?}")]
InvalidSegmentTag(String),
#[error("invalid UNA service string advice: must be exactly 9 bytes")]
InvalidUna,
#[error("missing required element {element_index} in segment {tag}")]
MissingRequiredElement {
tag: String,
element_index: usize,
},
#[error(
"missing required component {component_index} in element {element_index} of segment {tag}"
)]
MissingRequiredComponent {
tag: String,
element_index: usize,
component_index: usize,
},
#[error("serialized output contains invalid UTF-8")]
InvalidUtf8,
#[error(transparent)]
Io(#[from] IoError),
#[error("segment {tag} is not valid for message type {message_type}")]
InvalidSegmentForMessage {
tag: String,
message_type: String,
offset: usize,
},
#[error("segment {tag} has {actual} elements, expected between {min} and {max}")]
InvalidElementCount {
tag: String,
min: usize,
max: usize,
actual: usize,
offset: usize,
},
#[error("segment {tag} element {element_index} has {actual} components, expected {expected}")]
InvalidComponentCount {
tag: String,
element_index: usize,
expected: u8,
actual: u8,
offset: usize,
},
#[error(
"segment {tag} element {element_index}: '{value}' is not a valid code (code list {code_list})"
)]
InvalidCodeValue {
tag: String,
element_index: usize,
value: String,
code_list: String,
offset: usize,
suggestion: Option<&'static str>,
},
#[error("required segment {tag} is missing from message (position {expected_position})")]
MissingSegment {
tag: String,
expected_position: String,
},
#[error("segment {tag} has qualifier '{actual}', expected '{expected}'")]
QualifierMismatch {
tag: String,
actual: String,
expected: String,
offset: usize,
},
#[error("segment {tag} element {element_index}: conditional requirement not met ({condition})")]
ConditionalRequirementNotMet {
tag: String,
element_index: usize,
condition: String,
offset: usize,
},
#[error("validation failed with {error_count} issue(s); first issue: {first_message}")]
ValidationFailed {
error_count: usize,
first_message: String,
},
#[error("segment starting at byte offset {offset} exceeded maximum length of {limit} bytes")]
SegmentTooLong {
offset: usize,
limit: usize,
},
}
impl From<std::io::Error> for EdifactError {
fn from(e: std::io::Error) -> Self {
Self::Io(IoError(e))
}
}
impl EdifactError {
#[must_use]
pub const fn stable_code(&self) -> &'static str {
match self {
Self::UnexpectedEof { .. } => "E001",
Self::InvalidDelimiter { .. } => "E002",
Self::InvalidText { .. } => "E003",
Self::MessageCountMismatch { .. } => "E004",
Self::SegmentCountMismatch { .. } => "E005",
Self::InvalidSegmentTag(_) => "E006",
Self::InvalidUna => "E007",
Self::MissingRequiredElement { .. } => "E008",
Self::InvalidUtf8 => "E009",
Self::Io(_) => "E010",
Self::InvalidSegmentForMessage { .. } => "E011",
Self::InvalidElementCount { .. } => "E012",
Self::InvalidComponentCount { .. } => "E013",
Self::InvalidCodeValue { .. } => "E014",
Self::MissingSegment { .. } => "E015",
Self::QualifierMismatch { .. } => "E016",
Self::ConditionalRequirementNotMet { .. } => "E017",
Self::ValidationFailed { .. } => "E018",
Self::InvalidReleaseSequence { .. } => "E019",
Self::SegmentTooLong { .. } => "E020",
Self::MissingRequiredComponent { .. } => "E021",
}
}
#[must_use]
pub fn recovery_hint(&self) -> Option<&'static str> {
match self {
Self::UnexpectedEof { .. } => {
Some("Ensure every segment ends with the configured segment terminator")
}
Self::InvalidDelimiter { .. } => {
Some("Check UNA service string advice and delimiter bytes in the payload")
}
Self::InvalidText { .. } => {
Some("Input must be valid UTF-8 text for segment and element values")
}
Self::InvalidReleaseSequence { .. } => {
Some("Release character must escape one following byte; trailing '?' is invalid")
}
Self::InvalidSegmentTag(_) => Some("Segment tags must be 3 ASCII uppercase letters"),
Self::InvalidUna => {
Some("UNA must be exactly 9 bytes: 'UNA' followed by 6 service characters")
}
Self::MissingRequiredElement { .. } => {
Some("Provide all mandatory elements for the segment per directory rules")
}
Self::MissingRequiredComponent { .. } => {
Some("Provide all mandatory components for the composite element per directory rules")
}
Self::InvalidSegmentForMessage { .. } => {
Some("Remove unsupported segment or switch to the correct message type")
}
Self::InvalidElementCount { .. } => {
Some("Adjust the segment element count to the allowed min/max range")
}
Self::InvalidComponentCount { .. } => {
Some("Fix composite element arity to match the expected component count")
}
Self::InvalidCodeValue { .. } => {
Some("Use a value from the referenced code list for this element")
}
Self::MissingSegment { .. } => {
Some("Insert the required segment at the expected position")
}
Self::QualifierMismatch { .. } => {
Some("Set the segment qualifier to the expected value")
}
Self::ConditionalRequirementNotMet { .. } => {
Some("When the condition is met, include the conditionally required element")
}
Self::SegmentTooLong { limit, .. } => {
let _ = limit; Some("Increase max_segment_bytes in ReaderConfig or reject the input as malformed")
}
Self::ValidationFailed { .. }
| Self::MessageCountMismatch { .. }
| Self::SegmentCountMismatch { .. }
| Self::InvalidUtf8
| Self::Io(_) => None,
}
}
}
#[cfg(feature = "diagnostics")]
#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
impl miette::Diagnostic for EdifactError {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
Some(Box::new(self.stable_code()))
}
fn severity(&self) -> Option<miette::Severity> {
match self {
Self::InvalidCodeValue { .. }
| Self::InvalidComponentCount { .. }
| Self::QualifierMismatch { .. } => Some(miette::Severity::Warning),
_ => Some(miette::Severity::Error),
}
}
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
match self {
Self::InvalidUna => Some(Box::new(
"UNA segment must be exactly 9 bytes: 'UNA' + 6 service characters. See EDIFACT spec",
)),
Self::InvalidUtf8 => Some(Box::new(
"Internal error: serialized output contains invalid UTF-8. Please report this as a bug",
)),
Self::UnexpectedEof { offset } => Some(Box::new(format!(
"Check that all segments are terminated with the segment terminator (usually '). \
Reached end at offset {offset}",
))),
Self::InvalidDelimiter { byte, offset } => Some(Box::new(format!(
"The byte 0x{byte:02X} at offset {offset} is not a valid delimiter. \
Check UNA configuration",
))),
Self::InvalidText { offset } => Some(Box::new(format!(
"The byte sequence at offset {offset} contains invalid UTF-8. \
Ensure input is valid UTF-8",
))),
Self::InvalidReleaseSequence { offset } => Some(Box::new(format!(
"Release character at offset {offset} is dangling. \
Ensure '?' is followed by an escaped byte",
))),
Self::MessageCountMismatch { expected, actual } => Some(Box::new(format!(
"UNZ declares {expected} message(s) but {actual} UNH/UNT pair(s) were found. \
Check the UNZ message count",
))),
Self::SegmentCountMismatch { expected, actual, message_ref } => Some(Box::new(format!(
"UNT for message {message_ref} declares {expected} segment(s) but {actual} were found. \
Check the UNT segment count",
))),
Self::InvalidSegmentTag(tag) => Some(Box::new(format!(
"Segment tag '{tag}' must be exactly 3 ASCII uppercase letters",
))),
Self::MissingRequiredElement { tag, element_index } => Some(Box::new(format!(
"Segment {tag} requires element at index {element_index}",
))),
Self::MissingRequiredComponent { tag, element_index, component_index } => {
Some(Box::new(format!(
"Segment {tag} element {element_index} requires component at index {component_index}",
)))
}
Self::Io(e) => Some(Box::new(format!("I/O error: {e}"))),
Self::InvalidSegmentForMessage { tag, message_type, .. } => Some(Box::new(format!(
"Segment {tag} should not appear in a {message_type} message. \
Check the directory definition",
))),
Self::InvalidElementCount { tag, min, max, actual, .. } => Some(Box::new(format!(
"Segment {tag} should have between {min} and {max} elements, but has {actual}. \
Check segment structure",
))),
Self::InvalidComponentCount { tag, element_index, expected, actual, .. } => {
Some(Box::new(format!(
"In segment {tag}, element {element_index} should have {expected} components \
but has {actual}. Check element structure",
)))
}
Self::InvalidCodeValue { tag, element_index, value, code_list, .. } => {
Some(Box::new(format!(
"Value '{value}' in segment {tag} element {element_index} is not in the \
{code_list} code list. Check the directory for valid codes",
)))
}
Self::MissingSegment { tag, expected_position } => Some(Box::new(format!(
"Segment {tag} is required at position {expected_position} but is missing. \
Add this segment to the message",
))),
Self::QualifierMismatch { tag, actual, expected, .. } => Some(Box::new(format!(
"Segment {tag} has qualifier '{actual}' but expected '{expected}'. \
Check the segment's first component",
))),
Self::ConditionalRequirementNotMet { tag, element_index, condition, .. } => {
Some(Box::new(format!(
"In segment {tag}, element {element_index} is conditionally required when: \
{condition}. Check if the condition is met",
)))
}
Self::ValidationFailed { error_count, first_message } => Some(Box::new(format!(
"Validation found {error_count} issue(s). Start by fixing: {first_message}",
))),
Self::SegmentTooLong { offset, limit } => Some(Box::new(format!(
"Segment starting at byte offset {offset} exceeds the {limit}-byte limit. \
Use ReaderConfig::max_segment_bytes to adjust the limit if needed, \
or verify the input for a missing segment terminator",
))),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ValidationSeverity {
Critical,
Error,
Warning,
Info,
}
impl std::fmt::Display for ValidationSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Critical => f.write_str("critical"),
Self::Error => f.write_str("error"),
Self::Warning => f.write_str("warning"),
Self::Info => f.write_str("info"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ValidationIssue {
pub error_code: Option<&'static str>,
pub severity: ValidationSeverity,
pub message: String,
pub offset: Option<usize>,
pub segment_tag: Option<String>,
pub rule_id: Option<String>,
pub element_index: Option<u8>,
pub component_index: Option<u8>,
pub suggestion: Option<String>,
}
impl ValidationIssue {
pub fn new(severity: ValidationSeverity, message: impl Into<String>) -> Self {
Self {
error_code: None,
severity,
message: message.into(),
offset: None,
segment_tag: None,
rule_id: None,
element_index: None,
component_index: None,
suggestion: None,
}
}
pub fn with_error_code(mut self, code: &'static str) -> Self {
self.error_code = Some(code);
self
}
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
pub fn with_segment(mut self, tag: impl Into<String>) -> Self {
self.segment_tag = Some(tag.into());
self
}
pub fn with_rule_id(mut self, rule_id: impl Into<String>) -> Self {
self.rule_id = Some(rule_id.into());
self
}
pub fn with_element_index(mut self, element_index: u8) -> Self {
self.element_index = Some(element_index);
self
}
pub fn with_component_index(mut self, component_index: u8) -> Self {
self.component_index = Some(component_index);
self
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
#[must_use]
pub fn severity_label(&self) -> &'static str {
match self.severity {
ValidationSeverity::Critical => "CRITICAL",
ValidationSeverity::Error => "ERROR",
ValidationSeverity::Warning => "WARNING",
ValidationSeverity::Info => "INFO",
}
}
}
impl std::fmt::Display for ValidationIssue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.severity_label(), self.message)
}
}
impl std::error::Error for ValidationIssue {}
#[derive(Debug, Clone, Default)]
pub struct ValidationReport {
pub errors: Vec<ValidationIssue>,
pub warnings: Vec<ValidationIssue>,
pub infos: Vec<ValidationIssue>,
}
impl ValidationReport {
pub fn add_error(&mut self, issue: ValidationIssue) {
self.errors.push(issue);
}
pub fn add_warning(&mut self, issue: ValidationIssue) {
self.warnings.push(issue);
}
pub fn add_info(&mut self, issue: ValidationIssue) {
self.infos.push(issue);
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn total_issues(&self) -> usize {
self.errors.len() + self.warnings.len() + self.infos.len()
}
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
pub fn result(self) -> Result<Self, Self> {
if self.is_valid() {
Ok(self)
} else {
Err(self)
}
}
pub fn iter_issues(&self) -> impl Iterator<Item = &ValidationIssue> {
self.errors
.iter()
.chain(self.warnings.iter())
.chain(self.infos.iter())
}
pub fn has_any_issues(&self) -> bool {
!self.errors.is_empty() || !self.warnings.is_empty() || !self.infos.is_empty()
}
pub fn issues_for_rule_id<'a>(
&'a self,
rule_id: &'a str,
) -> impl Iterator<Item = &'a ValidationIssue> + 'a {
self.iter_issues()
.filter(move |issue| issue.rule_id.as_deref() == Some(rule_id))
}
fn filter_report<F>(&self, pred: F) -> Self
where
F: Fn(&ValidationIssue) -> bool,
{
Self {
errors: self.errors.iter().filter(|i| pred(i)).cloned().collect(),
warnings: self.warnings.iter().filter(|i| pred(i)).cloned().collect(),
infos: self.infos.iter().filter(|i| pred(i)).cloned().collect(),
}
}
pub fn filter_by_rule_id(&self, rule_id: &str) -> Self {
self.filter_report(|issue| issue.rule_id.as_deref() == Some(rule_id))
}
pub fn filter_by_rule_prefix(&self, prefix: &str) -> Self {
self.filter_report(|issue| {
issue
.rule_id
.as_deref()
.is_some_and(|id| id.starts_with(prefix))
})
}
pub fn render_deterministic(&self) -> String {
fn sorted_refs(issues: &[ValidationIssue]) -> Vec<&ValidationIssue> {
let mut refs: Vec<&ValidationIssue> = issues.iter().collect();
refs.sort_by(|left, right| {
left.offset
.unwrap_or(usize::MAX)
.cmp(&right.offset.unwrap_or(usize::MAX))
.then_with(|| {
left.segment_tag
.as_deref()
.unwrap_or("")
.cmp(right.segment_tag.as_deref().unwrap_or(""))
})
.then_with(|| {
left.rule_id
.as_deref()
.unwrap_or("")
.cmp(right.rule_id.as_deref().unwrap_or(""))
})
.then_with(|| {
left.element_index
.unwrap_or(u8::MAX)
.cmp(&right.element_index.unwrap_or(u8::MAX))
})
.then_with(|| {
left.component_index
.unwrap_or(u8::MAX)
.cmp(&right.component_index.unwrap_or(u8::MAX))
})
.then_with(|| {
left.error_code
.unwrap_or("")
.cmp(right.error_code.unwrap_or(""))
})
.then_with(|| left.message.cmp(&right.message))
});
refs
}
fn render_issue_line(out: &mut String, issue: &ValidationIssue) {
use std::fmt::Write as _;
out.push_str(" - ");
out.push_str(&issue.message);
if let Some(code) = issue.error_code {
out.push_str(" [");
out.push_str(code);
out.push(']');
}
if let Some(seg) = &issue.segment_tag {
out.push_str(" [segment=");
out.push_str(seg);
out.push(']');
}
if let Some(rule_id) = &issue.rule_id {
out.push_str(" [rule=");
out.push_str(rule_id);
out.push(']');
}
if let Some(element_index) = issue.element_index {
write!(out, " [element={element_index}]").ok();
}
if let Some(component_index) = issue.component_index {
write!(out, " [component={component_index}]").ok();
}
if let Some(offset) = issue.offset {
write!(out, " [offset={offset}]").ok();
}
if let Some(suggestion) = &issue.suggestion {
out.push_str(" [hint=");
out.push_str(suggestion);
out.push(']');
}
}
use std::fmt::Write as _;
let mut out = String::from("Validation Report:");
let errors = sorted_refs(&self.errors);
let warnings = sorted_refs(&self.warnings);
let infos = sorted_refs(&self.infos);
if !errors.is_empty() {
write!(out, "\n Errors ({})", errors.len()).ok();
for issue in &errors {
out.push('\n');
render_issue_line(&mut out, issue);
}
}
if !warnings.is_empty() {
write!(out, "\n Warnings ({})", warnings.len()).ok();
for issue in &warnings {
out.push('\n');
render_issue_line(&mut out, issue);
}
}
if !infos.is_empty() {
write!(out, "\n Info ({})", infos.len()).ok();
for issue in &infos {
out.push('\n');
render_issue_line(&mut out, issue);
}
}
out
}
}
#[cfg(feature = "diagnostics")]
impl miette::Diagnostic for ValidationReport {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
Some(Box::new("VALIDATION"))
}
fn severity(&self) -> Option<miette::Severity> {
if self.has_errors() {
Some(miette::Severity::Error)
} else if self.has_warnings() {
Some(miette::Severity::Warning)
} else {
Some(miette::Severity::Advice)
}
}
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
let msg = format!(
"Validation found {} error(s), {} warning(s), {} info(s)",
self.errors.len(),
self.warnings.len(),
self.infos.len()
);
Some(Box::new(msg))
}
}
impl std::fmt::Display for ValidationReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.render_deterministic())
}
}
impl std::error::Error for ValidationReport {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_report_collects_errors() {
let mut report = ValidationReport::default();
report.add_error(
ValidationIssue::new(ValidationSeverity::Error, "Test error")
.with_segment("BGM")
.with_offset(42),
);
report.add_warning(ValidationIssue::new(
ValidationSeverity::Warning,
"Test warning",
));
assert!(report.has_errors());
assert!(report.has_warnings());
assert_eq!(report.total_issues(), 2);
assert!(!report.is_valid());
}
#[test]
fn validation_report_result_conversion() {
let mut report = ValidationReport::default();
report.add_error(ValidationIssue::new(
ValidationSeverity::Error,
"Critical issue",
));
let result = report.result();
assert!(result.is_err());
}
#[test]
fn validation_report_passes_when_no_errors() {
let mut report = ValidationReport::default();
report.add_warning(ValidationIssue::new(
ValidationSeverity::Warning,
"Just a warning",
));
assert!(report.is_valid());
assert!(report.result().is_ok());
}
#[test]
fn validation_issue_builder() {
let issue = ValidationIssue::new(ValidationSeverity::Warning, "test message")
.with_error_code("E013")
.with_offset(100)
.with_segment("NAD")
.with_rule_id("DEMO-P001")
.with_element_index(1)
.with_component_index(2)
.with_suggestion("Check element count");
assert_eq!(issue.error_code, Some("E013"));
assert_eq!(issue.message, "test message");
assert_eq!(issue.offset, Some(100));
assert_eq!(issue.segment_tag, Some("NAD".to_owned()));
assert_eq!(issue.rule_id, Some("DEMO-P001".to_owned()));
assert_eq!(issue.element_index, Some(1));
assert_eq!(issue.component_index, Some(2));
assert_eq!(issue.suggestion, Some("Check element count".to_owned()));
}
#[test]
fn validation_report_display() {
let mut report = ValidationReport::default();
report.add_error(
ValidationIssue::new(ValidationSeverity::Error, "Error 1")
.with_error_code("E011")
.with_offset(8),
);
report.add_warning(ValidationIssue::new(
ValidationSeverity::Warning,
"Warning 1",
));
report.add_info(ValidationIssue::new(ValidationSeverity::Info, "Info 1"));
let display_str = format!("{}", report);
assert!(display_str.contains("Errors (1)"));
assert!(display_str.contains("Warnings (1)"));
assert!(display_str.contains("Info (1)"));
assert!(display_str.contains("[E011]"));
}
#[test]
fn validation_report_render_is_deterministic() {
let mut report = ValidationReport::default();
report.add_error(
ValidationIssue::new(ValidationSeverity::Error, "later")
.with_segment("BGM")
.with_offset(20),
);
report.add_error(
ValidationIssue::new(ValidationSeverity::Error, "earlier")
.with_segment("UNH")
.with_offset(1),
);
let rendered = report.render_deterministic();
let first = rendered.find("earlier").expect("missing first issue");
let second = rendered.find("later").expect("missing second issue");
assert!(first < second, "expected deterministic sort by offset");
}
#[test]
fn recovery_hint_exists_for_common_malformed_cases() {
let err = EdifactError::InvalidReleaseSequence { offset: 10 };
assert!(err.recovery_hint().is_some());
let err = EdifactError::InvalidCodeValue {
tag: "BGM".to_owned(),
element_index: 0,
value: "X".to_owned(),
code_list: "1001".to_owned(),
offset: 0,
suggestion: None,
};
assert!(err.recovery_hint().is_some());
}
#[test]
fn validation_report_can_filter_by_rule_id() {
let mut report = ValidationReport::default();
report.add_error(
ValidationIssue::new(ValidationSeverity::Error, "orders policy blocked")
.with_rule_id("ORDERS-P001"),
);
report.add_warning(
ValidationIssue::new(ValidationSeverity::Warning, "invoic policy warning")
.with_rule_id("INVOIC-P001"),
);
report.add_info(
ValidationIssue::new(ValidationSeverity::Info, "orders policy info")
.with_rule_id("ORDERS-P002"),
);
let only_orders_block = report.filter_by_rule_id("ORDERS-P001");
assert_eq!(only_orders_block.errors.len(), 1);
assert!(only_orders_block.warnings.is_empty());
assert!(only_orders_block.infos.is_empty());
let orders_family = report.filter_by_rule_prefix("ORDERS-");
assert_eq!(orders_family.total_issues(), 2);
assert!(orders_family.has_errors());
assert!(!orders_family.has_warnings());
let exact: Vec<_> = report.issues_for_rule_id("INVOIC-P001").collect();
assert_eq!(exact.len(), 1);
assert_eq!(exact[0].message, "invoic policy warning");
}
}