Skip to main content

web/pages/transaction/
validate.rs

1//web/src/pages/transaction/validate.rs - Shared validation logic for transaction operations
2
3use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse};
4use serde::Deserialize;
5use server::command::{CmdResult, FinanceEntity, account::ListAccounts};
6use sqlx::types::Uuid;
7use std::sync::Arc;
8
9use crate::{
10    AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate,
11    pages::validation::feedback::ValidationFeedback,
12};
13
14#[derive(Deserialize, Debug)]
15pub struct ValidateAmountRequest {
16    pub amount: Option<String>,
17}
18
19#[derive(Deserialize, Debug)]
20pub struct ValidateAccountRequest {
21    pub account_id: Option<String>,
22}
23
24#[derive(Deserialize, Debug)]
25pub struct ValidateFromAccountRequest {
26    pub from_account: Option<String>,
27}
28
29#[derive(Deserialize, Debug)]
30pub struct ValidateToAccountRequest {
31    pub to_account: Option<String>,
32}
33
34#[derive(Deserialize, Debug)]
35pub struct ValidateNoteRequest {
36    pub note: Option<String>,
37}
38
39/// Validation result type that can be converted to either HTML or JSON response
40#[derive(Debug)]
41pub enum ValidationResult {
42    Success(String),
43    Error(String),
44}
45
46impl ValidationResult {
47    #[must_use]
48    pub fn to_html_response(self) -> impl IntoResponse {
49        match self {
50            ValidationResult::Success(msg) => HtmlTemplate(ValidationFeedback::success(msg)),
51            ValidationResult::Error(msg) => HtmlTemplate(ValidationFeedback::error(msg)),
52        }
53    }
54
55    pub fn to_json_response(
56        self,
57    ) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
58        match self {
59            ValidationResult::Success(msg) => Ok(msg),
60            ValidationResult::Error(msg) => {
61                let error_response = serde_json::json!({
62                    "status": "fail",
63                    "message": msg,
64                });
65                Err((StatusCode::BAD_REQUEST, Json(error_response)))
66            }
67        }
68    }
69}
70
71/// Core validation logic for amounts
72#[must_use]
73pub fn validate_amount_logic(amount_str: Option<&str>) -> ValidationResult {
74    let amount_str = match amount_str {
75        Some(s) if !s.trim().is_empty() => s.trim(),
76        _ => return ValidationResult::Error(t!("Amount is required").to_string()),
77    };
78
79    match amount_str.parse::<f64>() {
80        Ok(num) => {
81            if num <= 0.0 {
82                ValidationResult::Error(t!("Amount must be greater than zero").to_string())
83            } else {
84                ValidationResult::Success(t!("Valid amount").to_string())
85            }
86        }
87        Err(_) => ValidationResult::Error(t!("Please enter a valid number").to_string()),
88    }
89}
90
91/// Core validation logic for account IDs
92pub async fn validate_account_logic(
93    user_id: Uuid,
94    account_id_str: Option<&str>,
95    field_name: &str,
96) -> ValidationResult {
97    let account_id_str = match account_id_str {
98        Some(s) if !s.trim().is_empty() => s.trim(),
99        _ => return ValidationResult::Error(format!("{field_name} is required")),
100    };
101
102    // Verify UUID format
103    let account_uuid = match Uuid::parse_str(account_id_str) {
104        Ok(uuid) => uuid,
105        Err(_) => return ValidationResult::Error(t!("Invalid account ID format").to_string()),
106    };
107
108    // Verify that the account exists and belongs to the user
109    match ListAccounts::new().user_id(user_id).run().await {
110        Ok(Some(CmdResult::TaggedEntities { entities, .. })) => {
111            for (entity, _) in entities {
112                if let FinanceEntity::Account(account) = entity
113                    && account.id == account_uuid
114                {
115                    return ValidationResult::Success(t!("Valid account").to_string());
116                }
117            }
118            ValidationResult::Error(t!("Account not found or not accessible").to_string())
119        }
120        _ => ValidationResult::Error(t!("Error validating account").to_string()),
121    }
122}
123
124/// Core validation logic for notes
125#[must_use]
126pub fn validate_note_logic(note: Option<&str>) -> ValidationResult {
127    match note {
128        Some(note_str) if note_str.len() > 500 => {
129            ValidationResult::Error(t!("Note is too long (max 500 characters)").to_string())
130        }
131        _ => ValidationResult::Success(t!("Note is valid").to_string()),
132    }
133}
134
135// HTML-based validation endpoints (for create transaction)
136pub async fn validate_amount_html(Json(form): Json<ValidateAmountRequest>) -> impl IntoResponse {
137    let result = validate_amount_logic(form.amount.as_deref());
138    result.to_html_response()
139}
140
141pub async fn validate_from_account_html(
142    State(_data): State<Arc<AppState>>,
143    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
144    Json(form): Json<ValidateFromAccountRequest>,
145) -> impl IntoResponse {
146    let user = &jwt_auth.user;
147    let result =
148        validate_account_logic(user.id, form.from_account.as_deref(), "Source account").await;
149    result.to_html_response()
150}
151
152pub async fn validate_to_account_html(
153    State(_data): State<Arc<AppState>>,
154    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
155    Json(form): Json<ValidateToAccountRequest>,
156) -> impl IntoResponse {
157    let user = &jwt_auth.user;
158    let result =
159        validate_account_logic(user.id, form.to_account.as_deref(), "Destination account").await;
160    result.to_html_response()
161}
162
163pub async fn validate_note_html(Json(form): Json<ValidateNoteRequest>) -> impl IntoResponse {
164    let result = validate_note_logic(form.note.as_deref());
165    result.to_html_response()
166}
167
168// JSON-based validation endpoints (for edit transaction)
169pub async fn validate_amount_json(
170    State(_data): State<Arc<AppState>>,
171    Extension(_jwt_auth): Extension<JWTAuthMiddleware>,
172    Json(form): Json<ValidateAmountRequest>,
173) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
174    let result = validate_amount_logic(form.amount.as_deref());
175    result.to_json_response()
176}
177
178pub async fn validate_from_account_json(
179    State(_data): State<Arc<AppState>>,
180    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
181    Json(form): Json<ValidateAccountRequest>,
182) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
183    let user = &jwt_auth.user;
184    let result = validate_account_logic(user.id, form.account_id.as_deref(), "From account").await;
185    result.to_json_response()
186}
187
188pub async fn validate_to_account_json(
189    State(_data): State<Arc<AppState>>,
190    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
191    Json(form): Json<ValidateAccountRequest>,
192) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
193    let user = &jwt_auth.user;
194    let result = validate_account_logic(user.id, form.account_id.as_deref(), "To account").await;
195    result.to_json_response()
196}
197
198pub async fn validate_note_json(
199    State(_data): State<Arc<AppState>>,
200    Extension(_jwt_auth): Extension<JWTAuthMiddleware>,
201    Json(form): Json<ValidateNoteRequest>,
202) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
203    let result = validate_note_logic(form.note.as_deref());
204    result.to_json_response()
205}