Mailer and payment

This commit is contained in:
Adrian Woźniak 2022-04-21 16:06:34 +02:00
parent 7634d4ccb1
commit 4943e370b6
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
9 changed files with 681 additions and 84 deletions

11
.env
View File

@ -4,8 +4,9 @@ RUST_LOG=debug
KEY_SECRET="NEPJs#8jjn8SK8GC7QEC^*P844UgsyEbQB8mRWXkT%3mPrwewZoc25MMby9H#R*w2KzaQgMkk#Pif$kxrLy*N5L!Ch%jxbWoa%gb"
JWT_SECRET="42^iFq&ZnQbUf!hwGWXd&CpyY6QQyJmkPU%esFCvne5&Ejcb3nJ4&GyHZp!MArZLf^9*5c6!!VgM$iZ8T%d#&bWTi&xbZk2S@4RN"
PGDATESTYLE=
SENDMAIL_SECRET=1a9dd7ba41edc5fd030e2281c52b7b49-02fa25a3-02e6d387
SENDMAIL_KEY_ID=02fa25a3-02e6d387
SMTP_HOST=smtp.mailgun.org
SMTP_USER=bazzar@sandbox69e5633f64264d8c973b3c82408dfb17.mailgun.org
SMTP_PASSWORD=49940d86d0742959f677c74cc542abb0-02fa25a3-f65fc7b4
SENDGRID_SECRET=SG.CUWRM-eoQfGJNqSU2bbwkg.NW5aBy5vZueCSOwIIyWUBqq5USChGiwAFrWzreBKvOU
SENDGRID_API_KEY=CUWRM-eoQfGJNqSU2bbwkg
SMTP_FROM=adrian.wozniak@ita-prog.pl
PAY_U_BEARER=d9a4536e-62ba-4f60-8017-6053211d3f47

283
Cargo.lock generated
View File

