1use anyhow::{Context, Result};
2use quick_xml::de::from_reader;
3use serde::{Deserialize, Serialize};
4use std::fs::File;
5use std::io::BufReader;
6use std::path::Path;
7
8mod bool_from_string {
10 use serde::{Deserialize, Deserializer};
11
12 pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
13 where
14 D: Deserializer<'de>,
15 {
16 let s = String::deserialize(deserializer)?;
17 match s.as_str() {
18 "1" => Ok(true),
19 "0" => Ok(false),
20 _ => Err(serde::de::Error::custom(format!(
21 "expected '0' or '1', got '{}'",
22 s
23 ))),
24 }
25 }
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29pub struct AtcRecord {
30 #[serde(rename(deserialize = "nroatc"))]
31 pub number: i32,
32 #[serde(rename(deserialize = "codigoatc"))]
33 pub code: String,
34 #[serde(rename(deserialize = "descatc"))]
35 pub description: String,
36}
37
38#[derive(Debug, Deserialize)]
39#[serde(rename = "aemps_prescripcion_atc")]
40struct AtcList {
41 #[serde(rename = "atc")]
42 records: Vec<AtcRecord>,
43}
44
45#[derive(Debug, Serialize, Deserialize)]
46pub struct DcpRecord {
47 #[serde(rename(deserialize = "codigodcp"))]
48 pub code: String,
49 #[serde(rename(deserialize = "nombredcp"))]
50 pub name: String,
51 #[serde(rename(deserialize = "codigodcsa"))]
52 pub dcsa_code: String,
53}
54
55#[derive(Debug, Deserialize)]
56#[serde(rename = "aemps_prescripcion_dcp")]
57struct DcpList {
58 #[serde(rename = "dcp")]
59 records: Vec<DcpRecord>,
60}
61#[derive(Debug, Serialize, Deserialize)]
62pub struct DcpfRecord {
63 #[serde(rename(deserialize = "codigodcpf"))]
64 pub code: String,
65 #[serde(rename(deserialize = "nombredcpf"))]
66 pub name: String,
67 #[serde(rename(deserialize = "codigodcp"))]
68 pub dcp_code: String,
69}
70
71#[derive(Debug, Deserialize)]
72#[serde(rename = "aemps_prescripcion_dcpf")]
73struct DcpfList {
74 #[serde(rename = "dcpf")]
75 records: Vec<DcpfRecord>,
76}
77
78#[derive(Debug, Serialize, Deserialize)]
79pub struct DcsaRecord {
80 #[serde(rename(deserialize = "codigodcsa"))]
81 pub code: String,
82 #[serde(rename(deserialize = "nombredcsa"))]
83 pub name: String,
84}
85
86#[derive(Debug, Deserialize)]
87#[serde(rename = "aemps_prescripcion_dcsa")]
88struct DcsaList {
89 #[serde(rename = "dcsa")]
90 records: Vec<DcsaRecord>,
91}
92
93#[derive(Debug, Serialize, Deserialize)]
94pub struct ContainerRecord {
95 #[serde(rename(deserialize = "codigoenvase"))]
96 pub code: String,
97 #[serde(rename(deserialize = "envase"))]
98 pub name: String,
99}
100
101#[derive(Debug, Deserialize)]
102#[serde(rename = "aemps_prescripcion_envases")]
103struct ContainerList {
104 #[serde(rename = "envases")]
105 records: Vec<ContainerRecord>,
106}
107#[derive(Debug, Serialize, Deserialize)]
108pub struct ExcipientRecord {
109 #[serde(rename(deserialize = "codigoedo"))]
110 pub code: String,
111 #[serde(rename(deserialize = "edo"))]
112 pub name: String,
113}
114
115#[derive(Debug, Deserialize)]
116#[serde(rename = "aemps_prescripcion_excipientes")]
117struct ExcipientList {
118 #[serde(rename = "excipientes")]
119 records: Vec<ExcipientRecord>,
120}
121
122#[derive(Debug, Serialize, Deserialize)]
123pub struct PharmaceuticalFormRecord {
124 #[serde(rename(deserialize = "codigoformafarmaceutica"))]
125 pub code: String,
126 #[serde(rename(deserialize = "formafarmaceutica"))]
127 pub name: String,
128 #[serde(rename(deserialize = "codigoformafarmaceuticasimplificada"), default)]
129 pub simplified_code: Option<String>,
130}
131
132#[derive(Debug, Deserialize)]
133#[serde(rename = "aemps_prescripcion_formas_farmaceuticas")]
134struct PharmaceuticalFormList {
135 #[serde(rename = "formasfarmaceuticas")]
136 records: Vec<PharmaceuticalFormRecord>,
137}
138
139#[derive(Debug, Serialize, Deserialize)]
140pub struct SimplifiedPharmaceuticalFormRecord {
141 #[serde(rename(deserialize = "codigoformafarmaceuticasimplificada"))]
142 pub code: String,
143 #[serde(rename(deserialize = "formafarmaceuticasimplificada"))]
144 pub name: String,
145}
146
147#[derive(Debug, Deserialize)]
148#[serde(rename = "aemps_prescripcion_formas_farmaceuticas_simplificadas")]
149struct SimplifiedPharmaceuticalFormList {
150 #[serde(rename = "formasfarmaceuticassimplificadas")]
151 records: Vec<SimplifiedPharmaceuticalFormRecord>,
152}
153
154#[derive(Debug, Serialize, Deserialize)]
155pub struct LaboratoryRecord {
156 #[serde(rename(deserialize = "codigolaboratorio"))]
157 pub code: String,
158 #[serde(rename(deserialize = "laboratorio"))]
159 pub name: String,
160 #[serde(rename(deserialize = "direccion"))]
161 pub address: Option<String>,
162 #[serde(rename(deserialize = "codigopostal"))]
163 pub zip: Option<String>,
164 #[serde(rename(deserialize = "localidad"))]
165 pub city: Option<String>,
166 #[serde(rename(deserialize = "cif"))]
167 pub vat: Option<String>,
168}
169
170#[derive(Debug, Deserialize)]
171#[serde(rename = "aemps_prescripcion_laboratorios")]
172struct LaboratoryList {
173 #[serde(rename = "laboratorios")]
174 records: Vec<LaboratoryRecord>,
175}
176
177#[derive(Debug, Serialize, Deserialize)]
178pub struct ActiveIngridientRecord {
179 #[serde(rename(deserialize = "nroprincipioactivo"))]
180 pub number: String,
181 #[serde(rename(deserialize = "codigoprincipioactivo"))]
182 pub code: String,
183 #[serde(rename(deserialize = "principioactivo"))]
184 pub name: String,
185}
186
187#[derive(Debug, Deserialize)]
188#[serde(rename = "aemps_prescripcion_principios_activos")]
189struct ActiveIngredientList {
190 #[serde(rename = "principiosactivos")]
191 records: Vec<ActiveIngridientRecord>,
192}
193
194#[derive(Debug, Serialize, Deserialize)]
195pub struct RegistrationStatusRecord {
196 #[serde(rename(deserialize = "codigosituacionregistro"))]
197 pub code: String,
198 #[serde(rename(deserialize = "situacionregistro"))]
199 pub name: String,
200}
201
202#[derive(Debug, Deserialize)]
203#[serde(rename = "aemps_prescripcion_situacion_registro")]
204struct RegistrationStatusList {
205 #[serde(rename = "situacionesregistro")]
206 records: Vec<RegistrationStatusRecord>,
207}
208
209#[derive(Debug, Serialize, Deserialize)]
210pub struct ContainerUnitRecord {
211 #[serde(rename(deserialize = "codigounidadcontenido"))]
212 pub code: String,
213 #[serde(rename(deserialize = "unidadcontenido"))]
214 pub name: String,
215}
216
217#[derive(Debug, Deserialize)]
218#[serde(rename = "aemps_prescripcion_unidad_contenido")]
219struct ContainerUnitList {
220 #[serde(rename = "unidadescontenido")]
221 records: Vec<ContainerUnitRecord>,
222}
223
224#[derive(Debug, Serialize, Deserialize)]
225pub struct AdministrationRouteRecord {
226 #[serde(rename(deserialize = "codigoviaadministracion"))]
227 pub code: String,
228 #[serde(rename(deserialize = "viaadministracion"))]
229 pub name: String,
230}
231
232#[derive(Debug, Deserialize)]
233#[serde(rename = "aemps_prescripcion_vias_administracion")]
234struct AdministrationRouteList {
235 #[serde(rename = "viasadministracion")]
236 records: Vec<AdministrationRouteRecord>,
237}
238
239#[derive(Debug, Serialize, Deserialize, Clone)]
245pub struct ActiveIngredient {
246 #[serde(rename(deserialize = "cod_principio_activo"), default)]
247 pub active_ingredient_code: Option<String>,
248 #[serde(rename(deserialize = "orden_colacion"))]
249 pub order: Option<String>,
250 #[serde(rename(deserialize = "dosis_pa"))]
251 pub dose: Option<String>,
252 #[serde(rename(deserialize = "unidad_dosis_pa"))]
253 pub dose_unit: Option<String>,
254 #[serde(rename(deserialize = "dosis_composicion"))]
255 pub composition_dose: Option<String>,
256 #[serde(rename(deserialize = "unidad_composicion"))]
257 pub composition_unit: Option<String>,
258 #[serde(rename(deserialize = "dosis_administracion"))]
259 pub administration_dose: Option<String>,
260 #[serde(rename(deserialize = "unidad_administracion"))]
261 pub administration_unit: Option<String>,
262 #[serde(rename(deserialize = "dosis_prescripcion"))]
263 pub prescription_dose: Option<String>,
264 #[serde(rename(deserialize = "unidad_prescripcion"))]
265 pub prescription_unit: Option<String>,
266}
267
268#[derive(Debug, Serialize, Deserialize, Clone)]
270pub struct AdminRoute {
271 #[serde(rename(deserialize = "cod_via_admin"))]
272 pub route_code: String,
273}
274
275#[derive(Debug, Serialize, Deserialize, Clone)]
277pub struct PrescriptionForm {
278 #[serde(rename(deserialize = "cod_forfar"))]
279 pub form_code: String,
280 #[serde(rename(deserialize = "cod_forfar_simplificada"))]
281 pub simplified_form_code: Option<String>,
282 #[serde(rename(deserialize = "nro_pactiv"))]
283 pub num_active_ingredients: Option<String>,
284 #[serde(rename(deserialize = "composicion_pa"), default)]
285 pub active_ingredients: Vec<ActiveIngredient>,
286 #[serde(rename(deserialize = "viasadministracion"), default)]
287 pub admin_routes: Vec<AdminRoute>,
288}
289
290#[derive(Debug, Serialize, Deserialize, Clone)]
292pub struct AtcDuplicate {
293 #[serde(rename(deserialize = "atc_duplicidad"))]
294 pub duplicate_atc: String,
295 #[serde(rename(deserialize = "descripcion_atc_duplicidad"))]
296 pub description: Option<String>,
297 #[serde(rename(deserialize = "efecto_duplicidad"))]
298 pub effect: Option<String>,
299 #[serde(rename(deserialize = "recomendacion_duplicidad"))]
300 pub recommendation: Option<String>,
301}
302
303#[derive(Debug, Serialize, Deserialize, Clone)]
305pub struct PrescriptionAtc {
306 #[serde(rename(deserialize = "cod_atc"))]
307 pub atc_code: String,
308 #[serde(rename(deserialize = "duplicidades"), default)]
309 pub duplicates: Vec<AtcDuplicate>,
310}
311
312#[derive(Debug, Serialize, Deserialize, Clone)]
314pub struct SupplyProblem {
315 #[serde(rename(deserialize = "fecha_inicio"))]
316 pub start_date: Option<String>,
317 #[serde(rename(deserialize = "observaciones"))]
318 pub observations: Option<String>,
319}
320
321#[derive(Debug, Serialize, Deserialize)]
326pub struct PrescriptionRecord {
327 pub cod_nacion: String,
328 pub nro_definitivo: String,
329 pub des_nomco: String,
330 pub des_prese: String,
331 pub cod_dcsa: Option<String>,
332 pub cod_dcp: Option<String>,
333 pub cod_dcpf: Option<String>,
334 pub des_dosific: Option<String>,
335 pub cod_envase: Option<String>,
336 pub contenido: Option<String>,
337 pub unid_contenido: Option<String>,
338 pub nro_conte: Option<String>,
339 #[serde(deserialize_with = "bool_from_string::deserialize")]
340 pub sw_psicotropo: bool,
341 #[serde(deserialize_with = "bool_from_string::deserialize")]
342 pub sw_estupefaciente: bool,
343 #[serde(deserialize_with = "bool_from_string::deserialize")]
344 pub sw_afecta_conduccion: bool,
345 #[serde(deserialize_with = "bool_from_string::deserialize")]
346 pub sw_triangulo_negro: bool,
347 pub url_fictec: Option<String>,
348 pub url_prosp: Option<String>,
349 #[serde(deserialize_with = "bool_from_string::deserialize")]
350 pub sw_receta: bool,
351 #[serde(deserialize_with = "bool_from_string::deserialize")]
352 pub sw_generico: bool,
353 #[serde(deserialize_with = "bool_from_string::deserialize")]
354 pub sw_sustituible: bool,
355 #[serde(deserialize_with = "bool_from_string::deserialize")]
356 pub sw_envase_clinico: bool,
357 #[serde(deserialize_with = "bool_from_string::deserialize")]
358 pub sw_uso_hospitalario: bool,
359 #[serde(deserialize_with = "bool_from_string::deserialize")]
360 pub sw_diagnostico_hospitalario: bool,
361 #[serde(deserialize_with = "bool_from_string::deserialize")]
362 pub sw_tld: bool,
363 #[serde(deserialize_with = "bool_from_string::deserialize")]
364 pub sw_especial_control_medico: bool,
365 #[serde(deserialize_with = "bool_from_string::deserialize")]
366 pub sw_huerfano: bool,
367 #[serde(deserialize_with = "bool_from_string::deserialize")]
368 pub sw_base_a_plantas: bool,
369 pub laboratorio_titular: Option<String>,
370 pub laboratorio_comercializador: Option<String>,
371 pub fecha_autorizacion: Option<String>,
372 #[serde(deserialize_with = "bool_from_string::deserialize")]
373 pub sw_comercializado: bool,
374 pub fec_comer: Option<String>,
375 pub cod_sitreg: Option<String>,
376 pub cod_sitreg_presen: Option<String>,
377 pub fecha_situacion_registro: Option<String>,
378 pub fec_sitreg_presen: Option<String>,
379 #[serde(deserialize_with = "bool_from_string::deserialize")]
380 pub sw_tiene_excipientes_decl_obligatoria: bool,
381 #[serde(deserialize_with = "bool_from_string::deserialize")]
382 pub biosimilar: bool,
383 #[serde(deserialize_with = "bool_from_string::deserialize")]
384 pub importacion_paralela: bool,
385 #[serde(deserialize_with = "bool_from_string::deserialize")]
386 pub radiofarmaco: bool,
387 #[serde(deserialize_with = "bool_from_string::deserialize")]
388 pub serializacion: bool,
389
390 #[serde(rename(deserialize = "formasfarmaceuticas"), default, skip_serializing)]
392 pub forms: Option<PrescriptionForm>,
393
394 #[serde(rename(deserialize = "atc"), default, skip_serializing)]
395 pub atc_codes: Vec<PrescriptionAtc>,
396
397 #[serde(rename(deserialize = "problemassuministro"), default, skip_serializing)]
398 pub supply_problems: Vec<SupplyProblem>,
399}
400
401#[derive(Debug, Deserialize)]
402pub struct Header {
403 pub listprescriptiondate: String,
404}
405
406#[derive(Debug, Deserialize)]
407#[serde(rename = "aemps_prescripcion")]
408pub struct PrescriptionList {
409 pub header: Option<Header>,
410 #[serde(rename = "prescription")]
411 pub records: Vec<PrescriptionRecord>,
412}
413
414macro_rules! impl_xml_parser {
415 ($(#[$attr:meta])* $fn_name:ident, $list_type:ty, $error_ctx:expr) => {
416 $(#[$attr])*
417 pub fn $fn_name<P: AsRef<Path>>(xml_path: P, csv_path: P) -> Result<()> {
418 let file = File::open(xml_path)?;
419 let reader = BufReader::new(file);
420 let list: $list_type = from_reader(reader).context($error_ctx)?;
421
422 let mut wtr = csv::Writer::from_path(csv_path)?;
423 for record in list.records {
424 wtr.serialize(record)?;
425 }
426 wtr.flush()?;
427
428 Ok(())
429 }
430 };
431 ($(#[$attr:meta])* $fn_name:ident, $list_type:ty, $error_ctx:expr, $mut_record:ident, $transform:block) => {
432 $(#[$attr])*
433 pub fn $fn_name<P: AsRef<Path>>(xml_path: P, csv_path: P) -> Result<()> {
434 let file = File::open(xml_path)?;
435 let reader = BufReader::new(file);
436 let list: $list_type = from_reader(reader).context($error_ctx)?;
437
438 let mut wtr = csv::Writer::from_path(csv_path)?;
439 for mut $mut_record in list.records {
440 $transform
441 wtr.serialize($mut_record)?;
442 }
443 wtr.flush()?;
444
445 Ok(())
446 }
447 };
448}
449
450impl_xml_parser!(
451 parse_atc_xml_to_csv,
453 AtcList,
454 "Failed to deserialize ATC XML",
455 record,
456 {
457 let prefix = format!("{} - ", record.code);
459 if record.description.starts_with(&prefix) {
460 record.description = record.description[prefix.len()..].to_string();
461 }
462 }
463);
464
465impl_xml_parser!(
466 parse_dcp_xml_to_csv,
468 DcpList,
469 "Failed to deserialize DCP XML"
470);
471
472impl_xml_parser!(
473 parse_dcpf_xml_to_csv,
475 DcpfList,
476 "Failed to deserialize DCPF XML"
477);
478
479impl_xml_parser!(
480 parse_dcsa_xml_to_csv,
482 DcsaList,
483 "Failed to deserialize DCSA XML"
484);
485
486impl_xml_parser!(
487 parse_envases_xml_to_csv,
489 ContainerList,
490 "Failed to deserialize Envases XML"
491);
492
493impl_xml_parser!(
494 parse_excipientes_xml_to_csv,
496 ExcipientList,
497 "Failed to deserialize Excipientes XML"
498);
499
500impl_xml_parser!(
501 parse_forma_farmaceutica_xml_to_csv,
503 PharmaceuticalFormList,
504 "Failed to deserialize Forma Farmaceutica XML"
505);
506
507impl_xml_parser!(
508 parse_forma_farmaceutica_simplificada_xml_to_csv,
510 SimplifiedPharmaceuticalFormList,
511 "Failed to deserialize Forma Farmaceutica Simplificada XML"
512);
513
514impl_xml_parser!(
515 parse_laboratorio_xml_to_csv,
517 LaboratoryList,
518 "Failed to deserialize Laboratorio XML"
519);
520
521impl_xml_parser!(
522 parse_principio_activo_xml_to_csv,
524 ActiveIngredientList,
525 "Failed to deserialize Principio Activo XML"
526);
527
528impl_xml_parser!(
529 parse_situacion_registro_xml_to_csv,
531 RegistrationStatusList,
532 "Failed to deserialize Situacion Registro XML"
533);
534
535impl_xml_parser!(
536 parse_unidad_contenido_xml_to_csv,
538 ContainerUnitList,
539 "Failed to deserialize Unidad Contenido XML"
540);
541
542impl_xml_parser!(
543 parse_via_administracion_xml_to_csv,
545 AdministrationRouteList,
546 "Failed to deserialize Via Administracion XML"
547);
548
549impl_xml_parser!(
550 parse_prescription_xml_to_csv,
552 PrescriptionList,
553 "Failed to deserialize Prescription XML"
554);
555
556pub fn parse_prescription_xml_to_csvs<P: AsRef<Path>>(xml_path: P, output_dir: P) -> Result<()> {
570 let file = File::open(xml_path)?;
571 let reader = BufReader::new(file);
572 let list: PrescriptionList =
573 from_reader(reader).context("Failed to deserialize Prescription XML")?;
574
575 let mut wtr_main = csv::Writer::from_path(output_dir.as_ref().join("prescriptions.csv"))?;
577 let mut wtr_forms = csv::Writer::from_path(output_dir.as_ref().join("prescription_forms.csv"))?;
578 let mut wtr_ingredients = csv::Writer::from_path(
579 output_dir
580 .as_ref()
581 .join("prescription_active_ingredients.csv"),
582 )?;
583 let mut wtr_routes =
584 csv::Writer::from_path(output_dir.as_ref().join("prescription_admin_routes.csv"))?;
585 let mut wtr_atc = csv::Writer::from_path(output_dir.as_ref().join("prescription_atc.csv"))?;
586 let mut wtr_atc_duplicates =
587 csv::Writer::from_path(output_dir.as_ref().join("prescription_atc_duplicates.csv"))?;
588 let mut wtr_supply =
589 csv::Writer::from_path(output_dir.as_ref().join("prescription_supply_problems.csv"))?;
590
591 for record in list.records {
593 let prescription_id = record.cod_nacion.clone();
595
596 wtr_main.serialize(&record)?;
598
599 if let Some(form) = &record.forms {
601 wtr_forms.write_record([
603 &prescription_id,
604 &form.form_code,
605 form.simplified_form_code.as_deref().unwrap_or(""),
606 form.num_active_ingredients.as_deref().unwrap_or(""),
607 ])?;
608
609 for ingredient in &form.active_ingredients {
611 wtr_ingredients.write_record([
612 &prescription_id,
613 ingredient.active_ingredient_code.as_deref().unwrap_or(""),
614 ingredient.order.as_deref().unwrap_or(""),
615 ingredient.dose.as_deref().unwrap_or(""),
616 ingredient.dose_unit.as_deref().unwrap_or(""),
617 ingredient.composition_dose.as_deref().unwrap_or(""),
618 ingredient.composition_unit.as_deref().unwrap_or(""),
619 ingredient.administration_dose.as_deref().unwrap_or(""),
620 ingredient.administration_unit.as_deref().unwrap_or(""),
621 ingredient.prescription_dose.as_deref().unwrap_or(""),
622 ingredient.prescription_unit.as_deref().unwrap_or(""),
623 ])?;
624 }
625
626 for route in &form.admin_routes {
628 wtr_routes.write_record([&prescription_id, &route.route_code])?;
629 }
630 }
631
632 for atc in &record.atc_codes {
634 wtr_atc.write_record([&prescription_id, &atc.atc_code])?;
635
636 for duplicate in &atc.duplicates {
638 wtr_atc_duplicates.write_record([
639 &prescription_id,
640 &atc.atc_code,
641 &duplicate.duplicate_atc,
642 duplicate.description.as_deref().unwrap_or(""),
643 duplicate.effect.as_deref().unwrap_or(""),
644 duplicate.recommendation.as_deref().unwrap_or(""),
645 ])?;
646 }
647 }
648
649 for problem in &record.supply_problems {
651 wtr_supply.write_record([
652 &prescription_id,
653 problem.start_date.as_deref().unwrap_or(""),
654 problem.observations.as_deref().unwrap_or(""),
655 ])?;
656 }
657 }
658
659 wtr_main.flush()?;
661 wtr_forms.flush()?;
662 wtr_ingredients.flush()?;
663 wtr_routes.flush()?;
664 wtr_atc.flush()?;
665 wtr_atc_duplicates.flush()?;
666 wtr_supply.flush()?;
667
668 Ok(())
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use std::io::Write;
675 use tempfile::NamedTempFile;
676
677 #[test]
678 fn test_parse_atc_xml() {
679 let mut xml_file = NamedTempFile::new().unwrap();
680 writeln!(
681 xml_file,
682 r#"<aemps_prescripcion_atc>
683 <atc>
684 <nroatc>1</nroatc>
685 <codigoatc>A01</codigoatc>
686 <descatc>A01 - DIGESTIVE</descatc>
687 </atc>
688 <atc>
689 <nroatc>2</nroatc>
690 <codigoatc>B01</codigoatc>
691 <descatc>B01 - BLOOD</descatc>
692 </atc>
693 </aemps_prescripcion_atc>"#
694 )
695 .unwrap();
696
697 let csv_file = NamedTempFile::new().unwrap();
698 let xml_path = xml_file.path();
699 let csv_path = csv_file.path();
700
701 let result = parse_atc_xml_to_csv(xml_path, csv_path);
702 assert!(result.is_ok());
703
704 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
705
706 let headers = csv_reader.headers().unwrap();
708 assert_eq!(headers.get(0).unwrap(), "number");
709 assert_eq!(headers.get(1).unwrap(), "code");
710 assert_eq!(headers.get(2).unwrap(), "description");
711
712 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
714 assert_eq!(records.len(), 2);
715 assert_eq!(records[0].get(1).unwrap(), "A01");
716 assert_eq!(records[0].get(2).unwrap(), "DIGESTIVE");
717 assert_eq!(records[1].get(1).unwrap(), "B01");
718 assert_eq!(records[1].get(2).unwrap(), "BLOOD");
719 }
720
721 #[test]
722 fn test_parse_dcp_xml() {
723 let mut xml_file = NamedTempFile::new().unwrap();
724 writeln!(
725 xml_file,
726 r#"<aemps_prescripcion_dcp>
727 <dcp>
728 <codigodcp>D01</codigodcp>
729 <nombredcp>DCP NAME</nombredcp>
730 <codigodcsa>S01</codigodcsa>
731 </dcp>
732 </aemps_prescripcion_dcp>"#
733 )
734 .unwrap();
735
736 let csv_file = NamedTempFile::new().unwrap();
737 let xml_path = xml_file.path();
738 let csv_path = csv_file.path();
739
740 let result = parse_dcp_xml_to_csv(xml_path, csv_path);
741 assert!(result.is_ok());
742
743 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
744
745 let headers = csv_reader.headers().unwrap();
747 assert_eq!(headers.get(0).unwrap(), "code");
748 assert_eq!(headers.get(1).unwrap(), "name");
749 assert_eq!(headers.get(2).unwrap(), "dcsa_code");
750
751 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
753 assert_eq!(records.len(), 1);
754 assert_eq!(records[0].get(0).unwrap(), "D01");
755 assert_eq!(records[0].get(1).unwrap(), "DCP NAME");
756 }
757
758 #[test]
759 fn test_parse_dcpf_xml() {
760 let mut xml_file = NamedTempFile::new().unwrap();
761 writeln!(
762 xml_file,
763 r#"<aemps_prescripcion_dcpf>
764 <dcpf>
765 <codigodcpf>DF01</codigodcpf>
766 <nombredcpf>DCPF NAME</nombredcpf>
767 <codigodcp>D01</codigodcp>
768 </dcpf>
769 </aemps_prescripcion_dcpf>"#
770 )
771 .unwrap();
772
773 let csv_file = NamedTempFile::new().unwrap();
774 let xml_path = xml_file.path();
775 let csv_path = csv_file.path();
776
777 let result = parse_dcpf_xml_to_csv(xml_path, csv_path);
778 assert!(result.is_ok());
779
780 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
781
782 let headers = csv_reader.headers().unwrap();
784 assert_eq!(headers.get(0).unwrap(), "code");
785 assert_eq!(headers.get(1).unwrap(), "name");
786 assert_eq!(headers.get(2).unwrap(), "dcp_code");
787
788 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
790 assert_eq!(records.len(), 1);
791 assert_eq!(records[0].get(0).unwrap(), "DF01");
792 assert_eq!(records[0].get(1).unwrap(), "DCPF NAME");
793 }
794
795 #[test]
796 fn test_parse_dcsa_xml() {
797 let mut xml_file = NamedTempFile::new().unwrap();
798 writeln!(
799 xml_file,
800 r#"<aemps_prescripcion_dcsa>
801 <dcsa>
802 <codigodcsa>S01</codigodcsa>
803 <nombredcsa>DCSA NAME</nombredcsa>
804 </dcsa>
805 </aemps_prescripcion_dcsa>"#
806 )
807 .unwrap();
808
809 let csv_file = NamedTempFile::new().unwrap();
810 let xml_path = xml_file.path();
811 let csv_path = csv_file.path();
812
813 let result = parse_dcsa_xml_to_csv(xml_path, csv_path);
814 assert!(result.is_ok());
815
816 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
817
818 let headers = csv_reader.headers().unwrap();
820 assert_eq!(headers.get(0).unwrap(), "code");
821 assert_eq!(headers.get(1).unwrap(), "name");
822
823 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
825 assert_eq!(records.len(), 1);
826 assert_eq!(records[0].get(0).unwrap(), "S01");
827 assert_eq!(records[0].get(1).unwrap(), "DCSA NAME");
828 }
829
830 #[test]
831 fn test_parse_envases_xml() {
832 let mut xml_file = NamedTempFile::new().unwrap();
833 writeln!(
834 xml_file,
835 r#"<aemps_prescripcion_envases>
836 <envases>
837 <codigoenvase>E01</codigoenvase>
838 <envase>ENVASE NAME</envase>
839 </envases>
840 </aemps_prescripcion_envases>"#
841 )
842 .unwrap();
843
844 let csv_file = NamedTempFile::new().unwrap();
845 let xml_path = xml_file.path();
846 let csv_path = csv_file.path();
847
848 let result = parse_envases_xml_to_csv(xml_path, csv_path);
849 assert!(result.is_ok());
850
851 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
852
853 let headers = csv_reader.headers().unwrap();
855 assert_eq!(headers.get(0).unwrap(), "code");
856 assert_eq!(headers.get(1).unwrap(), "name");
857
858 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
860 assert_eq!(records.len(), 1);
861 assert_eq!(records[0].get(0).unwrap(), "E01");
862 assert_eq!(records[0].get(1).unwrap(), "ENVASE NAME");
863 }
864
865 #[test]
866 fn test_parse_excipientes_xml() {
867 let mut xml_file = NamedTempFile::new().unwrap();
868 writeln!(
869 xml_file,
870 r#"<aemps_prescripcion_excipientes>
871 <excipientes>
872 <codigoedo>X01</codigoedo>
873 <edo>EXCIPIENTE NAME</edo>
874 </excipientes>
875 </aemps_prescripcion_excipientes>"#
876 )
877 .unwrap();
878
879 let csv_file = NamedTempFile::new().unwrap();
880 let xml_path = xml_file.path();
881 let csv_path = csv_file.path();
882
883 let result = parse_excipientes_xml_to_csv(xml_path, csv_path);
884 assert!(result.is_ok());
885
886 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
887
888 let headers = csv_reader.headers().unwrap();
890 assert_eq!(headers.get(0).unwrap(), "code");
891 assert_eq!(headers.get(1).unwrap(), "name");
892
893 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
895 assert_eq!(records.len(), 1);
896 assert_eq!(records[0].get(0).unwrap(), "X01");
897 assert_eq!(records[0].get(1).unwrap(), "EXCIPIENTE NAME");
898 }
899
900 #[test]
901 fn test_parse_forma_farmaceutica_xml() {
902 let mut xml_file = NamedTempFile::new().unwrap();
903 writeln!(
904 xml_file,
905 r#"<aemps_prescripcion_formas_farmaceuticas>
906 <formasfarmaceuticas>
907 <codigoformafarmaceutica>FF01</codigoformafarmaceutica>
908 <formafarmaceutica>FORMA NAME</formafarmaceutica>
909 <codigoformafarmaceuticasimplificada>SFF01</codigoformafarmaceuticasimplificada>
910 </formasfarmaceuticas>
911 </aemps_prescripcion_formas_farmaceuticas>"#
912 )
913 .unwrap();
914
915 let csv_file = NamedTempFile::new().unwrap();
916 let xml_path = xml_file.path();
917 let csv_path = csv_file.path();
918
919 let result = parse_forma_farmaceutica_xml_to_csv(xml_path, csv_path);
920 assert!(result.is_ok());
921
922 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
923
924 let headers = csv_reader.headers().unwrap();
926 assert_eq!(headers.get(0).unwrap(), "code");
927 assert_eq!(headers.get(1).unwrap(), "name");
928 assert_eq!(headers.get(2).unwrap(), "simplified_code");
929
930 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
932 assert_eq!(records.len(), 1);
933 assert_eq!(records[0].get(0).unwrap(), "FF01");
934 assert_eq!(records[0].get(1).unwrap(), "FORMA NAME");
935 assert_eq!(records[0].get(2).unwrap(), "SFF01");
936 }
937
938 #[test]
939 fn test_parse_forma_farmaceutica_simplificada_xml() {
940 let mut xml_file = NamedTempFile::new().unwrap();
941 writeln!(
942 xml_file,
943 r#"<aemps_prescripcion_formas_farmaceuticas_simplificadas>
944 <formasfarmaceuticassimplificadas>
945 <codigoformafarmaceuticasimplificada>SFF01</codigoformafarmaceuticasimplificada>
946 <formafarmaceuticasimplificada>SIMPLIFIED NAME</formafarmaceuticasimplificada>
947 </formasfarmaceuticassimplificadas>
948 </aemps_prescripcion_formas_farmaceuticas_simplificadas>"#
949 )
950 .unwrap();
951
952 let csv_file = NamedTempFile::new().unwrap();
953 let xml_path = xml_file.path();
954 let csv_path = csv_file.path();
955
956 let result = parse_forma_farmaceutica_simplificada_xml_to_csv(xml_path, csv_path);
957 assert!(result.is_ok());
958
959 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
960
961 let headers = csv_reader.headers().unwrap();
963 assert_eq!(headers.get(0).unwrap(), "code");
964 assert_eq!(headers.get(1).unwrap(), "name");
965
966 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
968 assert_eq!(records.len(), 1);
969 assert_eq!(records[0].get(0).unwrap(), "SFF01");
970 assert_eq!(records[0].get(1).unwrap(), "SIMPLIFIED NAME");
971 }
972
973 #[test]
974 fn test_parse_laboratorio_xml() {
975 let mut xml_file = NamedTempFile::new().unwrap();
976 writeln!(
977 xml_file,
978 r#"<aemps_prescripcion_laboratorios>
979 <laboratorios>
980 <codigolaboratorio>L01</codigolaboratorio>
981 <laboratorio>LAB NAME</laboratorio>
982 <direccion>ADDR</direccion>
983 <codigopostal>ZIP</codigopostal>
984 <localidad>CITY</localidad>
985 <cif>VAT</cif>
986 </laboratorios>
987 <laboratorios>
988 <codigolaboratorio>L02</codigolaboratorio>
989 <laboratorio>LAB NAME 2</laboratorio>
990 </laboratorios>
991 </aemps_prescripcion_laboratorios>"#
992 )
993 .unwrap();
994
995 let csv_file = NamedTempFile::new().unwrap();
996 let xml_path = xml_file.path();
997 let csv_path = csv_file.path();
998
999 let result = parse_laboratorio_xml_to_csv(xml_path, csv_path);
1000 assert!(result.is_ok());
1001
1002 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
1003
1004 let headers = csv_reader.headers().unwrap();
1006 assert_eq!(headers.get(0).unwrap(), "code");
1007 assert_eq!(headers.get(1).unwrap(), "name");
1008 assert_eq!(headers.get(2).unwrap(), "address");
1009 assert_eq!(headers.get(3).unwrap(), "zip");
1010 assert_eq!(headers.get(4).unwrap(), "city");
1011 assert_eq!(headers.get(5).unwrap(), "vat");
1012
1013 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
1015 assert_eq!(records.len(), 2);
1016 assert_eq!(records[0].get(0).unwrap(), "L01");
1017 assert_eq!(records[0].get(1).unwrap(), "LAB NAME");
1018 assert_eq!(records[0].get(2).unwrap(), "ADDR");
1019 assert_eq!(records[0].get(3).unwrap(), "ZIP");
1020 assert_eq!(records[0].get(4).unwrap(), "CITY");
1021 assert_eq!(records[0].get(5).unwrap(), "VAT");
1022
1023 assert_eq!(records[1].get(0).unwrap(), "L02");
1024 assert_eq!(records[1].get(1).unwrap(), "LAB NAME 2");
1025 assert_eq!(records[1].get(2).unwrap(), "");
1027 assert_eq!(records[1].get(3).unwrap(), "");
1028 assert_eq!(records[1].get(4).unwrap(), "");
1029 assert_eq!(records[1].get(5).unwrap(), "");
1030 }
1031
1032 #[test]
1033 fn test_parse_principio_activo_xml() {
1034 let mut xml_file = NamedTempFile::new().unwrap();
1035 writeln!(
1036 xml_file,
1037 r#"<aemps_prescripcion_principios_activos>
1038 <principiosactivos>
1039 <nroprincipioactivo>1</nroprincipioactivo>
1040 <codigoprincipioactivo>PA01</codigoprincipioactivo>
1041 <principioactivo>PRINCIPIO NAME</principioactivo>
1042 </principiosactivos>
1043 </aemps_prescripcion_principios_activos>"#
1044 )
1045 .unwrap();
1046
1047 let csv_file = NamedTempFile::new().unwrap();
1048 let xml_path = xml_file.path();
1049 let csv_path = csv_file.path();
1050
1051 let result = parse_principio_activo_xml_to_csv(xml_path, csv_path);
1052 assert!(result.is_ok());
1053
1054 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
1055
1056 let headers = csv_reader.headers().unwrap();
1058 assert_eq!(headers.get(0).unwrap(), "number");
1059 assert_eq!(headers.get(1).unwrap(), "code");
1060 assert_eq!(headers.get(2).unwrap(), "name");
1061
1062 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
1064 assert_eq!(records.len(), 1);
1065 assert_eq!(records[0].get(0).unwrap(), "1");
1066 assert_eq!(records[0].get(1).unwrap(), "PA01");
1067 assert_eq!(records[0].get(2).unwrap(), "PRINCIPIO NAME");
1068 }
1069
1070 #[test]
1071 fn test_parse_situacion_registro_xml() {
1072 let mut xml_file = NamedTempFile::new().unwrap();
1073 writeln!(
1074 xml_file,
1075 r#"<aemps_prescripcion_situacion_registro>
1076 <situacionesregistro>
1077 <codigosituacionregistro>1</codigosituacionregistro>
1078 <situacionregistro>Autorizado</situacionregistro>
1079 </situacionesregistro>
1080 </aemps_prescripcion_situacion_registro>"#
1081 )
1082 .unwrap();
1083
1084 let csv_file = NamedTempFile::new().unwrap();
1085 let xml_path = xml_file.path();
1086 let csv_path = csv_file.path();
1087
1088 let result = parse_situacion_registro_xml_to_csv(xml_path, csv_path);
1089 assert!(result.is_ok());
1090
1091 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
1092
1093 let headers = csv_reader.headers().unwrap();
1095 assert_eq!(headers.get(0).unwrap(), "code");
1096 assert_eq!(headers.get(1).unwrap(), "name");
1097
1098 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
1100 assert_eq!(records.len(), 1);
1101 assert_eq!(records[0].get(0).unwrap(), "1");
1102 assert_eq!(records[0].get(1).unwrap(), "Autorizado");
1103 }
1104
1105 #[test]
1106 fn test_parse_unidad_contenido_xml() {
1107 let mut xml_file = NamedTempFile::new().unwrap();
1108 writeln!(
1109 xml_file,
1110 r#"<aemps_prescripcion_unidad_contenido>
1111 <unidadescontenido>
1112 <codigounidadcontenido>1</codigounidadcontenido>
1113 <unidadcontenido>ampolla para inyección</unidadcontenido>
1114 </unidadescontenido>
1115 </aemps_prescripcion_unidad_contenido>"#
1116 )
1117 .unwrap();
1118
1119 let csv_file = NamedTempFile::new().unwrap();
1120 let xml_path = xml_file.path();
1121 let csv_path = csv_file.path();
1122
1123 let result = parse_unidad_contenido_xml_to_csv(xml_path, csv_path);
1124 assert!(result.is_ok());
1125
1126 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
1127
1128 let headers = csv_reader.headers().unwrap();
1130 assert_eq!(headers.get(0).unwrap(), "code");
1131 assert_eq!(headers.get(1).unwrap(), "name");
1132
1133 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
1135 assert_eq!(records.len(), 1);
1136 assert_eq!(records[0].get(0).unwrap(), "1");
1137 assert_eq!(records[0].get(1).unwrap(), "ampolla para inyección");
1138 }
1139
1140 #[test]
1141 fn test_parse_via_administracion_xml() {
1142 let mut xml_file = NamedTempFile::new().unwrap();
1143 writeln!(
1144 xml_file,
1145 r#"<aemps_prescripcion_vias_administracion>
1146 <viasadministracion>
1147 <codigoviaadministracion>7</codigoviaadministracion>
1148 <viaadministracion>HEMODIÁLISIS</viaadministracion>
1149 </viasadministracion>
1150 </aemps_prescripcion_vias_administracion>"#
1151 )
1152 .unwrap();
1153
1154 let csv_file = NamedTempFile::new().unwrap();
1155 let xml_path = xml_file.path();
1156 let csv_path = csv_file.path();
1157
1158 let result = parse_via_administracion_xml_to_csv(xml_path, csv_path);
1159 assert!(result.is_ok());
1160
1161 let mut csv_reader = csv::Reader::from_path(csv_path).unwrap();
1162
1163 let headers = csv_reader.headers().unwrap();
1165 assert_eq!(headers.get(0).unwrap(), "code");
1166 assert_eq!(headers.get(1).unwrap(), "name");
1167
1168 let records: Vec<csv::StringRecord> = csv_reader.records().map(|r| r.unwrap()).collect();
1170 assert_eq!(records.len(), 1);
1171 assert_eq!(records[0].get(0).unwrap(), "7");
1172 assert_eq!(records[0].get(1).unwrap(), "HEMODIÁLISIS");
1173 }
1174
1175 #[test]
1176 fn test_parse_prescription_with_nested() {
1177 let mut xml_file = NamedTempFile::new().unwrap();
1178 writeln!(
1179 xml_file,
1180 r#"<aemps_prescripcion>
1181 <prescription>
1182 <cod_nacion>600000</cod_nacion>
1183 <nro_definitivo>66337</nro_definitivo>
1184 <des_nomco>TEST</des_nomco>
1185 <des_prese>TEST</des_prese>
1186 <sw_psicotropo>0</sw_psicotropo>
1187 <sw_estupefaciente>0</sw_estupefaciente>
1188 <sw_afecta_conduccion>0</sw_afecta_conduccion>
1189 <sw_triangulo_negro>0</sw_triangulo_negro>
1190 <sw_receta>1</sw_receta>
1191 <sw_generico>1</sw_generico>
1192 <sw_sustituible>1</sw_sustituible>
1193 <sw_envase_clinico>1</sw_envase_clinico>
1194 <sw_uso_hospitalario>1</sw_uso_hospitalario>
1195 <sw_diagnostico_hospitalario>0</sw_diagnostico_hospitalario>
1196 <sw_tld>0</sw_tld>
1197 <sw_especial_control_medico>0</sw_especial_control_medico>
1198 <sw_huerfano>0</sw_huerfano>
1199 <sw_base_a_plantas>0</sw_base_a_plantas>
1200 <sw_comercializado>0</sw_comercializado>
1201 <sw_tiene_excipientes_decl_obligatoria>0</sw_tiene_excipientes_decl_obligatoria>
1202 <biosimilar>0</biosimilar>
1203 <importacion_paralela>0</importacion_paralela>
1204 <radiofarmaco>0</radiofarmaco>
1205 <serializacion>1</serializacion>
1206 <formasfarmaceuticas>
1207 <cod_forfar>288</cod_forfar>
1208 <cod_forfar_simplificada>34</cod_forfar_simplificada>
1209 <nro_pactiv>1</nro_pactiv>
1210 <composicion_pa>
1211 <cod_principio_activo>160</cod_principio_activo>
1212 </composicion_pa>
1213 <viasadministracion>
1214 <cod_via_admin>49</cod_via_admin>
1215 </viasadministracion>
1216 </formasfarmaceuticas>
1217 </prescription>
1218 </aemps_prescripcion>"#
1219 )
1220 .unwrap();
1221
1222 let file = File::open(xml_file.path()).unwrap();
1223 let reader = BufReader::new(file);
1224 let result: Result<PrescriptionList, _> = from_reader(reader);
1225
1226 match result {
1227 Ok(list) => {
1228 assert_eq!(list.records.len(), 1);
1229 let record = &list.records[0];
1230 assert_eq!(record.cod_nacion, "600000");
1231 assert!(record.forms.is_some());
1232 println!("Test passed! Nested structure deserialized successfully");
1233 }
1234 Err(e) => {
1235 panic!("Deserialization failed: {:?}", e);
1236 }
1237 }
1238 }
1239
1240 #[test]
1241 fn test_parse_prescription_to_multi_csv() {
1242 let mut xml_file = NamedTempFile::new().unwrap();
1243 writeln!(
1244 xml_file,
1245 r#"<aemps_prescripcion>
1246 <prescription>
1247 <cod_nacion>600000</cod_nacion>
1248 <nro_definitivo>66337</nro_definitivo>
1249 <des_nomco>TEST</des_nomco>
1250 <des_prese>TEST</des_prese>
1251 <sw_psicotropo>0</sw_psicotropo>
1252 <sw_estupefaciente>0</sw_estupefaciente>
1253 <sw_afecta_conduccion>0</sw_afecta_conduccion>
1254 <sw_triangulo_negro>0</sw_triangulo_negro>
1255 <sw_receta>1</sw_receta>
1256 <sw_generico>1</sw_generico>
1257 <sw_sustituible>1</sw_sustituible>
1258 <sw_envase_clinico>1</sw_envase_clinico>
1259 <sw_uso_hospitalario>1</sw_uso_hospitalario>
1260 <sw_diagnostico_hospitalario>0</sw_diagnostico_hospitalario>
1261 <sw_tld>0</sw_tld>
1262 <sw_especial_control_medico>0</sw_especial_control_medico>
1263 <sw_huerfano>0</sw_huerfano>
1264 <sw_base_a_plantas>0</sw_base_a_plantas>
1265 <sw_comercializado>0</sw_comercializado>
1266 <sw_tiene_excipientes_decl_obligatoria>0</sw_tiene_excipientes_decl_obligatoria>
1267 <biosimilar>0</biosimilar>
1268 <importacion_paralela>0</importacion_paralela>
1269 <radiofarmaco>0</radiofarmaco>
1270 <serializacion>1</serializacion>
1271 <formasfarmaceuticas>
1272 <cod_forfar>288</cod_forfar>
1273 <cod_forfar_simplificada>34</cod_forfar_simplificada>
1274 <nro_pactiv>1</nro_pactiv>
1275 <composicion_pa>
1276 <cod_principio_activo>160</cod_principio_activo>
1277 </composicion_pa>
1278 <viasadministracion>
1279 <cod_via_admin>49</cod_via_admin>
1280 </viasadministracion>
1281 </formasfarmaceuticas>
1282 <atc>
1283 <cod_atc>J01CR02</cod_atc>
1284 </atc>
1285 </prescription>
1286 </aemps_prescripcion>"#
1287 )
1288 .unwrap();
1289
1290 let output_dir = tempfile::tempdir().unwrap();
1291 let result = parse_prescription_xml_to_csvs(xml_file.path(), output_dir.path());
1292
1293 assert!(
1294 result.is_ok(),
1295 "Multi-CSV parsing failed: {:?}",
1296 result.err()
1297 );
1298
1299 assert!(output_dir.path().join("prescriptions.csv").exists());
1301 assert!(output_dir.path().join("prescription_forms.csv").exists());
1302 assert!(
1303 output_dir
1304 .path()
1305 .join("prescription_active_ingredients.csv")
1306 .exists()
1307 );
1308 assert!(
1309 output_dir
1310 .path()
1311 .join("prescription_admin_routes.csv")
1312 .exists()
1313 );
1314 assert!(output_dir.path().join("prescription_atc.csv").exists());
1315
1316 println!("Multi-CSV test passed! All 7 files created successfully");
1317 }
1318}