feat(klarna): wallet payment through klarna (#182)

This commit is contained in:
Narayan Bhat
2022-12-23 00:59:18 +05:30
committed by GitHub
parent 5477cc98ff
commit 06a3c38bd4
14 changed files with 351 additions and 53 deletions

View File

@ -14,6 +14,11 @@ pub enum PaymentOp {
Confirm,
}
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct Metadata {
pub order_details: OrderDetails,
}
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct PaymentsRequest {
@ -210,10 +215,30 @@ pub struct CCard {
pub card_cvc: Secret<String>,
}
#[derive(Default, Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct PayLaterData {
pub billing_email: String,
pub country: String,
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum KlarnaRedirectIssuer {
Stripe,
}
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum KlarnaSdkIssuer {
Klarna,
}
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PayLaterData {
KlarnaRedirect {
issuer_name: KlarnaRedirectIssuer,
billing_email: String,
billing_country: String,
},
KlarnaSdk {
issuer_name: KlarnaSdkIssuer,
token: String,
},
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@ -704,7 +729,22 @@ pub struct PaymentsRetrieveRequest {
pub connector: Option<String>,
}
#[derive(Default, Debug, serde::Deserialize, Clone)]
#[derive(Debug, serde::Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum SupportedWallets {
Paypal,
ApplePay,
Klarna,
Gpay,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, Clone)]
pub struct OrderDetails {
pub product_name: String,
pub quantity: u16,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct PaymentsSessionRequest {
pub payment_id: String,
pub client_secret: String,

View File

@ -1,7 +1,7 @@
#![allow(dead_code)]
mod transformers;
use std::fmt::Debug;
use api_models::payments as api_payments;
use bytes::Bytes;
use error_stack::{IntoReport, ResultExt};
use transformers as klarna;
@ -27,7 +27,7 @@ impl api::ConnectorCommon for Klarna {
}
fn common_get_content_type(&self) -> &'static str {
"application/x-www-form-urlencoded"
"application/json"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
@ -99,7 +99,7 @@ impl
// 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);
logger::debug!(klarna_session_request_logs=?klarna_req);
Ok(Some(klarna_req))
}
@ -113,7 +113,6 @@ impl
.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(),
))
@ -124,9 +123,10 @@ impl
data: &types::PaymentsSessionRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsSessionRouterData, errors::ConnectorError> {
logger::debug!(klarna_session_response_logs=?res);
let response: klarna::KlarnaSessionResponse = res
.response
.parse_struct("KlarnaPaymentsResponse")
.parse_struct("KlarnaSessionResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
@ -140,6 +140,7 @@ impl
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
logger::debug!(klarna_session_error_logs=?res);
let response: klarna::KlarnaErrorResponse = res
.parse_struct("KlarnaErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
@ -187,7 +188,108 @@ impl
types::PaymentsResponseData,
> for Klarna
{
//Not Implemented (R)
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> 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::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let payment_method_data = &req.request.payment_method_data;
match payment_method_data {
api_payments::PaymentMethod::PayLater(api_payments::PayLaterData::KlarnaSdk {
token,
..
}) => Ok(format!(
"{}payments/v1/authorizations/{}/order",
self.base_url(connectors),
token
)),
_ => Err(error_stack::report!(
errors::ConnectorError::NotImplemented(
"We only support wallet payments through klarna".to_string(),
)
)),
}
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let klarna_req = utils::Encode::<klarna::KlarnaPaymentsRequest>::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::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsAuthorizeType::get_url(
self, req, connectors,
)?)
.headers(types::PaymentsAuthorizeType::get_headers(self, req)?)
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsAuthorizeRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
logger::debug!(klarna_raw_response=?res);
let response: klarna::KlarnaPaymentsResponse = 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<types::ErrorResponse, errors::ConnectorError> {
logger::debug!(klarna_error_response=?res);
let response: klarna::KlarnaErrorResponse = res
.parse_struct("KlarnaErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(types::ErrorResponse {
code: response.error_code,
message: response.error_messages.join(" & "),
reason: None,
})
}
}
impl

View File

@ -1,13 +1,26 @@
use error_stack::{report, IntoReport, ResultExt};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
core::errors,
services,
types::{self, storage::enums},
};
#[derive(Default, Debug, Serialize)]
pub struct KlarnaPaymentsRequest {}
pub struct KlarnaPaymentsRequest {
order_lines: Vec<OrderLines>,
order_amount: i64,
purchase_country: String,
purchase_currency: enums::Currency,
}
#[derive(Default, Debug, Deserialize)]
pub struct KlarnaPaymentsResponse {
order_id: String,
redirection_url: String,
}
#[derive(Serialize)]
pub struct KlarnaSessionRequest {
intent: KlarnaSessionIntent,
@ -28,19 +41,24 @@ 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 {
match request.order_details.clone() {
Some(order_details) => 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,
name: order_details.product_name,
quantity: order_details.quantity,
unit_price: request.amount,
total_amount: request.amount,
}],
})
}),
None => Err(report!(errors::ConnectorError::MissingRequiredField {
field_name: "product_name".to_string()
})),
}
}
}
@ -64,16 +82,71 @@ impl TryFrom<types::PaymentsSessionResponseRouterData<KlarnaSessionResponse>>
}
}
#[derive(Serialize)]
impl TryFrom<&types::PaymentsAuthorizeRouterData> for KlarnaPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
let request = &item.request;
match request.order_details.clone() {
Some(order_details) => Ok(Self {
purchase_country: "US".to_string(),
purchase_currency: request.currency,
order_amount: request.amount,
order_lines: vec![OrderLines {
name: order_details.product_name,
quantity: order_details.quantity,
unit_price: request.amount,
total_amount: request.amount,
}],
}),
None => Err(report!(errors::ConnectorError::MissingRequiredField {
field_name: "product_name".to_string()
})),
}
}
}
impl TryFrom<types::PaymentsResponseRouterData<KlarnaPaymentsResponse>>
for types::PaymentsAuthorizeRouterData
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::PaymentsResponseRouterData<KlarnaPaymentsResponse>,
) -> Result<Self, Self::Error> {
let response = &item.response;
let url = Url::parse(&response.redirection_url)
.into_report()
.change_context(errors::ParsingError)
.attach_printable("Could not parse the redirection data")?;
let redirection_data = services::RedirectForm {
url: url.to_string(),
method: services::Method::Get,
form_fields: std::collections::HashMap::from_iter(
url.query_pairs()
.map(|(k, v)| (k.to_string(), v.to_string())),
),
};
Ok(Self {
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.order_id),
redirect: true,
redirection_data: Some(redirection_data),
mandate_reference: None,
}),
..item.data
})
}
}
#[derive(Debug, Serialize)]
pub struct OrderLines {
name: String,
quantity: u64,
quantity: u16,
unit_price: i64,
total_amount: i64,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
#[allow(dead_code)]
pub enum KlarnaSessionIntent {
Buy,
Tokenize,

View File

@ -176,14 +176,25 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest {
}
}),
api::PaymentMethod::BankTransfer => StripePaymentMethodData::Bank,
api::PaymentMethod::PayLater(ref klarna_data) => {
StripePaymentMethodData::Klarna(StripeKlarnaData {
api::PaymentMethod::PayLater(ref pay_later_data) => match pay_later_data {
api_models::payments::PayLaterData::KlarnaRedirect {
billing_email,
billing_country,
..
} => StripePaymentMethodData::Klarna(StripeKlarnaData {
payment_method_types: "klarna".to_string(),
payment_method_data_type: "klarna".to_string(),
billing_email: klarna_data.billing_email.clone(),
billing_country: klarna_data.country.clone(),
})
}
billing_email: billing_email.to_string(),
billing_country: billing_country.to_string(),
}),
api_models::payments::PayLaterData::KlarnaSdk { .. } => Err(
error_stack::report!(errors::ApiErrorResponse::NotImplemented)
.attach_printable(
"Stripe does not support klarna sdk payments".to_string(),
)
.change_context(errors::ParsingError),
)?,
},
api::PaymentMethod::Wallet(_) => StripePaymentMethodData::Wallet,
api::PaymentMethod::Paypal => StripePaymentMethodData::Paypal,
}),
@ -253,7 +264,7 @@ impl TryFrom<&types::VerifyRouterData> for SetupIntentRequest {
let metadata_txn_uuid = Uuid::new_v4().to_string();
let payment_data: StripePaymentMethodData =
(item.request.payment_method_data.clone(), item.auth_type).into();
(item.request.payment_method_data.clone(), item.auth_type).try_into()?;
Ok(Self {
confirm: true,
@ -751,10 +762,13 @@ pub struct StripeWebhookObjectId {
pub data: StripeWebhookDataId,
}
impl From<(api::PaymentMethod, enums::AuthenticationType)> for StripePaymentMethodData {
fn from((pm_data, auth_type): (api::PaymentMethod, enums::AuthenticationType)) -> Self {
impl TryFrom<(api::PaymentMethod, enums::AuthenticationType)> for StripePaymentMethodData {
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
(pm_data, auth_type): (api::PaymentMethod, enums::AuthenticationType),
) -> Result<Self, Self::Error> {
match pm_data {
api::PaymentMethod::Card(ref ccard) => Self::Card({
api::PaymentMethod::Card(ref ccard) => Ok(Self::Card({
let payment_method_auth_type = match auth_type {
enums::AuthenticationType::ThreeDs => Auth3ds::Any,
enums::AuthenticationType::NoThreeDs => Auth3ds::Automatic,
@ -768,16 +782,27 @@ impl From<(api::PaymentMethod, enums::AuthenticationType)> for StripePaymentMeth
payment_method_data_card_cvc: ccard.card_cvc.clone(),
payment_method_auth_type,
}
}),
api::PaymentMethod::BankTransfer => Self::Bank,
api::PaymentMethod::PayLater(ref klarna_data) => Self::Klarna(StripeKlarnaData {
})),
api::PaymentMethod::BankTransfer => Ok(Self::Bank),
api::PaymentMethod::PayLater(pay_later_data) => match pay_later_data {
api_models::payments::PayLaterData::KlarnaRedirect {
billing_email,
billing_country: country,
..
} => Ok(Self::Klarna(StripeKlarnaData {
payment_method_types: "klarna".to_string(),
payment_method_data_type: "klarna".to_string(),
billing_email: klarna_data.billing_email.clone(),
billing_country: klarna_data.country.clone(),
}),
api::PaymentMethod::Wallet(_) => Self::Wallet,
api::PaymentMethod::Paypal => Self::Paypal,
billing_email,
billing_country: country,
})),
api_models::payments::PayLaterData::KlarnaSdk { .. } => Err(error_stack::report!(
errors::ApiErrorResponse::NotImplemented
)
.attach_printable("Stripe does not support klarna sdk payments".to_string())
.change_context(errors::ParsingError))?,
},
api::PaymentMethod::Wallet(_) => Ok(Self::Wallet),
api::PaymentMethod::Paypal => Ok(Self::Paypal),
}
}
}

View File

@ -144,7 +144,7 @@ pub trait Domain<F: Clone, R>: Send + Sync {
}
#[async_trait]
pub trait UpdateTracker<F, D, R>: Send {
pub trait UpdateTracker<F, D, Req>: Send {
async fn update_trackers<'b>(
&'b self,
db: &dyn StorageInterface,
@ -152,7 +152,7 @@ pub trait UpdateTracker<F, D, R>: Send {
payment_data: D,
customer: Option<Customer>,
storage_scheme: enums::MerchantStorageScheme,
) -> RouterResult<(BoxedOperation<'b, F, R>, D)>
) -> RouterResult<(BoxedOperation<'b, F, Req>, D)>
where
F: 'b + Send;
}

View File

@ -509,6 +509,7 @@ impl PaymentCreate {
billing_address_id,
statement_descriptor_name: request.statement_descriptor_name.clone(),
statement_descriptor_suffix: request.statement_descriptor_suffix.clone(),
metadata: request.metadata.clone(),
..storage::PaymentIntentNew::default()
}
}

View File

@ -157,11 +157,11 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsSessionRequest> for
#[instrument(skip_all)]
async fn update_trackers<'b>(
&'b self,
_db: &dyn StorageInterface,
db: &dyn StorageInterface,
_payment_id: &api::PaymentIdType,
payment_data: PaymentData<F>,
mut payment_data: PaymentData<F>,
_customer: Option<storage::Customer>,
_storage_scheme: enums::MerchantStorageScheme,
storage_scheme: enums::MerchantStorageScheme,
) -> RouterResult<(
BoxedOperation<'b, F, api::PaymentsSessionRequest>,
PaymentData<F>,
@ -169,6 +169,21 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsSessionRequest> for
where
F: 'b + Send,
{
let metadata = payment_data.payment_intent.metadata.clone();
payment_data.payment_intent = match metadata {
Some(metadata) => db
.update_payment_intent(
payment_data.payment_intent,
storage::PaymentIntentUpdate::MetadataUpdate { metadata },
storage_scheme,
)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
})?,
None => payment_data.payment_intent,
};
Ok((Box::new(self), payment_data))
}
}

View File

@ -394,6 +394,23 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsAuthorizeData {
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "browser_info",
})?;
let parsed_metadata: Option<api_models::payments::Metadata> = payment_data
.payment_intent
.metadata
.map(|metadata_value| {
metadata_value
.parse_value("metadata")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "metadata",
})
.attach_printable("unable to parse metadata")
})
.transpose()
.unwrap_or_default();
let order_details = parsed_metadata.map(|data| data.order_details);
Ok(Self {
payment_method_data: {
let payment_method_type = payment_data
@ -418,6 +435,7 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsAuthorizeData {
amount: payment_data.amount.into(),
currency: payment_data.currency,
browser_info,
order_details,
})
}
}
@ -469,9 +487,25 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsCancelData {
}
impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsSessionData {
type Error = errors::ApiErrorResponse;
type Error = error_stack::Report<errors::ApiErrorResponse>;
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
let parsed_metadata: Option<api_models::payments::Metadata> = payment_data
.payment_intent
.metadata
.map(|metadata_value| {
metadata_value
.parse_value("metadata")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "metadata",
})
.attach_printable("unable to parse metadata")
})
.transpose()
.unwrap_or_default();
let order_details = parsed_metadata.map(|data| data.order_details);
Ok(Self {
amount: payment_data.amount.into(),
currency: payment_data.currency,
@ -480,6 +514,7 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsSessionData {
.billing
.and_then(|billing_address| billing_address.address.map(|address| address.country))
.flatten(),
order_details,
})
}
}