@ -609,13 +609,13 @@ dependencies = [
"hmac",
"jemallocator",
"jwt",
"lettre",
"log",
"oauth2",
"parking_lot 0.12.0",
"password-hash",
"pretty_env_logger",
"rand_core",
"sendgrid",
"serde",
"serde_json",
"sha2 0.10.2",
@ -815,6 +815,12 @@ dependencies = [
"phf_codegen",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "cipher"
version = "0.3.0"
@ -854,6 +860,22 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.2"
@ -961,6 +983,12 @@ dependencies = [
"cipher",
]
[[package]]
name = "data-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "derive_more"
version = "0.99.17"
@ -1047,15 +1075,6 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "email-encoding"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6690291166824e467790ac08ba42f241791567e8337bbf00c5a6e87889629f98"
dependencies = [
"base64",
]
[[package]]
name = "encoding_rs"
version = "0.8.31"
@ -1123,6 +1142,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
@ -1417,17 +1451,6 @@ dependencies = [
"digest 0.10.3",
]
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]]
name = "http"
version = "0.2.6"
@ -1520,6 +1543,19 @@ dependencies = [
"tokio-rustls 0.23.3",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "idna"
version = "0.2.3"
@ -1661,34 +1697,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lettre"
version = "0.10.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5144148f337be14dabfc0f0d85b691a68ac6c77ef22a5c47c5504b70a7c9fcf3"
dependencies = [
"async-trait",
"base64",
"email-encoding",
"fastrand",
"futures-io",
"futures-util",
"hostname",
"httpdate",
"idna",
"mime",
"nom",
"once_cell",
"quoted_printable",
"regex",
"rustls 0.20.4",
"rustls-pemfile",
"tokio",
"tokio-rustls 0.23.3",
"tracing",
"webpki-roots 0.22.3",
]
[[package]]
name = "libc"
version = "0.2.123"
@ -1772,12 +1780,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matches"
version = "0.1.9"
@ -1862,6 +1864,24 @@ dependencies = [
"winapi",
]
[[package]]
name = "native-tls"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nix"
version = "0.23.1"
@ -1981,6 +2001,39 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-sys",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "opentelemetry"
version = "0.17.0"
@ -2085,6 +2138,18 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc"
[[package]]
name = "pay_u"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
"serde_json",
"thiserror",
"tokio",
"ureq",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@ -2269,12 +2334,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "quoted_printable"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f"
[[package]]
name = "rand"
version = "0.8.5"
@ -2381,6 +2440,15 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "reqwest"
version = "0.11.10"
@ -2397,11 +2465,13 @@ dependencies = [
"http-body",
"hyper",
"hyper-rustls",
"hyper-tls",
"ipnet",
"js-sys",
"lazy_static",
"log",
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
"rustls 0.20.4",
@ -2410,6 +2480,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.23.3",
"url",
"wasm-bindgen",
@ -2512,6 +2583,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
"winapi",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -2538,6 +2619,29 @@ dependencies = [
"untrusted",
]
[[package]]
name = "security-framework"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "0.9.0"
@ -2559,6 +2663,20 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "sendgrid"
version = "0.17.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88afc7511b68247bf469e0dff55eb7ef305aab293211242695fd07cc17e36a78"
dependencies = [
"data-encoding",
"reqwest",
"serde",
"serde_json",
"thiserror",
"url",
]
[[package]]
name = "serde"
version = "1.0.136"
@ -2926,6 +3044,20 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tempfile"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [
"cfg-if",
"fastrand",
"libc",
"redox_syscall",
"remove_dir_all",
"winapi",
]
[[package]]
name = "tera"
version = "1.15.0"
@ -3099,6 +3231,16 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.22.0"
@ -3359,6 +3501,25 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5"
dependencies = [
"base64",
"chunked_transfer",
"flate2",
"log",
"once_cell",
"rustls 0.20.4",
"serde",
"serde_json",
"url",
"webpki 0.22.0",
"webpki-roots 0.22.3",
]
[[package]]
name = "url"
version = "2.2.2"

View File

@ -1,2 +1,2 @@
[workspace]
members = ["api"]
members = ["api","pay_u"]

View File

@ -63,4 +63,4 @@ async-trait = { version = "0.1.53", features = [] }
jemallocator = { version = "0.3.2", features = [] }
lettre = { version = "0.10.0-rc.5", default-features = false, features = ["tracing", "smtp-transport", "hostname", "builder", "tokio1-rustls-tls"] }
sendgrid = { version = "0.17.4", features = ["async"] }

View File

@ -1,13 +1,36 @@
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, Tokio1Executor};
use std::sync::Arc;
use crate::Email;
#[macro_export]
macro_rules! mail_async_handler {
($msg: ty, $async: ident, $res: ty) => {
impl actix::Handler<$msg> for EmailManager {
type Result = actix::ResponseActFuture<Self, Result<$res>>;
fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result {
use actix::WrapFuture;
let inner = self.0.clone();
Box::pin(async { $async(msg, inner).await }.into_actor(self))
}
}
};
}
#[derive(Debug, thiserror::Error)]
pub enum Error {}
pub type Result<T> = std::result::Result<T, Error>;
pub struct EmailManager {
mailer: AsyncSmtpTransport<Tokio1Executor>,
pub struct SendState {
pub success: bool,
}
pub struct EmailManager(Arc<Inner>);
pub(crate) struct Inner {
from: Email,
send_grid: sendgrid::SGClient,
}
impl actix::Actor for EmailManager {
@ -16,17 +39,41 @@ impl actix::Actor for EmailManager {
impl EmailManager {
pub fn build() -> Result<Self> {
let smtp_credentials = Credentials::new(
std::env::var("SMTP_USER").expect("Missing SMTP_USER variable"),
std::env::var("SMTP_PASSWORD").expect("Missing SMTP_PASSWORD variable"),
);
let from = std::env::var("SMTP_FROM").expect("Missing SMTP_FROM variable");
let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(
&std::env::var("SMTP_USER").expect("Missing SMTP_USER env variable"),
)
.expect("Can't construct async smtp mailer")
.credentials(smtp_credentials)
.build();
Ok(Self { mailer })
Ok(Self(Arc::new(Inner {
from: Email::from(from),
send_grid: sendgrid::SGClient::new(
std::env::var("SENDGRID_SECRET").expect("Missing SENDGRID_SECRET variable"),
),
})))
}
}
#[derive(actix::Message)]
#[rtype(result = "Result<SendState>")]
pub struct TestMail {
pub receiver: Email,
}
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_subject("Test e-mail")
.add_html("<h1>Test e-mail</h1>")
.build(),
)
.await;
log::debug!("{:?}", status);
Ok(SendState {
success: status.is_ok(),
})
}

View File

@ -13,7 +13,8 @@ use jemallocator::Jemalloc;
use password_hash::SaltString;
use validator::{validate_email, validate_length};
use crate::actors::{database, order_manager, token_manager};
use crate::actors::{database, email_manager, order_manager, token_manager};
use crate::email_manager::TestMail;
use crate::logic::encrypt_password;
use crate::model::{Email, Login, PassHash, Password, Role};
@ -66,6 +67,8 @@ enum Command {
GenerateHash(GenerateHashOpts),
#[options(help = "Create new account")]
CreateAccount(CreateAccountOpts),
#[options(help = "Check mailer config")]
TestMailer(TestMailerOpts),
}
impl Default for Command {
@ -104,6 +107,13 @@ impl ResolveDbUrl for ServerOpts {
}
}
#[derive(Options, Debug)]
pub struct TestMailerOpts {
help: bool,
#[options(help = "E-mail receiver")]
receiver: Option<Email>,
}
#[derive(Options, Debug)]
struct MigrateOpts {
help: bool,
@ -270,6 +280,27 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
Ok(())
}
async fn test_mailer(opts: TestMailerOpts) -> Result<()> {
let manager = email_manager::EmailManager::build()
.expect("Invalid email manager config")
.start();
if manager
.send(TestMail {
receiver: opts.receiver.expect("e-mail address is required"),
})
.await
.expect("Failed to execute actor")
.expect("Failed to send email")
.success
{
println!("Success!");
} else {
eprintln!("Failure!");
std::process::exit(1);
}
Ok(())
}
#[actix_web::main]
async fn main() -> Result<()> {
dotenv::dotenv().ok();
@ -281,5 +312,6 @@ async fn main() -> Result<()> {
Command::Server(opts) => server(opts).await,
Command::GenerateHash(opts) => generate_hash(opts).await,
Command::CreateAccount(opts) => create_account(opts).await,
Command::TestMailer(opts) => test_mailer(opts).await,
}
}

View File

@ -1,4 +1,5 @@
use std::fmt::Formatter;
use std::str::FromStr;
use derive_more::{Deref, Display, From};
use serde::de::{Error, Visitor};
@ -10,6 +11,8 @@ pub enum TransformError {
BelowMinimal,
#[error("Given value is not valid day flag")]
NotDay,
#[error("Given value is not valid email address")]
NotEmail,
}
pub type RecordId = i32;
@ -156,6 +159,18 @@ pub struct Login(String);
#[serde(transparent)]
pub struct Email(String);
impl FromStr for Email {
type Err = TransformError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if validator::validate_email(s) {
Ok(Self(String::from(s)))
} else {
Err(TransformError::NotEmail)
}
}
}
impl<'de> serde::Deserialize<'de> for Email {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where

17
pay_u/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "pay_u"
version = "0.1.0"
edition = "2021"
[dependencies]
ureq = { version = "2.4.0", features = ["json", "serde_json"] }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = { version = "1.0.79" }
chrono = { version = "0.4.19", features = ["serde"] }
thiserror = { version = "1.0.30" }
[dev-dependencies]
tokio = { version = "1.17.0", features = ["full"] }

324
pay_u/src/lib.rs Normal file
View File

@ -0,0 +1,324 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Client is not authorized. No bearer token available")]
NoToken,
#[error("{0}")]
UReq(#[from] ureq::Error),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("Total value is not sum of products price")]
IncorrectTotal,
}
pub type Result<T> = std::result::Result<T, Error>;
/// PayU payment status.
///
/// Each payment is initially Pending and can change according to following
/// graph:
///
/// <img src="https://developers.payu.com/images/order_statusesV2-en.png">
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PaymentStatus {
/// Payment is currently being processed.
Pending,
/// PayU is currently waiting for the merchant system to receive (capture)
/// the payment. This status is set if auto-receive is disabled on the
/// merchant system.
WaitingForConfirmation,
/// Payment has been accepted. PayU will pay out the funds shortly.
Completed,
/// Payment has been cancelled and the buyer has not been charged (no money
/// was taken from buyer's account).
Canceled,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Buyer {
/// Required customer e-mail
pub email: String,
/// Required customer phone number
pub phone: String,
/// Required customer first name
pub first_name: String,
/// Required customer last name
pub last_name: String,
/// Required customer language
pub language: String,
}
impl Buyer {
pub fn new<Email, Phone, FirstName, LastName, Language>(
email: Email,
phone: Phone,
first_name: FirstName,
last_name: LastName,
lang: Language,
) -> Self
where
Email: Into<String>,
Phone: Into<String>,
FirstName: Into<String>,
LastName: Into<String>,
Language: Into<String>,
{
Self {
email: email.into(),
phone: phone.into(),
first_name: first_name.into(),
last_name: last_name.into(),
language: lang.into(),
}
}
}
pub type Price = i32;
pub type Quantity = u32;
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Product {
pub name: String,
#[serde(serialize_with = "Product::serialize_unit_price")]
pub unit_price: Price,
#[serde(serialize_with = "Product::serialize_quantity")]
pub quantity: Quantity,
}
impl Product {
pub fn new<Name: Into<String>>(name: Name, unit_price: Price, quantity: Quantity) -> Self {
Self {
name: name.into(),
unit_price,
quantity,
}
}
fn serialize_unit_price<S>(v: &Price, ser: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser.serialize_str(&format!("{v}"))
}
fn serialize_quantity<S>(v: &Quantity, ser: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser.serialize_str(&format!("{v}"))
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Order {
/// URL to which web hook will be send. It's important to return 200 to all
/// notifications.
///
/// All notifications are send as POST with JSON payload
///
/// Notifications are sent immediately after a payment status changes. If
/// the notification is not received by the Shop application, it will be
/// sent again in accordance with the table below:
///
/// | Attempt | Time |
/// |---------|------|
/// | 1 | immediately |
/// | 2 | 1 minute |
/// | 3 | 2 minutes |
/// | 4 | 5 minutes |
/// | 5 | 10 minutes |
/// | 6 | 30 minutes |
/// | 7 | 1 hour |
/// | 8 | 2 hours |
/// | 9 | 3 hours |
/// | 10| 6 hours |
/// | 11| 9 hours |
/// | 12| 12 hours |
/// | 13| 15 hours |
/// | 14| 18 hours |
/// | 15| 21 hours |
/// | 16| 24 hours |
/// | 17| 36 hours |
/// | 18| 48 hours |
/// | 19| 60 hours |
/// | 20| 72 hours |
#[serde(skip_serializing_if = "Option::is_none")]
pub notify_url: Option<String>,
/// Customer client IP address
pub customer_ip: String,
/// Secret pos ip. This is connected to PayU account
pub merchant_pos_ip: String,
/// Transaction description
pub description: String,
/// 3 characters currency identifier, ex. PLN
pub currency_code: String,
/// Total price of the order in pennies (e.g. 1000 is 10.00 EUR). Applies
/// also to currencies without subunits (e.g. 1000 is 10 HUF).
#[serde(serialize_with = "Order::serialize_total_amount")]
pub total_amount: Price,
/// @see [Buyer]
pub buyer: Buyer,
/// List of products
pub products: Vec<Product>,
#[serde(skip_serializing)]
pub order_create_date: Option<String>,
}
impl Order {
fn serialize_total_amount<S>(v: &Price, ser: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser.serialize_str(&format!("{v}"))
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PaymentType {
Pbl,
CartToken,
Installments,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PayMethod {
#[serde(rename = "type")]
pub payment_type: PaymentType,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Status {
pub status_code: String,
}
impl Status {
pub fn is_success(&self) -> bool {
self.status_code.as_str() == "SUCCESS"
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreateOrderResult {
pub status: Status,
/// Client should be redirected to this URI
pub redirect_uri: String,
/// This should be connected to your own order
pub order_id: String,
/// This is YOUR_EXT_ORDER_ID
pub ext_order_id: String,
}
pub mod notify {
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StatusUpdate {
pub order: Order,
pub local_receipt_date_time: Option<String>,
pub properties: Option<Vec<Prop>>,
}
impl StatusUpdate {
pub fn status(&self) -> super::PaymentStatus {
self.order.status
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Order {
#[serde(flatten)]
pub order: super::Order,
pub pay_method: Option<super::PaymentType>,
pub status: super::PaymentStatus,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Prop {
pub name: String,
pub value: String,
}
}
pub struct Client {
pub bearer: Option<String>,
pub sandbox: bool,
}
impl Client {
pub fn create_order(&self, order: Order) -> Result<CreateOrderResult> {
println!("{}", serde_json::to_string_pretty(&order).unwrap());
if order.total_amount
!= order
.products
.iter()
.fold(0, |memo, p| memo + (p.unit_price * p.quantity as i32))
{
return Err(Error::IncorrectTotal);
}
let req = ureq::post(&format!("{}/orders", self.base_url()))
.set("Content-Type", "application/json")
.set(
"Authorization",
&self
.bearer
.as_ref()
.map(|s| format!("Bearer {s}"))
.ok_or(Error::NoToken)?,
);
eprintln!("{:?}", req);
let res = req.send_string(&serde_json::to_string_pretty(&order).unwrap())?;
res.into_json::<CreateOrderResult>().map_err(Into::into)
}
fn base_url(&self) -> &str {
if self.sandbox {
"https://secure.snd.payu.com/api/v2_1"
} else {
"https://secure.payu.com/api/v2_1"
}
}
}
#[cfg(test)]
mod tests {
use tokio::test;
use super::*;
#[test]
async fn create_order() {
let client = Client {
bearer: Some(String::from("d9a4536e-62ba-4f60-8017-6053211d3f47")),
sandbox: true,
};
let res = client.create_order(Order {
notify_url: Some(String::from("https://your.eshop.com/notify")),
customer_ip: "127.0.0.1".to_string(),
merchant_pos_ip: "300746".to_string(),
description: "RTV market".to_string(),
currency_code: "PLN".to_string(),
total_amount: 21000,
buyer: Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
products: vec![
Product::new("Wireless Mouse for Laptop", 15000, 1),
Product::new("HDMI cable", 6000, 1),
],
order_create_date: None,
});
eprintln!("{:#?}", res);
assert!(res.is_ok());
}
}