Init & create payment (not working)

This commit is contained in:
Adrian Woźniak 2022-12-09 16:33:35 +01:00
parent 9ea21c3276
commit 9775274d71
20 changed files with 1982 additions and 803 deletions

270
Cargo.lock generated
View File

@ -25,7 +25,7 @@ dependencies = [
"tarpc",
"testx",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
@ -51,7 +51,7 @@ dependencies = [
"parking_lot 0.12.1",
"pin-project-lite 0.2.9",
"smallvec",
"tokio 1.22.0",
"tokio 1.23.0",
"tokio-util 0.7.4",
]
@ -68,7 +68,7 @@ dependencies = [
"log",
"memchr",
"pin-project-lite 0.2.9",
"tokio 1.22.0",
"tokio 1.23.0",
"tokio-util 0.7.4",
]
@ -140,7 +140,7 @@ checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000"
dependencies = [
"actix-macros",
"futures-core",
"tokio 1.22.0",
"tokio 1.23.0",
]
[[package]]
@ -157,7 +157,7 @@ dependencies = [
"mio 0.8.5",
"num_cpus",
"socket2 0.4.7",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
]
@ -692,7 +692,7 @@ dependencies = [
"tarpc",
"testx",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
@ -734,7 +734,7 @@ dependencies = [
"strum",
"tarpc",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"whatlang",
]
@ -1099,9 +1099,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.82"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf"
dependencies = [
"cc",
"cxxbridge-flags",
@ -1111,9 +1111,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.82"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39"
dependencies = [
"cc",
"codespan-reporting",
@ -1126,15 +1126,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.82"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"
checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12"
[[package]]
name = "cxxbridge-macro"
version = "1.0.82"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
dependencies = [
"proc-macro2",
"quote",
@ -1178,9 +1178,9 @@ dependencies = [
[[package]]
name = "data-encoding"
version = "2.3.2"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
name = "database_manager"
@ -1373,7 +1373,7 @@ dependencies = [
"tarpc",
"testx",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
@ -1570,9 +1570,9 @@ dependencies = [
[[package]]
name = "filetime"
version = "0.2.18"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3"
checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
dependencies = [
"cfg-if 1.0.0",
"libc",
@ -1710,7 +1710,7 @@ dependencies = [
"rumqttc",
"serde",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"uuid 1.2.2",
]
@ -2007,30 +2007,11 @@ dependencies = [
"http",
"indexmap",
"slab",
"tokio 1.22.0",
"tokio 1.23.0",
"tokio-util 0.7.4",
"tracing",
]
[[package]]
name = "h2_wasi"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756e9dfbaf90887b69ea8113ef9d04a9fec0ec4ac17b84a95006381f6f7966f9"
dependencies = [
"bytes 1.3.0",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio-util_wasi",
"tokio_wasi",
"tracing",
]
[[package]]
name = "handlebars"
version = "4.3.5"
@ -2239,7 +2220,7 @@ dependencies = [
"itoa 1.0.4",
"pin-project-lite 0.2.9",
"socket2 0.4.7",
"tokio 1.22.0",
"tokio 1.23.0",
"tower-service",
"tracing",
"want",
@ -2267,34 +2248,10 @@ dependencies = [
"bytes 1.3.0",
"hyper 0.14.23",
"native-tls",
"tokio 1.22.0",
"tokio 1.23.0",
"tokio-native-tls",
]
[[package]]
name = "hyper_wasi"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b903a973bd98c326103ea94336a9ffe44c57c71517d6be7639438021053dc6e"
dependencies = [
"bytes 1.3.0",
"futures-channel",
"futures-core",
"futures-util",
"h2_wasi",
"http",
"http-body 0.4.5",
"httparse",
"httpdate 1.0.2",
"itoa 1.0.4",
"pin-project-lite 0.2.9",
"tokio_wasi",
"tower-service",
"tracing",
"want",
"wasmedge_wasi_socket",
]
[[package]]
name = "iana-time-zone"
version = "0.1.53"
@ -2371,9 +2328,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.21.2"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "261bf85ed492cd1c47c9ba675e48649682a9d2d2e77f515c5386d7726fb0ba76"
checksum = "197f4e300af8b23664d4077bf5c40e0afa9ba66a567bb5a51d3def3c7b287d1c"
dependencies = [
"console",
"lazy_static",
@ -2457,9 +2414,9 @@ dependencies = [
[[package]]
name = "ipnet"
version = "2.5.1"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745"
checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded"
[[package]]
name = "ipnetwork"
@ -2614,9 +2571,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "libc"
version = "0.2.137"
version = "0.2.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
[[package]]
name = "libgit2-sys"
@ -2904,19 +2861,6 @@ dependencies = [
"windows-sys 0.42.0",
]
[[package]]
name = "mio_wasi"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e260e68b7a4187e52caf00a035aaf7166391c2c2529123fe8229d4d8f86e15a2"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasmedge_wasi_socket",
"windows-sys 0.36.1",
]
[[package]]
name = "miow"
version = "0.2.2"
@ -2994,9 +2938,9 @@ dependencies = [
[[package]]
name = "nix"
version = "0.23.1"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
dependencies = [
"bitflags",
"cc",
@ -3095,22 +3039,11 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "openapi"
version = "1.0.0"
dependencies = [
"reqwest 0.11.13",
"serde",
"serde_derive",
"serde_json",
"url",
]
[[package]]
name = "openssl"
version = "0.10.43"
version = "0.10.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376"
checksum = "29d971fd5722fec23977260f6e81aa67d2f22cadbdc2aa049f1022d9a3be1566"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
@ -3140,9 +3073,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.78"
version = "0.9.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132"
checksum = "5454462c0eced1e97f2ec09036abc8da362e66802f66fd20f86854d9d8cbcbc4"
dependencies = [
"autocfg 1.1.0",
"cc",
@ -3257,7 +3190,7 @@ dependencies = [
"tarpc",
"testx",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
@ -3344,21 +3277,6 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
[[package]]
name = "pay_u"
version = "0.1.9"
dependencies = [
"chrono",
"derive_more",
"dotenv",
"log",
"reqwest_wasi",
"serde",
"serde_json",
"thiserror",
"tokio 1.22.0",
]
[[package]]
name = "payment_adapter"
version = "0.1.0"
@ -3388,6 +3306,7 @@ dependencies = [
"opentelemetry 0.17.0",
"opentelemetry-jaeger",
"payment_adapter",
"reqwest 0.11.13",
"rumqttc",
"serde",
"sqlx",
@ -3395,7 +3314,7 @@ dependencies = [
"tarpc",
"testx",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
@ -3955,7 +3874,6 @@ dependencies = [
"js-sys",
"log",
"mime",
"mime_guess",
"native-tls",
"once_cell",
"percent-encoding",
@ -3963,7 +3881,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"tokio 1.22.0",
"tokio 1.23.0",
"tokio-native-tls",
"tower-service",
"url",
@ -3973,41 +3891,6 @@ dependencies = [
"winreg 0.10.1",
]
[[package]]
name = "reqwest_wasi"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4c1c0524070b75dc4a924bdaac56d5c874205497fb3132f09f3270e4efb188d"
dependencies = [
"base64",
"bytes 1.3.0",
"encoding_rs",
"futures-core",
"futures-util",
"h2_wasi",
"http",
"http-body 0.4.5",
"hyper_wasi",
"ipnet",
"js-sys",
"lazy_static",
"log",
"mime",
"once_cell",
"percent-encoding",
"pin-project-lite 0.2.9",
"serde",
"serde_json",
"serde_urlencoded",
"tokio_wasi",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.10.1",
]
[[package]]
name = "ring"
version = "0.16.20"
@ -4084,7 +3967,7 @@ dependencies = [
"rustls-native-certs",
"rustls-pemfile 0.3.0",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tokio-rustls",
]
@ -4315,7 +4198,7 @@ dependencies = [
"tarpc",
"testx",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
@ -4758,7 +4641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396"
dependencies = [
"once_cell",
"tokio 1.22.0",
"tokio 1.23.0",
"tokio-rustls",
]
@ -4798,7 +4681,7 @@ dependencies = [
"tarpc",
"testx",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
@ -4940,7 +4823,7 @@ dependencies = [
"static_assertions",
"tarpc-plugins",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tokio-serde",
"tokio-util 0.7.4",
"tracing",
@ -5153,7 +5036,7 @@ dependencies = [
"sha2",
"testx",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
"uuid 1.2.2",
]
@ -5178,9 +5061,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.22.0"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
dependencies = [
"autocfg 1.1.0",
"bytes 1.3.0",
@ -5193,7 +5076,7 @@ dependencies = [
"signal-hook-registry",
"socket2 0.4.7",
"tokio-macros",
"winapi 0.3.9",
"windows-sys 0.42.0",
]
[[package]]
@ -5214,7 +5097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
"native-tls",
"tokio 1.22.0",
"tokio 1.23.0",
]
[[package]]
@ -5224,7 +5107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls",
"tokio 1.22.0",
"tokio 1.23.0",
"webpki",
]
@ -5252,7 +5135,7 @@ checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
dependencies = [
"futures-core",
"pin-project-lite 0.2.9",
"tokio 1.22.0",
"tokio 1.23.0",
]
[[package]]
@ -5290,42 +5173,10 @@ dependencies = [
"futures-sink",
"pin-project-lite 0.2.9",
"slab",
"tokio 1.22.0",
"tokio 1.23.0",
"tracing",
]
[[package]]
name = "tokio-util_wasi"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e7e921c33dc681c0b01aa4c5aa1bf619f46a7040a1565ddb1f84bc4666c1bd"
dependencies = [
"bytes 1.3.0",
"futures-core",
"futures-sink",
"pin-project-lite 0.2.9",
"tokio_wasi",
"tracing",
]
[[package]]
name = "tokio_wasi"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68e9a04f238fa1676caa7c897bf1dbe7a82269b675aca14176a028af7a2167d2"
dependencies = [
"autocfg 1.1.0",
"bytes 1.3.0",
"libc",
"memchr",
"mio_wasi",
"num_cpus",
"pin-project-lite 0.2.9",
"socket2 0.4.7",
"wasmedge_wasi_socket",
"winapi 0.3.9",
]
[[package]]
name = "toml"
version = "0.5.9"
@ -5474,9 +5325,9 @@ dependencies = [
[[package]]
name = "typenum"
version = "1.15.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "ucd-trie"
@ -5676,7 +5527,7 @@ dependencies = [
"log",
"rusty_pool",
"thiserror",
"tokio 1.22.0",
"tokio 1.23.0",
"wapc",
]
@ -5812,15 +5663,6 @@ dependencies = [
"leb128",
]
[[package]]
name = "wasmedge_wasi_socket"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc4e93009d7e2c2e3d29b65d1f56ef606466673e85fe417808beca50cb608bf"
dependencies = [
"libc",
]
[[package]]
name = "wasmparser"
version = "0.93.0"

View File

@ -25,8 +25,8 @@ members = [
# "crates/api",
"crates/web",
# vendor
"vendor/t_pay",
"vendor/pay_u",
# "vendor/t_pay",
# "vendor/pay_u",
]
exclude = [
"crates/payment_adapter_pay_u",

View File

@ -20,3 +20,15 @@ This script will treat all crates which starts with `payment_adapter_` as a paym
mkdir -p adapters
./scripts/build-adapters
```
##### `init`
| Argument | Result |
|-----------------------------------------|--------------------------|
| payment_argument::PaymentProviderConfig | payment_argument::Status |
* `create_payment`
| Argument | Result |
|--------------------------------|-------------------------------|
| payment_adapter::CreatePayment | payment_adapter::OrderCreated |

View File

@ -531,7 +531,7 @@ pub struct PaymentConfig {
#[serde(default)]
optional_payment: bool,
#[serde(flatten)]
providers: HashMap<String, PaymentProviderConfig>,
pub providers: HashMap<String, PaymentProviderConfig>,
}
impl Example for PaymentConfig {

View File

@ -0,0 +1,31 @@
= Payment Adapter Implementation
== Extention Lifetime
Payment Adapter extension is loaded as WASI file and initialize as a module.
Then to store it in runtime `name` function is called to load extension unique `name` (`String`).
Using this `name` extension configuration from application configuration, names must match! In config is missing is missing application will print error message is stop.
Then configuration is passed to extension to setup client. If everything goes as expected it should return `Status::Success`, in any other situation it must return `Status::Failure`, this also will cause to print error message and stop application.
Lastly if extension will be be dropped for some reason it will be detached from store and `teardown` function will be called.
1. `name`
2. `init`
3. `teardown`
== Example
[rust]
```
/// Mandatory for WASM runtime
#[no_mangle]
pub fn wapc_init() {
wapc::register_function("name", name);
wapc::register_function("init", init);
wapc::register_function("teardown", teardown);
wapc::register_function("create_payment", create_payment);
wapc::register_function("cancel_order", cancel_order);
}
```

View File

@ -9,6 +9,10 @@ pub const CONFIG_POS: u32 = 3;
pub enum Error {
#[error("Malformed create payment message")]
MalformedCreatePayment,
#[error("Provider rejected order")]
PaymentFailed,
#[error("HTTP request failed")]
HttpFailed,
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
@ -54,7 +58,6 @@ pub struct Item {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[repr(C)]
pub struct CreatePayment {
// pub client: PayUClient,
pub buyer: Buyer,
pub customer_ip: String,
pub currency: String,
@ -66,6 +69,30 @@ pub struct CreatePayment {
pub continue_uri: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[repr(C)]
pub struct OrderCreated {
pub ext_order_id: ExtOrderId,
pub redirect_uri: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[repr(C)]
pub enum HttpMethod {
Get,
Post,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[repr(C)]
pub struct HttpRequest {
pub method: HttpMethod,
pub url: String,
pub headers: Vec<(String, String)>,
pub bearer_auth: Option<String>,
pub body: Option<Vec<u8>>,
}
// #[tarpc::service]
// pub trait PaymentAdapter {
// async fn create_payment(msg: CreatePayment) -> Status;

View File

@ -38,12 +38,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64ct"
version = "1.5.3"
@ -95,12 +89,6 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
[[package]]
name = "cc"
version = "1.0.77"
@ -290,21 +278,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "encoding_rs"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
dependencies = [
"cfg-if",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
@ -424,31 +397,6 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "h2_wasi"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756e9dfbaf90887b69ea8113ef9d04a9fec0ec4ac17b84a95006381f6f7966f9"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio-util_wasi",
"tokio_wasi",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -458,64 +406,6 @@ dependencies = [
"libc",
]
[[package]]
name = "http"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper_wasi"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b903a973bd98c326103ea94336a9ffe44c57c71517d6be7639438021053dc6e"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2_wasi",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"tokio_wasi",
"tower-service",
"tracing",
"want",
"wasmedge_wasi_socket",
]
[[package]]
name = "iana-time-zone"
version = "0.1.53"
@ -561,16 +451,6 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -580,12 +460,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "ipnet"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745"
[[package]]
name = "itoa"
version = "1.0.4"
@ -662,25 +536,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mio_wasi"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e260e68b7a4187e52caf00a035aaf7166391c2c2529123fe8229d4d8f86e15a2"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasmedge_wasi_socket",
"windows-sys 0.36.1",
]
[[package]]
name = "model"
version = "0.1.0"
@ -776,7 +631,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-sys 0.42.0",
"windows-sys",
]
[[package]]
@ -796,19 +651,6 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
[[package]]
name = "pay_u"
version = "0.1.9"
dependencies = [
"chrono",
"derive_more",
"log",
"reqwest_wasi",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "payment_adapter"
version = "0.1.0"
@ -827,9 +669,10 @@ name = "payment_adapter_pay_u"
version = "0.1.0"
dependencies = [
"bincode",
"pay_u",
"chrono",
"payment_adapter",
"tokio",
"serde",
"thiserror",
"tracing",
"wapc-codec",
"wapc-guest",
@ -929,41 +772,6 @@ version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "reqwest_wasi"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4c1c0524070b75dc4a924bdaac56d5c874205497fb3132f09f3270e4efb188d"
dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2_wasi",
"http",
"http-body",
"hyper_wasi",
"ipnet",
"js-sys",
"lazy_static",
"log",
"mime",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"tokio_wasi",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]]
name = "rmp"
version = "0.8.11"
@ -1050,18 +858,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "slab"
version = "0.4.7"
@ -1077,16 +873,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "subtle"
version = "2.4.1"
@ -1159,49 +945,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
dependencies = [
"autocfg",
"pin-project-lite",
"windows-sys 0.42.0",
]
[[package]]
name = "tokio-util_wasi"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e7e921c33dc681c0b01aa4c5aa1bf619f46a7040a1565ddb1f84bc4666c1bd"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio_wasi",
"tracing",
]
[[package]]
name = "tokio_wasi"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68e9a04f238fa1676caa7c897bf1dbe7a82269b675aca14176a028af7a2167d2"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
"mio_wasi",
"num_cpus",
"pin-project-lite",
"socket2",
"wasmedge_wasi_socket",
"winapi",
]
[[package]]
name = "toml"
version = "0.5.9"
@ -1211,12 +954,6 @@ dependencies = [
"serde",
]
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
version = "0.1.37"
@ -1249,12 +986,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "try-lock"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "typenum"
version = "1.15.0"
@ -1330,16 +1061,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
"log",
"try-lock",
]
[[package]]
name = "wapc-codec"
version = "1.0.0"
@ -1397,18 +1118,6 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.83"
@ -1438,25 +1147,6 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
[[package]]
name = "wasmedge_wasi_socket"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc4e93009d7e2c2e3d29b65d1f56ef606466673e85fe417808beca50cb608bf"
dependencies = [
"libc",
]
[[package]]
name = "web-sys"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -1488,19 +1178,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
@ -1508,12 +1185,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.0",
"windows_x86_64_msvc",
]
[[package]]
@ -1522,48 +1199,24 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
@ -1576,23 +1229,8 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]

View File

@ -7,10 +7,11 @@ edition = "2021"
crate-type = ['cdylib']
[dependencies]
pay_u = { path = "../../vendor/pay_u" }
payment_adapter = { path = "../payment_adapter" }
bincode = { version = "1.3.3" }
wapc-codec = { version = "1.0.0" }
wapc-guest = { version = "1.0.0" }
tokio = { version="1.21.1", features=["rt","sync","time"] }
tracing = { version = "0.1.37" }
chrono = { version = "0.4.23", features = ['alloc', 'wasmbind'] }
serde = { version = "1.0.149", features = ['derive'] }
thiserror = { version = "1.0.37" }

View File

@ -0,0 +1,77 @@
//! This module allow to create credit request during create order request
use serde::{Deserialize, Serialize};
use crate::model::{Address, ShoppingCart};
/// Describe customer during credit request
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Applicant {
/// Applicants email address
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
/// Applicants phone number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
phone: Option<String>,
/// Applicants first name
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
first_name: Option<String>,
/// Applicants last name
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
last_name: Option<String>,
/// Language code, ISO-639-1 compliant. Denotes the language version of
/// PayU hosted payment page and of e-mail messages sent from PayU to the
/// payer (supported values are here).
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<String>,
/// National Identification Number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
nin: Option<String>,
/// Section containing data about applicants address.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
address: Option<Address>,
/// Additional information about person applying for credit.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
additional_info: Option<ApplicantAdditionalInfo>,
}
/// Allow to create credit request
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct Credit {
/// Section containing data of the ordered products
#[serde(skip_serializing_if = "Option::is_none")]
shopping_carts: Option<Vec<ShoppingCart>>,
/// Section containing data of person applying for a credit
#[serde(skip_serializing_if = "Option::is_none")]
applicant: Option<Applicant>,
}
impl Credit {
pub fn with_shopping_carts<ShoppingCarts>(mut self, shopping_carts: ShoppingCarts) -> Self
where
ShoppingCarts: Iterator<Item = ShoppingCart>,
{
self.shopping_carts = Some(shopping_carts.collect());
self
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ApplicantAdditionalInfo {
/// Information whether there were previous, successfully completed orders
/// for applicant.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
has_successfully_finished_order_in_shop: Option<String>,
}

View File

@ -0,0 +1,162 @@
use std::fmt;
use serde::de::{self, Error, Visitor};
pub(crate) fn deserialize_i32<'de, D>(d: D) -> std::result::Result<i32, D::Error>
where
D: serde::Deserializer<'de>,
{
d.deserialize_string(I32Visitor)
}
pub(crate) fn deserialize_i32_newtype<'de, N: From<i32>, D>(
d: D,
) -> std::result::Result<N, D::Error>
where
D: serde::Deserializer<'de>,
{
d.deserialize_string(I32Visitor).map(N::from)
}
pub(crate) fn deserialize_u32<'de, D>(d: D) -> std::result::Result<u32, D::Error>
where
D: serde::Deserializer<'de>,
{
d.deserialize_string(U32Visitor)
}
struct I32Visitor;
impl<'de> Visitor<'de> for I32Visitor {
type Value = i32;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer between -2^31 and 2^31")
}
fn visit_i8<E>(self, value: i8) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(i32::from(value))
}
fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
use std::i32;
if value >= i64::from(i32::MIN) && value <= i64::from(i32::MAX) {
Ok(value as i32)
} else {
Err(E::custom(format!("i32 out of range: {}", value)))
}
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
if let Ok(value) = v.parse::<i32>() {
Ok(value)
} else {
Err(E::custom(format!("str does not contains valid i32: {}", v)))
}
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: Error,
{
if let Ok(value) = v.parse::<i32>() {
Ok(value)
} else {
Err(E::custom(format!(
"string does not contains valid i32: {}",
v
)))
}
}
// Similar for other methods:
// - visit_i16
// - visit_u8
// - visit_u16
// - visit_u32
// - visit_u64
}
struct U32Visitor;
impl<'de> Visitor<'de> for U32Visitor {
type Value = u32;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer between -2^31 and 2^31")
}
fn visit_u8<E>(self, value: u8) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(u32::from(value))
}
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value)
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
use std::u32;
if value >= u64::from(u32::MIN) && value <= u64::from(u32::MAX) {
Ok(value as u32)
} else {
Err(E::custom(format!("i32 out of range: {}", value)))
}
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
if let Ok(value) = v.parse::<u32>() {
Ok(value)
} else {
Err(E::custom(format!("str does not contains valid i32: {}", v)))
}
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: Error,
{
if let Ok(value) = v.parse::<u32>() {
Ok(value)
} else {
Err(E::custom(format!(
"string does not contains valid i32: {}",
v
)))
}
}
// Similar for other methods:
// - visit_i16
// - visit_u8
// - visit_u16
// - visit_u32
// - visit_u64
}

View File

@ -1,133 +1,247 @@
mod credit;
mod deserialize;
mod model;
mod req;
mod res;
mod serialize;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use pay_u::{ClientId, ClientSecret, MerchantPosId};
use payment_adapter::*;
use wapc_codec::messagepack::*;
use wapc_guest as wapc;
use crate::req::OrderCreate;
struct PayU {
client: Arc<Mutex<pay_u::Client>>,
sandbox: bool,
merchant_pos_id: i32,
client_id: String,
client_secret: String,
bearer: Option<String>,
bearer_expires_at: chrono::DateTime<chrono::Utc>,
}
static mut CLIENT: Option<PayU> = None;
static mut RUNTIME: Option<tokio::runtime::Runtime> = None;
impl PayU {
/// Create new PayU client
pub fn new(client_id: String, client_secret: String, merchant_pos_id: i32) -> Self {
Self {
bearer: None,
sandbox: false,
merchant_pos_id,
client_id,
client_secret,
bearer_expires_at: chrono::Utc::now(),
}
}
fn base_url(&self) -> &str {
if self.sandbox {
"https://secure.snd.payu.com/api/v2_1"
} else {
"https://secure.payu.com/api/v2_1"
}
}
}
static mut CONFIG: Option<Arc<Mutex<PayU>>> = None;
fn client() -> Arc<Mutex<PayU>> {
unsafe { CONFIG.as_ref().unwrap().clone() }
}
impl PayU {
fn mount() {
wapc::register_function("teardown", Self::teardown);
wapc::register_function("name", Self::name);
wapc::register_function("init", Self::init);
wapc::register_function("create_payment", Self::create_payment);
wapc::register_function("cancel_order", Self::cancel_order);
}
fn teardown(_msg: &[u8]) -> wapc::CallResult {
Ok(vec![])
}
fn name(_msg: &[u8]) -> wapc::CallResult {
Ok(b"pay_u".to_vec())
}
fn init(msg: &[u8]) -> wapc::CallResult {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.enable_time()
.build()
.unwrap();
unsafe {
RUNTIME = Some(runtime);
}
let config: PaymentProviderConfig = match deserialize(msg) {
Ok(c) => c,
_ => return Err(Box::new(payment_adapter::Error::MalformedCreatePayment)),
_ => return Err(Box::new(Error::MalformedCreatePayment)),
};
let res_client = Arc::new(Mutex::new({
pay_u::Client::new(
ClientId::new(config.client_id.unwrap()),
ClientSecret::new(config.client_secret.unwrap()),
MerchantPosId::new(config.merchant_id.unwrap().parse().unwrap()),
)
}));
let c = res_client.clone();
unsafe { RUNTIME.as_ref().unwrap() }.block_on(async {
c.lock().unwrap().authorize().await.unwrap_or_else(|e| {
// tracing::error!("{e}");
dbg!(e);
std::process::exit(1);
});
});
let client = PayU::new(
config.client_id.unwrap(),
config.client_secret.unwrap(),
config.merchant_id.unwrap().parse().unwrap(),
);
unsafe {
CLIENT = Some(Self { client: res_client });
CONFIG = Some(Arc::new(Mutex::new(client)));
}
Ok(vec![])
Ok(serialize(Status::Success).unwrap_or_default())
}
fn create_payment(msg: &[u8]) -> wapc::CallResult {
let c: CreatePayment = match deserialize(msg) {
Ok(c) => c,
_ => return Err(Box::new(payment_adapter::Error::MalformedCreatePayment)),
_ => return Err(Box::new(Error::MalformedCreatePayment)),
};
let CreatePayment {
client,
buyer,
customer_ip,
currency,
description,
cart_products,
mut items,
order_ext_id,
notify_uri,
continue_uri,
} = c;
let pay_u::res::CreateOrder {
status: _,
redirect_uri,
order_id,
ext_order_id: _,
} = unsafe { RUNTIME.as_ref().unwrap() }.block_on(async {
let client = unsafe { CLIENT.as_ref().unwrap() };
client
.create_order(
pay_u::req::OrderCreate::build(
buyer.into(),
customer_ip,
currency,
description,
)
.map_err(|e| {
tracing::error!("{}", e);
Error::InvalidOrder
})?
.with_products(cart_products.into_iter().map(|p| {
pay_u::Product::new(
p.name.to_string(),
**p.price,
items
.remove(&p.id)
.map(|(quantity, _)| **quantity as u32)
.unwrap_or_default(),
)
}))
.with_ext_order_id(order_ext_id)
.with_notify_url(notify_uri)
.with_continue_url(continue_uri),
)
.await
.map_err(|e| {
tracing::error!("{}", e);
Error::PaymentFailed
})
.unwrap()
});
let _ = Ok((redirect_uri, ExtOrderId::new(order_id.0)));
wapc::console_log(&format!(
"IN_WASM: Received request for `ping` operation with payload : {:?}",
c
));
eprintln!("{:?}", c);
// let _res = wapc::host_call("binding", "sample:namespace", "pong", msg)?;
Ok(serialize(Status::Success).unwrap())
let res::CreateOrder {
status: _,
redirect_uri,
order_id,
ext_order_id: _,
} = create_payment(c)?;
Ok(serialize(OrderCreated {
redirect_uri,
ext_order_id: ExtOrderId::new(order_id),
})
.unwrap())
}
fn cancel_order(_msg: &[u8]) -> wapc::CallResult {
Ok(vec![])
}
}
fn create_payment(c: CreatePayment) -> Result<res::CreateOrder, Error> {
let config = client();
let pay_u = config.lock().unwrap();
let CreatePayment {
buyer,
customer_ip,
currency,
description,
cart_products,
items,
order_ext_id,
notify_uri,
continue_uri,
} = c;
let quantities = {
let len = items.len();
items
.iter()
.fold(HashMap::with_capacity(len), |mut h, item| {
h.insert(item.product_id, item.quantity);
h
})
};
let create_order = OrderCreate::build(
{
let Buyer {
email,
phone,
first_name,
last_name,
language,
} = buyer;
model::Buyer::new(email, phone, first_name, last_name, language)
},
customer_ip,
currency,
description,
)
.map_err(|e| {
tracing::error!("{}", e);
Error::MalformedCreatePayment
})?
.with_products(cart_products.into_iter().map(|p| {
model::Product::new(
p.name.to_string(),
p.unit_price,
quantities.get(&p.id).copied().unwrap_or_default() as model::Quantity,
)
}))
.with_ext_order_id(order_ext_id)
.with_notify_url(notify_uri)
.with_continue_url(continue_uri);
authorize()?;
let res = wapc::host_call(
"binding",
"http:req",
"http_req",
&serialize(HttpRequest {
method: HttpMethod::Post,
url: format!("{}/orders", pay_u.base_url()),
headers: vec![],
bearer_auth: pay_u.bearer.clone(),
body: serialize(create_order).ok(),
})
.unwrap(),
)
.map_err(|e| {
tracing::error!("{}", e);
Error::PaymentFailed
})
.and_then(|v| {
deserialize(&v).map_err(|e| {
tracing::error!("{}", e);
Error::PaymentFailed
})
})?;
Ok(res)
}
#[no_mangle]
pub fn wapc_init() {
PayU::mount();
}
fn authorize() -> Result<bool, Error> {
use chrono::{Duration, Utc};
let config = client();
let mut pay_u = config.lock().unwrap();
if Utc::now() - Duration::seconds(1) < pay_u.bearer_expires_at {
return Ok(true);
}
#[derive(serde::Deserialize)]
struct BearerResult {
access_token: String,
expires_in: i64,
}
let res = wapc::host_call("binding", "http:req", "http_req", &serialize(HttpRequest {
method: HttpMethod::Post,
url: format!(
"https://secure.payu.com/pl/standard/user/oauth/authorize?grant_type=client_credentials&client_id={}&client_secret={}",
pay_u.client_id,
pay_u.client_secret
),
headers: vec![],
bearer_auth: None,
body: None,
}).map_err(|e| {
tracing::error!("{}", e);
Error::PaymentFailed
})?).map_err(|e| {
tracing::error!("{}", e);
Error::PaymentFailed
})?;
let res: BearerResult = deserialize(&res).map_err(|e| {
tracing::error!("{}", e);
Error::PaymentFailed
})?;
tracing::trace!("Bearer is {}", res.access_token);
pay_u.bearer_expires_at = Utc::now() + Duration::seconds(res.expires_in);
pay_u.bearer = Some(res.access_token);
Ok(true)
}

View File

@ -0,0 +1,700 @@
use serde::{Deserialize, Serialize};
use crate::{deserialize, serialize};
pub static SUCCESS: &str = "SUCCESS";
pub type OrderId = String;
pub type MerchantPosId = i32;
#[derive(Debug, thiserror::Error)]
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")]
IncorrectTotal,
#[error("Buyer is required to place an order")]
NoBuyer,
#[error("Description is required to place an order")]
NoDescription,
#[error("Client is not authorized")]
Unauthorized,
#[error("Refund returned invalid response")]
Refund,
#[error("Create order returned invalid response")]
CreateOrder,
#[error("Failed to fetch order transactions")]
OrderTransactions,
#[error("Failed to fetch order details")]
OrderDetails,
#[error("Failed to fetch order refunds")]
OrderRefunds,
#[error("PayU rejected to create order with status {status_code:?}")]
CreateFailed {
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
},
#[error("PayU rejected to perform refund with status {status_code:?}")]
RefundFailed {
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
},
#[error("PayU rejected order details request with status {status_code:?}")]
OrderDetailsFailed {
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
},
#[error("PayU rejected order transactions details request with status {status_code:?}")]
OrderTransactionsFailed {
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
},
#[error("PayU returned order details but without any order")]
NoOrderInDetails,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CodeLiteral {
/// Request lacks "refund" object.
MissingRefundSection,
/// Transaction has not been finalized
TransNotEnded,
/// Lack of funds in account
NoBalance,
/// Refund amount exceeds transaction amount
AmountToBig,
/// Refund value is too small
AmountToSmall,
/// Refunds have been disabled
RefundDisabled,
/// Too many refund attempts have been made
RefundToOften,
/// Refund was already created
Paid,
/// Unknown error
UnknownError,
/// extRefundId was re-used and other params do not match the values
/// sent during the first call.
RefundIdempotencyMismatch,
/// Shop billing has not yet been completed
TransBillingEntriesNotCompleted,
/// The available time for refund has passed.
TransTooOld,
/// Transaction amount that remains after refund creation will be too
/// small to make another refund.
RemainingTransAmountTooSmall,
#[serde(other)]
/// Implementation changed
Unknown,
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Prop {
pub name: String,
pub value: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Refund {
pub refund_id: String,
pub ext_refund_id: Option<String>,
pub amount: String,
pub currency_code: String,
pub description: String,
pub creation_date_time: String,
pub status: String,
pub status_date_time: String,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Status {
/// One of
/// * `PENDING`: Payment is currently being processed.
/// * `WAITING_FOR_CONFIRMATION`: 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.
/// * `COMPLETED`: Payment has been accepted. PayU will pay out the funds
/// shortly.
/// * `CANCELED`: Payment has been cancelled and the buyer has not been
/// charged (no money was taken from buyer's account).
///
/// > Too prevent sending wrong status from server to PayU this field
/// > remains String
status_code: String,
status_desc: Option<String>,
code: Option<String>,
severity: Option<String>,
code_literal: Option<CodeLiteral>,
}
impl Status {
/// Check if http request was successful
///
/// # Examples
///
/// ```
/// # use pay_u::Status;
/// let status: Status = serde_json::from_str("{\"statusCode\":\"SUCCESS\"}").unwrap();
/// assert_eq!(status.is_success(), true);
/// ```
pub fn is_success(&self) -> bool {
self.status_code.as_str() == SUCCESS
}
/// Returns http status
///
/// # Examples
///
/// ```
/// # use pay_u::Status;
/// let status: Status = serde_json::from_str("{\"statusCode\":\"SUCCESS\"}").unwrap();
/// assert_eq!(status.status_code(), "SUCCESS");
/// ```
pub fn status_code(&self) -> &str {
&self.status_code
}
pub fn status_desc(&self) -> Option<&str> {
self.status_desc.as_deref()
}
pub fn code(&self) -> Option<&str> {
self.code.as_deref()
}
pub fn severity(&self) -> Option<&str> {
self.severity.as_deref()
}
pub fn code_literal(&self) -> Option<&CodeLiteral> {
self.code_literal.as_ref()
}
}
/// 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, Copy, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RefundStatus {
/// refund was completed successfully
Finalized,
/// refund was cancelled
Canceled,
/// refund in progress
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,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct Delivery {
#[serde(skip_serializing_if = "Option::is_none")]
/// Street name
street: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Postal box number
postal_box: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Postal code
postal_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// City
city: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Province
state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Two-letter country code compliant with ISO-3166.
country_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Address description
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Recipients name
recipient_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Recipients e-mail address
recipient_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Recipients phone number
recipient_phone: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct BuyerShippingAddress {
#[serde(skip_serializing_if = "Option::is_none")]
/// stores the shipping address
delivery: Option<Delivery>,
}
impl BuyerShippingAddress {
pub fn new_with_delivery(delivery: Delivery) -> Self {
Self {
delivery: Some(delivery),
}
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct Buyer {
/// Required customer e-mail
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
/// Required customer phone number
#[serde(skip_serializing_if = "Option::is_none")]
phone: Option<String>,
/// Required customer first name
#[serde(skip_serializing_if = "Option::is_none")]
first_name: Option<String>,
/// Required customer last name
#[serde(skip_serializing_if = "Option::is_none")]
last_name: Option<String>,
/// Required customer language
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
delivery: Option<BuyerShippingAddress>,
}
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: Some(email.into()),
phone: Some(phone.into()),
first_name: Some(first_name.into()),
last_name: Some(last_name.into()),
language: Some(lang.into()),
delivery: None,
}
}
pub fn email(&self) -> &str {
self.email.as_deref().unwrap_or_default()
}
pub fn with_email<S>(mut self, email: S) -> Self
where
S: Into<String>,
{
self.email = Some(email.into());
self
}
pub fn phone(&self) -> &str {
self.phone.as_deref().unwrap_or_default()
}
pub fn with_phone<S>(mut self, phone: S) -> Self
where
S: Into<String>,
{
self.phone = Some(phone.into());
self
}
pub fn first_name(&self) -> &str {
self.first_name.as_deref().unwrap_or_default()
}
pub fn with_first_name<S>(mut self, first_name: S) -> Self
where
S: Into<String>,
{
self.first_name = Some(first_name.into());
self
}
pub fn last_name(&self) -> &str {
self.last_name.as_deref().unwrap_or_default()
}
pub fn with_last_name<S>(mut self, last_name: S) -> Self
where
S: Into<String>,
{
self.last_name = Some(last_name.into());
self
}
pub fn language(&self) -> &str {
self.language.as_deref().unwrap_or_default()
}
pub fn with_language<S>(mut self, language: S) -> Self
where
S: Into<String>,
{
self.language = Some(language.into());
self
}
pub fn with_delivery(mut self, delivery: Delivery) -> Self {
self.delivery = Some(BuyerShippingAddress::new_with_delivery(delivery));
self
}
}
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 = "serialize::serialize_i32",
deserialize_with = "deserialize::deserialize_i32"
)]
pub unit_price: Price,
#[serde(
serialize_with = "serialize::serialize_u32",
deserialize_with = "deserialize::deserialize_u32"
)]
pub quantity: Quantity,
/// Product type, which can be virtual or material; (possible values true or
/// false).
#[serde(rename = "virtual", skip_serializing_if = "Option::is_none")]
pub virtual_product: Option<bool>,
/// Marketplace date from which the product (or offer) is available, ISO
/// format applies, e.g. "2019-03-27T10:57:59.000+01:00".
#[serde(skip_serializing_if = "Option::is_none")]
pub listing_date: Option<chrono::NaiveDateTime>,
}
impl Product {
pub fn new<Name: Into<String>>(name: Name, unit_price: Price, quantity: Quantity) -> Self {
Self {
name: name.into(),
unit_price,
quantity,
virtual_product: None,
listing_date: None,
}
}
/// Product type, which can be virtual or material; (possible values true or
/// false).
pub fn into_virtual(mut self) -> Self {
self.virtual_product = Some(true);
self
}
/// Product type, which can be virtual or material; (possible values true or
/// false).
pub fn non_virtual(mut self) -> Self {
self.virtual_product = Some(false);
self
}
/// Marketplace date from which the product (or offer) is available, ISO
/// format applies, e.g. "2019-03-27T10:57:59.000+01:00".
pub fn with_listing_date(mut self, listing_date: chrono::NaiveDateTime) -> Self {
self.listing_date = Some(listing_date);
self
}
fn erase_listing_date(mut self) -> Self {
self.listing_date = None;
self
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ShoppingCart {
/// Section containing data of shipping method.
#[serde(skip_serializing_if = "Option::is_none")]
shopping_method: Option<ShoppingMethod>,
/// Section containing data about ordered products.
/// > Note: product objects in the <shoppingCart.products> section do not
/// > have a listingDate field
#[serde(skip_serializing_if = "Option::is_none")]
products: Option<Vec<Product>>,
/// Submerchant identifier. This field should be consistent with field
/// extCustomerId in shoppingCarts section when order is placed in
/// marketplace.
ext_customer_id: String,
}
impl ShoppingCart {
pub fn new<ExtCustomerId, Products>(ext_customer_id: ExtCustomerId) -> Self
where
ExtCustomerId: Into<String>,
{
Self {
shopping_method: None,
ext_customer_id: ext_customer_id.into(),
products: None,
}
}
pub fn new_with_products<ExtCustomerId, Products>(
ext_customer_id: ExtCustomerId,
products: Products,
) -> Self
where
ExtCustomerId: Into<String>,
Products: Iterator<Item = Product>,
{
Self {
shopping_method: None,
ext_customer_id: ext_customer_id.into(),
products: Some(products.map(Product::erase_listing_date).collect()),
}
}
pub fn with_products<Products>(mut self, products: Products) -> Self
where
Products: Iterator<Item = Product>,
{
self.products = Some(products.map(Product::erase_listing_date).collect());
self
}
/// Section containing data of shipping method.
pub fn shopping_method(&self) -> &Option<ShoppingMethod> {
&self.shopping_method
}
/// Section containing data about ordered products.
/// > Note: product objects in the <shoppingCart.products> section do not
/// > have a listingDate field
pub fn products(&self) -> &Option<Vec<Product>> {
&self.products
}
/// Submerchant identifier. This field should be consistent with field
/// extCustomerId in shoppingCarts section when order is placed in
/// marketplace.
pub fn ext_customer_id(&self) -> &String {
&self.ext_customer_id
}
}
/// Type of shipment
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ShoppingMethodType {
Courier,
CollectionPointPickup,
ParcelLocker,
StorePickup,
}
/// Delivery address
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Address {
/// The full name of the pickup point, including its unique identifier, e.g.
/// „Parcel locker POZ29A”.
#[serde(skip_serializing_if = "Option::is_none")]
pub point_id: Option<String>,
/// Street name, possibly including house and flat number.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub street: Option<String>,
/// Street number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub street_no: Option<String>,
/// Flat number
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub flat_no: Option<String>,
/// Postal code
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub postal_code: Option<String>,
/// City
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub city: Option<String>,
/// Two-letter country code compliant with ISO-3166
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub country_code: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ShoppingMethod {
/// Shipping type
/// Recommended
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub shopping_type: Option<ShoppingMethodType>,
/// Shipping cost
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
/// Section containing data about shipping address.
/// Recommended
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<Address>,
}
/// 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,
}
}

View File

@ -0,0 +1,382 @@
//! Objects used to send requests to PayU
use serde::{Deserialize, Serialize};
use crate::credit::Credit;
pub use crate::model::*;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Refund {
pub(crate) description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) amount: Option<Price>,
}
impl Refund {
pub fn new<Description>(description: Description, amount: Option<Price>) -> Self
where
Description: Into<String>,
{
Self {
description: description.into(),
amount,
}
}
pub fn description(&self) -> &str {
&self.description
}
pub fn amount(&self) -> Option<Price> {
self.amount
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrderCreate {
/// 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(crate) ext_order_id: Option<String>,
/// 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(crate) notify_url: Option<String>,
/// 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")]
pub(crate) continue_url: Option<String>,
/// Payers IP address, e.g. 123.123.123.123. Note: 0.0.0.0 is not
/// accepted.
pub(crate) customer_ip: String,
/// Secret pos ip. This is connected to PayU account
#[serde(
serialize_with = "crate::serialize::serialize_newtype",
deserialize_with = "crate::deserialize::deserialize_i32_newtype"
)]
pub(crate) merchant_pos_id: MerchantPosId,
/// Transaction description
pub(crate) description: String,
/// 3 characters currency identifier, ex. PLN
pub(crate) 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 = "crate::serialize::serialize_i32",
deserialize_with = "crate::deserialize::deserialize_i32"
)]
pub(crate) total_amount: Price,
/// @see [crate::Buyer]
pub(crate) buyer: Option<Buyer>,
/// List of products
pub(crate) products: Vec<Product>,
#[serde(skip_serializing)]
pub(crate) 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")]
pub(crate) validity_time: Option<u16>,
/// Additional description of the order.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) additional_description: Option<String>,
/// Text visible on the PayU payment page (max. 80 chars).
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) 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.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) statement_description: Option<String>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub(crate) muct: Option<muct::MultiUseCartToken>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) credit: Option<Credit>,
}
impl OrderCreate {
pub fn build<CustomerIp, Currency, Description>(
buyer: Buyer,
customer_ip: CustomerIp,
currency: Currency,
description: Description,
) -> Result<Self>
where
CustomerIp: Into<String>,
Currency: Into<String>,
Description: Into<String>,
{
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,
continue_url: None,
customer_ip,
merchant_pos_id: 0,
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,
credit: 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
}
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
where
Products: Iterator<Item = Product>,
{
self.products.extend(products);
self.total_amount = self
.products
.iter()
.fold(0, |agg, p| agg + (p.quantity as i32 * p.unit_price as i32));
self
}
pub fn with_product(mut self, product: Product) -> Self {
self.products.push(product);
self.total_amount = self
.products
.iter()
.fold(0, |agg, p| agg + (p.quantity as i32 * p.unit_price as i32));
self
}
/// Description of the an order.
///
/// > This method will override initial description!
pub fn with_description<Description>(mut self, desc: Description) -> Self
where
Description: Into<String>,
{
self.description = String::from(desc.into().trim());
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
}
/// 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.
pub fn with_statement_description<Description>(mut self, desc: Description) -> Self
where
Description: Into<String>,
{
self.statement_description = Some(String::from(desc.into().trim()));
self
}
/// Add url to which PayU will be able to send http request with payment
/// status updates
///
/// All requests from PayU should receive 200 response!
///
/// See more [crate::res::Order::notify_url]
pub fn with_notify_url<NotifyUrl>(mut self, notify_url: NotifyUrl) -> Self
where
NotifyUrl: Into<String>,
{
self.notify_url = Some(notify_url.into());
self
}
/// 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).
pub fn with_continue_url<ContinueUrl>(mut self, continue_url: ContinueUrl) -> Self
where
ContinueUrl: Into<String>,
{
self.continue_url = Some(continue_url.into());
self
}
/// Section containing credit data. This information is not required,
/// but it is strongly recommended to include it. Otherwise the
/// buyer will be prompted to provide missing data on provider
/// page when payment by Installments or Pay later.
pub fn with_credit(mut self, credit: Credit) -> Self {
self.credit = Some(credit);
self
}
/// 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 |
pub fn notify_url(&self) -> &Option<String> {
&self.notify_url
}
/// Customer IP address from http request received from client
pub fn customer_ip(&self) -> &String {
&self.customer_ip
}
pub fn merchant_pos_id(&self) -> MerchantPosId {
self.merchant_pos_id
}
pub fn description(&self) -> &String {
&self.description
}
pub fn currency_code(&self) -> &String {
&self.currency_code
}
pub fn total_amount(&self) -> &Price {
&self.total_amount
}
pub fn buyer(&self) -> &Option<Buyer> {
&self.buyer
}
pub fn products(&self) -> &[Product] {
&self.products
}
pub fn order_create_date(&self) -> &Option<String> {
&self.order_create_date
}
pub(crate) fn with_merchant_pos_id(mut self, merchant_pos_id: MerchantPosId) -> Self {
self.merchant_pos_id = merchant_pos_id;
self
}
}

View File

@ -0,0 +1,144 @@
use crate::model::*;
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreateOrder {
/// Http status as a text
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: OrderId,
/// This is YOUR_EXT_ORDER_ID
pub ext_order_id: Option<String>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RefundDetails {
pub order_id: Option<String>,
pub refund: Option<Refund>,
pub status: Status,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Refunds {
pub refunds: Vec<Refund>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionPayMethod {
pub value: String,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CardProfile {
Consumer,
Business,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CardClassification {
Debit,
Credit,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionCartData {
/// // "543402******4014",
pub card_number_masked: String,
/// MC (MasterCard/Maestro), VS (Visa)
/// Example; "MC"
pub card_scheme: String,
pub card_profile: CardProfile,
pub card_classification: CardClassification,
/// Example: "000"
pub card_response_code: String,
/// Example: "000 - OK"
pub card_response_code_desc: String,
/// Example: "5"
pub card_eci_code: String,
/// Example: "AY",
pub card3ds_status: String,
/// Example: "PL",
pub card_bin_country: String,
/// Example: "MCC0111LL1121"
pub first_transaction_id: String,
}
/// > Installment proposal on the Sandbox environment is not related to the
/// > order amount and always returns data for 480 PLN.
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionCardInstallmentProposal {
/// Example: "5aff3ba8-0c37-4da1-ba4a-4ff24bcc2eed"
pub proposal_id: String,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionCart {
pub cart_data: TransactionCartData,
pub card_installment_proposal: TransactionCardInstallmentProposal,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
pub pay_method: TransactionPayMethod,
pub payment_flow: String,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Transactions {
pub transactions: Vec<Transaction>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Order {
/// Example: "{orderId}",
pub order_id: super::OrderId,
/// Example: "358766",
pub ext_order_id: Option<String>,
/// Example: "2014-10-27T14:58:17.443+01:00",
pub order_create_date: String,
/// Example: "http://localhost/OrderNotify/",
pub notify_url: Option<String>,
/// Example: "127.0.0.1",
pub customer_ip: String,
/// Example: "145227",
pub merchant_pos_id: String,
/// Example: "New order",
pub description: String,
/// Example: "PLN",
pub currency_code: String,
/// Example: "3200",
pub total_amount: String,
/// Example: "NEW",
pub status: String,
/// Example: `[{"name":"Product1","unitPrice":"1000","quantity":"1"}]`
pub products: Vec<super::Product>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrdersInfo {
pub orders: Vec<Order>,
pub status: Status,
pub properties: Option<Vec<Prop>>,
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct OrderInfo {
pub order: Order,
pub status: Status,
pub properties: Option<Vec<Prop>>,
}

View File

@ -0,0 +1,25 @@
use std::fmt::Display;
pub(crate) fn serialize_i32<S>(v: &i32, ser: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser.serialize_str(&format!("{v}"))
}
pub(crate) fn serialize_newtype<N: Display, S>(
v: &N,
ser: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser.serialize_str(&format!("{v}"))
}
pub(crate) fn serialize_u32<S>(v: &u32, ser: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser.serialize_str(&format!("{v}"))
}

View File

@ -34,6 +34,8 @@ wapc = { version = "1.0.0", features = [] }
wapc-codec = { version = "1.0.0" }
wapc-pool = { version = "1.0.0" }
wasmtime-provider = { version = "1.3.2", features = [] }
reqwest = { version = "0.11.13", features = ["default", "json", "blocking"] }
#pay_u = { path = "../../vendor/pay_u" }
[dev-dependencies]
fake = { version = "2.5.0" }

View File

@ -1,7 +1,12 @@
use std::fs::read_dir;
use std::path::PathBuf;
use std::str::FromStr;
use bincode::deserialize;
use config::{AppConfig, UpdateConfig};
use payment_adapter::{HttpMethod, HttpRequest};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use wapc::WasiParams;
use wapc_pool::HostPoolBuilder;
// use payment_adapter::{CreatePayment, PaymentAdapter, Status};
@ -43,7 +48,7 @@ async fn main() {
)
});
for file in dir.filter_map(|r| r.map(|r| r.path()).ok()) {
eprintln!("{:?}", file.extension());
eprintln!("{:?}", file);
if file.extension().and_then(|s| s.to_str()) != Some("wasm") {
continue;
}
@ -51,6 +56,7 @@ async fn main() {
let module = std::fs::read(&file).unwrap();
let engine = wasmtime_provider::WasmtimeEngineProviderBuilder::new()
.module_bytes(&module)
.wasi_params(WasiParams::default())
.build()
.unwrap();
@ -59,7 +65,11 @@ async fn main() {
.factory(move || {
wapc::WapcHost::new(
Box::new(engine.clone()),
Some(Box::new(move |_a, _b, _c, _d, _e| Ok(vec![]))),
Some(Box::new(
move |_a, _binding, _namespace, msg_name, payload| {
Ok(host_call(msg_name, payload)?)
},
)),
)
.unwrap()
})
@ -67,6 +77,18 @@ async fn main() {
.build();
{
let msg = config
.lock()
.payment()
.providers
.get("pay_u")
.cloned()
.unwrap_or_default();
pool.call("init", wapc_codec::messagepack::serialize(msg).unwrap())
.await
.unwrap();
let msg = payment_adapter::CreatePayment {
buyer: payment_adapter::Buyer {
email: "email".to_string(),
@ -105,3 +127,123 @@ async fn main() {
// let mqtt_client = mqtt::start(config.clone(), db.clone()).await;
// rpc::start(config, db, mqtt_client).await;
}
fn host_call(name: &str, payload: &[u8]) -> Result<Vec<u8>, payment_adapter::Error> {
match name {
"http_req" => {
let req: HttpRequest = match deserialize(payload) {
Ok(req) => req,
_ => return Ok(vec![]),
};
http_request(req)
}
_ => Ok(vec![]),
}
}
fn http_request(req: HttpRequest) -> Result<Vec<u8>, payment_adapter::Error> {
let HttpRequest {
method,
url,
headers,
bearer_auth,
body,
} = req;
let client = reqwest::blocking::ClientBuilder::default()
.user_agent("curl/7.82.0")
// .use_native_tls()
// Do not follow redirect!
.redirect(reqwest::redirect::Policy::none())
.connection_verbose(true)
.build()
.expect("Failed to create client");
let headers = {
let len = headers.len();
headers
.into_iter()
.fold(HeaderMap::with_capacity(len), |mut m, (k, v)| {
if let (Ok(v), Ok(k)) =
(HeaderValue::from_str(v.as_str()), HeaderName::from_str(&k))
{
m.insert(k, v);
}
m
})
};
match (method, bearer_auth) {
(HttpMethod::Get, Some(bearer)) => {
let text = client
.get(url)
.headers(headers)
.bearer_auth(bearer)
.send()
.map_err(|e| {
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?
.text()
.map_err(|e| {
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?;
Ok(text.into_bytes())
}
(HttpMethod::Get, _) => {
let text = client
.get(url)
.headers(headers)
.send()
.map_err(|e| {
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?
.text()
.map_err(|e| {
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?;
Ok(text.into_bytes())
}
(HttpMethod::Post, Some(bearer)) => {
let body = body.unwrap_or_default();
let len = body.len();
let body = std::io::Cursor::new(body);
let body = reqwest::blocking::Body::sized(body, len as u64);
let text = client
.post(url)
.headers(headers)
.bearer_auth(bearer)
.body(body)
.send()
.map_err(|e| {
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?
.text()
.map_err(|e| {
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?;
Ok(text.into_bytes())
}
(HttpMethod::Post, None) => {
let text = client
.post(url)
.headers(headers)
.body(body.unwrap_or_default())
.send()
.map_err(|e| {
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?
.text()
.map_err(|e| {
tracing::error!("{}", e);
payment_adapter::Error::HttpFailed
})?;
Ok(text.into_bytes())
}
}
}

View File

@ -1,109 +0,0 @@
use std::collections::HashMap;
use model::*;
// use crate::{Buyer, Error, PayUClient, Result};
#[derive(Debug, thiserror::Error)]
pub enum Error {}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
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 From<Buyer> for pay_u::Buyer {
fn from(b: Buyer) -> Self {
pay_u::Buyer::new(b.email, b.phone, b.first_name, b.last_name, b.language)
}
}
#[derive(Debug)]
pub struct Product {
pub id: ProductId,
pub name: String,
pub unit_price: Price,
pub quantity_unit: QuantityUnit,
pub quantity: Quantity,
}
impl From<Product> for pay_u::Product {
fn from(p: Product) -> Self {
pay_u::Product::new(p.name, **p.unit_price, **p.quantity as u32)
}
}
pub struct CreatePayment {
pub client: PayUClient,
pub buyer: Buyer,
pub customer_ip: String,
pub currency: String,
pub description: String,
pub cart_products: Vec<model::Product>,
pub items: HashMap<ProductId, (Quantity, QuantityUnit)>,
pub order_ext_id: String,
pub notify_uri: String,
pub continue_uri: String,
}
impl CreatePayment {
pub(crate) async fn create_payment(self) -> Result<(String, ExtOrderId)> {
let CreatePayment {
client,
buyer,
customer_ip,
currency,
description,
cart_products,
mut items,
order_ext_id,
notify_uri,
continue_uri,
} = self;
let pay_u::res::CreateOrder {
status: _,
redirect_uri,
order_id,
ext_order_id: _,
} = client
.lock()
.create_order(
pay_u::req::OrderCreate::build(buyer.into(), customer_ip, currency, description)
.map_err(|e| {
tracing::error!("{}", e);
Error::InvalidOrder
})?
.with_products(cart_products.into_iter().map(|p| {
pay_u::Product::new(
p.name.to_string(),
**p.price,
items
.remove(&p.id)
.map(|(quantity, _)| **quantity as u32)
.unwrap_or_default(),
)
}))
.with_ext_order_id(order_ext_id)
.with_notify_url(notify_uri)
.with_continue_url(continue_uri),
)
.await
.map_err(|e| {
tracing::error!("{}", e);
Error::PaymentFailed
})?;
Ok((redirect_uri, ExtOrderId::new(order_id.0)))
}
}

View File

@ -1295,17 +1295,6 @@ impl Client {
"https://secure.payu.com/api/v2_1"
}
}
fn build_client() -> reqwest::Client {
reqwest::ClientBuilder::default()
.user_agent("curl/7.82.0")
// .use_native_tls()
// Do not follow redirect!
.redirect(redirect::Policy::none())
.connection_verbose(true)
.build()
.expect("Failed to create client")
}
}
#[cfg(test)]