1
//web/src/pages/transaction/validate.rs - Shared validation logic for transaction operations
2

            
3
use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse};
4
use serde::Deserialize;
5
use server::command::{CmdResult, FinanceEntity, account::ListAccounts};
6
use sqlx::types::Uuid;
7
use std::sync::Arc;
8

            
9
use crate::{
10
    AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate,
11
    pages::validation::feedback::ValidationFeedback,
12
};
13

            
14
#[derive(Deserialize, Debug)]
15
pub struct ValidateAmountRequest {
16
    pub amount: Option<String>,
17
}
18

            
19
#[derive(Deserialize, Debug)]
20
pub struct ValidateAccountRequest {
21
    pub account_id: Option<String>,
22
}
23

            
24
#[derive(Deserialize, Debug)]
25
pub struct ValidateFromAccountRequest {
26
    pub from_account: Option<String>,
27
}
28

            
29
#[derive(Deserialize, Debug)]
30
pub struct ValidateToAccountRequest {
31
    pub to_account: Option<String>,
32
}
33

            
34
#[derive(Deserialize, Debug)]
35
pub 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)]
41
pub enum ValidationResult {
42
    Success(String),
43
    Error(String),
44
}
45

            
46
impl ValidationResult {
47
    #[must_use]
48
3
    pub fn to_html_response(self) -> impl IntoResponse {
49
3
        match self {
50
            ValidationResult::Success(msg) => HtmlTemplate(ValidationFeedback::success(msg)),
51
3
            ValidationResult::Error(msg) => HtmlTemplate(ValidationFeedback::error(msg)),
52
        }
53
3
    }
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]
73
pub 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
92
3
pub async fn validate_account_logic(
93
3
    user_id: Uuid,
94
3
    account_id_str: Option<&str>,
95
3
    field_name: &str,
96
3
) -> ValidationResult {
97
3
    let account_id_str = match account_id_str {
98
3
        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
3
    let account_uuid = match Uuid::parse_str(account_id_str) {
104
2
        Ok(uuid) => uuid,
105
1
        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
2
    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
2
        _ => ValidationResult::Error(t!("Error validating account").to_string()),
121
    }
122
3
}
123

            
124
/// Core validation logic for notes
125
#[must_use]
126
pub 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)
136
pub 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

            
141
2
pub async fn validate_from_account_html(
142
2
    State(_data): State<Arc<AppState>>,
143
2
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
144
2
    Json(form): Json<ValidateFromAccountRequest>,
145
2
) -> impl IntoResponse {
146
2
    let user = &jwt_auth.user;
147
2
    let result =
148
2
        validate_account_logic(user.id, form.from_account.as_deref(), "Source account").await;
149
2
    result.to_html_response()
150
2
}
151

            
152
1
pub async fn validate_to_account_html(
153
1
    State(_data): State<Arc<AppState>>,
154
1
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
155
1
    Json(form): Json<ValidateToAccountRequest>,
156
1
) -> impl IntoResponse {
157
1
    let user = &jwt_auth.user;
158
1
    let result =
159
1
        validate_account_logic(user.id, form.to_account.as_deref(), "Destination account").await;
160
1
    result.to_html_response()
161
1
}
162

            
163
pub 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)
169
pub 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

            
178
pub 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

            
188
pub 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

            
198
pub 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
}