279 lines
8.5 KiB
Rust
279 lines
8.5 KiB
Rust
use rocket::form::Form;
|
|
use rocket::http::{ContentType, Header, MediaType, Status};
|
|
use rocket::serde::json::Json;
|
|
use rocket::{Route, State};
|
|
use rocket_db_pools::Connection as DatabaseConnection;
|
|
use rocket_dyn_templates::{context, Template};
|
|
use rust_decimal::prelude::ToPrimitive;
|
|
use tracing::{debug, error, info};
|
|
|
|
use crate::firefly::{TransactionStore, TransactionUpdate};
|
|
use crate::receipts::*;
|
|
use crate::{Context, Database};
|
|
|
|
#[rocket::get("/")]
|
|
pub async fn list_receipts(
|
|
db: DatabaseConnection<Database>,
|
|
) -> (Status, Template) {
|
|
let mut repo = ReceiptsRepository::new(db);
|
|
match repo.list_receipts().await {
|
|
Ok(r) => (
|
|
Status::Ok,
|
|
Template::render(
|
|
"receipt-list",
|
|
context! {
|
|
receipts: r,
|
|
},
|
|
),
|
|
),
|
|
Err(e) => (
|
|
Status::InternalServerError,
|
|
Template::render(
|
|
"error",
|
|
context! {
|
|
error: e.to_string(),
|
|
},
|
|
),
|
|
),
|
|
}
|
|
}
|
|
|
|
#[rocket::get("/add")]
|
|
pub async fn receipt_form() -> Template {
|
|
Template::render("receipt-form", context! {})
|
|
}
|
|
|
|
#[rocket::post("/add", data = "<form>")]
|
|
pub async fn add_receipt(
|
|
form: Form<ReceiptPostForm<'_>>,
|
|
db: DatabaseConnection<Database>,
|
|
ctx: &State<Context>,
|
|
) -> (Status, Json<AddReceiptResponse>) {
|
|
let data = match ReceiptPostData::from_form(&form).await {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
return (
|
|
Status::BadRequest,
|
|
Json(AddReceiptResponse::Error(e.to_string())),
|
|
);
|
|
},
|
|
};
|
|
let mut repo = ReceiptsRepository::new(db);
|
|
let receipt = match repo.add_receipt(&data).await {
|
|
Ok(r) => {
|
|
info!("Created new receipt {}", r.id);
|
|
r
|
|
},
|
|
Err(e) => {
|
|
error!("Failed to insert new receipt record: {}", e);
|
|
return (
|
|
Status::InternalServerError,
|
|
Json(AddReceiptResponse::Error(e.to_string())),
|
|
);
|
|
},
|
|
};
|
|
let xact = match form.transaction {
|
|
Some(ref s) if s == "new" => {
|
|
let data = TransactionStore::new_withdrawal(
|
|
data.date,
|
|
data.amount.to_string(),
|
|
data.vendor,
|
|
ctx.config.firefly.default_account.clone(),
|
|
Some("(no name)".into()),
|
|
data.notes,
|
|
);
|
|
match ctx.firefly.create_transaction(data).await {
|
|
Ok(t) => {
|
|
info!("Successfully created transaction ID {}", t.data.id);
|
|
Some(t)
|
|
},
|
|
Err(e) => {
|
|
error!("Failed to create Firefly transaction: {}", e);
|
|
None
|
|
},
|
|
}
|
|
},
|
|
Some(ref s) if s == "deposit" => {
|
|
let data = TransactionStore::new_deposit(
|
|
data.date,
|
|
data.amount.to_string(),
|
|
data.vendor,
|
|
Some("(no name)".into()),
|
|
ctx.config.firefly.default_account.clone(),
|
|
data.notes,
|
|
);
|
|
match ctx.firefly.create_transaction(data).await {
|
|
Ok(t) => {
|
|
info!("Successfully created transaction ID {}", t.data.id);
|
|
Some(t)
|
|
},
|
|
Err(e) => {
|
|
error!("Failed to create Firefly transaction: {}", e);
|
|
None
|
|
},
|
|
}
|
|
},
|
|
Some(ref id) if !id.is_empty() => match ctx
|
|
.firefly
|
|
.get_transaction(id)
|
|
.await
|
|
{
|
|
Ok(t) => {
|
|
let mut needs_update = false;
|
|
let mut update = TransactionUpdate::from(t.data.clone());
|
|
let amount = t.data.amount();
|
|
if let Some(split) = update.transactions.last_mut() {
|
|
if data.amount.to_f64() != Some(amount) {
|
|
split.amount = data.amount.to_string();
|
|
needs_update = true;
|
|
}
|
|
if data.vendor != split.description {
|
|
split.description = data.vendor;
|
|
needs_update = true;
|
|
}
|
|
if !data.notes.is_empty() {
|
|
if let Some(notes) = split.notes.as_deref() {
|
|
if notes != data.notes.as_str() {
|
|
split.notes = Some(data.notes.clone());
|
|
needs_update = true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
debug!("Transaction {} has no splits", id);
|
|
}
|
|
if needs_update {
|
|
let res = ctx
|
|
.firefly
|
|
.update_transaction(&t.data.id, &update)
|
|
.await;
|
|
match res {
|
|
Ok(t) => {
|
|
info!("Successfully updated transaction {}", id);
|
|
Some(t)
|
|
},
|
|
Err(e) => {
|
|
error!(
|
|
"Failed to update trancation {}: {}",
|
|
id, e
|
|
);
|
|
Some(t)
|
|
},
|
|
}
|
|
} else {
|
|
debug!("Transaction {} does not need updated", id);
|
|
Some(t)
|
|
}
|
|
},
|
|
Err(e) => {
|
|
error!("Could not load Firefly transaction {}: {}", id, e);
|
|
None
|
|
},
|
|
},
|
|
Some(_) => None,
|
|
None => None,
|
|
};
|
|
if let Some(xact) = xact {
|
|
if let Some(t) = xact.data.attributes.transactions.first() {
|
|
if let Err(e) = ctx
|
|
.firefly
|
|
.attach_receipt(
|
|
&t.transaction_journal_id,
|
|
data.photo,
|
|
data.filename,
|
|
)
|
|
.await
|
|
{
|
|
error!(
|
|
"Failed to attach receipt to Firefly transaction: {}",
|
|
e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
(Status::Ok, Json(AddReceiptResponse::Success(receipt)))
|
|
}
|
|
|
|
#[rocket::get("/<id>")]
|
|
pub async fn get_receipt(
|
|
id: i32,
|
|
db: DatabaseConnection<Database>,
|
|
) -> Option<Template> {
|
|
let mut repo = ReceiptsRepository::new(db);
|
|
match repo.get_receipt(id).await {
|
|
Ok(r) => Some(Template::render("receipt", r)),
|
|
Err(e) => {
|
|
error!("Error fetching receipt image: {}", e);
|
|
None
|
|
},
|
|
}
|
|
}
|
|
|
|
#[derive(rocket::response::Responder)]
|
|
pub struct PhotoResponse {
|
|
content: Vec<u8>,
|
|
content_type: ContentType,
|
|
cache_control: Header<'static>,
|
|
}
|
|
|
|
impl PhotoResponse {
|
|
fn new(content: Vec<u8>, content_type: ContentType) -> Self {
|
|
let cache_control = Header::new("Cache-Control", "max-age=604800");
|
|
Self {
|
|
content,
|
|
content_type,
|
|
cache_control,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[rocket::get("/<id>/view/<_>")]
|
|
pub async fn view_receipt_photo(
|
|
id: i32,
|
|
db: DatabaseConnection<Database>,
|
|
) -> Option<PhotoResponse> {
|
|
let mut repo = ReceiptsRepository::new(db);
|
|
match repo.get_receipt_photo(id).await {
|
|
Ok((filename, image)) => {
|
|
let mt = filename
|
|
.rsplit_once('.')
|
|
.and_then(|(_, ext)| MediaType::from_extension(ext))
|
|
.unwrap_or(MediaType::Binary);
|
|
Some(PhotoResponse::new(image, ContentType(mt)))
|
|
},
|
|
Err(e) => {
|
|
error!("Error fetching receipt image: {}", e);
|
|
None
|
|
},
|
|
}
|
|
}
|
|
|
|
#[rocket::delete("/<id>")]
|
|
pub async fn delete_receipt(
|
|
id: i32,
|
|
db: DatabaseConnection<Database>,
|
|
) -> (Status, Json<DeleteReceiptResponse>) {
|
|
let mut repo = ReceiptsRepository::new(db);
|
|
match repo.delete_receipt(id).await {
|
|
Ok(_) => (Status::Ok, Json(DeleteReceiptResponse::Success)),
|
|
Err(e) => {
|
|
error!("Error fetching receipt image: {}", e);
|
|
(
|
|
Status::InternalServerError,
|
|
Json(DeleteReceiptResponse::Error(e.to_string())),
|
|
)
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn routes() -> Vec<Route> {
|
|
rocket::routes![
|
|
add_receipt,
|
|
delete_receipt,
|
|
get_receipt,
|
|
list_receipts,
|
|
receipt_form,
|
|
view_receipt_photo,
|
|
]
|
|
}
|