Add many additional create order fields
This commit is contained in:
parent
9d6534c2a2
commit
9658abe3b8
@ -9,6 +9,8 @@ use sqlx::PgPool;
|
||||
pub use stocks::*;
|
||||
pub use tokens::*;
|
||||
|
||||
use crate::config::SharedAppConfig;
|
||||
|
||||
pub mod account_orders;
|
||||
pub mod accounts;
|
||||
pub mod order_items;
|
||||
@ -72,8 +74,9 @@ impl Clone for Database {
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub(crate) async fn build(url: &str) -> Result<Self> {
|
||||
let pool = sqlx::PgPool::connect(url).await.map_err(Error::Connect)?;
|
||||
pub(crate) async fn build(config: SharedAppConfig) -> Result<Self> {
|
||||
let url = config.lock().database().url();
|
||||
let pool = sqlx::PgPool::connect(&url).await.map_err(Error::Connect)?;
|
||||
Ok(Database { pool })
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::SharedAppConfig;
|
||||
use crate::Email;
|
||||
|
||||
#[macro_export]
|
||||
@ -29,7 +30,7 @@ pub struct SendState {
|
||||
pub struct EmailManager(Arc<Inner>);
|
||||
|
||||
pub(crate) struct Inner {
|
||||
from: Email,
|
||||
config: SharedAppConfig,
|
||||
send_grid: sendgrid::SGClient,
|
||||
}
|
||||
|
||||
@ -38,14 +39,10 @@ impl actix::Actor for EmailManager {
|
||||
}
|
||||
|
||||
impl EmailManager {
|
||||
pub fn build() -> Result<Self> {
|
||||
let from = std::env::var("SMTP_FROM").expect("Missing SMTP_FROM variable");
|
||||
|
||||
pub fn build(config: SharedAppConfig) -> Result<Self> {
|
||||
Ok(Self(Arc::new(Inner {
|
||||
from: Email::from(from),
|
||||
send_grid: sendgrid::SGClient::new(
|
||||
std::env::var("SENDGRID_SECRET").expect("Missing SENDGRID_SECRET variable"),
|
||||
),
|
||||
config: config.clone(),
|
||||
send_grid: sendgrid::SGClient::new(config.lock().mail().sendgrid_secret()),
|
||||
})))
|
||||
}
|
||||
}
|
||||
@ -59,13 +56,12 @@ pub struct TestMail {
|
||||
mail_async_handler!(TestMail, test_mail, SendState);
|
||||
|
||||
pub(crate) async fn test_mail(msg: TestMail, inner: Arc<Inner>) -> Result<SendState> {
|
||||
let from: &str = &*inner.from;
|
||||
let status = inner
|
||||
.send_grid
|
||||
.send(
|
||||
sendgrid::Mail::new()
|
||||
.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_html("<h1>Test e-mail</h1>")
|
||||
.build(),
|
||||
|
@ -1,5 +1,6 @@
|
||||
use actix::Message;
|
||||
|
||||
use crate::config::SharedAppConfig;
|
||||
use crate::database::{self, SharedDatabase};
|
||||
use crate::model::{
|
||||
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 {
|
||||
use actix::WrapFuture;
|
||||
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 {
|
||||
db: SharedDatabase,
|
||||
config: SharedAppConfig,
|
||||
}
|
||||
|
||||
impl actix::Actor for OrderManager {
|
||||
@ -41,8 +44,8 @@ impl actix::Actor for OrderManager {
|
||||
}
|
||||
|
||||
impl OrderManager {
|
||||
pub fn new(db: SharedDatabase) -> Self {
|
||||
Self { db }
|
||||
pub fn new(config: SharedAppConfig, db: SharedDatabase) -> Self {
|
||||
Self { db, config }
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +58,11 @@ pub struct CreateOrder {
|
||||
|
||||
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
|
||||
.send(database::FindShoppingCart {
|
||||
id: msg.shopping_cart_id,
|
||||
|
@ -2,8 +2,9 @@ use std::sync::Arc;
|
||||
|
||||
use actix::Addr;
|
||||
use parking_lot::Mutex;
|
||||
use pay_u::{MerchantPosId, OrderCreateRequest};
|
||||
use pay_u::OrderCreateRequest;
|
||||
|
||||
use crate::config::SharedAppConfig;
|
||||
use crate::database;
|
||||
use crate::database::Database;
|
||||
use crate::model::{AccountId, Price, ProductId, Quantity, QuantityUnit, ShoppingCartId};
|
||||
@ -43,18 +44,12 @@ pub struct PaymentManager {
|
||||
}
|
||||
|
||||
impl PaymentManager {
|
||||
pub async fn build<ClientId, ClientSecret>(
|
||||
client_id: ClientId,
|
||||
client_secret: ClientSecret,
|
||||
merchant_pos_id: MerchantPosId,
|
||||
db: Addr<Database>,
|
||||
) -> 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);
|
||||
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(),
|
||||
);
|
||||
client.authorize().await?;
|
||||
Ok(Self {
|
||||
client: Arc::new(Mutex::new(client)),
|
||||
@ -154,14 +149,16 @@ pub(crate) async fn request_payment(
|
||||
}
|
||||
};
|
||||
|
||||
let mut client = client.lock();
|
||||
let order = client
|
||||
.create_order(
|
||||
OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency)
|
||||
.with_description(msg.description)
|
||||
.with_notify_url(msg.redirect_uri)
|
||||
.with_products(msg.products.into_iter().map(Into::into)),
|
||||
)
|
||||
.await?;
|
||||
let order = {
|
||||
client
|
||||
.lock()
|
||||
.create_order(
|
||||
OrderCreateRequest::new(msg.buyer.into(), msg.customer_ip, msg.currency)
|
||||
.with_description(msg.description)
|
||||
.with_notify_url(msg.redirect_uri)
|
||||
.with_products(msg.products.into_iter().map(Into::into)),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
Ok(order.order_id)
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix::{Addr, Message};
|
||||
use chrono::prelude::*;
|
||||
@ -8,6 +7,7 @@ use hmac::digest::KeyInit;
|
||||
use hmac::Hmac;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::config::SharedAppConfig;
|
||||
use crate::database::{Database, TokenByJti};
|
||||
use crate::model::{AccountId, Audience, Token, TokenString};
|
||||
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 {
|
||||
use actix::WrapFuture;
|
||||
let db = self.db.clone();
|
||||
let secret = self.secret.clone();
|
||||
Box::pin(async { $async(msg, db, secret).await }.into_actor(self))
|
||||
let config = self.config.clone();
|
||||
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 {
|
||||
db: Addr<Database>,
|
||||
secret: Arc<String>,
|
||||
config: SharedAppConfig,
|
||||
}
|
||||
|
||||
impl actix::Actor for TokenManager {
|
||||
@ -76,9 +76,8 @@ impl actix::Actor for TokenManager {
|
||||
}
|
||||
|
||||
impl TokenManager {
|
||||
pub fn new(db: Addr<Database>) -> Self {
|
||||
let secret = Arc::new(std::env::var("JWT_SECRET").expect("JWT_SECRET is required"));
|
||||
Self { db, secret }
|
||||
pub fn new(config: SharedAppConfig, db: Addr<Database>) -> Self {
|
||||
Self { db, config }
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +95,7 @@ token_async_handler!(CreateToken, create_token, (Token, TokenString));
|
||||
pub(crate) async fn create_token(
|
||||
msg: CreateToken,
|
||||
db: Addr<Database>,
|
||||
secret: Arc<String>,
|
||||
config: SharedAppConfig,
|
||||
) -> Result<(Token, TokenString)> {
|
||||
let CreateToken {
|
||||
customer_id,
|
||||
@ -129,6 +128,7 @@ pub(crate) async fn create_token(
|
||||
let token_string = {
|
||||
use jwt::SignWithKey;
|
||||
|
||||
let secret = config.lock().web().jwt_secret();
|
||||
let key: Hmac<Sha256> = build_key(secret)?;
|
||||
let mut claims = BTreeMap::new();
|
||||
|
||||
@ -195,12 +195,13 @@ token_async_handler!(Validate, validate, (Token, bool));
|
||||
pub(crate) async fn validate(
|
||||
msg: Validate,
|
||||
db: Addr<Database>,
|
||||
secret: Arc<String>,
|
||||
config: SharedAppConfig,
|
||||
) -> Result<(Token, bool)> {
|
||||
use jwt::VerifyWithKey;
|
||||
|
||||
log::info!("Validating token {:?}", msg.token);
|
||||
|
||||
let secret = config.lock().web().jwt_secret();
|
||||
let key: Hmac<Sha256> = build_key(secret)?;
|
||||
let claims: BTreeMap<String, String> = match msg.token.verify_with_key(&key) {
|
||||
Ok(claims) => claims,
|
||||
@ -260,7 +261,7 @@ pub(crate) async fn validate(
|
||||
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()) {
|
||||
Ok(key) => Ok(key),
|
||||
Err(e) => {
|
||||
|
@ -1,9 +1,36 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use password_hash::SaltString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
trait Example: Sized {
|
||||
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)]
|
||||
pub struct PaymentConfig {
|
||||
payu_client_id: Option<pay_u::ClientId>,
|
||||
@ -32,10 +59,9 @@ impl PaymentConfig {
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| std::env::var("PAYU_CLIENT_ID").ok().map(pay_u::ClientId))
|
||||
.unwrap_or_else(|| {
|
||||
panic!("payment config payu_client_id nor PAYU_CLIENT_ID env was given")
|
||||
})
|
||||
.expect("payment config payu_client_id nor PAYU_CLIENT_ID env was given")
|
||||
}
|
||||
|
||||
pub fn payu_client_secret(&self) -> pay_u::ClientSecret {
|
||||
self.payu_client_secret
|
||||
.as_ref()
|
||||
@ -45,14 +71,20 @@ impl PaymentConfig {
|
||||
.ok()
|
||||
.map(pay_u::ClientSecret)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!("payment config payu_client_secret nor PAYU_CLIENT_SECRET env was given")
|
||||
})
|
||||
.expect("payment config payu_client_secret nor PAYU_CLIENT_SECRET env was given")
|
||||
}
|
||||
|
||||
pub fn payu_client_merchant_id(&self) -> pay_u::MerchantPosId {
|
||||
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))
|
||||
.unwrap_or_else(|| panic!("payment config payu_client_merchant_id nor PAYU_CLIENT_MERCHANT_ID env was given"))
|
||||
.or_else(|| {
|
||||
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()
|
||||
.cloned()
|
||||
.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
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| std::env::var("PASS_SALT").ok())
|
||||
.unwrap_or_else(|| panic!("Web config pass_salt nor PASS_SALT env was given"))
|
||||
|
||||
pub fn pass_salt(&self) -> SaltString {
|
||||
SaltString::new(
|
||||
&self
|
||||
.pass_salt
|
||||
.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 {
|
||||
self.session_secret
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.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 {
|
||||
self.jwt_secret
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.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> {
|
||||
self.bind
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.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> {
|
||||
self.port.as_ref().copied().or_else(|| {
|
||||
std::env::var("BAZZAR_PORT")
|
||||
@ -126,6 +171,10 @@ impl WebConfig {
|
||||
.and_then(|s| s.parse::<u16>().ok())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_port(&mut self, port: u16) {
|
||||
self.port = Some(port);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
@ -157,25 +206,23 @@ impl MailConfig {
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| std::env::var("SENDGRID_SECRET").ok())
|
||||
.unwrap_or_else(|| {
|
||||
panic!("Mail sendgrid_secret config nor SENDGRID_SECRET env was given")
|
||||
})
|
||||
.expect("Mail sendgrid_secret config nor SENDGRID_SECRET env was given")
|
||||
}
|
||||
|
||||
pub fn sendgrid_api_key(&self) -> String {
|
||||
self.sendgrid_api_key
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| std::env::var("SENDGRID_API_KEY").ok())
|
||||
.unwrap_or_else(|| {
|
||||
panic!("Mail sendgrid_api_key config nor SENDGRID_API_KEY env was given")
|
||||
})
|
||||
.expect("Mail sendgrid_api_key config nor SENDGRID_API_KEY env was given")
|
||||
}
|
||||
|
||||
pub fn smtp_from(&self) -> String {
|
||||
self.smtp_from
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.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()
|
||||
.cloned()
|
||||
.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 {
|
||||
&self.payment
|
||||
}
|
||||
|
||||
pub fn web(&self) -> &WebConfig {
|
||||
&self.web
|
||||
}
|
||||
|
||||
pub fn mail(&self) -> &MailConfig {
|
||||
&self.mail
|
||||
}
|
||||
|
||||
pub fn database(&self) -> &DatabaseConfig {
|
||||
&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 {
|
||||
@ -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) {
|
||||
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 => {
|
||||
let config = AppConfig::example();
|
||||
std::fs::write(config_path, toml::to_string_pretty(&config).unwrap()).unwrap();
|
||||
config
|
||||
SharedAppConfig::new(config)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
|
198
api/src/main.rs
198
api/src/main.rs
@ -1,5 +1,4 @@
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix::Actor;
|
||||
use actix_session::storage::RedisActorSessionStore;
|
||||
@ -8,36 +7,30 @@ use actix_web::cookie::Key;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{App, HttpServer};
|
||||
use gumdrop::Options;
|
||||
use jemallocator::Jemalloc;
|
||||
use opts::{
|
||||
Command, CreateAccountCmd, CreateAccountOpts, GenerateHashOpts, MigrateOpts, Opts, ServerOpts,
|
||||
TestMailerOpts,
|
||||
};
|
||||
use password_hash::SaltString;
|
||||
use pay_u::MerchantPosId;
|
||||
use validator::{validate_email, validate_length};
|
||||
|
||||
use crate::actors::{database, email_manager, order_manager, payment_manager, token_manager};
|
||||
use crate::email_manager::TestMail;
|
||||
use crate::logic::encrypt_password;
|
||||
use crate::model::{Email, Login, PassHash, Password, Role};
|
||||
use crate::opts::UpdateConfig;
|
||||
|
||||
pub mod actors;
|
||||
pub mod config;
|
||||
pub mod logic;
|
||||
pub mod model;
|
||||
mod opts;
|
||||
pub mod routes;
|
||||
|
||||
#[global_allocator]
|
||||
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)]
|
||||
pub enum Error {
|
||||
#[error("Failed to boot. {0:?}")]
|
||||
@ -52,127 +45,6 @@ pub enum 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<()> {
|
||||
let secret_key = {
|
||||
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 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(&opts.db_url()).await?.start();
|
||||
let token_manager = token_manager::TokenManager::new(db.clone()).start();
|
||||
let order_manager = order_manager::OrderManager::new(db.clone()).start();
|
||||
let payment_manager = {
|
||||
let client_id = std::env::var("PAYU_CLIENT_ID").expect("Missing PAYU_CLIENT_ID env");
|
||||
let client_secret =
|
||||
std::env::var("PAYU_CLIENT_SECRET").expect("Missing PAYU_CLIENT_SECRET env");
|
||||
let merchant_id = std::env::var("PAYU_CLIENT_MERCHANT_ID")
|
||||
.expect("Missing PAYU_CLIENT_MERCHANT_ID env")
|
||||
.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()
|
||||
};
|
||||
let db = database::Database::build(app_config.clone()).await?.start();
|
||||
let token_manager = token_manager::TokenManager::new(app_config.clone(), db.clone()).start();
|
||||
let order_manager = order_manager::OrderManager::new(app_config.clone(), db.clone()).start();
|
||||
let payment_manager = payment_manager::PaymentManager::build(app_config.clone(), db.clone())
|
||||
.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),
|
||||
);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let config = app_config.clone();
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(actix_web::middleware::Compress::default())
|
||||
@ -211,7 +80,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
|
||||
RedisActorSessionStore::new(redis_connection_string),
|
||||
secret_key.clone(),
|
||||
))
|
||||
.app_data(Data::new(config.clone()))
|
||||
.app_data(Data::new(config))
|
||||
.app_data(Data::new(db.clone()))
|
||||
.app_data(Data::new(token_manager.clone()))
|
||||
.app_data(Data::new(order_manager.clone()))
|
||||
@ -219,10 +88,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
|
||||
.configure(routes::configure)
|
||||
// .default_service(web::to(HttpResponse::Ok))
|
||||
})
|
||||
.bind((
|
||||
app_config.web().bind().unwrap_or(opts.bind),
|
||||
app_config.web().port().unwrap_or(opts.port),
|
||||
))
|
||||
.bind(addr)
|
||||
.map_err(Error::Boot)?
|
||||
.run()
|
||||
.await
|
||||
@ -232,7 +98,9 @@ async fn server(opts: ServerOpts) -> Result<()> {
|
||||
async fn migrate(opts: MigrateOpts) -> Result<()> {
|
||||
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> =
|
||||
sqlx::migrate!("../db/migrate").run(db.pool()).await;
|
||||
match res {
|
||||
@ -262,7 +130,9 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
|
||||
if !validate_length(&opts.login, Some(4), Some(100), None) {
|
||||
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 {
|
||||
Some(path) => std::fs::read_to_string(path).map_err(Error::PassFile)?,
|
||||
None => {
|
||||
@ -287,8 +157,7 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
|
||||
if pass.trim().is_empty() {
|
||||
panic!("Password cannot be empty!");
|
||||
}
|
||||
let config = Config::load();
|
||||
let hash = encrypt_password(&Password::from(pass), &config.pass_salt).unwrap();
|
||||
let hash = encrypt_password(&Password::from(pass), &config.lock().web().pass_salt()).unwrap();
|
||||
|
||||
db.send(database::CreateAccount {
|
||||
email: Email::from(opts.email),
|
||||
@ -303,7 +172,10 @@ async fn create_account(opts: CreateAccountOpts) -> 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")
|
||||
.start();
|
||||
if manager
|
||||
|
222
api/src/opts.rs
Normal file
222
api/src/opts.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -1,18 +1,17 @@
|
||||
mod api_v1;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_session::Session;
|
||||
use actix_web::web::{scope, Data, Json, ServiceConfig};
|
||||
use actix_web::{delete, get, post, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::SharedAppConfig;
|
||||
use crate::database::{AccountByIdentity, Database};
|
||||
use crate::logic::encrypt_password;
|
||||
use crate::model::{Account, Email, Login, PassHash, Password, PasswordConfirmation, Role};
|
||||
use crate::routes::{RequireLogin, Result};
|
||||
use crate::{database, model, routes, Config};
|
||||
use crate::{database, model, routes};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! admin_send_db {
|
||||
@ -127,7 +126,7 @@ async fn register(
|
||||
session: Session,
|
||||
Json(input): Json<RegisterInput>,
|
||||
db: Data<Addr<Database>>,
|
||||
config: Data<Arc<Config>>,
|
||||
config: Data<SharedAppConfig>,
|
||||
) -> Result<HttpResponse> {
|
||||
let mut response = RegisterResponse::default();
|
||||
session.require_admin()?;
|
||||
@ -136,7 +135,7 @@ async fn register(
|
||||
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,
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
|
@ -1,17 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_session::Session;
|
||||
use actix_web::web::{Data, Json, ServiceConfig};
|
||||
use actix_web::{get, patch, post, HttpResponse};
|
||||
|
||||
use crate::config::SharedAppConfig;
|
||||
use crate::database::{self, Database};
|
||||
use crate::model::{AccountId, AccountState, PasswordConfirmation};
|
||||
use crate::routes::admin::Error;
|
||||
use crate::routes::RequireLogin;
|
||||
use crate::{
|
||||
admin_send_db, encrypt_password, routes, Config, Email, Login, PassHash, Password, Role,
|
||||
};
|
||||
use crate::{admin_send_db, encrypt_password, routes, Email, Login, PassHash, Password, Role};
|
||||
|
||||
#[get("/accounts")]
|
||||
pub async fn accounts(session: Session, db: Data<Addr<Database>>) -> routes::Result<HttpResponse> {
|
||||
@ -36,7 +33,7 @@ pub async fn update_account(
|
||||
session: Session,
|
||||
db: Data<Addr<Database>>,
|
||||
Json(payload): Json<UpdateAccountInput>,
|
||||
config: Data<Arc<Config>>,
|
||||
config: Data<SharedAppConfig>,
|
||||
) -> routes::Result<HttpResponse> {
|
||||
session.require_admin()?;
|
||||
|
||||
@ -48,7 +45,7 @@ pub async fn update_account(
|
||||
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,
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
@ -92,7 +89,7 @@ pub async fn create_account(
|
||||
session: Session,
|
||||
db: Data<Addr<Database>>,
|
||||
Json(payload): Json<CreateAccountInput>,
|
||||
config: Data<Arc<Config>>,
|
||||
config: Data<SharedAppConfig>,
|
||||
) -> routes::Result<HttpResponse> {
|
||||
session.require_admin()?;
|
||||
if payload.password != payload.password_confirmation {
|
||||
@ -100,7 +97,7 @@ pub async fn create_account(
|
||||
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,
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
|
204
pay_u/src/lib.rs
204
pay_u/src/lib.rs
@ -27,6 +27,8 @@ pub static SUCCESS: &str = "SUCCESS";
|
||||
pub enum Error {
|
||||
#[error("Client is not authorized. No bearer token available")]
|
||||
NoToken,
|
||||
#[error("Invalid customer ip. IP 0.0.0.0 is not acceptable")]
|
||||
CustomerIp,
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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
|
||||
/// notifications.
|
||||
///
|
||||
@ -352,7 +450,22 @@ pub struct OrderCreateRequest {
|
||||
/// | 20| 72 hours |
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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>,
|
||||
/// Payer’s IP address, e.g. 123.123.123.123. Note: 0.0.0.0 is not accepted.
|
||||
customer_ip: String,
|
||||
/// Secret pos ip. This is connected to PayU account
|
||||
#[serde(
|
||||
@ -377,29 +490,106 @@ pub struct OrderCreateRequest {
|
||||
products: Vec<Product>,
|
||||
#[serde(skip_serializing)]
|
||||
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 {
|
||||
pub fn new<CustomerIp, Currency>(
|
||||
pub fn build<CustomerIp, Currency, Description>(
|
||||
buyer: Buyer,
|
||||
customer_ip: CustomerIp,
|
||||
currency: Currency,
|
||||
) -> Self
|
||||
description: Description,
|
||||
) -> Result<Self>
|
||||
where
|
||||
CustomerIp: 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,
|
||||
customer_ip: customer_ip.into(),
|
||||
continue_url: None,
|
||||
customer_ip,
|
||||
merchant_pos_id: 0.into(),
|
||||
description: String::from(""),
|
||||
description: description.into(),
|
||||
currency_code: currency.into(),
|
||||
total_amount: 0,
|
||||
buyer: Some(buyer),
|
||||
products: Vec::new(),
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user