-
Notifications
You must be signed in to change notification settings - Fork 0
api: migrate transaction/budget money types to Decimal #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
api: migrate transaction/budget money types to Decimal #90
Conversation
… and currency_service
…or dynamic SQLx reads for accounts + currency/exchange
…x IF NOT EXISTS for indexes/triggers
Summary of ChangesHello @zensgit, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request primarily focuses on enhancing the precision of financial calculations within the API by migrating monetary data types from Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR migrates transaction and budget money types from f64 to Decimal to eliminate floating-point precision errors. While the core scope focuses on transaction/budget models and services, several auxiliary files also received updates to resolve SQLx offline cache inconsistencies, improve error handling patterns, and fix merge conflicts. The PR includes fixes for SQL query migrations from macro-based to dynamic queries, corrections to closure syntax for unwrap_or_else, and improvements to database migration idempotency.
Key changes:
- Transaction and budget models now use
Decimalfor all monetary amounts - SQL query patterns migrated from
sqlx::query!macro to dynamicsqlx::query()with manual binding to resolve offline cache issues - Error handling improved with more idiomatic Rust patterns
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| jive-api/src/services/exchange_rate_api.rs | Simplified closure syntax for unwrap_or_else with chrono::Utc::now |
| jive-api/src/services/currency_service.rs | Migrated from SQLx macro to dynamic query with manual type extraction; fixed closure syntax |
| jive-api/src/metrics.rs | Fixed redundant ok() call in if let pattern |
| jive-api/src/handlers/currency_handler_enhanced.rs | Migrated SQLx macro query to dynamic query with manual row extraction; fixed deleted closing brace |
| jive-api/src/handlers/accounts.rs | Migrated multiple SQLx macro queries to dynamic queries with manual Decimal type extraction; contains unresolved merge conflict markers |
| jive-api/migrations/037_add_net_worth_tracking.sql | Added idempotent index and trigger creation with IF NOT EXISTS checks |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| <<<<<<< HEAD | ||
| currency, current_balance, available_balance, credit_limit, status, | ||
| is_manual, color, notes, created_at, updated_at | ||
| ======= | ||
| currency, current_balance, available_balance, credit_limit, | ||
| status, is_manual, color, notes, created_at, updated_at | ||
| >>>>>>> 46ef8086 (api: unify Decimal mapping in accounts handler; fix clippy in metrics and currency_service) |
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unresolved merge conflict markers present in the RETURNING clause. These conflict markers must be removed and the correct column list retained before merging.
| <<<<<<< HEAD | |
| currency, current_balance, available_balance, credit_limit, status, | |
| is_manual, color, notes, created_at, updated_at | |
| ======= | |
| currency, current_balance, available_balance, credit_limit, | |
| status, is_manual, color, notes, created_at, updated_at | |
| >>>>>>> 46ef8086 (api: unify Decimal mapping in accounts handler; fix clippy in metrics and currency_service) | |
| currency, current_balance, available_balance, credit_limit, status, | |
| is_manual, color, notes, created_at, updated_at |
| .unwrap_or(None) | ||
| .unwrap_or(true), | ||
| color: row.get("color"), | ||
| icon: row.get("icon"), |
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field 'icon' is not included in the SELECT clause (line 190) but is being extracted from the row. This will cause a runtime error when the query executes.
| .unwrap_or(None) | ||
| .unwrap_or(true), | ||
| color: row.get("color"), | ||
| icon: row.get("icon"), |
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field 'icon' is not included in the SELECT clause of the INSERT...RETURNING statement (lines 276-283) but is being extracted from the row. This will cause a runtime error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request aims to migrate monetary types from f64 to Decimal to improve precision, which is a solid improvement. The changes primarily involve switching from sqlx::query! to sqlx::query() to better handle Decimal types. The updates to the SQL migration script to ensure idempotency are also a great addition. However, there are a couple of significant issues that need to be addressed. First, there are unresolved merge conflict markers in jive-api/src/handlers/accounts.rs which will prevent the code from compiling. Second, a recurring compilation error exists across multiple files where .unwrap_or(None) is incorrectly used on a Result type. This pattern needs to be fixed throughout the PR.
| <<<<<<< HEAD | ||
| currency, current_balance, available_balance, credit_limit, status, | ||
| is_manual, color, notes, created_at, updated_at | ||
| ======= | ||
| currency, current_balance, available_balance, credit_limit, | ||
| status, is_manual, color, notes, created_at, updated_at | ||
| >>>>>>> 46ef8086 (api: unify Decimal mapping in accounts handler; fix clippy in metrics and currency_service) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| let response = AccountResponse { | ||
| id: account.id, | ||
| ledger_id: account.ledger_id, | ||
| bank_id: account.bank_id, | ||
| name: account.name, | ||
| account_type: account.account_type, | ||
| account_number: account.account_number, | ||
| institution_name: account.institution_name, | ||
| currency: account.currency.unwrap_or_else(|| "CNY".to_string()), | ||
| current_balance: account.current_balance.unwrap_or(Decimal::ZERO), | ||
| available_balance: account.available_balance, | ||
| credit_limit: account.credit_limit, | ||
| status: account.status.unwrap_or_else(|| "active".to_string()), | ||
| is_manual: account.is_manual.unwrap_or(true), | ||
| color: account.color, | ||
| icon: None, | ||
| notes: account.notes, | ||
| created_at: account.created_at.unwrap_or_else(chrono::Utc::now), | ||
| updated_at: account.updated_at.unwrap_or_else(chrono::Utc::now), | ||
| id: row.get("id"), | ||
| ledger_id: row.get("ledger_id"), | ||
| bank_id: row.get("bank_id"), | ||
| name: row.get("name"), | ||
| account_type: row.get("account_type"), | ||
| account_number: row.get("account_number"), | ||
| institution_name: row.get("institution_name"), | ||
| currency: row | ||
| .try_get::<Option<String>, _>("currency") | ||
| .unwrap_or(None) | ||
| .unwrap_or_else(|| "CNY".to_string()), | ||
| current_balance: row | ||
| .try_get::<Option<Decimal>, _>("current_balance") | ||
| .unwrap_or(None) | ||
| .unwrap_or(Decimal::ZERO), | ||
| available_balance: row | ||
| .try_get::<Option<Decimal>, _>("available_balance") | ||
| .unwrap_or(None), | ||
| credit_limit: row | ||
| .try_get::<Option<Decimal>, _>("credit_limit") | ||
| .unwrap_or(None), | ||
| status: row | ||
| .try_get::<Option<String>, _>("status") | ||
| .unwrap_or(None) | ||
| .unwrap_or_else(|| "active".to_string()), | ||
| is_manual: row | ||
| .try_get::<Option<bool>, _>("is_manual") | ||
| .unwrap_or(None) | ||
| .unwrap_or(true), | ||
| color: row.get("color"), | ||
| icon: row.get("icon"), | ||
| notes: row.get("notes"), | ||
| created_at: row | ||
| .try_get::<Option<DateTime<Utc>>, _>("created_at") | ||
| .unwrap_or(None) | ||
| .unwrap_or_else(chrono::Utc::now), | ||
| updated_at: row | ||
| .try_get::<Option<DateTime<Utc>>, _>("updated_at") | ||
| .unwrap_or(None) | ||
| .unwrap_or_else(chrono::Utc::now), | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a recurring pattern of using .unwrap_or(None) on the Result returned by try_get. The Result type doesn't have an unwrap_or method; this will cause a compilation error. You might be intending to convert the Result to an Option first. A more idiomatic way to handle this is to use .ok().flatten() to convert Result<Option<T>, E> to Option<T>. This pattern appears multiple times in this function. I've provided a suggestion to fix this for the entire AccountResponse struct initialization.
let response = AccountResponse {
id: row.get("id"),
ledger_id: row.get("ledger_id"),
bank_id: row.get("bank_id"),
name: row.get("name"),
account_type: row.get("account_type"),
account_number: row.get("account_number"),
institution_name: row.get("institution_name"),
currency: row
.try_get::<Option<String>, _>("currency")
.ok()
.flatten()
.unwrap_or_else(|| "CNY".to_string()),
current_balance: row
.try_get::<Option<Decimal>, _>("current_balance")
.ok()
.flatten()
.unwrap_or(Decimal::ZERO),
available_balance: row
.try_get::<Option<Decimal>, _>("available_balance")
.ok()
.flatten(),
credit_limit: row
.try_get::<Option<Decimal>, _>("credit_limit")
.ok()
.flatten(),
status: row
.try_get::<Option<String>, _>("status")
.ok()
.flatten()
.unwrap_or_else(|| "active".to_string()),
is_manual: row
.try_get::<Option<bool>, _>("is_manual")
.ok()
.flatten()
.unwrap_or(true),
color: row.get("color"),
icon: row.get("icon"),
notes: row.get("notes"),
created_at: row
.try_get::<Option<DateTime<Utc>>, _>("created_at")
.ok()
.flatten()
.unwrap_or_else(chrono::Utc::now),
updated_at: row
.try_get::<Option<DateTime<Utc>>, _>("updated_at")
.ok()
.flatten()
.unwrap_or_else(chrono::Utc::now),
};| let response = AccountResponse { | ||
| id: account.id, | ||
| ledger_id: account.ledger_id, | ||
| bank_id: account.bank_id, | ||
| name: account.name, | ||
| account_type: account.account_type, | ||
| account_number: account.account_number, | ||
| institution_name: account.institution_name, | ||
| currency: account.currency.unwrap_or_else(|| "CNY".to_string()), | ||
| current_balance: account.current_balance.unwrap_or(Decimal::ZERO), | ||
| available_balance: account.available_balance, | ||
| credit_limit: account.credit_limit, | ||
| status: account.status.unwrap_or_else(|| "active".to_string()), | ||
| is_manual: account.is_manual.unwrap_or(true), | ||
| color: account.color, | ||
| icon: None, | ||
| notes: account.notes, | ||
| created_at: account.created_at.unwrap_or_else(chrono::Utc::now), | ||
| updated_at: account.updated_at.unwrap_or_else(chrono::Utc::now), | ||
| id: row.get("id"), | ||
| ledger_id: row.get("ledger_id"), | ||
| bank_id: row.get("bank_id"), | ||
| name: row.get("name"), | ||
| account_type: row.get("account_type"), | ||
| account_number: row.get("account_number"), | ||
| institution_name: row.get("institution_name"), | ||
| currency: row | ||
| .try_get::<Option<String>, _>("currency") | ||
| .unwrap_or(None) | ||
| .unwrap_or_else(|| "CNY".to_string()), | ||
| current_balance: row | ||
| .try_get::<Option<Decimal>, _>("current_balance") | ||
| .unwrap_or(None) | ||
| .unwrap_or(Decimal::ZERO), | ||
| available_balance: row | ||
| .try_get::<Option<Decimal>, _>("available_balance") | ||
| .unwrap_or(None), | ||
| credit_limit: row | ||
| .try_get::<Option<Decimal>, _>("credit_limit") | ||
| .unwrap_or(None), | ||
| status: row | ||
| .try_get::<Option<String>, _>("status") | ||
| .unwrap_or(None) | ||
| .unwrap_or_else(|| "active".to_string()), | ||
| is_manual: row | ||
| .try_get::<Option<bool>, _>("is_manual") | ||
| .unwrap_or(None) | ||
| .unwrap_or(true), | ||
| color: row.get("color"), | ||
| icon: row.get("icon"), | ||
| notes: row.get("notes"), | ||
| created_at: row | ||
| .try_get::<Option<DateTime<Utc>>, _>("created_at") | ||
| .unwrap_or(None) | ||
| .unwrap_or_else(chrono::Utc::now), | ||
| updated_at: row | ||
| .try_get::<Option<DateTime<Utc>>, _>("updated_at") | ||
| .unwrap_or(None) | ||
| .unwrap_or_else(chrono::Utc::now), | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the get_account function, there's a recurring compilation error here due to using .unwrap_or(None) on a Result. This should be corrected by using .ok().flatten() to correctly handle the Result<Option<T>, E> type. This applies to all try_get calls in this block.
let response = AccountResponse {
id: row.get("id"),
ledger_id: row.get("ledger_id" ),
bank_id: row.get("bank_id"),
name: row.get("name"),
account_type: row.get("account_type"),
account_number: row.get("account_number"),
institution_name: row.get("institution_name"),
currency: row
.try_get::<Option<String>, _>("currency")
.ok()
.flatten()
.unwrap_or_else(|| "CNY".to_string()),
current_balance: row
.try_get::<Option<Decimal>, _>("current_balance")
.ok()
.flatten()
.unwrap_or(Decimal::ZERO),
available_balance: row
.try_get::<Option<Decimal>, _>("available_balance")
.ok()
.flatten(),
credit_limit: row
.try_get::<Option<Decimal>, _>("credit_limit")
.ok()
.flatten(),
status: row
.try_get::<Option<String>, _>("status")
.ok()
.flatten()
.unwrap_or_else(|| "active".to_string()),
is_manual: row
.try_get::<Option<bool>, _>("is_manual")
.ok()
.flatten()
.unwrap_or(true),
color: row.get("color"),
icon: row.get("icon"),
notes: row.get("notes"),
created_at: row
.try_get::<Option<DateTime<Utc>>, _>("created_at")
.ok()
.flatten()
.unwrap_or_else(chrono::Utc::now),
updated_at: row
.try_get::<Option<DateTime<Utc>>, _>("updated_at")
.ok()
.flatten()
.unwrap_or_else(chrono::Utc::now),
};| let created_naive = row | ||
| .try_get::<Option<chrono::DateTime<Utc>>, _>("created_at") | ||
| .unwrap_or(None) | ||
| .map(|dt| dt.naive_utc()) | ||
| .unwrap_or_else(|| Utc::now().naive_utc()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of .unwrap_or(None) on the Result from try_get will cause a compilation error. You should use .ok().flatten() to convert the Result<Option<T>> to an Option<T> before calling .map().
let created_naive = row
.try_get::<Option<chrono::DateTime<Utc>>, _>("created_at")
.ok()
.flatten()
.map(|dt| dt.naive_utc())
.unwrap_or_else(|| Utc::now().naive_utc());| let base_currency = row | ||
| .try_get::<Option<String>, _>("base_currency") | ||
| .unwrap_or(None) | ||
| .unwrap_or_else(|| "CNY".to_string()); | ||
| let allow_multi_currency = row | ||
| .try_get::<Option<bool>, _>("allow_multi_currency") | ||
| .unwrap_or(None) | ||
| .unwrap_or(false); | ||
| let auto_convert = row | ||
| .try_get::<Option<bool>, _>("auto_convert") | ||
| .unwrap_or(None) | ||
| .unwrap_or(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block contains the same compilation error as seen in other files, using .unwrap_or(None) on a Result. Please use .ok().flatten() to correctly chain Option methods.
let base_currency = row
.try_get::<Option<String>, _>("base_currency")
.ok()
.flatten()
.unwrap_or_else(|| "CNY".to_string());
let allow_multi_currency = row
.try_get::<Option<bool>, _>("allow_multi_currency")
.ok()
.flatten()
.unwrap_or(false);
let auto_convert = row
.try_get::<Option<bool>, _>("auto_convert")
.ok()
.flatten()
.unwrap_or(false);* api: unify Decimal mapping in accounts handler; fix clippy in metrics and currency_service * api: fix Decimal/f64 mismatches and Option<DateTime> handling; refactor dynamic SQLx reads for accounts + currency/exchange * api/migrations: make net worth tables idempotent; ensure pgcrypto; fix IF NOT EXISTS for indexes/triggers * chore: workspace sync before merging base into PR branch * tests: remove WIP contract serialization test from this PR
Purpose
Scope
DB Compatibility
Validation
make api-sqlx-prepare-local; check passesSQLX_OFFLINE=true cargo clippy -- -D warningspassesSQLX_OFFLINE=true cargo test --testspasses (34/34)Out of scope (future PRs)
Rollback plan