Add optional payment

This commit is contained in:
eraden 2022-05-04 22:26:10 +02:00
parent 252f5373ff
commit b489b4435a
9 changed files with 156 additions and 76 deletions

View File

@ -33,6 +33,31 @@ macro_rules! db_async_handler {
} }
} }
}; };
($msg: ty, $async: ident, $res: ty, $inner_async: ident) => {
async fn $inner_async(msg: $msg, pool: sqlx::PgPool) -> Result<$res> {
let mut t = pool.begin().await?;
match $async(msg, &mut t).await {
Ok(res) => {
t.commit().await?;
Ok(res)
}
Err(e) => {
let _ = t.rollback().await;
Err(e)
}
}
}
impl actix::Handler<$msg> for Database {
type Result = actix::ResponseActFuture<Self, Result<$res>>;
fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result {
use actix::WrapFuture;
let pool = self.pool.clone();
Box::pin(async { $inner_async(msg, pool).await }.into_actor(self))
}
}
};
} }
#[macro_export] #[macro_export]

View File

@ -62,14 +62,17 @@ pub struct CreateAccountOrder {
pub shopping_cart_id: ShoppingCartId, pub shopping_cart_id: ShoppingCartId,
} }
db_async_handler!(CreateAccountOrder, create_account_order, AccountOrder); db_async_handler!(
CreateAccountOrder,
create_account_order,
AccountOrder,
inner_create_account_order
);
pub(crate) async fn create_account_order( pub(crate) async fn create_account_order(
msg: CreateAccountOrder, msg: CreateAccountOrder,
db: PgPool, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<AccountOrder> { ) -> Result<AccountOrder> {
let mut t = db.begin().await?;
let order: AccountOrder = match sqlx::query_as( let order: AccountOrder = match sqlx::query_as(
r#" r#"
INSERT INTO account_orders (buyer_id, status) INSERT INTO account_orders (buyer_id, status)
@ -79,13 +82,12 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id
) )
.bind(msg.buyer_id) .bind(msg.buyer_id)
.bind(OrderStatus::Confirmed) .bind(OrderStatus::Confirmed)
.fetch_one(&mut t) .fetch_one(&mut *t)
.await .await
{ {
Ok(order) => order, Ok(order) => order,
Err(e) => { Err(e) => {
log::error!("{e:?}"); log::error!("{e:?}");
t.rollback().await.ok();
return Err(super::Error::AccountOrder(Error::CantCreate)); return Err(super::Error::AccountOrder(Error::CantCreate));
} }
}; };
@ -97,13 +99,12 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id
quantity: item.quantity, quantity: item.quantity,
quantity_unit: item.quantity_unit, quantity_unit: item.quantity_unit,
}, },
&mut t, &mut *t,
) )
.await .await
{ {
log::error!("{e:?}"); log::error!("{e:?}");
t.rollback().await.ok();
return Err(super::Error::AccountOrder(Error::CantCreate)); return Err(super::Error::AccountOrder(Error::CantCreate));
} }
} }
@ -113,18 +114,15 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id
id: msg.shopping_cart_id, id: msg.shopping_cart_id,
state: ShoppingCartState::Closed, state: ShoppingCartState::Closed,
}, },
&mut t, t,
) )
.await .await
{ {
log::error!("{e:?}"); log::error!("{e:?}");
t.rollback().await.ok();
return Err(super::Error::AccountOrder(Error::CantCreate)); return Err(super::Error::AccountOrder(Error::CantCreate));
}; };
t.commit().await.ok();
Ok(order) Ok(order)
} }

View File

