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 list_pids(&self) -> Result<Vec<PidListEntry>, MapperError> {
193 let dir = self.data_dir.data_path();
194 let read_dir = std::fs::read_dir(dir).map_err(|_| MapperError::DataDirNotFound {
195 path: dir.display().to_string(),
196 })?;
197
198 let mut result = Vec::new();
199
200 for entry in read_dir.flatten() {
201 let path = entry.path();
202 if path.extension().is_some_and(|e| e == "bin") {
203 let stem = path
204 .file_stem()
205 .and_then(|s| s.to_str())
206 .unwrap_or("")
207 .to_string();
208 let fv = match stem.strip_prefix("edifact-data-") {
209 Some(v) => v.to_string(),
210 None => continue,
211 };
212 self.ensure_bundle_loaded(&fv)?;
213 let bundles = self.bundles.lock().unwrap();
214 if let Some(bundle) = bundles.get(&fv) {
215 for (variant, vc) in &bundle.variants {
216 for (pid_key, req) in &vc.pid_requirements {
217 let pid = pid_key
218 .strip_prefix("pid_")
219 .unwrap_or(pid_key)
220 .to_string();
221 result.push(PidListEntry {
222 fv: fv.clone(),
223 variant: variant.clone(),
224 pid,
225 beschreibung: req.beschreibung.clone(),
226 });
227 }
228 }
229 }
230 }
231 }
232
233 result.sort_by(|a, b| a.pid.cmp(&b.pid));
234 Ok(result)
235 }
236
237 pub fn validate_pid(
242 &self,
243 json: &serde_json::Value,
244 fv: &str,
245 variant: &str,
246 pid: &str,
247 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
248 self.ensure_bundle_loaded(fv)?;
249 let bundles = self.bundles.lock().unwrap();
250 let bundle = bundles.get(fv).unwrap();
251 let vc = bundle
252 .variant(variant)
253 .ok_or_else(|| MapperError::VariantNotFound {
254 fv: fv.to_string(),
255 variant: variant.to_string(),
256 })?;
257 let pid_key = format!("pid_{pid}");
258 let requirements =
259 vc.pid_requirements
260 .get(&pid_key)
261 .ok_or_else(|| MapperError::PidNotFound {
262 fv: fv.to_string(),
263 variant: variant.to_string(),
264 pid: pid.to_string(),
265 })?;
266
267 Ok(mig_bo4e::pid_validation::validate_pid_json_transaction(
268 json,
269 requirements,
270 ))
271 }
272
273 pub fn validate_pid_struct(
285 &self,
286 value: &impl serde::Serialize,
287 fv: &str,
288 variant: &str,
289 pid: &str,
290 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
291 let json = serde_json::to_value(value).map_err(|e| {
292 MapperError::Mapping(mig_bo4e::MappingError::TypeConversion(e.to_string()))
293 })?;
294 self.validate_pid(&json, fv, variant, pid)
295 }
296
297 pub fn validate_pid_with_conditions(
305 &self,
306 json: &serde_json::Value,
307 fv: &str,
308 variant: &str,
309 pid: &str,
310 ) -> Result<Vec<mig_bo4e::PidValidationError>, MapperError> {
311 self.ensure_bundle_loaded(fv)?;
312 let bundles = self.bundles.lock().unwrap();
313 let bundle = bundles.get(fv).unwrap();
314 let vc = bundle
315 .variant(variant)
316 .ok_or_else(|| MapperError::VariantNotFound {
317 fv: fv.to_string(),
318 variant: variant.to_string(),
319 })?;
320 let pid_key = format!("pid_{pid}");
321
322 let requirements =
323 vc.pid_requirements
324 .get(&pid_key)
325 .ok_or_else(|| MapperError::PidNotFound {
326 fv: fv.to_string(),
327 variant: variant.to_string(),
328 pid: pid.to_string(),
329 })?;
330
331 let evaluator = crate::evaluator_factory::create_evaluator(variant, fv);
333
334 if let Some(evaluator) = evaluator {
335 let defs = vc
337 .combined_defs
338 .get(&pid_key)
339 .ok_or_else(|| MapperError::PidNotFound {
340 fv: fv.to_string(),
341 variant: variant.to_string(),
342 pid: pid.to_string(),
343 })?;
344 let engine = MappingEngine::from_definitions(defs.clone());
345 let tree = engine.map_all_reverse(json, None);
346
347 let segments = crate::tree_to_segments::tree_to_owned_segments(&tree);
349
350 Ok(crate::evaluator_factory::validate_with_boxed_evaluator(
352 evaluator.as_ref(),
353 json,
354 requirements,
355 pid,
356 &segments,
357 ))
358 } else {
359 Ok(mig_bo4e::pid_validation::validate_pid_json_transaction(
361 json,
362 requirements,
363 ))
364 }
365 }
366
367 pub fn to_edifact(
393 &self,
394 msg_stammdaten: &serde_json::Value,
395 tx_stammdaten: &[serde_json::Value],
396 fv: &str,
397 variant: &str,
398 pid: &str,
399 ) -> Result<String, MapperError> {
400 self.ensure_bundle_loaded(fv)?;
401 let bundles = self.bundles.lock().unwrap();
402 let bundle = bundles.get(fv).unwrap();
403 let vc = bundle
404 .variant(variant)
405 .ok_or_else(|| MapperError::VariantNotFound {
406 fv: fv.to_string(),
407 variant: variant.to_string(),
408 })?;
409
410 let tx_group = vc
411 .tx_group(pid)
412 .ok_or_else(|| MapperError::PidNotFound {
413 fv: fv.to_string(),
414 variant: variant.to_string(),
415 pid: pid.to_string(),
416 })?;
417
418 let msg_engine = vc.msg_engine();
419 let tx_engine =
420 vc.tx_engine(pid)
421 .ok_or_else(|| MapperError::PidNotFound {
422 fv: fv.to_string(),
423 variant: variant.to_string(),
424 pid: pid.to_string(),
425 })?;
426
427 let filtered_mig =
428 vc.filtered_mig(pid)
429 .ok_or_else(|| MapperError::NoMigSchema {
430 fv: fv.to_string(),
431 variant: variant.to_string(),
432 })?;
433
434 let transaktionen: Vec<mig_bo4e::model::MappedTransaktion> = tx_stammdaten
436 .iter()
437 .map(|tx| mig_bo4e::model::MappedTransaktion {
438 stammdaten: tx.clone(),
439 nesting_info: Default::default(),
440 })
441 .collect();
442 let mapped = mig_bo4e::model::MappedMessage {
443 stammdaten: msg_stammdaten.clone(),
444 transaktionen,
445 nesting_info: Default::default(),
446 };
447
448 let tree = MappingEngine::map_interchange_reverse(
450 &msg_engine,
451 &tx_engine,
452 &mapped,
453 tx_group,
454 Some(&filtered_mig),
455 );
456
457 let disassembler =
459 mig_assembly::disassembler::Disassembler::new(&filtered_mig);
460 let segments = disassembler.disassemble(&tree);
461
462 let delimiters = edifact_primitives::EdifactDelimiters::default();
464 Ok(mig_assembly::renderer::render_edifact(
465 &segments,
466 &delimiters,
467 ))
468 }
469
470 pub fn to_edifact_struct(
476 &self,
477 nachricht: &impl serde::Serialize,
478 fv: &str,
479 variant: &str,
480 pid: &str,
481 ) -> Result<String, MapperError> {
482 let json = serde_json::to_value(nachricht)
483 .map_err(|e| MapperError::Serialization(e.to_string()))?;
484
485 let msg_stammdaten = json
486 .get("stammdaten")
487 .cloned()
488 .unwrap_or(serde_json::Value::Object(Default::default()));
489
490 let tx_stammdaten: Vec<serde_json::Value> = json
491 .get("transaktionen")
492 .and_then(|v| v.as_array())
493 .cloned()
494 .unwrap_or_default();
495
496 self.to_edifact(&msg_stammdaten, &tx_stammdaten, fv, variant, pid)
497 }
498
499 pub fn from_edifact<M, T>(
517 &self,
518 edifact: &str,
519 fv: &str,
520 variant: &str,
521 pid: &str,
522 ) -> Result<mig_bo4e::model::Interchange<M, T>, MapperError>
523 where
524 M: serde::de::DeserializeOwned,
525 T: serde::de::DeserializeOwned,
526 {
527 self.ensure_bundle_loaded(fv)?;
528 let bundles = self.bundles.lock().unwrap();
529 let bundle = bundles.get(fv).unwrap();
530 let vc = bundle
531 .variant(variant)
532 .ok_or_else(|| MapperError::VariantNotFound {
533 fv: fv.to_string(),
534 variant: variant.to_string(),
535 })?;
536
537 let tx_group = vc
538 .tx_group(pid)
539 .ok_or_else(|| MapperError::PidNotFound {
540 fv: fv.to_string(),
541 variant: variant.to_string(),
542 pid: pid.to_string(),
543 })?;
544
545 let msg_engine = vc.msg_engine();
546 let tx_engine =
547 vc.tx_engine(pid)
548 .ok_or_else(|| MapperError::PidNotFound {
549 fv: fv.to_string(),
550 variant: variant.to_string(),
551 pid: pid.to_string(),
552 })?;
553
554 let filtered_mig =
555 vc.filtered_mig(pid)
556 .ok_or_else(|| MapperError::NoMigSchema {
557 fv: fv.to_string(),
558 variant: variant.to_string(),
559 })?;
560
561 let svc = ConversionService::from_mig(filtered_mig);
563 let (chunks, trees) = svc.convert_interchange_to_trees(edifact)?;
564
565 let tree = trees
566 .first()
567 .ok_or_else(|| MapperError::Assembly(
568 mig_assembly::AssemblyError::ParseError("No messages in interchange".to_string()),
569 ))?;
570
571 let interchangedaten =
573 mig_bo4e::model::extract_interchangedaten(&chunks.envelope);
574 let msg_chunk = chunks.messages.first().ok_or_else(|| {
575 MapperError::Assembly(mig_assembly::AssemblyError::ParseError(
576 "No message chunks".to_string(),
577 ))
578 })?;
579 let (unh_ref, nachrichten_typ) =
580 mig_bo4e::model::extract_unh_fields(&msg_chunk.unh);
581 let nachrichtendaten = mig_bo4e::model::Nachrichtendaten {
582 unh_referenz: unh_ref,
583 nachrichten_typ,
584 };
585
586 MappingEngine::map_interchange_typed::<M, T>(
588 &msg_engine,
589 &tx_engine,
590 tree,
591 tx_group,
592 true,
593 nachrichtendaten,
594 interchangedaten,
595 )
596 .map_err(|e| MapperError::Serialization(e.to_string()))
597 }
598
599 pub fn detect_pid(&self, edifact: &str) -> Result<String, MapperError> {
611 let segments = mig_assembly::tokenize::parse_to_segments(edifact.as_bytes())?;
612 let chunks = mig_assembly::split_messages(segments)?;
613 let msg_chunk =
614 chunks
615 .messages
616 .first()
617 .ok_or_else(|| MapperError::Assembly(
618 mig_assembly::AssemblyError::ParseError(
619 "No messages found in EDIFACT content".to_string(),
620 ),
621 ))?;
622 let msg_segments = msg_chunk.message_segments();
623 mig_assembly::pid_detect::detect_pid(&msg_segments).map_err(MapperError::Assembly)
624 }
625
626 pub fn association_code(&self, fv: &str, variant: &str) -> Result<String, MapperError> {
637 let meta = self.message_metadata(fv, variant)?;
638 Ok(meta.association_code)
639 }
640
641 pub fn message_metadata(
646 &self,
647 fv: &str,
648 variant: &str,
649 ) -> Result<MessageMetadata, MapperError> {
650 self.ensure_bundle_loaded(fv)?;
651 let bundles = self.bundles.lock().unwrap();
652 let bundle = bundles.get(fv).unwrap();
653 let vc = bundle
654 .variant(variant)
655 .ok_or_else(|| MapperError::VariantNotFound {
656 fv: fv.to_string(),
657 variant: variant.to_string(),
658 })?;
659 let mig = vc
660 .mig_schema
661 .as_ref()
662 .ok_or_else(|| MapperError::NoMigSchema {
663 fv: fv.to_string(),
664 variant: variant.to_string(),
665 })?;
666 Ok(MessageMetadata {
667 message_type: mig.message_type.clone(),
668 release: release_code_for_message_type(&mig.message_type),
669 association_code: mig.version.clone(),
670 })
671 }
672
673 pub fn to_edifact_interchange(
697 &self,
698 envelope: &InterchangeEnvelope,
699 messages: &[InterchangeMessage],
700 ) -> Result<String, MapperError> {
701 let delimiters = edifact_primitives::EdifactDelimiters::default();
702 let sep = delimiters.component as char;
703 let elem = delimiters.element as char;
704 let seg_term = delimiters.segment as char;
705
706 let mut output = String::new();
707
708 output.push_str(&format!(
710 "UNA{}{}{}{}{}{}",
711 sep, elem, delimiters.decimal as char, delimiters.release as char, ' ', seg_term, ));
718
719 let now = chrono::Utc::now();
721 let date_str = now.format("%y%m%d").to_string();
722 let time_str = now.format("%H%M").to_string();
723 let sender = &envelope.sender;
724 let receiver = &envelope.receiver;
725 let interchange_ref = &envelope.interchange_ref;
726 output.push_str(&format!(
727 "UNB{elem}UNOC{sep}3{elem}{sid}{sep}{sq}{elem}{rid}{sep}{rq}{elem}{date_str}{sep}{time_str}{elem}{interchange_ref}{seg_term}",
728 sid = sender.id,
729 sq = sender.qualifier,
730 rid = receiver.id,
731 rq = receiver.qualifier,
732 ));
733
734 let mut message_count = 0u32;
735
736 for msg in messages {
737 let meta = self.message_metadata(&msg.fv, &msg.variant)?;
738
739 let body = self.to_edifact(
741 &msg.msg_stammdaten,
742 &msg.tx_stammdaten,
743 &msg.fv,
744 &msg.variant,
745 &msg.pid,
746 )?;
747
748 let body_seg_count = body
750 .split(seg_term)
751 .filter(|s: &&str| !s.is_empty())
752 .count();
753 let segment_count = body_seg_count + 2;
755
756 output.push_str(&format!(
758 "UNH{elem}{ref}{elem}{msg_type}{sep}D{sep}{release}{sep}UN{sep}{assoc}{seg_term}",
759 ref = msg.message_ref,
760 msg_type = meta.message_type,
761 release = meta.release,
762 assoc = meta.association_code,
763 ));
764
765 output.push_str(&body);
767
768 output.push_str(&format!(
770 "UNT{elem}{segment_count}{elem}{ref}{seg_term}",
771 ref = msg.message_ref,
772 ));
773
774 message_count += 1;
775 }
776
777 output.push_str(&format!(
779 "UNZ{elem}{message_count}{elem}{interchange_ref}{seg_term}",
780 ));
781
782 Ok(output)
783 }
784
785 pub fn loaded_format_versions(&self) -> Vec<String> {
787 self.bundles.lock().unwrap().keys().cloned().collect()
788 }
789
790 pub fn variants(&self, fv: &str) -> Result<Vec<String>, MapperError> {
794 self.ensure_bundle_loaded(fv)?;
795 let bundles = self.bundles.lock().unwrap();
796 let bundle = bundles.get(fv).unwrap();
797 Ok(bundle.variants.keys().cloned().collect())
798 }
799}
800
801#[derive(Debug, Clone)]
803pub struct MessageMetadata {
804 pub message_type: String,
806 pub release: String,
808 pub association_code: String,
810}
811
812#[derive(Debug, Clone)]
814pub struct InterchangeEnvelope {
815 pub sender: EdifactParty,
817 pub receiver: EdifactParty,
819 pub interchange_ref: String,
821}
822
823#[derive(Debug, Clone)]
825pub struct EdifactParty {
826 pub id: String,
828 pub qualifier: String,
830}
831
832impl EdifactParty {
833 pub fn bdew(id: &str) -> Self {
835 Self {
836 id: id.to_string(),
837 qualifier: "500".to_string(),
838 }
839 }
840
841 pub fn gs1(id: &str) -> Self {
843 Self {
844 id: id.to_string(),
845 qualifier: "14".to_string(),
846 }
847 }
848}
849
850#[derive(Debug, Clone)]
853pub struct InterchangeMessage {
854 pub message_ref: String,
856 pub msg_stammdaten: serde_json::Value,
858 pub tx_stammdaten: Vec<serde_json::Value>,
860 pub fv: String,
862 pub variant: String,
864 pub pid: String,
866}
867
868fn release_code_for_message_type(msg_type: &str) -> String {
872 match msg_type {
873 "APERAK" => "07B",
874 "COMDIS" => "17A",
875 "CONTRL" => "04B",
876 "IFTSTA" => "18A",
877 "INSRPT" => "18A",
878 "INVOIC" => "06A",
879 "MSCONS" => "04B",
880 "ORDCHG" => "09B",
881 "ORDERS" => "09B",
882 "ORDRSP" => "10A",
883 "PARTIN" => "20B",
884 "PRICAT" => "20B",
885 "QUOTES" => "10A",
886 "REMADV" => "05A",
887 "REQOTE" => "10A",
888 "UTILMD" => "11A",
889 "UTILTS" => "18A",
890 _ => "04B", }
892 .to_string()
893}
894
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899 use std::path::Path;
900
901 fn data_dir() -> Option<std::path::PathBuf> {
902 let dist = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist");
904 if dist.join("edifact-data-FV2504.bin").exists() {
905 return Some(dist);
906 }
907 let cache = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../cache/mappings");
908 if cache.join("FV2504").exists() {
909 return Some(cache);
910 }
911 eprintln!("Skipping test: no DataBundle files found");
912 None
913 }
914
915 #[test]
916 fn test_to_edifact_produces_edifact_output() {
917 let Some(data_dir) = data_dir() else {
918 return;
919 };
920 let mapper =
921 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
922
923 let msg_stammdaten = serde_json::json!({
924 "marktteilnehmer": [{
925 "marktrolle": "MS",
926 "rollencodenummer": "9900123456789",
927 "codepflegeCode": "293"
928 }]
929 });
930 let tx_stammdaten = serde_json::json!({
931 "prozessdaten": {
932 "pruefidentifikator": "55001",
933 "vorgangId": "ABC123",
934 "transaktionsgrund": "E01"
935 }
936 });
937
938 let result = mapper.to_edifact(
939 &msg_stammdaten,
940 &[tx_stammdaten],
941 "FV2504",
942 "UTILMD_Strom",
943 "55001",
944 );
945 assert!(result.is_ok(), "to_edifact failed: {:?}", result.err());
946 let edifact = result.unwrap();
947 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
948 assert!(edifact.contains("NAD"), "Should contain NAD segment");
950 assert!(edifact.contains("IDE"), "Should contain IDE segment");
952 }
953
954 #[test]
955 fn test_to_edifact_struct_produces_edifact_output() {
956 let Some(data_dir) = data_dir() else {
957 return;
958 };
959 let mapper =
960 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
961
962 let nachricht = serde_json::json!({
963 "stammdaten": {
964 "marktteilnehmer": [{
965 "marktrolle": "MS",
966 "rollencodenummer": "9900123456789",
967 "codepflegeCode": "293"
968 }]
969 },
970 "transaktionen": [{
971 "prozessdaten": {
972 "pruefidentifikator": "55001",
973 "vorgangId": "ABC123"
974 }
975 }]
976 });
977
978 let result = mapper.to_edifact_struct(&nachricht, "FV2504", "UTILMD_Strom", "55001");
979 assert!(
980 result.is_ok(),
981 "to_edifact_struct failed: {:?}",
982 result.err()
983 );
984 let edifact = result.unwrap();
985 assert!(!edifact.is_empty(), "EDIFACT output should not be empty");
986 }
987
988 #[test]
989 fn test_to_edifact_invalid_fv_returns_error() {
990 let Some(data_dir) = data_dir() else {
991 return;
992 };
993 let mapper =
994 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
995
996 let result = mapper.to_edifact(
997 &serde_json::json!({}),
998 &[serde_json::json!({})],
999 "FV9999",
1000 "UTILMD_Strom",
1001 "55001",
1002 );
1003 assert!(result.is_err());
1004 }
1005
1006 #[test]
1007 fn test_to_edifact_invalid_variant_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 "FV2504",
1018 "NONEXISTENT",
1019 "55001",
1020 );
1021 assert!(result.is_err());
1022 }
1023
1024 #[test]
1025 fn test_to_edifact_invalid_pid_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 "UTILMD_Strom",
1037 "99999",
1038 );
1039 assert!(result.is_err());
1040 }
1041
1042 #[test]
1043 fn test_association_code() {
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 code = mapper.association_code("FV2504", "UTILMD_Strom").unwrap();
1051 assert_eq!(code, "S2.1");
1052
1053 let code = mapper.association_code("FV2504", "MSCONS").unwrap();
1054 assert_eq!(code, "2.4c");
1055 }
1056
1057 #[test]
1058 fn test_message_metadata() {
1059 let Some(data_dir) = data_dir() else {
1060 return;
1061 };
1062 let mapper =
1063 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1064
1065 let meta = mapper.message_metadata("FV2504", "UTILMD_Strom").unwrap();
1066 assert_eq!(meta.message_type, "UTILMD");
1067 assert_eq!(meta.release, "11A");
1068 assert_eq!(meta.association_code, "S2.1");
1069 }
1070
1071 #[test]
1072 fn test_to_edifact_interchange() {
1073 let Some(data_dir) = data_dir() else {
1074 return;
1075 };
1076 let mapper =
1077 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1078
1079 let result = mapper.to_edifact_interchange(
1080 &InterchangeEnvelope {
1081 sender: EdifactParty::bdew("9900000000003"),
1082 receiver: EdifactParty::bdew("9900000000001"),
1083 interchange_ref: "REF001".to_string(),
1084 },
1085 &[InterchangeMessage {
1086 message_ref: "MSG001".to_string(),
1087 msg_stammdaten: serde_json::json!({
1088 "marktteilnehmer": [{
1089 "marktrolle": "MS",
1090 "rollencodenummer": "9900123456789",
1091 "codepflegeCode": "293"
1092 }]
1093 }),
1094 tx_stammdaten: vec![serde_json::json!({
1095 "prozessdaten": {
1096 "pruefidentifikator": "55001",
1097 "vorgangId": "ABC123",
1098 "transaktionsgrund": "E01"
1099 }
1100 })],
1101 fv: "FV2504".to_string(),
1102 variant: "UTILMD_Strom".to_string(),
1103 pid: "55001".to_string(),
1104 }],
1105 );
1106 assert!(
1107 result.is_ok(),
1108 "to_edifact_interchange failed: {:?}",
1109 result.err()
1110 );
1111 let edifact = result.unwrap();
1112
1113 assert!(edifact.starts_with("UNA:+.? '"), "Should start with UNA");
1115 assert!(edifact.contains("UNB+UNOC:3+9900000000003:500+9900000000001:500+"),
1116 "Should contain UNB with sender/receiver");
1117 assert!(edifact.contains("UNH+MSG001+UTILMD:D:11A:UN:S2.1'"),
1118 "Should contain UNH with correct S009");
1119 assert!(edifact.contains("NAD"), "Should contain body NAD segment");
1120 assert!(edifact.contains("UNT+"), "Should contain UNT");
1121 assert!(edifact.contains("+MSG001'"), "UNT should reference message ref");
1122 assert!(edifact.contains("UNZ+1+REF001'"), "Should contain UNZ with count and ref");
1123 }
1124
1125 #[test]
1126 fn test_detect_pid_from_rff_z13() {
1127 let Some(data_dir) = data_dir() else {
1128 return;
1129 };
1130 let mapper =
1131 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1132
1133 let edifact = "\
1134 UNB+UNOC:3+9978842000002:500+9900269000000:500+250331:1329+REF001'\
1135 UNH+MSG001+UTILMD:D:11A:UN:S2.1'\
1136 BGM+E01+DOC001'\
1137 DTM+137:202503311329?+00:303'\
1138 NAD+MS+9978842000002::293'\
1139 NAD+MR+9900269000000::293'\
1140 IDE+24+TX001'\
1141 DTM+92:202505312200?+00:303'\
1142 DTM+93:202512312300?+00:303'\
1143 STS+7++E01+ZW4+E03'\
1144 LOC+Z16+12345678900'\
1145 RFF+Z13:55001'\
1146 UNT+12+MSG001'\
1147 UNZ+1+REF001'";
1148
1149 let pid = mapper.detect_pid(edifact).unwrap();
1150 assert_eq!(pid, "55001");
1151 }
1152
1153 #[test]
1154 fn test_detect_pid_no_messages_returns_error() {
1155 let Some(data_dir) = data_dir() else {
1156 return;
1157 };
1158 let mapper =
1159 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1160
1161 let edifact = "UNB+UNOC:3+SENDER:500+RECEIVER:500+250401:1200+REF'\
1162 UNZ+0+REF'";
1163 assert!(mapper.detect_pid(edifact).is_err());
1164 }
1165
1166 #[test]
1167 fn test_list_pids_returns_entries() {
1168 let Some(data_dir) = data_dir() else {
1169 return;
1170 };
1171 let mapper = Mapper::from_data_dir(DataDir::path(&data_dir)).unwrap();
1172 let pids = mapper.list_pids().expect("list_pids should succeed");
1173 assert!(!pids.is_empty(), "should return at least one PID");
1174 assert!(
1175 pids.iter().any(|p| p.pid == "55001"),
1176 "should include PID 55001"
1177 );
1178 assert!(
1179 pids.iter().any(|p| p.fv == "FV2504"),
1180 "should include FV2504"
1181 );
1182 assert!(
1183 pids.iter().any(|p| p.variant == "UTILMD_Strom"),
1184 "should include UTILMD_Strom"
1185 );
1186 }
1187
1188 #[test]
1189 fn test_pid_requirements_returns_requirements() {
1190 let Some(data_dir) = data_dir() else {
1191 return;
1192 };
1193 let mapper =
1194 Mapper::from_data_dir(DataDir::path(&data_dir).eager(&["FV2504"])).unwrap();
1195
1196 let req = mapper
1197 .pid_requirements("FV2504", "UTILMD_Strom", "55001")
1198 .expect("pid_requirements should succeed");
1199
1200 assert_eq!(req.pid, "55001");
1201 assert!(
1202 !req.entities.is_empty(),
1203 "55001 should have at least one entity"
1204 );
1205 assert!(
1206 req.entities.iter().any(|e| e.entity == "Prozessdaten"),
1207 "55001 should have a Prozessdaten entity"
1208 );
1209 }
1210}