Add webhook

This commit is contained in:
Adrian Woźniak 2023-05-22 22:06:05 +02:00
parent 5801dbea2d
commit ab1661100b
8 changed files with 691 additions and 375 deletions

388
Cargo.lock generated
View File

@ -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"

View File

@ -193,6 +193,12 @@ pub struct CurrencyCode([char; 3]);
impl Into<String> 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);

View File

@ -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" }

View File

@ -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<u8>);
pub struct PaymentProcessorError {
@ -48,7 +41,7 @@ pub struct PaymentProcessorContext {
pub resource_id: String,
pub customer: Option<Customer>,
pub context: PaymentProcessCtx,
pub payment_session_data: Box<dyn PaymentSessionData>,
pub payment_session_data: Option<Box<dyn PaymentSessionData>>,
}
#[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<PaymentProcessorSessionResponse>;
/**
@ -158,7 +165,7 @@ pub trait PaymentAdapter {
*/
async fn update_payment(
&mut self,
ctx: PaymentProcessorContext,
ctx: &mut PaymentProcessorContext,
) -> PResult<Option<PaymentProcessorSessionResponse>>;
/**
@ -201,7 +208,7 @@ pub trait PaymentAdapter {
async fn retrieve_payment(
&mut self,
payment_session_data: &mut Box<dyn PaymentSessionData>,
) -> PResult<()>;
) -> PResult<Box<dyn PaymentSessionData>>;
/**
* Cancel an existing session
@ -231,3 +238,7 @@ pub fn session_mut_ref<T: PaymentSessionData + Any>(
) -> Option<&mut T> {
<dyn Any>::downcast_mut(session)
}
pub trait Plugin {
fn mount(&self, config: &mut actix_web::web::ServiceConfig);
}

View File

@ -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" }

View File

@ -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<PaymentSessionStatus> {
let charge: stripe::Charge = stripe::Charge::async_get(stripe, ext_id)
async fn payment_status(
client: &::stripe::Client,
ext_id: String,
) -> FResult<PaymentSessionStatus> {
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<bool>,
pub automatic_payment_methods: Option<bool>,
pub payment_description: Option<String>,
}
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<PaymentProcessorSessionResponse> {
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<Option<PaymentProcessorSessionResponse>> {
todo!()
}
async fn refund_payment(
&mut self,
payment_session_data: &mut Box<dyn PaymentSessionData>,
refund_amount: Amount,
) -> PResult<()> {
todo!()
}
async fn authorize_payment(
&mut self,
payment_session_data: &mut Box<dyn PaymentSessionData>,
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<dyn PaymentSessionData>,
) -> PResult<()> {
let Some(session) = session_mut_ref::<Charge>(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<dyn PaymentSessionData>,
) -> PResult<()> {
todo!()
}
async fn retrieve_payment(
&mut self,
payment_session_data: &mut Box<dyn PaymentSessionData>,
) -> PResult<()> {
todo!()
}
async fn cancel_payment(
&mut self,
payment_session_data: &mut Box<dyn PaymentSessionData>,
) -> PResult<()> {
todo!()
}
async fn payment_status(
&mut self,
payment_session_data: &mut Box<dyn PaymentSessionData>,
) -> PResult<PaymentSessionStatus> {
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<String>;
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<String> {
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<String> {
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);
}
}

View File

@ -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<PaymentProcessorSessionResponse> {
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(&currency_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<Option<PaymentProcessorSessionResponse>> {
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::<Intent>(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::<Intent>(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<dyn PaymentSessionData>,
refund_amount: Amount,
) -> PResult<()> {
let session = session_mut_ref::<Intent>(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<dyn PaymentSessionData>,
_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<dyn PaymentSessionData>,
) -> PResult<()> {
let Some(session) = session_mut_ref::<Intent>(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<dyn PaymentSessionData>,
) -> PResult<()> {
self.cancel_payment(payment_session_data).await
}
async fn retrieve_payment(
&mut self,
payment_session_data: &mut Box<dyn PaymentSessionData>,
) -> PResult<Box<dyn PaymentSessionData>> {
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<dyn PaymentSessionData>,
) -> PResult<()> {
let session =
session_mut_ref::<Intent>(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<dyn PaymentSessionData>,
) -> PResult<PaymentSessionStatus> {
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<bool>,
pub automatic_payment_methods: Option<bool>,
pub payment_description: Option<String>,
pub setup_future_usage: SetupFutureUsage,
}

View File

@ -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(())
}