Add many additional create order fields

This commit is contained in:
Adrian Woźniak 2022-04-28 15:54:18 +02:00
parent 9d6534c2a2
commit 9658abe3b8
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
11 changed files with 614 additions and 260 deletions

View File

@ -9,6 +9,8 @@ use sqlx::PgPool;
pub use stocks::*; pub use stocks::*;
pub use tokens::*; pub use tokens::*;
use crate::config::SharedAppConfig;
pub mod account_orders; pub mod account_orders;
pub mod accounts; pub mod accounts;
pub mod order_items; pub mod order_items;
@ -72,8 +74,9 @@ impl Clone for Database {
} }
impl Database { impl Database {
pub(crate) async fn build(url: &str) -> Result<Self> { pub(crate) async fn build(config: SharedAppConfig) -> Result<Self> {
let pool = sqlx::PgPool::connect(url).await.map_err(Error::Connect)?; let url = config.lock().database().url();
let pool = sqlx::PgPool::connect(&url).await.map_err(Error::Connect)?;
Ok(Database { pool }) Ok(Database { pool })
} }

View File

@ -1,5 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use crate::config::SharedAppConfig;
use crate::Email; use crate::Email;
#[macro_export] #[macro_export]
@ -29,7 +30,7 @@ pub struct SendState {
pub struct EmailManager(Arc<Inner>); pub struct EmailManager(Arc<Inner>);
pub(crate) struct Inner { pub(crate) struct Inner {
from: Email, config: SharedAppConfig,
send_grid: sendgrid::SGClient, send_grid: sendgrid::SGClient,
} }
@ -38,14 +39,10 @@ impl actix::Actor for EmailManager {
} }
impl EmailManager { impl EmailManager {
pub fn build() -> Result<Self> { pub fn build(config: SharedAppConfig) -> Result<Self> {
let from = std::env::var("SMTP_FROM").expect("Missing SMTP_FROM variable");
Ok(Self(Arc::new(Inner { Ok(Self(Arc::new(Inner {
from: Email::from(from), config: config.clone(),
send_grid: sendgrid::SGClient::new( send_grid: sendgrid::SGClient::new(config.lock().mail().sendgrid_secret()),
std::env::var("SENDGRID_SECRET").expect("Missing SENDGRID_SECRET variable"),
),
}))) })))
} }
} }
@ -59,13 +56,12 @@ pub struct TestMail {
mail_async_handler!(TestMail, test_mail, SendState); mail_async_handler!(TestMail, test_mail, SendState);
pub(crate) async fn test_mail(msg: TestMail, inner: Arc<Inner>) -> Result<SendState> { pub(crate) async fn test_mail(msg: TestMail, inner: Arc<Inner>) -> Result<SendState> {
let from: &str = &*inner.from;
let status = inner let status = inner
.send_grid .send_grid
.send( .send(
sendgrid::Mail::new() sendgrid::Mail::new()
.add_to((msg.receiver.as_str(), "User").into()) .add_to((msg.receiver.as_str(), "User").into())
.add_from(from) .add_from(&inner.config.lock().mail().smtp_from())
.add_subject("Test e-mail") .add_subject("Test e-mail")
.add_html("<h1>Test e-mail</h1>") .add_html("<h1>Test e-mail</h1>")
.build(), .build(),

View File

@ -1,5 +1,6 @@
use actix::Message; use actix::Message;
use crate::config::SharedAppConfig;
use crate::database::{self, SharedDatabase}; use crate::database::{self, SharedDatabase};
use crate::model::{ use crate::model::{
AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem, AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem,
@ -14,7 +15,8 @@ macro_rules! order_async_handler {
fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result {
use actix::WrapFuture; use actix::WrapFuture;
let db = self.db.clone(); let db = self.db.clone();
Box::pin(async { $async(msg, db).await }.into_actor(self)) let config = self.config.clone();
Box::pin(async { $async(msg, db, config).await }.into_actor(self))
} }
} }
}; };
@ -34,6 +36,7 @@ pub type Result<T> = std::result::Result<T, Error>;
pub struct OrderManager { pub struct OrderManager {
db: SharedDatabase, db: SharedDatabase,
config: SharedAppConfig,
} }
impl actix::Actor for OrderManager { impl actix::Actor for OrderManager {
@ -41,8 +44,8 @@ impl actix::Actor for OrderManager {
} }
impl OrderManager { impl OrderManager {
pub fn new(db: SharedDatabase) -> Self { pub fn new(config: SharedAppConfig, db: SharedDatabase) -> Self {
Self { db } Self { db, config }
} }
} }
@ -55,7 +58,11 @@ pub struct CreateOrder {
order_async_handler!(CreateOrder, create_order, AccountOrder); order_async_handler!(CreateOrder, create_order, AccountOrder);
pub(crate) async fn create_order(msg: CreateOrder, db: SharedDatabase) -> Result<AccountOrder> { pub(crate) async fn create_order(
msg: CreateOrder,
db: SharedDatabase,
_config: SharedAppConfig,
) -> Result<AccountOrder> {
let cart: ShoppingCart = match db let cart: ShoppingCart = match db
.send(database::FindShoppingCart { .send(database::FindShoppingCart {
id: msg.shopping_cart_id, id: msg.shopping_cart_id,

View File

@ -2,8 +2,9 @@ use std::sync::Arc;
use actix::Addr; use actix::Addr;
use parking_lot::Mutex; use parking_lot::Mutex;
use pay_u::{MerchantPosId, OrderCreateRequest}; use pay_u::OrderCreateRequest;
use crate::config::SharedAppConfig;
use crate::database; use crate::database;
use crate::database::Database; use crate::database::Database;
use crate::model::{AccountId, Price, ProductId, Quantity, QuantityUnit, ShoppingCartId}; use crate::model::{AccountId, Price, ProductId, Quantity, QuantityUnit, ShoppingCartId};
@ -43,18 +44,12 @@ pub struct PaymentManager {
} }
impl PaymentManager { impl PaymentManager {
pub async fn build<ClientId, ClientSecret>( pub async fn build(config: SharedAppConfig, db: Addr<Database>) -> Result<Self> {
client_id: ClientId, let mut client = pay_u::Client::new(
client_secret: ClientSecret, config.lock().payment().payu_client_id(),
merchant_pos_id: MerchantPosId, config.lock().payment().payu_client_secret(),
db: Addr<Database>, config.lock().payment().payu_client_merchant_id(),
) -> Result<Self> );
where
ClientId: Into<pay_u::ClientId>,
ClientSecret: Into<pay_u::ClientSecret>,
{
let mut client =
pay_u::Client::new(client_id.into(), client_secret.into(), merchant_pos_id);
client.authorize().await?; client.authorize().await?;
Ok(Self { Ok(Self {
client: Arc::new(Mutex::new(client)), client: Arc::new(Mutex::new(client)),
@ -154,14 +149,16 @@ pub(crate) async fn request_payment(
} }
}; };
let mut client = client.lock(); let order = {
let order = client client
.create_order( .lock()
OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency) .create_order(
.with_description(msg.description) OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency)
.with_notify_url(msg.redirect_uri) .with_description(msg.description)
.with_products(msg.products.into_iter().map(Into::into)), .with_notify_url(msg.redirect_uri)
) .with_products(msg.products.into_iter().map(Into::into)),
.await?; )
.await?
};
Ok(order.order_id) Ok(order.order_id)
} }

View File

@ -1,6 +1,5 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
use actix::{Addr, Message}; use actix::{Addr, Message};
use chrono::prelude::*; use chrono::prelude::*;
@ -8,6 +7,7 @@ use hmac::digest::KeyInit;
use hmac::Hmac; use hmac::Hmac;
use sha2::Sha256; use sha2::Sha256;
use crate::config::SharedAppConfig;
use crate::database::{Database, TokenByJti}; use crate::database::{Database, TokenByJti};
use crate::model::{AccountId, Audience, Token, TokenString}; use crate::model::{AccountId, Audience, Token, TokenString};
use crate::{database, Role}; use crate::{database, Role};
@ -21,8 +21,8 @@ macro_rules! token_async_handler {
fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result {
use actix::WrapFuture; use actix::WrapFuture;
let db = self.db.clone(); let db = self.db.clone();
let secret = self.secret.clone(); let config = self.config.clone();
Box::pin(async { $async(msg, db, secret).await }.into_actor(self)) Box::pin(async { $async(msg, db, config).await }.into_actor(self))
} }
} }
}; };
@ -68,7 +68,7 @@ pub type Result<T> = std::result::Result<T, Error>;
pub struct TokenManager { pub struct TokenManager {
db: Addr<Database>, db: Addr<Database>,
secret: Arc<String>, config: SharedAppConfig,
} }
impl actix::Actor for TokenManager { impl actix::Actor for TokenManager {
@ -76,9 +76,8 @@ impl actix::Actor for TokenManager {
} }
impl TokenManager { impl TokenManager {
pub fn new(db: Addr<Database>) -> Self { pub fn new(config: SharedAppConfig, db: Addr<Database>) -> Self {
let secret = Arc::new(std::env::var("JWT_SECRET").expect("JWT_SECRET is required")); Self { db, config }
Self { db, secret }
} }
} }
@ -96,7 +95,7 @@ token_async_handler!(CreateToken, create_token, (Token, TokenString));
pub(crate) async fn create_token( pub(crate) async fn create_token(
msg: CreateToken, msg: CreateToken,
db: Addr<Database>, db: Addr<Database>,
secret: Arc<String>, config: SharedAppConfig,
) -> Result<(Token, TokenString)> { ) -> Result<(Token, TokenString)> {
let CreateToken { let CreateToken {
customer_id, customer_id,
@ -129,6 +128,7 @@ pub(crate) async fn create_token(
let token_string = { let token_string = {
use jwt::SignWithKey; use jwt::SignWithKey;
let secret = config.lock().web().jwt_secret();
let key: Hmac<Sha256> = build_key(secret)?; let key: Hmac<Sha256> = build_key(secret)?;
let mut claims = BTreeMap::new(); let mut claims = BTreeMap::new();
@ -195,12 +195,13 @@ token_async_handler!(Validate, validate, (Token, bool));
pub(crate) async fn validate( pub(crate) async fn validate(
msg: Validate, msg: Validate,
db: Addr<Database>, db: Addr<Database>,
secret: Arc<String>, config: SharedAppConfig,
) -> Result<(Token, bool)> { ) -> Result<(Token, bool)> {
use jwt::VerifyWithKey; use jwt::VerifyWithKey;
log::info!("Validating token {:?}", msg.token); log::info!("Validating token {:?}", msg.token);
let secret = config.lock().web().jwt_secret();
let key: Hmac<Sha256> = build_key(secret)?; let key: Hmac<Sha256> = build_key(secret)?;
let claims: BTreeMap<String, String> = match msg.token.verify_with_key(&key) { let claims: BTreeMap<String, String> = match msg.token.verify_with_key(&key) {
Ok(claims) => claims, Ok(claims) => claims,
@ -260,7 +261,7 @@ pub(crate) async fn validate(
Ok((token, true)) Ok((token, true))
} }
fn build_key(secret: Arc<String>) -> Result<Hmac<Sha256>> { fn build_key(secret: String) -> Result<Hmac<Sha256>> {
match Hmac::new_from_slice(secret.as_bytes()) { match Hmac::new_from_slice(secret.as_bytes()) {
Ok(key) => Ok(key), Ok(key) => Ok(key),
Err(e) => { Err(e) => {

View File

@ -1,9 +1,36 @@
use std::sync::Arc;
use parking_lot::Mutex;
use password_hash::SaltString;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
trait Example: Sized { trait Example: Sized {
fn example() -> Self; fn example() -> Self;
} }
#[derive(Clone)]
pub struct SharedAppConfig(Arc<Mutex<AppConfig>>);
impl SharedAppConfig {
fn new(app_config: AppConfig) -> Self {
Self(Arc::new(Mutex::new(app_config)))
}
}
impl std::ops::Deref for SharedAppConfig {
type Target = Arc<Mutex<AppConfig>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for SharedAppConfig {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
pub struct PaymentConfig { pub struct PaymentConfig {
payu_client_id: Option<pay_u::ClientId>, payu_client_id: Option<pay_u::ClientId>,
@ -32,10 +59,9 @@ impl PaymentConfig {
.as_ref() .as_ref()
.cloned() .cloned()
.or_else(|| std::env::var("PAYU_CLIENT_ID").ok().map(pay_u::ClientId)) .or_else(|| std::env::var("PAYU_CLIENT_ID").ok().map(pay_u::ClientId))
.unwrap_or_else(|| { .expect("payment config payu_client_id nor PAYU_CLIENT_ID env was given")
panic!("payment config payu_client_id nor PAYU_CLIENT_ID env was given")
})
} }
pub fn payu_client_secret(&self) -> pay_u::ClientSecret { pub fn payu_client_secret(&self) -> pay_u::ClientSecret {
self.payu_client_secret self.payu_client_secret
.as_ref() .as_ref()
@ -45,14 +71,20 @@ impl PaymentConfig {
.ok() .ok()
.map(pay_u::ClientSecret) .map(pay_u::ClientSecret)
}) })
.unwrap_or_else(|| { .expect("payment config payu_client_secret nor PAYU_CLIENT_SECRET env was given")
panic!("payment config payu_client_secret nor PAYU_CLIENT_SECRET env was given")
})
} }
pub fn payu_client_merchant_id(&self) -> pay_u::MerchantPosId { pub fn payu_client_merchant_id(&self) -> pay_u::MerchantPosId {
self.payu_client_merchant_id self.payu_client_merchant_id
.or_else(|| std::env::var("PAYU_CLIENT_MERCHANT_ID").ok().and_then(|s| s.parse::<i32>().ok()).map(pay_u::MerchantPosId)) .or_else(|| {
.unwrap_or_else(|| panic!("payment config payu_client_merchant_id nor PAYU_CLIENT_MERCHANT_ID env was given")) std::env::var("PAYU_CLIENT_MERCHANT_ID")
.ok()
.and_then(|s| s.parse::<i32>().ok())
.map(pay_u::MerchantPosId)
})
.expect(
"payment config payu_client_merchant_id nor PAYU_CLIENT_MERCHANT_ID env was given",
)
} }
} }
@ -90,35 +122,48 @@ impl WebConfig {
.as_ref() .as_ref()
.cloned() .cloned()
.or_else(|| std::env::var("WEB_HOST").ok()) .or_else(|| std::env::var("WEB_HOST").ok())
.unwrap_or_else(|| panic!("web host config nor WEB_HOST env was not given")) .expect("web host config nor WEB_HOST env was not given")
} }
pub fn pass_salt(&self) -> String {
self.pass_salt pub fn pass_salt(&self) -> SaltString {
.as_ref() SaltString::new(
.cloned() &self
.or_else(|| std::env::var("PASS_SALT").ok()) .pass_salt
.unwrap_or_else(|| panic!("Web config pass_salt nor PASS_SALT env was given")) .as_ref()
.cloned()
.or_else(|| std::env::var("PASS_SALT").ok())
.expect("Web config pass_salt nor PASS_SALT env was given"),
)
.expect("Invalid password hash")
} }
pub fn session_secret(&self) -> String { pub fn session_secret(&self) -> String {
self.session_secret self.session_secret
.as_ref() .as_ref()
.cloned() .cloned()
.or_else(|| std::env::var("SESSION_SECRET").ok()) .or_else(|| std::env::var("SESSION_SECRET").ok())
.unwrap_or_else(|| panic!("Web config session_secret nor SESSION_SECRET env was given")) .expect("Web config session_secret nor SESSION_SECRET env was given")
} }
pub fn jwt_secret(&self) -> String { pub fn jwt_secret(&self) -> String {
self.jwt_secret self.jwt_secret
.as_ref() .as_ref()
.cloned() .cloned()
.or_else(|| std::env::var("JWT_SECRET").ok()) .or_else(|| std::env::var("JWT_SECRET").ok())
.unwrap_or_else(|| panic!("Web config jwt_secret nor JWT_SECRET env was given")) .expect("Web config jwt_secret nor JWT_SECRET env was given")
} }
pub fn bind(&self) -> Option<String> { pub fn bind(&self) -> Option<String> {
self.bind self.bind
.as_ref() .as_ref()
.cloned() .cloned()
.or_else(|| std::env::var("BAZZAR_BIND").ok()) .or_else(|| std::env::var("BAZZAR_BIND").ok())
} }
pub fn set_bind<S: Into<String>>(&mut self, bind: S) {
self.bind = Some(bind.into());
}
pub fn port(&self) -> Option<u16> { pub fn port(&self) -> Option<u16> {
self.port.as_ref().copied().or_else(|| { self.port.as_ref().copied().or_else(|| {
std::env::var("BAZZAR_PORT") std::env::var("BAZZAR_PORT")
@ -126,6 +171,10 @@ impl WebConfig {
.and_then(|s| s.parse::<u16>().ok()) .and_then(|s| s.parse::<u16>().ok())
}) })
} }
pub fn set_port(&mut self, port: u16) {
self.port = Some(port);
}
} }
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
@ -157,25 +206,23 @@ impl MailConfig {
.as_ref() .as_ref()
.cloned() .cloned()
.or_else(|| std::env::var("SENDGRID_SECRET").ok()) .or_else(|| std::env::var("SENDGRID_SECRET").ok())
.unwrap_or_else(|| { .expect("Mail sendgrid_secret config nor SENDGRID_SECRET env was given")
panic!("Mail sendgrid_secret config nor SENDGRID_SECRET env was given")
})
} }
pub fn sendgrid_api_key(&self) -> String { pub fn sendgrid_api_key(&self) -> String {
self.sendgrid_api_key self.sendgrid_api_key
.as_ref() .as_ref()
.cloned() .cloned()
.or_else(|| std::env::var("SENDGRID_API_KEY").ok()) .or_else(|| std::env::var("SENDGRID_API_KEY").ok())
.unwrap_or_else(|| { .expect("Mail sendgrid_api_key config nor SENDGRID_API_KEY env was given")
panic!("Mail sendgrid_api_key config nor SENDGRID_API_KEY env was given")
})
} }
pub fn smtp_from(&self) -> String { pub fn smtp_from(&self) -> String {
self.smtp_from self.smtp_from
.as_ref() .as_ref()
.cloned() .cloned()
.or_else(|| std::env::var("SMTP_FROM").ok()) .or_else(|| std::env::var("SMTP_FROM").ok())
.unwrap_or_else(|| panic!("Mail smtp_from config nor SMTP_FROM env was given")) .expect("Mail smtp_from config nor SMTP_FROM env was given")
} }
} }
@ -198,7 +245,11 @@ impl DatabaseConfig {
.as_ref() .as_ref()
.cloned() .cloned()
.or_else(|| std::env::var("DATABASE_URL").ok()) .or_else(|| std::env::var("DATABASE_URL").ok())
.unwrap_or_else(|| panic!("Database url nor DATABASE_URL env was given")) .expect("Database url nor DATABASE_URL env was given")
}
pub fn set_url<S: Into<String>>(&mut self, url: S) {
self.url = Some(url.into());
} }
} }
@ -228,15 +279,34 @@ impl AppConfig {
pub fn payment(&self) -> &PaymentConfig { pub fn payment(&self) -> &PaymentConfig {
&self.payment &self.payment
} }
pub fn web(&self) -> &WebConfig { pub fn web(&self) -> &WebConfig {
&self.web &self.web
} }
pub fn mail(&self) -> &MailConfig { pub fn mail(&self) -> &MailConfig {
&self.mail &self.mail
} }
pub fn database(&self) -> &DatabaseConfig { pub fn database(&self) -> &DatabaseConfig {
&self.database &self.database
} }
pub fn payment_mut(&mut self) -> &mut PaymentConfig {
&mut self.payment
}
pub fn web_mut(&mut self) -> &mut WebConfig {
&mut self.web
}
pub fn mail_mut(&mut self) -> &mut MailConfig {
&mut self.mail
}
pub fn database_mut(&mut self) -> &mut DatabaseConfig {
&mut self.database
}
} }
impl Default for AppConfig { impl Default for AppConfig {
@ -251,13 +321,13 @@ impl Default for AppConfig {
} }
} }
pub fn load(config_path: &str) -> AppConfig { pub fn load(config_path: &str) -> SharedAppConfig {
match std::fs::read_to_string(config_path) { match std::fs::read_to_string(config_path) {
Ok(c) => toml::from_str(&c).unwrap(), Ok(c) => SharedAppConfig::new(toml::from_str(&c).unwrap()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => { Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let config = AppConfig::example(); let config = AppConfig::example();
std::fs::write(config_path, toml::to_string_pretty(&config).unwrap()).unwrap(); std::fs::write(config_path, toml::to_string_pretty(&config).unwrap()).unwrap();
config SharedAppConfig::new(config)
} }
Err(e) => { Err(e) => {
log::error!("{e:?}"); log::error!("{e:?}");

View File

@ -1,5 +1,4 @@
use std::io::Write; use std::io::Write;
use std::sync::Arc;
use actix::Actor; use actix::Actor;
use actix_session::storage::RedisActorSessionStore; use actix_session::storage::RedisActorSessionStore;
@ -8,36 +7,30 @@ 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};
use gumdrop::Options;
use jemallocator::Jemalloc; use jemallocator::Jemalloc;
use opts::{
Command, CreateAccountCmd, CreateAccountOpts, GenerateHashOpts, MigrateOpts, Opts, ServerOpts,
TestMailerOpts,
};
use password_hash::SaltString; use password_hash::SaltString;
use pay_u::MerchantPosId;
use validator::{validate_email, validate_length}; use validator::{validate_email, validate_length};
use crate::actors::{database, email_manager, order_manager, payment_manager, token_manager}; use crate::actors::{database, email_manager, order_manager, payment_manager, token_manager};
use crate::email_manager::TestMail; use crate::email_manager::TestMail;
use crate::logic::encrypt_password; use crate::logic::encrypt_password;
use crate::model::{Email, Login, PassHash, Password, Role}; use crate::model::{Email, Login, PassHash, Password, Role};
use crate::opts::UpdateConfig;
pub mod actors; pub mod actors;
pub mod config; pub mod config;
pub mod logic; pub mod logic;
pub mod model; pub mod model;
mod opts;
pub mod routes; pub mod routes;
#[global_allocator] #[global_allocator]
static GLOBAL: Jemalloc = Jemalloc; static GLOBAL: Jemalloc = Jemalloc;
trait ResolveDbUrl {
fn own_db_url(&self) -> Option<String>;
fn db_url(&self) -> String {
self.own_db_url()
.or_else(|| std::env::var("DATABASE_URL").ok())
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
}
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("Failed to boot. {0:?}")] #[error("Failed to boot. {0:?}")]
@ -52,127 +45,6 @@ pub enum Error {
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
#[derive(Options, Debug)]
struct Opts {
help: bool,
#[options(command)]
cmd: Option<Command>,
}
#[derive(Options, Debug)]
enum Command {
#[options(help = "Run server")]
Server(ServerOpts),
#[options(help = "Migrate database")]
Migrate(MigrateOpts),
#[options(help = "Generate new salt for passwords")]
GenerateHash(GenerateHashOpts),
#[options(help = "Create new account")]
CreateAccount(CreateAccountOpts),
#[options(help = "Check mailer config")]
TestMailer(TestMailerOpts),
}
impl Default for Command {
fn default() -> Self {
Command::Server(ServerOpts::default())
}
}
#[derive(Options, Debug)]
struct GenerateHashOpts {
help: bool,
}
#[derive(Options, Debug)]
struct ServerOpts {
help: bool,
bind: String,
port: u16,
db_url: Option<String>,
}
impl Default for ServerOpts {
fn default() -> Self {
Self {
help: false,
bind: "0.0.0.0".to_string(),
port: 8080,
db_url: None,
}
}
}
impl ResolveDbUrl for ServerOpts {
fn own_db_url(&self) -> Option<String> {
self.db_url.as_deref().map(String::from)
}
}
#[derive(Options, Debug)]
pub struct TestMailerOpts {
help: bool,
#[options(help = "E-mail receiver")]
receiver: Option<Email>,
}
#[derive(Options, Debug)]
struct MigrateOpts {
help: bool,
db_url: Option<String>,
}
impl ResolveDbUrl for MigrateOpts {
fn own_db_url(&self) -> Option<String> {
self.db_url.as_deref().map(String::from)
}
}
#[derive(Debug, Options)]
struct CreateAccountOpts {
help: bool,
#[options(command)]
cmd: Option<CreateAccountCmd>,
}
#[derive(Debug, Options)]
enum CreateAccountCmd {
Admin(CreateAccountDefinition),
User(CreateAccountDefinition),
}
#[derive(Debug, Options)]
struct CreateAccountDefinition {
help: bool,
#[options(free)]
login: String,
#[options(free)]
email: String,
#[options(free)]
pass_file: Option<String>,
#[options(help = "Database url, it will also look for DATABASE_URL env")]
db_url: Option<String>,
}
impl ResolveDbUrl for CreateAccountDefinition {
fn own_db_url(&self) -> Option<String> {
self.db_url.as_deref().map(String::from)
}
}
pub struct Config {
pass_salt: SaltString,
}
impl Config {
fn load() -> Self {
let pass_salt =
SaltString::new(&std::env::var("PASS_SALT").expect("PASS_SALT is required"))
.expect("Invalid password salt");
Self { pass_salt }
}
}
async fn server(opts: ServerOpts) -> Result<()> { async fn server(opts: ServerOpts) -> Result<()> {
let secret_key = { let secret_key = {
let key_secret = std::env::var("SESSION_SECRET") let key_secret = std::env::var("SESSION_SECRET")
@ -181,28 +53,25 @@ async fn server(opts: ServerOpts) -> Result<()> {
}; };
let redis_connection_string = "127.0.0.1:6379"; let redis_connection_string = "127.0.0.1:6379";
let app_config = crate::config::load("./bazzar.toml"); let app_config = config::load("./bazzar.toml");
{
opts.update_config(&mut *app_config.lock());
}
let config = Arc::new(Config::load()); let db = database::Database::build(app_config.clone()).await?.start();
let db = database::Database::build(&opts.db_url()).await?.start(); let token_manager = token_manager::TokenManager::new(app_config.clone(), db.clone()).start();
let token_manager = token_manager::TokenManager::new(db.clone()).start(); let order_manager = order_manager::OrderManager::new(app_config.clone(), db.clone()).start();
let order_manager = order_manager::OrderManager::new(db.clone()).start(); let payment_manager = payment_manager::PaymentManager::build(app_config.clone(), db.clone())
let payment_manager = { .await
let client_id = std::env::var("PAYU_CLIENT_ID").expect("Missing PAYU_CLIENT_ID env"); .expect("Failed to start payment manager")
let client_secret = .start();
std::env::var("PAYU_CLIENT_SECRET").expect("Missing PAYU_CLIENT_SECRET env"); let addr = (
let merchant_id = std::env::var("PAYU_CLIENT_MERCHANT_ID") app_config.lock().web().bind().unwrap_or(opts.bind),
.expect("Missing PAYU_CLIENT_MERCHANT_ID env") app_config.lock().web().port().unwrap_or(opts.port),
.parse::<i32>() );
.map(MerchantPosId::from)
.expect("Variable PAYU_CLIENT_MERCHANT_ID must be number");
payment_manager::PaymentManager::build(client_id, client_secret, merchant_id, db.clone())
.await
.expect("Failed to start payment manager")
.start()
};
HttpServer::new(move || { HttpServer::new(move || {
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::Compress::default())
@ -211,7 +80,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
RedisActorSessionStore::new(redis_connection_string), RedisActorSessionStore::new(redis_connection_string),
secret_key.clone(), secret_key.clone(),
)) ))
.app_data(Data::new(config.clone())) .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()))
.app_data(Data::new(order_manager.clone())) .app_data(Data::new(order_manager.clone()))
@ -219,10 +88,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
.configure(routes::configure) .configure(routes::configure)
// .default_service(web::to(HttpResponse::Ok)) // .default_service(web::to(HttpResponse::Ok))
}) })
.bind(( .bind(addr)
app_config.web().bind().unwrap_or(opts.bind),
app_config.web().port().unwrap_or(opts.port),
))
.map_err(Error::Boot)? .map_err(Error::Boot)?
.run() .run()
.await .await
@ -232,7 +98,9 @@ async fn server(opts: ServerOpts) -> Result<()> {
async fn migrate(opts: MigrateOpts) -> Result<()> { async fn migrate(opts: MigrateOpts) -> Result<()> {
use sqlx::migrate::MigrateError; use sqlx::migrate::MigrateError;
let db = database::Database::build(&opts.db_url()).await?; let config = config::load("./bazzar.toml");
opts.update_config(&mut *config.lock());
let db = database::Database::build(config).await?;
let res: std::result::Result<(), MigrateError> = let res: std::result::Result<(), MigrateError> =
sqlx::migrate!("../db/migrate").run(db.pool()).await; sqlx::migrate!("../db/migrate").run(db.pool()).await;
match res { match res {
@ -262,7 +130,9 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
if !validate_length(&opts.login, Some(4), Some(100), None) { if !validate_length(&opts.login, Some(4), Some(100), None) {
panic!("Login must have at least 4 characters and no more than 100"); panic!("Login must have at least 4 characters and no more than 100");
} }
let db = database::Database::build(&opts.db_url()).await?.start(); let config = config::load("./bazzar.toml");
opts.update_config(&mut *config.lock());
let db = database::Database::build(config.clone()).await?.start();
let pass = match opts.pass_file { let pass = match opts.pass_file {
Some(path) => std::fs::read_to_string(path).map_err(Error::PassFile)?, Some(path) => std::fs::read_to_string(path).map_err(Error::PassFile)?,
None => { None => {
@ -287,8 +157,7 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
if pass.trim().is_empty() { if pass.trim().is_empty() {
panic!("Password cannot be empty!"); panic!("Password cannot be empty!");
} }
let config = Config::load(); let hash = encrypt_password(&Password::from(pass), &config.lock().web().pass_salt()).unwrap();
let hash = encrypt_password(&Password::from(pass), &config.pass_salt).unwrap();
db.send(database::CreateAccount { db.send(database::CreateAccount {
email: Email::from(opts.email), email: Email::from(opts.email),
@ -303,7 +172,10 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
} }
async fn test_mailer(opts: TestMailerOpts) -> Result<()> { async fn test_mailer(opts: TestMailerOpts) -> Result<()> {
let manager = email_manager::EmailManager::build() let config = config::load("./bazzar.toml");
opts.update_config(&mut *config.lock());
let manager = email_manager::EmailManager::build(config)
.expect("Invalid email manager config") .expect("Invalid email manager config")
.start(); .start();
if manager if manager

222
api/src/opts.rs Normal file
View File

@ -0,0 +1,222 @@
use gumdrop::Options;
use crate::config::AppConfig;
use crate::model::Email;
pub trait UpdateConfig {
fn update_config(&self, config: &mut AppConfig);
}
pub trait ResolveDbUrl {
fn own_db_url(&self) -> Option<String>;
fn db_url(&self) -> String {
self.own_db_url()
.or_else(|| std::env::var("DATABASE_URL").ok())
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
}
}
#[derive(Options, Debug)]
pub struct Opts {
pub help: bool,
#[options(command)]
pub cmd: Option<Command>,
}
impl UpdateConfig for Opts {
fn update_config(&self, config: &mut AppConfig) {
match &self.cmd {
None => {}
Some(cmd) => {
cmd.update_config(config);
}
}
}
}
#[derive(Options, Debug)]
pub enum Command {
#[options(help = "Run server")]
Server(ServerOpts),
#[options(help = "Migrate database")]
Migrate(MigrateOpts),
#[options(help = "Generate new salt for passwords")]
GenerateHash(GenerateHashOpts),
#[options(help = "Create new account")]
CreateAccount(CreateAccountOpts),
#[options(help = "Check mailer config")]
TestMailer(TestMailerOpts),
}
impl UpdateConfig for Command {
fn update_config(&self, config: &mut AppConfig) {
match self {
Command::Server(opts) => {
opts.update_config(config);
}
Command::Migrate(opts) => {
opts.update_config(config);
}
Command::GenerateHash(opts) => {
opts.update_config(config);
}
Command::CreateAccount(opts) => {
opts.update_config(config);
}
Command::TestMailer(opts) => {
opts.update_config(config);
}
}
}
}
impl Default for Command {
fn default() -> Self {
Command::Server(ServerOpts::default())
}
}
#[derive(Options, Debug)]
pub struct GenerateHashOpts {
pub help: bool,
}
impl UpdateConfig for GenerateHashOpts {
fn update_config(&self, _config: &mut AppConfig) {}
}
#[derive(Options, Debug)]
pub struct ServerOpts {
pub help: bool,
pub bind: String,
pub port: u16,
pub db_url: Option<String>,
}
impl Default for ServerOpts {
fn default() -> Self {
Self {
help: false,
bind: "0.0.0.0".to_string(),
port: 8080,
db_url: None,
}
}
}
impl UpdateConfig for ServerOpts {
fn update_config(&self, config: &mut AppConfig) {
{
let web = config.web_mut();
if web.bind().is_none() {
web.set_bind(&self.bind);
}
if web.port().is_none() {
web.set_port(self.port);
}
}
if let Some(url) = self.db_url.as_ref() {
config.database_mut().set_url(url);
}
}
}
impl ResolveDbUrl for ServerOpts {
fn own_db_url(&self) -> Option<String> {
self.db_url.as_deref().map(String::from)
}
}
#[derive(Options, Debug)]
pub struct TestMailerOpts {
pub help: bool,
#[options(help = "E-mail receiver")]
pub receiver: Option<Email>,
}
impl UpdateConfig for TestMailerOpts {
fn update_config(&self, _config: &mut AppConfig) {}
}
#[derive(Options, Debug)]
pub struct MigrateOpts {
pub help: bool,
pub db_url: Option<String>,
}
impl UpdateConfig for MigrateOpts {
fn update_config(&self, config: &mut AppConfig) {
if let Some(url) = self.db_url.as_deref() {
config.database_mut().set_url(url);
}
}
}
impl ResolveDbUrl for MigrateOpts {
fn own_db_url(&self) -> Option<String> {
self.db_url.as_deref().map(String::from)
}
}
#[derive(Debug, Options)]
pub struct CreateAccountOpts {
pub help: bool,
#[options(command)]
pub cmd: Option<CreateAccountCmd>,
}
impl UpdateConfig for CreateAccountOpts {
fn update_config(&self, config: &mut AppConfig) {
match &self.cmd {
None => {}
Some(opts) => opts.update_config(config),
};
}
}
#[derive(Debug, Options)]
pub enum CreateAccountCmd {
Admin(CreateAccountDefinition),
User(CreateAccountDefinition),
}
impl UpdateConfig for CreateAccountCmd {
fn update_config(&self, config: &mut AppConfig) {
match &self {
CreateAccountCmd::Admin(opts) => {
opts.update_config(config);
}
CreateAccountCmd::User(opts) => {
opts.update_config(config);
}
}
}
}
#[derive(Debug, Options)]
pub struct CreateAccountDefinition {
pub help: bool,
#[options(free)]
pub login: String,
#[options(free)]
pub email: String,
#[options(free)]
pub pass_file: Option<String>,
#[options(help = "Database url, it will also look for DATABASE_URL env")]
pub db_url: Option<String>,
}
impl UpdateConfig for CreateAccountDefinition {
fn update_config(&self, config: &mut AppConfig) {
if let Some(url) = self.db_url.as_deref() {
config.database_mut().set_url(url);
}
}
}
impl ResolveDbUrl for CreateAccountDefinition {
fn own_db_url(&self) -> Option<String> {
self.db_url.as_deref().map(String::from)
}
}

View File

@ -1,18 +1,17 @@
mod api_v1; mod api_v1;
use std::sync::Arc;
use actix::Addr; use actix::Addr;
use actix_session::Session; use actix_session::Session;
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, HttpResponse};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::config::SharedAppConfig;
use crate::database::{AccountByIdentity, Database}; use crate::database::{AccountByIdentity, Database};
use crate::logic::encrypt_password; use crate::logic::encrypt_password;
use crate::model::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role}; use crate::model::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role};
use crate::routes::{RequireLogin, Result}; use crate::routes::{RequireLogin, Result};
use crate::{database, model, routes, Config}; use crate::{database, model, routes};
#[macro_export] #[macro_export]
macro_rules! admin_send_db { macro_rules! admin_send_db {
@ -127,7 +126,7 @@ async fn register(
session: Session, session: Session,
Json(input): Json<RegisterInput>, Json(input): Json<RegisterInput>,
db: Data<Addr<Database>>, db: Data<Addr<Database>>,
config: Data<Arc<Config>>, config: Data<SharedAppConfig>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let mut response = RegisterResponse::default(); let mut response = RegisterResponse::default();
session.require_admin()?; session.require_admin()?;
@ -136,7 +135,7 @@ async fn register(
response.errors.push(RegisterError::PasswordDiffer); response.errors.push(RegisterError::PasswordDiffer);
} }
let hash = match encrypt_password(&input.password, &config.pass_salt) { let hash = match encrypt_password(&input.password, &config.lock().web().pass_salt()) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
log::error!("{e:?}"); log::error!("{e:?}");

View File

@ -1,17 +1,14 @@
use std::sync::Arc;
use actix::Addr; use actix::Addr;
use actix_session::Session; use actix_session::Session;
use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::web::{Data, Json, ServiceConfig};
use actix_web::{get, patch, post, HttpResponse}; use actix_web::{get, patch, post, HttpResponse};
use crate::config::SharedAppConfig;
use crate::database::{self, Database}; use crate::database::{self, Database};
use crate::model::{AccountId, AccountState, PasswordConfirmation}; use crate::model::{AccountId, AccountState, PasswordConfirmation};
use crate::routes::admin::Error; use crate::routes::admin::Error;
use crate::routes::RequireLogin; use crate::routes::RequireLogin;
use crate::{ use crate::{admin_send_db, encrypt_password, routes, Email, Login, PassHash, Password, Role};
admin_send_db, encrypt_password, routes, Config, Email, Login, PassHash, Password, Role,
};
#[get("/accounts")] #[get("/accounts")]
pub async fn accounts(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> { pub async fn accounts(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
@ -36,7 +33,7 @@ pub async fn update_account(
session: Session, session: Session,
db: Data<Addr<Database>>, db: Data<Addr<Database>>,
Json(payload): Json<UpdateAccountInput>, Json(payload): Json<UpdateAccountInput>,
config: Data<Arc<Config>>, config: Data<SharedAppConfig>,
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
session.require_admin()?; session.require_admin()?;
@ -48,7 +45,7 @@ pub async fn update_account(
routes::admin::Error::DifferentPasswords, routes::admin::Error::DifferentPasswords,
)); ));
} }
let hash = match encrypt_password(&p1, &config.pass_salt) { let hash = match encrypt_password(&p1, &config.lock().web().pass_salt()) {
Ok(hash) => hash, Ok(hash) => hash,
Err(e) => { Err(e) => {
log::error!("{e:?}"); log::error!("{e:?}");
@ -92,7 +89,7 @@ pub async fn create_account(
session: Session, session: Session,
db: Data<Addr<Database>>, db: Data<Addr<Database>>,
Json(payload): Json<CreateAccountInput>, Json(payload): Json<CreateAccountInput>,
config: Data<Arc<Config>>, config: Data<SharedAppConfig>,
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
session.require_admin()?; session.require_admin()?;
if payload.password != payload.password_confirmation { if payload.password != payload.password_confirmation {
@ -100,7 +97,7 @@ pub async fn create_account(
routes::admin::Error::DifferentPasswords, routes::admin::Error::DifferentPasswords,
)); ));
} }
let hash = match encrypt_password(&payload.password, &config.pass_salt) { let hash = match encrypt_password(&payload.password, &config.lock().web().pass_salt()) {
Ok(hash) => hash, Ok(hash) => hash,
Err(e) => { Err(e) => {
log::error!("{e:?}"); log::error!("{e:?}");

View File

@ -27,6 +27,8 @@ pub static SUCCESS: &str = "SUCCESS";
pub enum Error { pub enum Error {
#[error("Client is not authorized. No bearer token available")] #[error("Client is not authorized. No bearer token available")]
NoToken, NoToken,
#[error("Invalid customer ip. IP 0.0.0.0 is not acceptable")]
CustomerIp,
#[error("{0}")] #[error("{0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("Total value is not sum of products price")] #[error("Total value is not sum of products price")]
@ -316,9 +318,105 @@ impl Product {
} }
} }
/// MultiUseCartToken
pub mod muct {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CardOnFile {
/// Payment initialized by the card owner who agreed to save card for
/// future use. You can expect full authentication (3D Secure
/// and/or CVV). If you want to use multi-use token (TOKC_)
/// later, you have to be confident, that first payment was
/// successful. Default value for single-use token (TOK_).
///
/// In case of plain card data payments you should retrieve transaction
/// data to obtain first TransactionId. It should be passed in
/// payMethods.payMethod.card section for transactions marked as
/// STANDARD, STANDARD_CARDHOLDER and STANDARD_MERCHANT;
/// STANDARD_CARDHOLDER - payment with already saved card,
/// initialized by the card owner. This transaction has
/// multi-use token (TOKC_). Depending of payment parameters
/// (e.g. high transaction amount) strong authentication can be
/// expected (3D Secure and/or CVV). Default value for multi-use token
/// (TOKC_);
First,
/// Payment with already saved card, initialized by the shop without the
/// card owner participation. This transaction has multi-use token
/// (TOKC_). By the definition, this payment type does not
/// require strong authentication. You cannot use it if FIRST
/// card-on-file payment failed.
StandardMerchant,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Recurring {
/// Payment initialized by the card owner who agreed to save card for
/// future use in recurring plan. You can expect full authentication (3D
/// Secure and/or CVV). If you want to use multi-use token (TOKC_)
/// later, you have to be confident, that first recurring
/// payment was successful.
First,
/// Subsequent recurring payment (user is not present). This transaction
/// has multi use token (TOKC_). You cannot use it if FIRST recurring
/// payment failed.
Standard,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MultiUseCartToken {
/// Information about party initializing order:
///
/// * `FIRST` - payment initialized by the card owner who agreed to save
/// card for future use. You can expect full authentication (3D Secure
/// and/or CVV). If you want to use multi-use token (TOKC_) later, you
/// have to be confident, that first payment was successful. Default
/// value for single-use token (TOK_).
///
/// In case of plain card data payments you should retrieve
/// transaction data to obtain first TransactionId. It should
/// be passed in payMethods.payMethod.card section for
/// transactions marked as STANDARD, STANDARD_CARDHOLDER and
/// STANDARD_MERCHANT; STANDARD_CARDHOLDER - payment with
/// already saved card, initialized by the card owner. This
/// transaction has multi-use token (TOKC_). Depending of payment
/// parameters (e.g. high transaction amount) strong authentication
/// can be expected (3D Secure and/or CVV). Default value for
/// multi-use token (TOKC_);
/// * `STANDARD_MERCHANT` - payment with already saved card, initialized
/// by the shop without the card owner participation. This transaction
/// has multi-use token (TOKC_). By the definition, this payment type
/// does not require strong authentication. You cannot use it if FIRST
/// card-on-file payment failed.
///
/// `cardOnFile` parameter cannot be used with recurring parameter.
pub card_on_file: CardOnFile,
/// Marks the order as recurring payment.
///
/// * `FIRST` - payment initialized by the card owner who agreed to save
/// card for future use in recurring plan. You can expect full
/// authentication (3D Secure and/or CVV). If you want to use
/// multi-use token (TOKC_) later, you have to be confident, that
/// first recurring payment was successful.
/// * `STANDARD` - subsequent recurring payment (user is not present).
/// This transaction has multi use token (TOKC_). You cannot use it if
/// FIRST recurring payment failed.
///
/// `recurring` parameter cannot be used with cardOnFile parameter.
pub recurring: Recurring,
}
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OrderCreateRequest { pub struct OrderCreateRequest {
/// ID of an order used in merchant system. Order identifier assigned by the
/// merchant. It enables merchants to find a specific order in their system.
/// This value must be unique within a single POS.
ext_order_id: Option<String>,
/// URL to which web hook will be send. It's important to return 200 to all /// URL to which web hook will be send. It's important to return 200 to all
/// notifications. /// notifications.
/// ///
@ -352,7 +450,22 @@ pub struct OrderCreateRequest {
/// | 20| 72 hours | /// | 20| 72 hours |
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
notify_url: Option<String>, notify_url: Option<String>,
/// Customer client IP address /// Address for redirecting the customer after payment is commenced. If the
/// payment has not been authorized, error=501 parameter will be added.
/// Please note that no decision regarding payment status should be made
/// depending on the presence or lack of this parameter (to get payment
/// status, wait for notification or retrieve order details).
///
/// IMPORTANT: the address must be compliant with the structure below:
/// <img src="https://developers.payu.com/images/continueUrlStructure_en.png" />
///
/// Please keep in mind:
/// * accepted schemas are http and https,
/// * such elements as port, path, query and fragment are optional,
/// * query values must be encoded.
#[serde(skip_serializing_if = "Option::is_none")]
continue_url: Option<String>,
/// Payers IP address, e.g. 123.123.123.123. Note: 0.0.0.0 is not accepted.
customer_ip: String, customer_ip: String,
/// Secret pos ip. This is connected to PayU account /// Secret pos ip. This is connected to PayU account
#[serde( #[serde(
@ -377,29 +490,106 @@ pub struct OrderCreateRequest {
products: Vec<Product>, products: Vec<Product>,
#[serde(skip_serializing)] #[serde(skip_serializing)]
order_create_date: Option<String>, order_create_date: Option<String>,
/// Duration for the validity of an order (in seconds), during which time
/// payment must be made. Default value 86400.
#[serde(skip_serializing_if = "Option::is_none")]
validity_time: Option<u16>,
/// Additional description of the order.
#[serde(skip_serializing_if = "Option::is_none")]
additional_description: Option<String>,
/// Text visible on the PayU payment page (max. 80 chars).
#[serde(skip_serializing_if = "Option::is_none")]
visible_description: Option<String>,
/// Payment recipient name followed by payment description (order ID, ticket
/// number etc) visible on card statement (max. 22 chars). The name should
/// be easy to recognize by the cardholder (e.g "shop.com 124343"). If field
/// is not provided, static name configured by PayU will be used.
statement_description: Option<String>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
muct: Option<muct::MultiUseCartToken>,
} }
impl OrderCreateRequest { impl OrderCreateRequest {
pub fn new<CustomerIp, Currency>( pub fn build<CustomerIp, Currency, Description>(
buyer: Buyer, buyer: Buyer,
customer_ip: CustomerIp, customer_ip: CustomerIp,
currency: Currency, currency: Currency,
) -> Self description: Description,
) -> Result<Self>
where where
CustomerIp: Into<String>, CustomerIp: Into<String>,
Currency: Into<String>, Currency: Into<String>,
Description: Into<String>,
{ {
Self { let customer_ip = customer_ip.into();
if &customer_ip == "0.0.0.0" {
return Err(Error::CustomerIp);
}
Ok(Self {
ext_order_id: None,
notify_url: None, notify_url: None,
customer_ip: customer_ip.into(), continue_url: None,
customer_ip,
merchant_pos_id: 0.into(), merchant_pos_id: 0.into(),
description: String::from(""), description: description.into(),
currency_code: currency.into(), currency_code: currency.into(),
total_amount: 0, total_amount: 0,
buyer: Some(buyer), buyer: Some(buyer),
products: Vec::new(), products: Vec::new(),
order_create_date: None, order_create_date: None,
} validity_time: None,
additional_description: None,
visible_description: None,
statement_description: None,
muct: None,
})
}
/// ID of an order used in merchant system. Order identifier assigned by the
/// merchant. It enables merchants to find a specific order in their system.
/// This value must be unique within a single POS.
pub fn with_ext_order_id<S: Into<String>>(mut self, ext_order_id: S) -> Self {
self.ext_order_id = Some(ext_order_id.into());
self
}
/// Duration for the validity of an order (in seconds), during which time
/// payment must be made. Default value 86400.
pub fn with_validity_time(mut self, validity_time: u16) -> Self {
self.validity_time = Some(validity_time);
self
}
/// Additional description of the order.
pub fn with_additional_description<S: Into<String>>(
mut self,
additional_description: S,
) -> Self {
self.additional_description = Some(additional_description.into());
self
}
/// Text visible on the PayU payment page (max. 80 chars).
pub fn with_visible_description(mut self, visible_description: &str) -> Self {
let visible_description = if visible_description.len() > 60 {
&visible_description[..60]
} else {
visible_description
};
self.visible_description = Some(String::from(visible_description));
self
}
pub fn with_multi_use_token(
mut self,
recurring: muct::Recurring,
card_on_file: muct::CardOnFile,
) -> Self {
self.muct = Some(muct::MultiUseCartToken {
recurring,
card_on_file,
});
self
} }
pub fn with_products<Products>(mut self, products: Products) -> Self pub fn with_products<Products>(mut self, products: Products) -> Self