Medusa
This commit is contained in:
parent
8e1f9625d1
commit
30d0baebc1
74
Cargo.lock
generated
74
Cargo.lock
generated
@ -336,6 +336,12 @@ version = "1.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
|
||||
[[package]]
|
||||
name = "anymap"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.4.1"
|
||||
@ -1101,6 +1107,16 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.94"
|
||||
@ -1791,6 +1807,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghost"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e77ac7b51b8e6313251737fcef4b1c01a2ea102bde68415b62c0ee9268fec357"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.26.2"
|
||||
@ -2293,6 +2320,28 @@ dependencies = [
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0eb5160c60ba1e809707918ee329adb99d222888155835c6feedba19f6c3fd4"
|
||||
dependencies = [
|
||||
"ctor",
|
||||
"ghost",
|
||||
"inventory-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory-impl"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e41b53715c6f0c4be49510bb82dee2c1e51c8586d885abe65396e82ed518548"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-extras"
|
||||
version = "0.15.0"
|
||||
@ -3184,12 +3233,15 @@ name = "payment_adapter"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"config",
|
||||
"futures",
|
||||
"model",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"toml 0.7.3",
|
||||
"tracing",
|
||||
"traitcast",
|
||||
"uuid 1.3.2",
|
||||
]
|
||||
|
||||
@ -4555,6 +4607,7 @@ dependencies = [
|
||||
"fulfillment_adapter",
|
||||
"payment_adapter",
|
||||
"payup",
|
||||
"serde",
|
||||
"tokio 1.28.0",
|
||||
"tracing",
|
||||
]
|
||||
@ -5170,6 +5223,27 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "traitcast"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f80b1cde694e5ff2dcb33875530f2f031a9a34dec8ba2744cacaf80a88658740"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"lazy_static",
|
||||
"traitcast_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "traitcast_core"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aabba8f4a83963f61a84d8cfc5829b4fad692aa6c6ad5d7b08b9549777e3cc4a"
|
||||
dependencies = [
|
||||
"anymap",
|
||||
"inventory",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trust-dns-native-tls"
|
||||
version = "0.20.4"
|
||||
|
@ -1,4 +1,6 @@
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FError {
|
||||
#[error("Payment gate request failed")]
|
||||
HttpError,
|
||||
}
|
||||
|
||||
@ -9,6 +11,7 @@ pub struct FulfillmentOption {}
|
||||
pub trait FulfillmentPayload {}
|
||||
pub trait FulfillmentCart {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait FulfillmentAdapter {
|
||||
fn identifier() -> &'static str;
|
||||
|
||||
@ -22,7 +25,7 @@ pub trait FulfillmentAdapter {
|
||||
* to create shipping options in Medusa that can be chosen between by
|
||||
* the customer.
|
||||
*/
|
||||
fn fulfillment_options(&mut self) -> FResult<&[FulfillmentOption]>;
|
||||
async fn fulfillment_options(&mut self) -> FResult<Vec<FulfillmentOption>>;
|
||||
|
||||
/**
|
||||
* Called before a shipping method is set on a cart to ensure that the
|
||||
@ -31,7 +34,7 @@ pub trait FulfillmentAdapter {
|
||||
* point. It is up to the fulfillment provider to enforce that the
|
||||
* correct data is being sent through.
|
||||
*/
|
||||
fn validate_fulfillment_payload<P: FulfillmentPayload, C: FulfillmentCart>(
|
||||
async fn validate_fulfillment_payload<P: FulfillmentPayload, C: FulfillmentCart>(
|
||||
&mut self,
|
||||
payload: P,
|
||||
cart: C,
|
||||
@ -41,33 +44,35 @@ pub trait FulfillmentAdapter {
|
||||
* Called before a shipping option is created in Admin. Use this to
|
||||
* ensure that a fulfillment option does in fact exist.
|
||||
*/
|
||||
fn validate_option<P: FulfillmentPayload>(&mut self, payload: P) -> FResult<bool>;
|
||||
async fn validate_option<P: FulfillmentPayload>(&mut self, payload: P) -> FResult<bool>;
|
||||
|
||||
fn can_calculate<P: FulfillmentPayload>(&mut self, payload: P) -> FResult<bool>;
|
||||
async fn can_calculate<P: FulfillmentPayload>(&mut self, payload: P) -> FResult<bool>;
|
||||
|
||||
/**
|
||||
* Used to calculate a price for a given shipping option.
|
||||
*/
|
||||
fn calculate_price<P: FulfillmentPayload, C: FulfillmentCart>(data: P, cart: C)
|
||||
-> FResult<u64>;
|
||||
async fn calculate_price<P: FulfillmentPayload, C: FulfillmentCart>(
|
||||
data: P,
|
||||
cart: C,
|
||||
) -> FResult<u64>;
|
||||
|
||||
// FULFILLMENT
|
||||
fn create_fulfillment(&mut self);
|
||||
async fn create_fulfillment(&mut self);
|
||||
|
||||
fn cancel_fulfillment(&mut self);
|
||||
async fn cancel_fulfillment(&mut self);
|
||||
|
||||
/**
|
||||
* Used to retrieve documents associated with a fulfillment.
|
||||
* Will default to returning no documents.
|
||||
*/
|
||||
fn fulfillment_documents(&mut self);
|
||||
async fn fulfillment_documents(&mut self);
|
||||
|
||||
// REFUND
|
||||
fn create_return(&mut self);
|
||||
async fn create_return(&mut self);
|
||||
|
||||
fn return_documents(&mut self);
|
||||
async fn return_documents(&mut self);
|
||||
|
||||
//
|
||||
fn shipment_documents(&mut self);
|
||||
fn documents(&mut self);
|
||||
async fn shipment_documents(&mut self);
|
||||
async fn documents(&mut self);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
pub mod api;
|
||||
|
||||
pub mod encrypt;
|
||||
pub mod v3;
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops;
|
||||
|
4031
crates/model/src/v3.rs
Normal file
4031
crates/model/src/v3.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -58,35 +58,72 @@ impl PayUPayment {
|
||||
}
|
||||
|
||||
impl PaymentAdapter for PayUPayment {
|
||||
fn init(&mut self) -> PResult<Status> {
|
||||
async fn new(config: Config) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn teardown(&mut self) -> PResult<Status> {
|
||||
async fn initialize_payment(
|
||||
&mut self,
|
||||
ctx: PaymentProcessorContext,
|
||||
) -> PResult<PaymentProcessorSessionResponse> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn create_payment(&mut self, msg: CreatePayment) -> PResult<PaymentCreated> {
|
||||
async fn update_payment(
|
||||
&mut self,
|
||||
ctx: PaymentProcessorContext,
|
||||
) -> PResult<Option<PaymentProcessorSessionResponse>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn retrieve_payment(&mut self) {
|
||||
async fn refund_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
refund_amount: Amount,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn authorize_payment(&mut self) {
|
||||
async fn authorize_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
data: PaymentProcessCtx,
|
||||
) -> PResult<(PaymentSessionStatus, PaymentSessionData)> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn capture_payment(&mut self) {
|
||||
async fn capture_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn refund_payment(&mut self, msg: RefundPayment) -> PResult<Refunded> {
|
||||
async fn delete_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn delete_payment(&mut self, msg: DeletePayment) -> PResult<Deleted> {
|
||||
async fn retrieve_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn cancel_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn payment_status(
|
||||
&mut self,
|
||||
payment_session_data: &PaymentSessionData,
|
||||
) -> PResult<PaymentSessionStatus> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -12,3 +12,6 @@ thiserror = { version = "1" }
|
||||
tracing = { version = "0" }
|
||||
uuid = { version = "1", features = ['v4'] }
|
||||
async-trait = { version = "0.1.68" }
|
||||
chrono = { version = "0.4.24" }
|
||||
toml = { version = "0.7.3" }
|
||||
traitcast = { version = "0.5.0" }
|
||||
|
@ -1,29 +1,13 @@
|
||||
#![feature(trait_upcasting)]
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
pub use config::PaymentProviderConfig;
|
||||
pub use model::*;
|
||||
pub use model::v3::*;
|
||||
pub use uuid;
|
||||
|
||||
pub const CONFIG_POS: u32 = 3;
|
||||
|
||||
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
|
||||
#[repr(C)]
|
||||
pub enum Error {
|
||||
#[error("Malformed create payment message")]
|
||||
MalformedCreatePayment,
|
||||
#[error("Malformed payment status update message")]
|
||||
MalformedStatusUpdate,
|
||||
#[error("Malformed authorize response")]
|
||||
MalformedAuthorize,
|
||||
#[error("Message pack: malformed create payment message")]
|
||||
MsgPackDeserializationFailed,
|
||||
#[error("Provider rejected authorization")]
|
||||
AuthorizeFailed,
|
||||
#[error("Provider rejected order")]
|
||||
PaymentFailed,
|
||||
#[error("HTTP request failed")]
|
||||
HttpFailed,
|
||||
#[error("Unknown host operation {0:?}")]
|
||||
UnknownOperation(String),
|
||||
}
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||
#[repr(C)]
|
||||
@ -32,59 +16,99 @@ pub enum Status {
|
||||
Failure = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[repr(C)]
|
||||
pub struct Buyer {
|
||||
/// Required customer e-mail
|
||||
pub email: String,
|
||||
/// Required customer phone number
|
||||
pub phone: String,
|
||||
/// Required customer first name
|
||||
pub first_name: String,
|
||||
/// Required customer last name
|
||||
pub last_name: String,
|
||||
/// Required customer language
|
||||
pub language: String,
|
||||
pub struct AnyData(pub Vec<u8>);
|
||||
|
||||
pub struct PaymentProcessorError {
|
||||
pub error: String,
|
||||
pub code: Option<String>,
|
||||
pub detail: Option<AnyData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[repr(C)]
|
||||
pub struct Product {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub unit_price: i32,
|
||||
pub quantity_unit: String,
|
||||
pub quantity: i32,
|
||||
pub struct CustomerMetadata(pub HashMap<String, AnyData>);
|
||||
|
||||
pub struct PaymentProcessCtx(pub HashMap<String, AnyData>);
|
||||
|
||||
pub struct UpdateRequests {
|
||||
pub customer_metadata: Option<CustomerMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[repr(C)]
|
||||
pub struct Item {
|
||||
pub product_id: i32,
|
||||
pub quantity: i32,
|
||||
pub quantity_unit: String,
|
||||
pub struct PaymentSessionData {
|
||||
pub id: Option<String>,
|
||||
pub meta: HashMap<String, AnyData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PaymentProcessorSessionResponse {
|
||||
pub update_requests: Option<UpdateRequests>,
|
||||
pub session_data: PaymentSessionData,
|
||||
}
|
||||
|
||||
pub struct PaymentProcessorContext {
|
||||
pub billing_address: Option<Address>,
|
||||
pub email: Email,
|
||||
pub currency_code: CurrencyCode,
|
||||
pub amount: Amount,
|
||||
pub resource_id: String,
|
||||
pub customer: Option<Customer>,
|
||||
pub context: PaymentProcessCtx,
|
||||
pub payment_session_data: PaymentSessionData,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CreatePayment {
|
||||
pub buyer: Buyer,
|
||||
// pub buyer: Buyer,
|
||||
pub customer_ip: String,
|
||||
pub currency: String,
|
||||
pub description: String,
|
||||
pub cart_products: Vec<Product>,
|
||||
pub items: Vec<Item>,
|
||||
pub items: Vec<LineItem>,
|
||||
|
||||
//
|
||||
pub cart: Cart,
|
||||
pub customer: Option<Customer>,
|
||||
pub customer_groups: Option<CustomerGroup>,
|
||||
pub gift_cards: Vec<GiftCard>,
|
||||
pub region: Region,
|
||||
pub related_discounts: HashMap<DiscountId, Discount>,
|
||||
pub related_shipping_methods: HashMap<Uuid, ShippingMethod>,
|
||||
// cart line items and [LineItem::original_item_id]
|
||||
pub related_items: HashMap<Uuid, LineItem>,
|
||||
/// [Payment::swap_id]
|
||||
pub related_payments: HashMap<PaymentId, Payment>,
|
||||
pub related_line_item_adjustments: HashMap<LineItemAdjustmentId, LineItemAdjustment>,
|
||||
pub related_tax_lines: HashMap<Uuid, LineItemTaxLine>,
|
||||
pub related_currencies: HashMap<CurrencyCode, Currency>,
|
||||
/// [Return::swap_id]
|
||||
pub related_returns: HashMap<ReturnId, Return>,
|
||||
//
|
||||
pub order_ext_id: Option<String>,
|
||||
pub notify_uri: String,
|
||||
pub continue_uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug)]
|
||||
pub struct PaymentCreated {
|
||||
pub ext_order_id: Option<ExtOrderId>,
|
||||
pub redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug)]
|
||||
pub struct DeletePayment {
|
||||
pub ext_order_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Deleted {
|
||||
pub ext_order_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RefundPayment {
|
||||
pub refund: RefundType,
|
||||
pub provider_order_id: Uuid,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RefundType {
|
||||
/// Refund entire payment
|
||||
Full,
|
||||
@ -92,97 +116,132 @@ pub enum RefundType {
|
||||
Partial(Price),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RefundPayment {
|
||||
pub refund: RefundType,
|
||||
pub provider_order_id: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug)]
|
||||
pub struct Refunded {}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UpdateStatus {
|
||||
pub payload: Vec<u8>,
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PError {
|
||||
#[error("Payment gate request failed")]
|
||||
HttpError,
|
||||
#[error("Operation requires Charge id")]
|
||||
NoChargeId,
|
||||
#[error("There's no charge with id {0:?}")]
|
||||
ChargeNotExists(String),
|
||||
#[error("Failed capture charge with id {0:?}")]
|
||||
FailedCapture(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StatusUpdated {}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DeletePayment {
|
||||
pub ext_order_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Deleted {
|
||||
pub ext_order_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum HttpMethod {
|
||||
Get,
|
||||
Post,
|
||||
}
|
||||
|
||||
pub enum PaymentSessionStatus {
|
||||
Pending,
|
||||
RequiresMore,
|
||||
Canceled,
|
||||
Authorized,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[repr(C)]
|
||||
pub struct HttpRequest {
|
||||
pub method: HttpMethod,
|
||||
pub url: String,
|
||||
pub headers: Vec<(String, String)>,
|
||||
pub bearer_auth: Option<String>,
|
||||
pub body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub mod tracing {
|
||||
macro_rules! log_levels {
|
||||
($($lvl: ident),+) => {
|
||||
$(
|
||||
#[macro_export]
|
||||
macro_rules! $lvl {
|
||||
($fmt: expr) => {
|
||||
wapc::console_log($fmt);
|
||||
};
|
||||
($fmt: expr, $arg1: expr) => {
|
||||
wapc::console_log(&format!($fmt, $arg1));
|
||||
};
|
||||
($fmt: expr, $arg1: expr, $arg2: expr) => {
|
||||
wapc::console_log(&format!($fmt, $arg1, $arg2));
|
||||
};
|
||||
($fmt: expr, $arg1: expr, $arg2: expr, $arg3: expr) => {
|
||||
wapc::console_log(&format!($fmt, $arg1, $arg2, $arg3));
|
||||
};
|
||||
($fmt: expr, $arg1: expr, $arg2: expr, $arg3: expr, $arg4: expr) => {
|
||||
wapc::console_log(&format!($fmt, $arg1, $arg2, $arg3, $arg4));
|
||||
};
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
log_levels!(error, warn, debug, info, trace, log);
|
||||
}
|
||||
|
||||
pub enum PError {}
|
||||
|
||||
pub type PResult<T> = Result<T, PError>;
|
||||
|
||||
pub trait PaymentAdapter {
|
||||
fn init(&mut self) -> PResult<Status>;
|
||||
fn teardown(&mut self) -> PResult<Status>;
|
||||
pub struct Config(String);
|
||||
|
||||
fn create_payment(&mut self, msg: CreatePayment) -> PResult<PaymentCreated>;
|
||||
fn retrieve_payment(&mut self);
|
||||
fn authorize_payment(&mut self);
|
||||
fn capture_payment(&mut self);
|
||||
fn refund_payment(&mut self, msg: RefundPayment) -> PResult<Refunded>;
|
||||
fn delete_payment(&mut self, msg: DeletePayment) -> PResult<Deleted>;
|
||||
impl Config {
|
||||
pub fn config<S: serde::de::DeserializeOwned>(self) -> Result<S, toml::de::Error> {
|
||||
toml::from_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait PaymentAdapter {
|
||||
async fn new(config: Config) -> Self;
|
||||
|
||||
/**
|
||||
* Initiate a payment session with the external provider
|
||||
*/
|
||||
async fn initialize_payment(
|
||||
&mut self,
|
||||
ctx: PaymentProcessorContext,
|
||||
) -> PResult<PaymentProcessorSessionResponse>;
|
||||
|
||||
/**
|
||||
* Update an existing payment session
|
||||
*/
|
||||
async fn update_payment(
|
||||
&mut self,
|
||||
ctx: PaymentProcessorContext,
|
||||
) -> PResult<Option<PaymentProcessorSessionResponse>>;
|
||||
|
||||
/**
|
||||
* Refund an existing session
|
||||
*/
|
||||
async fn refund_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
refund_amount: Amount,
|
||||
) -> PResult<PaymentSessionData>;
|
||||
|
||||
/**
|
||||
* Authorize an existing session if it is not already authorized
|
||||
*/
|
||||
async fn authorize_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
data: PaymentProcessCtx,
|
||||
) -> PResult<(PaymentSessionStatus, PaymentSessionData)>;
|
||||
|
||||
/**
|
||||
* Capture an existing session
|
||||
*/
|
||||
async fn capture_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData>;
|
||||
|
||||
/**
|
||||
* Delete an existing session
|
||||
*/
|
||||
async fn delete_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData>;
|
||||
|
||||
/**
|
||||
* Retrieve an existing session
|
||||
*/
|
||||
async fn retrieve_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData>;
|
||||
|
||||
/**
|
||||
* Cancel an existing session
|
||||
*/
|
||||
async fn cancel_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData>;
|
||||
|
||||
/**
|
||||
* Return the status of the session
|
||||
*/
|
||||
async fn payment_status(
|
||||
&mut self,
|
||||
payment_session_data: &PaymentSessionData,
|
||||
) -> PResult<PaymentSessionStatus>;
|
||||
}
|
||||
|
||||
// use traitcast::{Traitcast, TraitcastFrom};
|
||||
|
||||
pub trait APaymentSessionData: Any {
|
||||
fn id(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
pub struct BPSD(Box<dyn APaymentSessionData>);
|
||||
|
||||
impl BPSD {
|
||||
pub fn as_mut_ref<T>(&mut self) -> Option<&mut T> {
|
||||
use std::boxed::Box;
|
||||
|
||||
let r = self.0.deref_mut();
|
||||
if <dyn Any>::is::<&T>(r) {
|
||||
Some(unsafe { r as &mut T })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn f(mut psd: &mut BPSD) {
|
||||
use std::boxed::Box;
|
||||
let c = psd.deref_mut();
|
||||
}
|
||||
|
@ -1,396 +1,399 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::read_dir;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, LockResult, MutexGuard};
|
||||
|
||||
use config::{AppConfig, SharedAppConfig, UpdateConfig};
|
||||
use payment_adapter::{HttpMethod, HttpRequest};
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use wapc::WasiParams;
|
||||
use wapc_pool::{HostPool, HostPoolBuilder};
|
||||
|
||||
use crate::rpc::ModuleSender;
|
||||
|
||||
// mod actions;
|
||||
// mod context;
|
||||
// mod db;
|
||||
mod mqtt;
|
||||
// pub mod pay_u_adapter;
|
||||
mod rpc;
|
||||
// pub mod t_pay_adapter;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum WasmMsg {
|
||||
CreateOrder {
|
||||
create_payment: payment_adapter::CreatePayment,
|
||||
tx: Sender<ResultMsg>,
|
||||
},
|
||||
UpdateStatus {
|
||||
update_status: payment_adapter::UpdateStatus,
|
||||
tx: Sender<ResultMsg>,
|
||||
},
|
||||
Cancel {
|
||||
cancel: payment_adapter::RefundPayment,
|
||||
tx: Sender<ResultMsg>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ResultMsg {
|
||||
OrderCreated(payment_adapter::PaymentCreated),
|
||||
StatusUpdated(payment_adapter::StatusUpdated),
|
||||
Cancel(payment_adapter::Refunded),
|
||||
}
|
||||
|
||||
pub struct HostChannel {
|
||||
pub host: HostPool,
|
||||
pub channel: std::sync::mpsc::Receiver<WasmMsg>,
|
||||
}
|
||||
|
||||
impl HostChannel {
|
||||
pub fn start(self) {
|
||||
tokio::spawn(async move {
|
||||
let HostChannel {
|
||||
host: pool,
|
||||
channel,
|
||||
} = self;
|
||||
|
||||
loop {
|
||||
let msg = match channel.recv() {
|
||||
Ok(msg) => msg,
|
||||
_ => continue,
|
||||
};
|
||||
match msg {
|
||||
WasmMsg::CreateOrder {
|
||||
create_payment: msg,
|
||||
tx,
|
||||
} => {
|
||||
Self::send_and_rec(
|
||||
"create_payment",
|
||||
msg,
|
||||
tx,
|
||||
&pool,
|
||||
ResultMsg::OrderCreated,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
WasmMsg::UpdateStatus {
|
||||
update_status: msg,
|
||||
tx,
|
||||
} => {
|
||||
Self::send_and_rec(
|
||||
"update_status",
|
||||
msg,
|
||||
tx,
|
||||
&pool,
|
||||
ResultMsg::StatusUpdated,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
WasmMsg::Cancel { cancel: msg, tx } => {
|
||||
Self::send_and_rec("cancel_order", msg, tx, &pool, ResultMsg::Cancel)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn send_and_rec<R, T, F>(
|
||||
name: &str,
|
||||
msg: R,
|
||||
tx: Sender<ResultMsg>,
|
||||
pool: &HostPool,
|
||||
f: F,
|
||||
) -> Result<(), ()>
|
||||
where
|
||||
R: serde::Serialize,
|
||||
T: serde::de::DeserializeOwned,
|
||||
F: FnOnce(T) -> ResultMsg,
|
||||
{
|
||||
macro_rules! ok_or_bail {
|
||||
($run: expr) => {
|
||||
match $run {
|
||||
Ok(r) => r,
|
||||
_ => return Err(()),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let msg = ok_or_bail!(wapc_codec::messagepack::serialize(msg));
|
||||
let call_result = ok_or_bail!(pool.call(name, msg).await);
|
||||
let result: T = ok_or_bail!(wapc_codec::messagepack::deserialize(&call_result));
|
||||
if let Err(e) = tx.send(f(result)) {
|
||||
tracing::error!("{e:?}");
|
||||
Err(())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Modules(Arc<std::sync::Mutex<HashMap<String, ModuleSender>>>);
|
||||
|
||||
impl Modules {
|
||||
pub fn new(h: HashMap<String, ModuleSender>) -> Self {
|
||||
Self(Arc::new(std::sync::Mutex::new(h)))
|
||||
}
|
||||
|
||||
pub fn lock(&self) -> LockResult<MutexGuard<'_, HashMap<String, ModuleSender>>> {
|
||||
self.0.lock()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(gumdrop::Options)]
|
||||
pub struct Opts {
|
||||
pub adapters_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl UpdateConfig for Opts {
|
||||
fn update_config(&self, config: &mut AppConfig) {
|
||||
if let Some(path) = self.adapters_path.clone() {
|
||||
config.payment_mut().adapters_path = Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
config::init_tracing("payments");
|
||||
|
||||
let opts: Opts = gumdrop::parse_args_default_or_exit();
|
||||
|
||||
let config = config::default_load(&opts);
|
||||
let files = scan_adapters_dir(&config);
|
||||
let mut modules = HashMap::with_capacity(files.len());
|
||||
|
||||
load_adapters(&config, &mut modules, files).await;
|
||||
|
||||
// for pool in modules.values() {
|
||||
// let msg = payment_adapter::CreatePayment {
|
||||
// buyer: payment_adapter::Buyer {
|
||||
// email: "hello@example.com".to_string(),
|
||||
// phone: "530698478".to_string(),
|
||||
// first_name: "Joe".to_string(),
|
||||
// last_name: "Doe".to_string(),
|
||||
// language: "pl".to_string(),
|
||||
// },
|
||||
// customer_ip: "12.22.34.54".to_string(),
|
||||
// currency: "PLN".to_string(),
|
||||
// description: "Nesciunt fugit libero quis dolorum quo.
|
||||
// Tempore aut nisi voluptatem. Odio et aspernatur est. Sint vel
|
||||
// molestias sunt cumque quibusdam reprehenderit est.".to_string(),
|
||||
// cart_products: vec![Product {
|
||||
// id: 23,
|
||||
// name: "Socks".to_string(),
|
||||
// unit_price: 1542,
|
||||
// quantity_unit: "Unit".to_string(),
|
||||
// quantity: 2,
|
||||
// }],
|
||||
// items: vec![Item {
|
||||
// product_id: 23,
|
||||
// quantity: 2,
|
||||
// quantity_unit: "Unit".to_string(),
|
||||
// }],
|
||||
// order_ext_id: None,
|
||||
// notify_uri: "https://localhost:3030/notify_uri".to_string(),
|
||||
// continue_uri: "https://localhost:3030/continue_uri".to_string(),
|
||||
// };
|
||||
//
|
||||
// tracing::info!("Start create_payment");
|
||||
// let call_result = pool
|
||||
// .call(
|
||||
// "create_payment",
|
||||
// wapc_codec::messagepack::serialize(msg).unwrap(),
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let result: payment_adapter::OrderCreated =
|
||||
// wapc_codec::messagepack::deserialize(&call_result).unwrap();
|
||||
//
|
||||
// tracing::info!("create payment res {:?}", result)
|
||||
// }
|
||||
|
||||
let modules = Modules::new(modules);
|
||||
let mqtt_client = mqtt::start(config.clone(), modules.clone()).await;
|
||||
rpc::start(config, mqtt_client, modules.clone()).await;
|
||||
}
|
||||
|
||||
async fn load_adapters(
|
||||
config: &SharedAppConfig,
|
||||
modules: &mut HashMap<String, ModuleSender>,
|
||||
files: Vec<PathBuf>,
|
||||
) {
|
||||
for file in files {
|
||||
let module = std::fs::read(&file).unwrap();
|
||||
let engine = wasmtime_provider::WasmtimeEngineProviderBuilder::new()
|
||||
.module_bytes(&module)
|
||||
.wasi_params(WasiParams::default())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let pool = HostPoolBuilder::new()
|
||||
.name("pool")
|
||||
.factory(move || {
|
||||
wapc::WapcHost::new(
|
||||
Box::new(engine.clone()),
|
||||
Some(Box::new(
|
||||
move |_a, binding, _namespace, msg_name, payload| {
|
||||
Ok(host_call(binding, msg_name, payload)?)
|
||||
},
|
||||
)),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.max_threads(5)
|
||||
.build();
|
||||
let name = pool
|
||||
.call("name", vec![])
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("Failed to load adapter {file:?} `name`. {e}"));
|
||||
let name: String = wapc_codec::messagepack::deserialize(&name)
|
||||
.unwrap_or_else(|e| panic!("Adapter `name` ({name:?}) is not valid string. {e}"));
|
||||
|
||||
let msg = config
|
||||
.lock()
|
||||
.payment()
|
||||
.providers
|
||||
.get(&name)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
tracing::info!("Start init");
|
||||
pool.call("init", wapc_codec::messagepack::serialize(msg).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
HostChannel {
|
||||
host: pool,
|
||||
channel: rx,
|
||||
}
|
||||
.start();
|
||||
|
||||
modules.insert(name, ModuleSender::new(tx));
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_adapters_dir(config: &SharedAppConfig) -> Vec<PathBuf> {
|
||||
let adapters_path = config.lock().payment().adapters_path.clone();
|
||||
let adapters = adapters_path.unwrap_or_else(|| panic!("No payment adapters path provided"));
|
||||
let dir = read_dir(&adapters).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Failed to load payment adapters at path {:?}. {}",
|
||||
adapters, e
|
||||
)
|
||||
});
|
||||
let files = dir
|
||||
.filter_map(|r| r.map(|r| r.path()).ok())
|
||||
.filter(|file| file.extension().and_then(|s| s.to_str()) == Some("wasm"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if files.is_empty() {
|
||||
panic!("No payment adapters found in adapters directory");
|
||||
}
|
||||
files
|
||||
}
|
||||
|
||||
// use std::collections::HashMap;
|
||||
// use std::fs::read_dir;
|
||||
// use std::path::PathBuf;
|
||||
// use std::str::FromStr;
|
||||
// use std::sync::mpsc::Sender;
|
||||
// use std::sync::{Arc, LockResult, MutexGuard};
|
||||
//
|
||||
// use config::{AppConfig, SharedAppConfig, UpdateConfig};
|
||||
// use payment_adapter::{HttpMethod, HttpRequest};
|
||||
// use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
// use wapc::WasiParams;
|
||||
// use wapc_pool::{HostPool, HostPoolBuilder};
|
||||
//
|
||||
// use crate::rpc::ModuleSender;
|
||||
//
|
||||
// // mod actions;
|
||||
// // mod context;
|
||||
// // mod db;
|
||||
// mod mqtt;
|
||||
// // pub mod pay_u_adapter;
|
||||
// mod rpc;
|
||||
// // pub mod t_pay_adapter;
|
||||
//
|
||||
// #[derive(Clone)]
|
||||
// pub enum WasmMsg {
|
||||
// CreateOrder {
|
||||
// create_payment: payment_adapter::CreatePayment,
|
||||
// tx: Sender<ResultMsg>,
|
||||
// },
|
||||
// UpdateStatus {
|
||||
// update_status: payment_adapter::UpdateStatus,
|
||||
// tx: Sender<ResultMsg>,
|
||||
// },
|
||||
// Cancel {
|
||||
// cancel: payment_adapter::RefundPayment,
|
||||
// tx: Sender<ResultMsg>,
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// #[derive(Clone)]
|
||||
// pub enum ResultMsg {
|
||||
// OrderCreated(payment_adapter::PaymentCreated),
|
||||
// StatusUpdated(payment_adapter::StatusUpdated),
|
||||
// Cancel(payment_adapter::Refunded),
|
||||
// }
|
||||
//
|
||||
// pub struct HostChannel {
|
||||
// pub host: HostPool,
|
||||
// pub channel: std::sync::mpsc::Receiver<WasmMsg>,
|
||||
// }
|
||||
//
|
||||
// impl HostChannel {
|
||||
// pub fn start(self) {
|
||||
// tokio::spawn(async move {
|
||||
// let HostChannel {
|
||||
// host: pool,
|
||||
// channel,
|
||||
// } = self;
|
||||
//
|
||||
// loop {
|
||||
// let msg = match channel.recv() {
|
||||
// Ok(msg) => msg,
|
||||
// _ => continue,
|
||||
// };
|
||||
// match msg {
|
||||
// WasmMsg::CreateOrder {
|
||||
// create_payment: msg,
|
||||
// tx,
|
||||
// } => {
|
||||
// Self::send_and_rec(
|
||||
// "create_payment",
|
||||
// msg,
|
||||
// tx,
|
||||
// &pool,
|
||||
// ResultMsg::OrderCreated,
|
||||
// )
|
||||
// .await
|
||||
// .ok();
|
||||
// }
|
||||
// WasmMsg::UpdateStatus {
|
||||
// update_status: msg,
|
||||
// tx,
|
||||
// } => {
|
||||
// Self::send_and_rec(
|
||||
// "update_status",
|
||||
// msg,
|
||||
// tx,
|
||||
// &pool,
|
||||
// ResultMsg::StatusUpdated,
|
||||
// )
|
||||
// .await
|
||||
// .ok();
|
||||
// }
|
||||
// WasmMsg::Cancel { cancel: msg, tx } => {
|
||||
// Self::send_and_rec("cancel_order", msg, tx, &pool,
|
||||
// ResultMsg::Cancel) .await
|
||||
// .ok();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// async fn send_and_rec<R, T, F>(
|
||||
// name: &str,
|
||||
// msg: R,
|
||||
// tx: Sender<ResultMsg>,
|
||||
// pool: &HostPool,
|
||||
// f: F,
|
||||
// ) -> Result<(), ()>
|
||||
// where
|
||||
// R: serde::Serialize,
|
||||
// T: serde::de::DeserializeOwned,
|
||||
// F: FnOnce(T) -> ResultMsg,
|
||||
// {
|
||||
// macro_rules! ok_or_bail {
|
||||
// ($run: expr) => {
|
||||
// match $run {
|
||||
// Ok(r) => r,
|
||||
// _ => return Err(()),
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// let msg = ok_or_bail!(wapc_codec::messagepack::serialize(msg));
|
||||
// let call_result = ok_or_bail!(pool.call(name, msg).await);
|
||||
// let result: T =
|
||||
// ok_or_bail!(wapc_codec::messagepack::deserialize(&call_result)); if
|
||||
// let Err(e) = tx.send(f(result)) { tracing::error!("{e:?}");
|
||||
// Err(())
|
||||
// } else {
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[derive(Clone)]
|
||||
// pub struct Modules(Arc<std::sync::Mutex<HashMap<String, ModuleSender>>>);
|
||||
//
|
||||
// impl Modules {
|
||||
// pub fn new(h: HashMap<String, ModuleSender>) -> Self {
|
||||
// Self(Arc::new(std::sync::Mutex::new(h)))
|
||||
// }
|
||||
//
|
||||
// pub fn lock(&self) -> LockResult<MutexGuard<'_, HashMap<String,
|
||||
// ModuleSender>>> { self.0.lock()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[derive(gumdrop::Options)]
|
||||
// pub struct Opts {
|
||||
// pub adapters_path: Option<PathBuf>,
|
||||
// }
|
||||
//
|
||||
// impl UpdateConfig for Opts {
|
||||
// fn update_config(&self, config: &mut AppConfig) {
|
||||
// if let Some(path) = self.adapters_path.clone() {
|
||||
// config.payment_mut().adapters_path = Some(path);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[tokio::main]
|
||||
// async fn main() {
|
||||
// config::init_tracing("payments");
|
||||
//
|
||||
// let opts: Opts = gumdrop::parse_args_default_or_exit();
|
||||
//
|
||||
// let config = config::default_load(&opts);
|
||||
// let files = scan_adapters_dir(&config);
|
||||
// let mut modules = HashMap::with_capacity(files.len());
|
||||
//
|
||||
// load_adapters(&config, &mut modules, files).await;
|
||||
//
|
||||
// // for pool in modules.values() {
|
||||
// // let msg = payment_adapter::CreatePayment {
|
||||
// // buyer: payment_adapter::Buyer {
|
||||
// // email: "hello@example.com".to_string(),
|
||||
// // phone: "530698478".to_string(),
|
||||
// // first_name: "Joe".to_string(),
|
||||
// // last_name: "Doe".to_string(),
|
||||
// // language: "pl".to_string(),
|
||||
// // },
|
||||
// // customer_ip: "12.22.34.54".to_string(),
|
||||
// // currency: "PLN".to_string(),
|
||||
// // description: "Nesciunt fugit libero quis dolorum quo.
|
||||
// // Tempore aut nisi voluptatem. Odio et aspernatur est. Sint vel
|
||||
// // molestias sunt cumque quibusdam reprehenderit est.".to_string(),
|
||||
// // cart_products: vec![Product {
|
||||
// // id: 23,
|
||||
// // name: "Socks".to_string(),
|
||||
// // unit_price: 1542,
|
||||
// // quantity_unit: "Unit".to_string(),
|
||||
// // quantity: 2,
|
||||
// // }],
|
||||
// // items: vec![Item {
|
||||
// // product_id: 23,
|
||||
// // quantity: 2,
|
||||
// // quantity_unit: "Unit".to_string(),
|
||||
// // }],
|
||||
// // order_ext_id: None,
|
||||
// // notify_uri: "https://localhost:3030/notify_uri".to_string(),
|
||||
// // continue_uri: "https://localhost:3030/continue_uri".to_string(),
|
||||
// // };
|
||||
// //
|
||||
// // tracing::info!("Start create_payment");
|
||||
// // let call_result = pool
|
||||
// // .call(
|
||||
// // "create_payment",
|
||||
// // wapc_codec::messagepack::serialize(msg).unwrap(),
|
||||
// // )
|
||||
// // .await
|
||||
// // .unwrap();
|
||||
// // let result: payment_adapter::OrderCreated =
|
||||
// // wapc_codec::messagepack::deserialize(&call_result).unwrap();
|
||||
// //
|
||||
// // tracing::info!("create payment res {:?}", result)
|
||||
// // }
|
||||
//
|
||||
// let modules = Modules::new(modules);
|
||||
// let mqtt_client = mqtt::start(config.clone(), modules.clone()).await;
|
||||
// rpc::start(config, mqtt_client, modules.clone()).await;
|
||||
// }
|
||||
//
|
||||
// async fn load_adapters(
|
||||
// config: &SharedAppConfig,
|
||||
// modules: &mut HashMap<String, ModuleSender>,
|
||||
// files: Vec<PathBuf>,
|
||||
// ) {
|
||||
// for file in files {
|
||||
// let module = std::fs::read(&file).unwrap();
|
||||
// let engine = wasmtime_provider::WasmtimeEngineProviderBuilder::new()
|
||||
// .module_bytes(&module)
|
||||
// .wasi_params(WasiParams::default())
|
||||
// .build()
|
||||
// .unwrap();
|
||||
//
|
||||
// let pool = HostPoolBuilder::new()
|
||||
// .name("pool")
|
||||
// .factory(move || {
|
||||
// wapc::WapcHost::new(
|
||||
// Box::new(engine.clone()),
|
||||
// Some(Box::new(
|
||||
// move |_a, binding, _namespace, msg_name, payload| {
|
||||
// Ok(host_call(binding, msg_name, payload)?)
|
||||
// },
|
||||
// )),
|
||||
// )
|
||||
// .unwrap()
|
||||
// })
|
||||
// .max_threads(5)
|
||||
// .build();
|
||||
// let name = pool
|
||||
// .call("name", vec![])
|
||||
// .await
|
||||
// .unwrap_or_else(|e| panic!("Failed to load adapter {file:?}
|
||||
// `name`. {e}")); let name: String =
|
||||
// wapc_codec::messagepack::deserialize(&name) .unwrap_or_else(|e|
|
||||
// panic!("Adapter `name` ({name:?}) is not valid string. {e}"));
|
||||
//
|
||||
// let msg = config
|
||||
// .lock()
|
||||
// .payment()
|
||||
// .providers
|
||||
// .get(&name)
|
||||
// .cloned()
|
||||
// .unwrap_or_default();
|
||||
//
|
||||
// tracing::info!("Start init");
|
||||
// pool.call("init", wapc_codec::messagepack::serialize(msg).unwrap())
|
||||
// .await
|
||||
// .unwrap();
|
||||
//
|
||||
// let (tx, rx) = std::sync::mpsc::channel();
|
||||
// HostChannel {
|
||||
// host: pool,
|
||||
// channel: rx,
|
||||
// }
|
||||
// .start();
|
||||
//
|
||||
// modules.insert(name, ModuleSender::new(tx));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fn scan_adapters_dir(config: &SharedAppConfig) -> Vec<PathBuf> {
|
||||
// let adapters_path = config.lock().payment().adapters_path.clone();
|
||||
// let adapters = adapters_path.unwrap_or_else(|| panic!("No payment
|
||||
// adapters path provided")); let dir =
|
||||
// read_dir(&adapters).unwrap_or_else(|e| { panic!(
|
||||
// "Failed to load payment adapters at path {:?}. {}",
|
||||
// adapters, e
|
||||
// )
|
||||
// });
|
||||
// let files = dir
|
||||
// .filter_map(|r| r.map(|r| r.path()).ok())
|
||||
// .filter(|file| file.extension().and_then(|s| s.to_str()) ==
|
||||
// Some("wasm")) .collect::<Vec<_>>();
|
||||
//
|
||||
// if files.is_empty() {
|
||||
// panic!("No payment adapters found in adapters directory");
|
||||
// }
|
||||
// files
|
||||
// }
|
||||
//
|
||||
// // #[tracing::instrument]
|
||||
// fn host_call(binding: &str, name: &str, payload: &[u8]) -> Result<Vec<u8>,
|
||||
// payment_adapter::Error> { match name {
|
||||
// "http_req" => {
|
||||
// let req: HttpRequest = match
|
||||
// wapc_codec::messagepack::deserialize(payload) { Ok(req) =>
|
||||
// req, Err(e) => {
|
||||
// tracing::error!("{:?} payload {:?}", binding, payload);
|
||||
// tracing::error!("Failed to deserialize. {}", e);
|
||||
// return
|
||||
// Err(payment_adapter::Error::MsgPackDeserializationFailed); }
|
||||
// };
|
||||
// http_request(req)
|
||||
// }
|
||||
// _ => Err(payment_adapter::Error::UnknownOperation(name.to_string())),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[tracing::instrument]
|
||||
fn host_call(binding: &str, name: &str, payload: &[u8]) -> Result<Vec<u8>, payment_adapter::Error> {
|
||||
match name {
|
||||
"http_req" => {
|
||||
let req: HttpRequest = match wapc_codec::messagepack::deserialize(payload) {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
tracing::error!("{:?} payload {:?}", binding, payload);
|
||||
tracing::error!("Failed to deserialize. {}", e);
|
||||
return Err(payment_adapter::Error::MsgPackDeserializationFailed);
|
||||
}
|
||||
};
|
||||
http_request(req)
|
||||
}
|
||||
_ => Err(payment_adapter::Error::UnknownOperation(name.to_string())),
|
||||
}
|
||||
}
|
||||
// fn http_request(req: HttpRequest) -> Result<Vec<u8>, payment_adapter::Error>
|
||||
// { tracing::info!("{:?}", req);
|
||||
// let HttpRequest {
|
||||
// method,
|
||||
// url,
|
||||
// headers,
|
||||
// bearer_auth,
|
||||
// body,
|
||||
// } = req;
|
||||
// let client = reqwest::blocking::ClientBuilder::default()
|
||||
// .user_agent("curl/7.82.0")
|
||||
// // Do not follow redirect!
|
||||
// .redirect(reqwest::redirect::Policy::none())
|
||||
// .connection_verbose(true)
|
||||
// .build()
|
||||
// .expect("Failed to create client");
|
||||
//
|
||||
// let headers = {
|
||||
// let len = headers.len();
|
||||
// headers
|
||||
// .into_iter()
|
||||
// .fold(HeaderMap::with_capacity(len), |mut m, (k, v)| {
|
||||
// if let (Ok(v), Ok(k)) =
|
||||
// (HeaderValue::from_str(v.as_str()),
|
||||
// HeaderName::from_str(&k)) {
|
||||
// m.insert(k, v);
|
||||
// }
|
||||
// m
|
||||
// })
|
||||
// };
|
||||
//
|
||||
// let response = match (method, bearer_auth) {
|
||||
// (HttpMethod::Get, Some(bearer)) => client
|
||||
// .get(url)
|
||||
// .headers(headers)
|
||||
// .bearer_auth(bearer)
|
||||
// .send()
|
||||
// .map_err(|e| {
|
||||
// tracing::error!("{}", e);
|
||||
// payment_adapter::Error::HttpFailed
|
||||
// })?,
|
||||
// (HttpMethod::Get, _) =>
|
||||
// client.get(url).headers(headers).send().map_err(|e| {
|
||||
// tracing::error!("{}", e); payment_adapter::Error::HttpFailed
|
||||
// })?,
|
||||
// (HttpMethod::Post, Some(bearer)) => client
|
||||
// .post(url)
|
||||
// .headers(headers)
|
||||
// .bearer_auth(bearer)
|
||||
// .body(body.unwrap_or_default())
|
||||
// .send()
|
||||
// .map_err(|e| {
|
||||
// tracing::error!("{}", e);
|
||||
// payment_adapter::Error::HttpFailed
|
||||
// })?,
|
||||
// (HttpMethod::Post, None) => client
|
||||
// .post(url)
|
||||
// .headers(headers)
|
||||
// .body(body.unwrap_or_default())
|
||||
// .send()
|
||||
// .map_err(|e| {
|
||||
// eprintln!("POST NONE {}", e);
|
||||
// tracing::error!("{}", e);
|
||||
// payment_adapter::Error::HttpFailed
|
||||
// })?,
|
||||
// };
|
||||
// tracing::info!("HTTP response status {:?}", response.status());
|
||||
// let text = response.bytes().map_err(|e| {
|
||||
// tracing::error!("{}", e);
|
||||
// payment_adapter::Error::HttpFailed
|
||||
// })?;
|
||||
// tracing::info!("HTTP Result {:?}", text);
|
||||
// Ok(text.to_vec())
|
||||
// }
|
||||
|
||||
#[tracing::instrument]
|
||||
fn http_request(req: HttpRequest) -> Result<Vec<u8>, payment_adapter::Error> {
|
||||
tracing::info!("{:?}", req);
|
||||
let HttpRequest {
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
bearer_auth,
|
||||
body,
|
||||
} = req;
|
||||
let client = reqwest::blocking::ClientBuilder::default()
|
||||
.user_agent("curl/7.82.0")
|
||||
// Do not follow redirect!
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.connection_verbose(true)
|
||||
.build()
|
||||
.expect("Failed to create client");
|
||||
|
||||
let headers = {
|
||||
let len = headers.len();
|
||||
headers
|
||||
.into_iter()
|
||||
.fold(HeaderMap::with_capacity(len), |mut m, (k, v)| {
|
||||
if let (Ok(v), Ok(k)) =
|
||||
(HeaderValue::from_str(v.as_str()), HeaderName::from_str(&k))
|
||||
{
|
||||
m.insert(k, v);
|
||||
}
|
||||
m
|
||||
})
|
||||
};
|
||||
|
||||
let response = match (method, bearer_auth) {
|
||||
(HttpMethod::Get, Some(bearer)) => client
|
||||
.get(url)
|
||||
.headers(headers)
|
||||
.bearer_auth(bearer)
|
||||
.send()
|
||||
.map_err(|e| {
|
||||
tracing::error!("{}", e);
|
||||
payment_adapter::Error::HttpFailed
|
||||
})?,
|
||||
(HttpMethod::Get, _) => client.get(url).headers(headers).send().map_err(|e| {
|
||||
tracing::error!("{}", e);
|
||||
payment_adapter::Error::HttpFailed
|
||||
})?,
|
||||
(HttpMethod::Post, Some(bearer)) => client
|
||||
.post(url)
|
||||
.headers(headers)
|
||||
.bearer_auth(bearer)
|
||||
.body(body.unwrap_or_default())
|
||||
.send()
|
||||
.map_err(|e| {
|
||||
tracing::error!("{}", e);
|
||||
payment_adapter::Error::HttpFailed
|
||||
})?,
|
||||
(HttpMethod::Post, None) => client
|
||||
.post(url)
|
||||
.headers(headers)
|
||||
.body(body.unwrap_or_default())
|
||||
.send()
|
||||
.map_err(|e| {
|
||||
eprintln!("POST NONE {}", e);
|
||||
tracing::error!("{}", e);
|
||||
payment_adapter::Error::HttpFailed
|
||||
})?,
|
||||
};
|
||||
tracing::info!("HTTP response status {:?}", response.status());
|
||||
let text = response.bytes().map_err(|e| {
|
||||
tracing::error!("{}", e);
|
||||
payment_adapter::Error::HttpFailed
|
||||
})?;
|
||||
tracing::info!("HTTP Result {:?}", text);
|
||||
Ok(text.to_vec())
|
||||
}
|
||||
fn main() {}
|
||||
|
@ -7,7 +7,7 @@ use config::SharedAppConfig;
|
||||
use payment_adapter::RefundPayment;
|
||||
use tarpc::context;
|
||||
|
||||
use crate::{Modules, ResultMsg, WasmMsg};
|
||||
use crate::{Modules, ResultMsg};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ModuleSender {
|
||||
@ -59,7 +59,7 @@ impl PaymentsServer {
|
||||
.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[tarpc::server]
|
||||
impl Payments for PaymentsServer {
|
||||
async fn start_payment(
|
||||
@ -156,3 +156,4 @@ pub async fn start(config: SharedAppConfig, mqtt_client: AsyncClient, modules: M
|
||||
})
|
||||
.await;
|
||||
}
|
||||
*/
|
||||
|
@ -13,6 +13,7 @@ rustflags = ["-C", "prefer-dynamic", "-C", "rpath"]
|
||||
payment_adapter = { path = "../payment_adapter" }
|
||||
fulfillment_adapter = { path = "../fulfillment_adapter" }
|
||||
tokio = { version = "1.27.0" }
|
||||
payup = { version = "*" }
|
||||
payup = { version = "*", features = [] }
|
||||
tracing = { version = "0.1.37" }
|
||||
async-trait = { version = "0.1.68" }
|
||||
serde = { version = "1.0.162", features = ['derive'] }
|
||||
|
@ -1,126 +1,242 @@
|
||||
#![crate_type = "rlib"]
|
||||
|
||||
use fulfillment_adapter::{
|
||||
FError, FResult, FulfillmentAdapter, FulfillmentCart, FulfillmentOption, FulfillmentPayload,
|
||||
};
|
||||
use payment_adapter::{
|
||||
CreatePayment, DeletePayment, Deleted, PResult, PaymentAdapter, PaymentCreated,
|
||||
PaymentSessionStatus, RefundPayment, Refunded, Status,
|
||||
};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
pub struct StripeAdapter {
|
||||
stripe: payup::stripe::Auth,
|
||||
use fulfillment_adapter::*;
|
||||
use payment_adapter::*;
|
||||
use payup::stripe;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub enum CaptureMethod {
|
||||
Automatic,
|
||||
Manual,
|
||||
}
|
||||
|
||||
impl StripeAdapter {
|
||||
fn payment_status(&mut self, ext_id: String) -> FResult<PaymentSessionStatus> {
|
||||
let charge = payup::stripe::Charge::get(self.stripe.clone(), ext_id).map_err(|e| {
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub enum SetupFutureUsage {
|
||||
OnSession,
|
||||
OffSession,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct StripeConfig {
|
||||
pub api_key: String,
|
||||
pub client: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StripeIntent {
|
||||
pub capture_method: CaptureMethod,
|
||||
pub setup_future_usage: SetupFutureUsage,
|
||||
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)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::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 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,
|
||||
})
|
||||
}
|
||||
|
||||
#[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.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 {
|
||||
Some(c) if !c.has_stripe_id() => Some(UpdateRequests {
|
||||
customer_metadata: Some(CustomerMetadata(HashMap::from([(
|
||||
"id".to_string(),
|
||||
AnyData(change.customer.unwrap_or_default().into_bytes()),
|
||||
)]))),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(PaymentProcessorSessionResponse {
|
||||
update_requests,
|
||||
session_data: payment_session_data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PaymentAdapter for StripeAdapter {
|
||||
fn init(&mut self) -> PResult<Status> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn teardown(&mut self) -> PResult<Status> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn create_payment(&mut self, msg: CreatePayment) -> PResult<PaymentCreated> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn retrieve_payment(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn authorize_payment(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn capture_payment(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn refund_payment(&mut self, msg: RefundPayment) -> PResult<Refunded> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn delete_payment(&mut self, msg: DeletePayment) -> PResult<Deleted> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl FulfillmentAdapter for StripeAdapter {
|
||||
fn identifier() -> &'static str {
|
||||
"stripe"
|
||||
}
|
||||
|
||||
fn fulfillment_options(&mut self) -> FResult<&[FulfillmentOption]> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn validate_fulfillment_payload<P: FulfillmentPayload, C: FulfillmentCart>(
|
||||
async fn update_payment(
|
||||
&mut self,
|
||||
payload: P,
|
||||
cart: C,
|
||||
) -> FResult<bool> {
|
||||
ctx: PaymentProcessorContext,
|
||||
) -> PResult<Option<PaymentProcessorSessionResponse>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn validate_option<P: FulfillmentPayload>(&mut self, payload: P) -> FResult<bool> {
|
||||
async fn refund_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
refund_amount: Amount,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn can_calculate<P: FulfillmentPayload>(&mut self, payload: P) -> FResult<bool> {
|
||||
async fn authorize_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
data: PaymentProcessCtx,
|
||||
) -> PResult<(PaymentSessionStatus, PaymentSessionData)> {
|
||||
self.payment_status(&payment_session_data)
|
||||
.await
|
||||
.map(|status| (status, payment_session_data))
|
||||
}
|
||||
|
||||
async fn capture_payment(
|
||||
&mut self,
|
||||
mut payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
let id = payment_session_data
|
||||
.id
|
||||
.clone()
|
||||
.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)
|
||||
})?;
|
||||
payment_session_data.id = charge.id;
|
||||
Ok(payment_session_data)
|
||||
}
|
||||
|
||||
async fn delete_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn calculate_price<P: FulfillmentPayload, C: FulfillmentCart>(
|
||||
data: P,
|
||||
cart: C,
|
||||
) -> FResult<u64> {
|
||||
async fn retrieve_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn create_fulfillment(&mut self) {
|
||||
async fn cancel_payment(
|
||||
&mut self,
|
||||
payment_session_data: PaymentSessionData,
|
||||
) -> PResult<PaymentSessionData> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn cancel_fulfillment(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn fulfillment_documents(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn create_return(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn return_documents(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn shipment_documents(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn documents(&mut self) {
|
||||
todo!()
|
||||
async fn payment_status(
|
||||
&mut self,
|
||||
payment_session_data: &PaymentSessionData,
|
||||
) -> PResult<PaymentSessionStatus> {
|
||||
payment_status(
|
||||
self.stripe.clone(),
|
||||
payment_session_data.id.clone().unwrap_or_default(),
|
||||
)
|
||||
.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>;
|
||||
}
|
||||
|
||||
impl StripeCustomerMetadata for Customer {
|
||||
fn has_stripe_id(&self) -> bool {
|
||||
self.metadata
|
||||
.as_ref()
|
||||
.map_or_else(|| false, |h| h.contains_key("stripe_id"))
|
||||
}
|
||||
|
||||
fn stripe_id(&self) -> Option<String> {
|
||||
self.metadata
|
||||
.as_ref()?
|
||||
.get("stripe_id")
|
||||
.and_then(|v| String::from_utf8(v.clone()).ok())
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user