receipts/src/routes/receipts.rs

309 lines
9.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::imaging;
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 {
split.notes = data.notes.into();
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>/thumbnail/<_>")]
pub async fn view_receipt_thumbnail(
id: i32,
db: DatabaseConnection<Database>,
) -> Option<PhotoResponse> {
let mut repo = ReceiptsRepository::new(db);
match repo.get_receipt_photo(id).await {
Ok((_, image)) => {
let thumbnail = match imaging::thumbnail(&image) {
Ok(Some(t)) => t,
Ok(None) => return None,
Err(e) => {
error!("Failed to create receipt photo thumbnail: {}", e);
return None;
},
};
Some(PhotoResponse::new(thumbnail, ContentType(MediaType::WEBP)))
},
Err(e) => {
error!("Error fetching receipt image: {}", e);
None
},
}
}
#[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_thumbnail,
view_receipt_photo,
]
}