From 4498ec442ffc1e166ba945fa5604a06f64ab9280 Mon Sep 17 00:00:00 2001 From: DSeeLP <46624152+DSeeLP@users.noreply.github.com> Date: Tue, 11 Mar 2025 11:12:56 +0100 Subject: [PATCH] make payments work --- migrations/000000_baseline.sql | 2 +- src/api/transactions.rs | 94 +++++++++++++++++++++++++++++++-- src/model/account.rs | 8 +-- src/model/transaction.rs | 7 +-- tests/integration/payments.hurl | 42 +++++++++++++++ 5 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 tests/integration/payments.hurl diff --git a/migrations/000000_baseline.sql b/migrations/000000_baseline.sql index da1033e..5e7c7f0 100644 --- a/migrations/000000_baseline.sql +++ b/migrations/000000_baseline.sql @@ -17,7 +17,7 @@ create table transactions( "from" uuid not null references accounts(id), "to" uuid not null references accounts(id), amount bigint not null constraint positive_amount check (amount > 0), - timestamp timestamp without time zone not null default now(), + timestamp timestamp with time zone not null default now(), message text ); diff --git a/src/api/transactions.rs b/src/api/transactions.rs index b897ace..23f203c 100644 --- a/src/api/transactions.rs +++ b/src/api/transactions.rs @@ -5,6 +5,7 @@ use deadpool_postgres::GenericClient; use garde::Validate; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use tracing::instrument; use uuid::Uuid; use crate::model::{ @@ -119,7 +120,7 @@ pub struct BodyMakePayment { pub amount: u64, } -#[derive(Deserialize, Validate)] +#[derive(Debug, Deserialize, Validate, PartialEq)] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[serde(untagged)] pub enum NameOrUuid { @@ -127,7 +128,7 @@ pub enum NameOrUuid { Name(#[garde(dive)] Name), } -#[derive(Validate)] +#[derive(Debug, Validate, PartialEq)] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] pub struct AccountSelector { #[garde(dive)] @@ -137,6 +138,7 @@ pub struct AccountSelector { } impl AccountSelector { + #[instrument(skip(client))] async fn account_id( &self, client: &impl GenericClient, @@ -160,7 +162,7 @@ impl AccountSelector { } #[doc(hidden)] -#[derive(Deserialize, Validate)] +#[derive(Debug, Deserialize, Validate, PartialEq)] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[serde(untagged)] pub enum UnvalidatedAccountSelector { @@ -204,6 +206,7 @@ pub trait ValidateTransform: Sized { ) -> Option; } +#[derive(Debug, PartialEq)] pub struct UnvalidatedTransform { src: Dest::Src, } @@ -308,7 +311,7 @@ impl ValidateTransform for AccountSelector { } } -#[derive(Deserialize)] +#[derive(Debug, Deserialize, PartialEq)] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] pub struct MakePayment { from: NameOrUuid, @@ -375,6 +378,89 @@ pub async fn make_payment( if from.balance < amount { todo!("not enough money") } + let update_balance_stmt = client + .prepare_cached("update accounts set balance = balance + $2 where id = $1") + .await?; + client + .execute(&update_balance_stmt, &[&from.id, &(-(amount as i64))]) + .await?; + client + .execute(&update_balance_stmt, &[&to, &(amount as i64)]) + .await?; let transaction = Transaction::create(&mut client, from.id, to, amount, None).await?; + client.commit().await?; Ok(Json(transaction)) } + +#[cfg(test)] +mod tests { + use crate::{ + api::transactions::{ + AccountSelector, NameOrUuid, UnvalidatedAccountSelector, UnvalidatedTransform, + }, + model::Name, + }; + + use super::MakePayment; + + #[test] + fn payment_body() { + let uuid = uuid::uuid!("6fd8b7ab-7278-45b5-9916-2b09b4224a38"); + let payment = serde_json::from_str::( + r#"{"from":"personal", "to": { "user": "6fd8b7ab-7278-45b5-9916-2b09b4224a38" }, "amount": 100}"#, + ) + .unwrap(); + assert_eq!( + MakePayment { + from: NameOrUuid::Name(Name("personal".into())), + to: UnvalidatedTransform { + src: UnvalidatedAccountSelector::Object { + user: NameOrUuid::Id(uuid), + account: None + } + }, + amount: 100 + }, + payment + ); + assert_eq!( + payment.validate().unwrap(), + ( + NameOrUuid::Name(Name("personal".into())), + AccountSelector { + user: NameOrUuid::Id(uuid), + account: None + }, + 100 + ) + ); + let payment = serde_json::from_str::( + r#"{"from":"personal", "to": { "user": "test", "account": "abc" }, "amount": 100}"#, + ) + .unwrap(); + assert_eq!( + MakePayment { + from: NameOrUuid::Name(Name("personal".into())), + to: UnvalidatedTransform { + src: UnvalidatedAccountSelector::Object { + user: NameOrUuid::Name(Name("test".into())), + account: Some(Name("abc".into())) + } + }, + amount: 100 + }, + payment + ); + assert_eq!( + payment.validate().unwrap(), + ( + NameOrUuid::Name(Name("personal".into())), + AccountSelector { + user: NameOrUuid::Name(Name("test".into())), + account: Some(Name("abc".into())) + }, + 100 + ) + ); + } +} diff --git a/src/model/account.rs b/src/model/account.rs index 11a10da..ff06d74 100644 --- a/src/model/account.rs +++ b/src/model/account.rs @@ -8,7 +8,7 @@ use crate::api::{Pagination, PaginationType, RequestPagination}; use super::count; -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] pub struct Account { pub id: Uuid, @@ -17,14 +17,14 @@ pub struct Account { pub balance: u64, } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] pub struct AccountInfo { pub id: Uuid, pub user: Uuid, pub name: String, } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] pub struct UserAccountInfo { pub id: Uuid, @@ -32,7 +32,7 @@ pub struct UserAccountInfo { pub balance: u64, } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] pub struct ReducedAccountInfo { pub id: Uuid, diff --git a/src/model/transaction.rs b/src/model/transaction.rs index 24ba54d..60f483b 100644 --- a/src/model/transaction.rs +++ b/src/model/transaction.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use deadpool_postgres::GenericClient; use serde::{Deserialize, Serialize}; use tokio_postgres::Row; -use uuid::Uuid; +use uuid::{NoContext, Timestamp, Uuid}; use crate::api::{Pagination, RequestPagination}; @@ -63,10 +63,11 @@ impl Transaction { message: Option, ) -> Result { let stmt = client - .prepare_cached(r#"insert into transactions("from", "to", amount, message) values ($1, $2, $3, $4) returning timestamp"#) + .prepare_cached(r#"insert into transactions(id, "from", "to", amount, message) values ($1, $2, $3, $4, $5) returning timestamp"#) .await?; + let id = Uuid::new_v7(Timestamp::now(NoContext)); let row = client - .query_one(&stmt, &[&from, &to, &(amount as i64), &message]) + .query_one(&stmt, &[&id, &from, &to, &(amount as i64), &message]) .await?; let timestamp = row.get(0); Ok(Self { diff --git a/tests/integration/payments.hurl b/tests/integration/payments.hurl new file mode 100644 index 0000000..d66ad6c --- /dev/null +++ b/tests/integration/payments.hurl @@ -0,0 +1,42 @@ +POST {{host}}/api/login +{ + "name": "user1", + "password": "this-is-a-password" +} +HTTP 200 + +[Captures] +user1-token: jsonpath "$.token" + +POST {{host}}/api/login +{ + "name": "user2", + "password": "this-is-a-password" +} +HTTP 200 + +[Captures] +user2-token: jsonpath "$.token" + +POST {{host}}/api/transactions +Authorization: Bearer {{user1-token}} +{ + "from": "personal", + "to": { + "user": "user2" + }, + "amount": 100 +} +HTTP 200 + +GET {{host}}/api/users/@me/balance +Authorization: Bearer {{user1-token}} +HTTP 200 +[Asserts] +jsonpath "$.balance" == 99900 + +GET {{host}}/api/users/@me/balance +Authorization: Bearer {{user2-token}} +HTTP 200 +[Asserts] +jsonpath "$.balance" == 100100