186 lines
4.7 KiB
Rust
186 lines
4.7 KiB
Rust
use reqwest::header::{HeaderMap, HeaderValue};
|
|
use reqwest::Client;
|
|
use serde::{Deserialize, Serialize};
|
|
use tracing::{error, info};
|
|
|
|
pub struct Firefly {
|
|
url: String,
|
|
client: reqwest::Client,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct TransactionSplit {
|
|
pub transaction_journal_id: String,
|
|
pub date: chrono::DateTime<chrono::FixedOffset>,
|
|
pub amount: String,
|
|
pub description: String,
|
|
pub notes: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct TransactionAttributes {
|
|
pub group_title: Option<String>,
|
|
pub transactions: Vec<TransactionSplit>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct TransactionRead {
|
|
pub id: String,
|
|
pub attributes: TransactionAttributes,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct TransactionArray {
|
|
pub data: Vec<TransactionRead>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct TransactionSingle {
|
|
pub data: TransactionRead,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct TransactionSplitUpdate {
|
|
pub transaction_journal_id: String,
|
|
pub amount: String,
|
|
pub notes: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct TransactionUpdate {
|
|
pub transactions: Vec<TransactionSplitUpdate>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[non_exhaustive]
|
|
enum AttachableType {
|
|
TransactionJournal,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct AttachmentStore {
|
|
filename: String,
|
|
attachable_type: AttachableType,
|
|
attachable_id: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct AttachmentRead {
|
|
id: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct AttachmentSingle {
|
|
data: AttachmentRead,
|
|
}
|
|
|
|
impl TransactionRead {
|
|
pub fn amount(&self) -> f64 {
|
|
self.attributes
|
|
.transactions
|
|
.iter()
|
|
.filter_map(|t| match t.amount.parse::<f64>() {
|
|
Ok(v) => Some(v),
|
|
Err(e) => {
|
|
error!("Invalid amount: {}", e);
|
|
None
|
|
},
|
|
})
|
|
.sum()
|
|
}
|
|
}
|
|
|
|
impl Firefly {
|
|
pub fn new(url: impl Into<String>, token: &str) -> Self {
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(
|
|
"Accept",
|
|
HeaderValue::from_static("application/vnd.api+json"),
|
|
);
|
|
headers.insert(
|
|
"Authorization",
|
|
HeaderValue::from_str(&format!("Bearer {}", &token)).unwrap(),
|
|
);
|
|
let client =
|
|
Client::builder().default_headers(headers).build().unwrap();
|
|
Self {
|
|
url: url.into().trim_end_matches("/").into(),
|
|
client,
|
|
}
|
|
}
|
|
|
|
pub fn url(&self) -> &str {
|
|
&self.url
|
|
}
|
|
|
|
pub async fn search_transactions(
|
|
&self,
|
|
query: &str,
|
|
) -> Result<TransactionArray, reqwest::Error> {
|
|
let url = format!("{}/api/v1/search/transactions", self.url);
|
|
let res = self
|
|
.client
|
|
.get(url)
|
|
.query(&[("query", query)])
|
|
.send()
|
|
.await?;
|
|
res.error_for_status_ref()?;
|
|
res.json().await
|
|
}
|
|
|
|
pub async fn get_transaction(
|
|
&self,
|
|
id: &str,
|
|
) -> Result<TransactionSingle, reqwest::Error> {
|
|
let url = format!("{}/api/v1/transactions/{}", self.url, id);
|
|
let res = self.client.get(url).send().await?;
|
|
res.error_for_status_ref()?;
|
|
res.json().await
|
|
}
|
|
|
|
pub async fn update_transaction(
|
|
&self,
|
|
id: &str,
|
|
txn: &TransactionUpdate,
|
|
) -> Result<TransactionSingle, reqwest::Error> {
|
|
let url = format!("{}/api/v1/transactions/{}", self.url, id);
|
|
let res = self.client.put(url).json(txn).send().await?;
|
|
res.error_for_status_ref()?;
|
|
info!("Successfully updated transaction {}", id);
|
|
res.json().await
|
|
}
|
|
|
|
pub async fn attach_receipt(
|
|
&self,
|
|
id: &str,
|
|
content: impl Into<reqwest::Body>,
|
|
filename: impl Into<String>,
|
|
) -> Result<(), reqwest::Error> {
|
|
let filename = filename.into();
|
|
info!("Attaching receipt {} to transaction {}", filename, id);
|
|
let url = format!("{}/api/v1/attachments", self.url);
|
|
let data = AttachmentStore {
|
|
filename,
|
|
attachable_type: AttachableType::TransactionJournal,
|
|
attachable_id: id.into(),
|
|
};
|
|
let res = self.client.post(url).json(&data).send().await?;
|
|
res.error_for_status_ref()?;
|
|
let attachment: AttachmentSingle = res.json().await?;
|
|
info!("Created attachment {}", attachment.data.id);
|
|
let url = format!(
|
|
"{}/api/v1/attachments/{}/upload",
|
|
self.url, attachment.data.id
|
|
);
|
|
let res = self
|
|
.client
|
|
.post(url)
|
|
.header("Content-Type", "application/octet-stream")
|
|
.body(content)
|
|
.send()
|
|
.await?;
|
|
res.error_for_status_ref()?;
|
|
Ok(())
|
|
}
|
|
}
|