Skip to main content

cima_rs/
parser.rs

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
8// Helper module for deserializing "0"/"1" strings as booleans
9mod 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// ============================================================================
240// Prescription Nested Entity Structs
241// ============================================================================
242
243/// Active ingredient composition for a prescription
244#[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/// Administration route for a prescription
269#[derive(Debug, Serialize, Deserialize, Clone)]
270pub struct AdminRoute {
271    #[serde(rename(deserialize = "cod_via_admin"))]
272    pub route_code: String,
273}
274
275/// Pharmaceutical form for a prescription
276#[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/// ATC duplicate information
291#[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/// ATC code for a prescription
304#[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/// Supply problem for a prescription
313#[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// ============================================================================
322// Main Prescription Record
323// ============================================================================
324
325#[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    // Nested collections (not serialized to main CSV)
391    #[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    /// Parses the ATC XML file and writes its content to a CSV file.
452    parse_atc_xml_to_csv,
453    AtcList,
454    "Failed to deserialize ATC XML",
455    record,
456    {
457        // Clean description by removing "CODE - " prefix if it exists
458        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    /// Parses the DCP XML file and writes its content to a CSV file.
467    parse_dcp_xml_to_csv,
468    DcpList,
469    "Failed to deserialize DCP XML"
470);
471
472impl_xml_parser!(
473    /// Parses the DCPF XML file and writes its content to a CSV file.
474    parse_dcpf_xml_to_csv,
475    DcpfList,
476    "Failed to deserialize DCPF XML"
477);
478
479impl_xml_parser!(
480    /// Parses the DCSA XML file and writes its content to a CSV file.
481    parse_dcsa_xml_to_csv,
482    DcsaList,
483    "Failed to deserialize DCSA XML"
484);
485
486impl_xml_parser!(
487    /// Parses the Envases XML file and writes its content to a CSV file.
488    parse_envases_xml_to_csv,
489    ContainerList,
490    "Failed to deserialize Envases XML"
491);
492
493impl_xml_parser!(
494    /// Parses the Excipientes XML file and writes its content to a CSV file.
495    parse_excipientes_xml_to_csv,
496    ExcipientList,
497    "Failed to deserialize Excipientes XML"
498);
499
500impl_xml_parser!(
501    /// Parses the Forma Farmaceutica XML file and writes its content to a CSV file.
502    parse_forma_farmaceutica_xml_to_csv,
503    PharmaceuticalFormList,
504    "Failed to deserialize Forma Farmaceutica XML"
505);
506
507impl_xml_parser!(
508    /// Parses the Forma Farmaceutica Simplificada XML file and writes its content to a CSV file.
509    parse_forma_farmaceutica_simplificada_xml_to_csv,
510    SimplifiedPharmaceuticalFormList,
511    "Failed to deserialize Forma Farmaceutica Simplificada XML"
512);
513
514impl_xml_parser!(
515    /// Parses the Laboratorio XML file and writes its content to a CSV file.
516    parse_laboratorio_xml_to_csv,
517    LaboratoryList,
518    "Failed to deserialize Laboratorio XML"
519);
520
521impl_xml_parser!(
522    /// Parses the Principio Activo XML file and writes its content to a CSV file.
523    parse_principio_activo_xml_to_csv,
524    ActiveIngredientList,
525    "Failed to deserialize Principio Activo XML"
526);
527
528impl_xml_parser!(
529    /// Parses the Situacion Registro XML file and writes its content to a CSV file.
530    parse_situacion_registro_xml_to_csv,
531    RegistrationStatusList,
532    "Failed to deserialize Situacion Registro XML"
533);
534
535impl_xml_parser!(
536    /// Parses the Unidad Contenido XML file and writes its content to a CSV file.
537    parse_unidad_contenido_xml_to_csv,
538    ContainerUnitList,
539    "Failed to deserialize Unidad Contenido XML"
540);
541
542impl_xml_parser!(
543    /// Parses the Via Administracion XML file and writes its content to a CSV file.
544    parse_via_administracion_xml_to_csv,
545    AdministrationRouteList,
546    "Failed to deserialize Via Administracion XML"
547);
548
549impl_xml_parser!(
550    /// Parses the Prescription XML file and writes its content to a CSV file.
551    parse_prescription_xml_to_csv,
552    PrescriptionList,
553    "Failed to deserialize Prescription XML"
554);
555
556/// Parses the Prescription XML file and writes content to multiple CSV files for normalized data.
557///
558/// This function extracts nested entities (forms, active ingredients, admin routes, ATC codes, supply problems)
559/// into separate CSV files with proper relationships via prescription_id.
560///
561/// # Output Files
562/// - `prescriptions.csv` - Main prescription records
563/// - `prescription_forms.csv` - Pharmaceutical forms (1:1 with prescriptions)
564/// - `prescription_active_ingredients.csv` - Active ingredients (1:N)
565/// - `prescription_admin_routes.csv` - Administration routes (1:N)
566/// - `prescription_atc.csv` - ATC codes (1:N)
567/// - `prescription_atc_duplicates.csv` - ATC duplicates (nested 1:N)
568/// - `prescription_supply_problems.csv` - Supply problems (1:N)
569pub 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    // Create CSV writers for each output file
576    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    // Process each prescription record
592    for record in list.records {
593        // Use cod_nacion as prescription ID (matches DB primary key)
594        let prescription_id = record.cod_nacion.clone();
595
596        // Write main prescription record (nested collections are skipped via serde)
597        wtr_main.serialize(&record)?;
598
599        // Write pharmaceutical form and its nested entities
600        if let Some(form) = &record.forms {
601            // Write form record
602            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            // Write active ingredients
610            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            // Write administration routes
627            for route in &form.admin_routes {
628                wtr_routes.write_record([&prescription_id, &route.route_code])?;
629            }
630        }
631
632        // Write ATC codes and their duplicates
633        for atc in &record.atc_codes {
634            wtr_atc.write_record([&prescription_id, &atc.atc_code])?;
635
636            // Write ATC duplicates
637            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        // Write supply problems
650        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    // Flush all writers
660    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        // Verify CSV headers use Rust field names
707        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        // Verify CSV data
713        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        // Verify CSV headers use Rust field names
746        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        // Verify CSV data
752        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        // Verify CSV headers use Rust field names
783        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        // Verify CSV data
789        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        // Verify CSV headers use Rust field names
819        let headers = csv_reader.headers().unwrap();
820        assert_eq!(headers.get(0).unwrap(), "code");
821        assert_eq!(headers.get(1).unwrap(), "name");
822
823        // Verify CSV data
824        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        // Verify CSV headers use Rust field names
854        let headers = csv_reader.headers().unwrap();
855        assert_eq!(headers.get(0).unwrap(), "code");
856        assert_eq!(headers.get(1).unwrap(), "name");
857
858        // Verify CSV data
859        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        // Verify CSV headers use Rust field names
889        let headers = csv_reader.headers().unwrap();
890        assert_eq!(headers.get(0).unwrap(), "code");
891        assert_eq!(headers.get(1).unwrap(), "name");
892
893        // Verify CSV data
894        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        // Verify CSV headers use Rust field names
925        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        // Verify CSV data
931        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        // Verify CSV headers use Rust field names
962        let headers = csv_reader.headers().unwrap();
963        assert_eq!(headers.get(0).unwrap(), "code");
964        assert_eq!(headers.get(1).unwrap(), "name");
965
966        // Verify CSV data
967        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        // Verify CSV headers use Rust field names
1005        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        // Verify CSV data
1014        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        // Second record has empty strings for optional fields
1026        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        // Verify CSV headers use Rust field names
1057        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        // Verify CSV data
1063        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        // Verify CSV headers use Rust field names
1094        let headers = csv_reader.headers().unwrap();
1095        assert_eq!(headers.get(0).unwrap(), "code");
1096        assert_eq!(headers.get(1).unwrap(), "name");
1097
1098        // Verify CSV data
1099        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        // Verify CSV headers use Rust field names
1129        let headers = csv_reader.headers().unwrap();
1130        assert_eq!(headers.get(0).unwrap(), "code");
1131        assert_eq!(headers.get(1).unwrap(), "name");
1132
1133        // Verify CSV data
1134        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        // Verify CSV headers use Rust field names
1164        let headers = csv_reader.headers().unwrap();
1165        assert_eq!(headers.get(0).unwrap(), "code");
1166        assert_eq!(headers.get(1).unwrap(), "name");
1167
1168        // Verify CSV data
1169        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        // Verify all 7 CSV files were created
1300        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}