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),
"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
);

View File

@ -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<Self>;
}
#[derive(Debug, PartialEq)]
pub struct UnvalidatedTransform<Dest: ValidateTransform> {
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::<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;
#[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,

View File

@ -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<String>,
) -> Result<Transaction, tokio_postgres::Error> {
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 {

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