feat(braintree): Sessions flow for braintree and klarna (#121)

This commit is contained in:
Narayan Bhat
2022-12-13 12:35:17 +05:30
committed by GitHub
parent eb4fe6f46b
commit 7dca6f1e5a
19 changed files with 606 additions and 31 deletions

View File

@ -61,6 +61,8 @@ base_url = "https://api.stripe.com/"
[connectors.braintree]
base_url = "https://api.sandbox.braintreegateway.com/"
[connectors.klarna]
base_url = "https://api-na.playground.klarna.com/"
[scheduler]
stream = "SCHEDULER_STREAM"

View File

@ -113,6 +113,15 @@ base_url = "https://api.stripe.com/"
[connectors.braintree]
base_url = "https://api.sandbox.braintreegateway.com/"
[connectors.klarna]
base_url = "https://api-na.playground.klarna.com/"
# This data is used to call respective connectors for wallets and cards
[connectors.supported]
wallets = ["klarna","braintree"]
cards = ["stripe","adyen","authorizedotnet","checkout","braintree"]
# Scheduler settings provides a point to modify the behaviour of scheduler flow.
# It defines the the streams/queues name and configuration as well as event selection variables
[scheduler]

View File

@ -66,4 +66,12 @@ base_url = "https://api.sandbox.checkout.com/"
base_url = "https://api.stripe.com/"
[connectors.braintree]
base_url = "https://api.sandbox.braintreegateway.com/"
base_url = "https://api.sandbox.braintreegateway.com/"
[connectors.klarna]
base_url = "https://api-na.playground.klarna.com/"
[connectors.supported]
wallets = ["klarna","braintree"]
cards = ["stripe","adyen","authorizedotnet","checkout","braintree"]

View File

@ -532,13 +532,8 @@ impl From<PaymentsStartRequest> for PaymentsResponse {
impl From<PaymentsSessionRequest> for PaymentsResponse {
fn from(item: PaymentsSessionRequest) -> Self {
let payment_id = match item.payment_id {
PaymentIdType::PaymentIntentId(id) => Some(id),
_ => None,
};
Self {
payment_id,
payment_id: Some(item.payment_id),
..Default::default()
}
}
@ -664,11 +659,12 @@ pub struct PaymentsRetrieveRequest {
pub struct ConnectorSessionToken {
pub connector_name: String,
pub session_token: String,
pub session_id: Option<String>,
}
#[derive(Default, Debug, serde::Deserialize, Clone)]
pub struct PaymentsSessionRequest {
pub payment_id: PaymentIdType,
pub payment_id: String,
pub client_secret: String,
}

View File

@ -85,3 +85,20 @@ pub mod iso8601 {
}
}
}
/// https://github.com/serde-rs/serde/issues/994#issuecomment-316895860
pub mod json_string {
use serde::de::{self, Deserialize, DeserializeOwned, Deserializer};
use serde_json;
/// Deserialize a string which is in json format
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: DeserializeOwned,
D: Deserializer<'de>,
{
let j = String::deserialize(deserializer)?;
serde_json::from_str(&j).map_err(de::Error::custom)
}
}

View File

@ -112,6 +112,7 @@ pub struct Connectors {
pub checkout: ConnectorParams,
pub stripe: ConnectorParams,
pub braintree: ConnectorParams,
pub klarna: ConnectorParams,
pub supported: SupportedConnectors,
}
@ -173,7 +174,8 @@ impl Settings {
.try_parsing(true)
.separator("__")
.list_separator(",")
.with_list_parse_key("redis.cluster_urls"),
.with_list_parse_key("redis.cluster_urls")
.with_list_parse_key("connectors.supported.wallets"),
)
.build()?;

View File

@ -3,9 +3,10 @@ pub mod adyen;
pub mod authorizedotnet;
pub mod braintree;
pub mod checkout;
pub mod klarna;
pub mod stripe;
pub use self::{
aci::Aci, adyen::Adyen, authorizedotnet::Authorizedotnet, braintree::Braintree,
checkout::Checkout, stripe::Stripe,
checkout::Checkout, klarna::Klarna, stripe::Stripe,
};

View File

@ -61,7 +61,99 @@ impl
types::PaymentsResponseData,
> for Braintree
{
//TODO: implement sessions flow
fn get_headers(
&self,
req: &types::PaymentsSessionRouterData,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut headers = vec![
(
headers::CONTENT_TYPE.to_string(),
types::PaymentsSessionType::get_content_type(self).to_string(),
),
(headers::X_ROUTER.to_string(), "test".to_string()),
(headers::X_API_VERSION.to_string(), "6".to_string()),
(headers::ACCEPT.to_string(), "application/json".to_string()),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
headers.append(&mut api_key);
Ok(headers)
}
fn get_content_type(&self) -> &'static str {
"application/json"
}
fn get_url(
&self,
req: &types::PaymentsSessionRouterData,
connectors: Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let auth_type = braintree::BraintreeAuthType::try_from(&req.connector_auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(format!(
"{}/merchants/{}/client_token",
self.base_url(connectors),
auth_type.merchant_account,
))
}
fn build_request(
&self,
req: &types::PaymentsSessionRouterData,
connectors: Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsSessionType::get_url(self, req, connectors)?)
.headers(types::PaymentsSessionType::get_headers(self, req)?)
.body(types::PaymentsSessionType::get_request_body(self, req)?)
.build(),
);
logger::debug!(session_request=?request);
Ok(request)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: braintree::ErrorResponse = res
.parse_struct("Error Response")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
code: consts::NO_ERROR_CODE.to_string(),
message: response.api_error_response.message,
reason: None,
})
}
fn get_request_body(
&self,
_req: &types::PaymentsSessionRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
Ok(None)
}
fn handle_response(
&self,
data: &types::PaymentsSessionRouterData,
res: Response,
) -> CustomResult<types::PaymentsSessionRouterData, errors::ConnectorError> {
logger::debug!(payment_session_response_braintree=?res);
let response: braintree::BraintreeSessionTokenResponse = res
.response
.parse_struct("braintree SessionTokenReponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
}
impl api::PreVerify for Braintree {}

View File

@ -154,12 +154,54 @@ impl<F, T>
}
}
#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)]
impl<F, T>
TryFrom<
types::ResponseRouterData<F, BraintreeSessionTokenResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
BraintreeSessionTokenResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(types::RouterData {
response: Ok(types::PaymentsResponseData::SessionResponse {
session_token: item.response.client_token.value.authorization_fingerprint,
session_id: None,
}),
..item.data
})
}
}
#[derive(Default, Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BraintreePaymentsResponse {
transaction: TransactionResponse,
}
#[derive(Default, Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationFingerprint {
authorization_fingerprint: String,
}
#[derive(Default, Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientToken {
#[serde(with = "common_utils::custom_serde::json_string")]
pub value: AuthorizationFingerprint,
}
#[derive(Default, Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BraintreeSessionTokenResponse {
pub client_token: ClientToken,
}
#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TransactionResponse {

