use crate::{
client::ItadClient,
types::{
DealInfo, DealsResponse, GameInfoResponse, GamePriceItem, GameSearchItem, PriceHistoryItem,
},
};
use dealve_core::{
models::{Deal, GameInfo, PriceHistoryPoint},
DealveError, Result,
};
use std::{cmp::Ordering, collections::HashMap};
impl ItadClient {
pub async fn get_deals(
&self,
country: &str,
limit: usize,
offset: usize,
shop_id: Option<u32>,
sort: Option<&str>,
) -> Result<Vec<Deal>> {
let api_key = self
.api_key()
.ok_or_else(|| DealveError::Config("API key is required".to_string()))?;
let url = format!("{}/deals/v2", self.base_url());
let mut query_params: Vec<(&str, String)> = vec![
("key", api_key.to_string()),
("country", country.to_string()),
("limit", limit.to_string()),
("offset", offset.to_string()),
];
if let Some(id) = shop_id {
query_params.push(("shops", id.to_string()));
}
if let Some(s) = sort {
query_params.push(("sort", s.to_string()));
}
let response = self
.client()
.get(&url)
.query(&query_params)
.send()
.await
.map_err(|e| DealveError::Network(e.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(DealveError::Api(format!(
"API returned status {}: {}",
status, body
)));
}
let deals_response: DealsResponse = response
.json()
.await
.map_err(|e| DealveError::Parse(e.to_string()))?;
Ok(deals_response.list.into_iter().map(Deal::from).collect())
}
pub async fn get_game_info(&self, game_id: &str) -> Result<GameInfo> {
let api_key = self
.api_key()
.ok_or_else(|| DealveError::Config("API key is required".to_string()))?;
let url = format!("{}/games/info/v2", self.base_url());
let response = self
.client()
.get(&url)
.query(&[("key", api_key), ("id", game_id)])
.send()
.await
.map_err(|e| DealveError::Network(e.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(DealveError::Api(format!(
"API returned status {}: {}",
status, body
)));
}
let info_response: GameInfoResponse = response
.json()
.await
.map_err(|e| DealveError::Parse(e.to_string()))?;
Ok(GameInfo::from(info_response))
}
pub async fn search_games(&self, title: &str, results: usize) -> Result<Vec<GameSearchItem>> {
let api_key = self
.api_key()
.ok_or_else(|| DealveError::Config("API key is required".to_string()))?;
if title.trim().is_empty() || results == 0 {
return Ok(vec![]);
}
let url = format!("{}/games/search/v1", self.base_url());
let query_params = [
("key", api_key.to_string()),
("title", title.to_string()),
("results", results.to_string()),
];
let response = self
.client()
.get(&url)
.query(&query_params)
.send()
.await
.map_err(|e| DealveError::Network(e.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(DealveError::Api(format!(
"API returned status {}: {}",
status, body
)));
}
response
.json()
.await
.map_err(|e| DealveError::Parse(e.to_string()))
}
pub async fn get_prices_for_games(
&self,
ids: &[String],
country: &str,
shop_id: Option<u32>,
) -> Result<Vec<GamePriceItem>> {
let api_key = self
.api_key()
.ok_or_else(|| DealveError::Config("API key is required".to_string()))?;
if ids.is_empty() {
return Ok(vec![]);
}
let url = format!("{}/games/prices/v3", self.base_url());
let mut query_params: Vec<(&str, String)> = vec![
("key", api_key.to_string()),
("country", country.to_string()),
("deals", "true".to_string()),
];
if let Some(id) = shop_id {
query_params.push(("capacity", "1".to_string()));
query_params.push(("shops", id.to_string()));
}
let response = self
.client()
.post(&url)
.query(&query_params)
.json(ids)
.send()
.await
.map_err(|e| DealveError::Network(e.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(DealveError::Api(format!(
"API returned status {}: {}",
status, body
)));
}
response
.json()
.await
.map_err(|e| DealveError::Parse(e.to_string()))
}
pub async fn search_deals(
&self,
query: &str,
country: &str,
shop_id: Option<u32>,
limit: usize,
) -> Result<Vec<Deal>> {
let query = query.trim();
if query.is_empty() || limit == 0 {
return Ok(vec![]);
}
let search_results = self.search_games(query, limit).await?;
if search_results.is_empty() {
return Ok(vec![]);
}
let mut ids = Vec::with_capacity(search_results.len());
let mut titles_by_id = HashMap::with_capacity(search_results.len());
for result in search_results {
if titles_by_id.contains_key(&result.id) {
continue;
}
ids.push(result.id.clone());
titles_by_id.insert(result.id, result.title);
}
let prices = self.get_prices_for_games(&ids, country, shop_id).await?;
let mut deals_by_id: HashMap<String, (DealInfo, Option<f64>)> = HashMap::new();
for price_item in prices {
let history_low = price_item
.history_low
.and_then(|h| h.all.map(|price| price.amount));
if let Some(best_deal) = select_best_deal(price_item.deals) {
deals_by_id.insert(price_item.id, (best_deal, history_low));
}
}
let mut deals = Vec::new();
for id in ids {
let Some((deal_info, history_low)) = deals_by_id.remove(&id) else {
continue;
};
let title = titles_by_id.remove(&id).unwrap_or_else(|| id.clone());
deals.push(Deal {
id,
title,
shop: dealve_core::models::Shop {
id: deal_info.shop.id.to_string(),
name: deal_info.shop.name,
},
price: dealve_core::models::Price {
amount: deal_info.price.amount,
currency: deal_info.price.currency,
discount: deal_info.cut,
},
regular_price: deal_info.regular.amount,
url: deal_info.url,
history_low: history_low.or_else(|| deal_info.history_low.map(|h| h.amount)),
});
}
Ok(deals)
}
pub async fn get_price_history(
&self,
game_id: &str,
country: &str,
) -> Result<Vec<PriceHistoryPoint>> {
let api_key = self
.api_key()
.ok_or_else(|| DealveError::Config("API key is required".to_string()))?;
let url = format!("{}/games/history/v2", self.base_url());
let one_year_ago = chrono::Utc::now() - chrono::Duration::days(365);
let since = one_year_ago.format("%Y-%m-%dT%H:%M:%SZ").to_string();
let response = self
.client()
.get(&url)
.query(&[
("key", api_key),
("id", game_id),
("country", country),
("since", since.as_str()),
])
.send()
.await
.map_err(|e| DealveError::Network(e.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(DealveError::Api(format!(
"API returned status {}: {}",
status, body
)));
}
let history_items: Vec<PriceHistoryItem> = response
.json()
.await
.map_err(|e| DealveError::Parse(e.to_string()))?;
let mut points: Vec<PriceHistoryPoint> = history_items
.into_iter()
.filter_map(|item| {
let deal = item.deal?;
let timestamp = chrono::DateTime::parse_from_rfc3339(&item.timestamp)
.ok()?
.timestamp();
Some(PriceHistoryPoint {
timestamp,
price: deal.price.amount,
shop_name: item.shop.name,
})
})
.collect();
points.sort_by_key(|p| p.timestamp);
Ok(points)
}
pub async fn validate_api_key(api_key: &str) -> Result<()> {
let client = reqwest::Client::new();
let url = "https://api.isthereanydeal.com/deals/v2";
let response = client
.get(url)
.query(&[("key", api_key), ("limit", "1"), ("country", "US")])
.send()
.await
.map_err(|e| DealveError::Network(e.to_string()))?;
match response.status().as_u16() {
200..=299 => Ok(()),
401 | 403 => Err(DealveError::Api("Invalid API key".to_string())),
429 => Err(DealveError::Api(
"Rate limited - please wait and try again".to_string(),
)),
_ => {
let body = response.text().await.unwrap_or_default();
Err(DealveError::Api(format!("API error: {}", body)))
}
}
}
}
fn select_best_deal(deals: Vec<DealInfo>) -> Option<DealInfo> {
deals.into_iter().min_by(|a, b| {
let price_order = a.price.amount.total_cmp(&b.price.amount);
if price_order == Ordering::Equal {
b.cut.cmp(&a.cut)
} else {
price_order
}
})
}