From ab1661100b42d0af79eccc3dd043abede1fdd231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Mon, 22 May 2023 22:06:05 +0200 Subject: [PATCH] Add webhook --- Cargo.lock | 388 +++++++++++++---------- crates/model/src/v3.rs | 6 + crates/payment_adapter/Cargo.toml | 1 + crates/payment_adapter/src/lib.rs | 41 ++- crates/stripe_adapter/Cargo.toml | 3 +- crates/stripe_adapter/src/lib.rs | 231 +++----------- crates/stripe_adapter/src/przelewy_24.rs | 330 +++++++++++++++++++ crates/stripe_adapter/src/routes.rs | 66 ++++ 8 files changed, 691 insertions(+), 375 deletions(-) create mode 100644 crates/stripe_adapter/src/przelewy_24.rs create mode 100644 crates/stripe_adapter/src/routes.rs diff --git a/Cargo.lock b/Cargo.lock index 79c742a..0fe989e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -265,7 +265,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.9", "once_cell", "version_check", ] @@ -277,7 +277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if 1.0.0", - "getrandom", + "getrandom 0.2.9", "once_cell", "version_check", ] @@ -360,6 +360,42 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-stripe" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bb25e3145c6216eafb3eca6f8cd6e016f43c9d4416d0af7984de46acf4f288" +dependencies = [ + "chrono", + "futures-util", + "hex", + "hmac", + "http-types", + "hyper 0.14.26", + "hyper-tls 0.5.0", + "serde", + "serde_json", + "serde_path_to_error", + "serde_qs 0.10.1", + "sha2", + "smart-default", + "smol_str", + "thiserror", + "tokio 1.28.0", + "uuid 0.8.2", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -799,6 +835,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35" +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.1.0" @@ -1389,18 +1434,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "enum-as-inner" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "570d109b813e904becc80d8d5da38376818a143348413f7149f1340fe04754d4" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "enum-ordinalize" version = "3.1.12" @@ -1734,6 +1767,21 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite 0.2.9", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.28" @@ -1794,6 +1842,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.9" @@ -2076,17 +2135,6 @@ dependencies = [ "digest", ] -[[package]] -name = "hostname" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -dependencies = [ - "libc", - "match_cfg", - "winapi 0.3.9", -] - [[package]] name = "http" version = "0.2.9" @@ -2119,6 +2167,27 @@ dependencies = [ "pin-project-lite 0.2.9", ] +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "base64 0.13.1", + "futures-lite", + "http", + "infer", + "pin-project-lite 0.2.9", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs 0.8.5", + "serde_urlencoded", + "url", +] + [[package]] name = "httparse" version = "1.8.0" @@ -2279,6 +2348,12 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "insta" version = "1.29.0" @@ -2382,18 +2457,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ipconfig" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" -dependencies = [ - "socket2 0.3.19", - "widestring", - "winapi 0.3.9", - "winreg 0.6.2", -] - [[package]] name = "ipnet" version = "2.7.2" @@ -2691,15 +2754,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "lru-cache" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "lzma-sys" version = "0.1.20" @@ -2730,12 +2784,6 @@ dependencies = [ "libc", ] -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "matchers" version = "0.1.0" @@ -2899,7 +2947,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom", + "getrandom 0.2.9", ] [[package]] @@ -3137,6 +3185,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + [[package]] name = "parking_lot" version = "0.11.2" @@ -3232,6 +3286,7 @@ dependencies = [ name = "payment_adapter" version = "0.1.0" dependencies = [ + "actix-web", "async-trait", "chrono", "config", @@ -3277,20 +3332,6 @@ dependencies = [ "wasmtime-provider", ] -[[package]] -name = "payup" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d91028a90d25a925901ce59668da6bb074cb374d80f84a648a5b9a60f8b598" -dependencies = [ - "reqwest 0.11.17", - "serde", - "serde_derive", - "serde_json", - "tokio 1.28.0", - "trust-dns-resolver", -] - [[package]] name = "percent-encoding" version = "2.2.0" @@ -3478,12 +3519,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quote" version = "1.0.26" @@ -3526,7 +3561,7 @@ dependencies = [ "libc", "rand_chacha 0.1.1", "rand_core 0.4.2", - "rand_hc", + "rand_hc 0.1.0", "rand_isaac", "rand_jitter", "rand_os", @@ -3535,6 +3570,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + [[package]] name = "rand" version = "0.8.5" @@ -3556,6 +3604,16 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3581,13 +3639,22 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.9", ] [[package]] @@ -3599,6 +3666,15 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_isaac" version = "0.1.1" @@ -3716,7 +3792,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", + "getrandom 0.2.9", "redox_syscall 0.2.16", "thiserror", ] @@ -3829,7 +3905,6 @@ dependencies = [ "js-sys", "log", "mime", - "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -3847,16 +3922,6 @@ dependencies = [ "winreg 0.10.1", ] -[[package]] -name = "resolv-conf" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error", -] - [[package]] name = "ring" version = "0.16.20" @@ -4178,7 +4243,7 @@ checksum = "9c0e296ea0569d20467e9a1df3cb6ed66ce3b791a7eaf1e1110ae231f75e2b46" dependencies = [ "enclose", "futures", - "getrandom", + "getrandom 0.2.9", "gloo-file", "gloo-timers", "gloo-utils", @@ -4260,6 +4325,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_qs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_spanned" version = "0.6.1" @@ -4389,6 +4485,26 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.3.19" @@ -4603,11 +4719,12 @@ dependencies = [ name = "stripe_adapter" version = "0.1.0" dependencies = [ + "actix-web", + "async-stripe", "async-trait", "derive_more", "fulfillment_adapter", "payment_adapter", - "payup", "serde", "tokio 1.28.0", "tracing", @@ -5245,67 +5362,6 @@ dependencies = [ "inventory", ] -[[package]] -name = "trust-dns-native-tls" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3061a6b3a19c97ffddc52359221b4b854255750bf2c48b7e52db50dd81dfa13" -dependencies = [ - "futures-channel", - "futures-util", - "native-tls", - "tokio 1.28.0", - "tokio-native-tls", - "trust-dns-proto", -] - -[[package]] -name = "trust-dns-proto" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca94d4e9feb6a181c690c4040d7a24ef34018d8313ac5044a61d21222ae24e31" -dependencies = [ - "async-trait", - "cfg-if 1.0.0", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna 0.2.3", - "ipnet", - "lazy_static", - "log", - "rand 0.8.5", - "smallvec", - "thiserror", - "tinyvec", - "tokio 1.28.0", - "url", -] - -[[package]] -name = "trust-dns-resolver" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecae383baad9995efaa34ce8e57d12c3f305e545887472a492b838f4b5cfb77a" -dependencies = [ - "cfg-if 1.0.0", - "futures-util", - "ipconfig", - "lazy_static", - "log", - "lru-cache", - "parking_lot 0.11.2", - "resolv-conf", - "smallvec", - "thiserror", - "tokio 1.28.0", - "tokio-native-tls", - "trust-dns-native-tls", - "trust-dns-proto", -] - [[package]] name = "try-lock" version = "0.2.4" @@ -5420,6 +5476,7 @@ dependencies = [ "form_urlencoded", "idna 0.3.0", "percent-encoding", + "serde", ] [[package]] @@ -5428,7 +5485,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom", + "getrandom 0.2.9", "serde", ] @@ -5439,7 +5496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" dependencies = [ "atomic", - "getrandom", + "getrandom 0.2.9", "md-5", "serde", "sha1_smol", @@ -5484,6 +5541,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "want" version = "0.3.0" @@ -5529,6 +5592,12 @@ dependencies = [ "wapc", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -6008,12 +6077,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "widestring" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" - [[package]] name = "wiggle" version = "3.0.1" @@ -6307,15 +6370,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "winreg" version = "0.7.0" diff --git a/crates/model/src/v3.rs b/crates/model/src/v3.rs index 6250bab..55c4ff8 100644 --- a/crates/model/src/v3.rs +++ b/crates/model/src/v3.rs @@ -193,6 +193,12 @@ pub struct CurrencyCode([char; 3]); impl Into for CurrencyCode { fn into(self) -> String { + self.to_string() + } +} + +impl ToString for CurrencyCode { + fn to_string(&self) -> String { let mut s = String::with_capacity(3); for c in self.0 { s.push(c); diff --git a/crates/payment_adapter/Cargo.toml b/crates/payment_adapter/Cargo.toml index b6b2868..c95435b 100644 --- a/crates/payment_adapter/Cargo.toml +++ b/crates/payment_adapter/Cargo.toml @@ -15,3 +15,4 @@ async-trait = { version = "0.1.68" } chrono = { version = "0.4.24" } toml = { version = "0.7.3" } traitcast = { version = "0.5.0" } +actix-web = { version = "4.3.1" } diff --git a/crates/payment_adapter/src/lib.rs b/crates/payment_adapter/src/lib.rs index 8b063df..ad543b4 100644 --- a/crates/payment_adapter/src/lib.rs +++ b/crates/payment_adapter/src/lib.rs @@ -8,13 +8,6 @@ pub use model::v3::*; pub use uuid; use uuid::Uuid; -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] -#[repr(C)] -pub enum Status { - Success = 0, - Failure = 1, -} - pub struct AnyData(pub Vec); pub struct PaymentProcessorError { @@ -48,7 +41,7 @@ pub struct PaymentProcessorContext { pub resource_id: String, pub customer: Option, pub context: PaymentProcessCtx, - pub payment_session_data: Box, + pub payment_session_data: Option>, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -121,12 +114,24 @@ pub struct Refunded {} pub enum PError { #[error("Payment gate request failed")] HttpError, - #[error("Operation requires Charge id")] - NoChargeId, + #[error("Operation requires payment intent id")] + NoPaymentIntentId, + #[error("Operation requires customer to be fulfill")] + RequiresCustomer, + #[error("Operation requires customer payment gate id to be fulfill")] + RequiresCustomerExtId, #[error("There's no charge with id {0:?}")] ChargeNotExists(String), - #[error("Failed capture charge with id {0:?}")] - FailedCapture(String), + #[error("Failed to capture payment intent with id {0:?}")] + CaptureFailed(String), + #[error("Failed to cancel payment intent with id {0:?}")] + CancelFailed(String), + #[error("Failed to update payment intent with id {0:?}")] + UpdateFailed(String), + #[error("Failed to create refund for payment intent with id {0:?}")] + RefundFailed(String), + #[error("Payment init failed")] + InitializeFailed, #[error("Invalid charge for given payment adapter")] InvalidType, } @@ -145,12 +150,14 @@ impl Config { pub trait PaymentAdapter { async fn new(config: Config) -> Self; + fn identifier(&self) -> &'static str; + /** * Initiate a payment session with the external provider */ async fn initialize_payment( &mut self, - ctx: PaymentProcessorContext, + ctx: &mut PaymentProcessorContext, ) -> PResult; /** @@ -158,7 +165,7 @@ pub trait PaymentAdapter { */ async fn update_payment( &mut self, - ctx: PaymentProcessorContext, + ctx: &mut PaymentProcessorContext, ) -> PResult>; /** @@ -201,7 +208,7 @@ pub trait PaymentAdapter { async fn retrieve_payment( &mut self, payment_session_data: &mut Box, - ) -> PResult<()>; + ) -> PResult>; /** * Cancel an existing session @@ -231,3 +238,7 @@ pub fn session_mut_ref( ) -> Option<&mut T> { ::downcast_mut(session) } + +pub trait Plugin { + fn mount(&self, config: &mut actix_web::web::ServiceConfig); +} diff --git a/crates/stripe_adapter/Cargo.toml b/crates/stripe_adapter/Cargo.toml index a279db4..8d30f03 100644 --- a/crates/stripe_adapter/Cargo.toml +++ b/crates/stripe_adapter/Cargo.toml @@ -13,8 +13,9 @@ rustflags = ["-C", "prefer-dynamic", "-C", "rpath"] payment_adapter = { path = "../payment_adapter" } fulfillment_adapter = { path = "../fulfillment_adapter" } tokio = { version = "1.27.0" } -payup = { version = "*", features = [] } tracing = { version = "0.1.37" } async-trait = { version = "0.1.68" } serde = { version = "1.0.162", features = ['derive'] } derive_more = { version = "0.99.17" } +async-stripe = { version = "0.21.0", features = ['tokio', 'async', 'runtime-tokio-hyper'] } +actix-web = { version = "4.3.1" } diff --git a/crates/stripe_adapter/src/lib.rs b/crates/stripe_adapter/src/lib.rs index c9b25e7..536b654 100644 --- a/crates/stripe_adapter/src/lib.rs +++ b/crates/stripe_adapter/src/lib.rs @@ -1,12 +1,15 @@ #![crate_type = "rlib"] use std::collections::HashMap; -use std::ops::DerefMut; +use std::str::FromStr; -use derive_more::DerefMut; +use actix_web::HttpResponse; use fulfillment_adapter::*; use payment_adapter::*; -use payup::stripe; +use tracing::warn; + +mod przelewy_24; +mod routes; #[derive(Debug, serde::Deserialize, serde::Serialize)] pub enum CaptureMethod { @@ -14,7 +17,7 @@ pub enum CaptureMethod { Manual, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Copy, Clone, serde::Deserialize, serde::Serialize)] pub enum SetupFutureUsage { OnSession, OffSession, @@ -33,199 +36,35 @@ pub struct StripeIntent { pub payment_method_types: &'static [&'static str], } -pub struct StripeAdapter { - stripe: stripe::Auth, - config: StripeConfig, -} - -async fn payment_status(stripe: stripe::Auth, ext_id: String) -> FResult { - let charge: stripe::Charge = stripe::Charge::async_get(stripe, ext_id) +async fn payment_status( + client: &::stripe::Client, + ext_id: String, +) -> FResult { + let id = ::stripe::PaymentIntentId::from_str(&ext_id).map_err(|e| { + warn!("{e}"); + FError::HttpError + })?; + let intent = ::stripe::PaymentIntent::retrieve(client, &id, &[]) .await .map_err(|e| { - tracing::warn!("{e}"); + warn!("{e}"); FError::HttpError })?; - Ok(match charge.status.as_deref() { - Some("requires_payment_method" | "requires_confirmation" | "processing") => { - PaymentSessionStatus::Pending - } - Some("requires_action") => PaymentSessionStatus::RequiresMore, - Some("canceled") => PaymentSessionStatus::Canceled, - Some("requires_capture" | "succeeded") => PaymentSessionStatus::Authorized, - _ => PaymentSessionStatus::Pending, + Ok(match intent.status { + ::stripe::PaymentIntentStatus::RequiresPaymentMethod + | ::stripe::PaymentIntentStatus::RequiresConfirmation + | ::stripe::PaymentIntentStatus::Processing => PaymentSessionStatus::Pending, + ::stripe::PaymentIntentStatus::RequiresAction => PaymentSessionStatus::RequiresMore, + ::stripe::PaymentIntentStatus::Canceled => PaymentSessionStatus::Canceled, + ::stripe::PaymentIntentStatus::RequiresCapture + | ::stripe::PaymentIntentStatus::Succeeded => PaymentSessionStatus::Authorized, }) } -#[derive(serde::Deserialize, serde::Serialize)] -pub struct StripePrzelewy24Config { - pub api_key: String, - pub client: String, - pub webhook_secret: String, - pub capture: Option, - pub automatic_payment_methods: Option, - pub payment_description: Option, -} - -pub struct StripePrzelewy24 { - stripe: stripe::Auth, - config: StripePrzelewy24Config, -} - -#[async_trait::async_trait] -impl PaymentAdapter for StripePrzelewy24 { - async fn new(config: Config) -> Self { - let config: StripePrzelewy24Config = - config.config().expect("Malformed Stripe Przelewy24 config"); - let stripe = stripe::Auth::new(config.client.clone(), config.api_key.clone()); - Self { config, stripe } - } - - async fn initialize_payment( - &mut self, - ctx: PaymentProcessorContext, - ) -> PResult { - let PaymentProcessorContext { - billing_address: _, - email, - currency_code, - amount, - resource_id: _, - customer, - context, - payment_session_data, - } = ctx; - let desc = context - .0 - .get("payment_description") - .and_then(|v| String::from_utf8(v.0.clone()).ok()) - .or_else(|| self.config.payment_description.clone()); - - let change: stripe::Charge = stripe::Charge { - amount: Some(amount.0.to_string()), - captured: self.config.capture.clone(), - currency: Some(currency_code.into()), - description: desc, - customer: customer.as_ref().map(|c| c.id.0.to_string()), - receipt_email: Some(email.0), - ..stripe::Charge::new() - } - .async_post(self.stripe.clone()) - .await - .unwrap(); - - let update_requests = match customer.as_ref() { - Some(c) if !c.has_stripe_id() => Some(UpdateRequests { - customer_metadata: Some(CustomerMetadata(HashMap::from([( - "id".to_string(), - AnyData(change.customer.clone().unwrap_or_default().into_bytes()), - )]))), - }), - _ => None, - }; - - Ok(PaymentProcessorSessionResponse { - update_requests, - session_data: Box::new(Charge(change)), - }) - } - - async fn update_payment( - &mut self, - ctx: PaymentProcessorContext, - ) -> PResult> { - todo!() - } - - async fn refund_payment( - &mut self, - payment_session_data: &mut Box, - refund_amount: Amount, - ) -> PResult<()> { - todo!() - } - - async fn authorize_payment( - &mut self, - payment_session_data: &mut Box, - data: PaymentProcessCtx, - ) -> PResult<(PaymentSessionStatus, ())> { - self.payment_status(payment_session_data) - .await - .map(|status| (status, ())) - } - - async fn capture_payment( - &mut self, - payment_session_data: &mut Box, - ) -> PResult<()> { - let Some(session) = session_mut_ref::(payment_session_data) else { - return Err(PError::InvalidType); - }; - let id = session.id().ok_or_else(|| PError::NoChargeId)?; - let charge = stripe::Charge::async_get(self.stripe.clone(), id.clone()) - .await - .map_err(|_| PError::ChargeNotExists(id.clone()))?; - let change = charge - .async_capture(self.stripe.clone()) - .await - .map_err(|e| { - tracing::warn!("{e}"); - PError::FailedCapture(id) - })?; - session.0 = change; - Ok(()) - } - - async fn delete_payment( - &mut self, - payment_session_data: &mut Box, - ) -> PResult<()> { - todo!() - } - - async fn retrieve_payment( - &mut self, - payment_session_data: &mut Box, - ) -> PResult<()> { - todo!() - } - - async fn cancel_payment( - &mut self, - payment_session_data: &mut Box, - ) -> PResult<()> { - todo!() - } - - async fn payment_status( - &mut self, - payment_session_data: &mut Box, - ) -> PResult { - payment_status( - self.stripe.clone(), - payment_session_data - .id() - .ok_or_else(|| PError::NoChargeId)?, - ) - .await - .map_err(|_| PError::HttpError) - } -} - -impl StripePrzelewy24 { - pub fn payment_intent_options() -> StripeIntent { - StripeIntent { - capture_method: CaptureMethod::Automatic, - setup_future_usage: SetupFutureUsage::OnSession, - payment_method_types: &["p24"], - } - } -} - pub trait StripeCustomerMetadata { fn has_stripe_id(&self) -> bool; - fn stripe_id(&self) -> Option; + fn stripe_id(&self) -> Option<&str>; } impl StripeCustomerMetadata for Customer { @@ -235,19 +74,27 @@ impl StripeCustomerMetadata for Customer { .map_or_else(|| false, |h| h.contains_key("stripe_id")) } - fn stripe_id(&self) -> Option { + fn stripe_id(&self) -> Option<&str> { self.metadata .as_ref()? .get("stripe_id") - .and_then(|v| String::from_utf8(v.clone()).ok()) + .and_then(|v| std::str::from_utf8(v).ok()) } } #[derive(Debug, derive_more::Deref, derive_more::DerefMut)] -struct Charge(stripe::Charge); +struct Intent(::stripe::PaymentIntent); -impl PaymentSessionData for Charge { +impl PaymentSessionData for Intent { fn id(&self) -> Option { - self.0.id.clone() + Some(self.id.as_str().to_owned()) + } +} + +pub struct StripePlugin {} + +impl Plugin for StripePlugin { + fn mount(&self, config: &mut actix_web::web::ServiceConfig) { + config.service(routes::stripe_hooks); } } diff --git a/crates/stripe_adapter/src/przelewy_24.rs b/crates/stripe_adapter/src/przelewy_24.rs new file mode 100644 index 0000000..b024ee0 --- /dev/null +++ b/crates/stripe_adapter/src/przelewy_24.rs @@ -0,0 +1,330 @@ +use std::str::FromStr; + +use payment_adapter::{ + session_mut_ref, session_ref, Amount, AnyData, Config, CustomerMetadata, PError, PResult, + PaymentAdapter, PaymentProcessCtx, PaymentProcessorContext, PaymentProcessorSessionResponse, + PaymentSessionData, PaymentSessionStatus, UpdateRequests, +}; +use tracing::warn; + +use crate::{Intent, SetupFutureUsage, StripeCustomerMetadata}; + +pub struct StripePrzelewy24 { + config: StripePrzelewy24Config, + client: ::stripe::Client, +} + +#[async_trait::async_trait] +impl PaymentAdapter for StripePrzelewy24 { + async fn new(config: Config) -> Self { + let config: StripePrzelewy24Config = + config.config().expect("Malformed Stripe Przelewy24 config"); + let client = ::stripe::Client::new(config.api_key.as_str()); + Self { config, client } + } + + fn identifier(&self) -> &'static str { + "stripe-przelewy24" + } + + async fn initialize_payment( + &mut self, + ctx: &mut PaymentProcessorContext, + ) -> PResult { + let PaymentProcessorContext { + billing_address: _, + email, + currency_code, + amount, + resource_id, + customer, + context, + payment_session_data: _, + } = ctx; + + let desc = context + .0 + .get("payment_description") + .and_then(|v| String::from_utf8(v.0.clone()).ok()) + .or_else(|| self.config.payment_description.clone()); + + let intent = ::stripe::PaymentIntent::create( + &self.client, + ::stripe::CreatePaymentIntent { + amount: amount.0, + application_fee_amount: None, + automatic_payment_methods: self.config.automatic_payment_methods.clone().map( + |enabled| ::stripe::CreatePaymentIntentAutomaticPaymentMethods { enabled }, + ), + capture_method: Some(::stripe::PaymentIntentCaptureMethod::Automatic), + confirm: None, + confirmation_method: None, + currency: ::stripe::Currency::from_str(¤cy_code.to_string()) + .unwrap_or_else(|_| ::stripe::Currency::default()), + customer: None, + description: desc.as_deref(), + error_on_requires_action: None, + expand: &[], + mandate: None, + mandate_data: None, + metadata: Some({ + let mut m = ::stripe::Metadata::with_capacity(1); + m.insert("resource_id".into(), resource_id.clone()); + m + }), + off_session: None, + on_behalf_of: None, + payment_method: None, + payment_method_data: None, + payment_method_options: None, + payment_method_types: Some(vec!["p24".into()]), + radar_options: None, + receipt_email: Some(email.0.as_str()), + return_url: None, + setup_future_usage: Some(::stripe::PaymentIntentSetupFutureUsage::OnSession), + shipping: None, + statement_descriptor: None, + statement_descriptor_suffix: None, + transfer_data: None, + transfer_group: None, + use_stripe_sdk: None, + }, + ) + .await + .map_err(|e| { + warn!("Initialize payment failed: {e}"); + PError::InitializeFailed + })?; + + let update_requests = match customer.as_ref() { + Some(c) if !c.has_stripe_id() => Some(UpdateRequests { + customer_metadata: Some(CustomerMetadata(HashMap::from([( + "id".to_string(), + AnyData( + intent + .customer + .clone() + .map(|customer| customer.id()) + .unwrap_or_default() + .as_bytes() + .to_vec(), + ), + )]))), + }), + _ => None, + }; + + Ok(PaymentProcessorSessionResponse { + update_requests, + session_data: Box::new(Intent(intent)), + }) + } + + async fn update_payment( + &mut self, + ctx: &mut PaymentProcessorContext, + ) -> PResult> { + let customer = ctx + .customer + .as_ref() + .ok_or_else(|| PError::RequiresCustomer)?; + let stripe_id = &customer + .stripe_id() + .ok_or_else(|| PError::RequiresCustomerExtId)?; + + let session = ctx + .payment_session_data + .as_mut() + .and_then(|session| session_mut_ref::(session)) + .ok_or_else(|| PError::InvalidType)?; + + let session_customer_id = session.customer.as_ref().map(|c| c.id()); + if session_customer_id.map(|c| c.as_str() == *stripe_id) == Some(false) { + return self.initialize_payment(ctx).await.map(Some); + } else if let Some(session) = &mut ctx.payment_session_data { + let session = + session_ref::(session).ok_or_else(|| PError::NoPaymentIntentId)?; + + if ctx.amount.0 == session.amount { + return Ok(None); + } + + let intent = ::stripe::PaymentIntent::update( + &self.client, + &session.id, + ::stripe::UpdatePaymentIntent { + amount: Some(ctx.amount.0.clone()), + application_fee_amount: None, + capture_method: None, + currency: None, + customer: None, + description: None, + expand: &[], + metadata: None, + payment_method: None, + payment_method_data: None, + payment_method_options: None, + payment_method_types: None, + receipt_email: None, + setup_future_usage: None, + shipping: None, + statement_descriptor: None, + statement_descriptor_suffix: None, + transfer_data: None, + transfer_group: None, + }, + ) + .await + .map_err(|e| { + warn!("Failed to update payment intent: {e}"); + PError::UpdateFailed(session.id.as_str().to_string()) + })?; + Ok(Some(PaymentProcessorSessionResponse { + update_requests: None, + session_data: Box::new(Intent(intent)), + })) + } else { + Err(PError::NoPaymentIntentId) + } + } + + async fn refund_payment( + &mut self, + payment_session_data: &mut Box, + refund_amount: Amount, + ) -> PResult<()> { + let session = session_mut_ref::(payment_session_data) + .ok_or_else(|| PError::NoPaymentIntentId)?; + + ::stripe::Refund::create( + &self.client, + ::stripe::CreateRefund { + amount: Some(refund_amount.0), + charge: None, + currency: None, + customer: None, + expand: &[], + instructions_email: None, + metadata: None, + origin: None, + payment_intent: Some(session.id.clone()), + reason: None, + refund_application_fee: None, + reverse_transfer: None, + }, + ) + .await + .map_err(|e| { + warn!("Failed to create refund: {e}"); + PError::RefundFailed(session.id.as_str().to_string()) + })?; + + Ok(()) + } + + async fn authorize_payment( + &mut self, + payment_session_data: &mut Box, + _data: PaymentProcessCtx, + ) -> PResult<(PaymentSessionStatus, ())> { + self.payment_status(payment_session_data) + .await + .map(|status| (status, ())) + } + + async fn capture_payment( + &mut self, + payment_session_data: &mut Box, + ) -> PResult<()> { + let Some(session) = session_mut_ref::(payment_session_data) else { + return Err(PError::InvalidType); + }; + let id = session.0.id.as_str(); + + let intent = ::stripe::PaymentIntent::capture( + &self.client, + id, + ::stripe::CapturePaymentIntent { + amount_to_capture: None, + application_fee_amount: None, + }, + ) + .await + .map_err(|e| { + warn!("{e}"); + PError::CaptureFailed(id.to_owned()) + })?; + session.0 = intent; + Ok(()) + } + + async fn delete_payment( + &mut self, + payment_session_data: &mut Box, + ) -> PResult<()> { + self.cancel_payment(payment_session_data).await + } + + async fn retrieve_payment( + &mut self, + payment_session_data: &mut Box, + ) -> PResult> { + let id = payment_session_data.id().unwrap_or_default(); + let id = ::stripe::PaymentIntentId::from_str(&id).map_err(|_| PError::NoPaymentIntentId)?; + let intent = ::stripe::PaymentIntent::retrieve(&self.client, &id, &[]) + .await + .map_err(|e| { + warn!("Failed to retrieve payment intent: {e}"); + PError::NoPaymentIntentId + })?; + Ok(Box::new(Intent(intent))) + } + + async fn cancel_payment( + &mut self, + payment_session_data: &mut Box, + ) -> PResult<()> { + let session = + session_mut_ref::(payment_session_data).ok_or_else(|| PError::InvalidType)?; + let id = session.id.as_str(); + let intent = ::stripe::PaymentIntent::cancel( + &self.client, + id, + ::stripe::CancelPaymentIntent { + cancellation_reason: None, + }, + ) + .await + .map_err(|e| { + warn!("Cancel payment intent failed: {e}"); + PError::CancelFailed(id.to_owned()) + })?; + session.0 = intent; + Ok(()) + } + + async fn payment_status( + &mut self, + payment_session_data: &mut Box, + ) -> PResult { + crate::payment_status( + &self.client, + payment_session_data + .id() + .ok_or_else(|| PError::NoPaymentIntentId)?, + ) + .await + .map_err(|_| PError::HttpError) + } +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct StripePrzelewy24Config { + pub api_key: String, + pub client: String, + pub webhook_secret: String, + pub capture: Option, + pub automatic_payment_methods: Option, + pub payment_description: Option, + pub setup_future_usage: SetupFutureUsage, +} diff --git a/crates/stripe_adapter/src/routes.rs b/crates/stripe_adapter/src/routes.rs new file mode 100644 index 0000000..e0941fe --- /dev/null +++ b/crates/stripe_adapter/src/routes.rs @@ -0,0 +1,66 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use stripe::{EventObject, EventType, Webhook, WebhookError}; + +#[actix_web::web::post("/stripe/hooks")] +pub async fn stripe_hooks(req: HttpRequest, payload: web::Bytes) -> HttpResponse { + handle_webhook(req, payload).unwrap(); + HttpResponse::Ok().finish() +} + +pub fn handle_webhook(req: HttpRequest, payload: web::Bytes) -> Result<(), WebhookError> { + let payload_str = std::str::from_utf8(payload.borrow()).unwrap(); + + let stripe_signature = get_header_value(&req, "Stripe-Signature").unwrap_or_default(); + + if let Ok(event) = Webhook::construct_event(payload_str, stripe_signature, "whsec_xxxxx") { + match event.type_ { + EventType::AccountUpdated => { + if let EventObject::Account(account) = event.data.object { + handle_account_updated(account)?; + } + } + EventType::CheckoutSessionCompleted => { + if let EventObject::CheckoutSession(session) = event.data.object { + handle_checkout_session(session)?; + } + } + EventType::PaymentIntentSucceeded => { + if let EventObject::PaymentIntent(intent) = event.data.object { + handle_payment_intent(intent)?; + } + } + + _ => { + println!("Unknown event encountered in webhook: {:?}", event.type_); + } + } + } else { + println!("Failed to construct webhook event, ensure your webhook secret is correct."); + } + + Ok(()) +} + +fn get_header_value<'b>(req: &'b HttpRequest, key: &'b str) -> Option<&'b str> { + req.headers().get(key)?.to_str().ok() +} + +fn handle_account_updated(account: stripe::Account) -> Result<(), WebhookError> { + println!( + "Received account updated webhook for account: {:?}", + account.id + ); + Ok(()) +} + +fn handle_checkout_session(session: stripe::CheckoutSession) -> Result<(), WebhookError> { + println!( + "Received checkout session completed webhook with id: {:?}", + session.id + ); + Ok(()) +} + +fn handle_payment_intent(_intent: stripe::PaymentIntent) -> Result<(), WebhookError> { + Ok(()) +}