This commit is contained in:
eraden 2022-05-04 07:26:29 +02:00
parent 791d32d0d8
commit b4dc801820
12 changed files with 396 additions and 96 deletions

View File

@ -35,6 +35,41 @@ macro_rules! db_async_handler {
};
}
#[macro_export]
macro_rules! query_db {
($db: expr, $msg: expr, default $fail: expr) => {
match $db.send($msg).await {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
log::error!("{e}");
$fail
}
Err(e) => {
log::error!("{e:?}");
$fail
}
}
};
($db: expr, $msg: expr, $fail: expr) => {
$crate::query_db!($db, $msg, $fail, $fail)
};
($db: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => {
match $db.send($msg).await {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
log::error!("{e}");
return Err($db_fail);
}
Err(e) => {
log::error!("{e:?}");
return Err($act_fail);
}
}
};
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to connect to database. {0:?}")]

View File

@ -31,7 +31,7 @@ pub(crate) async fn all_account_orders(
) -> Result<Vec<AccountOrder>> {
sqlx::query_as(
r#"
SELECT id, buyer_id, status, order_id, order_ext_id
SELECT id, buyer_id, status, order_ext_id
FROM account_orders
ORDER BY id DESC
"#,
@ -148,7 +148,7 @@ pub(crate) async fn update_account_order(
UPDATE account_orders
SET buyer_id = $2 AND status = $3 AND order_id = $4
WHERE id = $1
RETURNING id, buyer_id, status, order_id, order_ext_id
RETURNING id, buyer_id, status, order_ext_id
"#,
)
.bind(msg.id)
@ -185,7 +185,7 @@ pub(crate) async fn update_account_order_by_ext(
UPDATE account_orders
SET status = $2
WHERE order_ext_id = $1
RETURNING id, buyer_id, status, order_id, order_ext_id
RETURNING id, buyer_id, status, order_ext_id
"#,
)
.bind(msg.order_ext_id)
@ -209,7 +209,7 @@ db_async_handler!(FindAccountOrder, find_account_order, AccountOrder);
pub(crate) async fn find_account_order(msg: FindAccountOrder, db: PgPool) -> Result<AccountOrder> {
sqlx::query_as(
r#"
SELECT id, buyer_id, status, order_id, order_ext_id
SELECT id, buyer_id, status, order_ext_id
FROM account_orders
WHERE id = $1
"#,

View File

@ -2,8 +2,8 @@ use sqlx::PgPool;
use super::Result;
use crate::database::Database;
use crate::db_async_handler;
use crate::model::*;
use crate::{db_async_handler, model};
#[derive(Debug, thiserror::Error)]
pub enum Error {
@ -15,6 +15,8 @@ pub enum Error {
NotExists,
#[error("Failed to load all order items")]
All,
#[error("Failed to load order items")]
OrderItems,
}
#[derive(actix::Message)]
@ -101,3 +103,29 @@ WHERE id = $1
super::Error::OrderItem(Error::NotExists)
})
}
#[derive(actix::Message)]
#[rtype(result = "Result<Vec<OrderItem>>")]
pub struct OrderItems {
pub order_id: model::AccountOrderId,
}
db_async_handler!(OrderItems, order_items, Vec<OrderItem>);
pub(crate) async fn order_items(msg: OrderItems, pool: PgPool) -> Result<Vec<OrderItem>> {
sqlx::query_as(
r#"
SELECT id, product_id, order_id, quantity, quantity_unit
FROM order_items
WHERE order_id = $1
ORDER BY id DESC
"#,
)
.bind(msg.order_id)
.fetch_all(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
super::Error::OrderItem(Error::OrderItems)
})
}

View File

@ -19,6 +19,8 @@ pub enum Error {
Update,
#[error("Unable to delete product")]
Delete,
#[error("Unable to find products for shopping cart")]
ShoppingCartProducts,
}
#[derive(Message)]
@ -168,3 +170,42 @@ RETURNING id,
database::Error::Product(Error::Delete)
})
}
#[derive(Message)]
#[rtype(result = "Result<Vec<model::Product>>")]
pub struct ShoppingCartProducts {
pub shopping_cart_id: model::ShoppingCartId,
}
crate::db_async_handler!(
ShoppingCartProducts,
shopping_cart_products,
Vec<model::Product>
);
pub(crate) async fn shopping_cart_products(
msg: ShoppingCartProducts,
pool: PgPool,
) -> Result<Vec<Product>> {
sqlx::query_as(
r#"
SELECT products.id,
products.name,
products.short_description,
products.long_description,
products.category,
products.price,
products.deliver_days_flag
FROM products
INNER JOIN shopping_cart_items ON shopping_cart_items.product_id = products.id
WHERE shopping_cart_id = $1
"#,
)
.bind(msg.shopping_cart_id)
.fetch_all(&pool)
.await
.map_err(|e| {
log::error!("{e:?}");
database::Error::Product(Error::ShoppingCartProducts)
})
}

