1use serde::Deserialize;
4
5use crate::client::Client;
6use crate::error::Result;
7use crate::response::de_opt_from_str;
8
9#[derive(Debug, Clone, Copy)]
12pub struct Users<'a> {
13 client: &'a Client,
14}
15
16impl<'a> Users<'a> {
17 pub(crate) fn new(client: &'a Client) -> Self {
18 Self { client }
19 }
20
21 pub async fn get_balances(&self) -> Result<Balances> {
38 let payload: BalancesPayload = self
39 .client
40 .send("namecheap.users.getBalances", Vec::new())
41 .await?;
42 Ok(payload.result)
43 }
44
45 pub async fn get_pricing(&self, request: &PricingRequest) -> Result<PricingResult> {
76 let payload: GetPricingPayload = self
77 .client
78 .send("namecheap.users.getPricing", request.to_params())
79 .await?;
80 Ok(PricingResult {
81 product_types: payload.result.product_types,
82 })
83 }
84}
85
86#[derive(Debug, Deserialize)]
87struct BalancesPayload {
88 #[serde(rename = "UserGetBalancesResult")]
89 result: Balances,
90}
91
92#[derive(Debug, Clone, Deserialize)]
98#[non_exhaustive]
99pub struct Balances {
100 #[serde(rename = "@Currency")]
102 pub currency: String,
103 #[serde(rename = "@AvailableBalance")]
105 pub available_balance: f64,
106 #[serde(rename = "@AccountBalance")]
108 pub account_balance: f64,
109 #[serde(rename = "@EarnedAmount")]
111 pub earned_amount: f64,
112 #[serde(rename = "@WithdrawableAmount")]
114 pub withdrawable_amount: f64,
115 #[serde(rename = "@FundsRequiredForAutoRenew")]
117 pub funds_required_for_auto_renew: f64,
118}
119
120#[derive(Debug, Clone)]
125pub struct PricingRequest {
126 pub product_type: String,
128 pub product_category: Option<String>,
130 pub action_name: Option<String>,
133 pub product_name: Option<String>,
136 pub promotion_code: Option<String>,
138}
139
140impl PricingRequest {
141 #[must_use]
143 pub fn domains() -> Self {
144 Self {
145 product_type: "DOMAIN".to_owned(),
146 product_category: Some("DOMAINS".to_owned()),
147 action_name: None,
148 product_name: None,
149 promotion_code: None,
150 }
151 }
152
153 pub fn new(product_type: impl Into<String>) -> Self {
155 Self {
156 product_type: product_type.into(),
157 product_category: None,
158 action_name: None,
159 product_name: None,
160 promotion_code: None,
161 }
162 }
163
164 #[must_use]
166 pub fn action(mut self, action: impl Into<String>) -> Self {
167 self.action_name = Some(action.into());
168 self
169 }
170
171 #[must_use]
173 pub fn tld(mut self, tld: impl Into<String>) -> Self {
174 self.product_name = Some(tld.into());
175 self
176 }
177
178 #[must_use]
180 pub fn category(mut self, category: impl Into<String>) -> Self {
181 self.product_category = Some(category.into());
182 self
183 }
184
185 #[must_use]
187 pub fn promotion_code(mut self, code: impl Into<String>) -> Self {
188 self.promotion_code = Some(code.into());
189 self
190 }
191
192 fn to_params(&self) -> Vec<(String, String)> {
193 let mut params = vec![("ProductType".to_owned(), self.product_type.clone())];
194 if let Some(category) = &self.product_category {
195 params.push(("ProductCategory".to_owned(), category.clone()));
196 }
197 if let Some(action) = &self.action_name {
198 params.push(("ActionName".to_owned(), action.clone()));
199 }
200 if let Some(product) = &self.product_name {
201 params.push(("ProductName".to_owned(), product.clone()));
202 }
203 if let Some(code) = &self.promotion_code {
204 params.push(("PromotionCode".to_owned(), code.clone()));
205 }
206 params
207 }
208}
209
210#[derive(Debug, Deserialize)]
211struct GetPricingPayload {
212 #[serde(rename = "UserGetPricingResult", default)]
213 result: UserGetPricingResult,
214}
215
216#[derive(Debug, Default, Deserialize)]
217struct UserGetPricingResult {
218 #[serde(rename = "ProductType", default)]
219 product_types: Vec<ProductTypePricing>,
220}
221
222#[derive(Debug, Clone)]
224#[non_exhaustive]
225pub struct PricingResult {
226 pub product_types: Vec<ProductTypePricing>,
229}
230
231impl PricingResult {
232 #[must_use]
236 pub fn prices(&self) -> Vec<&Price> {
237 self.product_types
238 .iter()
239 .flat_map(|product_type| &product_type.categories)
240 .flat_map(|category| &category.products)
241 .flat_map(|product| &product.prices)
242 .collect()
243 }
244}
245
246#[derive(Debug, Clone, Deserialize)]
248#[non_exhaustive]
249pub struct ProductTypePricing {
250 #[serde(rename = "@Name")]
252 pub name: String,
253 #[serde(rename = "ProductCategory", default)]
255 pub categories: Vec<ProductCategoryPricing>,
256}
257
258#[derive(Debug, Clone, Deserialize)]
260#[non_exhaustive]
261pub struct ProductCategoryPricing {
262 #[serde(rename = "@Name")]
264 pub name: String,
265 #[serde(rename = "Product", default)]
267 pub products: Vec<ProductPricing>,
268}
269
270#[derive(Debug, Clone, Deserialize)]
272#[non_exhaustive]
273pub struct ProductPricing {
274 #[serde(rename = "@Name")]
276 pub name: String,
277 #[serde(rename = "Price", default)]
279 pub prices: Vec<Price>,
280}
281
282#[derive(Debug, Clone, Deserialize)]
284#[non_exhaustive]
285pub struct Price {
286 #[serde(rename = "@Duration")]
288 pub duration: u32,
289 #[serde(rename = "@DurationType")]
291 pub duration_type: String,
292 #[serde(rename = "@Price")]
294 pub price: f64,
295 #[serde(rename = "@RegularPrice")]
297 pub regular_price: f64,
298 #[serde(rename = "@YourPrice")]
300 pub your_price: f64,
301 #[serde(
303 rename = "@PromotionPrice",
304 default,
305 deserialize_with = "de_opt_from_str"
306 )]
307 pub promotion_price: Option<f64>,
308 #[serde(
310 rename = "@AdditionalCost",
311 default,
312 deserialize_with = "de_opt_from_str"
313 )]
314 pub additional_cost: Option<f64>,
315 #[serde(rename = "@Currency")]
317 pub currency: String,
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use reqwest::StatusCode;
324
325 #[test]
326 fn parses_balances_response() {
327 let body = r#"<?xml version="1.0" encoding="utf-8"?>
328 <ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
329 <Errors />
330 <CommandResponse Type="namecheap.users.getBalances">
331 <UserGetBalancesResult Currency="USD" AvailableBalance="4932.96" AccountBalance="4932.96" EarnedAmount="381.30" WithdrawableAmount="1500.00" FundsRequiredForAutoRenew="0.00" />
332 </CommandResponse>
333 </ApiResponse>"#;
334
335 let payload: BalancesPayload = crate::response::parse(StatusCode::OK, body).unwrap();
336 let balances = payload.result;
337 assert_eq!(balances.currency, "USD");
338 assert_eq!(balances.available_balance, 4932.96);
339 assert_eq!(balances.earned_amount, 381.30);
340 assert_eq!(balances.funds_required_for_auto_renew, 0.0);
341 }
342
343 #[test]
344 fn parses_get_pricing_response() {
345 let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
346 <Errors />
347 <CommandResponse Type="namecheap.users.getPricing">
348 <UserGetPricingResult>
349 <ProductType Name="domains">
350 <ProductCategory Name="register">
351 <Product Name="com">
352 <Price Duration="1" DurationType="YEAR" Price="13.98" RegularPrice="13.98" YourPrice="13.98" AdditionalCost="0.20" PromotionPrice="0.0" Currency="USD" />
353 <Price Duration="2" DurationType="YEAR" Price="26.26" RegularPrice="13.98" YourPrice="26.26" AdditionalCost="0.20" PromotionPrice="0.0" Currency="USD" />
354 </Product>
355 </ProductCategory>
356 </ProductType>
357 </UserGetPricingResult>
358 </CommandResponse>
359 </ApiResponse>"#;
360
361 let payload: GetPricingPayload = crate::response::parse(StatusCode::OK, body).unwrap();
362 let result = PricingResult {
363 product_types: payload.result.product_types,
364 };
365 assert_eq!(result.product_types.len(), 1);
366 assert_eq!(result.product_types[0].name, "domains");
367 assert_eq!(result.product_types[0].categories[0].name, "register");
368 assert_eq!(
369 result.product_types[0].categories[0].products[0].name,
370 "com"
371 );
372
373 let prices = result.prices();
374 assert_eq!(prices.len(), 2);
375 assert_eq!(prices[0].duration, 1);
376 assert_eq!(prices[0].price, 13.98);
377 assert_eq!(prices[0].regular_price, 13.98);
378 assert_eq!(prices[0].additional_cost, Some(0.20));
379 assert_eq!(prices[0].currency, "USD");
380 assert_eq!(prices[1].duration, 2);
381 assert_eq!(prices[1].price, 26.26);
382 }
383
384 #[test]
385 fn pricing_request_builds_params() {
386 let params = PricingRequest::domains()
387 .action("REGISTER")
388 .tld("com")
389 .to_params();
390 let get = |key: &str| {
391 params
392 .iter()
393 .find(|(k, _)| k == key)
394 .map(|(_, v)| v.as_str())
395 };
396 assert_eq!(get("ProductType"), Some("DOMAIN"));
397 assert_eq!(get("ProductCategory"), Some("DOMAINS"));
398 assert_eq!(get("ActionName"), Some("REGISTER"));
399 assert_eq!(get("ProductName"), Some("com"));
400 }
401}