From 5c7225f077531a22346c8ab530fa4fe8b5b23fd5 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Tue, 11 Mar 2025 20:05:41 -0500 Subject: [PATCH] Refactor backend * Reorganizing code into more logical modules: - `routes` specifically for Rocket handler functions - `receipts` data model for receipts - `transactions` for Firefly transactions * Encapsulate database operations for receipts using the repository pattern; move SQL queries to external files (`sqlx` can only use string literals or external files for queries, not variables or constants) * Remove obsolete routes, templates for old transaction-focused pages --- .containerignore | 1 + ...cbcfc9b0f084b755d0d9f065dbfe31c53679d.json | 27 -- ...bb6fda45b0ebd5244309472921a934e1b829.json} | 4 +- ...a49f5cd14598e029d6bacfeb2326daf142119.json | 57 ++++ ...cceef07851bbe9b429fd62b9d02bf01f52e1.json} | 4 +- Containerfile | 1 + sql/receipts/get-receipt.sql | 6 + sql/receipts/insert-receipt.sql | 7 + sql/receipts/list-receipts.sql | 5 + src/main.rs | 241 +--------------- src/receipts.rs | 269 +++++------------- src/routes/mod.rs | 2 + src/routes/receipts.rs | 137 +++++++++ src/routes/transactions.rs | 41 +++ src/transactions.rs | 40 +++ 15 files changed, 380 insertions(+), 462 deletions(-) delete mode 100644 .sqlx/query-16d204cf990ebd5f78cfe21a5a9cbcfc9b0f084b755d0d9f065dbfe31c53679d.json rename .sqlx/{query-e62b8704ebb897be63463427ba9e808ddb9c7fb5d3326b388d5e7f1066732aa9.json => query-71dcdc6a24d99eff2dd7af673a0ebb6fda45b0ebd5244309472921a934e1b829.json} (81%) create mode 100644 .sqlx/query-a3fb24d192843a656565cebcc78a49f5cd14598e029d6bacfeb2326daf142119.json rename .sqlx/{query-9fe465619a9818b201fbf091018a75eea38e846468d1877062db8c5606550a13.json => query-e173b8a98dcdf9e4b259ae59e0ddcceef07851bbe9b429fd62b9d02bf01f52e1.json} (81%) create mode 100644 sql/receipts/get-receipt.sql create mode 100644 sql/receipts/insert-receipt.sql create mode 100644 sql/receipts/list-receipts.sql create mode 100644 src/routes/mod.rs create mode 100644 src/routes/receipts.rs create mode 100644 src/routes/transactions.rs create mode 100644 src/transactions.rs diff --git a/.containerignore b/.containerignore index dee510c..dc5cdeb 100644 --- a/.containerignore +++ b/.containerignore @@ -4,4 +4,5 @@ !js/ !migrations/ !src/ +!sql/ !templates/ diff --git a/.sqlx/query-16d204cf990ebd5f78cfe21a5a9cbcfc9b0f084b755d0d9f065dbfe31c53679d.json b/.sqlx/query-16d204cf990ebd5f78cfe21a5a9cbcfc9b0f084b755d0d9f065dbfe31c53679d.json deleted file mode 100644 index fe4859e..0000000 --- a/.sqlx/query-16d204cf990ebd5f78cfe21a5a9cbcfc9b0f084b755d0d9f065dbfe31c53679d.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nINSERT INTO receipts (\n vendor, date, amount, notes, filename, image\n) VALUES (\n$1, $2, $3, $4, $5, $6\n)\nRETURNING id\n", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Timestamptz", - "Numeric", - "Text", - "Varchar", - "Bytea" - ] - }, - "nullable": [ - false - ] - }, - "hash": "16d204cf990ebd5f78cfe21a5a9cbcfc9b0f084b755d0d9f065dbfe31c53679d" -} diff --git a/.sqlx/query-e62b8704ebb897be63463427ba9e808ddb9c7fb5d3326b388d5e7f1066732aa9.json b/.sqlx/query-71dcdc6a24d99eff2dd7af673a0ebb6fda45b0ebd5244309472921a934e1b829.json similarity index 81% rename from .sqlx/query-e62b8704ebb897be63463427ba9e808ddb9c7fb5d3326b388d5e7f1066732aa9.json rename to .sqlx/query-71dcdc6a24d99eff2dd7af673a0ebb6fda45b0ebd5244309472921a934e1b829.json index 71dbf92..f6bfb68 100644 --- a/.sqlx/query-e62b8704ebb897be63463427ba9e808ddb9c7fb5d3326b388d5e7f1066732aa9.json +++ b/.sqlx/query-71dcdc6a24d99eff2dd7af673a0ebb6fda45b0ebd5244309472921a934e1b829.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT\n id, vendor, date, amount, notes, filename\nFROM\n receipts\nORDER BY date\n", + "query": "SELECT\n id, vendor, date, amount, notes, filename\nFROM\n receipts\nORDER BY date\n", "describe": { "columns": [ { @@ -46,5 +46,5 @@ false ] }, - "hash": "e62b8704ebb897be63463427ba9e808ddb9c7fb5d3326b388d5e7f1066732aa9" + "hash": "71dcdc6a24d99eff2dd7af673a0ebb6fda45b0ebd5244309472921a934e1b829" } diff --git a/.sqlx/query-a3fb24d192843a656565cebcc78a49f5cd14598e029d6bacfeb2326daf142119.json b/.sqlx/query-a3fb24d192843a656565cebcc78a49f5cd14598e029d6bacfeb2326daf142119.json new file mode 100644 index 0000000..d0ba457 --- /dev/null +++ b/.sqlx/query-a3fb24d192843a656565cebcc78a49f5cd14598e029d6bacfeb2326daf142119.json @@ -0,0 +1,57 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO receipts (\n vendor, date, amount, notes, filename, image\n) VALUES (\n $1, $2, $3, $4, $5, $6\n)\nRETURNING\n id, vendor, date, amount, notes, filename\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "vendor", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "date", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "amount", + "type_info": "Numeric" + }, + { + "ordinal": 4, + "name": "notes", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "filename", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Timestamptz", + "Numeric", + "Text", + "Varchar", + "Bytea" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false + ] + }, + "hash": "a3fb24d192843a656565cebcc78a49f5cd14598e029d6bacfeb2326daf142119" +} diff --git a/.sqlx/query-9fe465619a9818b201fbf091018a75eea38e846468d1877062db8c5606550a13.json b/.sqlx/query-e173b8a98dcdf9e4b259ae59e0ddcceef07851bbe9b429fd62b9d02bf01f52e1.json similarity index 81% rename from .sqlx/query-9fe465619a9818b201fbf091018a75eea38e846468d1877062db8c5606550a13.json rename to .sqlx/query-e173b8a98dcdf9e4b259ae59e0ddcceef07851bbe9b429fd62b9d02bf01f52e1.json index 8cfee24..d066e89 100644 --- a/.sqlx/query-9fe465619a9818b201fbf091018a75eea38e846468d1877062db8c5606550a13.json +++ b/.sqlx/query-e173b8a98dcdf9e4b259ae59e0ddcceef07851bbe9b429fd62b9d02bf01f52e1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT\n id, vendor, date, amount, notes, filename\nFROM\n receipts\nWHERE\n id = $1\n", + "query": "SELECT\n id, vendor, date, amount, notes, filename\nFROM\n receipts\nWHERE\n id = $1\n", "describe": { "columns": [ { @@ -48,5 +48,5 @@ false ] }, - "hash": "9fe465619a9818b201fbf091018a75eea38e846468d1877062db8c5606550a13" + "hash": "e173b8a98dcdf9e4b259ae59e0ddcceef07851bbe9b429fd62b9d02bf01f52e1" } diff --git a/Containerfile b/Containerfile index ccd7a6b..838340a 100644 --- a/Containerfile +++ b/Containerfile @@ -14,6 +14,7 @@ COPY Cargo.* . COPY src src COPY .sqlx .sqlx COPY migrations migrations +COPY sql sql RUN --mount=type=cache,target=/root/.cargo \ cargo build --release --locked diff --git a/sql/receipts/get-receipt.sql b/sql/receipts/get-receipt.sql new file mode 100644 index 0000000..06b8d9b --- /dev/null +++ b/sql/receipts/get-receipt.sql @@ -0,0 +1,6 @@ +SELECT + id, vendor, date, amount, notes, filename +FROM + receipts +WHERE + id = $1 diff --git a/sql/receipts/insert-receipt.sql b/sql/receipts/insert-receipt.sql new file mode 100644 index 0000000..ce1eff4 --- /dev/null +++ b/sql/receipts/insert-receipt.sql @@ -0,0 +1,7 @@ +INSERT INTO receipts ( + vendor, date, amount, notes, filename, image +) VALUES ( + $1, $2, $3, $4, $5, $6 +) +RETURNING + id, vendor, date, amount, notes, filename diff --git a/sql/receipts/list-receipts.sql b/sql/receipts/list-receipts.sql new file mode 100644 index 0000000..e148802 --- /dev/null +++ b/sql/receipts/list-receipts.sql @@ -0,0 +1,5 @@ +SELECT + id, vendor, date, amount, notes, filename +FROM + receipts +ORDER BY date diff --git a/src/main.rs b/src/main.rs index b5b9379..ae56083 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,21 @@ mod config; mod firefly; mod receipts; +mod routes; +mod transactions; -use chrono::{DateTime, FixedOffset}; use rocket::fairing::{self, AdHoc}; -use rocket::form::Form; -use rocket::fs::{FileServer, TempFile}; -use rocket::http::Status; +use rocket::fs::FileServer; use rocket::response::{Redirect, Responder}; use rocket::serde::json::Json; -use rocket::tokio::io::{AsyncReadExt, BufReader}; -use rocket::{Rocket, State}; +use rocket::Rocket; use rocket_db_pools::Database as RocketDatabase; -use rocket_dyn_templates::{context, Template}; +use rocket_dyn_templates::Template; use serde::Serialize; use tracing::{debug, error, info}; use config::Config; -use firefly::{ - Firefly, TransactionRead, TransactionSplitUpdate, TransactionUpdate, -}; +use firefly::Firefly; struct Context { #[allow(dead_code)] @@ -46,21 +42,6 @@ impl Context { #[database("receipts")] struct Database(rocket_db_pools::sqlx::PgPool); -#[derive(Serialize)] -pub struct Transaction { - pub id: String, - pub amount: f64, - pub description: String, - pub date: DateTime, -} - -#[derive(rocket::FromForm)] -struct TransactionPostData<'r> { - amount: f64, - notes: String, - photo: Vec>, -} - #[derive(Serialize)] struct ErrorBody { error: String, @@ -82,207 +63,11 @@ impl ErrorResponse { } } -impl TryFrom for Transaction { - type Error = &'static str; - - fn try_from(t: TransactionRead) -> Result { - let first_split = match t.attributes.transactions.first() { - Some(t) => t, - None => { - error!("Invalid transaction {}: no splits", t.id); - return Err("Transaction has no splits"); - }, - }; - let date = first_split.date; - let amount = t.amount(); - let description = if let Some(title) = &t.attributes.group_title { - title.into() - } else { - first_split.description.clone() - }; - Ok(Self { - id: t.id, - amount, - description, - date, - }) - } -} - #[rocket::get("/")] async fn index() -> Redirect { Redirect::to("/receipts") } -#[rocket::get("/transactions", format = "html", rank = 2)] -async fn transaction_list(ctx: &State) -> (Status, Template) { - let result = ctx - .firefly - .search_transactions(&ctx.config.firefly.search_query) - .await; - - match result { - Ok(r) => { - let transactions: Vec<_> = r - .data - .into_iter() - .filter_map(|t| match Transaction::try_from(t) { - Ok(t) => Some(t), - Err(e) => { - error!("Error parsing transaction details: {}", e); - None - }, - }) - .collect(); - ( - Status::Ok, - Template::render( - "transaction-list", - context! { - transactions: transactions, - }, - ), - ) - }, - Err(e) => { - error!("Error fetching transaction list: {}", e); - ( - Status::InternalServerError, - Template::render( - "error", - context! { - error: "Failed to fetch transaction list from Firefly III", - }, - ), - ) - }, - } -} - -#[rocket::get("/transactions", format = "json")] -async fn transaction_list_json( - ctx: &State, -) -> Result>, ErrorResponse> { - let result = ctx - .firefly - .search_transactions(&ctx.config.firefly.search_query) - .await - .map_err(|e| { - error!("Error fetching transaction list: {}", e); - ErrorResponse::new( - "Failed to fetch transaction list from Firefly III", - ) - })?; - - Ok(Json( - result - .data - .into_iter() - .filter_map(|t| match Transaction::try_from(t) { - Ok(t) => Some(t), - Err(e) => { - error!("Error parsing transaction details: {}", e); - None - }, - }) - .collect(), - )) -} - -#[rocket::get("/transactions/")] -async fn get_transaction(id: &str, ctx: &State) -> Option