make payments work

This commit is contained in:
DSeeLP 2025-03-11 11:12:56 +01:00
parent 2988cc09aa
commit 4498ec442f
5 changed files with 141 additions and 12 deletions

View File

@ -17,7 +17,7 @@ create table transactions(
"from" uuid not null references accounts(id), "from" uuid not null references accounts(id),
"to" uuid not null references accounts(id), "to" uuid not null references accounts(id),
amount bigint not null constraint positive_amount check (amount > 0), 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 message text
); );

View File

@ -5,6 +5,7 @@ use deadpool_postgres::GenericClient;
use garde::Validate; use garde::Validate;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::instrument;
use uuid::Uuid; use uuid::Uuid;
use crate::model::{ use crate::model::{
@ -119,7 +120,7 @@ pub struct BodyMakePayment {
pub amount: u64, pub amount: u64,
} }
#[derive(Deserialize, Validate)] #[derive(Debug, Deserialize, Validate, PartialEq)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
#[serde(untagged)] #[serde(untagged)]
pub enum NameOrUuid { pub enum NameOrUuid {
@ -127,7 +128,7 @@ pub enum NameOrUuid {
Name(#[garde(dive)] Name), Name(#[garde(dive)] Name),
} }
#[derive(Validate)] #[derive(Debug, Validate, PartialEq)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
pub struct AccountSelector { pub struct AccountSelector {
#[garde(dive)] #[garde(dive)]
@ -137,6 +138,7 @@ pub struct AccountSelector {
} }
impl AccountSelector { impl AccountSelector {
#[instrument(skip(client))]
async fn account_id( async fn account_id(
&self, &self,
client: &impl GenericClient, client: &impl GenericClient,
@ -160,7 +162,7 @@ impl AccountSelector {
} }
#[doc(hidden)] #[doc(hidden)]
#[derive(Deserialize, Validate)] #[derive(Debug, Deserialize, Validate, PartialEq)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
#[serde(untagged)] #[serde(untagged)]
pub enum UnvalidatedAccountSelector { pub enum UnvalidatedAccountSelector {
@ -204,6 +206,7 @@ pub trait ValidateTransform: Sized {
) -> Option<Self>; ) -> Option<Self>;
} }
#[derive(Debug, PartialEq)]
pub struct UnvalidatedTransform<Dest: ValidateTransform> { pub struct UnvalidatedTransform<Dest: ValidateTransform> {
src: Dest::Src, src: Dest::Src,
} }
@ -308,7 +311,7 @@ impl ValidateTransform for AccountSelector {
} }
} }
#[derive(Deserialize)] #[derive(Debug, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
pub struct MakePayment { pub struct MakePayment {
from: NameOrUuid, from: NameOrUuid,
@ -375,6 +378,89 @@ pub async fn make_payment(
if from.balance < amount { if from.balance < amount {
todo!("not enough money") 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?; let transaction = Transaction::create(&mut client, from.id, to, amount, None).await?;
client.commit().await?;
Ok(Json(transaction)) 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::<MakePayment>(
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::<MakePayment>(
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
)
);
}
}

View File

@ -8,7 +8,7 @@ use crate::api::{Pagination, PaginationType, RequestPagination};
use super::count; use super::count;
#[derive(Serialize)] #[derive(Debug, Serialize)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
pub struct Account { pub struct Account {
pub id: Uuid, pub id: Uuid,
@ -17,14 +17,14 @@ pub struct Account {
pub balance: u64, pub balance: u64,
} }
#[derive(Serialize)] #[derive(Debug, Serialize)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
pub struct AccountInfo { pub struct AccountInfo {
pub id: Uuid, pub id: Uuid,
pub user: Uuid, pub user: Uuid,
pub name: String, pub name: String,
} }
#[derive(Serialize)] #[derive(Debug, Serialize)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
pub struct UserAccountInfo { pub struct UserAccountInfo {
pub id: Uuid, pub id: Uuid,
@ -32,7 +32,7 @@ pub struct UserAccountInfo {
pub balance: u64, pub balance: u64,
} }
#[derive(Serialize)] #[derive(Debug, Serialize)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
pub struct ReducedAccountInfo { pub struct ReducedAccountInfo {
pub id: Uuid, pub id: Uuid,

View File

@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
use deadpool_postgres::GenericClient; use deadpool_postgres::GenericClient;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio_postgres::Row; use tokio_postgres::Row;
use uuid::Uuid; use uuid::{NoContext, Timestamp, Uuid};
use crate::api::{Pagination, RequestPagination}; use crate::api::{Pagination, RequestPagination};
@ -63,10 +63,11 @@ impl Transaction {
message: Option<String>, message: Option<String>,
) -> Result<Transaction, tokio_postgres::Error> { ) -> Result<Transaction, tokio_postgres::Error> {
let stmt = client 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?; .await?;
let id = Uuid::new_v7(Timestamp::now(NoContext));
let row = client let row = client
.query_one(&stmt, &[&from, &to, &(amount as i64), &message]) .query_one(&stmt, &[&id, &from, &to, &(amount as i64), &message])
.await?; .await?;
let timestamp = row.get(0); let timestamp = row.get(0);
Ok(Self { Ok(Self {

View File

@ -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