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
|
||||
PGHOST=127.0.0.1
|
||||
PGPASSWORD=password
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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")]
|
||||
|
@@ -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(())
|
||||
|
22
src/main.rs
22
src/main.rs
@@ -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();
|
||||
}
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user