Implement logic

This commit is contained in:
2025-11-19 21:18:37 +00:00
parent a0a7236752
commit c21242d206
31 changed files with 4802 additions and 139 deletions

View File

@@ -0,0 +1,126 @@
use reqwest::{Client, Url};
use serde::de::DeserializeOwned;
use thiserror::Error;
use tracing::instrument;
use crate::models::{AccountArray, TransactionStore, TransactionArray, TransactionUpdate};
#[derive(Error, Debug)]
pub enum FireflyError {
#[error("Request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("API Error: {0}")]
ApiError(String),
#[error("URL Parse Error: {0}")]
UrlParseError(#[from] url::ParseError),
}
pub struct FireflyClient {
base_url: Url,
client: Client,
access_token: String,
}
impl FireflyClient {
pub fn new(base_url: &str, access_token: &str) -> Result<Self, FireflyError> {
Ok(Self {
base_url: Url::parse(base_url)?,
client: Client::new(),
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")?;
url.query_pairs_mut()
.append_pair("type", "asset");
self.get_authenticated(url).await
}
#[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");
self.get_authenticated(url).await
}
#[instrument(skip(self, transaction))]
pub async fn store_transaction(&self, transaction: TransactionStore) -> Result<(), FireflyError> {
let url = self.base_url.join("/api/v1/transactions")?;
let response = self.client.post(url)
.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?;
return Err(FireflyError::ApiError(format!("Store Transaction Failed {}: {}", status, text)));
}
Ok(())
}
#[instrument(skip(self))]
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))?;
{
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.
pairs.append_pair("limit", "50");
}
self.get_authenticated(url).await
}
#[instrument(skip(self, update))]
pub async fn update_transaction(&self, id: &str, update: TransactionUpdate) -> Result<(), FireflyError> {
let url = self.base_url.join(&format!("/api/v1/transactions/{}", id))?;
let response = self.client.put(url)
.bearer_auth(&self.access_token)
.header("accept", "application/json")
.json(&update)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await?;
return Err(FireflyError::ApiError(format!("Update Transaction Failed {}: {}", status, text)));
}
Ok(())
}
async fn get_authenticated<T: DeserializeOwned>(&self, url: Url) -> Result<T, FireflyError> {
let response = self.client.get(url)
.bearer_auth(&self.access_token)
.header("accept", "application/json")
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await?;
return Err(FireflyError::ApiError(format!("API request failed {}: {}", status, text)));
}
let data = response.json().await?;
Ok(data)
}
}

View File

@@ -0,0 +1,2 @@
pub mod client;
pub mod models;

View File

@@ -0,0 +1,81 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountRead {
pub id: String,
pub attributes: Account,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Account {
pub name: String,
pub iban: Option<String>,
#[serde(rename = "type")]
pub account_type: String,
pub active: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountArray {
pub data: Vec<AccountRead>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionRead {
pub id: String,
pub attributes: Transaction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
pub transactions: Vec<TransactionSplit>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionSplit {
pub date: String,
pub amount: String,
pub description: String,
pub external_id: Option<String>,
pub currency_code: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionArray {
pub data: Vec<TransactionRead>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionSplitStore {
#[serde(rename = "type")]
pub transaction_type: String,
pub date: String,
pub amount: String,
pub description: String,
pub source_id: Option<String>,
pub source_name: Option<String>,
pub destination_id: Option<String>,
pub destination_name: Option<String>,
pub currency_code: Option<String>,
pub foreign_amount: Option<String>,
pub foreign_currency_code: Option<String>,
pub external_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionStore {
pub transactions: Vec<TransactionSplitStore>,
pub apply_rules: Option<bool>,
pub fire_webhooks: Option<bool>,
pub error_if_duplicate_hash: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionUpdate {
pub transactions: Vec<TransactionSplitUpdate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionSplitUpdate {
pub external_id: Option<String>,
}