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

2
.env
View File

@@ -1,3 +1,5 @@
INTRASYS_HOST=0.0.0.0
INTRASYS_PORT=8080
PGUSER=postgres PGUSER=postgres
PGHOST=127.0.0.1 PGHOST=127.0.0.1
PGPASSWORD=password PGPASSWORD=password

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,61 +1,23 @@
use std::str::FromStr; use bigdecimal::BigDecimal;
use bigdecimal::{BigDecimal, ParseBigDecimalError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Database, Decode, Encode, FromRow, Type, error::BoxDynError}; use sqlx::FromRow;
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
// }
// }
type Amount = BigDecimal; type Amount = BigDecimal;
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Account { pub(crate) struct Account {
pub account_id: i64, pub account_id: i64,
pub balance: Amount, pub balance: Amount,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct CreateAccount { pub(crate) struct CreateAccount {
pub account_id: i64, pub account_id: i64,
pub initial_balance: Amount, pub initial_balance: Amount,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct TransactionRequest { pub(crate) struct TransactionRequest {
pub source_account_id: i64, pub source_account_id: i64,
pub destination_account_id: i64, pub destination_account_id: i64,
pub amount: Amount, pub amount: Amount,