use serde::Deserialize;
use crate::client::Client;
use crate::error::Result;
use crate::response::de_opt_from_str;
#[derive(Debug, Clone, Copy)]
pub struct Users<'a> {
client: &'a Client,
}
impl<'a> Users<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn get_balances(&self) -> Result<Balances> {
let payload: BalancesPayload = self
.client
.send("namecheap.users.getBalances", Vec::new())
.await?;
Ok(payload.result)
}
pub async fn get_pricing(&self, request: &PricingRequest) -> Result<PricingResult> {
let payload: GetPricingPayload = self
.client
.send("namecheap.users.getPricing", request.to_params())
.await?;
Ok(PricingResult {
product_types: payload.result.product_types,
})
}
}
#[derive(Debug, Deserialize)]
struct BalancesPayload {
#[serde(rename = "UserGetBalancesResult")]
result: Balances,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct Balances {
#[serde(rename = "@Currency")]
pub currency: String,
#[serde(rename = "@AvailableBalance")]
pub available_balance: f64,
#[serde(rename = "@AccountBalance")]
pub account_balance: f64,
#[serde(rename = "@EarnedAmount")]
pub earned_amount: f64,
#[serde(rename = "@WithdrawableAmount")]
pub withdrawable_amount: f64,
#[serde(rename = "@FundsRequiredForAutoRenew")]
pub funds_required_for_auto_renew: f64,
}
#[derive(Debug, Clone)]
pub struct PricingRequest {
pub product_type: String,
pub product_category: Option<String>,
pub action_name: Option<String>,
pub product_name: Option<String>,
pub promotion_code: Option<String>,
}
impl PricingRequest {
#[must_use]
pub fn domains() -> Self {
Self {
product_type: "DOMAIN".to_owned(),
product_category: Some("DOMAINS".to_owned()),
action_name: None,
product_name: None,
promotion_code: None,
}
}
pub fn new(product_type: impl Into<String>) -> Self {
Self {
product_type: product_type.into(),
product_category: None,
action_name: None,
product_name: None,
promotion_code: None,
}
}
#[must_use]
pub fn action(mut self, action: impl Into<String>) -> Self {
self.action_name = Some(action.into());
self
}
#[must_use]
pub fn tld(mut self, tld: impl Into<String>) -> Self {
self.product_name = Some(tld.into());
self
}
#[must_use]
pub fn category(mut self, category: impl Into<String>) -> Self {
self.product_category = Some(category.into());
self
}
#[must_use]
pub fn promotion_code(mut self, code: impl Into<String>) -> Self {
self.promotion_code = Some(code.into());
self
}
fn to_params(&self) -> Vec<(String, String)> {
let mut params = vec![("ProductType".to_owned(), self.product_type.clone())];
if let Some(category) = &self.product_category {
params.push(("ProductCategory".to_owned(), category.clone()));
}
if let Some(action) = &self.action_name {
params.push(("ActionName".to_owned(), action.clone()));
}
if let Some(product) = &self.product_name {
params.push(("ProductName".to_owned(), product.clone()));
}
if let Some(code) = &self.promotion_code {
params.push(("PromotionCode".to_owned(), code.clone()));
}
params
}
}
#[derive(Debug, Deserialize)]
struct GetPricingPayload {
#[serde(rename = "UserGetPricingResult", default)]
result: UserGetPricingResult,
}
#[derive(Debug, Default, Deserialize)]
struct UserGetPricingResult {
#[serde(rename = "ProductType", default)]
product_types: Vec<ProductTypePricing>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PricingResult {
pub product_types: Vec<ProductTypePricing>,
}
impl PricingResult {
#[must_use]
pub fn prices(&self) -> Vec<&Price> {
self.product_types
.iter()
.flat_map(|product_type| &product_type.categories)
.flat_map(|category| &category.products)
.flat_map(|product| &product.prices)
.collect()
}
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ProductTypePricing {
#[serde(rename = "@Name")]
pub name: String,
#[serde(rename = "ProductCategory", default)]
pub categories: Vec<ProductCategoryPricing>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ProductCategoryPricing {
#[serde(rename = "@Name")]
pub name: String,
#[serde(rename = "Product", default)]
pub products: Vec<ProductPricing>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ProductPricing {
#[serde(rename = "@Name")]
pub name: String,
#[serde(rename = "Price", default)]
pub prices: Vec<Price>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct Price {
#[serde(rename = "@Duration")]
pub duration: u32,
#[serde(rename = "@DurationType")]
pub duration_type: String,
#[serde(rename = "@Price")]
pub price: f64,
#[serde(rename = "@RegularPrice")]
pub regular_price: f64,
#[serde(rename = "@YourPrice")]
pub your_price: f64,
#[serde(
rename = "@PromotionPrice",
default,
deserialize_with = "de_opt_from_str"
)]
pub promotion_price: Option<f64>,
#[serde(
rename = "@AdditionalCost",
default,
deserialize_with = "de_opt_from_str"
)]
pub additional_cost: Option<f64>,
#[serde(rename = "@Currency")]
pub currency: String,
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::StatusCode;
#[test]
fn parses_balances_response() {
let body = r#"<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<CommandResponse Type="namecheap.users.getBalances">
<UserGetBalancesResult Currency="USD" AvailableBalance="4932.96" AccountBalance="4932.96" EarnedAmount="381.30" WithdrawableAmount="1500.00" FundsRequiredForAutoRenew="0.00" />
</CommandResponse>
</ApiResponse>"#;
let payload: BalancesPayload = crate::response::parse(StatusCode::OK, body).unwrap();
let balances = payload.result;
assert_eq!(balances.currency, "USD");
assert_eq!(balances.available_balance, 4932.96);
assert_eq!(balances.earned_amount, 381.30);
assert_eq!(balances.funds_required_for_auto_renew, 0.0);
}
#[test]
fn parses_get_pricing_response() {
let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<CommandResponse Type="namecheap.users.getPricing">
<UserGetPricingResult>
<ProductType Name="domains">
<ProductCategory Name="register">
<Product Name="com">
<Price Duration="1" DurationType="YEAR" Price="13.98" RegularPrice="13.98" YourPrice="13.98" AdditionalCost="0.20" PromotionPrice="0.0" Currency="USD" />
<Price Duration="2" DurationType="YEAR" Price="26.26" RegularPrice="13.98" YourPrice="26.26" AdditionalCost="0.20" PromotionPrice="0.0" Currency="USD" />
</Product>
</ProductCategory>
</ProductType>
</UserGetPricingResult>
</CommandResponse>
</ApiResponse>"#;
let payload: GetPricingPayload = crate::response::parse(StatusCode::OK, body).unwrap();
let result = PricingResult {
product_types: payload.result.product_types,
};
assert_eq!(result.product_types.len(), 1);
assert_eq!(result.product_types[0].name, "domains");
assert_eq!(result.product_types[0].categories[0].name, "register");
assert_eq!(
result.product_types[0].categories[0].products[0].name,
"com"
);
let prices = result.prices();
assert_eq!(prices.len(), 2);
assert_eq!(prices[0].duration, 1);
assert_eq!(prices[0].price, 13.98);
assert_eq!(prices[0].regular_price, 13.98);
assert_eq!(prices[0].additional_cost, Some(0.20));
assert_eq!(prices[0].currency, "USD");
assert_eq!(prices[1].duration, 2);
assert_eq!(prices[1].price, 26.26);
}
#[test]
fn pricing_request_builds_params() {
let params = PricingRequest::domains()
.action("REGISTER")
.tld("com")
.to_params();
let get = |key: &str| {
params
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
};
assert_eq!(get("ProductType"), Some("DOMAIN"));
assert_eq!(get("ProductCategory"), Some("DOMAINS"));
assert_eq!(get("ActionName"), Some("REGISTER"));
assert_eq!(get("ProductName"), Some("com"));
}
}