2025-11-22 16:23:53 +00:00
|
|
|
use crate::models::{AccountArray, TransactionArray, TransactionStore, TransactionUpdate};
|
2025-11-21 17:04:31 +01:00
|
|
|
use reqwest::Url;
|
|
|
|
|
use reqwest_middleware::ClientWithMiddleware;
|
2025-11-19 21:18:37 +00:00
|
|
|
use serde::de::DeserializeOwned;
|
|
|
|
|
use thiserror::Error;
|
|
|
|
|
use tracing::instrument;
|
|
|
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
|
|
|
|
pub enum FireflyError {
|
|
|
|
|
#[error("Request failed: {0}")]
|
|
|
|
|
RequestFailed(#[from] reqwest::Error),
|
2025-11-21 17:04:31 +01:00
|
|
|
#[error("Middleware error: {0}")]
|
|
|
|
|
MiddlewareError(#[from] reqwest_middleware::Error),
|
2025-11-19 21:18:37 +00:00
|
|
|
#[error("API Error: {0}")]
|
|
|
|
|
ApiError(String),
|
|
|
|
|
#[error("URL Parse Error: {0}")]
|
|
|
|
|
UrlParseError(#[from] url::ParseError),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct FireflyClient {
|
|
|
|
|
base_url: Url,
|
2025-11-21 17:04:31 +01:00
|
|
|
client: ClientWithMiddleware,
|
2025-11-19 21:18:37 +00:00
|
|
|
access_token: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FireflyClient {
|
|
|
|
|
pub fn new(base_url: &str, access_token: &str) -> Result<Self, FireflyError> {
|
2025-11-21 17:04:31 +01:00
|
|
|
Self::with_client(base_url, access_token, None)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 16:23:53 +00:00
|
|
|
pub fn with_client(
|
|
|
|
|
base_url: &str,
|
|
|
|
|
access_token: &str,
|
|
|
|
|
client: Option<ClientWithMiddleware>,
|
|
|
|
|
) -> Result<Self, FireflyError> {
|
2025-11-19 21:18:37 +00:00
|
|
|
Ok(Self {
|
|
|
|
|
base_url: Url::parse(base_url)?,
|
2025-11-22 16:23:53 +00:00
|
|
|
client: client.unwrap_or_else(|| {
|
|
|
|
|
reqwest_middleware::ClientBuilder::new(reqwest::Client::new()).build()
|
|
|
|
|
}),
|
2025-11-19 21:18:37 +00:00
|
|
|
access_token: access_token.to_string(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[instrument(skip(self))]
|
|
|
|
|
pub async fn get_accounts(&self, _iban: &str) -> Result<AccountArray, FireflyError> {
|
|
|
|
|
let mut url = self.base_url.join("/api/v1/accounts")?;
|
2025-11-22 16:23:53 +00:00
|
|
|
url.query_pairs_mut().append_pair("type", "asset");
|
|
|
|
|
|
2025-11-19 21:18:37 +00:00
|
|
|
self.get_authenticated(url).await
|
|
|
|
|
}
|
2025-11-22 16:23:53 +00:00
|
|
|
|
2025-11-19 21:18:37 +00:00
|
|
|
#[instrument(skip(self))]
|
|
|
|
|
pub async fn search_accounts(&self, query: &str) -> Result<AccountArray, FireflyError> {
|
|
|
|
|
let mut url = self.base_url.join("/api/v1/search/accounts")?;
|
|
|
|
|
url.query_pairs_mut()
|
|
|
|
|
.append_pair("query", query)
|
|
|
|
|
.append_pair("type", "asset")
|
|
|
|
|
.append_pair("field", "all");
|
2025-11-22 16:23:53 +00:00
|
|
|
|
2025-11-19 21:18:37 +00:00
|
|
|
self.get_authenticated(url).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[instrument(skip(self, transaction))]
|
2025-11-22 16:23:53 +00:00
|
|
|
pub async fn store_transaction(
|
|
|
|
|
&self,
|
|
|
|
|
transaction: TransactionStore,
|
|
|
|
|
) -> Result<(), FireflyError> {
|
2025-11-19 21:18:37 +00:00
|
|
|
let url = self.base_url.join("/api/v1/transactions")?;
|
2025-11-22 16:23:53 +00:00
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.client
|
|
|
|
|
.post(url)
|
2025-11-19 21:18:37 +00:00
|
|
|
.bearer_auth(&self.access_token)
|
|
|
|
|
.header("accept", "application/json")
|
|
|
|
|
.json(&transaction)
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let status = response.status();
|
|
|
|
|
let text = response.text().await?;
|
2025-11-22 16:23:53 +00:00
|
|
|
return Err(FireflyError::ApiError(format!(
|
|
|
|
|
"Store Transaction Failed {}: {}",
|
|
|
|
|
status, text
|
|
|
|
|
)));
|
2025-11-19 21:18:37 +00:00
|
|
|
}
|
2025-11-22 16:23:53 +00:00
|
|
|
|
2025-11-19 21:18:37 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-11-22 16:23:53 +00:00
|
|
|
|
2025-11-19 21:18:37 +00:00
|
|
|
#[instrument(skip(self))]
|
2025-11-22 16:23:53 +00:00
|
|
|
pub async fn list_account_transactions(
|
|
|
|
|
&self,
|
|
|
|
|
account_id: &str,
|
|
|
|
|
start: Option<&str>,
|
|
|
|
|
end: Option<&str>,
|
|
|
|
|
) -> Result<TransactionArray, FireflyError> {
|
|
|
|
|
let mut url = self
|
|
|
|
|
.base_url
|
|
|
|
|
.join(&format!("/api/v1/accounts/{}/transactions", account_id))?;
|
2025-11-19 21:18:37 +00:00
|
|
|
{
|
|
|
|
|
let mut pairs = url.query_pairs_mut();
|
|
|
|
|
if let Some(s) = start {
|
|
|
|
|
pairs.append_pair("start", s);
|
|
|
|
|
}
|
|
|
|
|
if let Some(e) = end {
|
|
|
|
|
pairs.append_pair("end", e);
|
|
|
|
|
}
|
|
|
|
|
// Limit to 50, could be higher but safer to page if needed. For heuristic checks 50 is usually plenty per day range.
|
2025-11-22 16:23:53 +00:00
|
|
|
pairs.append_pair("limit", "50");
|
2025-11-19 21:18:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.get_authenticated(url).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[instrument(skip(self, update))]
|
2025-11-22 16:23:53 +00:00
|
|
|
pub async fn update_transaction(
|
|
|
|
|
&self,
|
|
|
|
|
id: &str,
|
|
|
|
|
update: TransactionUpdate,
|
|
|
|
|
) -> Result<(), FireflyError> {
|
|
|
|
|
let url = self
|
|
|
|
|
.base_url
|
|
|
|
|
.join(&format!("/api/v1/transactions/{}", id))?;
|
2025-11-19 21:18:37 +00:00
|
|
|
|
2025-11-22 16:23:53 +00:00
|
|
|
let response = self
|
|
|
|
|
.client
|
|
|
|
|
.put(url)
|
2025-11-19 21:18:37 +00:00
|
|
|
.bearer_auth(&self.access_token)
|
|
|
|
|
.header("accept", "application/json")
|
|
|
|
|
.json(&update)
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
2025-11-22 16:23:53 +00:00
|
|
|
let status = response.status();
|
|
|
|
|
let text = response.text().await?;
|
|
|
|
|
return Err(FireflyError::ApiError(format!(
|
|
|
|
|
"Update Transaction Failed {}: {}",
|
|
|
|
|
status, text
|
|
|
|
|
)));
|
2025-11-19 21:18:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_authenticated<T: DeserializeOwned>(&self, url: Url) -> Result<T, FireflyError> {
|
2025-11-22 16:23:53 +00:00
|
|
|
let response = self
|
|
|
|
|
.client
|
|
|
|
|
.get(url)
|
2025-11-19 21:18:37 +00:00
|
|
|
.bearer_auth(&self.access_token)
|
|
|
|
|
.header("accept", "application/json")
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
2025-11-22 16:23:53 +00:00
|
|
|
let status = response.status();
|
|
|
|
|
let text = response.text().await?;
|
|
|
|
|
return Err(FireflyError::ApiError(format!(
|
|
|
|
|
"API request failed {}: {}",
|
|
|
|
|
status, text
|
|
|
|
|
)));
|
2025-11-19 21:18:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let data = response.json().await?;
|
|
|
|
|
Ok(data)
|
|
|
|
|
}
|
|
|
|
|
}
|