mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-20 02:59:20 +01:00
make payments work
This commit is contained in:
parent
2988cc09aa
commit
4498ec442f
@ -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
|
||||
);
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
42
tests/integration/payments.hurl
Normal file
42
tests/integration/payments.hurl
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user