@ -1,5 +1,4 @@
use actix::Message; use actix::Message;
use sqlx::PgPool;
use super::Result; use super::Result;
use crate::database::Database; use crate::database::Database;
@ -27,9 +26,12 @@ pub enum Error {
#[rtype(result = "Result<Vec<model::Product>>")] #[rtype(result = "Result<Vec<model::Product>>")]
pub struct AllProducts; pub struct AllProducts;
crate::db_async_handler!(AllProducts, all, Vec<Product>); crate::db_async_handler!(AllProducts, all, Vec<Product>, inner_all);
pub(crate) async fn all(_msg: AllProducts, pool: PgPool) -> Result<Vec<model::Product>> { pub(crate) async fn all<'e, E>(_msg: AllProducts, pool: E) -> Result<Vec<model::Product>>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, SELECT id,
@ -42,7 +44,7 @@ SELECT id,
FROM products FROM products
"#, "#,
) )
.fetch_all(&pool) .fetch_all(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -61,9 +63,12 @@ pub struct CreateProduct {
pub deliver_days_flag: Days, pub deliver_days_flag: Days,
} }
crate::db_async_handler!(CreateProduct, create_product, Product); crate::db_async_handler!(CreateProduct, create_product, Product, inner_create_product);
pub(crate) async fn create_product(msg: CreateProduct, pool: PgPool) -> Result<model::Product> { pub(crate) async fn create_product<'e, E>(msg: CreateProduct, pool: E) -> Result<model::Product>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as( sqlx::query_as(
r#" r#"
INSERT INTO products (name, short_description, long_description, category, price, deliver_days_flag) INSERT INTO products (name, short_description, long_description, category, price, deliver_days_flag)
@ -83,7 +88,7 @@ RETURNING id,
.bind(msg.category) .bind(msg.category)
.bind(msg.price) .bind(msg.price)
.bind(msg.deliver_days_flag) .bind(msg.deliver_days_flag)
.fetch_one(&pool) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -103,9 +108,12 @@ pub struct UpdateProduct {
pub deliver_days_flag: Days, pub deliver_days_flag: Days,
} }
crate::db_async_handler!(UpdateProduct, update_product, Product); crate::db_async_handler!(UpdateProduct, update_product, Product, inner_update_product);
pub(crate) async fn update_product(msg: UpdateProduct, pool: PgPool) -> Result<model::Product> { pub(crate) async fn update_product<'e, E>(msg: UpdateProduct, pool: E) -> Result<model::Product>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as( sqlx::query_as(
r#" r#"
UPDATE products UPDATE products
@ -132,7 +140,7 @@ RETURNING id,
.bind(msg.category) .bind(msg.category)
.bind(msg.price) .bind(msg.price)
.bind(msg.deliver_days_flag) .bind(msg.deliver_days_flag)
.fetch_one(&pool) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -146,9 +154,17 @@ pub struct DeleteProduct {
pub product_id: ProductId, pub product_id: ProductId,
} }
crate::db_async_handler!(DeleteProduct, delete_product, Option<model::Product>); crate::db_async_handler!(
DeleteProduct,
delete_product,
Option<model::Product>,
inner_delete_product
);
pub(crate) async fn delete_product(msg: DeleteProduct, pool: PgPool) -> Result<Option<Product>> { pub(crate) async fn delete_product<'e, E>(msg: DeleteProduct, pool: E) -> Result<Option<Product>>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as( sqlx::query_as(
r#" r#"
DELETE FROM products DELETE FROM products
@ -163,7 +179,7 @@ RETURNING id,
"#, "#,
) )
.bind(msg.product_id) .bind(msg.product_id)
.fetch_optional(&pool) .fetch_optional(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -180,13 +196,17 @@ pub struct ShoppingCartProducts {
crate::db_async_handler!( crate::db_async_handler!(
ShoppingCartProducts, ShoppingCartProducts,
shopping_cart_products, shopping_cart_products,
Vec<model::Product> Vec<model::Product>,
inner_shopping_cart_products
); );
pub(crate) async fn shopping_cart_products( pub(crate) async fn shopping_cart_products<'e, E>(
msg: ShoppingCartProducts, msg: ShoppingCartProducts,
pool: PgPool, pool: E,
) -> Result<Vec<Product>> { ) -> Result<Vec<Product>>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT products.id, SELECT products.id,
@ -202,7 +222,7 @@ WHERE shopping_cart_id = $1
"#, "#,
) )
.bind(msg.shopping_cart_id) .bind(msg.shopping_cart_id)
.fetch_all(&pool) .fetch_all(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");

View File

@ -229,9 +229,17 @@ pub struct CartItems {
pub shopping_cart_id: ShoppingCartId, pub shopping_cart_id: ShoppingCartId,
} }
db_async_handler!(CartItems, cart_items, Vec<ShoppingCartItem>); db_async_handler!(
CartItems,
cart_items,
Vec<ShoppingCartItem>,
inner_cart_items
);
pub(crate) async fn cart_items(msg: CartItems, pool: PgPool) -> Result<Vec<ShoppingCartItem>> { pub(crate) async fn cart_items<'e, E>(msg: CartItems, pool: E) -> Result<Vec<ShoppingCartItem>>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
let shopping_cart_id = msg.shopping_cart_id; let shopping_cart_id = msg.shopping_cart_id;
sqlx::query_as( sqlx::query_as(
r#" r#"
@ -241,7 +249,7 @@ WHERE shopping_cart_id = $1
"#, "#,
) )
.bind(msg.shopping_cart_id) .bind(msg.shopping_cart_id)
.fetch_all(&pool) .fetch_all(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");

View File

@ -232,12 +232,13 @@ pub struct EnsureActiveShoppingCart {
db_async_handler!( db_async_handler!(
EnsureActiveShoppingCart, EnsureActiveShoppingCart,
ensure_active_shopping_cart, ensure_active_shopping_cart,
ShoppingCart ShoppingCart,
inner_ensure_active_shopping_cart
); );
pub(crate) async fn ensure_active_shopping_cart( pub(crate) async fn ensure_active_shopping_cart(
msg: EnsureActiveShoppingCart, msg: EnsureActiveShoppingCart,
pool: PgPool, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ShoppingCart> { ) -> Result<ShoppingCart> {
if let Ok(Some(cart)) = sqlx::query_as( if let Ok(Some(cart)) = sqlx::query_as(
r#" r#"
@ -249,7 +250,7 @@ RETURNING id, buyer_id, payment_method, state;
"#, "#,
) )
.bind(msg.buyer_id) .bind(msg.buyer_id)
.fetch_optional(&pool) .fetch_optional(&mut *pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -265,7 +266,7 @@ WHERE buyer_id = $1 AND state = 'active'
"#, "#,
) )
.bind(msg.buyer_id) .bind(msg.buyer_id)
.fetch_one(&pool) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");

View File

@ -159,6 +159,9 @@ pub struct RequestPayment {
pub buyer: Buyer, pub buyer: Buyer,
pub customer_ip: String, pub customer_ip: String,
pub buyer_id: AccountId, pub buyer_id: AccountId,
/// False if customer is allowed to be charged on site.
/// Otherwise it should be true to use payment service for charging
pub charge_client: bool,
} }
pay_async_handler!(RequestPayment, request_payment, CreatePaymentResult); pay_async_handler!(RequestPayment, request_payment, CreatePaymentResult);
@ -177,6 +180,7 @@ pub(crate) async fn request_payment(
format!("{}/payment/success", w.host()), format!("{}/payment/success", w.host()),
) )
}; };
let cart: model::ShoppingCart = query_db!( let cart: model::ShoppingCart = query_db!(
db, db,
database::EnsureActiveShoppingCart { database::EnsureActiveShoppingCart {
@ -233,46 +237,55 @@ pub(crate) async fn request_payment(
Error::CreateOrder Error::CreateOrder
); );
let pay_u::res::CreateOrder { let payment_required = {
status: _, let l = config.lock();
redirect_uri, l.payment().optional_payment() != false
order_id,
ext_order_id: _,
} = {
client
.lock()
.create_order(
pay_u::req::OrderCreate::build(
msg.buyer.into(),
msg.customer_ip,
msg.currency,
format!("Order #{}", db_order.id),
)?
.with_products(cart_products.into_iter().map(|p| {
pay_u::Product::new(
p.name.to_string(),
**p.price,
items
.remove(&p.id)
.map(|(quantity, _)| **quantity as u32)
.unwrap_or_default(),
)
}))
.with_ext_order_id(db_order.order_ext_id.to_string())
.with_notify_url(notify_uri)
.with_continue_url(continue_uri),
)
.await?
}; };
let redirect_uri = if msg.charge_client || payment_required {
let pay_u::res::CreateOrder {
status: _,
redirect_uri,
order_id,
ext_order_id: _,
} = {
client
.lock()
.create_order(
pay_u::req::OrderCreate::build(
msg.buyer.into(),
msg.customer_ip,
msg.currency,
format!("Order #{}", db_order.id),
)?
.with_products(cart_products.into_iter().map(|p| {
pay_u::Product::new(
p.name.to_string(),
**p.price,
items
.remove(&p.id)
.map(|(quantity, _)| **quantity as u32)
.unwrap_or_default(),
)
}))
.with_ext_order_id(db_order.order_ext_id.to_string())
.with_notify_url(notify_uri)
.with_continue_url(continue_uri),
)
.await?
};
query_db!( query_db!(
db, db,
database::SetOrderServiceId { database::SetOrderServiceId {
service_order_id: order_id.0, service_order_id: order_id.0,
id: db_order.id, id: db_order.id,
}, },
Error::CreateOrder Error::CreateOrder
); );
redirect_uri
} else {
String::from("/pay-on-site")
};
let order_items = query_db!( let order_items = query_db!(
db, db,

View File

@ -38,6 +38,7 @@ pub struct PaymentConfig {
payu_client_id: Option<pay_u::ClientId>, payu_client_id: Option<pay_u::ClientId>,
payu_client_secret: Option<pay_u::ClientSecret>, payu_client_secret: Option<pay_u::ClientSecret>,
payu_client_merchant_id: Option<pay_u::MerchantPosId>, payu_client_merchant_id: Option<pay_u::MerchantPosId>,
optional_payment: bool,
} }
impl Example for PaymentConfig { impl Example for PaymentConfig {
@ -51,11 +52,16 @@ impl Example for PaymentConfig {
)), )),
/// "Create payu account and copy here merchant id" /// "Create payu account and copy here merchant id"
payu_client_merchant_id: Some(pay_u::MerchantPosId::from(0)), payu_client_merchant_id: Some(pay_u::MerchantPosId::from(0)),
optional_payment: true,
} }
} }
} }
impl PaymentConfig { impl PaymentConfig {
pub fn optional_payment(&self) -> bool {
self.optional_payment
}
pub fn payu_client_id(&self) -> pay_u::ClientId { pub fn payu_client_id(&self) -> pay_u::ClientId {
self.payu_client_id self.payu_client_id
.as_ref() .as_ref()

View File

@ -40,10 +40,14 @@ pub enum Error {
#[get("/")] #[get("/")]
async fn landing() -> HttpResponse { async fn landing() -> HttpResponse {
HttpResponse::SeeOther().append_header((actix_web::http::header::LOCATION, ""));
HttpResponse::NotImplemented().body("") HttpResponse::NotImplemented().body("")
} }
#[get("/pay-on-site")]
async fn pay_on_site() -> HttpResponse {
HttpResponse::Ok().body("<h1>Pay on Site</h1>")
}
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {
config.service(landing).configure(api_v1::configure); config.service(landing).configure(api_v1::configure);
} }

View File

@ -200,6 +200,9 @@ pub struct CreateOrderInput {
pub last_name: String, pub last_name: String,
/// Required customer language /// Required customer language
pub language: String, pub language: String,
/// False if customer is allowed to be charged on site.
/// Otherwise it should be true to use payment service for charging
pub charge_client: bool,
} }
#[post("/order")] #[post("/order")]
@ -233,6 +236,7 @@ pub(crate) async fn create_order(
first_name, first_name,
last_name, last_name,
language, language,
charge_client,
} = payload; } = payload;
let ip = match req.peer_addr() { let ip = match req.peer_addr() {
Some(ip) => ip, Some(ip) => ip,
@ -252,6 +256,7 @@ pub(crate) async fn create_order(
}, },
customer_ip: ip.to_string(), customer_ip: ip.to_string(),
buyer_id, buyer_id,
charge_client
}, },
routes::Error::Public(PublicError::DatabaseConnection) routes::Error::Public(PublicError::DatabaseConnection)
); );