mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 17:47:54 +08:00
feat(braintree): Sessions flow for braintree and klarna (#121)
This commit is contained in:
@ -61,6 +61,8 @@ base_url = "https://api.stripe.com/"
|
|||||||
[connectors.braintree]
|
[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/"
|
||||||
|
|
||||||
[scheduler]
|
[scheduler]
|
||||||
stream = "SCHEDULER_STREAM"
|
stream = "SCHEDULER_STREAM"
|
||||||
|
|||||||
@ -113,6 +113,15 @@ base_url = "https://api.stripe.com/"
|
|||||||
[connectors.braintree]
|
[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/"
|
||||||
|
|
||||||
|
# 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.
|
# 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
|
# It defines the the streams/queues name and configuration as well as event selection variables
|
||||||
[scheduler]
|
[scheduler]
|
||||||
|
|||||||
@ -67,3 +67,11 @@ base_url = "https://api.stripe.com/"
|
|||||||
|
|
||||||
[connectors.braintree]
|
[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"]
|
||||||
|
|
||||||
|
|||||||
@ -532,13 +532,8 @@ impl From<PaymentsStartRequest> for PaymentsResponse {
|
|||||||
|
|
||||||
impl From<PaymentsSessionRequest> for PaymentsResponse {
|
impl From<PaymentsSessionRequest> for PaymentsResponse {
|
||||||
fn from(item: PaymentsSessionRequest) -> Self {
|
fn from(item: PaymentsSessionRequest) -> Self {
|
||||||
let payment_id = match item.payment_id {
|
|
||||||
PaymentIdType::PaymentIntentId(id) => Some(id),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
payment_id,
|
payment_id: Some(item.payment_id),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -664,11 +659,12 @@ pub struct PaymentsRetrieveRequest {
|
|||||||
pub struct ConnectorSessionToken {
|
pub struct ConnectorSessionToken {
|
||||||
pub connector_name: String,
|
pub connector_name: String,
|
||||||
pub session_token: String,
|
pub session_token: String,
|
||||||
|
pub session_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, serde::Deserialize, Clone)]
|
#[derive(Default, Debug, serde::Deserialize, Clone)]
|
||||||
pub struct PaymentsSessionRequest {
|
pub struct PaymentsSessionRequest {
|
||||||
pub payment_id: PaymentIdType,
|
pub payment_id: String,
|
||||||
pub client_secret: String,
|
pub client_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -112,6 +112,7 @@ pub struct Connectors {
|
|||||||
pub checkout: ConnectorParams,
|
pub checkout: ConnectorParams,
|
||||||
pub stripe: ConnectorParams,
|
pub stripe: ConnectorParams,
|
||||||
pub braintree: ConnectorParams,
|
pub braintree: ConnectorParams,
|
||||||
|
pub klarna: ConnectorParams,
|
||||||
pub supported: SupportedConnectors,
|
pub supported: SupportedConnectors,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +174,8 @@ impl Settings {
|
|||||||
.try_parsing(true)
|
.try_parsing(true)
|
||||||
.separator("__")
|
.separator("__")
|
||||||
.list_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()?;
|
.build()?;
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,10 @@ pub mod adyen;
|
|||||||
pub mod authorizedotnet;
|
pub mod authorizedotnet;
|
||||||
pub mod braintree;
|
pub mod braintree;
|
||||||
pub mod checkout;
|
pub mod checkout;
|
||||||
|
pub mod klarna;
|
||||||
pub mod stripe;
|
pub mod stripe;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
aci::Aci, adyen::Adyen, authorizedotnet::Authorizedotnet, braintree::Braintree,
|
aci::Aci, adyen::Adyen, authorizedotnet::Authorizedotnet, braintree::Braintree,
|
||||||
checkout::Checkout, stripe::Stripe,
|
checkout::Checkout, klarna::Klarna, stripe::Stripe,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -61,7 +61,99 @@ impl
|
|||||||
types::PaymentsResponseData,
|
types::PaymentsResponseData,
|
||||||
> for Braintree
|
> 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 {}
|
impl api::PreVerify for Braintree {}
|
||||||
|
|||||||
@ -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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BraintreePaymentsResponse {
|
pub struct BraintreePaymentsResponse {
|
||||||
transaction: TransactionResponse,
|
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)]
|
#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct TransactionResponse {
|
pub struct TransactionResponse {
|
||||||
|
|||||||
241
crates/router/src/connector/klarna.rs
Normal file
241
crates/router/src/connector/klarna.rs
Normal 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 {}
|
||||||
121
crates/router/src/connector/klarna/transformers.rs
Normal file
121
crates/router/src/connector/klarna/transformers.rs
Normal 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>,
|
||||||
|
}
|
||||||
@ -398,16 +398,19 @@ where
|
|||||||
CallConnectorAction::Trigger,
|
CallConnectorAction::Trigger,
|
||||||
merchant_account.storage_scheme,
|
merchant_account.storage_scheme,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?; //FIXME: remove this error propogation
|
||||||
|
|
||||||
match res.response {
|
match res.response {
|
||||||
Ok(connector_response) => {
|
Ok(connector_response) => {
|
||||||
if let types::PaymentsResponseData::SessionResponse { session_token } =
|
if let types::PaymentsResponseData::SessionResponse {
|
||||||
connector_response
|
session_token,
|
||||||
|
session_id,
|
||||||
|
} = connector_response
|
||||||
{
|
{
|
||||||
payment_data
|
payment_data
|
||||||
.sessions_token
|
.sessions_token
|
||||||
.push(api::ConnectorSessionToken {
|
.push(api::ConnectorSessionToken {
|
||||||
|
session_id,
|
||||||
connector_name,
|
connector_name,
|
||||||
session_token,
|
session_token,
|
||||||
});
|
});
|
||||||
@ -558,6 +561,7 @@ pub fn should_call_connector<Op: Debug, F: Clone>(
|
|||||||
enums::IntentStatus::RequiresCapture
|
enums::IntentStatus::RequiresCapture
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
"PaymentSession" => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,6 +115,14 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>
|
|||||||
.attach_printable("Database error when finding connector response")
|
.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((
|
Ok((
|
||||||
Box::new(self),
|
Box::new(self),
|
||||||
PaymentData {
|
PaymentData {
|
||||||
@ -137,7 +145,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>
|
|||||||
sessions_token: vec![],
|
sessions_token: vec![],
|
||||||
connector_response,
|
connector_response,
|
||||||
},
|
},
|
||||||
None,
|
Some(customer_details),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -174,10 +182,7 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsSessionRequest> for Paymen
|
|||||||
operations::ValidateResult<'a>,
|
operations::ValidateResult<'a>,
|
||||||
)> {
|
)> {
|
||||||
//paymentid is already generated and should be sent in the request
|
//paymentid is already generated and should be sent in the request
|
||||||
let given_payment_id = request
|
let given_payment_id = request.payment_id.clone();
|
||||||
.payment_id
|
|
||||||
.get_payment_intent_id()
|
|
||||||
.change_context(errors::ApiErrorResponse::PaymentNotFound)?;
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Box::new(self),
|
Box::new(self),
|
||||||
|
|||||||
@ -459,8 +459,11 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsCancelData {
|
|||||||
impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsSessionData {
|
impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsSessionData {
|
||||||
type Error = errors::ApiErrorResponse;
|
type Error = errors::ApiErrorResponse;
|
||||||
|
|
||||||
fn try_from(_payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
|
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {})
|
Ok(Self {
|
||||||
|
amount: payment_data.amount.into(),
|
||||||
|
currency: payment_data.currency,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,9 @@ impl Payments {
|
|||||||
.app_data(web::Data::new(state))
|
.app_data(web::Data::new(state))
|
||||||
.service(web::resource("").route(web::post().to(payments_create)))
|
.service(web::resource("").route(web::post().to(payments_create)))
|
||||||
.service(web::resource("/list").route(web::get().to(payments_list)))
|
.service(web::resource("/list").route(web::get().to(payments_list)))
|
||||||
|
.service(
|
||||||
|
web::resource("/session_tokens").route(web::get().to(payments_connector_session)),
|
||||||
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{payment_id}")
|
web::resource("/{payment_id}")
|
||||||
.route(web::get().to(payments_retrieve))
|
.route(web::get().to(payments_retrieve))
|
||||||
@ -75,9 +78,6 @@ impl Payments {
|
|||||||
web::resource("/{payment_id}/{merchant_id}/response/{connector}")
|
web::resource("/{payment_id}/{merchant_id}/response/{connector}")
|
||||||
.route(web::get().to(payments_response)),
|
.route(web::get().to(payments_response)),
|
||||||
)
|
)
|
||||||
.service(
|
|
||||||
web::resource("/session_tokens").route(web::get().to(payments_connector_session)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,9 @@ pub type PaymentsCancelResponseRouterData<R> =
|
|||||||
ResponseRouterData<api::Void, R, PaymentsCancelData, PaymentsResponseData>;
|
ResponseRouterData<api::Void, R, PaymentsCancelData, PaymentsResponseData>;
|
||||||
pub type PaymentsSyncResponseRouterData<R> =
|
pub type PaymentsSyncResponseRouterData<R> =
|
||||||
ResponseRouterData<api::PSync, R, PaymentsSyncData, PaymentsResponseData>;
|
ResponseRouterData<api::PSync, R, PaymentsSyncData, PaymentsResponseData>;
|
||||||
|
pub type PaymentsSessionResponseRouterData<R> =
|
||||||
|
ResponseRouterData<api::Session, R, PaymentsSessionData, PaymentsResponseData>;
|
||||||
|
|
||||||
pub type RefundsResponseRouterData<F, R> =
|
pub type RefundsResponseRouterData<F, R> =
|
||||||
ResponseRouterData<F, R, RefundsData, RefundsResponseData>;
|
ResponseRouterData<F, R, RefundsData, RefundsResponseData>;
|
||||||
|
|
||||||
@ -51,6 +54,8 @@ pub type RefundExecuteType =
|
|||||||
dyn services::ConnectorIntegration<api::Execute, RefundsData, RefundsResponseData>;
|
dyn services::ConnectorIntegration<api::Execute, RefundsData, RefundsResponseData>;
|
||||||
pub type RefundSyncType =
|
pub type RefundSyncType =
|
||||||
dyn services::ConnectorIntegration<api::RSync, RefundsData, RefundsResponseData>;
|
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>;
|
pub type VerifyRouterData = RouterData<api::Verify, VerifyRequestData, PaymentsResponseData>;
|
||||||
|
|
||||||
@ -118,12 +123,15 @@ pub struct PaymentsCancelData {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PaymentsSessionData {
|
pub struct PaymentsSessionData {
|
||||||
//TODO: Add the fields here as required
|
pub amount: i32,
|
||||||
|
pub currency: storage_enums::Currency,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PaymentsSessionResponseData {
|
pub struct ConnectorSessionToken {
|
||||||
pub client_token: Option<String>,
|
pub connector_name: String,
|
||||||
|
pub session_id: Option<String>,
|
||||||
|
pub session_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -137,6 +145,19 @@ pub struct VerifyRequestData {
|
|||||||
pub setup_mandate_details: Option<payments::MandateData>,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum PaymentsResponseData {
|
pub enum PaymentsResponseData {
|
||||||
TransactionResponse {
|
TransactionResponse {
|
||||||
@ -147,6 +168,7 @@ pub enum PaymentsResponseData {
|
|||||||
},
|
},
|
||||||
SessionResponse {
|
SessionResponse {
|
||||||
session_token: String,
|
session_token: String,
|
||||||
|
session_id: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -83,7 +83,7 @@ impl ConnectorData {
|
|||||||
let connector_name = types::Connector::from_str(name)
|
let connector_name = types::Connector::from_str(name)
|
||||||
.into_report()
|
.into_report()
|
||||||
.change_context(errors::ConnectorError::InvalidConnectorName)
|
.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)?;
|
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||||
Ok(ConnectorData {
|
Ok(ConnectorData {
|
||||||
connector,
|
connector,
|
||||||
@ -102,6 +102,7 @@ impl ConnectorData {
|
|||||||
"checkout" => Ok(Box::new(&connector::Checkout)),
|
"checkout" => Ok(Box::new(&connector::Checkout)),
|
||||||
"authorizedotnet" => Ok(Box::new(&connector::Authorizedotnet)),
|
"authorizedotnet" => Ok(Box::new(&connector::Authorizedotnet)),
|
||||||
"braintree" => Ok(Box::new(&connector::Braintree)),
|
"braintree" => Ok(Box::new(&connector::Braintree)),
|
||||||
|
"klarna" => Ok(Box::new(&connector::Klarna)),
|
||||||
_ => Err(report!(errors::UnexpectedError)
|
_ => Err(report!(errors::UnexpectedError)
|
||||||
.attach_printable(format!("invalid connector name: {connector_name}")))
|
.attach_printable(format!("invalid connector name: {connector_name}")))
|
||||||
.change_context(errors::ConnectorError::InvalidConnectorName)
|
.change_context(errors::ConnectorError::InvalidConnectorName)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ pub enum Connector {
|
|||||||
Aci,
|
Aci,
|
||||||
Authorizedotnet,
|
Authorizedotnet,
|
||||||
Braintree,
|
Braintree,
|
||||||
|
Klarna,
|
||||||
#[default]
|
#[default]
|
||||||
Dummy,
|
Dummy,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,3 +57,11 @@ base_url = "http://stripe-mock:12111/"
|
|||||||
|
|
||||||
[connectors.braintree]
|
[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"]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user