View File

@ -0,0 +1,241 @@
#![allow(dead_code)]
mod transformers;
use std::fmt::Debug;
use bytes::Bytes;
use error_stack::{IntoReport, ResultExt};
use transformers as klarna;
use crate::{
configs::settings::Connectors,
core::errors::{self, CustomResult},
headers,
services::{self, logger},
types::{
self,
api::{self, ConnectorCommon},
ErrorResponse, Response,
},
utils::{self, BytesExt},
};
#[derive(Debug, Clone)]
pub struct Klarna;
impl api::ConnectorCommon for Klarna {
fn id(&self) -> &'static str {
"klarna"
}
fn common_get_content_type(&self) -> &'static str {
"application/x-www-form-urlencoded"
}
fn base_url(&self, connectors: Connectors) -> String {
connectors.klarna.base_url
}
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let auth: klarna::KlarnaAuthType = auth_type
.try_into()
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(headers::AUTHORIZATION.to_string(), auth.basic_token)])
}
}
impl api::Payment for Klarna {}
impl api::PaymentAuthorize for Klarna {}
impl api::PaymentSync for Klarna {}
impl api::PaymentVoid for Klarna {}
impl api::PaymentCapture for Klarna {}
impl api::PaymentSession for Klarna {}
impl
services::ConnectorIntegration<
api::Session,
types::PaymentsSessionData,
types::PaymentsResponseData,
> for Klarna
{
fn get_headers(
&self,
req: &types::PaymentsSessionRouterData,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut header = vec![
(
headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
),
(headers::X_ROUTER.to_string(), "test".to_string()),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::PaymentsSessionRouterData,
connectors: Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}{}",
self.base_url(connectors),
"payments/v1/sessions"
))
}
fn get_request_body(
&self,
req: &types::PaymentsSessionRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
// encode only for for urlencoded things.
let klarna_req = utils::Encode::<klarna::KlarnaSessionRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
logger::debug!(klarna_payment_logs=?klarna_req);
Ok(Some(klarna_req))
}
fn build_request(
&self,
req: &types::PaymentsSessionRouterData,
connectors: Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsSessionType::get_url(self, req, connectors)?)
.headers(types::PaymentsSessionType::get_headers(self, req)?)
.header(headers::X_ROUTER, "test")
.body(types::PaymentsSessionType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsSessionRouterData,
res: Response,
) -> CustomResult<types::PaymentsSessionRouterData, errors::ConnectorError> {
let response: klarna::KlarnaSessionResponse = res
.response
.parse_struct("KlarnaPaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: klarna::KlarnaErrorResponse = res
.parse_struct("KlarnaErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
code: response.error_code,
message: response.error_messages.join(" & "),
reason: None,
})
}
}
impl api::PreVerify for Klarna {}
impl
services::ConnectorIntegration<
api::Verify,
types::VerifyRequestData,
types::PaymentsResponseData,
> for Klarna
{
// TODO: Critical Implement
}
impl
services::ConnectorIntegration<
api::Capture,
types::PaymentsCaptureData,
types::PaymentsResponseData,
> for Klarna
{
// Not Implemented (R)
}
impl
services::ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Klarna
{
// Not Implemented (R)
}
impl
services::ConnectorIntegration<
api::Authorize,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
> for Klarna
{
//Not Implemented (R)
}
impl
services::ConnectorIntegration<
api::Void,
types::PaymentsCancelData,
types::PaymentsResponseData,
> for Klarna
{
}
impl api::Refund for Klarna {}
impl api::RefundExecute for Klarna {}
impl api::RefundSync for Klarna {}
impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
for Klarna
{
}
impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
for Klarna
{
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Klarna {
fn get_webhook_object_reference_id(
&self,
_body: &[u8],
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
fn get_webhook_event_type(
&self,
_body: &[u8],
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
fn get_webhook_resource_object(
&self,
_body: &[u8],
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
}
impl services::ConnectorRedirectResponse for Klarna {}

View File

@ -0,0 +1,121 @@
use serde::{Deserialize, Serialize};
use crate::{
core::errors,
types::{self, storage::enums},
};
#[derive(Default, Debug, Serialize)]
pub struct KlarnaPaymentsRequest {}
#[derive(Serialize)]
pub struct KlarnaSessionRequest {
intent: KlarnaSessionIntent,
purchase_country: String,
purchase_currency: enums::Currency,
locale: String,
order_amount: i32,
order_lines: Vec<OrderLines>,
}
#[derive(Deserialize)]
pub struct KlarnaSessionResponse {
pub client_token: String,
pub session_id: String,
}
impl TryFrom<&types::PaymentsSessionRouterData> for KlarnaSessionRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsSessionRouterData) -> Result<Self, Self::Error> {
let request = &item.request;
Ok(Self {
intent: KlarnaSessionIntent::Buy,
purchase_country: "US".to_string(),
purchase_currency: request.currency,
order_amount: request.amount,
locale: "en-US".to_string(),
order_lines: vec![OrderLines {
name: "Battery Power Pack".to_string(),
quantity: 1,
unit_price: request.amount,
total_amount: request.amount,
}],
})
}
}
impl TryFrom<types::PaymentsSessionResponseRouterData<KlarnaSessionResponse>>
for types::PaymentsSessionRouterData
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::PaymentsSessionResponseRouterData<KlarnaSessionResponse>,
) -> Result<Self, Self::Error> {
let response = &item.response;
Ok(types::RouterData {
response: Ok(types::PaymentsResponseData::SessionResponse {
session_id: Some(response.session_id.clone()),
session_token: response.client_token.clone(),
}),
..item.data
})
}
}
#[derive(Serialize)]
pub struct OrderLines {
name: String,
quantity: u64,
unit_price: i32,
total_amount: i32,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub enum KlarnaSessionIntent {
Buy,
Tokenize,
BuyAndTokenize,
}
pub struct KlarnaAuthType {
pub basic_token: String,
}
impl TryFrom<&types::ConnectorAuthType> for KlarnaAuthType {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
if let types::ConnectorAuthType::HeaderKey { api_key } = auth_type {
Ok(Self {
basic_token: api_key.to_string(),
})
} else {
Err(errors::ConnectorError::FailedToObtainAuthType.into())
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KlarnaPaymentStatus {
Succeeded,
Failed,
#[default]
Processing,
}
impl From<KlarnaPaymentStatus> for enums::AttemptStatus {
fn from(item: KlarnaPaymentStatus) -> Self {
match item {
KlarnaPaymentStatus::Succeeded => enums::AttemptStatus::Charged,
KlarnaPaymentStatus::Failed => enums::AttemptStatus::Failure,
KlarnaPaymentStatus::Processing => enums::AttemptStatus::Authorizing,
}
}
}
#[derive(Deserialize)]
pub struct KlarnaErrorResponse {
pub error_code: String,
pub error_messages: Vec<String>,
}

View File

@ -398,16 +398,19 @@ where
CallConnectorAction::Trigger,
merchant_account.storage_scheme,
)
.await?;
.await?; //FIXME: remove this error propogation
match res.response {
Ok(connector_response) => {
if let types::PaymentsResponseData::SessionResponse { session_token } =
connector_response
if let types::PaymentsResponseData::SessionResponse {
session_token,
session_id,
} = connector_response
{
payment_data
.sessions_token
.push(api::ConnectorSessionToken {
session_id,
connector_name,
session_token,
});
@ -558,6 +561,7 @@ pub fn should_call_connector<Op: Debug, F: Clone>(
enums::IntentStatus::RequiresCapture
)
}
"PaymentSession" => true,
_ => false,
}
}

View File

@ -115,6 +115,14 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>
.attach_printable("Database error when finding connector response")
})?;
let customer_details = payments::CustomerDetails {
customer_id: payment_intent.customer_id.clone(),
name: None,
email: None,
phone: None,
phone_country_code: None,
};
Ok((
Box::new(self),
PaymentData {
@ -137,7 +145,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>
sessions_token: vec![],
connector_response,
},
None,
Some(customer_details),
))
}
}
@ -174,10 +182,7 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsSessionRequest> for Paymen
operations::ValidateResult<'a>,
)> {
//paymentid is already generated and should be sent in the request
let given_payment_id = request
.payment_id
.get_payment_intent_id()
.change_context(errors::ApiErrorResponse::PaymentNotFound)?;
let given_payment_id = request.payment_id.clone();
Ok((
Box::new(self),

View File

@ -459,8 +459,11 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsCancelData {
impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsSessionData {
type Error = errors::ApiErrorResponse;
fn try_from(_payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
Ok(Self {})
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
Ok(Self {
amount: payment_data.amount.into(),
currency: payment_data.currency,
})
}
}

View File

@ -59,6 +59,9 @@ impl Payments {
.app_data(web::Data::new(state))
.service(web::resource("").route(web::post().to(payments_create)))
.service(web::resource("/list").route(web::get().to(payments_list)))
.service(
web::resource("/session_tokens").route(web::get().to(payments_connector_session)),
)
.service(
web::resource("/{payment_id}")
.route(web::get().to(payments_retrieve))
@ -75,9 +78,6 @@ impl Payments {
web::resource("/{payment_id}/{merchant_id}/response/{connector}")
.route(web::get().to(payments_response)),
)
.service(
web::resource("/session_tokens").route(web::get().to(payments_connector_session)),
)
}
}

View File

@ -36,6 +36,9 @@ pub type PaymentsCancelResponseRouterData<R> =
ResponseRouterData<api::Void, R, PaymentsCancelData, PaymentsResponseData>;
pub type PaymentsSyncResponseRouterData<R> =
ResponseRouterData<api::PSync, R, PaymentsSyncData, PaymentsResponseData>;
pub type PaymentsSessionResponseRouterData<R> =
ResponseRouterData<api::Session, R, PaymentsSessionData, PaymentsResponseData>;
pub type RefundsResponseRouterData<F, R> =
ResponseRouterData<F, R, RefundsData, RefundsResponseData>;
@ -51,6 +54,8 @@ pub type RefundExecuteType =
dyn services::ConnectorIntegration<api::Execute, RefundsData, RefundsResponseData>;
pub type RefundSyncType =
dyn services::ConnectorIntegration<api::RSync, RefundsData, RefundsResponseData>;
pub type PaymentsSessionType =
dyn services::ConnectorIntegration<api::Session, PaymentsSessionData, PaymentsResponseData>;
pub type VerifyRouterData = RouterData<api::Verify, VerifyRequestData, PaymentsResponseData>;
@ -118,12 +123,15 @@ pub struct PaymentsCancelData {
#[derive(Debug, Clone)]
pub struct PaymentsSessionData {
//TODO: Add the fields here as required
pub amount: i32,
pub currency: storage_enums::Currency,
}
#[derive(serde::Serialize, Debug)]
pub struct PaymentsSessionResponseData {
pub client_token: Option<String>,
#[derive(Debug, Clone)]
pub struct ConnectorSessionToken {
pub connector_name: String,
pub session_id: Option<String>,
pub session_token: String,
}
#[derive(Debug, Clone)]
@ -137,6 +145,19 @@ pub struct VerifyRequestData {
pub setup_mandate_details: Option<payments::MandateData>,
}
#[derive(Debug, Clone)]
pub struct PaymentsTransactionResponse {
pub resource_id: ResponseId,
pub redirection_data: Option<services::RedirectForm>,
pub redirect: bool,
}
#[derive(Debug, Clone)]
pub struct PaymentsSessionResponse {
pub session_id: Option<String>,
pub session_token: String,
}
#[derive(Debug, Clone)]
pub enum PaymentsResponseData {
TransactionResponse {
@ -147,6 +168,7 @@ pub enum PaymentsResponseData {
},
SessionResponse {
session_token: String,
session_id: Option<String>,
},
}

View File

@ -83,7 +83,7 @@ impl ConnectorData {
let connector_name = types::Connector::from_str(name)
.into_report()
.change_context(errors::ConnectorError::InvalidConnectorName)
.attach_printable_lazy(|| format!("unable to parse connector name {:?}", connector))
.attach_printable_lazy(|| format!("unable to parse connector name {connector:?}"))
.change_context(errors::ApiErrorResponse::InternalServerError)?;
Ok(ConnectorData {
connector,
@ -102,6 +102,7 @@ impl ConnectorData {
"checkout" => Ok(Box::new(&connector::Checkout)),
"authorizedotnet" => Ok(Box::new(&connector::Authorizedotnet)),
"braintree" => Ok(Box::new(&connector::Braintree)),
"klarna" => Ok(Box::new(&connector::Klarna)),
_ => Err(report!(errors::UnexpectedError)
.attach_printable(format!("invalid connector name: {connector_name}")))
.change_context(errors::ConnectorError::InvalidConnectorName)

View File

@ -7,6 +7,7 @@ pub enum Connector {
Aci,
Authorizedotnet,
Braintree,
Klarna,
#[default]
Dummy,
}

View File

@ -56,4 +56,12 @@ base_url = "https://api.sandbox.checkout.com/"
base_url = "http://stripe-mock:12111/"
[connectors.braintree]
base_url = "https://api.sandbox.braintreegateway.com/"
base_url = "https://api.sandbox.braintreegateway.com/"
[connectors.klarna]
base_url = "https://api-na.playground.klarna.com/"
[connectors.supported]
wallets = ["klarna","braintree"]
cards = ["stripe","adyen","authorizedotnet","checkout","braintree"]