1use edifact_rs::OwnedSegment;
4use time::{OffsetDateTime, UtcOffset};
5
6use crate::{
7 datetime::{self, DtmFormat, DtmValue, DvgwPeriod},
8 document::{Carrier, DvgwDocument, DvgwMessageType},
9 error::{Error, sanitize_code},
10 model::{ItemDescription, LineItem, LocationGroup, Party, Quantity, Reference, nad, rff},
11 pruefidentifikator::Pruefidentifikator,
12 version::DvgwVersion,
13};
14
15#[derive(Debug, Clone)]
23#[non_exhaustive]
24pub struct DvgwMessage {
25 pub message_type: DvgwMessageType,
27 pub document: DvgwDocument,
29 pub carrier: Carrier,
31 pub version: Option<DvgwVersion>,
33 pub message_ref: String,
35 pub document_number: Option<String>,
37 pub pruefidentifikator: Option<Pruefidentifikator>,
39 pub timezone: UtcOffset,
42 pub message_datetime: Option<OffsetDateTime>,
44 pub validity_period: Option<DvgwPeriod>,
49 pub original_nomination_datetime: Option<OffsetDateTime>,
52 pub references: Vec<Reference>,
54 pub parties: Vec<Party>,
56 pub items: Vec<LineItem>,
58 pub(crate) undecodable_dtm: Vec<String>,
61 segments: Vec<OwnedSegment>,
63}
64
65impl DvgwMessage {
66 #[must_use]
68 pub fn sender(&self) -> Option<&Party> {
69 self.party(nad::ABSENDER)
70 }
71
72 #[must_use]
74 pub fn receiver(&self) -> Option<&Party> {
75 self.party(nad::EMPFAENGER)
76 }
77
78 #[must_use]
80 pub fn party(&self, role: &str) -> Option<&Party> {
81 self.parties.iter().find(|p| p.role == role)
82 }
83
84 #[must_use]
86 pub fn reference(&self, qualifier: &str) -> Option<&str> {
87 self.references
88 .iter()
89 .find(|r| r.qualifier == qualifier)
90 .map(|r| r.value.as_str())
91 }
92
93 #[must_use]
95 pub fn clearingnummer(&self) -> Option<&str> {
96 self.reference(rff::CLEARINGNUMMER)
97 }
98
99 #[must_use]
104 pub fn original_nomination_ref(&self) -> Option<&str> {
105 self.reference(rff::ORIGINAL_NOMINIERUNG)
106 }
107
108 pub fn quantities(&self) -> impl Iterator<Item = &Quantity> {
110 self.items.iter().flat_map(LineItem::quantities)
111 }
112
113 #[must_use]
123 pub fn energy_by_qualifier(&self) -> crate::model::EnergyByQualifier {
124 self.energy_by_qualifier_where(|_| true)
125 }
126
127 #[must_use]
134 pub fn energy_by_qualifier_where(
135 &self,
136 keep: impl Fn(&LineItem) -> bool,
137 ) -> crate::model::EnergyByQualifier {
138 let mut totals = crate::model::EnergyByQualifier::new();
139 for quantity in self
140 .items
141 .iter()
142 .filter(|item| keep(item))
143 .flat_map(LineItem::quantities)
144 {
145 if let Some(kwh) = quantity.energy_kwh() {
146 *totals.entry(quantity.qualifier.clone()).or_default() += kwh;
147 }
148 }
149 totals
150 }
151
152 #[must_use]
160 pub fn single_energy_kwh(
161 &self,
162 keep: impl Fn(&LineItem) -> bool + Copy,
163 ) -> Option<rust_decimal::Decimal> {
164 let selected: Vec<&LineItem> = self.items.iter().filter(|i| keep(i)).collect();
165 if selected.is_empty() {
166 return None;
167 }
168 let complete = selected
170 .iter()
171 .flat_map(|i| i.quantities())
172 .all(|q| q.energy_kwh().is_some());
173 if !complete {
174 return None;
175 }
176 let totals = self.energy_by_qualifier_where(keep);
177 match totals.len() {
178 1 => totals.into_values().next(),
179 _ => None,
180 }
181 }
182
183 #[must_use]
189 pub fn energy_is_complete(&self) -> bool {
190 let mut any = false;
191 for quantity in self.quantities() {
192 any = true;
193 if quantity.energy_kwh().is_none() {
194 return false;
195 }
196 }
197 any
198 }
199
200 #[must_use]
202 pub fn segments(&self) -> &[OwnedSegment] {
203 &self.segments
204 }
205
206 pub fn serialize(&self) -> Result<Vec<u8>, Error> {
216 edifact_rs::segments_to_bytes(&self.segments).map_err(|e| Error::Serialize(e.to_string()))
217 }
218
219 pub(crate) fn from_segments(segments: Vec<OwnedSegment>) -> Result<Self, Error> {
229 let unh = find(&segments, "UNH").ok_or(Error::MissingSegment("UNH"))?;
230 let message_ref = unh.element_str(0).unwrap_or_default().to_owned();
231 let carrier_code = unh.component_str(1, 0).unwrap_or_default().to_owned();
232 let version = unh.component_str(1, 4).and_then(DvgwVersion::parse);
233
234 let bgm = find(&segments, "BGM").ok_or(Error::MissingSegment("BGM"))?;
235 let document_code = bgm.component_str(0, 0).unwrap_or_default();
236 let document =
237 DvgwDocument::from_code(document_code).ok_or_else(|| Error::UnknownDocumentCode {
238 raw_code: sanitize_code(document_code),
239 })?;
240 let document_number = bgm.component_str(1, 0).map(str::to_owned);
241
242 let expected = document.carrier();
244 let carrier = match Carrier::from_unh_code(&carrier_code) {
245 Some(c) if c == expected => c,
246 _ => {
247 return Err(Error::CarrierMismatch {
248 document: document.code(),
249 expected: expected.as_str(),
250 raw_code: sanitize_code(&carrier_code),
251 });
252 }
253 };
254
255 let mut undecodable_dtm = Vec::new();
258 let timezone = header_dtm(&segments, "Z05", UtcOffset::UTC, &mut undecodable_dtm)
259 .and_then(DtmValue::as_hours)
260 .and_then(|h| UtcOffset::from_hms(h, 0, 0).ok())
261 .unwrap_or(UtcOffset::UTC);
262
263 let message_datetime = header_dtm(&segments, "137", timezone, &mut undecodable_dtm)
264 .and_then(DtmValue::as_instant);
265 let validity_period = header_dtm(&segments, "Z01", timezone, &mut undecodable_dtm)
266 .and_then(DtmValue::as_period);
267 let original_nomination_datetime =
268 header_dtm(&segments, "9", timezone, &mut undecodable_dtm)
269 .and_then(DtmValue::as_instant);
270
271 let header_end = segments
272 .iter()
273 .position(|s| s.tag == "LIN")
274 .unwrap_or(segments.len());
275 let header = &segments[..header_end];
276
277 let references: Vec<Reference> = header
278 .iter()
279 .filter(|s| s.tag == "RFF")
280 .filter_map(read_reference)
281 .collect();
282 let parties: Vec<Party> = header
283 .iter()
284 .filter(|s| s.tag == "NAD")
285 .filter_map(read_party)
286 .collect();
287 let pruefidentifikator = references
288 .iter()
289 .find(|r| r.qualifier == rff::PRUEFIDENTIFIKATOR)
290 .and_then(|r| r.value.parse::<Pruefidentifikator>().ok());
291
292 let items = parse_items(&segments[header_end..], timezone, &mut undecodable_dtm);
293
294 Ok(Self {
295 message_type: document.message_type(),
296 document,
297 carrier,
298 version,
299 message_ref,
300 document_number,
301 pruefidentifikator,
302 timezone,
303 message_datetime,
304 validity_period,
305 original_nomination_datetime,
306 references,
307 parties,
308 items,
309 undecodable_dtm,
310 segments,
311 })
312 }
313}
314
315fn find<'a>(segments: &'a [OwnedSegment], tag: &str) -> Option<&'a OwnedSegment> {
318 segments.iter().find(|s| s.tag == tag)
319}
320
321fn read_reference(seg: &OwnedSegment) -> Option<Reference> {
322 Some(Reference {
323 qualifier: seg.component_str(0, 0)?.to_owned(),
324 value: seg.component_str(0, 1).unwrap_or_default().to_owned(),
325 })
326}
327
328fn read_party(seg: &OwnedSegment) -> Option<Party> {
329 Some(Party {
330 role: seg.element_str(0)?.to_owned(),
331 id: seg.component_str(1, 0).unwrap_or_default().to_owned(),
332 agency: seg
333 .component_str(1, 2)
334 .filter(|a| !a.is_empty())
335 .map(str::to_owned),
336 })
337}
338
339fn read_item_description(seg: &OwnedSegment) -> ItemDescription {
340 ItemDescription {
341 characteristic: seg
342 .element_str(1)
343 .filter(|s| !s.is_empty())
344 .map(str::to_owned),
345 code: seg
346 .component_str(2, 0)
347 .filter(|s| !s.is_empty())
348 .map(str::to_owned),
349 }
350}
351
352fn read_dtm(
357 seg: &OwnedSegment,
358 offset: UtcOffset,
359 undecodable: &mut Vec<String>,
360) -> Option<DtmValue> {
361 let qualifier = seg.component_str(0, 0)?;
362 let value = seg.component_str(0, 1).unwrap_or_default();
363 let format = seg.component_str(0, 2).and_then(DtmFormat::from_code);
364 let Some(format) = format else {
365 undecodable.push(qualifier.to_owned());
366 return None;
367 };
368 let decoded = datetime::decode(value, format, offset);
369 if decoded.is_none() {
370 undecodable.push(qualifier.to_owned());
371 }
372 decoded
373}
374
375fn header_dtm(
376 segments: &[OwnedSegment],
377 qualifier: &str,
378 offset: UtcOffset,
379 undecodable: &mut Vec<String>,
380) -> Option<DtmValue> {
381 let header_end = segments
382 .iter()
383 .position(|s| s.tag == "LIN")
384 .unwrap_or(segments.len());
385 let seg = segments[..header_end]
386 .iter()
387 .find(|s| s.tag == "DTM" && s.component_str(0, 0) == Some(qualifier))?;
388 read_dtm(seg, offset, undecodable)
389}
390
391fn parse_items(
397 segments: &[OwnedSegment],
398 offset: UtcOffset,
399 undecodable: &mut Vec<String>,
400) -> Vec<LineItem> {
401 let mut items: Vec<LineItem> = Vec::new();
402 let mut current_period: Option<DvgwPeriod> = None;
404
405 for seg in segments {
406 match &*seg.tag {
407 "LIN" => {
408 items.push(LineItem {
409 number: seg
410 .element_str(0)
411 .filter(|s| !s.is_empty())
412 .map(str::to_owned),
413 item_type: seg
416 .component_str(2, 1)
417 .filter(|s| !s.is_empty())
418 .map(str::to_owned),
419 descriptions: Vec::new(),
420 locations: Vec::new(),
421 parties: Vec::new(),
422 });
423 current_period = None;
424 }
425 "IMD" => {
426 if let Some(item) = items.last_mut() {
427 item.descriptions.push(read_item_description(seg));
428 }
429 }
430 "LOC" => {
431 if let Some(item) = items.last_mut() {
432 item.locations.push(LocationGroup {
433 qualifier: seg.element_str(0).unwrap_or_default().to_owned(),
434 code: seg
435 .component_str(1, 0)
436 .filter(|s| !s.is_empty())
437 .map(str::to_owned),
438 agency: seg
439 .component_str(1, 2)
440 .filter(|s| !s.is_empty())
441 .map(str::to_owned),
442 quantities: Vec::new(),
443 });
444 }
445 current_period = None;
446 }
447 "DTM" => {
450 if let Some(period) =
451 read_dtm(seg, offset, undecodable).and_then(DtmValue::as_period)
452 {
453 current_period = Some(period);
454 }
455 }
456 "QTY" => {
457 let raw_value = seg.component_str(0, 1).unwrap_or_default().to_owned();
458 let quantity = Quantity {
459 qualifier: seg.component_str(0, 0).unwrap_or_default().to_owned(),
460 value: raw_value.parse().ok(),
461 raw_value,
462 unit: seg
463 .component_str(0, 2)
464 .filter(|s| !s.is_empty())
465 .map(str::to_owned),
466 period: current_period,
467 status: Vec::new(),
468 };
469 if let Some(location) = items.last_mut().and_then(|i| i.locations.last_mut()) {
470 location.quantities.push(quantity);
471 }
472 }
473 "STS" => {
474 let code = seg.component_str(0, 0).filter(|s| !s.is_empty());
475 let target = items
476 .last_mut()
477 .and_then(|i| i.locations.last_mut())
478 .and_then(|l| l.quantities.last_mut());
479 if let (Some(code), Some(quantity)) = (code, target) {
480 quantity.status.push(code.to_owned());
481 }
482 }
483 "NAD" => {
484 if let (Some(item), Some(party)) = (items.last_mut(), read_party(seg)) {
485 item.parties.push(party);
486 }
487 }
488 _ => {}
489 }
490 }
491 items
492}