second commit
This commit is contained in:
2
.env
2
.env
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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")]
|
||||||
|
@@ -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(())
|
||||||
|
22
src/main.rs
22
src/main.rs
@@ -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();
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user