Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions jive-api/src/models/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
Expand All @@ -9,7 +10,7 @@ pub struct Transaction {
pub ledger_id: Uuid,
pub account_id: Uuid,
pub transaction_date: DateTime<Utc>,
pub amount: f64,
pub amount: Decimal,
pub transaction_type: TransactionType,
pub category_id: Option<Uuid>,
pub category_name: Option<String>,
Expand Down Expand Up @@ -45,7 +46,7 @@ pub struct TransactionCreate {
pub ledger_id: Uuid,
pub account_id: Uuid,
pub transaction_date: DateTime<Utc>,
pub amount: f64,
pub amount: Decimal,
pub transaction_type: TransactionType,
pub category_id: Option<Uuid>,
pub category_name: Option<String>,
Expand All @@ -58,7 +59,7 @@ pub struct TransactionCreate {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionUpdate {
pub transaction_date: Option<DateTime<Utc>>,
pub amount: Option<f64>,
pub amount: Option<Decimal>,
pub transaction_type: Option<TransactionType>,
pub category_id: Option<Uuid>,
pub category_name: Option<String>,
Expand Down
9 changes: 5 additions & 4 deletions jive-api/src/services/transaction_service.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::error::{ApiError, ApiResult};
use crate::models::transaction::{Transaction, TransactionCreate, TransactionType};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While Decimal has been correctly introduced for many parts of the service, the migration is incomplete in this file. There are still functions using f64 for monetary values, which could lead to precision errors and runtime bugs.

To complete the migration, please update the following:

  1. auto_categorize function: The transaction amount is still fetched as an f64 around line 284. This should be Decimal.

    // Around line 284
    let transaction: Option<(String, Option<String>, f64)> =
        sqlx::query_as(...)

    This should be changed to:

    let transaction: Option<(String, Option<String>, Decimal)> =
        sqlx::query_as(...)
  2. TransactionStatistics struct: This struct (around line 383) and its usage in get_statistics still rely on f64. All monetary fields should be Decimal.

    // Around line 383
    pub struct TransactionStatistics {
        pub total_income: Option<f64>,
        pub total_expense: Option<f64>,
        pub net_amount: Option<f64>,
        pub avg_expense: Option<f64>,
        pub max_expense: Option<f64>,
        // ...
    }

    This should be updated to use Option<Decimal> for all monetary fields.

Ensuring Decimal is used consistently is critical for the correctness of financial calculations.

use sqlx::PgPool;
use std::collections::HashMap;
use uuid::Uuid;
Expand Down Expand Up @@ -28,7 +29,7 @@ impl TransactionService {
let data_snapshot = data.clone();

// 获取账户当前余额
let current_balance: Option<(f64,)> =
let current_balance: Option<(Decimal,)> =
sqlx::query_as("SELECT current_balance FROM accounts WHERE id = $1 FOR UPDATE")
.bind(data.account_id)
.fetch_optional(&mut *tx)
Expand Down Expand Up @@ -127,7 +128,7 @@ impl TransactionService {
target_account_id: Uuid,
) -> ApiResult<()> {
// 获取目标账户余额
let target_balance: Option<(f64,)> =
let target_balance: Option<(Decimal,)> =
sqlx::query_as("SELECT current_balance FROM accounts WHERE id = $1 FOR UPDATE")
.bind(target_account_id)
.fetch_optional(&mut **tx)
Expand Down Expand Up @@ -190,14 +191,14 @@ impl TransactionService {
.map_err(|e| ApiError::DatabaseError(e.to_string()))?;

let mut created_transactions = Vec::new();
let mut account_balances: HashMap<Uuid, f64> = HashMap::new();
let mut account_balances: HashMap<Uuid, Decimal> = HashMap::new();

// 预加载所有相关账户的余额
for trans in &transactions {
if let std::collections::hash_map::Entry::Vacant(e) =
account_balances.entry(trans.account_id)
{
let balance: Option<(f64,)> =
let balance: Option<(Decimal,)> =
sqlx::query_as("SELECT current_balance FROM accounts WHERE id = $1")
.bind(trans.account_id)
.fetch_optional(&mut *tx)
Expand Down
31 changes: 31 additions & 0 deletions jive-api/tests/contract_decimal_transaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use chrono::{TimeZone, Utc};
use rust_decimal::Decimal;
use serde_json::Value;

#[test]
fn transaction_decimal_amount_serializes_as_string() {
use jive_money_api::models::transaction::{Transaction, TransactionStatus, TransactionType};
use uuid::Uuid;

let tx = Transaction {
id: Uuid::nil(),
ledger_id: Uuid::nil(),
account_id: Uuid::nil(),
transaction_date: Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
amount: Decimal::new(12345, 2), // 123.45
transaction_type: TransactionType::Income,
category_id: None,
category_name: Some("Salary".to_string()),
payee: Some("Company".to_string()),
notes: None,
status: TransactionStatus::Cleared,
related_transaction_id: None,
created_at: Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
updated_at: Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
};

let val: Value = serde_json::to_value(&tx).expect("serialize transaction");
assert!(val.get("amount").and_then(|v| v.as_str()).is_some(), "amount should be string");
assert_eq!(val["amount"].as_str().unwrap(), "123.45");
}