View File

@ -101,6 +101,7 @@ pub struct PaymentsAuthorizeData {
pub off_session: Option<bool>,
pub setup_mandate_details: Option<payments::MandateData>,
pub browser_info: Option<BrowserInformation>,
pub order_details: Option<api_models::payments::OrderDetails>,
}
#[derive(Debug, Clone)]
@ -127,6 +128,7 @@ pub struct PaymentsSessionData {
pub amount: i64,
pub currency: storage_enums::Currency,
pub country: Option<String>,
pub order_details: Option<api_models::payments::OrderDetails>,
}
#[derive(Debug, Clone)]

View File

@ -47,6 +47,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
setup_mandate_details: None,
capture_method: None,
browser_info: None,
order_details: None,
},
response: Err(types::ErrorResponse::default()),
payment_method_id: None,

View File

@ -47,6 +47,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
setup_mandate_details: None,
capture_method: None,
browser_info: None,
order_details: None,
},
payment_method_id: None,
response: Err(types::ErrorResponse::default()),

View File

@ -44,6 +44,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
setup_mandate_details: None,
capture_method: None,
browser_info: None,
order_details: None,
},
response: Err(types::ErrorResponse::default()),
payment_method_id: None,

View File

@ -0,0 +1 @@
ALTER TABLE payment_intent ALTER COLUMN metadata SET DEFAULT '{}'::JSONB;

View File

@ -0,0 +1 @@
ALTER TABLE payment_intent ALTER COLUMN metadata DROP DEFAULT;