use reqwest::Url; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::{debug, instrument}; use crate::models::{TokenResponse, PaginatedResponse, Requisition, Account, TransactionsResponse}; #[derive(Error, Debug)] pub enum GoCardlessError { #[error("Request failed: {0}")] RequestFailed(#[from] reqwest::Error), #[error("Middleware error: {0}")] MiddlewareError(#[from] reqwest_middleware::Error), #[error("API Error: {0}")] ApiError(String), #[error("Serialization error: {0}")] SerializationError(#[from] serde_json::Error), #[error("URL Parse Error: {0}")] UrlParseError(#[from] url::ParseError), } pub struct GoCardlessClient { base_url: Url, client: ClientWithMiddleware, secret_id: String, secret_key: String, access_token: Option, access_expires_at: Option>, } #[derive(Serialize)] struct TokenRequest<'a> { secret_id: &'a str, secret_key: &'a str, } impl GoCardlessClient { pub fn new(base_url: &str, secret_id: &str, secret_key: &str) -> Result { Self::with_client(base_url, secret_id, secret_key, None) } pub fn with_client(base_url: &str, secret_id: &str, secret_key: &str, client: Option) -> Result { Ok(Self { base_url: Url::parse(base_url)?, client: client.unwrap_or_else(|| reqwest_middleware::ClientBuilder::new(reqwest::Client::new()).build()), secret_id: secret_id.to_string(), secret_key: secret_key.to_string(), access_token: None, access_expires_at: None, }) } #[instrument(skip(self))] pub async fn obtain_access_token(&mut self) -> Result<(), GoCardlessError> { // Check if current token is still valid (with 60s buffer) if let Some(expires) = self.access_expires_at { if chrono::Utc::now() < expires - chrono::Duration::seconds(60) { debug!("Access token is still valid"); return Ok(()); } } let url = self.base_url.join("/api/v2/token/new/")?; let body = TokenRequest { secret_id: &self.secret_id, secret_key: &self.secret_key, }; debug!("Requesting new access token"); let response = self.client.post(url) .json(&body) .send() .await?; if !response.status().is_success() { let status = response.status(); let text = response.text().await?; return Err(GoCardlessError::ApiError(format!("Token request failed {}: {}", status, text))); } let token_resp: TokenResponse = response.json().await?; self.access_token = Some(token_resp.access); self.access_expires_at = Some(chrono::Utc::now() + chrono::Duration::seconds(token_resp.access_expires as i64)); debug!("Access token obtained"); Ok(()) } #[instrument(skip(self))] pub async fn get_requisitions(&self) -> Result, GoCardlessError> { let url = self.base_url.join("/api/v2/requisitions/")?; self.get_authenticated(url).await } #[instrument(skip(self))] pub async fn get_account(&self, id: &str) -> Result { let url = self.base_url.join(&format!("/api/v2/accounts/{}/", id))?; self.get_authenticated(url).await } #[instrument(skip(self))] pub async fn get_transactions(&self, account_id: &str, date_from: Option<&str>, date_to: Option<&str>) -> Result { let mut url = self.base_url.join(&format!("/api/v2/accounts/{}/transactions/", account_id))?; { let mut pairs = url.query_pairs_mut(); if let Some(from) = date_from { pairs.append_pair("date_from", from); } if let Some(to) = date_to { pairs.append_pair("date_to", to); } } self.get_authenticated(url).await } async fn get_authenticated Deserialize<'de>>(&self, url: Url) -> Result { let token = self.access_token.as_ref().ok_or(GoCardlessError::ApiError("No access token available. Call obtain_access_token() first.".into()))?; let response = self.client.get(url) .bearer_auth(token) .header("accept", "application/json") .send() .await?; if !response.status().is_success() { let status = response.status(); let text = response.text().await?; return Err(GoCardlessError::ApiError(format!("API request failed {}: {}", status, text))); } let data = response.json().await?; Ok(data) } }