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)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("Failed to connect to database. {0:?}")] #[error("Failed to connect to database. {0:?}")]

View File

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

View File

@ -2,8 +2,8 @@ use sqlx::PgPool;
use super::Result; use super::Result;
use crate::database::Database; use crate::database::Database;
use crate::db_async_handler;
use crate::model::*; use crate::model::*;
use crate::{db_async_handler, model};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@ -15,6 +15,8 @@ pub enum Error {
NotExists, NotExists,
#[error("Failed to load all order items")] #[error("Failed to load all order items")]
All, All,
#[error("Failed to load order items")]
OrderItems,
} }
#[derive(actix::Message)] #[derive(actix::Message)]
@ -101,3 +103,29 @@ WHERE id = $1
super::Error::OrderItem(Error::NotExists) 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, Update,
#[error("Unable to delete product")] #[error("Unable to delete product")]
Delete, Delete,
#[error("Unable to find products for shopping cart")]
ShoppingCartProducts,
} }
#[derive(Message)] #[derive(Message)]
@ -168,3 +170,42 @@ RETURNING id,
database::Error::Product(Error::Delete) 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 std::sync::Arc;
use actix::Addr; use actix::Addr;
@ -5,10 +6,8 @@ use parking_lot::Mutex;
use crate::config::SharedAppConfig; use crate::config::SharedAppConfig;
use crate::database::Database; use crate::database::Database;
use crate::model::{ use crate::model::{AccountId, OrderStatus, Price, ProductId, Quantity, QuantityUnit};
AccountId, OrderStatus, Price, ProductId, Quantity, QuantityUnit, ShoppingCartId, use crate::{database, model, query_db};
};
use crate::{database, model};
#[macro_export] #[macro_export]
macro_rules! pay_async_handler { macro_rules! pay_async_handler {
@ -20,7 +19,43 @@ macro_rules! pay_async_handler {
use actix::WrapFuture; use actix::WrapFuture;
let client = self.client.clone(); let client = self.client.clone();
let db = self.db.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), PayU(#[from] pay_u::Error),
#[error("Failed to create order")] #[error("Failed to create order")]
CreateOrder, CreateOrder,
#[error("Failed to create order. Shopping cart is not available")]
UnavailableShoppingCart,
} }
pub type Result<T> = std::result::Result<T, Error>; 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 { pub struct PaymentManager {
client: PayUClient, client: PayUClient,
db: Addr<Database>, db: Addr<Database>,
config: SharedAppConfig,
} }
impl PaymentManager { impl PaymentManager {
pub async fn build(config: SharedAppConfig, db: Addr<Database>) -> Result<Self> { pub async fn build(config: SharedAppConfig, db: Addr<Database>) -> Result<Self> {
let mut client = pay_u::Client::new( let mut client = {
config.lock().payment().payu_client_id(), let l = config.lock();
config.lock().payment().payu_client_secret(), let p = l.payment();
config.lock().payment().payu_client_merchant_id(), pay_u::Client::new(
); p.payu_client_id(),
p.payu_client_secret(),
p.payu_client_merchant_id(),
)
};
client.authorize().await?; client.authorize().await?;
Ok(Self { Ok(Self {
client: Arc::new(Mutex::new(client)), client: Arc::new(Mutex::new(client)),
db, 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)] #[derive(Debug, actix::Message)]
#[rtype(result = "Result<pay_u::OrderId>")] #[rtype(result = "Result<CreatePaymentResult>")]
pub struct RequestPayment { pub struct RequestPayment {
pub products: Vec<Product>,
pub currency: String, pub currency: String,
pub description: String,
pub buyer: Buyer, pub buyer: Buyer,
pub customer_ip: String, pub customer_ip: String,
pub buyer_id: AccountId, 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( pub(crate) async fn request_payment(
msg: RequestPayment, msg: RequestPayment,
client: PayUClient, client: PayUClient,
db: Addr<Database>, db: Addr<Database>,
) -> Result<pay_u::OrderId> { config: SharedAppConfig,
let db_order: model::AccountOrder = match db ) -> Result<CreatePaymentResult> {
.send(database::CreateAccountOrder { let (notify_uri, continue_uri) = {
buyer_id: msg.buyer_id, let l = config.lock();
items: msg let w = l.web();
.products (
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() .iter()
.map(|product| database::create_order::OrderItem { .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: cart_products
.iter()
.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, product_id: product.id,
quantity: product.quantity, quantity,
quantity_unit: product.quantity_unit, quantity_unit,
}
}) })
.collect(), .collect(),
shopping_cart_id: msg.shopping_cart_id, shopping_cart_id: cart.id,
}) },
.await Error::CreateOrder
{ );
Ok(Ok(order)) => order,
Ok(Err(e)) => {
log::error!("{e}");
return Err(Error::CreateOrder);
}
Err(e) => {
log::error!("{e:?}");
return Err(Error::CreateOrder);
}
};
let order = { let pay_u::res::CreateOrder {
status: _,
redirect_uri,
order_id: _,
ext_order_id: _,
} = {
client client
.lock() .lock()
.create_order( .create_order(
@ -159,16 +246,38 @@ pub(crate) async fn request_payment(
msg.buyer.into(), msg.buyer.into(),
msg.customer_ip, msg.customer_ip,
msg.currency, 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_ext_order_id(db_order.order_ext_id.to_string())
.with_notify_url(msg.redirect_uri) .with_notify_url(notify_uri)
.with_continue_url(msg.continue_uri), .with_continue_url(continue_uri),
) )
.await? .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)] #[derive(Debug, serde::Deserialize)]
@ -187,6 +296,7 @@ pub(crate) async fn update_payment(
msg: UpdatePayment, msg: UpdatePayment,
_client: PayUClient, _client: PayUClient,
db: Addr<Database>, db: Addr<Database>,
_config: SharedAppConfig,
) -> Result<()> { ) -> Result<()> {
let status = msg.notification.0.status(); let status = msg.notification.0.status();
let order_ext_id = match msg.notification.0.order.ext_order_id { let order_ext_id = match msg.notification.0.order.ext_order_id {

View File

@ -139,11 +139,16 @@ impl WebConfig {
.expect("Invalid password hash") .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 self.session_secret
.as_ref() .as_ref()
.cloned() .map(|s| Key::from(s.as_bytes()))
.or_else(|| std::env::var("SESSION_SECRET").ok()) .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") .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::Actor;
use actix_session::storage::RedisActorSessionStore; use actix_session::storage::RedisActorSessionStore;
use actix_session::SessionMiddleware; use actix_session::SessionMiddleware;
use actix_web::cookie::Key;
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{App, HttpServer}; use actix_web::{App, HttpServer};
@ -48,11 +47,6 @@ pub enum Error {
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
async fn server(opts: ServerOpts) -> Result<()> { 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 redis_connection_string = "127.0.0.1:6379";
let app_config = config::default_load(&opts); let app_config = config::default_load(&opts);
@ -64,21 +58,27 @@ async fn server(opts: ServerOpts) -> Result<()> {
.await .await
.expect("Failed to start payment manager") .expect("Failed to start payment manager")
.start(); .start();
let addr = ( let addr = {
app_config.lock().web().bind().unwrap_or(opts.bind), let l = app_config.lock();
app_config.lock().web().port().unwrap_or(opts.port), let w = l.web();
); (w.bind().unwrap_or(opts.bind), w.port().unwrap_or(opts.port))
};
println!("Listen at {:?}", addr);
HttpServer::new(move || { HttpServer::new(move || {
let config = app_config.clone(); let config = app_config.clone();
App::new() App::new()
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(actix_web::middleware::Compress::default())
.wrap(actix_web::middleware::NormalizePath::trim())
.wrap(SessionMiddleware::new( .wrap(SessionMiddleware::new(
RedisActorSessionStore::new(redis_connection_string), 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(config))
.app_data(Data::new(db.clone())) .app_data(Data::new(db.clone()))
.app_data(Data::new(token_manager.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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct ProductId(RecordId); pub struct ProductId(RecordId);
@ -592,6 +605,12 @@ pub struct ShoppingCartItem {
pub quantity_unit: QuantityUnit, pub quantity_unit: QuantityUnit,
} }
impl ShoppingCartItem {
pub fn quantity(&self) -> Quantity {
self.quantity
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display, Debug)] #[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display, Debug)]
#[sqlx(transparent)] #[sqlx(transparent)]
#[serde(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)] #[derive(Serialize, Debug)]
pub struct AccountOrder { pub struct AccountOrder {
pub id: AccountOrderId, pub id: AccountOrderId,

View File

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

View File

@ -1,21 +1,21 @@
use actix::Addr; use actix::Addr;
use actix_web::web::{scope, Data, Json, ServiceConfig}; 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 actix_web_httpauth::extractors::bearer::BearerAuth;
use crate::actors::cart_manager; use crate::actors::cart_manager;
use crate::actors::cart_manager::CartManager; use crate::actors::cart_manager::CartManager;
use crate::database::Database; use crate::database::Database;
use crate::model::{ use crate::model::{
AccountId, ProductId, Quantity, QuantityUnit, ShoppingCart, ShoppingCartId, ShoppingCartItem, AccountId, ProductId, Quantity, QuantityUnit, ShoppingCart, ShoppingCartItem,
ShoppingCartItemId, ShoppingCartItemId,
}; };
use crate::order_manager::OrderManager; use crate::payment_manager::PaymentManager;
use crate::routes::public::api_v1::ShoppingCartError; use crate::routes::public::api_v1::ShoppingCartError;
use crate::routes::public::Error as PublicError; use crate::routes::public::Error as PublicError;
use crate::routes::{RequireUser, Result}; use crate::routes::{RequireUser, Result};
use crate::token_manager::TokenManager; use crate::token_manager::TokenManager;
use crate::{database, order_manager, routes}; use crate::{database, model, payment_manager, query_pay, routes};
#[get("/shopping-cart")] #[get("/shopping-cart")]
async fn shopping_cart( async fn shopping_cart(
@ -190,38 +190,77 @@ async fn delete_cart_item(
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct CreateOrderInput { 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")] #[post("/order")]
pub(crate) async fn create_order( pub(crate) async fn create_order(
req: HttpRequest,
Json(payload): Json<CreateOrderInput>, Json(payload): Json<CreateOrderInput>,
tm: Data<Addr<TokenManager>>, tm: Data<Addr<TokenManager>>,
credentials: BearerAuth, credentials: BearerAuth,
om: Data<Addr<OrderManager>>, payment: Data<Addr<PaymentManager>>,
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
let (token, _) = credentials.require_user(tm.into_inner()).await?; let (
let order = match om model::Token {
.send(order_manager::CreateOrder { id: _,
account_id: AccountId::from(token.subject), customer_id: _,
shopping_cart_id: payload.shopping_cart_id, role: _,
}) issuer: _,
.await subject,
{ audience: _,
Ok(Ok(order)) => order, expiration_time: _,
Ok(Err(e)) => { not_before_time: _,
log::error!("{e}"); issued_at_time: _,
return Err(routes::Error::Public(PublicError::ApiV1( jwt_id: _,
super::Error::AddOrder, },
))); _,
} ) = credentials.require_user(tm.into_inner()).await?;
Err(e) => {
log::error!("{e}"); let buyer_id = model::AccountId::from(subject);
return Err(routes::Error::Public(PublicError::DatabaseConnection)); 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) { pub(crate) fn configure(config: &mut ServiceConfig) {

View File

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