mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-20 11:09:21 +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),
|
"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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
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