diff --git a/.cargo/config.toml b/.cargo/config.toml index 3e26d32..c62c036 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,3 +3,5 @@ linker = "clang" rustflags = [ "-C", "link-arg=-fuse-ld=mold", ] +[build] +rustc-wrapper = "/usr/bin/sccache" diff --git a/Cargo.lock b/Cargo.lock index 2987682..8d94503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -599,7 +599,7 @@ checksum = "0b0e103ce36d217d568903ad27b14ec2238ecb5d65bad2e756a8f3c0d651506e" dependencies = [ "cap-primitives", "cap-std", - "io-lifetimes", + "io-lifetimes 0.7.5", "windows-sys 0.36.1", ] @@ -612,10 +612,10 @@ dependencies = [ "ambient-authority", "fs-set-times", "io-extras", - "io-lifetimes", + "io-lifetimes 0.7.5", "ipnet", "maybe-owned", - "rustix", + "rustix 0.35.13", "winapi-util", "windows-sys 0.36.1", "winx", @@ -639,9 +639,9 @@ checksum = "c9d6e70b626eceac9d6fc790fe2d72cc3f2f7bc3c35f467690c54a526b0f56db" dependencies = [ "cap-primitives", "io-extras", - "io-lifetimes", + "io-lifetimes 0.7.5", "ipnet", - "rustix", + "rustix 0.35.13", ] [[package]] @@ -652,7 +652,7 @@ checksum = "c3a0524f7c4cff2ea547ae2b652bf7a348fd3e48f76556dc928d8b45ab2f1d50" dependencies = [ "cap-primitives", "once_cell", - "rustix", + "rustix 0.35.13", "winx", ] @@ -767,7 +767,6 @@ dependencies = [ "cookie", "parking_lot 0.12.1", "password-hash", - "pay_u", "serde", "serde_json", "thiserror", @@ -856,18 +855,18 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c200df7d943cd2b8cb3a67f6a56781c63849f122d74deff24d1767c3918b0bdc" +checksum = "b62c772976416112fa4484cbd688cb6fb35fd430005c1c586224fc014018abad" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-codegen" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f365623f4c3d576f47f11868568d0c90e18ac169497a9ed73c433fe2d3f9f2fb" +checksum = "9b40ed2dd13c2ac7e24f88a3090c68ad3414eb1d066a95f8f1f7b3b819cb4e46" dependencies = [ "arrayvec", "bumpalo", @@ -886,24 +885,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbaf79f8ae63bd86dc40a04417a7cc1691a217f6db204438026c164679b4694" +checksum = "bb927a8f1c27c34ee3759b6b0ffa528d2330405d5cc4511f0cab33fe2279f4b5" dependencies = [ "cranelift-codegen-shared", ] [[package]] name = "cranelift-codegen-shared" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587db55845c943d8211e9c7198a977fa6686b44f18df15f31cec9a12fcf5dda8" +checksum = "43dfa417b884a9ab488d95fd6b93b25e959321fe7bfd7a0a960ba5d7fb7ab927" [[package]] name = "cranelift-egraph" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6dccc0b16b7b8c1278162e436beebb35f3d321743b639d2b578138d630f43e" +checksum = "e0a66b39785efd8513d2cca967ede56d6cc57c8d7986a595c7c47d0c78de8dce" dependencies = [ "cranelift-entity", "fxhash", @@ -915,18 +914,18 @@ dependencies = [ [[package]] name = "cranelift-entity" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b062935d2c6dba87387d2ac163eb9c54967ed6143c3136fffaba8acb5eaa9e" +checksum = "0637ffde963cb5d759bc4d454cfa364b6509e6c74cdaa21298add0ed9276f346" dependencies = [ "serde", ] [[package]] name = "cranelift-frontend" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476ea81fe736b858d2d2c53b9d9fd28082589f57ebe4e1654a68af7359800a0c" +checksum = "fb72b8342685e850cb037350418f62cc4fc55d6c2eb9c7ca01b82f9f1a6f3d56" dependencies = [ "cranelift-codegen", "log", @@ -936,15 +935,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c50a465703c15d3d913f6b0db8320c4e92c940f0f0cad874c7fcf5aecc066c0" +checksum = "850579cb9e4b448f7c301f1e6e6cbad99abe3f1f1d878a4994cb66e33c6db8cd" [[package]] name = "cranelift-native" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d9e0d1382584b8d454ec12c86fd562b64ccd454c1199846c1b7d158db9ed38" +checksum = "2d0a279e5bcba3e0466c734d8d8eb6bfc1ad29e95c37f3e4955b492b5616335e" dependencies = [ "cranelift-codegen", "libc", @@ -953,9 +952,9 @@ dependencies = [ [[package]] name = "cranelift-wasm" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f54959195c47437544a1a4d2602381949a12918e0179bcc82d909cc34cf08dd" +checksum = "e6b8c5e7ffb754093fb89ec4bd4f9dbb9f1c955427299e334917d284745835c2" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -1205,6 +1204,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1225,6 +1234,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "displaydoc" version = "0.2.3" @@ -1355,6 +1375,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime 2.1.0", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.2.8" @@ -1472,6 +1505,16 @@ dependencies = [ "tasque", ] +[[package]] +name = "file-per-thread-logger" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e16290574b39ee41c71aeb90ae960c504ebaf1e2a1c87bd52aa56ed6e1a02f" +dependencies = [ + "env_logger 0.9.3", + "log", +] + [[package]] name = "flate2" version = "1.0.25" @@ -1575,8 +1618,8 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a267b6a9304912e018610d53fe07115d8b530b160e85db4d2d3a59f3ddde1aec" dependencies = [ - "io-lifetimes", - "rustix", + "io-lifetimes 0.7.5", + "rustix 0.35.13", "windows-sys 0.36.1", ] @@ -1872,6 +1915,25 @@ dependencies = [ "tracing", ] +[[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 = "handlebars" version = "4.3.5" @@ -2050,6 +2112,30 @@ dependencies = [ "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", + "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" @@ -2165,7 +2251,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5d8c2ab5becd8720e30fd25f8fa5500d8dc3fceadd8378f05859bd7b46fc49" dependencies = [ - "io-lifetimes", + "io-lifetimes 0.7.5", "windows-sys 0.36.1", ] @@ -2179,6 +2265,16 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + [[package]] name = "iovec" version = "0.1.4" @@ -2207,8 +2303,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d508111813f9af3afd2f92758f77e4ed2cc9371b642112c6a48d22eb73105c5" dependencies = [ "hermit-abi 0.2.6", - "io-lifetimes", - "rustix", + "io-lifetimes 0.7.5", + "rustix 0.35.13", "windows-sys 0.36.1", ] @@ -2227,6 +2323,26 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +[[package]] +name = "ittapi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c4f6ff06169ce7048dac5150b1501c7e3716a929721aeb06b87e51a43e42f4" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e078cce01485f418bae3beb34dd604aaedf2065502853c7da17fbce8e64eda" +dependencies = [ + "cc", +] + [[package]] name = "jobserver" version = "0.1.25" @@ -2370,6 +2486,12 @@ version = "0.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" +[[package]] +name = "linux-raw-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" + [[package]] name = "local-channel" version = "0.1.3" @@ -2462,6 +2584,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memfd" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b20a59d985586e4a5aef64564ac77299f8586d8be6cf9106a5a40207e8908efb" +dependencies = [ + "rustix 0.36.4", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -2542,6 +2673,19 @@ 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" @@ -2967,16 +3111,16 @@ checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" [[package]] name = "pay_u" version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c9728962c794530dd3e728dd1ad23326dcbfbea34442f2cb0f789ba5e7a112" dependencies = [ "chrono", "derive_more", + "dotenv", "log", - "reqwest", + "reqwest_wasi", "serde", "serde_json", "thiserror", + "tokio", ] [[package]] @@ -2985,8 +3129,17 @@ version = "0.1.0" dependencies = [ "config", "model", + "wasmtime", +] + +[[package]] +name = "payment_adapter_pay_u" +version = "0.1.0" +dependencies = [ + "pay_u", + "payment_adapter", + "wasmtime", "wasmtime-api", - "wasmtime-wasi", ] [[package]] @@ -3002,6 +3155,7 @@ dependencies = [ "model", "opentelemetry 0.17.0", "opentelemetry-jaeger", + "payment_adapter", "rumqttc", "serde", "sqlx", @@ -3015,6 +3169,7 @@ dependencies = [ "tracing-subscriber", "uuid 1.2.2", "wasmtime", + "wasmtime-wasi", ] [[package]] @@ -3025,9 +3180,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f400b0f7905bf702f9f3dc3df5a121b16c54e9e8012c082905fdf09a931861a" +checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" dependencies = [ "thiserror", "ucd-trie", @@ -3035,9 +3190,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "423c2ba011d6e27b02b482a3707c773d19aec65cc024637aec44e19652e66f63" +checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344" dependencies = [ "pest", "pest_generator", @@ -3045,9 +3200,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e64e6c2c85031c02fdbd9e5c72845445ca0a724d419aa0bc068ac620c9935c1" +checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c" dependencies = [ "pest", "pest_meta", @@ -3058,9 +3213,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57959b91f0a133f89a68be874a5c88ed689c19cd729ecdb5d762ebf16c64d662" +checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20" dependencies = [ "once_cell", "pest", @@ -3123,7 +3278,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" dependencies = [ - "env_logger", + "env_logger 0.7.1", "log", ] @@ -3519,6 +3674,41 @@ dependencies = [ "winreg", ] +[[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 = "ring" version = "0.16.20" @@ -3643,14 +3833,28 @@ checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9" dependencies = [ "bitflags", "errno", - "io-lifetimes", + "io-lifetimes 0.7.5", "itoa", "libc", - "linux-raw-sys", + "linux-raw-sys 0.0.46", "once_cell", "windows-sys 0.42.0", ] +[[package]] +name = "rustix" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes 1.0.3", + "libc", + "linux-raw-sys 0.1.3", + "windows-sys 0.42.0", +] + [[package]] name = "rustls" version = "0.20.7" @@ -4273,9 +4477,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", @@ -4292,8 +4496,8 @@ dependencies = [ "bitflags", "cap-fs-ext", "cap-std", - "io-lifetimes", - "rustix", + "io-lifetimes 0.7.5", + "rustix 0.35.13", "windows-sys 0.36.1", "winx", ] @@ -4552,9 +4756,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", @@ -4624,6 +4828,38 @@ dependencies = [ "tracing", ] +[[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 1.1.0", + "bytes", + "libc", + "memchr", + "mio_wasi", + "num_cpus", + "pin-project-lite", + "socket2", + "wasmedge_wasi_socket", + "winapi 0.3.9", +] + [[package]] name = "toml" version = "0.5.9" @@ -4941,9 +5177,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi-cap-std-sync" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe15d7e9ee5bb76cb64b9c29ff00c62642e8552e7f2a8b4758897b0a89a582d" +checksum = "ecbeebb8985a5423f36f976b2f4a0b3c6ce38d7d9a7247e1ce07aa2880e4f29b" dependencies = [ "anyhow", "async-trait", @@ -4953,10 +5189,10 @@ dependencies = [ "cap-time-ext", "fs-set-times", "io-extras", - "io-lifetimes", + "io-lifetimes 0.7.5", "is-terminal", "once_cell", - "rustix", + "rustix 0.35.13", "system-interface", "tracing", "wasi-common", @@ -4965,16 +5201,16 @@ dependencies = [ [[package]] name = "wasi-common" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e0ef82a2154554def1a220afd48f95cb0f22be343b16930e8957113bd3d967" +checksum = "81e2171f3783fe6600ee24ff6c58ca1b329c55e458cc1622ecc1fd0427648607" dependencies = [ "anyhow", "bitflags", "cap-rand", "cap-std", "io-extras", - "rustix", + "rustix 0.35.13", "thiserror", "tracing", "wasmtime", @@ -5050,6 +5286,24 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +[[package]] +name = "wasm-encoder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05632e0a66a6ed8cca593c24223aabd6262f256c3693ad9822c315285f010614" +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" @@ -5061,11 +5315,12 @@ dependencies = [ [[package]] name = "wasmtime" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad9bd12d0823195f6c833f340d8d1df39e2bbf40f5767416560ca7476b97e47" +checksum = "d18265705b1c49218776577d9f301d79ab06888c7f4a32e2ed24e68a55738ce7" dependencies = [ "anyhow", + "async-trait", "bincode", "cfg-if 1.0.0", "indexmap", @@ -5079,10 +5334,13 @@ dependencies = [ "serde", "target-lexicon", "wasmparser", + "wasmtime-cache", "wasmtime-cranelift", "wasmtime-environ", + "wasmtime-fiber", "wasmtime-jit", "wasmtime-runtime", + "wat", "windows-sys 0.36.1", ] @@ -5094,18 +5352,38 @@ checksum = "f6db9127057c8be35bf5e4dd3d124b99ffe65db4b5631e701a115e3fb4d92590" [[package]] name = "wasmtime-asm-macros" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b6694b753be856b36d47744cdf2bd525bac53d0de5981132d5430bb62c496e4" +checksum = "a201583f6c79b96e74dcce748fa44fb2958f474ef13c93f880ea4d3bed31ae4f" dependencies = [ "cfg-if 1.0.0", ] [[package]] -name = "wasmtime-cranelift" -version = "3.0.0" +name = "wasmtime-cache" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c55d30708ebc24b6fa2a247807821642967487388845c7fc5320fef1010abe8" +checksum = "3f37efc6945b08fcb634cffafc438dd299bac55a27c836954656c634d3e63c31" +dependencies = [ + "anyhow", + "base64", + "bincode", + "directories-next", + "file-per-thread-logger", + "log", + "rustix 0.35.13", + "serde", + "sha2", + "toml", + "windows-sys 0.36.1", + "zstd", +] + +[[package]] +name = "wasmtime-cranelift" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe208297e045ea0ee6702be88772ea40f918d55fbd4163981a4699aff034b634" dependencies = [ "anyhow", "cranelift-codegen", @@ -5124,9 +5402,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01be016d65ec9200a2d4efbc2ca983bbb7264332e49c11179aaf7587e57d854d" +checksum = "754b97f7441ac780a7fa738db5b9c23c1b70ef4abccd8ad205ada5669d196ba2" dependencies = [ "anyhow", "cranelift-entity", @@ -5142,10 +5420,23 @@ dependencies = [ ] [[package]] -name = "wasmtime-jit" -version = "3.0.0" +name = "wasmtime-fiber" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d36042d7962fa1b2a6bfb96d3b33e2283138e7396bc29b2c6970f2a1e80a0ed" +checksum = "e5f54abc960b4a055ba16b942cbbd1da641e0ad44cc97a7608f3d43c069b120e" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "rustix 0.35.13", + "wasmtime-asm-macros", + "windows-sys 0.36.1", +] + +[[package]] +name = "wasmtime-jit" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32800cb6e29faabab7056593f70a4c00c65c75c365aaf05406933f2169d0c22f" dependencies = [ "addr2line", "anyhow", @@ -5153,6 +5444,7 @@ dependencies = [ "cfg-if 1.0.0", "cpp_demangle", "gimli", + "ittapi", "log", "object", "rustc-demangle", @@ -5160,6 +5452,7 @@ dependencies = [ "target-lexicon", "thiserror", "wasmtime-environ", + "wasmtime-jit-debug", "wasmtime-jit-icache-coherence", "wasmtime-runtime", "windows-sys 0.36.1", @@ -5167,18 +5460,20 @@ dependencies = [ [[package]] name = "wasmtime-jit-debug" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad4511b8abbdbaf3e9aaa4044ead8bd31b70e2da5e43e2cb91605f871ca23d56" +checksum = "fe057012a0ba6cee3685af1e923d6e0a6cb9baf15fb3ffa4be3d7f712c7dec42" dependencies = [ + "object", "once_cell", + "rustix 0.35.13", ] [[package]] name = "wasmtime-jit-icache-coherence" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb7b3e58024d8d395dfc4efbe2a58360a1998565b118b0342b3cf62a4084bde" +checksum = "e6bbabb309c06cc238ee91b1455b748c45f0bdcab0dda2c2db85b0a1e69fcb66" dependencies = [ "cfg-if 1.0.0", "libc", @@ -5187,9 +5482,9 @@ dependencies = [ [[package]] name = "wasmtime-runtime" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4034f371135e9e2e81430dda14f6f5a49a222c6557ec4f65301edc093b216d38" +checksum = "09a23b6e138e89594c0189162e524a29e217aec8f9a4e1959a34f74c64e8d17d" dependencies = [ "anyhow", "cc", @@ -5198,22 +5493,24 @@ dependencies = [ "libc", "log", "mach", + "memfd", "memoffset 0.6.5", "paste", "rand 0.8.5", - "rustix", + "rustix 0.35.13", "thiserror", "wasmtime-asm-macros", "wasmtime-environ", + "wasmtime-fiber", "wasmtime-jit-debug", "windows-sys 0.36.1", ] [[package]] name = "wasmtime-types" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a770de14a3b5676dfd8a3c06bcab829d9cb58b113911634f3ec3b6960b1d79e3" +checksum = "68ec7615fde8c79737f1345d81f0b18da83b3db929a87b4604f27c932246d1e2" dependencies = [ "cranelift-entity", "serde", @@ -5223,9 +5520,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648d6b4360af358bf2a0688ef7e35d4b413f7185257bf8de6a58f09fb4d7eca1" +checksum = "ca539adf155dca1407aa3656e5661bf2364b1f3ebabc7f0a8bd62629d876acfa" dependencies = [ "anyhow", "wasi-cap-std-sync", @@ -5243,6 +5540,27 @@ dependencies = [ "leb128", ] +[[package]] +name = "wast" +version = "50.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2cbb59d4ac799842791fe7e806fa5dbbf6b5554d538e51cc8e176db6ff0ae34" +dependencies = [ + "leb128", + "memchr", + "unicode-width", + "wasm-encoder", +] + +[[package]] +name = "wat" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "584aaf7a1ecf4d383bbe1a25eeab0cbb8ff96acc6796707ff65cde48f4632f15" +dependencies = [ + "wast 50.0.0", +] + [[package]] name = "web" version = "0.1.0" @@ -5315,9 +5633,9 @@ dependencies = [ [[package]] name = "wiggle" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff29f3353b12c949adc6ad6d89edd87f3fa227b1ee1a26f437ae5e9dfe42ba5f" +checksum = "2da09ca5b8bb9278a2123e8c36342166b9aaa55a0dbab18b231f46d6f6ab85bc" dependencies = [ "anyhow", "async-trait", @@ -5330,9 +5648,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03743b2f04849564d6a2cd6ba32861d93f2d46baddad449473ec399d58b78e3" +checksum = "ba5796f53b429df7d44cfdaae8f6d9cd981d82aec3516561352ca9c5e73ee185" dependencies = [ "anyhow", "heck", @@ -5345,9 +5663,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "544319bbf95f2e0fc2c410b2098aff28a885e6cf59d02a67f5647eec1679d4ec" +checksum = "8b830eb7203d48942fb8bc8bb105f76e7d09c33a082d638e990e02143bb2facd" dependencies = [ "proc-macro2", "quote", @@ -5514,7 +5832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7b01e010390eb263a4518c8cebf86cb67469d1511c00b749a47b64c39e8054d" dependencies = [ "bitflags", - "io-lifetimes", + "io-lifetimes 0.7.5", "windows-sys 0.36.1", ] @@ -5527,7 +5845,7 @@ dependencies = [ "anyhow", "log", "thiserror", - "wast", + "wast 35.0.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8702355..82a4087 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,14 @@ members = [ "crates/fs_manager", "crates/lang_provider", "crates/payment_adapter", + "crates/payment_adapter_pay_u", # artifacts # "crates/db-seed", # "crates/api", "crates/web", # vendor "vendor/t_pay", + "vendor/pay_u", ] [profile.release] diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index fc78a40..20549ca 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -20,7 +20,7 @@ async-trait = { version = "0.1", features = [] } bytes = { version = "1.1.0" } channels = { path = "../channels", features = ['accounts', 'carts', 'emails', 'search'] } chrono = { version = "0.4", features = ["serde"] } -config = { path = "../config" } +config = { path = "../config", features = ['full'] } #database_manager = { path = "../database_manager" } derive_more = { version = "0.99", features = [] } dotenv = { version = "0.15", features = [] } diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index dc0d52b..7c5815d 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -3,12 +3,16 @@ name = "config" version = "0.1.0" edition = "2021" +[features] +full = ['actix-web', 'cookie'] +default = [] + [dependencies] -actix-web = { version = "4.0", features = [] } -cookie = { version = "0.16.1", features = ["signed"] } +actix-web = { version = "4.0", features = [], optional = true } +cookie = { version = "0.16.1", features = ["signed"], optional = true } parking_lot = { version = "0.12", features = [] } password-hash = { version = "0.4", features = ["alloc"] } -pay_u = { version = '0.1', features = ["single-client"] } +#pay_u = { path = "../../vendor/pay_u", features = ["single-client"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = [] } thiserror = { version = "1.0" } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 2ef50e4..8e63472 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -99,6 +99,7 @@ impl WebConfig { .expect("Invalid password hash") } + #[cfg(feature = "full")] pub fn session_secret(&self) -> actix_web::cookie::Key { use actix_web::cookie::Key; diff --git a/crates/payment_adapter/Cargo.toml b/crates/payment_adapter/Cargo.toml index 2925e1e..431677d 100644 --- a/crates/payment_adapter/Cargo.toml +++ b/crates/payment_adapter/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -wasmtime-wasi = { version = "3.0.0" } -wasmtime-api = { version = "0.4.0" } -config = { path = "../config" } +config = { path = "../config", default-features = false, features = [] } model = { path = "../model" } +wasmtime = { version = "3.0.1" } +#socket2 = { version = "0.4.7", default-features = false, features = [] } diff --git a/crates/payment_adapter/src/lib.rs b/crates/payment_adapter/src/lib.rs index 2ceb842..fa50f5b 100644 --- a/crates/payment_adapter/src/lib.rs +++ b/crates/payment_adapter/src/lib.rs @@ -1,7 +1,15 @@ use std::collections::HashMap; pub use config::PaymentProviderConfig; -use model::{Price, ProductId, Quantity, QuantityUnit}; +pub use model::*; +use wasmtime::WasmResults; + +#[derive(Debug)] +#[repr(C)] +pub enum Status { + Success = 0, + Failure = 1, +} #[derive(Debug)] pub struct Buyer { @@ -42,7 +50,7 @@ pub struct CreatePayment { pub trait PaymentAdapter { fn name() -> &'static str; - fn init(&mut self, config: &PaymentProviderConfig); + fn init(&mut self, config: PaymentProviderConfig); - fn create_payment(&self, msg: CreatePayment); + fn create_payment(&self, msg: CreatePayment) -> Status; } diff --git a/crates/payment_adapter_pay_u/Cargo.toml b/crates/payment_adapter_pay_u/Cargo.toml new file mode 100644 index 0000000..e9c609b --- /dev/null +++ b/crates/payment_adapter_pay_u/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "payment_adapter_pay_u" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ['cdylib'] + +[dependencies] +payment_adapter = { path = "../payment_adapter" } +pay_u = { path = "../../vendor/pay_u" } +wasmtime = { version = "3.0.1" } +wasmtime-api = { version = "0.4.0" } diff --git a/crates/payment_adapter_pay_u/src/lib.rs b/crates/payment_adapter_pay_u/src/lib.rs new file mode 100644 index 0000000..4778b6b --- /dev/null +++ b/crates/payment_adapter_pay_u/src/lib.rs @@ -0,0 +1,28 @@ +use payment_adapter::{CreatePayment, PaymentAdapter, PaymentProviderConfig, Status}; +use wasmtime::{ValType, WasmParams, WasmResults}; + +#[derive(Default)] +pub struct PayUAdapter {} + +impl PayUAdapter { + pub fn new() -> Self { + Self::default() + } +} + +impl PaymentAdapter for PayUAdapter { + fn name() -> &'static str { + "pay_u" + } + + fn init(&mut self, config: PaymentProviderConfig) {} + + fn create_payment(&self, msg: CreatePayment) -> Status { + Status::Failure + } +} + +#[no_mangle] +pub fn adapter() -> PayUAdapter { + PayUAdapter::new() +} diff --git a/crates/payment_manager/Cargo.toml b/crates/payment_manager/Cargo.toml index 7fdd445..c1fcc3c 100644 --- a/crates/payment_manager/Cargo.toml +++ b/crates/payment_manager/Cargo.toml @@ -27,7 +27,9 @@ tracing-opentelemetry = { version = "0.17.4" } tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } uuid = { version = "1.2.1", features = ["serde"] } wasmtime = { version = "3.0.0", default-features = false, features = ['cranelift', 'parallel-compilation', 'pooling-allocator'] } +wasmtime-wasi = { version = "3.0.0" } gumdrop = { version = "0.8.1", features = [] } +payment_adapter = { path = "../payment_adapter" } [dev-dependencies] fake = { version = "2.5.0" } diff --git a/crates/payment_manager/src/main.rs b/crates/payment_manager/src/main.rs index 5b8ba80..f96a2a4 100644 --- a/crates/payment_manager/src/main.rs +++ b/crates/payment_manager/src/main.rs @@ -2,6 +2,7 @@ use std::fs::read_dir; use std::path::PathBuf; use config::{AppConfig, UpdateConfig}; +use payment_adapter::PaymentAdapter; // mod actions; // mod context; @@ -31,6 +32,16 @@ async fn main() { let config = config::default_load(&opts); let engine = wasmtime::Engine::default(); + let mut linker = wasmtime::Linker::new(&engine); + wasmtime_wasi::add_to_linker(&mut linker, |cx| cx).unwrap(); + + let wasi_ctx = wasmtime_wasi::WasiCtxBuilder::new() + .inherit_env() + .unwrap() + .inherit_stdio() + .inherit_stdio() + .build(); + let mut store = wasmtime::Store::new(&engine, wasi_ctx); { let adapters_path = config.lock().payment().adapters_path.clone(); @@ -42,7 +53,23 @@ async fn main() { ) }); for file in dir.filter_map(|r| r.map(|r| r.path()).ok()) { - wasmtime::Module::from_file(&engine, file).unwrap(); + let module = wasmtime::Module::from_file(&engine, file).unwrap(); + let instance = linker.instantiate(&mut store, &module).unwrap(); + let fnc = + match instance.get_typed_func::<(), dyn PaymentAdapter, _>(&mut store, "adapter") { + Err(e) => { + tracing::error!("{}", e); + continue; + } + Ok(fnc) => fnc, + }; + let adapter = match fnc.call(&mut store, ()) { + Err(e) => { + tracing::error!("{}", e); + continue; + } + Ok(adapter) => adapter, + }; } } diff --git a/vendor/pay_u/Cargo.toml b/vendor/pay_u/Cargo.toml new file mode 100644 index 0000000..1af24e1 --- /dev/null +++ b/vendor/pay_u/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "pay_u" +description = "PayU Rest API wrapper" +version = "0.1.9" +edition = "2021" +license = "MIT" +repository = "https://git.sr.ht/~tsumanu/payu-rs" +authors = ["Tsumanu "] +categories = ["web-programming", "api-bindings"] +keywords = ["payment", "payu"] + +[features] +single-client = [] +default = ['single-client'] + +[dependencies] +#reqwest = { version = "*", features = ["default", "json", "blocking"] } +reqwest_wasi = { version = "*", features = ["default", "json", "blocking"] } +#isahc = { version = "1.7.2", features = [] } +serde = { version = "*", features = ["derive"] } +serde_json = { version = "*" } +chrono = { version = "*", features = ["serde"] } +thiserror = { version = "*" } +log = { version = "*" } +derive_more = { version = "*" } + +[dev-dependencies] +tokio = { version = "1.17.0", features = ["full"] } +dotenv = { version = "0.15.0" } diff --git a/vendor/pay_u/src/credit.rs b/vendor/pay_u/src/credit.rs new file mode 100644 index 0000000..3fa8b7c --- /dev/null +++ b/vendor/pay_u/src/credit.rs @@ -0,0 +1,77 @@ +//! This module allow to create credit request during create order request + +use serde::{Deserialize, Serialize}; + +use crate::{Address, ShoppingCart}; + +/// Describe customer during credit request +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Applicant { + /// Applicant’s email address + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + email: Option, + /// Applicant’s phone number + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + phone: Option, + /// Applicant’s first name + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + first_name: Option, + /// Applicant’s last name + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + last_name: Option, + /// 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, + /// National Identification Number + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + nin: Option, + /// Section containing data about applicant’s address. + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + address: Option
, + /// Additional information about person applying for credit. + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + additional_info: Option, +} + +/// 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>, + /// Section containing data of person applying for a credit + #[serde(skip_serializing_if = "Option::is_none")] + applicant: Option, +} + +impl Credit { + pub fn with_shopping_carts(mut self, shopping_carts: ShoppingCarts) -> Self + where + ShoppingCarts: Iterator, + { + 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, +} diff --git a/vendor/pay_u/src/deserialize.rs b/vendor/pay_u/src/deserialize.rs new file mode 100644 index 0000000..9aa1736 --- /dev/null +++ b/vendor/pay_u/src/deserialize.rs @@ -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 +where + D: serde::Deserializer<'de>, +{ + d.deserialize_string(I32Visitor) +} + +pub(crate) fn deserialize_i32_newtype<'de, N: From, D>( + d: D, +) -> std::result::Result +where + D: serde::Deserializer<'de>, +{ + d.deserialize_string(I32Visitor).map(N::from) +} + +pub(crate) fn deserialize_u32<'de, D>(d: D) -> std::result::Result +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(self, value: i8) -> Result + where + E: de::Error, + { + Ok(i32::from(value)) + } + + fn visit_i32(self, value: i32) -> Result + where + E: de::Error, + { + Ok(value) + } + + fn visit_i64(self, value: i64) -> Result + 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(self, v: &str) -> Result + where + E: Error, + { + if let Ok(value) = v.parse::() { + Ok(value) + } else { + Err(E::custom(format!("str does not contains valid i32: {}", v))) + } + } + + fn visit_string(self, v: String) -> Result + where + E: Error, + { + if let Ok(value) = v.parse::() { + 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(self, value: u8) -> Result + where + E: de::Error, + { + Ok(u32::from(value)) + } + + fn visit_u32(self, value: u32) -> Result + where + E: de::Error, + { + Ok(value) + } + + fn visit_u64(self, value: u64) -> Result + 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(self, v: &str) -> Result + where + E: Error, + { + if let Ok(value) = v.parse::() { + Ok(value) + } else { + Err(E::custom(format!("str does not contains valid i32: {}", v))) + } + } + + fn visit_string(self, v: String) -> Result + where + E: Error, + { + if let Ok(value) = v.parse::() { + 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 +} diff --git a/vendor/pay_u/src/lib.rs b/vendor/pay_u/src/lib.rs new file mode 100644 index 0000000..8e4a74f --- /dev/null +++ b/vendor/pay_u/src/lib.rs @@ -0,0 +1,1475 @@ +//! This is unofficial client support integration with the REST API 2.1 +//! protocol. It presents various methods of implementing online payments via +//! different PayU services and is dedicated primarily to developers wanting to +//! implement the PayU payment services. + +pub mod credit; +mod deserialize; +pub mod notify; +pub mod req; +pub mod res; +mod serialize; + +use std::sync::Arc; + +use reqwest::redirect; +use serde::{Deserialize, Serialize}; + +use crate::res::OrdersInfo; + +macro_rules! get_client { + ($self:expr) => {{ + #[cfg(feature = "single-client")] + { + $self.client.clone() + } + #[cfg(not(feature = "single-client"))] + { + Client::build_client() + } + }}; +} + +pub static SUCCESS: &str = "SUCCESS"; + +#[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("{0}")] + Reqwest(#[from] reqwest::Error), + #[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, + code: Option, + severity: Option, + code_literal: Option, + }, + #[error("PayU rejected to perform refund with status {status_code:?}")] + RefundFailed { + status_code: String, + status_desc: Option, + code: Option, + severity: Option, + code_literal: Option, + }, + #[error("PayU rejected order details request with status {status_code:?}")] + OrderDetailsFailed { + status_code: String, + status_desc: Option, + code: Option, + severity: Option, + code_literal: Option, + }, + #[error("PayU rejected order transactions details request with status {status_code:?}")] + OrderTransactionsFailed { + status_code: String, + status_desc: Option, + code: Option, + severity: Option, + code_literal: Option, + }, + #[error("PayU returned order details but without any order")] + NoOrderInDetails, +} + +pub type Result = std::result::Result; + +/// PayU internal order id +/// +/// Unique order identifier +#[derive( + Debug, + Clone, + serde::Deserialize, + serde::Serialize, + derive_more::Display, + derive_more::From, + derive_more::Deref, +)] +#[serde(transparent)] +pub struct OrderId(pub String); + +impl OrderId { + pub fn new>(id: S) -> Self { + Self(id.into()) + } +} + +/// PayU internal merchant id +/// +/// This value is customer identifier +#[derive( + Debug, + serde::Deserialize, + serde::Serialize, + Copy, + Clone, + derive_more::Display, + derive_more::From, + derive_more::Deref, + derive_more::Constructor, +)] +#[serde(transparent)] +pub struct MerchantPosId(pub i32); + +/// Public payu OAuth client identifier +#[derive( + Debug, + Clone, + serde::Deserialize, + serde::Serialize, + derive_more::Display, + derive_more::From, + derive_more::Deref, +)] +#[serde(transparent)] +pub struct ClientId(pub String); + +impl ClientId { + pub fn new>(id: S) -> Self { + Self(id.into()) + } +} + +/// Secret payu OAuth client identifier +#[derive( + Debug, + Clone, + serde::Deserialize, + serde::Serialize, + derive_more::Display, + derive_more::From, + derive_more::Deref, +)] +#[serde(transparent)] +pub struct ClientSecret(pub String); + +impl ClientSecret { + pub fn new>(id: S) -> Self { + Self(id.into()) + } +} + +/// PayU payment status. +/// +/// Each payment is initially Pending and can change according to following +/// graph: +/// +/// +#[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, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Postal box number + postal_box: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Postal code + postal_code: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// City + city: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Province + state: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Two-letter country code compliant with ISO-3166. + country_code: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Address description + name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Recipient’s name + recipient_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Recipient’s e-mail address + recipient_email: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Recipient’s phone number + recipient_phone: Option, +} + +#[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, +} + +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, + /// Required customer phone number + #[serde(skip_serializing_if = "Option::is_none")] + phone: Option, + /// Required customer first name + #[serde(skip_serializing_if = "Option::is_none")] + first_name: Option, + /// Required customer last name + #[serde(skip_serializing_if = "Option::is_none")] + last_name: Option, + /// Required customer language + #[serde(skip_serializing_if = "Option::is_none")] + language: Option, + #[serde(skip_serializing_if = "Option::is_none")] + delivery: Option, +} + +impl Buyer { + pub fn new( + email: Email, + phone: Phone, + first_name: FirstName, + last_name: LastName, + lang: Language, + ) -> Self + where + Email: Into, + Phone: Into, + FirstName: Into, + LastName: Into, + Language: Into, + { + 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(mut self, email: S) -> Self + where + S: Into, + { + self.email = Some(email.into()); + self + } + pub fn phone(&self) -> &str { + self.phone.as_deref().unwrap_or_default() + } + pub fn with_phone(mut self, phone: S) -> Self + where + S: Into, + { + 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(mut self, first_name: S) -> Self + where + S: Into, + { + 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(mut self, last_name: S) -> Self + where + S: Into, + { + self.last_name = Some(last_name.into()); + self + } + pub fn language(&self) -> &str { + self.language.as_deref().unwrap_or_default() + } + pub fn with_language(mut self, language: S) -> Self + where + S: Into, + { + 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, + /// 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, +} + +impl Product { + pub fn new>(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, + /// Section containing data about ordered products. + /// > Note: product objects in the section do not + /// > have a listingDate field + #[serde(skip_serializing_if = "Option::is_none")] + products: Option>, + /// 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(ext_customer_id: ExtCustomerId) -> Self + where + ExtCustomerId: Into, + { + Self { + shopping_method: None, + ext_customer_id: ext_customer_id.into(), + products: None, + } + } + + pub fn new_with_products( + ext_customer_id: ExtCustomerId, + products: Products, + ) -> Self + where + ExtCustomerId: Into, + Products: Iterator, + { + Self { + shopping_method: None, + ext_customer_id: ext_customer_id.into(), + products: Some(products.map(Product::erase_listing_date).collect()), + } + } + + pub fn with_products(mut self, products: Products) -> Self + where + Products: Iterator, + { + self.products = Some(products.map(Product::erase_listing_date).collect()); + self + } + + /// Section containing data of shipping method. + pub fn shopping_method(&self) -> &Option { + &self.shopping_method + } + + /// Section containing data about ordered products. + /// > Note: product objects in the section do not + /// > have a listingDate field + pub fn products(&self) -> &Option> { + &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, + /// Street name, possibly including house and flat number. + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub street: Option, + /// Street number + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub street_no: Option, + /// Flat number + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub flat_no: Option, + /// Postal code + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub postal_code: Option, + /// City + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, + /// Two-letter country code compliant with ISO-3166 + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub country_code: Option, +} + +#[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, + /// Shipping cost + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub price: Option, + /// Section containing data about shipping address. + /// Recommended + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option
, +} + +/// MultiUseCartToken +pub mod muct { + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + pub enum CardOnFile { + /// Payment initialized by the card owner who agreed to save card for + /// future use. You can expect full authentication (3D Secure + /// and/or CVV). If you want to use multi-use token (TOKC_) + /// later, you have to be confident, that first payment was + /// successful. Default value for single-use token (TOK_). + /// + /// In case of plain card data payments you should retrieve transaction + /// data to obtain first TransactionId. It should be passed in + /// payMethods.payMethod.card section for transactions marked as + /// STANDARD, STANDARD_CARDHOLDER and STANDARD_MERCHANT; + /// STANDARD_CARDHOLDER - payment with already saved card, + /// initialized by the card owner. This transaction has + /// multi-use token (TOKC_). Depending of payment parameters + /// (e.g. high transaction amount) strong authentication can be + /// expected (3D Secure and/or CVV). Default value for multi-use token + /// (TOKC_); + First, + /// Payment with already saved card, initialized by the shop without the + /// card owner participation. This transaction has multi-use token + /// (TOKC_). By the definition, this payment type does not + /// require strong authentication. You cannot use it if FIRST + /// card-on-file payment failed. + StandardMerchant, + } + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + pub enum Recurring { + /// Payment initialized by the card owner who agreed to save card for + /// future use in recurring plan. You can expect full authentication (3D + /// Secure and/or CVV). If you want to use multi-use token (TOKC_) + /// later, you have to be confident, that first recurring + /// payment was successful. + First, + /// Subsequent recurring payment (user is not present). This transaction + /// has multi use token (TOKC_). You cannot use it if FIRST recurring + /// payment failed. + Standard, + } + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct MultiUseCartToken { + /// Information about party initializing order: + /// + /// * `FIRST` - payment initialized by the card owner who agreed to save + /// card for future use. You can expect full authentication (3D Secure + /// and/or CVV). If you want to use multi-use token (TOKC_) later, you + /// have to be confident, that first payment was successful. Default + /// value for single-use token (TOK_). + /// + /// In case of plain card data payments you should retrieve + /// transaction data to obtain first TransactionId. It should + /// be passed in payMethods.payMethod.card section for + /// transactions marked as STANDARD, STANDARD_CARDHOLDER and + /// STANDARD_MERCHANT; STANDARD_CARDHOLDER - payment with + /// already saved card, initialized by the card owner. This + /// transaction has multi-use token (TOKC_). Depending of payment + /// parameters (e.g. high transaction amount) strong authentication + /// can be expected (3D Secure and/or CVV). Default value for + /// multi-use token (TOKC_); + /// * `STANDARD_MERCHANT` - payment with already saved card, initialized + /// by the shop without the card owner participation. This transaction + /// has multi-use token (TOKC_). By the definition, this payment type + /// does not require strong authentication. You cannot use it if FIRST + /// card-on-file payment failed. + /// + /// `cardOnFile` parameter cannot be used with recurring parameter. + pub card_on_file: CardOnFile, + /// Marks the order as recurring payment. + /// + /// * `FIRST` - payment initialized by the card owner who agreed to save + /// card for future use in recurring plan. You can expect full + /// authentication (3D Secure and/or CVV). If you want to use + /// multi-use token (TOKC_) later, you have to be confident, that + /// first recurring payment was successful. + /// * `STANDARD` - subsequent recurring payment (user is not present). + /// This transaction has multi use token (TOKC_). You cannot use it if + /// FIRST recurring payment failed. + /// + /// `recurring` parameter cannot be used with cardOnFile parameter. + pub recurring: Recurring, + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PaymentType { + Pbl, + CardToken, + Installments, +} + +/// Wrapper around pay method. It's used only for deserializing notifications +/// +/// # Examples +/// +/// ``` +/// # use pay_u::PayMethod; +/// let method: PayMethod = serde_json::from_str(r#" +/// { +/// "type": "INSTALLMENTS" +/// } +/// "#).unwrap(); +/// ``` +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PayMethod { + #[serde(rename = "type")] + pub payment_type: PaymentType, +} + +#[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, + code: Option, + severity: Option, + code_literal: Option, +} + +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() + } +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum StatusCode { + ErrorValueMissing, + OpenpayuBusinessError, + OpenpayuErrorValueInvalid, + OpenpayuErrorInternal, +} + +#[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, +} + +#[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, + pub amount: String, + pub currency_code: String, + pub description: String, + pub creation_date_time: String, + pub status: String, + pub status_date_time: String, +} + +pub struct Client { + sandbox: bool, + merchant_pos_id: MerchantPosId, + client_id: ClientId, + client_secret: ClientSecret, + bearer: Option, + bearer_expires_at: chrono::DateTime, + #[cfg(feature = "single-client")] + client: Arc, +} + +impl Client { + /// Create new PayU client + pub fn new( + client_id: ClientId, + client_secret: ClientSecret, + merchant_pos_id: MerchantPosId, + ) -> Self { + #[cfg(feature = "single-client")] + { + Self { + bearer: None, + sandbox: false, + merchant_pos_id, + client_id, + client_secret, + bearer_expires_at: chrono::Utc::now(), + client: Arc::new(Self::build_client()), + } + } + #[cfg(not(feature = "single-client"))] + { + Self { + bearer: None, + sandbox: false, + merchant_pos_id, + client_id, + client_secret, + bearer_expires_at: chrono::Utc::now(), + } + } + } + + /// All operation will be performed in sandbox PayU environment + /// + /// Examples: + /// + /// ``` + /// # use pay_u::*; + /// + /// async fn test() { + /// let client_id = ClientId::new(std::env::var("PAYU_CLIENT_ID").unwrap()); + /// let client_secret = ClientSecret::new(std::env::var("PAYU_CLIENT_SECRET").unwrap()); + /// let merchant_id = std::env::var("PAYU_CLIENT_MERCHANT_ID").unwrap().parse::().map(MerchantPosId::from).unwrap(); + /// let mut client = Client::new(client_id, client_secret, merchant_id).sandbox(); + /// } + /// ``` + pub fn sandbox(mut self) -> Self { + self.sandbox = true; + self + } + + /// Set your own bearer key + /// + /// Examples: + /// + /// ``` + /// # use pay_u::*; + /// + /// async fn test() { + /// let client_id = ClientId::new(std::env::var("PAYU_CLIENT_ID").unwrap()); + /// let client_secret = ClientSecret::new(std::env::var("PAYU_CLIENT_SECRET").unwrap()); + /// let merchant_id = std::env::var("PAYU_CLIENT_MERCHANT_ID").unwrap().parse::().map(MerchantPosId::from).unwrap(); + /// let mut client = Client::new(client_id, client_secret, merchant_id).with_bearer("a89sdhas9d8yasd8", 9_999_999); + /// } + /// ``` + pub fn with_bearer>(mut self, bearer: Bearer, expires_in: i64) -> Self { + self.bearer = Some(bearer.into()); + self.bearer_expires_at = chrono::Utc::now() + chrono::Duration::seconds(expires_in); + self + } + + /// Create new order in PayU + /// + /// ### IMPORTANT: + /// Do not follow redirect for any reason. Location points to + /// payment page which is useless in this context + /// + /// ### IMPORTANT: + /// Do not use rustls. It'll freeze application! + /// + /// # Examples + /// + /// ```rust + /// # use pay_u::*; + /// async fn pay() { + /// let mut client = Client::new(ClientId::new("145227"), ClientSecret::new("12f071174cb7eb79d4aac5bc2f07563f"), MerchantPosId::new(300746)) + /// .sandbox() + /// .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 2000); + /// let res = client + /// .create_order( + /// req::OrderCreate::build( + /// Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"), + /// "127.0.0.1", + /// "PLN", + /// "Some description" + /// ).expect("All fields must be valid") + /// .with_notify_url("https://your.eshop.com/notify") + /// .with_description("Replace description") + /// .with_products([ + /// Product::new("Wireless Mouse for Laptop", 15000, 1), + /// Product::new("HDMI cable", 6000, 1), + /// ].into_iter()), + /// ) + /// .await; + /// } + /// ``` + pub async fn create_order(&mut self, order: req::OrderCreate) -> Result { + self.authorize().await?; + if order.total_amount + != order + .products + .iter() + .fold(0, |memo, p| memo + (p.unit_price * p.quantity as i32)) + { + return Err(Error::IncorrectTotal); + } + + if order.buyer().is_none() { + return Err(Error::NoBuyer); + } + + if order.description().trim().is_empty() { + return Err(Error::NoDescription); + } + + let bearer = self.bearer.as_ref().cloned().unwrap_or_default(); + let path = format!("{}/orders", self.base_url()); + let client = get_client!(self); + let text = client + .post(path) + .bearer_auth(bearer) + .json(&order.with_merchant_pos_id(self.merchant_pos_id)) + .send() + .await? + .text() + .await?; + log::trace!("Response: {}", text); + let res: res::CreateOrder = serde_json::from_str(&text).map_err(|e| { + log::error!("{e:?}"); + Error::CreateOrder + })?; + if !res.status.is_success() { + let Status { + status_code, + status_desc, + code, + severity, + code_literal, + } = res.status; + return Err(Error::CreateFailed { + status_desc, + status_code, + code, + severity, + code_literal, + }); + } + Ok(res) + } + + /// The PayU system fully supports refunds for processed payments, the + /// balance of which is transferred directly to the buyer’s account. + /// + /// > For „PayU | Pay later” payment method funds are transferred to a + /// > credit provider. + /// + /// You can process refund requests as either full or partial. For partial + /// refunds, always specify the amount in the lowest unit of a given + /// currency, which must be the same currency as the initial order (np. for + /// Poland lowest currency unit is “grosz” so 10 pln should be given as + /// 1000). + /// + /// You can send several partial refund requests for each single order. The + /// total value of the requests must not exceed the order value. + /// + /// The payu system allows multiple partial refunds to be executed at the + /// same time. To do so, the extRefundId parameter must be sent in the + /// request. In situations where partial refunds will not be executed more + /// than once per second, the extRefundId parameter is not required. + /// + /// # Examples + /// + /// ``` + /// # use pay_u::*; + /// async fn partial_refund() { + /// let mut client = Client::new(ClientId::new("145227"), ClientSecret::new("12f071174cb7eb79d4aac5bc2f07563f"), MerchantPosId::new(300746)) + /// .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 2000) + /// .sandbox(); + /// let res = client + /// .refund( + /// OrderId::new("H9LL64F37H160126GUEST000P01"), + /// req::Refund::new("Refund", Some(1000)), + /// ) + /// .await; + /// } + /// async fn full_refund() { + /// let mut client = Client::new(ClientId::new("145227"), ClientSecret::new("12f071174cb7eb79d4aac5bc2f07563f"), MerchantPosId::new(300746)) + /// .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 2000) + /// .sandbox(); + /// let res = client + /// .refund( + /// OrderId::new("H9LL64F37H160126GUEST000P01"), + /// req::Refund::new("Refund", None), + /// ) + /// .await; + /// } + /// ``` + pub async fn refund( + &mut self, + order_id: OrderId, + refund: req::Refund, + ) -> Result { + #[derive(Serialize, Debug)] + #[serde(rename_all = "camelCase")] + struct RefundWrapper { + refund: req::Refund, + } + + self.authorize().await?; + if refund.description().trim().is_empty() { + return Err(Error::NoDescription); + } + + let bearer = self.bearer.as_ref().cloned().unwrap_or_default(); + let path = format!("{}/orders/{}/refunds", self.base_url(), order_id); + let client = get_client!(self); + let text = client + .post(path) + .bearer_auth(bearer) + .json(&RefundWrapper { refund }) + .send() + .await? + .text() + .await?; + log::trace!("Response: {}", text); + let res: res::RefundDetails = serde_json::from_str(&text).map_err(|e| { + log::error!("Invalid PayU response {e:?}"); + Error::Refund + })?; + if !res.status.is_success() { + let Status { + status_code, + status_desc, + code, + severity, + code_literal, + } = res.status; + return Err(Error::RefundFailed { + status_desc, + status_code, + code, + severity, + code_literal, + }); + } + Ok(res) + } + + /// Order details request. You may use it to completely remove `Order` + /// persistence and use extOrderId to connect your data with PayU data. + /// + /// # Examples + /// + /// ``` + /// # use pay_u::*; + /// async fn order_details() { + /// let mut client = Client::new(ClientId::new("145227"), ClientSecret::new("12f071174cb7eb79d4aac5bc2f07563f"), MerchantPosId::new(300746)) + /// .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 2000) + /// .sandbox(); + /// let res = client + /// .order_details(OrderId::new("H9LL64F37H160126GUEST000P01")) + /// .await; + /// } + /// ``` + pub async fn order_details(&mut self, order_id: OrderId) -> Result { + self.authorize().await?; + let bearer = self.bearer.as_ref().cloned().unwrap_or_default(); + let path = format!("{}/orders/{}", self.base_url(), order_id); + let client = get_client!(self); + let text = client + .get(path) + .bearer_auth(bearer) + .send() + .await? + .text() + .await?; + log::trace!("Response: {}", text); + dbg!(&text); + let mut res: OrdersInfo = serde_json::from_str(&text).map_err(|e| { + log::error!("{e:?}"); + dbg!(e); + Error::OrderDetails + })?; + if !res.status.is_success() { + let Status { + status_code, + status_desc, + code, + severity, + code_literal, + } = res.status; + return Err(Error::OrderDetailsFailed { + status_code, + status_desc, + code, + severity, + code_literal, + }); + } + Ok(res::OrderInfo { + order: if res.orders.is_empty() { + return Err(Error::NoOrderInDetails); + } else { + res.orders.remove(0) + }, + status: res.status, + properties: res.properties, + }) + } + + /// The transaction retrieve request message enables you to retrieve the + /// details of transactions created for an order. + /// + /// Using this endpoint is extremely useful if you would like to get bank + /// account details or card details. + /// + /// > Please note that although card details are available right after + /// > transaction has been processed, the bank details may be available + /// > either after few minutes or on the next business day, depending on the + /// > bank. + /// + /// # Examples + /// + /// ``` + /// # use pay_u::*; + /// async fn order_transactions() { + /// let mut client = Client::new(ClientId::new("145227"), ClientSecret::new("12f071174cb7eb79d4aac5bc2f07563f"), MerchantPosId::new(300746)) + /// .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 2000) + /// .sandbox(); + /// let res = client + /// .order_transactions(OrderId::new("H9LL64F37H160126GUEST000P01")) + /// .await; + /// } + /// ``` + pub async fn order_transactions(&mut self, order_id: OrderId) -> Result { + self.authorize().await?; + let bearer = self.bearer.as_ref().cloned().unwrap_or_default(); + let path = format!("{}/orders/{}/transactions", self.base_url(), order_id); + let client = get_client!(self); + let text = client + .get(path) + .bearer_auth(bearer) + .send() + .await? + .text() + .await?; + log::trace!("Response: {}", text); + dbg!(&text); + serde_json::from_str(&text).map_err(|e| { + log::error!("{e:?}"); + dbg!(e); + Error::OrderTransactions + }) + } + + /// The transaction retrieve request message enables you to retrieve the + /// details of transactions created for an order. + /// + /// Using this endpoint is extremely useful if you would like to get bank + /// account details or card details. + /// + /// > Please note that although card details are available right after + /// > transaction has been processed, the bank details may be available + /// > either after few minutes or on the next business day, depending on the + /// > bank. + /// + /// # Examples + /// + /// ``` + /// # use pay_u::*; + /// async fn order_transactions() { + /// let mut client = Client::new(ClientId::new("145227"), ClientSecret::new("12f071174cb7eb79d4aac5bc2f07563f"), MerchantPosId::new(300746)) + /// .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 2000) + /// .sandbox(); + /// let res = client + /// .order_transactions(OrderId::new("H9LL64F37H160126GUEST000P01")) + /// .await; + /// } + /// ``` + pub async fn order_refunds(&mut self, order_id: OrderId) -> Result> { + self.authorize().await?; + let bearer = self.bearer.as_ref().cloned().unwrap_or_default(); + let path = format!("{}/orders/{}/refunds", self.base_url(), order_id); + let client = get_client!(self); + let text = client + .get(path) + .bearer_auth(bearer) + .send() + .await? + .text() + .await?; + log::trace!("Response: {}", text); + let res::Refunds { refunds, .. } = serde_json::from_str(&text).map_err(|e| { + log::error!("{e:?}"); + Error::OrderRefunds + })?; + Ok(refunds) + } + + /// Get or refresh token + pub async fn authorize(&mut self) -> Result { + use chrono::{Duration, Utc}; + if Utc::now() - Duration::seconds(1) < self.bearer_expires_at { + return Ok(true); + } + #[derive(Deserialize)] + struct BearerResult { + access_token: String, + expires_in: i64, + } + + let client = get_client!(self); + let res = client.post(&format!( + "https://secure.payu.com/pl/standard/user/oauth/authorize?grant_type=client_credentials&client_id={}&client_secret={}", + self.client_id, + self.client_secret + )) + .send() + .await?; + let res = res.json::().await.map_err(|e| { + log::error!("{e}"); + Error::Unauthorized + })?; + log::trace!("Bearer is {}", res.access_token); + self.bearer_expires_at = Utc::now() + Duration::seconds(res.expires_in); + self.bearer = Some(res.access_token); + Ok(true) + } + + fn base_url(&self) -> &str { + if self.sandbox { + "https://secure.snd.payu.com/api/v2_1" + } else { + "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)] +mod tests { + use super::*; + use crate::res::CreateOrder; + + fn build_client() -> Client { + dotenv::dotenv().ok(); + Client::new( + ClientId::new("145227"), + ClientSecret::new("12f071174cb7eb79d4aac5bc2f07563f"), + MerchantPosId::new(300746), + ) + .sandbox() + .with_bearer("d9a4536e-62ba-4f60-8017-6053211d3f47", 999999) + } + + async fn perform_create_order(client: &mut Client) -> Result { + client + .create_order( + req::OrderCreate::build( + Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"), + "127.0.0.1", + "PLN", + "Desc", + )? + .with_notify_url("https://your.eshop.com/notify") + .with_description("RTV market") + .with_products( + [ + Product::new("Wireless Mouse for Laptop", 15000, 1), + Product::new("HDMI cable", 6000, 1), + ] + .into_iter(), + ), + ) + .await + } + + #[tokio::test] + async fn create_order() { + let mut client = build_client(); + let res = perform_create_order(&mut client).await; + + if res.is_err() { + eprintln!("create_order res is {res:?}"); + } + assert!(res.is_ok()); + } + + #[tokio::test] + async fn partial_refund() { + let mut client = build_client(); + let CreateOrder { order_id, .. } = perform_create_order(&mut client) + .await + .expect("Failed to create"); + let res = client + .refund(order_id, req::Refund::new("Refund", Some(10))) + .await; + + if res.is_err() { + eprintln!("partial_refund res is {res:?}"); + } + assert!(matches!(res, Err(Error::RefundFailed { .. }))); + } + + #[tokio::test] + async fn full_refund() { + let mut client = build_client(); + let CreateOrder { order_id, .. } = perform_create_order(&mut client) + .await + .expect("Failed to create"); + let res = client + .refund(order_id, req::Refund::new("Refund", None)) + .await; + + if res.is_err() { + eprintln!("full_refund res is {res:?}"); + } + assert!(matches!(res, Err(Error::RefundFailed { .. }))); + } + + #[tokio::test] + async fn order_details() { + let mut client = build_client(); + let CreateOrder { order_id, .. } = perform_create_order(&mut client) + .await + .expect("Failed to create"); + let res = client.order_details(order_id).await; + + if res.is_err() { + eprintln!("order_details res is {res:?}"); + } + assert!(matches!(res, Ok(res::OrderInfo { .. }))); + } + + #[tokio::test] + async fn order_transactions() { + let mut client = build_client(); + let CreateOrder { order_id, .. } = perform_create_order(&mut client) + .await + .expect("Failed to create"); + let res = client.order_transactions(order_id).await; + if res.is_err() { + eprintln!("order_transactions res is {res:?}"); + } + assert!(matches!(res, Err(Error::OrderTransactions))); + } + + #[test] + fn check_accepted_refund_json() { + let res = serde_json::from_str::(include_str!( + "../tests/responses/accepted_refund.json" + )); + assert!(res.is_ok()); + } + #[test] + fn check_cancel_json() { + let res = serde_json::from_str::(include_str!( + "../tests/responses/cancel.json" + )); + assert!(res.is_ok()); + } + #[test] + fn check_completed_cart_token_json() { + let res = serde_json::from_str::(include_str!( + "../tests/responses/completed_cart_token.json" + )); + assert!(res.is_ok()); + } + #[test] + fn check_completed_installments_json() { + let res = serde_json::from_str::(include_str!( + "../tests/responses/completed_installments.json" + )); + assert!(res.is_ok()); + } + #[test] + fn check_completed_pbl_json() { + let res = serde_json::from_str::(include_str!( + "../tests/responses/completed_pbl.json" + )); + assert!(res.is_ok()); + } + #[test] + fn check_refund_json() { + let res = serde_json::from_str::(include_str!( + "../tests/responses/refund.json" + )); + assert!(res.is_ok()); + } + #[test] + fn check_rejection_json() { + let res = serde_json::from_str::(include_str!( + "../tests/responses/rejection.json" + )); + assert!(res.is_ok()); + } + #[test] + fn check_custom_literal_json() { + let res = serde_json::from_str::(include_str!( + "../tests/responses/custom_code_literal.json" + )); + assert!(res.is_ok()); + } +} diff --git a/vendor/pay_u/src/notify.rs b/vendor/pay_u/src/notify.rs new file mode 100644 index 0000000..cc118aa --- /dev/null +++ b/vendor/pay_u/src/notify.rs @@ -0,0 +1,161 @@ +//! Notification objects. Those objects will be send on notify_url if was given. +//! +//! To enable notifications for a specific payment, specify the notifyUrl +//! parameter in the payment form. Each payment can receive a different URL to +//! which notifications will be sent. +//! +//! Every notification is sent asynchronously. After your system receives a +//! notification with the status COMPLETED, instruct it to ignore any further +//! notifications. +//! +//! After sending a notification, PayU system requires a 200 HTTP status code in +//! response. If it receives a different status code, it will resend the +//! notification. Your system should also take account of situations where a +//! notification is sent several times with the same status. For each repeated +//! notification, response with code 200 should be sent as well. +//! +//! To ensure trusted communication between PayU and your shop, you must verify +//! the signature value available in the OpenPayu-Signature header each time +//! your system receives any notification from a PayU server. Refer to the +//! Verification of notifications signature for more information. +//! +//! Notifications are sent for orders in the following statuses: PENDING, +//! WAITING_FOR_CONFIRMATION, COMPLETED, CANCELED. +//! +//! Note: if you filter IP addresses, remember to allow IPs used by PayU to send +//! the notifications. These are: +//! +//! ### PRODUCTION +//! +//! > 185.68.12.10, 185.68.12.11, 185.68.12.12, 185.68.12.26, 185.68.12.27, +//! > 185.68.12.28 +//! +//! ### SANDBOX +//! +//! > 185.68.14.10, 185.68.14.11, 185.68.14.12, 185.68.14.26, 185.68.14.27, +//! > 185.68.14.28 +//! +//! ## Payment lifecycle +//! +//! You can configure a separate auto-receive / automatic collection parameter +//! for each payment method via the Management Panel. +//! +//! By default, auto-receive is enabled. The basic payment sequence is as +//! follows: +//! +//! 1) Each successfully authorized payment for an order is captured. +//! 2) The buyer is charged with the order amount. +//! 3) The shop balance is increased by the order amount. +//! 4) PayU calculates its commission to the order. +//! +//! If the auto-receive is turned off, you should capture each order using a PUT +//! method (Order capture) or cancel using DELETE method (Cancellation). +//! +//! If no such action is taken the order is auto-canceled. Automatic +//! cancellation occurs after a number of days indicated for the payment method. +//! +//! + +use serde::Deserialize; + +use super::deserialize; +use crate::OrderId; + +/// Payment notification object received on notify_url +/// +/// See [crate::req::CreateOrder::notify_url] +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct StatusUpdate { + /// Section containing order object + pub order: Order, + /// Moment of accepting the transaction and adding funds from the + /// transaction to the Shop balance. Format: "%Y-%M-%DT%h:%m:%s%z." + /// Example: "2020-06-09T17:52:04.644+02:00". If the millisecond counter + /// is "000" then milliseconds are not sent and the format changes to: + /// "%Y-%M-%DT%h:%m:%s". Is present only for the status "COMPLETED". + pub local_receipt_date_time: Option, + /// Array of objects related to transaction identification. In case of + /// statuses: + /// * `"WAITING_FOR_CONFIRMATION"` and `"COMPLETED"` - Contains one element + /// with two parameters: name and value, + /// * `"PENDING"` - may contain object with aforementioned parameters or it + /// can be empty. + /// + /// Also properties `name` + /// + /// Static value. The payment identifier, displayed on transaction + /// statements as "Trans ID" and within the transaction search option in + /// the Management Panel. + /// + /// Also properties `value` + /// + /// Transaction ID in PayU system (data type - string). + pub properties: Option>, +} + +impl StatusUpdate { + pub fn status(&self) -> super::PaymentStatus { + self.order.status + } + + /// See [crate::req::OrderCreate::ext_order_id] + pub fn ext_order_id(&self) -> &str { + self.order.ext_order_id.as_deref().unwrap_or_default() + } + + pub fn has_ext_order_id(&self) -> bool { + self.order.ext_order_id.is_some() + } +} + +/// Refund notification object +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RefundUpdate { + pub ext_order_id: String, + pub order_id: OrderId, + pub refund: Refund, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Refund { + pub refund_id: String, + pub amount: String, + pub currency_code: String, + pub status: super::RefundStatus, + pub status_date_time: String, + pub reason: String, + pub reason_description: String, + pub refund_date: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Order { + pub notify_url: Option, + /// Customer client IP address + pub customer_ip: String, + /// Secret pos ip. This is connected to PayU account + #[serde(deserialize_with = "deserialize::deserialize_i32_newtype")] + pub merchant_pos_id: super::MerchantPosId, + /// 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(deserialize_with = "deserialize::deserialize_i32")] + pub total_amount: super::Price, + /// @see [crate::Buyer] + pub buyer: Option, + /// List of products + pub products: Vec, + #[serde(skip_serializing)] + pub order_create_date: Option, + pub pay_method: Option, + pub status: super::PaymentStatus, + pub ext_order_id: Option, +} diff --git a/vendor/pay_u/src/req.rs b/vendor/pay_u/src/req.rs new file mode 100644 index 0000000..d05e05f --- /dev/null +++ b/vendor/pay_u/src/req.rs @@ -0,0 +1,381 @@ +//! Objects used to send requests to PayU + +use serde::{Deserialize, Serialize}; + +use crate::credit::Credit; + +#[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, +} + +impl Refund { + pub fn new(description: Description, amount: Option) -> Self + where + Description: Into, + { + Self { + description: description.into(), + amount, + } + } + + pub fn description(&self) -> &str { + &self.description + } + + pub fn amount(&self) -> Option { + 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, + /// 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, + /// 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: + /// + /// + /// 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, + /// Payer’s 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: super::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: super::Price, + /// @see [crate::Buyer] + pub(crate) buyer: Option, + /// List of products + pub(crate) products: Vec, + #[serde(skip_serializing)] + pub(crate) order_create_date: Option, + /// 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, + /// Additional description of the order. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) additional_description: Option, + /// Text visible on the PayU payment page (max. 80 chars). + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) visible_description: Option, + /// 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, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub(crate) muct: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) credit: Option, +} + +impl OrderCreate { + pub fn build( + buyer: super::Buyer, + customer_ip: CustomerIp, + currency: Currency, + description: Description, + ) -> super::Result + where + CustomerIp: Into, + Currency: Into, + Description: Into, + { + let customer_ip = customer_ip.into(); + if &customer_ip == "0.0.0.0" { + return Err(super::Error::CustomerIp); + } + Ok(Self { + ext_order_id: None, + notify_url: None, + continue_url: None, + customer_ip, + merchant_pos_id: 0.into(), + 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>(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: super::muct::Recurring, + card_on_file: super::muct::CardOnFile, + ) -> Self { + self.muct = Some(super::muct::MultiUseCartToken { + recurring, + card_on_file, + }); + self + } + + pub fn with_products(mut self, products: Products) -> Self + where + Products: Iterator, + { + 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: super::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(mut self, desc: Description) -> Self + where + Description: Into, + { + self.description = String::from(desc.into().trim()); + self + } + + /// Additional description of the order. + pub fn with_additional_description>( + 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(mut self, desc: Description) -> Self + where + Description: Into, + { + 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(mut self, notify_url: NotifyUrl) -> Self + where + NotifyUrl: Into, + { + 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(mut self, continue_url: ContinueUrl) -> Self + where + ContinueUrl: Into, + { + 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 { + &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) -> super::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) -> &super::Price { + &self.total_amount + } + + pub fn buyer(&self) -> &Option { + &self.buyer + } + + pub fn products(&self) -> &[super::Product] { + &self.products + } + + pub fn order_create_date(&self) -> &Option { + &self.order_create_date + } + + pub(crate) fn with_merchant_pos_id(mut self, merchant_pos_id: super::MerchantPosId) -> Self { + self.merchant_pos_id = merchant_pos_id; + self + } +} diff --git a/vendor/pay_u/src/res.rs b/vendor/pay_u/src/res.rs new file mode 100644 index 0000000..1fcf1d2 --- /dev/null +++ b/vendor/pay_u/src/res.rs @@ -0,0 +1,144 @@ +use crate::{OrderId, Refund, Status}; + +#[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, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RefundDetails { + pub order_id: Option, + pub refund: Option, + pub status: Status, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Refunds { + pub refunds: Vec, +} + +#[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, +} + +#[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, + /// Example: "2014-10-27T14:58:17.443+01:00", + pub order_create_date: String, + /// Example: "http://localhost/OrderNotify/", + pub notify_url: Option, + /// 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, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OrdersInfo { + pub orders: Vec, + pub status: Status, + pub properties: Option>, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OrderInfo { + pub order: Order, + pub status: Status, + pub properties: Option>, +} diff --git a/vendor/pay_u/src/serialize.rs b/vendor/pay_u/src/serialize.rs new file mode 100644 index 0000000..54682ad --- /dev/null +++ b/vendor/pay_u/src/serialize.rs @@ -0,0 +1,25 @@ +use std::fmt::Display; + +pub(crate) fn serialize_i32(v: &i32, ser: S) -> std::result::Result +where + S: serde::Serializer, +{ + ser.serialize_str(&format!("{v}")) +} + +pub(crate) fn serialize_newtype( + v: &N, + ser: S, +) -> std::result::Result +where + S: serde::Serializer, +{ + ser.serialize_str(&format!("{v}")) +} + +pub(crate) fn serialize_u32(v: &u32, ser: S) -> std::result::Result +where + S: serde::Serializer, +{ + ser.serialize_str(&format!("{v}")) +}