second commit

This commit is contained in:
2025-07-04 12:11:26 +08:00
parent 0cf47db3e8
commit 19ac57c338
6 changed files with 42 additions and 71 deletions

View File

@@ -1,11 +1,10 @@
use sqlx::PgPool;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
use sqlx::{PgPool, Postgres, Transaction};
use tracing::info;
pub type DbPool = PgPool;
pub type DbTransaction<'a> = Transaction<'a, Postgres>;
pub(crate) type DbPool = PgPool;
pub async fn create_pool() -> Result<DbPool, sqlx::Error> {
pub(crate) async fn create_pool() -> Result<DbPool, sqlx::Error> {
info!("Creating database connection pool...");
PgPoolOptions::new()
.max_connections(5)
@@ -13,7 +12,7 @@ pub async fn create_pool() -> Result<DbPool, sqlx::Error> {
.await
}
pub async fn run_migrations(pool: &DbPool) -> Result<(), sqlx::migrate::MigrateError> {
pub(crate) async fn run_migrations(pool: &DbPool) -> Result<(), sqlx::migrate::MigrateError> {
info!("Running database migrations...");
sqlx::migrate!("./migrations").run(pool).await
}

View File

@@ -6,7 +6,7 @@ use sqlx::Error as SqlxError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
pub(crate) enum AppError {
#[error("Account not found: {0}")]
AccountNotFound(i64),
#[error("Account already exists")]

View File

@@ -6,7 +6,7 @@ use axum::{
};
use bigdecimal::BigDecimal;
use sqlx::types::BigDecimal as SqlxBigDecimal;
use tracing::info;
use tracing::debug;
use crate::{
db::DbPool,
@@ -14,7 +14,7 @@ use crate::{
models::{Account, CreateAccount, TransactionRequest},
};
pub async fn create_account(
pub(crate) async fn create_account(
State(pool): State<DbPool>,
Json(payload): Json<CreateAccount>,
) -> Result<(), AppError> {
@@ -29,7 +29,7 @@ pub async fn create_account(
"initial_balance is negative",
)));
}
info!("Creating account: {:?}", payload.account_id);
debug!("Creating account: {:?}", payload.account_id);
sqlx::query(
r#"
INSERT INTO accounts (account_id, balance)
@@ -37,7 +37,7 @@ pub async fn create_account(
"#,
)
.bind(payload.account_id)
.bind(SqlxBigDecimal::from(payload.initial_balance))
.bind(payload.initial_balance)
.execute(&pool)
.await
.map_err(|err| match err {
@@ -48,7 +48,7 @@ pub async fn create_account(
Ok(())
}
pub async fn get_account(
pub(crate) async fn get_account(
State(pool): State<DbPool>,
Path(account_id): Path<i64>,
) -> Result<Json<Account>, AppError> {
@@ -57,7 +57,7 @@ pub async fn get_account(
"account_id is invalid",
)));
}
info!("Fetching account: {}", account_id);
debug!("Fetching account: {}", account_id);
let account = sqlx::query_as::<_, Account>(
r#"
@@ -74,7 +74,7 @@ pub async fn get_account(
Ok(Json(account))
}
pub async fn process_transaction(
pub(crate) async fn process_transaction(
State(pool): State<DbPool>,
Json(payload): Json<TransactionRequest>,
) -> Result<(), AppError> {
@@ -95,10 +95,6 @@ pub async fn process_transaction(
"Minimum amount is 0.01",
)));
}
info!(
"Processing transaction from {} to {} for amount {}",
payload.source_account_id, payload.destination_account_id, payload.amount
);
let mut tx = pool.begin().await.map_err(AppError::from)?;
@@ -113,7 +109,7 @@ pub async fn process_transaction(
.await
.map_err(|_| AppError::AccountNotFound(payload.source_account_id))?;
if source_balance < SqlxBigDecimal::from(payload.amount.clone()) {
if source_balance < payload.amount.clone() {
return Err(AppError::InsufficientFunds);
}
@@ -125,7 +121,7 @@ pub async fn process_transaction(
WHERE account_id = $2
"#,
)
.bind(SqlxBigDecimal::from(payload.amount.clone()))
.bind(payload.amount.clone())
.bind(payload.source_account_id)
.execute(&mut *tx)
.await
@@ -138,7 +134,7 @@ pub async fn process_transaction(
WHERE account_id = $2
"#,
)
.bind(SqlxBigDecimal::from(payload.amount.clone()))
.bind(payload.amount.clone())
.bind(payload.destination_account_id)
.execute(&mut *tx)
.await
@@ -156,11 +152,15 @@ pub async fn process_transaction(
)
.bind(payload.source_account_id)
.bind(payload.destination_account_id)
.bind(SqlxBigDecimal::from(payload.amount.clone()))
.bind(payload.amount.clone())
.execute(&mut *tx)
.await
.map_err(AppError::from)?;
debug!(
"Processing transaction from {} to {} for amount {}",
payload.source_account_id, payload.destination_account_id, payload.amount
);
tx.commit().await.map_err(AppError::from)?;
Ok(())

View File

@@ -1,10 +1,12 @@
use std::env;
use axum::{
Router,
routing::{get, post},
};
use dotenv::dotenv;
use tracing::Level;
use tracing_subscriber::{FmtSubscriber, layer::SubscriberExt, util::SubscriberInitExt};
use tracing::{Level, info};
use tracing_subscriber::FmtSubscriber;
mod db;
mod errors;
@@ -13,12 +15,14 @@ mod models;
#[tokio::main]
async fn main() {
//Parse .env file and add the environmental variables configured there
dotenv().ok();
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::TRACE)
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("Setting default tracing subscriber failed");
dotenv().ok();
let pool = db::create_pool().await.expect("Failed to create pool");
@@ -27,15 +31,19 @@ async fn main() {
.await
.expect("Failed to run migrations");
// Build our application's routes
// Build the application's routes
let app = Router::new()
.route("/accounts", post(handlers::create_account))
.route("/accounts/{account_id}", get(handlers::get_account))
.route("/transactions", post(handlers::process_transaction))
.with_state(pool);
// Run our app
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
// Run the server
let host = env::var("INTRASYS_HOST").unwrap_or(String::from("127.0.0.1"));
let port = env::var("INTRASYS_PORT").unwrap_or(String::from("8080"));
let listener = tokio::net::TcpListener::bind(format!("{}:{}", host, port))
.await
.unwrap();
info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}

View File

@@ -1,61 +1,23 @@
use std::str::FromStr;
use bigdecimal::{BigDecimal, ParseBigDecimalError};
use bigdecimal::BigDecimal;
use serde::{Deserialize, Serialize};
use sqlx::{Database, Decode, Encode, FromRow, Type, error::BoxDynError};
use std::cmp::PartialOrd;
use std::error::Error;
use std::fmt::Display;
// use validator::{Validate, ValidateRange};
// #[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, Clone, Type)]
// #[sqlx(transparent)]
// pub struct Amount(BigDecimal);
// impl Amount {
// fn new(text: &str) -> Result<Amount, ParseBigDecimalError> {
// Ok(Amount(BigDecimal::from_str(text)?))
// }
// }
// impl Display for Amount {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// self.0.fmt(f)
// }
// }
// impl ValidateRange<Amount> for Amount {
// fn greater_than(&self, max: Amount) -> Option<bool> {
// Some(self > &max)
// }
// fn less_than(&self, min: Amount) -> Option<bool> {
// Some(self < &min)
// }
// }
// impl From<Amount> for BigDecimal {
// fn from(value: Amount) -> Self {
// value.0
// }
// }
use sqlx::FromRow;
type Amount = BigDecimal;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Account {
pub(crate) struct Account {
pub account_id: i64,
pub balance: Amount,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateAccount {
pub(crate) struct CreateAccount {
pub account_id: i64,
pub initial_balance: Amount,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TransactionRequest {
pub(crate) struct TransactionRequest {
pub source_account_id: i64,
pub destination_account_id: i64,
pub amount: Amount,