1use time::{OffsetDateTime, UtcOffset};
46
47use crate::{
48 datetime::{DvgwPeriod, format_instant, format_period},
49 document::{DVGW_AGENCY_CODE, DvgwDocument},
50 error::Error,
51 model::{nad, rff},
52};
53
54#[derive(Debug, Clone, Default)]
56pub struct Position {
57 number: Option<String>,
58 item_type: Option<String>,
59 description: Option<String>,
60 locations: Vec<LocationDraft>,
61 parties: Vec<PartyDraft>,
62}
63
64#[derive(Debug, Clone)]
65struct PartyDraft {
66 role: String,
67 code: String,
68 agency: String,
69}
70
71#[derive(Debug, Clone)]
72struct LocationDraft {
73 qualifier: String,
74 code: Option<String>,
75 quantities: Vec<QuantityDraft>,
76}
77
78#[derive(Debug, Clone)]
79struct QuantityDraft {
80 qualifier: String,
81 value: String,
82 unit: Option<String>,
83 period: DvgwPeriod,
84 status: Vec<String>,
85}
86
87impl Position {
88 #[must_use]
90 pub fn new() -> Self {
91 Self::default()
92 }
93
94 #[must_use]
96 pub fn number(mut self, number: impl Into<String>) -> Self {
97 self.number = Some(number.into());
98 self
99 }
100
101 #[must_use]
103 pub fn item_type(mut self, code: impl Into<String>) -> Self {
104 self.item_type = Some(code.into());
105 self
106 }
107
108 #[must_use]
110 pub fn description(mut self, code: impl Into<String>) -> Self {
111 self.description = Some(code.into());
112 self
113 }
114
115 #[must_use]
118 pub fn location(mut self, qualifier: impl Into<String>, code: Option<&str>) -> Self {
119 self.locations.push(LocationDraft {
120 qualifier: qualifier.into(),
121 code: code.map(str::to_owned),
122 quantities: Vec::new(),
123 });
124 self
125 }
126
127 #[must_use]
139 pub fn quantity(
140 self,
141 qualifier: impl Into<String>,
142 value: impl Into<String>,
143 period: DvgwPeriod,
144 ) -> Self {
145 self.push_quantity(qualifier.into(), value.into(), None, period)
146 }
147
148 #[must_use]
155 pub fn quantity_in(
156 self,
157 qualifier: impl Into<String>,
158 value: impl Into<String>,
159 unit: impl Into<String>,
160 period: DvgwPeriod,
161 ) -> Self {
162 self.push_quantity(qualifier.into(), value.into(), Some(unit.into()), period)
163 }
164
165 fn push_quantity(
166 mut self,
167 qualifier: String,
168 value: String,
169 unit: Option<String>,
170 period: DvgwPeriod,
171 ) -> Self {
172 let location = self
173 .locations
174 .last_mut()
175 .expect("call Position::location before Position::quantity");
176 location.quantities.push(QuantityDraft {
177 qualifier,
178 value,
179 unit,
180 period,
181 status: Vec::new(),
182 });
183 self
184 }
185
186 #[must_use]
193 pub fn status(mut self, code: impl Into<String>) -> Self {
194 self.locations
195 .last_mut()
196 .and_then(|l| l.quantities.last_mut())
197 .expect("call Position::quantity before Position::status")
198 .status
199 .push(code.into());
200 self
201 }
202
203 #[must_use]
206 pub fn party(self, role: impl Into<String>, code: impl Into<String>) -> Self {
207 self.party_coded(role, code, DVGW_AGENCY_CODE)
208 }
209
210 #[must_use]
213 pub fn party_coded(
214 mut self,
215 role: impl Into<String>,
216 code: impl Into<String>,
217 agency: impl Into<String>,
218 ) -> Self {
219 self.parties.push(PartyDraft {
220 role: role.into(),
221 code: code.into(),
222 agency: agency.into(),
223 });
224 self
225 }
226}
227
228fn esc(value: &str) -> String {
234 let mut out = String::with_capacity(value.len());
235 for c in value.chars() {
236 if matches!(c, '+' | ':' | '\'' | '?') {
237 out.push('?');
238 }
239 out.push(c);
240 }
241 out
242}
243
244#[derive(Debug, Clone)]
249pub struct MessageBuilder {
250 document: DvgwDocument,
251 message_ref: String,
252 document_number: String,
253 version: Option<String>,
254 timezone: UtcOffset,
255 pruefidentifikator: Option<u32>,
256 message_datetime: Option<OffsetDateTime>,
257 validity_period: Option<DvgwPeriod>,
258 clearingnummer: Option<String>,
259 original_nomination: Option<(String, OffsetDateTime)>,
260 references: Vec<(String, String)>,
261 parties: Vec<PartyDraft>,
262 positions: Vec<Position>,
263}
264
265impl MessageBuilder {
266 #[must_use]
268 pub fn new(document: DvgwDocument) -> Self {
269 Self {
270 document,
271 message_ref: "1".to_owned(),
272 document_number: String::new(),
273 version: None,
274 timezone: UtcOffset::UTC,
275 pruefidentifikator: None,
276 message_datetime: None,
277 validity_period: None,
278 clearingnummer: None,
279 original_nomination: None,
280 references: Vec::new(),
281 parties: Vec::new(),
282 positions: Vec::new(),
283 }
284 }
285
286 #[must_use]
288 pub fn message_ref(mut self, value: impl Into<String>) -> Self {
289 self.message_ref = value.into();
290 self
291 }
292
293 #[must_use]
295 pub fn document_number(mut self, value: impl Into<String>) -> Self {
296 self.document_number = value.into();
297 self
298 }
299
300 #[must_use]
304 pub fn version(mut self, value: impl Into<String>) -> Self {
305 self.version = Some(value.into());
306 self
307 }
308
309 #[must_use]
311 pub fn pruefidentifikator(mut self, pid: u32) -> Self {
312 self.pruefidentifikator = Some(pid);
313 self
314 }
315
316 #[must_use]
318 pub fn message_datetime(mut self, value: OffsetDateTime) -> Self {
319 self.message_datetime = Some(value);
320 self
321 }
322
323 #[must_use]
326 pub fn validity_period(mut self, period: DvgwPeriod) -> Self {
327 self.validity_period = Some(period);
328 self
329 }
330
331 #[must_use]
333 pub fn sender(self, code: impl Into<String>) -> Self {
334 self.party(nad::ABSENDER, code)
335 }
336
337 #[must_use]
339 pub fn sender_coded(self, code: impl Into<String>, agency: impl Into<String>) -> Self {
340 self.party_coded(nad::ABSENDER, code, agency)
341 }
342
343 #[must_use]
345 pub fn receiver(self, code: impl Into<String>) -> Self {
346 self.party(nad::EMPFAENGER, code)
347 }
348
349 #[must_use]
351 pub fn receiver_coded(self, code: impl Into<String>, agency: impl Into<String>) -> Self {
352 self.party_coded(nad::EMPFAENGER, code, agency)
353 }
354
355 #[must_use]
357 pub fn party(self, role: impl Into<String>, code: impl Into<String>) -> Self {
358 self.party_coded(role, code, DVGW_AGENCY_CODE)
359 }
360
361 #[must_use]
363 pub fn party_coded(
364 mut self,
365 role: impl Into<String>,
366 code: impl Into<String>,
367 agency: impl Into<String>,
368 ) -> Self {
369 self.parties.push(PartyDraft {
370 role: role.into(),
371 code: code.into(),
372 agency: agency.into(),
373 });
374 self
375 }
376
377 #[must_use]
380 pub fn clearingnummer(mut self, value: impl Into<String>) -> Self {
381 self.clearingnummer = Some(value.into());
382 self
383 }
384
385 #[must_use]
388 pub fn original_nomination(
389 mut self,
390 value: impl Into<String>,
391 processed_at: OffsetDateTime,
392 ) -> Self {
393 self.original_nomination = Some((value.into(), processed_at));
394 self
395 }
396
397 #[must_use]
399 pub fn reference(mut self, qualifier: impl Into<String>, value: impl Into<String>) -> Self {
400 self.references.push((qualifier.into(), value.into()));
401 self
402 }
403
404 #[must_use]
406 pub fn position(mut self, position: Position) -> Self {
407 self.positions.push(position);
408 self
409 }
410
411 pub fn build(&self) -> Result<Vec<u8>, Error> {
423 let missing = |what: &str| Error::Serialize(format!("{what} is required but was not set"));
424
425 if self.document_number.is_empty() {
426 return Err(missing("BGM C106 DE 1004 Dokumentennummer"));
427 }
428 let pid = self
429 .pruefidentifikator
430 .ok_or_else(|| missing("SG1 RFF+Z13 Prüfidentifikator"))?;
431 let message_datetime = self
432 .message_datetime
433 .ok_or_else(|| missing("DTM+137 Datum und Zeit der Nachricht"))?;
434 let validity = self
435 .validity_period
436 .ok_or_else(|| missing("DTM+Z01 Gültigkeitszeitraum"))?;
437 for role in [nad::ABSENDER, nad::EMPFAENGER] {
438 if !self.parties.iter().any(|p| p.role == role) {
439 return Err(missing(&format!("NAD+{role}")));
440 }
441 }
442 if self.positions.is_empty() {
443 return Err(missing("at least one LIN position"));
444 }
445
446 let agency = DVGW_AGENCY_CODE;
447 let mut segments: Vec<String> = Vec::new();
448
449 let family = self.document.message_type();
450 let version = self
451 .version
452 .as_deref()
453 .unwrap_or_else(|| family.anwendungscode());
454 segments.push(format!(
455 "UNH+{}+{}:D:07A:UN:{}",
456 esc(&self.message_ref),
457 self.document.carrier().as_str(),
458 esc(version)
459 ));
460 segments.push(format!(
461 "BGM+{}::{agency}+{}",
462 self.document.code(),
463 esc(&self.document_number)
464 ));
465 segments.push(format!("DTM+Z05:{}:805", self.timezone.whole_hours()));
467 segments.push(format!(
468 "DTM+137:{}:203",
469 format_instant(message_datetime, self.timezone)
470 ));
471 segments.push(format!(
472 "DTM+Z01:{}:719",
473 format_period(validity, self.timezone)
474 ));
475 if let Some(clearing) = &self.clearingnummer {
479 segments.push(format!("RFF+{}:{}", rff::CLEARINGNUMMER, esc(clearing)));
480 }
481 segments.push(format!("RFF+{}:{pid}", rff::PRUEFIDENTIFIKATOR));
482 if let Some((original, processed_at)) = &self.original_nomination {
483 segments.push(format!(
484 "RFF+{}:{}",
485 rff::ORIGINAL_NOMINIERUNG,
486 esc(original)
487 ));
488 segments.push(format!(
489 "DTM+9:{}:203",
490 format_instant(*processed_at, self.timezone)
491 ));
492 }
493 for (qualifier, value) in &self.references {
494 segments.push(format!("RFF+{}:{}", esc(qualifier), esc(value)));
495 }
496 for party in &self.parties {
497 segments.push(format!(
498 "NAD+{}+{}::{}",
499 esc(&party.role),
500 esc(&party.code),
501 esc(&party.agency)
502 ));
503 }
504
505 let default_unit = family.admitted_units()[0];
506 for (index, position) in self.positions.iter().enumerate() {
507 self.render_position(position, index, agency, default_unit, &mut segments);
508 }
509
510 segments.push("UNS+S".to_owned());
511 segments.push(format!(
514 "UNT+{}+{}",
515 segments.len() + 1,
516 esc(&self.message_ref)
517 ));
518
519 let mut out = String::new();
520 for segment in segments {
521 out.push_str(&segment);
522 out.push('\'');
523 }
524 Ok(out.into_bytes())
525 }
526
527 fn render_position(
529 &self,
530 position: &Position,
531 index: usize,
532 agency: &str,
533 default_unit: &str,
534 segments: &mut Vec<String>,
535 ) {
536 {
537 let number = position
538 .number
539 .clone()
540 .unwrap_or_else(|| (index + 1).to_string());
541 let number = esc(&number);
542 match &position.item_type {
543 Some(item_type) => {
544 segments.push(format!("LIN+{number}++:{}::{agency}", esc(item_type)));
545 }
546 None => segments.push(format!("LIN+{number}")),
547 }
548 if let Some(code) = &position.description {
549 segments.push(format!("IMD++05G+{}::{agency}", esc(code)));
550 }
551 for location in &position.locations {
552 let loc = match &location.code {
553 Some(code) => {
554 format!("LOC+{}+{}::{agency}", esc(&location.qualifier), esc(code))
555 }
556 None => format!("LOC+{}", esc(&location.qualifier)),
557 };
558 for quantity in &location.quantities {
561 segments.push(loc.clone());
562 segments.push(format!(
563 "DTM+2:{}:719",
564 format_period(quantity.period, self.timezone)
565 ));
566 segments.push(format!(
567 "QTY+{}:{}:{}",
568 esc(&quantity.qualifier),
569 esc(&quantity.value),
570 esc(quantity.unit.as_deref().unwrap_or(default_unit))
571 ));
572 for status in &quantity.status {
573 segments.push(format!("STS+{}::{agency}", esc(status)));
574 }
575 }
576 if location.quantities.is_empty() {
577 segments.push(loc);
578 }
579 }
580 for party in &position.parties {
581 segments.push(format!(
582 "NAD+{}+{}::{}",
583 esc(&party.role),
584 esc(&party.code),
585 esc(&party.agency)
586 ));
587 }
588 }
589 }
590}