docaroo_rs/
procedures.rs

1//! Procedures API operations for likelihood scoring
2
3use crate::{
4    client::DocarooClient,
5    error::Result,
6    models::{LikelihoodRequest, LikelihoodResponse},
7};
8
9/// Client for procedure likelihood operations
10#[derive(Debug, Clone)]
11pub struct ProceduresClient {
12    client: DocarooClient,
13}
14
15impl ProceduresClient {
16    /// Create a new procedures client
17    pub(crate) fn new(client: DocarooClient) -> Self {
18        Self { client }
19    }
20
21    /// Get procedure likelihood scores for healthcare providers
22    ///
23    /// Evaluate the likelihood that healthcare providers (NPIs) perform specific medical
24    /// procedures or services. The API analyzes historical claims data and provider
25    /// specialties to generate confidence scores from 0.0 (unlikely) to 1.0 (highly likely).
26    ///
27    /// # Arguments
28    ///
29    /// * `request` - The likelihood request containing NPIs, billing code, and code type
30    ///
31    /// # Returns
32    ///
33    /// A `LikelihoodResponse` containing likelihood scores organized by NPI and metadata
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if:
38    /// - The request contains invalid parameters
39    /// - Authentication fails (invalid API key)
40    /// - Rate limits are exceeded
41    /// - The API returns an error response
42    ///
43    /// # Example
44    ///
45    /// ```no_run
46    /// use docaroo_rs::{DocarooClient, models::LikelihoodRequest};
47    ///
48    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
49    /// let client = DocarooClient::new("your-api-key");
50    /// 
51    /// let request = LikelihoodRequest::builder()
52    ///     .npis(vec!["1487648176".to_string()])
53    ///     .condition_code("99214")
54    ///     .code_type("CPT")
55    ///     .build();
56    ///
57    /// let response = client.procedures().get_likelihood(request).await?;
58    ///
59    /// // Process the response
60    /// for (npi, data) in response.data {
61    ///     println!("NPI {}: Likelihood score = {:.2}", npi, data.likelihood);
62    /// }
63    /// # Ok(())
64    /// # }
65    /// ```
66    pub async fn get_likelihood(&self, request: LikelihoodRequest) -> Result<LikelihoodResponse> {
67        // Validate request
68        self.validate_likelihood_request(&request)?;
69
70        // Build URL
71        let url = self.client.build_url("/procedures/likelihood")?;
72
73        // Send request
74        let response = self
75            .client
76            .http_client()
77            .post(url)
78            .json(&request)
79            .send()
80            .await?;
81
82        // Handle response
83        DocarooClient::handle_response(response).await
84    }
85
86    /// Validate a likelihood request before sending
87    fn validate_likelihood_request(&self, request: &LikelihoodRequest) -> Result<()> {
88        use crate::error::DocarooError;
89
90        // Validate NPIs
91        if request.npis.is_empty() {
92            return Err(DocarooError::InvalidRequest(
93                "At least one NPI must be provided".to_string(),
94            ));
95        }
96
97        // Validate NPI format (10 digits)
98        for npi in &request.npis {
99            if npi.len() != 10 || !npi.chars().all(|c| c.is_ascii_digit()) {
100                return Err(DocarooError::InvalidRequest(format!(
101                    "Invalid NPI format: '{}'. NPIs must be 10-digit numbers",
102                    npi
103                )));
104            }
105        }
106
107        // Validate condition code is not empty
108        if request.condition_code.trim().is_empty() {
109            return Err(DocarooError::InvalidRequest(
110                "Condition code cannot be empty".to_string(),
111            ));
112        }
113
114        // Validate code type is not empty
115        if request.code_type.trim().is_empty() {
116            return Err(DocarooError::InvalidRequest(
117                "Code type cannot be empty".to_string(),
118            ));
119        }
120
121        Ok(())
122    }
123
124    /// Check multiple providers for a procedure at once
125    ///
126    /// This is a convenience method that allows checking multiple providers
127    /// for the same procedure in a single request.
128    ///
129    /// # Arguments
130    ///
131    /// * `npis` - List of National Provider Identifiers
132    /// * `condition_code` - Medical billing code
133    /// * `code_type` - Medical billing code standard (e.g., "CPT")
134    ///
135    /// # Example
136    ///
137    /// ```no_run
138    /// # use docaroo_rs::DocarooClient;
139    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
140    /// let client = DocarooClient::new("your-api-key");
141    /// 
142    /// let npis = vec!["1487648176", "1234567890"];
143    /// let response = client.procedures()
144    ///     .check_providers(&npis, "99214", "CPT")
145    ///     .await?;
146    /// # Ok(())
147    /// # }
148    /// ```
149    pub async fn check_providers(
150        &self,
151        npis: &[&str],
152        condition_code: impl Into<String>,
153        code_type: impl Into<String>,
154    ) -> Result<LikelihoodResponse> {
155        let request = LikelihoodRequest::builder()
156            .npis(npis.iter().map(|&s| s.to_string()).collect::<Vec<_>>())
157            .condition_code(condition_code)
158            .code_type(code_type)
159            .build();
160
161        self.get_likelihood(request).await
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_validate_likelihood_request_valid() {
171        let client = DocarooClient::new("test-key");
172        let procedures_client = ProceduresClient::new(client);
173
174        let request = LikelihoodRequest::builder()
175            .npis(vec![String::from("1234567890")])
176            .condition_code("99214")
177            .code_type("CPT")
178            .build();
179
180        assert!(procedures_client.validate_likelihood_request(&request).is_ok());
181    }
182
183    #[test]
184    fn test_validate_likelihood_request_empty_npis() {
185        let client = DocarooClient::new("test-key");
186        let procedures_client = ProceduresClient::new(client);
187
188        let request = LikelihoodRequest {
189            npis: vec![],
190            condition_code: "99214".to_string(),
191            code_type: "CPT".to_string(),
192        };
193
194        let result = procedures_client.validate_likelihood_request(&request);
195        assert!(result.is_err());
196        assert!(result
197            .unwrap_err()
198            .to_string()
199            .contains("At least one NPI must be provided"));
200    }
201
202    #[test]
203    fn test_validate_likelihood_request_invalid_npi() {
204        let client = DocarooClient::new("test-key");
205        let procedures_client = ProceduresClient::new(client);
206
207        let request = LikelihoodRequest::builder()
208            .npis(vec![String::from("ABC1234567")]) // Contains letters
209            .condition_code("99214")
210            .code_type("CPT")
211            .build();
212
213        let result = procedures_client.validate_likelihood_request(&request);
214        assert!(result.is_err());
215        assert!(result.unwrap_err().to_string().contains("Invalid NPI format"));
216    }
217
218    #[test]
219    fn test_validate_likelihood_request_empty_code_type() {
220        let client = DocarooClient::new("test-key");
221        let procedures_client = ProceduresClient::new(client);
222
223        let request = LikelihoodRequest {
224            npis: vec!["1234567890".to_string()],
225            condition_code: "99214".to_string(),
226            code_type: "".to_string(),
227        };
228
229        let result = procedures_client.validate_likelihood_request(&request);
230        assert!(result.is_err());
231        assert!(result
232            .unwrap_err()
233            .to_string()
234            .contains("Code type cannot be empty"));
235    }
236}