receipts/src/receipts.rs

197 lines
5.1 KiB
Rust

use chrono::{DateTime, FixedOffset};
use rocket::fs::TempFile;
use rocket::tokio::io::{AsyncReadExt, BufReader};
use rocket_db_pools::Connection;
use serde::Serialize;
use sqlx::types::Decimal;
use tracing::error;
use crate::imaging;
use crate::Database;
#[derive(Debug, Serialize)]
pub struct Receipt {
pub id: i32,
pub date: DateTime<FixedOffset>,
pub vendor: String,
pub amount: Decimal,
pub notes: Option<String>,
pub filename: String,
pub image: Vec<u8>,
}
#[derive(Debug, Serialize)]
pub struct ReceiptJson {
pub id: i32,
pub date: DateTime<FixedOffset>,
pub vendor: String,
pub amount: Decimal,
pub notes: Option<String>,
pub filename: String,
}
#[derive(rocket::FromForm)]
pub struct ReceiptPostForm<'r> {
pub transaction: Option<String>,
pub date: String,
pub vendor: String,
pub amount: String,
pub is_restaurant: Option<bool>,
pub notes: String,
pub photo: TempFile<'r>,
}
pub struct ReceiptPostData {
pub date: DateTime<FixedOffset>,
pub vendor: String,
pub amount: Decimal,
pub is_restaurant: bool,
pub notes: String,
pub filename: String,
pub photo: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum ReceiptPostFormError {
#[error("Invalid date: {0}")]
Date(#[from] chrono::format::ParseError),
#[error("Invalid amount: {0}")]
Amount(#[from] rust_decimal::Error),
#[error("Error reading photo: {0}")]
Photo(#[from] std::io::Error),
#[error("Unsupported image type")]
UnsupportedImageFormat,
}
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AddReceiptResponse {
Success(ReceiptJson),
Error(String),
}
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DeleteReceiptResponse {
Success,
Error(String),
}
impl ReceiptPostData {
pub async fn from_form(
form: &ReceiptPostForm<'_>,
) -> Result<Self, ReceiptPostFormError> {
let date = DateTime::parse_from_str(
&format!("{} 00:00:00 +0000", form.date),
"%Y-%m-%d %H:%M:%S %z",
)?;
let vendor = form.vendor.clone();
use rust_decimal::prelude::FromStr;
let amount = Decimal::from_str(&form.amount)?;
let is_restaurant = form.is_restaurant.unwrap_or_default();
let notes = form.notes.clone();
let stream = form.photo.open().await?;
let mut reader = BufReader::new(stream);
let mut photo = Vec::new();
reader.read_to_end(&mut photo).await?;
let extension = match imaging::get_type(&photo).as_deref() {
Some("BMP") => "bmp",
Some("JPEG") => "jpg",
Some("PDF") => "pdf",
Some("PNG") => "png",
Some("PPM") => "ppm",
Some("TIFF") => "tiff",
Some("WEBP") => "webp",
Some(f) => {
error!("Unsupported image format: {}", f);
return Err(ReceiptPostFormError::UnsupportedImageFormat);
},
None => {
return Err(ReceiptPostFormError::UnsupportedImageFormat);
},
};
let filename = form
.photo
.name()
.map(|n| format!("{}.{}", n, extension))
.unwrap_or("photo.jpg".into());
Ok(Self {
date,
vendor,
amount,
is_restaurant,
notes,
filename,
photo,
})
}
}
pub struct ReceiptsRepository {
conn: Connection<Database>,
}
impl ReceiptsRepository {
pub fn new(conn: Connection<Database>) -> Self {
Self { conn }
}
pub async fn list_receipts(
&mut self,
) -> Result<Vec<ReceiptJson>, sqlx::Error> {
sqlx::query_file_as!(ReceiptJson, "sql/receipts/list-receipts.sql")
.fetch_all(&mut **self.conn)
.await
}
pub async fn add_receipt(
&mut self,
data: &ReceiptPostData,
) -> Result<ReceiptJson, sqlx::Error> {
let result = sqlx::query_file_as!(
ReceiptJson,
"sql/receipts/insert-receipt.sql",
data.vendor,
data.date,
data.amount,
data.notes,
data.filename,
data.photo,
)
.fetch_one(&mut **self.conn)
.await?;
Ok(result)
}
pub async fn get_receipt(
&mut self,
id: i32,
) -> Result<ReceiptJson, sqlx::Error> {
sqlx::query_file_as!(ReceiptJson, "sql/receipts/get-receipt.sql", id,)
.fetch_one(&mut **self.conn)
.await
}
pub async fn get_receipt_photo(
&mut self,
id: i32,
) -> Result<(String, Vec<u8>), sqlx::Error> {
let result = sqlx::query!(
"SELECT filename, image FROM receipts WHERE id = $1",
id,
)
.fetch_one(&mut **self.conn)
.await?;
Ok((result.filename, result.image))
}
pub async fn delete_receipt(
&mut self,
id: i32,
) -> Result<(), sqlx::Error> {
sqlx::query_as!(ReceiptJson, "DELETE FROM receipts WHERE id = $1", id)
.execute(&mut **self.conn)
.await?;
Ok(())
}
}