diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 092b3b84b6..1a884d5671 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -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, } -#[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, } -#[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, diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index a842835793..01703125aa 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -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::::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 { + 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 { + 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, 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 { + 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, errors::ConnectorError> { + let klarna_req = utils::Encode::::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, 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 { + 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 { + 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 diff --git a/crates/router/src/connector/klarna/transformers.rs b/crates/router/src/connector/klarna/transformers.rs index 295d11baf8..64a777c0ce 100644 --- a/crates/router/src/connector/klarna/transformers.rs +++ b/crates/router/src/connector/klarna/transformers.rs @@ -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, + 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; fn try_from(item: &types::PaymentsSessionRouterData) -> Result { 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, - }], - }) + 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: 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> } } -#[derive(Serialize)] +impl TryFrom<&types::PaymentsAuthorizeRouterData> for KlarnaPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + 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> + for types::PaymentsAuthorizeRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsResponseRouterData, + ) -> Result { + 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, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 8db92ba8b5..d74609c0d4 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -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; + fn try_from( + (pm_data, auth_type): (api::PaymentMethod, enums::AuthenticationType), + ) -> Result { 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 { - 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, + })), + 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, + 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), } } } diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 81bf3fe55d..2b6d4fa2be 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -144,7 +144,7 @@ pub trait Domain: Send + Sync { } #[async_trait] -pub trait UpdateTracker: Send { +pub trait UpdateTracker: Send { async fn update_trackers<'b>( &'b self, db: &dyn StorageInterface, @@ -152,7 +152,7 @@ pub trait UpdateTracker: Send { payment_data: D, customer: Option, storage_scheme: enums::MerchantStorageScheme, - ) -> RouterResult<(BoxedOperation<'b, F, R>, D)> + ) -> RouterResult<(BoxedOperation<'b, F, Req>, D)> where F: 'b + Send; } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 196ec19dff..33f1df2a88 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -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() } } diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 918a4cc14f..239bfabef0 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -157,11 +157,11 @@ impl UpdateTracker, 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, + mut payment_data: PaymentData, _customer: Option, - _storage_scheme: enums::MerchantStorageScheme, + storage_scheme: enums::MerchantStorageScheme, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsSessionRequest>, PaymentData, @@ -169,6 +169,21 @@ impl UpdateTracker, 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)) } } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 564a2af0cc..eea134d453 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -394,6 +394,23 @@ impl TryFrom> for types::PaymentsAuthorizeData { .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "browser_info", })?; + + let parsed_metadata: Option = 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 TryFrom> for types::PaymentsAuthorizeData { amount: payment_data.amount.into(), currency: payment_data.currency, browser_info, + order_details, }) } } @@ -469,9 +487,25 @@ impl TryFrom> for types::PaymentsCancelData { } impl TryFrom> for types::PaymentsSessionData { - type Error = errors::ApiErrorResponse; + type Error = error_stack::Report; fn try_from(payment_data: PaymentData) -> Result { + let parsed_metadata: Option = 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 TryFrom> for types::PaymentsSessionData { .billing .and_then(|billing_address| billing_address.address.map(|address| address.country)) .flatten(), + order_details, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c8d831e51c..5aed689c9e 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -101,6 +101,7 @@ pub struct PaymentsAuthorizeData { pub off_session: Option, pub setup_mandate_details: Option, pub browser_info: Option, + pub order_details: Option, } #[derive(Debug, Clone)] @@ -127,6 +128,7 @@ pub struct PaymentsSessionData { pub amount: i64, pub currency: storage_enums::Currency, pub country: Option, + pub order_details: Option, } #[derive(Debug, Clone)] diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index aed7c9c70d..a8d8f85866 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -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, diff --git a/crates/router/tests/connectors/authorizedotnet.rs b/crates/router/tests/connectors/authorizedotnet.rs index 5c4c33f3d2..31feab732f 100644 --- a/crates/router/tests/connectors/authorizedotnet.rs +++ b/crates/router/tests/connectors/authorizedotnet.rs @@ -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()), diff --git a/crates/router/tests/connectors/checkout.rs b/crates/router/tests/connectors/checkout.rs index 06f0af68c4..0bea948fa9 100644 --- a/crates/router/tests/connectors/checkout.rs +++ b/crates/router/tests/connectors/checkout.rs @@ -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, diff --git a/migrations/2022-12-21-124904_remove_metadata_default_as_null/down.sql b/migrations/2022-12-21-124904_remove_metadata_default_as_null/down.sql new file mode 100644 index 0000000000..4155ffac0c --- /dev/null +++ b/migrations/2022-12-21-124904_remove_metadata_default_as_null/down.sql @@ -0,0 +1 @@ +ALTER TABLE payment_intent ALTER COLUMN metadata SET DEFAULT '{}'::JSONB; \ No newline at end of file diff --git a/migrations/2022-12-21-124904_remove_metadata_default_as_null/up.sql b/migrations/2022-12-21-124904_remove_metadata_default_as_null/up.sql new file mode 100644 index 0000000000..9683f1cf28 --- /dev/null +++ b/migrations/2022-12-21-124904_remove_metadata_default_as_null/up.sql @@ -0,0 +1 @@ +ALTER TABLE payment_intent ALTER COLUMN metadata DROP DEFAULT; \ No newline at end of file