Skip to main content

cima_rs/endpoints/
medications.rs

1use crate::api_client::CimaClient;
2use crate::models::{Medication, MedicationSummary};
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5
6/// Medication search parameters
7#[derive(Debug, Default, Clone)]
8pub struct SearchMedicationsParams {
9    /// Medication name
10    pub name: Option<String>,
11    /// Laboratory name
12    pub laboratory: Option<String>,
13    /// Active ingredient 1 name
14    pub active_ingredient_1: Option<String>,
15    /// Active ingredient 2 name
16    pub active_ingredient_2: Option<String>,
17    /// Active ingredient 1 ID
18    pub active_ingredient_1_id: Option<i32>,
19    /// Active ingredient 2 ID
20    pub active_ingredient_2_id: Option<i32>,
21    /// National code
22    pub national_code: Option<String>,
23    /// ATC code or description
24    pub atc: Option<String>,
25    /// Registration number
26    pub registration_number: Option<String>,
27    /// Number of active ingredients
28    pub active_ingredient_count: Option<i32>,
29    /// 1: has black triangle, 0: no black triangle
30    pub black_triangle: Option<u8>,
31    /// 1: orphan drug, 0: not orphan
32    pub orphan: Option<u8>,
33    /// 1: biosimilar, 0: not biosimilar
34    pub biosimilar: Option<u8>,
35    /// Substitutable type (1-5)
36    pub substitutable_type: Option<u8>,
37    /// VMP code ID
38    pub vmp: Option<String>,
39    /// 1: commercialized, 0: not commercialized
40    pub commercialized: Option<u8>,
41    /// 1: only authorized, 0: only not authorized
42    pub authorized: Option<u8>,
43    /// 1: requires prescription, 0: no prescription
44    pub prescription: Option<u8>,
45    /// 1: narcotics
46    pub narcotic: Option<u8>,
47    /// 1: psychotropics
48    pub psychotropic: Option<u8>,
49    /// 1: narcotics or psychotropics
50    pub narcotic_or_psychotropic: Option<u8>,
51    /// Page number
52    pub page: Option<u32>,
53}
54
55impl SearchMedicationsParams {
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Build query parameters as vector of tuples
61    pub(crate) fn to_query_params(&self) -> Vec<(&str, String)> {
62        let mut params = Vec::new();
63
64        if let Some(ref v) = self.name {
65            params.push(("nombre", v.clone()));
66        }
67        if let Some(ref v) = self.laboratory {
68            params.push(("laboratorio", v.clone()));
69        }
70        if let Some(ref v) = self.active_ingredient_1 {
71            params.push(("practiv1", v.clone()));
72        }
73        if let Some(ref v) = self.active_ingredient_2 {
74            params.push(("practiv2", v.clone()));
75        }
76        if let Some(v) = self.active_ingredient_1_id {
77            params.push(("idpractiv1", v.to_string()));
78        }
79        if let Some(v) = self.active_ingredient_2_id {
80            params.push(("idpractiv2", v.to_string()));
81        }
82        if let Some(ref v) = self.national_code {
83            params.push(("cn", v.clone()));
84        }
85        if let Some(ref v) = self.atc {
86            params.push(("atc", v.clone()));
87        }
88        if let Some(ref v) = self.registration_number {
89            params.push(("nregistro", v.clone()));
90        }
91        if let Some(v) = self.active_ingredient_count {
92            params.push(("npactiv", v.to_string()));
93        }
94        if let Some(v) = self.black_triangle {
95            params.push(("triangulo", v.to_string()));
96        }
97        if let Some(v) = self.orphan {
98            params.push(("huerfano", v.to_string()));
99        }
100        if let Some(v) = self.biosimilar {
101            params.push(("biosimilar", v.to_string()));
102        }
103        if let Some(v) = self.substitutable_type {
104            params.push(("sust", v.to_string()));
105        }
106        if let Some(ref v) = self.vmp {
107            params.push(("vmp", v.clone()));
108        }
109        if let Some(v) = self.commercialized {
110            params.push(("comerc", v.to_string()));
111        }
112        if let Some(v) = self.authorized {
113            params.push(("autorizados", v.to_string()));
114        }
115        if let Some(v) = self.prescription {
116            params.push(("receta", v.to_string()));
117        }
118        if let Some(v) = self.narcotic {
119            params.push(("estupefaciente", v.to_string()));
120        }
121        if let Some(v) = self.psychotropic {
122            params.push(("psicotropo", v.to_string()));
123        }
124        if let Some(v) = self.narcotic_or_psychotropic {
125            params.push(("estuopsico", v.to_string()));
126        }
127        if let Some(v) = self.page {
128            params.push(("pagina", v.to_string()));
129        }
130
131        params
132    }
133}
134
135/// Query for searching in technical data sheet
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct TechnicalSheetQuery {
138    /// Section to search (1-10, can have sublevels "4.1")
139    #[serde(rename = "seccion")]
140    pub section: String,
141    /// Text to search for
142    #[serde(rename = "texto")]
143    pub text: String,
144    /// 1: must contain text, 0: must not contain
145    #[serde(rename = "contiene")]
146    pub contains: u8,
147}
148
149impl CimaClient {
150    /// Get medication information by registration number or national code
151    pub async fn get_medication(
152        &self,
153        registration_number: Option<&str>,
154        national_code: Option<&str>,
155    ) -> Result<Medication> {
156        let mut params = Vec::new();
157
158        if let Some(nr) = registration_number {
159            params.push(("nregistro", nr.to_string()));
160        }
161        if let Some(cn) = national_code {
162            params.push(("cn", cn.to_string()));
163        }
164
165        if params.is_empty() {
166            anyhow::bail!("Must provide either registration_number or national_code");
167        }
168
169        self.get_with_params("medicamento", &params)
170            .await
171            .context("Failed to get medication")
172    }
173
174    /// Search medications according to specified parameters
175    ///
176    /// Returns a paginated response with medication search results.
177    pub async fn search_medications(
178        &self,
179        params: &SearchMedicationsParams,
180    ) -> Result<crate::models::PaginatedResponse<MedicationSummary>> {
181        let query_params = params.to_query_params();
182
183        self.get_with_params("medicamentos", &query_params)
184            .await
185            .context("Failed to search medications")
186    }
187
188    /// Search medications by content in technical data sheet
189    pub async fn search_in_technical_sheet(
190        &self,
191        queries: &[TechnicalSheetQuery],
192    ) -> Result<Vec<MedicationSummary>> {
193        self.post("buscarEnFichaTecnica", queries)
194            .await
195            .context("Failed to search in technical sheet")
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_search_params_to_query() {
205        let params = SearchMedicationsParams {
206            name: Some("Paracetamol".to_string()),
207            black_triangle: Some(1),
208            page: Some(2),
209            ..Default::default()
210        };
211
212        let query = params.to_query_params();
213        assert_eq!(query.len(), 3);
214        assert!(
215            query
216                .iter()
217                .any(|(k, v)| k == &"nombre" && v == "Paracetamol")
218        );
219        assert!(query.iter().any(|(k, v)| k == &"triangulo" && v == "1"));
220        assert!(query.iter().any(|(k, v)| k == &"pagina" && v == "2"));
221    }
222
223    #[test]
224    fn test_technical_sheet_query_serialization() {
225        let query = TechnicalSheetQuery {
226            section: "4.1".to_string(),
227            text: "cáncer".to_string(),
228            contains: 1,
229        };
230
231        let json = serde_json::to_string(&query).unwrap();
232        assert!(json.contains("4.1"));
233        assert!(json.contains("cáncer"));
234    }
235}