1use std::collections::HashMap;
4use std::sync::Mutex;
5
6use mig_assembly::ConversionService;
7use mig_bo4e::engine::DataBundle;
8use mig_bo4e::MappingEngine;
9
10use crate::data_dir::DataDir;
11use crate::error::MapperError;
12
13pub struct Bo4eResult {
15 pub pid: String,
17 pub message_type: String,
19 pub variant: String,
21 pub bo4e: serde_json::Value,
23}
24
25#[derive(Debug, Clone)]
62pub struct PidListEntry {
63 pub fv: String,
64 pub variant: String,
65 pub pid: String,
66 pub beschreibung: String,
67}
68
69pub struct Mapper {
70 data_dir: DataDir,
71 bundles: Mutex<HashMap<String, DataBundle>>,
72}
73
74impl Mapper {
75 pub fn from_data_dir(data_dir: DataDir) -> Result<Self, MapperError> {
80 let mapper = Self {
81 data_dir,
82 bundles: Mutex::new(HashMap::new()),
83 };
84 let eager_fvs: Vec<String> = mapper.data_dir.eager_fvs().to_vec();
85 for fv in &eager_fvs {
86 mapper.ensure_bundle_loaded(fv)?;
87 }
88 Ok(mapper)
89 }
90
91 fn ensure_bundle_loaded(&self, fv: &str) -> Result<(), MapperError> {
93 let mut bundles = self.bundles.lock().unwrap();
94 if bundles.contains_key(fv) {
95 return Ok(());
96 }
97 let path = self.data_dir.bundle_path(fv);
98 if !path.exists() {
99 return Err(MapperError::BundleNotFound { fv: fv.to_string() });
100 }
101 let bundle = DataBundle::load(&path)?;
102 bundles.insert(fv.to_string(), bundle);
103 Ok(())
104 }
105
106 pub fn conversion_service(
110 &self,
111 fv: &str,
112 variant: &str,
113 ) -> Result<ConversionService, MapperError> {
114 self.ensure_bundle_loaded(fv)?;
115 let bundles = self.bundles.lock().unwrap();
116 let bundle = bundles.get(fv).unwrap();
117 let vc = bundle
118 .variant(variant)
119 .ok_or_else(|| MapperError::VariantNotFound {
120 fv: fv.to_string(),
121 variant: variant.to_string(),
122 })?;
123 let mig = vc
124 .mig_schema
125 .as_ref()
126 .ok_or_else(|| MapperError::VariantNotFound {
127 fv: fv.to_string(),
128 variant: format!("{variant} (no MIG schema in bundle)"),
129 })?;
130 Ok(ConversionService::from_mig(mig.clone()))
131 }
132
133 pub fn engine(&self, fv: &str, variant: &str, pid: &str) -> Result<MappingEngine, MapperError> {
137 self.ensure_bundle_loaded(fv)?;
138 let bundles = self.bundles.lock().unwrap();
139 let bundle = bundles.get(fv).unwrap();
140 let vc = bundle
141 .variant(variant)
142 .ok_or_else(|| MapperError::VariantNotFound {
143 fv: fv.to_string(),
144 variant: variant.to_string(),
145 })?;
146 let pid_key = format!("pid_{pid}");
147 let defs = vc
148 .combined_defs
149 .get(&pid_key)
150 .ok_or_else(|| MapperError::PidNotFound {
151 fv: fv.to_string(),
152 variant: variant.to_string(),
153 pid: pid.to_string(),
154 })?;
155 Ok(MappingEngine::from_definitions(defs.clone()))
156 }
157
158 pub fn pid_requirements(
163 &self,
164 fv: &str,
165 variant: &str,
166 pid: &str,
167 ) -> Result<mig_bo4e::pid_requirements::PidRequirements, MapperError> {
168 self.ensure_bundle_loaded(fv)?;
169 let bundles = self.bundles.lock().unwrap();
170 let bundle = bundles.get(fv).unwrap();
171 let vc = bundle
172 .variant(variant)
173 .ok_or_else(|| MapperError::VariantNotFound {
174 fv: fv.to_string(),
175 variant: variant.to_string(),
176 })?;
177 let pid_key = format!("pid_{pid}");
178 vc.pid_requirements
179 .get(&pid_key)
180 .cloned()
181 .ok_or_else(|| MapperError::PidNotFound {
182 fv: fv.to_string(),
183 variant: variant.to_string(),
184 pid: pid.to_string(),
185 })
186 }
187
188 pub fn bo4e_catalog(
194 &self,
195 fv: &str,
196 ) -> Result<mig_bo4e::bo4e_catalog::Bo4eCatalog, MapperError> {
197 self.ensure_bundle_loaded(fv)?;
198 let bundles = self.bundles.lock().unwrap();
199 let bundle = bundles.get(fv).unwrap();
200 Ok(bundle.bo4e_catalog.clone())
201 }
202
203 pub fn list_pids(&self) -> Result<Vec<PidListEntry>, MapperError> {
208 let dir = self.data_dir.data_path();
209 let read_dir = std::fs::read_dir(dir).map_err(|_| MapperError::DataDirNotFound {
210 path: dir.display().to_string(),
211 })?;
212
213 let mut result = Vec::new();
214
215 for entry in read_dir.flatten() {
216 let path = entry.path();
217 if path.extension().is_some_and(|e| e == "bin") {
218 let stem = path
219 .file_stem()
220 .and_then(|s| s.to_str())
221 .unwrap_or("")
222 .to_string();
223 let fv = match stem.strip_prefix("edifact-data-") {
224 Some(v) => v.to_string(),
225 None => continue,
226 };
227 self.ensure_bundle_loaded(&fv)?;
228 let bundles = self.bundles.lock().unwrap();
229 if let Some(bundle) = bundles.get(&fv) {
230 for (variant, vc) in &bundle.variants {
231 for (pid_key, req) in &vc.pid_requirements {
232 let pid = pid_key
233 .strip_prefix("pid_")
234 .unwrap_or(pid_key)
235 .to_string();
236 result.push(PidListEntry {
237 fv: fv.clone(),
238 variant: variant.clone(),
239 pid,
240 beschreibung: req.beschreibung.clone(),
241 });
242 }
243 }
244 }
245 }
246 }
247
248 result.sort_by(|a, b| a.pid.cmp(&b.pid));
249 Ok(result)
250 }
251
252 pub fn validate_pid(
257 &self,
258 json: &serde_json::Value,
259 fv: &str,
260 variant: &str,
261 pid: &str,
262 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
263 self.ensure_bundle_loaded(fv)?;
264 let bundles = self.bundles.lock().unwrap();
265 let bundle = bundles.get(fv).unwrap();
266 let vc = bundle
267 .variant(variant)
268 .ok_or_else(|| MapperError::VariantNotFound {
269 fv: fv.to_string(),
270 variant: variant.to_string(),
271 })?;
272 let pid_key = format!("pid_{pid}");
273 let requirements =
274 vc.pid_requirements
275 .get(&pid_key)
276 .ok_or_else(|| MapperError::PidNotFound {
277 fv: fv.to_string(),
278 variant: variant.to_string(),
279 pid: pid.to_string(),
280 })?;
281
282 Ok(mig_bo4e::pid_validation::validate_pid_json(
283 json,
284 requirements,
285 ))
286 }
287
288 pub fn validate_pid_struct(
300 &self,
301 value: &impl serde::Serialize,
302 fv: &str,
303 variant: &str,
304 pid: &str,
305 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
306 let json = serde_json::to_value(value).map_err(|e| {
307 MapperError::Mapping(mig_bo4e::MappingError::TypeConversion(e.to_string()))
308 })?;
309 self.validate_pid(&json, fv, variant, pid)
310 }
311
312 pub fn validate_pid_with_conditions(
320 &self,
321 json: &serde_json::Value,
322 fv: &str,
323 variant: &str,
324 pid: &str,
325 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
326 self.ensure_bundle_loaded(fv)?;
327 let bundles = self.bundles.lock().unwrap();
328 let bundle = bundles.get(fv).unwrap();
329 let vc = bundle
330 .variant(variant)
331 .ok_or_else(|| MapperError::VariantNotFound {
332 fv: fv.to_string(),
333 variant: variant.to_string(),
334 })?;
335 let pid_key = format!("pid_{pid}");
336
337 let requirements =
338 vc.pid_requirements
339 .get(&pid_key)
340 .ok_or_else(|| MapperError::PidNotFound {
341 fv: fv.to_string(),
342 variant: variant.to_string(),
343 pid: pid.to_string(),
344 })?;
345
346 let evaluator = crate::evaluator_factory::create_evaluator(variant, fv);
348
349 if let Some(evaluator) = evaluator {
350 let defs = vc
352 .combined_defs
353 .get(&pid_key)
354 .ok_or_else(|| MapperError::PidNotFound {
355 fv: fv.to_string(),
356 variant: variant.to_string(),
357 pid: pid.to_string(),
358 })?;
359 let engine = MappingEngine::from_definitions(defs.clone());
360 let tree = engine.map_all_reverse(json, None);
361
362 let segments = crate::tree_to_segments::tree_to_owned_segments(&tree);
364
365 Ok(crate::evaluator_factory::validate_with_boxed_evaluator(
367 evaluator.as_ref(),
368 json,
369 requirements,
370 pid,
371 &segments,
372 ))
373 } else {
374 Ok(mig_bo4e::pid_validation::validate_pid_json_transaction(
376 json,
377 requirements,
378 ))
379 }
380 }
381
382 pub fn to_edifact(
408 &self,
409 msg_stammdaten: &serde_json::Value,
410 tx_stammdaten: &[serde_json::Value],
411 fv: &str,
412 variant: &str,
413 pid: &str,
414 ) -> Result<String, MapperError> {
415 self.ensure_bundle_loaded(fv)?;
416 let bundles = self.bundles.lock().unwrap();
417 let bundle = bundles.get(fv).unwrap();
418 let vc = bundle
419 .variant(variant)
420 .ok_or_else(|| MapperError::VariantNotFound {
421 fv: fv.to_string(),
422 variant: variant.to_string(),
423 })?;
424
425 let tx_group = vc
426 .tx_group(pid)
427 .ok_or_else(|| MapperError::PidNotFound {
428 fv: fv.to_string(),
429 variant: variant.to_string(),
430 pid: pid.to_string(),
431 })?;
432
433 let msg_engine = vc.msg_engine(pid);
434 let tx_engine =
435 vc.tx_engine(pid)
436 .ok_or_else(|| MapperError::PidNotFound {
437 fv: fv.to_string(),
438 variant: variant.to_string(),
439 pid: pid.to_string(),
440 })?;
441
442 let filtered_mig =
443 vc.filtered_mig(pid)
444 .ok_or_else(|| MapperError::NoMigSchema {
445 fv: fv.to_string(),
446 variant: variant.to_string(),
447 })?;
448
449 let transaktionen: Vec<mig_bo4e::model::MappedTransaktion> = tx_stammdaten
451 .iter()
452 .map(|tx| mig_bo4e::model::MappedTransaktion {
453 stammdaten: tx.clone(),
454 nesting_info: Default::default(),
455 dp_routing: Default::default(),
456 })
457 .collect();
458 let mapped = mig_bo4e::model::MappedMessage {
459 stammdaten: msg_stammdaten.clone(),
460 transaktionen,
461 nesting_info: Default::default(),
462 dp_routing: Default::default(),
463 inter_group_segments: Default::default(),
464 };
465
466 let tree = MappingEngine::map_interchange_reverse(
468 &msg_engine,
469 &tx_engine,
470 &mapped,
471 tx_group,
472 Some(&filtered_mig),
473 );
474
475 let disassembler =
477 mig_assembly::disassembler::Disassembler::new(&filtered_mig);
478 let segments = disassembler.disassemble(&tree);
479
480 let delimiters = edifact_primitives::EdifactDelimiters::default();
482 Ok(mig_assembly::renderer::render_edifact(
483 &segments,
484 &delimiters,
485 ))
486 }
487
488 pub fn to_edifact_struct(
494 &self,
495 nachricht: &impl serde::Serialize,
496 fv: &str,
497 variant: &str,
498 pid: &str,
499 ) -> Result<String, MapperError> {
500 let json = serde_json::to_value(nachricht)
501 .map_err(|e| MapperError::Serialization(e.to_string()))?;
502
503 let msg_stammdaten = json
504 .get("stammdaten")
505 .cloned()
506 .unwrap_or(serde_json::Value::Object(Default::default()));
507
508 let tx_stammdaten: Vec<serde_json::Value> = json
509 .get("transaktionen")
510 .and_then(|v| v.as_array())
511 .cloned()
512 .unwrap_or_default();
513
514 self.to_edifact(&msg_stammdaten, &tx_stammdaten, fv, variant, pid)
515 }
516
517 pub fn from_edifact<M, T>(
535 &self,
536 edifact: &str,
537 fv: &str,
538 variant: &str,
539 pid: &str,
540 ) -> Result<mig_bo4e::model::Interchange<M, T>, MapperError>
541 where
542 M: serde::de::DeserializeOwned,
543 T: serde::de::DeserializeOwned,
544 {
545 self.ensure_bundle_loaded(fv)?;
546 let bundles = self.bundles.lock().unwrap();
547 let bundle = bundles.get(fv).unwrap();
548 let vc = bundle
549 .variant(variant)
550 .ok_or_else(|| MapperError::VariantNotFound {
551 fv: fv.to_string(),
552 variant: variant.to_string(),
553 })?;
554
555 let tx_group = vc
556 .tx_group(pid)
557 .ok_or_else(|| MapperError::PidNotFound {
558 fv: fv.to_string(),
559 variant: variant.to_string(),
560 pid: pid.to_string(),
561 })?;
562
563 let msg_engine = vc.msg_engine(pid);
564 let tx_engine =
565 vc.tx_engine(pid)
566 .ok_or_else(|| MapperError::PidNotFound {
567 fv: fv.to_string(),
568 variant: variant.to_string(),
569 pid: pid.to_string(),
570 })?;
571
572 let filtered_mig =
573 vc.filtered_mig(pid)
574 .ok_or_else(|| MapperError::NoMigSchema {
575 fv: fv.to_string(),
576 variant: variant.to_string(),
577 })?;
578
579 let svc = ConversionService::from_mig(filtered_mig);
581 let (chunks, trees) = svc.convert_interchange_to_trees(edifact)?;
582
583 let tree = trees
584 .first()
585 .ok_or_else(|| MapperError::Assembly(
586 mig_assembly::AssemblyError::ParseError("No messages in interchange".to_string()),
587 ))?;
588
589 let interchangedaten =
591 mig_bo4e::model::extract_interchangedaten(&chunks.envelope);
592 let msg_chunk = chunks.messages.first().ok_or_else(|| {
593 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
594 "No message chunks".to_string(),
595 ))
596 })?;
597 let (unh_ref, nachrichten_typ) =
598 mig_bo4e::model::extract_unh_fields(&msg_chunk.unh);
599 let nachrichtendaten = mig_bo4e::model::Nachrichtendaten {
600 unh_referenz: unh_ref,
601 nachrichten_typ,
602 };
603
604 MappingEngine::map_interchange_typed::<M, T>(
606 &msg_engine,
607 &tx_engine,
608 tree,
609 tx_group,
610 true,
611 nachrichtendaten,
612 interchangedaten,
613 )
614 .map_err(|e| MapperError::Serialization(e.to_string()))
615 }
616
617 pub fn detect_pid(&self, edifact: &str) -> Result<String, MapperError> {
629 let segments = mig_assembly::tokenize::parse_to_segments(edifact.as_bytes())?;
630 let chunks = mig_assembly::split_messages(segments)?;
631 let msg_chunk =
632 chunks
633 .messages
634 .first()
635 .ok_or_else(|| MapperError::Assembly(
636 mig_assembly::AssemblyError::ParseError(
637 "No messages found in EDIFACT content".to_string(),
638 ),
639 ))?;
640 let msg_segments = msg_chunk.message_segments();
641 mig_assembly::pid_detect::detect_pid(&msg_segments).map_err(MapperError::Assembly)
642 }
643
644 pub fn association_code(&self, fv: &str, variant: &str) -> Result<String, MapperError> {
655 let meta = self.message_metadata(fv, variant)?;
656 Ok(meta.association_code)
657 }
658
659 pub fn message_metadata(
664 &self,
665 fv: &str,
666 variant: &str,
667 ) -> Result<MessageMetadata, MapperError> {
668 self.ensure_bundle_loaded(fv)?;
669 let bundles = self.bundles.lock().unwrap();
670 let bundle = bundles.get(fv).unwrap();
671 let vc = bundle
672 .variant(variant)
673 .ok_or_else(|| MapperError::VariantNotFound {
674 fv: fv.to_string(),
675 variant: variant.to_string(),
676 })?;
677 let mig = vc
678 .mig_schema
679 .as_ref()
680 .ok_or_else(|| MapperError::NoMigSchema {
681 fv: fv.to_string(),
682 variant: variant.to_string(),
683 })?;
684 Ok(MessageMetadata {
685 message_type: mig.message_type.clone(),
686 release: release_code_for_message_type(&mig.message_type),
687 association_code: mig.version.clone(),
688 })
689 }
690
691 pub fn to_edifact_interchange(
715 &self,
716 envelope: &InterchangeEnvelope,
717 messages: &[InterchangeMessage],
718 ) -> Result<String, MapperError> {
719 let delimiters = edifact_primitives::EdifactDelimiters::default();
720 let sep = delimiters.component as char;
721 let elem = delimiters.element as char;
722 let seg_term = delimiters.segment as char;
723
724 let mut output = String::new();
725
726 output.push_str(&format!(
728 "UNA{}{}{}{}{}{}",
729 sep, elem, delimiters.decimal as char, delimiters.release as char, ' ', seg_term, ));
736
737 let now = chrono::Utc::now();
739 let date_str = now.format("%y%m%d").to_string();
740 let time_str = now.format("%H%M").to_string();
741 let sender = &envelope.sender;
742 let receiver = &envelope.receiver;
743 let interchange_ref = &envelope.interchange_ref;
744 output.push_str(&format!(
745 "UNB{elem}UNOC{sep}3{elem}{sid}{sep}{sq}{elem}{rid}{sep}{rq}{elem}{date_str}{sep}{time_str}{elem}{interchange_ref}{seg_term}",
746 sid = sender.id,
747 sq = sender.qualifier,
748 rid = receiver.id,
749 rq = receiver.qualifier,
750 ));
751
752 let mut message_count = 0u32;
753
754 for msg in messages {
755 let meta = self.message_metadata(&msg.fv, &msg.variant)?;
756
757 let body = self.to_edifact(
759 &msg.msg_stammdaten,
760 &msg.tx_stammdaten,
761 &msg.fv,
762 &msg.variant,
763 &msg.pid,
764 )?;
765
766 let body_seg_count = body
768 .split(seg_term)
769 .filter(|s: &&str| !s.is_empty())
770 .count();
771 let segment_count = body_seg_count + 2;
773
774 output.push_str(&format!(
776 "UNH{elem}{ref}{elem}{msg_type}{sep}D{sep}{release}{sep}UN{sep}{assoc}{seg_term}",
777 ref = msg.message_ref,
778 msg_type = meta.message_type,
779 release = meta.release,
780 assoc = meta.association_code,
781 ));
782
783 output.push_str(&body);
785
786 output.push_str(&format!(
788 "UNT{elem}{segment_count}{elem}{ref}{seg_term}",
789 ref = msg.message_ref,
790 ));
791
792 message_count += 1;
793 }
794
795 output.push_str(&format!(
797 "UNZ{elem}{message_count}{elem}{interchange_ref}{seg_term}",
798 ));
799
800 Ok(output)
801 }
802
803 pub fn loaded_format_versions(&self) -> Vec<String> {
805 self.bundles.lock().unwrap().keys().cloned().collect()
806 }
807
808 pub fn variants(&self, fv: &str) -> Result<Vec<String>, MapperError> {
812 self.ensure_bundle_loaded(fv)?;
813 let bundles = self.bundles.lock().unwrap();
814 let bundle = bundles.get(fv).unwrap();
815 Ok(bundle.variants.keys().cloned().collect())
816 }
817}
818
819#[derive(Debug, Clone)]
821pub struct MessageMetadata {
822 pub message_type: String,
824 pub release: String,
826 pub association_code: String,
828}
829
830#[derive(Debug, Clone)]
832pub struct InterchangeEnvelope {
833 pub sender: EdifactParty,
835 pub receiver: EdifactParty,
837 pub interchange_ref: String,
839}
840
841#[derive(Debug, Clone)]
843pub struct EdifactParty {
844 pub id: String,
846 pub qualifier: String,
848}
849
850impl EdifactParty {
851 pub fn bdew(id: &str) -> Self {
853 Self {
854 id: id.to_string(),
855 qualifier: "500".to_string(),
856 }
857 }
858
859 pub fn gs1(id: &str) -> Self {
861 Self {
862 id: id.to_string(),
863 qualifier: "14".to_string(),
864 }
865 }
866}
867
868#[derive(Debug, Clone)]
871pub struct InterchangeMessage {
872 pub message_ref: String,
874 pub msg_stammdaten: serde_json::Value,
876 pub tx_stammdaten: Vec<serde_json::Value>,
878 pub fv: String,
880 pub variant: String,
882 pub pid: String,
884}
885
886fn release_code_for_message_type(msg_type: &str) -> String {
890 match msg_type {
891 "APERAK" => "07B",
892 "COMDIS" => "17A",
893 "CONTRL" => "04B",
894 "IFTSTA" => "18A",
895 "INSRPT" => "18A",
896 "INVOIC" => "06A",
897 "MSCONS" => "04B",
898 "ORDCHG" => "09B",
899 "ORDERS" => "09B",
900 "ORDRSP" => "10A",
901 "PARTIN" => "20B",
902 "PRICAT" => "20B",
903 "QUOTES" => "10A",
904 "REMADV" => "05A",
905 "REQOTE" => "10A",
906 "UTILMD" => "11A",
907 "UTILTS" => "18A",
908 _ => "04B", }
910 .to_string()
911}
912
913
914#[cfg(test)]
915mod tests {
916 use super::*;
917 use std::path::Path;
918
919 fn data_dir() -> Option<std::path::PathBuf> {
920 let dist = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist");
922 if dist.join("edifact-data-FV2504.bin").exists() {
923 return Some(dist);
924 }
925 let cache = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../cache/mappings");
926 if cache.join("FV2504").exists() {
927 return Some(cache);
928 }
929 eprintln!("Skipping test: no DataBundle files found");
930 None
931 }
932
933 #[test]
934 fn test_to_edifact_produces_edifact_output() {
935 let Some(data_dir) = data_dir() else {
936 return;
937 };
938 let mapper =
939 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
940
941 let msg_stammdaten = serde_json::json!({
942 "marktteilnehmer": [{
943 "marktrolle": "MS",
944 "rollencodenummer": "9900123456789",
945 "codepflegeCode": "293"
946 }]
947 });
948 let tx_stammdaten = serde_json::json!({
949 "prozessdaten": {
950 "pruefidentifikator": "55001",
951 "vorgangId": "ABC123",
952 "transaktionsgrund": "E01"
953 }
954 });
955
956 let result = mapper.to_edifact(
957 &msg_stammdaten,
958 &[tx_stammdaten],
959 "FV2504",
960 "UTILMD_Strom",
961 "55001",
962 );
963 assert!(result.is_ok(), "to_edifact failed: {:?}", result.err());
964 let edifact = result.unwrap();
965 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
966 assert!(edifact.contains("NAD"), "Should contain NAD segment");
968 assert!(edifact.contains("IDE"), "Should contain IDE segment");
970 }
971
972 #[test]
973 fn test_to_edifact_struct_produces_edifact_output() {
974 let Some(data_dir) = data_dir() else {
975 return;
976 };
977 let mapper =
978 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
979
980 let nachricht = serde_json::json!({
981 "stammdaten": {
982 "marktteilnehmer": [{
983 "marktrolle": "MS",
984 "rollencodenummer": "9900123456789",
985 "codepflegeCode": "293"
986 }]
987 },
988 "transaktionen": [{
989 "prozessdaten": {
990 "pruefidentifikator": "55001",
991 "vorgangId": "ABC123"
992 }
993 }]
994 });
995
996 let result = mapper.to_edifact_struct(&nachricht, "FV2504", "UTILMD_Strom", "55001");
997 assert!(
998 result.is_ok(),
999 "to_edifact_struct failed: {:?}",
1000 result.err()
1001 );
1002 let edifact = result.unwrap();
1003 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
1004 }
1005
1006 #[test]
1007 fn test_to_edifact_invalid_fv_returns_error() {
1008 let Some(data_dir) = data_dir() else {
1009 return;
1010 };
1011 let mapper =
1012 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1013
1014 let result = mapper.to_edifact(
1015 &serde_json::json!({}),
1016 &[serde_json::json!({})],
1017 "FV9999",
1018 "UTILMD_Strom",
1019 "55001",
1020 );
1021 assert!(result.is_err());
1022 }
1023
1024 #[test]
1025 fn test_to_edifact_invalid_variant_returns_error() {
1026 let Some(data_dir) = data_dir() else {
1027 return;
1028 };
1029 let mapper =
1030 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1031
1032 let result = mapper.to_edifact(
1033 &serde_json::json!({}),
1034 &[serde_json::json!({})],
1035 "FV2504",
1036 "NONEXISTENT",
1037 "55001",
1038 );
1039 assert!(result.is_err());
1040 }
1041
1042 #[test]
1043 fn test_to_edifact_invalid_pid_returns_error() {
1044 let Some(data_dir) = data_dir() else {
1045 return;
1046 };
1047 let mapper =
1048 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1049
1050 let result = mapper.to_edifact(
1051 &serde_json::json!({}),
1052 &[serde_json::json!({})],
1053 "FV2504",
1054 "UTILMD_Strom",
1055 "99999",
1056 );
1057 assert!(result.is_err());
1058 }
1059
1060 #[test]
1061 fn test_association_code() {
1062 let Some(data_dir) = data_dir() else {
1063 return;
1064 };
1065 let mapper =
1066 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1067
1068 let code = mapper.association_code("FV2504", "UTILMD_Strom").unwrap();
1069 assert_eq!(code, "S2.1");
1070
1071 let code = mapper.association_code("FV2504", "MSCONS").unwrap();
1072 assert_eq!(code, "2.4c");
1073 }
1074
1075 #[test]
1076 fn test_message_metadata() {
1077 let Some(data_dir) = data_dir() else {
1078 return;
1079 };
1080 let mapper =
1081 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1082
1083 let meta = mapper.message_metadata("FV2504", "UTILMD_Strom").unwrap();
1084 assert_eq!(meta.message_type, "UTILMD");
1085 assert_eq!(meta.release, "11A");
1086 assert_eq!(meta.association_code, "S2.1");
1087 }
1088
1089 #[test]
1090 fn test_to_edifact_interchange() {
1091 let Some(data_dir) = data_dir() else {
1092 return;
1093 };
1094 let mapper =
1095 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1096
1097 let result = mapper.to_edifact_interchange(
1098 &InterchangeEnvelope {
1099 sender: EdifactParty::bdew("9900000000003"),
1100 receiver: EdifactParty::bdew("9900000000001"),
1101 interchange_ref: "REF001".to_string(),
1102 },
1103 &[InterchangeMessage {
1104 message_ref: "MSG001".to_string(),
1105 msg_stammdaten: serde_json::json!({
1106 "marktteilnehmer": [{
1107 "marktrolle": "MS",
1108 "rollencodenummer": "9900123456789",
1109 "codepflegeCode": "293"
1110 }]
1111 }),
1112 tx_stammdaten: vec![serde_json::json!({
1113 "prozessdaten": {
1114 "pruefidentifikator": "55001",
1115 "vorgangId": "ABC123",
1116 "transaktionsgrund": "E01"
1117 }
1118 })],
1119 fv: "FV2504".to_string(),
1120 variant: "UTILMD_Strom".to_string(),
1121 pid: "55001".to_string(),
1122 }],
1123 );
1124 assert!(
1125 result.is_ok(),
1126 "to_edifact_interchange failed: {:?}",
1127 result.err()
1128 );
1129 let edifact = result.unwrap();
1130
1131 assert!(edifact.starts_with("UNA:+.? '"), "Should start with UNA");
1133 assert!(edifact.contains("UNB+UNOC:3+9900000000003:500+9900000000001:500+"),
1134 "Should contain UNB with sender/receiver");
1135 assert!(edifact.contains("UNH+MSG001+UTILMD:D:11A:UN:S2.1'"),
1136 "Should contain UNH with correct S009");
1137 assert!(edifact.contains("NAD"), "Should contain body NAD segment");
1138 assert!(edifact.contains("UNT+"), "Should contain UNT");
1139 assert!(edifact.contains("+MSG001'"), "UNT should reference message ref");
1140 assert!(edifact.contains("UNZ+1+REF001'"), "Should contain UNZ with count and ref");
1141 }
1142
1143 #[test]
1144 fn test_detect_pid_from_rff_z13() {
1145 let Some(data_dir) = data_dir() else {
1146 return;
1147 };
1148 let mapper =
1149 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1150
1151 let edifact = "\
1152 UNB+UNOC:3+9978842000002:500+9900269000000:500+250331:1329+REF001'\
1153 UNH+MSG001+UTILMD:D:11A:UN:S2.1'\
1154 BGM+E01+DOC001'\
1155 DTM+137:202503311329?+00:303'\
1156 NAD+MS+9978842000002::293'\
1157 NAD+MR+9900269000000::293'\
1158 IDE+24+TX001'\
1159 DTM+92:202505312200?+00:303'\
1160 DTM+93:202512312300?+00:303'\
1161 STS+7++E01+ZW4+E03'\
1162 LOC+Z16+12345678900'\
1163 RFF+Z13:55001'\
1164 UNT+12+MSG001'\
1165 UNZ+1+REF001'";
1166
1167 let pid = mapper.detect_pid(edifact).unwrap();
1168 assert_eq!(pid, "55001");
1169 }
1170
1171 #[test]
1172 fn test_detect_pid_no_messages_returns_error() {
1173 let Some(data_dir) = data_dir() else {
1174 return;
1175 };
1176 let mapper =
1177 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1178
1179 let edifact = "UNB+UNOC:3+SENDER:500+RECEIVER:500+250401:1200+REF'\
1180 UNZ+0+REF'";
1181 assert!(mapper.detect_pid(edifact).is_err());
1182 }
1183
1184 #[test]
1185 fn test_list_pids_returns_entries() {
1186 let Some(data_dir) = data_dir() else {
1187 return;
1188 };
1189 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir)).unwrap();
1190 let pids = mapper.list_pids().expect("list_pids should succeed");
1191 assert!(!pids.is_empty(), "should return at least one PID");
1192 assert!(
1193 pids.iter().any(|p| p.pid == "55001"),
1194 "should include PID 55001"
1195 );
1196 assert!(
1197 pids.iter().any(|p| p.fv == "FV2504"),
1198 "should include FV2504"
1199 );
1200 assert!(
1201 pids.iter().any(|p| p.variant == "UTILMD_Strom"),
1202 "should include UTILMD_Strom"
1203 );
1204 }
1205
1206 #[test]
1207 fn test_pid_requirements_returns_requirements() {
1208 let Some(data_dir) = data_dir() else {
1209 return;
1210 };
1211 let mapper =
1212 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1213
1214 let req = mapper
1215 .pid_requirements("FV2504", "UTILMD_Strom", "55001")
1216 .expect("pid_requirements should succeed");
1217
1218 assert_eq!(req.pid, "55001");
1219 assert!(
1220 !req.entities.is_empty(),
1221 "55001 should have at least one entity"
1222 );
1223 assert!(
1224 req.entities.iter().any(|e| e.entity == "Prozessdaten"),
1225 "55001 should have a Prozessdaten entity"
1226 );
1227 }
1228}