game_manager_lib\services/
itad.rs

1//! Serviço para interagir com a API da IsThereAnyDeal (ITAD)
2//!
3//! Fornece funções para buscar IDs de jogos e obter informações de preços.
4//! A ITAD é uma plataforma que agrega ofertas de jogos de várias lojas digitais.
5
6use crate::constants::ITAD_API_URL;
7use crate::security;
8use crate::utils::http_client::HTTP_CLIENT;
9use serde::{Deserialize, Serialize};
10
11// === ESTRUTURAS PÚBLICAS ===
12
13#[derive(Debug, Serialize, Deserialize)]
14pub struct ItadLookupResult {
15    pub found: bool,
16    pub id: Option<String>,
17    pub slug: Option<String>,
18    pub title: Option<String>,
19}
20
21#[derive(Debug, Serialize, Deserialize)]
22pub struct ItadShop {
23    pub id: u64,
24    pub name: String,
25}
26
27#[derive(Debug, Serialize, Deserialize)]
28pub struct ItadPrice {
29    pub price: f64,
30    pub currency: String,
31    pub cut: Option<i32>,
32    pub shop: ItadShop,
33    pub url: Option<String>,
34    pub voucher: Option<String>,
35}
36
37#[derive(Debug, Serialize, Deserialize)]
38pub struct ItadGameOverview {
39    pub id: String,
40    pub title: Option<String>,
41    pub current: Option<ItadPrice>,
42    pub lowest: Option<ItadPrice>,
43}
44
45// === ESTRUTURAS INTERNAS (Espelho exato do JSON da API) ===
46
47#[derive(Debug, Deserialize)]
48struct RawItadResponse {
49    prices: Vec<RawItadGame>,
50}
51
52#[derive(Debug, Deserialize)]
53struct RawItadGame {
54    id: String,
55    current: Option<RawItadDeal>,
56    lowest: Option<RawItadDeal>,
57}
58
59#[derive(Debug, Deserialize)]
60struct RawItadDeal {
61    shop: ItadShop,
62    price: RawPriceValue,
63    cut: Option<i32>,
64    url: Option<String>,
65    voucher: Option<String>,
66}
67
68#[derive(Debug, Deserialize)]
69struct RawPriceValue {
70    amount: f64,
71    currency: String,
72}
73
74// === IMPLEMENTAÇÃO ===
75
76/// Busca o ID do jogo na ITAD pelo título
77pub async fn find_game_id(title: &str) -> Result<String, String> {
78    let key = security::get_itad_api_key();
79    if key.is_empty() {
80        return Err("API Key da ITAD não configurada".into());
81    }
82
83    let url = format!(
84        "{}/games/lookup/v1?key={}&title={}",
85        ITAD_API_URL,
86        key,
87        urlencoding::encode(title)
88    );
89
90    let res = HTTP_CLIENT
91        .get(&url)
92        .send()
93        .await
94        .map_err(|e| e.to_string())?;
95
96    if !res.status().is_success() {
97        return Err(format!("Erro ITAD Lookup: {}", res.status()));
98    }
99
100    let response_text = res.text().await.map_err(|e| e.to_string())?;
101
102    #[derive(Deserialize)]
103    struct LookupResponse {
104        game: Option<GameInfo>,
105    }
106    #[derive(Deserialize)]
107    struct GameInfo {
108        id: String,
109    }
110
111    let response: LookupResponse = serde_json::from_str(&response_text).map_err(|e| {
112        tracing::error!("Erro JSON Lookup: {}", e);
113        format!("Erro de JSON: {}", e)
114    })?;
115
116    match response.game {
117        Some(game_info) => Ok(game_info.id),
118        None => Err("Jogo não encontrado na ITAD".into()),
119    }
120}
121
122/// Busca informações de preço para uma lista de IDs da ITAD
123pub async fn get_prices(itad_ids: Vec<String>) -> Result<Vec<ItadGameOverview>, String> {
124    let key = security::get_itad_api_key();
125    if key.is_empty() {
126        return Err("API Key ausente".into());
127    }
128    if itad_ids.is_empty() {
129        return Ok(vec![]);
130    }
131
132    let url = format!("{}/games/overview/v2?key={}&country=BR", ITAD_API_URL, key);
133
134    tracing::debug!("ITAD Prices Request: {} items", itad_ids.len());
135
136    let res = HTTP_CLIENT
137        .post(&url)
138        .json(&itad_ids)
139        .send()
140        .await
141        .map_err(|e| e.to_string())?;
142
143    let status = res.status();
144
145    if !status.is_success() {
146        let body = res.text().await.unwrap_or_default();
147        tracing::error!("Erro ITAD Prices: {}", body);
148        return Err(format!("Erro ITAD Prices: {}", status));
149    }
150
151    let response_text = res.text().await.map_err(|e| e.to_string())?;
152
153    // 1. Deserializa para a estrutura bruta (Raw) que corresponde ao JSON
154    let raw_response: RawItadResponse = serde_json::from_str(&response_text).map_err(|e| {
155        tracing::error!("Erro ao parsear JSON da ITAD: {}", e);
156        // Dica de debug: mostra onde quebrou
157        format!(
158            "Erro de JSON (linha: {}, col: {}): {}",
159            e.line(),
160            e.column(),
161            e
162        )
163    })?;
164
165    // 2. Converte para a estrutura limpa (Public)
166    let clean_overview: Vec<ItadGameOverview> = raw_response
167        .prices
168        .into_iter()
169        .map(|raw| {
170            ItadGameOverview {
171                id: raw.id,
172                title: None, // A API de preços não retorna título, o wishlist.rs já tem o nome
173                current: raw.current.map(convert_deal),
174                lowest: raw.lowest.map(convert_deal),
175            }
176        })
177        .collect();
178
179    Ok(clean_overview)
180}
181
182// Helper para converter o deal bruto para o limpo (achatando o preço)
183fn convert_deal(raw: RawItadDeal) -> ItadPrice {
184    ItadPrice {
185        price: raw.price.amount,
186        currency: raw.price.currency,
187        cut: raw.cut,
188        shop: raw.shop,
189        url: raw.url,
190        voucher: raw.voucher,
191    }
192}