173 lines
4.3 KiB
Rust
173 lines
4.3 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::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 date: String,
|
|
pub vendor: String,
|
|
pub amount: String,
|
|
pub notes: String,
|
|
pub photo: TempFile<'r>,
|
|
}
|
|
|
|
pub struct ReceiptPostData {
|
|
pub date: DateTime<FixedOffset>,
|
|
pub vendor: String,
|
|
pub amount: Decimal,
|
|
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),
|
|
}
|
|
|
|
#[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 notes = form.notes.clone();
|
|
let filename = form
|
|
.photo
|
|
.raw_name()
|
|
.map(|n| n.dangerous_unsafe_unsanitized_raw().as_str())
|
|
.unwrap_or("photo.jpg")
|
|
.into();
|
|
let stream = form.photo.open().await?;
|
|
let mut reader = BufReader::new(stream);
|
|
let mut photo = Vec::new();
|
|
reader.read_to_end(&mut photo).await?;
|
|
Ok(Self {
|
|
date,
|
|
vendor,
|
|
amount,
|
|
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(())
|
|
}
|
|
}
|