View File

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::sync::Arc;
use actix::Addr;
@ -5,10 +6,8 @@ use parking_lot::Mutex;
use crate::config::SharedAppConfig;
use crate::database::Database;
use crate::model::{
AccountId, OrderStatus, Price, ProductId, Quantity, QuantityUnit, ShoppingCartId,
};
use crate::{database, model};
use crate::model::{AccountId, OrderStatus, Price, ProductId, Quantity, QuantityUnit};
use crate::{database, model, query_db};
#[macro_export]
macro_rules! pay_async_handler {
@ -20,7 +19,43 @@ macro_rules! pay_async_handler {
use actix::WrapFuture;
let client = self.client.clone();
let db = self.db.clone();
Box::pin(async { $async(msg, client, db).await }.into_actor(self))
let config = self.config.clone();
Box::pin(async { $async(msg, client, db, config).await }.into_actor(self))
}
}
};
}
#[macro_export]
macro_rules! query_pay {
($manager: expr, $msg: expr, default $fail: expr) => {
match $manager.send($msg).await {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
log::error!("Payment {e}");
$fail
}
Err(e) => {
log::error!("Payment {e:?}");
$fail
}
}
};
($manager: expr, $msg: expr, $fail: expr) => {
$crate::query_pay!($manager, $msg, $fail, $fail)
};
($manager: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => {
match $manager.send($msg).await {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
log::error!("Payment {e}");
return Err($db_fail);
}
Err(e) => {
log::error!("Payment {e:?}");
return Err($act_fail);
}
}
};
@ -34,6 +69,8 @@ pub enum Error {
PayU(#[from] pay_u::Error),
#[error("Failed to create order")]
CreateOrder,
#[error("Failed to create order. Shopping cart is not available")]
UnavailableShoppingCart,
}
pub type Result<T> = std::result::Result<T, Error>;
@ -42,19 +79,25 @@ pub type Result<T> = std::result::Result<T, Error>;
pub struct PaymentManager {
client: PayUClient,
db: Addr<Database>,
config: SharedAppConfig,
}
impl PaymentManager {
pub async fn build(config: SharedAppConfig, db: Addr<Database>) -> Result<Self> {
let mut client = pay_u::Client::new(
config.lock().payment().payu_client_id(),
config.lock().payment().payu_client_secret(),
config.lock().payment().payu_client_merchant_id(),
);
let mut client = {
let l = config.lock();
let p = l.payment();
pay_u::Client::new(
p.payu_client_id(),
p.payu_client_secret(),
p.payu_client_merchant_id(),
)
};
client.authorize().await?;
Ok(Self {
client: Arc::new(Mutex::new(client)),
db,
config,
})
}
}
@ -103,55 +146,99 @@ impl From<Product> for pay_u::Product {
}
}
pub struct CreatePaymentResult {
pub order: model::AccountOrder,
pub items: Vec<model::OrderItem>,
pub redirect_uri: String,
}
#[derive(Debug, actix::Message)]
#[rtype(result = "Result<pay_u::OrderId>")]
#[rtype(result = "Result<CreatePaymentResult>")]
pub struct RequestPayment {
pub products: Vec<Product>,
pub currency: String,
pub description: String,
pub buyer: Buyer,
pub customer_ip: String,
pub buyer_id: AccountId,
pub shopping_cart_id: ShoppingCartId,
pub redirect_uri: String,
pub continue_uri: String,
}
pay_async_handler!(RequestPayment, request_payment, pay_u::OrderId);
pay_async_handler!(RequestPayment, request_payment, CreatePaymentResult);
pub(crate) async fn request_payment(
msg: RequestPayment,
client: PayUClient,
db: Addr<Database>,
) -> Result<pay_u::OrderId> {
let db_order: model::AccountOrder = match db
.send(database::CreateAccountOrder {
config: SharedAppConfig,
) -> Result<CreatePaymentResult> {
let (notify_uri, continue_uri) = {
let l = config.lock();
let w = l.web();
(
format!("{}/api/v1/payment/notify", w.host()),
format!("{}/payment/success", w.host()),
)
};
let cart: model::ShoppingCart = query_db!(
db,
database::EnsureActiveShoppingCart {
buyer_id: msg.buyer_id
},
Error::UnavailableShoppingCart
);
let cart_items: Vec<model::ShoppingCartItem> = query_db!(
db,
database::CartItems {
shopping_cart_id: cart.id,
},
Error::UnavailableShoppingCart
);
let mut items =
cart_items
.iter()
.fold(HashMap::with_capacity(cart_items.len()), |mut agg, item| {
agg.insert(item.product_id, (item.quantity, item.quantity_unit));
agg
});
let cart_products: Vec<model::Product> = query_db!(
db,
database::ShoppingCartProducts {
shopping_cart_id: cart.id,
},
Error::UnavailableShoppingCart
);
let db_order: model::AccountOrder = query_db!(
db,
database::CreateAccountOrder {
buyer_id: msg.buyer_id,
items: msg
.products
items: cart_products
.iter()
.map(|product| database::create_order::OrderItem {
product_id: product.id,
quantity: product.quantity,
quantity_unit: product.quantity_unit,
.map(|product| {
let (quantity, quantity_unit) =
items.get(&product.id).cloned().unwrap_or_else(|| {
(
model::Quantity::try_from(0).unwrap(),
model::QuantityUnit::Gram,
)
});
database::create_order::OrderItem {
product_id: product.id,
quantity,
quantity_unit,
}
})
.collect(),
shopping_cart_id: msg.shopping_cart_id,
})
.await
{
Ok(Ok(order)) => order,
Ok(Err(e)) => {
log::error!("{e}");
return Err(Error::CreateOrder);
}
Err(e) => {
log::error!("{e:?}");
return Err(Error::CreateOrder);
}
};
shopping_cart_id: cart.id,
},
Error::CreateOrder
);
let order = {
let pay_u::res::CreateOrder {
status: _,
redirect_uri,
order_id: _,
ext_order_id: _,
} = {
client
.lock()
.create_order(
@ -159,16 +246,38 @@ pub(crate) async fn request_payment(
msg.buyer.into(),
msg.customer_ip,
msg.currency,
msg.description,
format!("Order #{}", db_order.id),
)?
.with_products(msg.products.into_iter().map(Into::into))
.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(msg.redirect_uri)
.with_continue_url(msg.continue_uri),
.with_notify_url(notify_uri)
.with_continue_url(continue_uri),
)
.await?
};
Ok(order.order_id)
let order_items = query_db!(
db,
database::OrderItems {
order_id: db_order.id
},
Error::CreateOrder
);
Ok(CreatePaymentResult {
order: db_order,
items: order_items,
redirect_uri,
})
}
#[derive(Debug, serde::Deserialize)]
@ -187,6 +296,7 @@ pub(crate) async fn update_payment(
msg: UpdatePayment,
_client: PayUClient,
db: Addr<Database>,
_config: SharedAppConfig,
) -> Result<()> {
let status = msg.notification.0.status();
let order_ext_id = match msg.notification.0.order.ext_order_id {

View File

@ -139,11 +139,16 @@ impl WebConfig {
.expect("Invalid password hash")
}
pub fn session_secret(&self) -> String {
pub fn session_secret(&self) -> actix_web::cookie::Key {
use actix_web::cookie::Key;
self.session_secret
.as_ref()
.cloned()
.or_else(|| std::env::var("SESSION_SECRET").ok())
.map(|s| Key::from(s.as_bytes()))
.or_else(|| {
std::env::var("SESSION_SECRET")
.ok()
.map(|s| Key::from(s.as_bytes()))
})
.expect("Web config session_secret nor SESSION_SECRET env was given")
}

View File

@ -5,7 +5,6 @@ use std::io::Write;
use actix::Actor;
use actix_session::storage::RedisActorSessionStore;
use actix_session::SessionMiddleware;
use actix_web::cookie::Key;
use actix_web::middleware::Logger;
use actix_web::web::Data;
use actix_web::{App, HttpServer};
@ -48,11 +47,6 @@ pub enum Error {
pub type Result<T> = std::result::Result<T, Error>;
async fn server(opts: ServerOpts) -> Result<()> {
let secret_key = {
let key_secret = std::env::var("SESSION_SECRET")
.expect("session requires secret key with 64 or more characters");
Key::from(key_secret.as_bytes())
};
let redis_connection_string = "127.0.0.1:6379";
let app_config = config::default_load(&opts);
@ -64,21 +58,27 @@ async fn server(opts: ServerOpts) -> Result<()> {
.await
.expect("Failed to start payment manager")
.start();
let addr = (
app_config.lock().web().bind().unwrap_or(opts.bind),
app_config.lock().web().port().unwrap_or(opts.port),
);
let addr = {
let l = app_config.lock();
let w = l.web();
(w.bind().unwrap_or(opts.bind), w.port().unwrap_or(opts.port))
};
println!("Listen at {:?}", addr);
HttpServer::new(move || {
let config = app_config.clone();
App::new()
.wrap(Logger::default())
.wrap(actix_web::middleware::Compress::default())
.wrap(actix_web::middleware::NormalizePath::trim())
.wrap(SessionMiddleware::new(
RedisActorSessionStore::new(redis_connection_string),
secret_key.clone(),
{
let l = config.lock();
l.web().session_secret()
},
))
.wrap(actix_web::middleware::Compress::default())
.wrap(actix_web::middleware::NormalizePath::trim())
.app_data(Data::new(config))
.app_data(Data::new(db.clone()))
.app_data(Data::new(token_manager.clone()))

View File

@ -456,7 +456,20 @@ impl From<FullAccount> for Account {
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug, Copy, Clone, Deref, Display, From)]
#[derive(
sqlx::Type,
Serialize,
Deserialize,
Debug,
Copy,
Clone,
PartialEq,
Eq,
Hash,
Deref,
Display,
From,
)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct ProductId(RecordId);
@ -592,6 +605,12 @@ pub struct ShoppingCartItem {
pub quantity_unit: QuantityUnit,
}
impl ShoppingCartItem {
pub fn quantity(&self) -> Quantity {
self.quantity
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display, Debug)]
#[sqlx(transparent)]
#[serde(transparent)]

View File

@ -34,6 +34,29 @@ impl From<(Vec<model::AccountOrder>, Vec<model::OrderItem>)> for AccountOrders {
}
}
impl From<(model::AccountOrder, Vec<model::OrderItem>)> for AccountOrder {
fn from(
(
model::AccountOrder {
id,
buyer_id,
status,
order_id,
order_ext_id: _,
},
mut items,
): (model::AccountOrder, Vec<model::OrderItem>),
) -> Self {
AccountOrder {
id,
buyer_id,
status,
order_id,
items: items.drain_filter(|item| item.order_id == id).collect(),
}
}
}
#[derive(Serialize, Debug)]
pub struct AccountOrder {
pub id: AccountOrderId,

View File

@ -1,7 +1,7 @@
use actix::Addr;
use actix_session::Session;
use actix_web::get;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{delete, get, patch, post};
use crate::database::Database;
use crate::model::api::AccountOrders;

View File

@ -1,21 +1,21 @@
use actix::Addr;
use actix_web::web::{scope, Data, Json, ServiceConfig};
use actix_web::{delete, get, post, HttpResponse};
use actix_web::{delete, get, post, HttpRequest, HttpResponse};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use crate::actors::cart_manager;
use crate::actors::cart_manager::CartManager;
use crate::database::Database;
use crate::model::{
AccountId, ProductId, Quantity, QuantityUnit, ShoppingCart, ShoppingCartId, ShoppingCartItem,
AccountId, ProductId, Quantity, QuantityUnit, ShoppingCart, ShoppingCartItem,
ShoppingCartItemId,
};
use crate::order_manager::OrderManager;
use crate::payment_manager::PaymentManager;
use crate::routes::public::api_v1::ShoppingCartError;
use crate::routes::public::Error as PublicError;
use crate::routes::{RequireUser, Result};
use crate::token_manager::TokenManager;
use crate::{database, order_manager, routes};
use crate::{database, model, payment_manager, query_pay, routes};
#[get("/shopping-cart")]
async fn shopping_cart(
@ -190,38 +190,77 @@ async fn delete_cart_item(
#[derive(serde::Deserialize)]
pub struct CreateOrderInput {
pub shopping_cart_id: ShoppingCartId,
/// Required customer e-mail
pub email: String,
/// Required customer phone number
pub phone: String,
/// Required customer first name
pub first_name: String,
/// Required customer last name
pub last_name: String,
/// Required customer language
pub language: String,
}
#[post("/order")]
pub(crate) async fn create_order(
req: HttpRequest,
Json(payload): Json<CreateOrderInput>,
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
om: Data<Addr<OrderManager>>,
payment: Data<Addr<PaymentManager>>,
) -> routes::Result<HttpResponse> {
let (token, _) = credentials.require_user(tm.into_inner()).await?;
let order = match om
.send(order_manager::CreateOrder {
account_id: AccountId::from(token.subject),
shopping_cart_id: payload.shopping_cart_id,
})
.await
{
Ok(Ok(order)) => order,
Ok(Err(e)) => {
log::error!("{e}");
return Err(routes::Error::Public(PublicError::ApiV1(
super::Error::AddOrder,
)));
}
Err(e) => {
log::error!("{e}");
return Err(routes::Error::Public(PublicError::DatabaseConnection));
}
let (
model::Token {
id: _,
customer_id: _,
role: _,
issuer: _,
subject,
audience: _,
expiration_time: _,
not_before_time: _,
issued_at_time: _,
jwt_id: _,
},
_,
) = credentials.require_user(tm.into_inner()).await?;
let buyer_id = model::AccountId::from(subject);
let CreateOrderInput {
email,
phone,
first_name,
last_name,
language,
} = payload;
let ip = match req.peer_addr() {
Some(ip) => ip,
_ => return Ok(HttpResponse::BadRequest().body("No IP")),
};
Ok(HttpResponse::Created().json(order))
let payment_manager::CreatePaymentResult { redirect_uri, .. } = query_pay!(
payment,
payment_manager::RequestPayment {
currency: "PLN".to_string(),
buyer: payment_manager::Buyer {
email,
phone,
first_name,
last_name,
language,
},
customer_ip: ip.to_string(),
buyer_id,
},
routes::Error::Public(PublicError::DatabaseConnection)
);
Ok(HttpResponse::SeeOther()
.append_header(("Location", redirect_uri.as_str()))
.body(format!(
"<a href=\"{redirect_uri}\">Go to {redirect_uri}</a>"
)))
}
pub(crate) fn configure(config: &mut ServiceConfig) {

View File

@ -85,7 +85,7 @@ async fn sign_in(
Ok(HttpResponse::Created().json(SignInOutput { token: string }))
}
#[post("/pay_u/notify")]
#[post("/payment/notify")]
async fn handle_notification(
Json(notify): Json<PaymentNotification>,
payment: Data<Addr<PaymentManager>>,