diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 14e624233c36..20ec4157f566 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6557,6 +6557,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", @@ -19423,6 +19424,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index c7527405a5f2..2aa19e4e223c 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -9276,6 +9276,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", @@ -23842,6 +23843,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", diff --git a/config/config.example.toml b/config/config.example.toml index 4d9950226451..11e8bed7dacd 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -223,6 +223,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url= "https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 56dd0ef7451e..7fdc0c6d5e83 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -65,6 +65,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -298,6 +299,10 @@ debit.currency = "USD" ali_pay.currency = "GBP,CNY" we_chat_pay.currency = "GBP,CNY" +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 464f9bcba32e..9c91c12f3b43 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -69,6 +69,7 @@ iatapay.base_url = "https://iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://secure.api.itau/" jpmorgan.base_url = "https://api-ms.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.klarna.com/" mifinity.base_url = "https://secure.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -332,6 +333,10 @@ debit.currency = "USD" ali_pay.currency = "GBP,CNY" we_chat_pay.currency = "GBP,CNY" +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index fb210a28862c..152f4f04cd89 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -69,6 +69,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -333,6 +334,10 @@ debit.currency = "USD" ali_pay.currency = "GBP,CNY" we_chat_pay.currency = "GBP,CNY" +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } diff --git a/config/development.toml b/config/development.toml index 7008af1f28a2..133823d6cb37 100644 --- a/config/development.toml +++ b/config/development.toml @@ -241,6 +241,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url= "https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -491,6 +492,10 @@ paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,N credit = { currency = "USD" } debit = { currency = "USD" } +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,EUR,EUR,CAD,CZK,DKK,EUR,EUR,EUR,EUR,EUR,EUR,EUR,NZD,NOK,PLN,EUR,EUR,SEK,CHF,GBP,USD" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index b861e47e5945..1ad2ef91336c 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -154,6 +154,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 0989614c8a04..3a3fe5e8f429 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -88,7 +88,7 @@ pub enum Connector { // Inespay, Iatapay, Itaubank, - //Jpmorgan, + Jpmorgan, Klarna, Mifinity, Mollie, @@ -175,6 +175,7 @@ impl Connector { (Self::Airwallex, _) | (Self::Deutschebank, _) | (Self::Globalpay, _) + | (Self::Jpmorgan, _) | (Self::Paypal, _) | (Self::Payu, _) | (Self::Trustpay, PaymentMethod::BankRedirect) @@ -234,7 +235,7 @@ impl Connector { | Self::Iatapay // | Self::Inespay | Self::Itaubank - //| Self::Jpmorgan + | Self::Jpmorgan | Self::Klarna | Self::Mifinity | Self::Mollie diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 1776ade36661..127d5a9901c6 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -83,7 +83,7 @@ pub enum RoutableConnectors { Iatapay, // Inespay, Itaubank, - //Jpmorgan, + Jpmorgan, Klarna, Mifinity, Mollie, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 1af99f110208..985a6d7b547f 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -198,6 +198,7 @@ pub struct ConnectorConfig { pub gpayments: Option, pub helcim: Option, // pub inespay: Option, + pub jpmorgan: Option, pub klarna: Option, pub mifinity: Option, pub mollie: Option, @@ -370,6 +371,7 @@ impl ConnectorConfig { Connector::Gpayments => Ok(connector_data.gpayments), Connector::Helcim => Ok(connector_data.helcim), // Connector::Inespay => Ok(connector_data.inespay), + Connector::Jpmorgan => Ok(connector_data.jpmorgan), Connector::Klarna => Ok(connector_data.klarna), Connector::Mifinity => Ok(connector_data.mifinity), Connector::Mollie => Ok(connector_data.mollie), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index fcf0dca100cb..f49f6b311f6e 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -1731,6 +1731,47 @@ api_key="Client Secret" api_secret="Certificates" key2="Certificate Key" +[jpmorgan] +[[jpmorgan.credit]] + payment_method_type = "American Express" +[[jpmorgan.credit]] + payment_method_type = "ChaseNet" +[[jpmorgan.credit]] + payment_method_type = "Diners Club" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "JCB" +[[jpmorgan.credit]] + payment_method_type = "Mastercard" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "UnionPay" +[[jpmorgan.credit]] + payment_method_type = "Visa" + [[jpmorgan.debit]] + payment_method_type = "American Express" +[[jpmorgan.debit]] + payment_method_type = "ChaseNet" +[[jpmorgan.debit]] + payment_method_type = "Diners Club" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "JCB" +[[jpmorgan.debit]] + payment_method_type = "Mastercard" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "UnionPay" +[[jpmorgan.debit]] + payment_method_type = "Visa" +[jpmorgan.connector_auth.BodyKey] +api_key="Client ID" +key1="Client Secret" + [klarna] [[klarna.pay_later]] payment_method_type = "klarna" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 443d3fd72ac7..7216d4f7890b 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1443,6 +1443,46 @@ api_key="Client Secret" api_secret="Certificates" key2="Certificate Key" +[jpmorgan] +[[jpmorgan.credit]] + payment_method_type = "American Express" +[[jpmorgan.credit]] + payment_method_type = "ChaseNet" +[[jpmorgan.credit]] + payment_method_type = "Diners Club" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "JCB" +[[jpmorgan.credit]] + payment_method_type = "Mastercard" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "UnionPay" +[[jpmorgan.credit]] + payment_method_type = "Visa" + [[jpmorgan.debit]] + payment_method_type = "American Express" +[[jpmorgan.debit]] + payment_method_type = "ChaseNet" +[[jpmorgan.debit]] + payment_method_type = "Diners Club" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "JCB" +[[jpmorgan.debit]] + payment_method_type = "Mastercard" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "UnionPay" +[[jpmorgan.debit]] + payment_method_type = "Visa" +[jpmorgan.connector_auth.BodyKey] +api_key="Access Token" + [klarna] [[klarna.pay_later]] payment_method_type = "klarna" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 4d3f6ede1403..4c6ee7566f31 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -1681,6 +1681,46 @@ api_key="Client Secret" api_secret="Certificates" key2="Certificate Key" +[jpmorgan] +[[jpmorgan.credit]] + payment_method_type = "American Express" +[[jpmorgan.credit]] + payment_method_type = "ChaseNet" +[[jpmorgan.credit]] + payment_method_type = "Diners Club" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "JCB" +[[jpmorgan.credit]] + payment_method_type = "Mastercard" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "UnionPay" +[[jpmorgan.credit]] + payment_method_type = "Visa" + [[jpmorgan.debit]] + payment_method_type = "American Express" +[[jpmorgan.debit]] + payment_method_type = "ChaseNet" +[[jpmorgan.debit]] + payment_method_type = "Diners Club" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "JCB" +[[jpmorgan.debit]] + payment_method_type = "Mastercard" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "UnionPay" +[[jpmorgan.debit]] + payment_method_type = "Visa" +[jpmorgan.connector_auth.BodyKey] +api_key="Access Token" + [klarna] [[klarna.pay_later]] payment_method_type = "klarna" diff --git a/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs b/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs index 6095a53ad00b..9e5a24faab5f 100644 --- a/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs +++ b/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs @@ -1,14 +1,15 @@ pub mod transformers; - +use base64::Engine; +use common_enums::enums; use common_utils::{ errors::CustomResult, ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, MinorUnit, MinorUnitForConnector}, }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ - router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_data::{AccessToken, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, @@ -21,33 +22,36 @@ use hyperswitch_domain_models::{ }, router_response_types::{PaymentsResponseData, RefundsResponseData}, types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, }, }; -// use hyperswitch_interfaces::{ api::{self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorValidation}, configs::Connectors, - errors, + consts, errors, events::connector_api_logs::ConnectorEvent, - types::{self, Response}, + types::{self, RefreshTokenType, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; -use transformers as jpmorgan; +use masking::{Mask, Maskable, PeekInterface}; +use transformers::{self as jpmorgan, JpmorganErrorResponse}; -use crate::{constants::headers, types::ResponseRouterData, utils}; +use crate::{ + constants::headers, + types::{RefreshTokenRouterData, ResponseRouterData}, + utils, +}; #[derive(Clone)] pub struct Jpmorgan { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl Jpmorgan { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &MinorUnitForConnector, } } } @@ -79,14 +83,38 @@ where &self, req: &RouterData, _connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( + ) -> CustomResult)>, errors::ConnectorError> { + let mut headers = vec![( headers::CONTENT_TYPE.to_string(), self.get_content_type().to_string().into(), )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + let auth_header = ( + headers::AUTHORIZATION.to_string(), + format!( + "Bearer {}", + req.access_token + .clone() + .ok_or(errors::ConnectorError::FailedToObtainAuthType)? + .token + .peek() + ) + .into_masked(), + ); + let request_id = ( + headers::REQUEST_ID.to_string(), + req.connector_request_reference_id + .clone() + .to_string() + .into_masked(), + ); + let merchant_id = ( + headers::MERCHANT_ID.to_string(), + req.merchant_id.get_string_repr().to_string().into_masked(), + ); + headers.push(auth_header); + headers.push(request_id); + headers.push(merchant_id); + Ok(headers) } } @@ -96,11 +124,7 @@ impl ConnectorCommon for Jpmorgan { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Base - //todo!() - // TODO! Check connector documentation, on which unit they are processing the currency. - // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, - // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base + api::CurrencyUnit::Minor } fn common_get_content_type(&self) -> &'static str { @@ -111,36 +135,29 @@ impl ConnectorCommon for Jpmorgan { connectors.jpmorgan.base_url.as_ref() } - fn get_auth_header( - &self, - auth_type: &ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = jpmorgan::JpmorganAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) - } - fn build_error_response( &self, res: Response, event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { - let response: jpmorgan::JpmorganErrorResponse = res + let response: JpmorganErrorResponse = res .response .parse_struct("JpmorganErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); + event_builder.map(|i| i.set_response_body(&response)); + + let response_message = response + .response_message + .as_ref() + .map_or_else(|| consts::NO_ERROR_MESSAGE.to_string(), ToString::to_string); Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: response.response_code, + message: response_message.clone(), + reason: Some(response_message), attempt_status: None, connector_transaction_id: None, }) @@ -148,14 +165,150 @@ impl ConnectorCommon for Jpmorgan { } impl ConnectorValidation for Jpmorgan { - //TODO: implement functions when support enabled + fn validate_capture_method( + &self, + capture_method: Option, + _pmt: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Automatic | enums::CaptureMethod::Manual => Ok(()), + enums::CaptureMethod::ManualMultiple + | enums::CaptureMethod::Scheduled + | enums::CaptureMethod::SequentialAutomatic => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Jpmorgan"), + ))? + } + } + } + + fn validate_psync_reference_id( + &self, + data: &PaymentsSyncData, + _is_three_ds: bool, + _status: enums::AttemptStatus, + _connector_meta_data: Option, + ) -> CustomResult<(), errors::ConnectorError> { + if data.encoded_data.is_some() + || data + .connector_transaction_id + .get_connector_transaction_id() + .is_ok() + { + return Ok(()); + } + Err(errors::ConnectorError::MissingConnectorTransactionID.into()) + } } impl ConnectorIntegration for Jpmorgan { //TODO: implement sessions flow } -impl ConnectorIntegration for Jpmorgan {} +impl ConnectorIntegration for Jpmorgan { + fn get_headers( + &self, + req: &RefreshTokenRouterData, + _connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let client_id = req.request.app_id.clone(); + + let client_secret = req.request.id.clone(); + + let creds = format!( + "{}:{}", + client_id.peek(), + client_secret.unwrap_or_default().peek() + ); + let encoded_creds = common_utils::consts::BASE64_ENGINE.encode(creds); + + let auth_string = format!("Basic {}", encoded_creds); + Ok(vec![ + ( + headers::CONTENT_TYPE.to_string(), + RefreshTokenType::get_content_type(self).to_string().into(), + ), + ( + headers::AUTHORIZATION.to_string(), + auth_string.into_masked(), + ), + ]) + } + + fn get_content_type(&self) -> &'static str { + "application/x-www-form-urlencoded" + } + + fn get_url( + &self, + _req: &RefreshTokenRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!( + "{}/am/oauth2/alpha/access_token", + connectors + .jpmorgan + .secondary_base_url + .as_ref() + .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)? + )) + } + + fn get_request_body( + &self, + req: &RefreshTokenRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = jpmorgan::JpmorganAuthUpdateRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &RefreshTokenRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .attach_default_headers() + .headers(RefreshTokenType::get_headers(self, req, connectors)?) + .url(&RefreshTokenType::get_url(self, req, connectors)?) + .set_body(RefreshTokenType::get_request_body(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &RefreshTokenRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: jpmorgan::JpmorganAuthUpdateResponse = res + .response + .parse_struct("jpmorgan JpmorganAuthUpdateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} impl ConnectorIntegration for Jpmorgan @@ -167,7 +320,7 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -178,9 +331,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/payments", self.base_url(connectors))) } fn get_request_body( @@ -188,7 +341,7 @@ impl ConnectorIntegration CustomResult { - let amount = utils::convert_amount( + let amount: MinorUnit = utils::convert_amount( self.amount_converter, req.request.minor_amount, req.request.currency, @@ -249,12 +402,102 @@ impl ConnectorIntegration for Jpmorgan { + fn get_headers( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult { + let tid = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}/payments/{}/captures", + self.base_url(connectors), + tid + )) + } + + fn get_request_body( + &self, + req: &PaymentsCaptureRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let amount: MinorUnit = utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, + req.request.currency, + )?; + + let connector_router_data = jpmorgan::JpmorganRouterData::from((amount, req)); + let connector_req = jpmorgan::JpmorganCaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsCaptureRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: jpmorgan::JpmorganPaymentsResponse = res + .response + .parse_struct("Jpmorgan PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + impl ConnectorIntegration for Jpmorgan { fn get_headers( &self, req: &PaymentsSyncRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -264,10 +507,15 @@ impl ConnectorIntegration for Jpm fn get_url( &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + req: &PaymentsSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let tid = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!("{}/payments/{}", self.base_url(connectors), tid)) } fn build_request( @@ -313,12 +561,12 @@ impl ConnectorIntegration for Jpm } } -impl ConnectorIntegration for Jpmorgan { +impl ConnectorIntegration for Jpmorgan { fn get_headers( &self, - req: &PaymentsCaptureRouterData, + req: &PaymentsCancelRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -328,34 +576,41 @@ impl ConnectorIntegration fo fn get_url( &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, + req: &PaymentsCancelRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let tid = req.request.connector_transaction_id.clone(); + Ok(format!("{}/payments/{}", self.base_url(connectors), tid)) } fn get_request_body( &self, - _req: &PaymentsCaptureRouterData, + req: &PaymentsCancelRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let amount: MinorUnit = utils::convert_amount( + self.amount_converter, + req.request.minor_amount.unwrap_or_default(), + req.request.currency.unwrap_or_default(), + )?; + + let connector_router_data = jpmorgan::JpmorganRouterData::from((amount, req)); + let connector_req = jpmorgan::JpmorganCancelRequest::try_from(connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, - req: &PaymentsCaptureRouterData, + req: &PaymentsCancelRouterData, connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() - .method(Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .method(Method::Patch) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCaptureType::get_request_body( + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(), @@ -364,14 +619,15 @@ impl ConnectorIntegration fo fn handle_response( &self, - data: &PaymentsCaptureRouterData, + data: &PaymentsCancelRouterData, event_builder: Option<&mut ConnectorEvent>, res: Response, - ) -> CustomResult { - let response: jpmorgan::JpmorganPaymentsResponse = res + ) -> CustomResult { + let response: jpmorgan::JpmorganCancelResponse = res .response - .parse_struct("Jpmorgan PaymentsCaptureResponse") + .parse_struct("JpmrorganPaymentsVoidResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -390,14 +646,12 @@ impl ConnectorIntegration fo } } -impl ConnectorIntegration for Jpmorgan {} - impl ConnectorIntegration for Jpmorgan { fn get_headers( &self, req: &RefundsRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -410,7 +664,7 @@ impl ConnectorIntegration for Jpmorga _req: &RefundsRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Err(errors::ConnectorError::NotImplemented("Refunds".to_string()).into()) } fn get_request_body( @@ -434,18 +688,19 @@ impl ConnectorIntegration for Jpmorga req: &RefundsRouterData, connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { - let request = RequestBuilder::new() - .method(Method::Post) - .url(&types::RefundExecuteType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundExecuteType::get_headers( - self, req, connectors, - )?) - .set_body(types::RefundExecuteType::get_request_body( - self, req, connectors, - )?) - .build(); - Ok(Some(request)) + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .set_body(types::RefundExecuteType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } fn handle_response( @@ -454,9 +709,9 @@ impl ConnectorIntegration for Jpmorga event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: jpmorgan::RefundResponse = res + let response: jpmorgan::JpmorganRefundResponse = res .response - .parse_struct("jpmorgan RefundResponse") + .parse_struct("JpmorganRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -481,22 +736,21 @@ impl ConnectorIntegration for Jpmorgan &self, req: &RefundSyncRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { self.common_get_content_type() } - fn get_url( &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, + req: &RefundSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let tid = req.request.connector_transaction_id.clone(); + Ok(format!("{}/refunds/{}", self.base_url(connectors), tid)) } - fn build_request( &self, req: &RefundSyncRouterData, @@ -521,7 +775,7 @@ impl ConnectorIntegration for Jpmorgan event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: jpmorgan::RefundResponse = res + let response: jpmorgan::JpmorganRefundSyncResponse = res .response .parse_struct("jpmorgan RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; diff --git a/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs b/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs index 90b058697051..78c1b5eefb5f 100644 --- a/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs @@ -1,12 +1,15 @@ -use common_enums::enums; -use common_utils::types::StringMinorUnit; +use common_enums::enums::CaptureMethod; +use common_utils::types::MinorUnit; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, + router_data::{AccessToken, ConnectorAuthType, RouterData}, router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, + router_request_types::{PaymentsCancelData, ResponseId}, router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + RefreshTokenRouterData, RefundsRouterData, + }, }; use hyperswitch_interfaces::errors; use masking::Secret; @@ -14,18 +17,17 @@ use serde::{Deserialize, Serialize}; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + utils::{ + get_unimplemented_payment_method_error_message, CardData, RouterData as OtherRouterData, + }, }; - -//TODO: Fill the struct with respective fields pub struct JpmorganRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: MinorUnit, pub router_data: T, } -impl From<(StringMinorUnit, T)> for JpmorganRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts +impl From<(MinorUnit, T)> for JpmorganRouterData { + fn from((amount, item): (MinorUnit, T)) -> Self { Self { amount, router_data: item, @@ -33,20 +35,102 @@ impl From<(StringMinorUnit, T)> for JpmorganRouterData { } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] +#[derive(Debug, Clone, Serialize)] +pub struct JpmorganAuthUpdateRequest { + pub grant_type: String, + pub scope: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JpmorganAuthUpdateResponse { + pub access_token: Secret, + pub scope: String, + pub token_type: String, + pub expires_in: i64, +} + +impl TryFrom<&RefreshTokenRouterData> for JpmorganAuthUpdateRequest { + type Error = error_stack::Report; + fn try_from(_item: &RefreshTokenRouterData) -> Result { + Ok(Self { + grant_type: String::from("client_credentials"), + scope: String::from("jpm:payments:sandbox"), + }) + } +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(AccessToken { + token: item.response.access_token, + expires: item.response.expires_in, + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JpmorganPaymentsRequest { - amount: StringMinorUnit, - card: JpmorganCard, + capture_method: CapMethod, + amount: MinorUnit, + currency: common_enums::Currency, + merchant: JpmorganMerchant, + payment_method_type: JpmorganPaymentMethodType, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JpmorganCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, + account_number: Secret, + expiry: Expiry, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganPaymentMethodType { + card: JpmorganCard, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Expiry { + month: Secret, + year: Secret, +} + +#[derive(Serialize, Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganMerchantSoftware { + company_name: Secret, + product_name: Secret, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganMerchant { + merchant_software: JpmorganMerchantSoftware, +} + +fn map_capture_method( + capture_method: CaptureMethod, +) -> Result> { + match capture_method { + CaptureMethod::Automatic => Ok(CapMethod::Now), + CaptureMethod::Manual => Ok(CapMethod::Manual), + CaptureMethod::Scheduled + | CaptureMethod::ManualMultiple + | CaptureMethod::SequentialAutomatic => { + Err(errors::ConnectorError::NotImplemented("Capture Method".to_string()).into()) + } + } } impl TryFrom<&JpmorganRouterData<&PaymentsAuthorizeRouterData>> for JpmorganPaymentsRequest { @@ -56,84 +140,226 @@ impl TryFrom<&JpmorganRouterData<&PaymentsAuthorizeRouterData>> for JpmorganPaym ) -> Result { match item.router_data.request.payment_method_data.clone() { PaymentMethodData::Card(req_card) => { + if item.router_data.is_three_ds() { + return Err(errors::ConnectorError::NotSupported { + message: "3DS payments".to_string(), + connector: "Jpmorgan", + } + .into()); + } + + let capture_method = + map_capture_method(item.router_data.request.capture_method.unwrap_or_default()); + + let merchant_software = JpmorganMerchantSoftware { + company_name: String::from("JPMC").into(), + product_name: String::from("Hyperswitch").into(), + }; + + let merchant = JpmorganMerchant { merchant_software }; + + let expiry: Expiry = Expiry { + month: req_card.card_exp_month.clone(), + year: req_card.get_expiry_year_4_digit(), + }; + + let account_number = Secret::new(req_card.card_number.to_string()); + let card = JpmorganCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, + account_number, + expiry, }; + + let payment_method_type = JpmorganPaymentMethodType { card }; + Ok(Self { - amount: item.amount.clone(), - card, + capture_method: capture_method?, + currency: item.router_data.request.currency, + amount: item.amount, + merchant, + payment_method_type, }) } - _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), + PaymentMethodData::CardDetailsForNetworkTransactionId(_) + | PaymentMethodData::CardRedirect(_) + | PaymentMethodData::Wallet(_) + | PaymentMethodData::PayLater(_) + | PaymentMethodData::BankRedirect(_) + | PaymentMethodData::BankDebit(_) + | PaymentMethodData::BankTransfer(_) + | PaymentMethodData::Crypto(_) + | PaymentMethodData::MandatePayment + | PaymentMethodData::Reward + | PaymentMethodData::RealTimePayment(_) + | PaymentMethodData::MobilePayment(_) + | PaymentMethodData::Upi(_) + | PaymentMethodData::Voucher(_) + | PaymentMethodData::GiftCard(_) + | PaymentMethodData::OpenBanking(_) + | PaymentMethodData::CardToken(_) + | PaymentMethodData::NetworkToken(_) => Err(errors::ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("jpmorgan"), + ) + .into()), } } } -//TODO: Fill the struct with respective fields -// Auth Struct +//JP Morgan uses access token only due to which we aren't reading the fields in this struct +#[derive(Debug)] pub struct JpmorganAuthType { - pub(super) api_key: Secret, + pub(super) _api_key: Secret, + pub(super) _key1: Secret, } impl TryFrom<&ConnectorAuthType> for JpmorganAuthType { type Error = error_stack::Report; fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), + ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + _api_key: api_key.to_owned(), + _key1: key1.to_owned(), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum JpmorganPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum JpmorganTransactionStatus { + Success, + Denied, + Error, } -impl From for common_enums::AttemptStatus { - fn from(item: JpmorganPaymentStatus) -> Self { - match item { - JpmorganPaymentStatus::Succeeded => Self::Charged, - JpmorganPaymentStatus::Failed => Self::Failure, - JpmorganPaymentStatus::Processing => Self::Authorizing, - } - } +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "UPPERCASE")] +pub enum JpmorganTransactionState { + Closed, + Authorized, + Voided, + #[default] + Pending, + Declined, + Error, } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct JpmorganPaymentsResponse { - status: JpmorganPaymentStatus, - id: String, + transaction_id: String, + request_id: String, + transaction_state: JpmorganTransactionState, + response_status: String, + response_code: String, + response_message: String, + payment_method_type: PaymentMethodType, + capture_method: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Merchant { + merchant_id: Option, + merchant_software: MerchantSoftware, + merchant_category_code: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantSoftware { + company_name: Secret, + product_name: Secret, + version: Option>, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentMethodType { + card: Option, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Card { + expiry: Option, + card_type: Option>, + card_type_name: Option>, + masked_account_number: Option>, + card_type_indicators: Option, + network_response: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkResponse { + address_verification_result: Option>, + address_verification_result_code: Option>, + card_verification_result_code: Option>, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExpiryResponse { + month: Option>, + year: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CardTypeIndicators { + issuance_country_code: Option>, + is_durbin_regulated: Option, + card_product_types: Secret>, +} + +pub fn attempt_status_from_transaction_state( + transaction_state: JpmorganTransactionState, +) -> common_enums::AttemptStatus { + match transaction_state { + JpmorganTransactionState::Authorized => common_enums::AttemptStatus::Authorized, + JpmorganTransactionState::Closed => common_enums::AttemptStatus::Charged, + JpmorganTransactionState::Declined | JpmorganTransactionState::Error => { + common_enums::AttemptStatus::Failure + } + JpmorganTransactionState::Pending => common_enums::AttemptStatus::Pending, + JpmorganTransactionState::Voided => common_enums::AttemptStatus::Voided, + } } impl TryFrom> for RouterData { type Error = error_stack::Report; + fn try_from( item: ResponseRouterData, ) -> Result { + let transaction_state = match item.response.transaction_state { + JpmorganTransactionState::Closed => match item.response.capture_method { + Some(CapMethod::Now) => JpmorganTransactionState::Closed, + _ => JpmorganTransactionState::Authorized, + }, + JpmorganTransactionState::Authorized => JpmorganTransactionState::Authorized, + JpmorganTransactionState::Voided => JpmorganTransactionState::Voided, + JpmorganTransactionState::Pending => JpmorganTransactionState::Pending, + JpmorganTransactionState::Declined => JpmorganTransactionState::Declined, + JpmorganTransactionState::Error => JpmorganTransactionState::Error, + }; + let status = attempt_status_from_transaction_state(transaction_state); + Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), + status, response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), redirection_data: Box::new(None), mandate_reference: Box::new(None), connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), incremental_authorization_allowed: None, charge_id: None, }), @@ -142,24 +368,179 @@ impl TryFrom, + amount: MinorUnit, + currency: Option, } -impl TryFrom<&JpmorganRouterData<&RefundsRouterData>> for JpmorganRefundRequest { +#[derive(Debug, Default, Copy, Serialize, Deserialize, Clone)] +#[serde(rename_all = "UPPERCASE")] +pub enum CapMethod { + #[default] + Now, + Delayed, + Manual, +} + +impl TryFrom<&JpmorganRouterData<&PaymentsCaptureRouterData>> for JpmorganCaptureRequest { + type Error = error_stack::Report; + fn try_from( + item: &JpmorganRouterData<&PaymentsCaptureRouterData>, + ) -> Result { + let capture_method = Some(map_capture_method( + item.router_data.request.capture_method.unwrap_or_default(), + )?); + Ok(Self { + capture_method, + amount: item.amount, + currency: Some(item.router_data.request.currency), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganCaptureResponse { + pub transaction_id: String, + pub request_id: String, + pub transaction_state: JpmorganTransactionState, + pub response_status: JpmorganTransactionStatus, + pub response_code: String, + pub response_message: String, + pub payment_method_type: PaymentMethodTypeCapRes, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentMethodTypeCapRes { + pub card: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardCapRes { + pub card_type: Option>, + pub card_type_name: Option>, + unmasked_account_number: Option>, +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let status = attempt_status_from_transaction_state(item.response.transaction_state); + Ok(Self { + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganPSyncResponse { + transaction_id: String, + transaction_state: JpmorganTransactionState, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum JpmorganResponseStatus { + Success, + Denied, + Error, +} + +impl + TryFrom> + for RouterData +{ type Error = error_stack::Report; - fn try_from(item: &JpmorganRouterData<&RefundsRouterData>) -> Result { + fn try_from( + item: ResponseRouterData, + ) -> Result { + let status = attempt_status_from_transaction_state(item.response.transaction_state); Ok(Self { - amount: item.amount.to_owned(), + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data }) } } -// Type definition for Refund Response +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TransactionData { + payment_type: Option>, + status_code: Secret, + txn_secret: Option>, + tid: Option>, + test_mode: Option>, + status: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganRefundRequest { + pub merchant: MerchantRefundReq, + pub amount: MinorUnit, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantRefundReq { + pub merchant_software: MerchantSoftware, +} + +impl TryFrom<&JpmorganRouterData<&RefundsRouterData>> for JpmorganRefundRequest { + type Error = error_stack::Report; + fn try_from(_item: &JpmorganRouterData<&RefundsRouterData>) -> Result { + Err(errors::ConnectorError::NotImplemented("Refunds".to_string()).into()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganRefundResponse { + pub transaction_id: Option, + pub request_id: String, + pub transaction_state: JpmorganTransactionState, + pub amount: MinorUnit, + pub currency: common_enums::Currency, + pub response_status: JpmorganResponseStatus, + pub response_code: String, + pub response_message: String, + pub transaction_reference_id: Option, + pub remaining_refundable_amount: Option, +} #[allow(dead_code)] #[derive(Debug, Serialize, Default, Deserialize, Clone)] @@ -170,59 +551,192 @@ pub enum RefundStatus { Processing, } -impl From for enums::RefundStatus { +impl From for common_enums::RefundStatus { fn from(item: RefundStatus) -> Self { match item { RefundStatus::Succeeded => Self::Success, RefundStatus::Failed => Self::Failure, RefundStatus::Processing => Self::Pending, - //TODO: Review mapping } } } -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct RefundResponse { id: String, status: RefundStatus, } -impl TryFrom> for RefundsRouterData { +pub fn refund_status_from_transaction_state( + transaction_state: JpmorganTransactionState, +) -> common_enums::RefundStatus { + match transaction_state { + JpmorganTransactionState::Voided | JpmorganTransactionState::Closed => { + common_enums::RefundStatus::Success + } + JpmorganTransactionState::Declined | JpmorganTransactionState::Error => { + common_enums::RefundStatus::Failure + } + JpmorganTransactionState::Pending | JpmorganTransactionState::Authorized => { + common_enums::RefundStatus::Pending + } + } +} + +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item + .response + .transaction_id + .clone() + .ok_or(errors::ConnectorError::ResponseHandlingFailed)?, + refund_status: refund_status_from_transaction_state( + item.response.transaction_state, + ), }), ..item.data }) } } -impl TryFrom> for RefundsRouterData { +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganRefundSyncResponse { + transaction_id: String, + request_id: String, + transaction_state: JpmorganTransactionState, + amount: MinorUnit, + currency: common_enums::Currency, + response_status: JpmorganResponseStatus, + response_code: String, +} + +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.transaction_id.clone(), + refund_status: refund_status_from_transaction_state( + item.response.transaction_state, + ), + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganCancelRequest { + pub amount: Option, + pub is_void: Option, + pub reversal_reason: Option, +} + +impl TryFrom> for JpmorganCancelRequest { + type Error = error_stack::Report; + fn try_from(item: JpmorganRouterData<&PaymentsCancelRouterData>) -> Result { + Ok(Self { + amount: item.router_data.request.amount, + is_void: Some(true), + reversal_reason: item.router_data.request.cancellation_reason.clone(), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganCancelResponse { + transaction_id: String, + request_id: String, + response_status: JpmorganResponseStatus, + response_code: String, + response_message: String, + payment_method_type: JpmorganPaymentMethodTypeCancelResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganPaymentMethodTypeCancelResponse { + pub card: CardCancelResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardCancelResponse { + pub card_type: Secret, + pub card_type_name: Secret, +} + +impl + TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + JpmorganCancelResponse, + PaymentsCancelData, + PaymentsResponseData, + >, + ) -> Result { + let status = match item.response.response_status { + JpmorganResponseStatus::Success => common_enums::AttemptStatus::Voided, + JpmorganResponseStatus::Denied | JpmorganResponseStatus::Error => { + common_enums::AttemptStatus::Failure + } + }; + Ok(Self { + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, }), ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganValidationErrors { + pub code: Option, + pub message: Option, + pub entity: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganErrorInformation { + pub code: Option, + pub message: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct JpmorganErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, + pub response_status: JpmorganTransactionStatus, + pub response_code: String, + pub response_message: Option, } diff --git a/crates/hyperswitch_connectors/src/constants.rs b/crates/hyperswitch_connectors/src/constants.rs index 9ca4ee9591fb..3d99f8d167ff 100644 --- a/crates/hyperswitch_connectors/src/constants.rs +++ b/crates/hyperswitch_connectors/src/constants.rs @@ -10,6 +10,7 @@ pub(crate) mod headers { pub(crate) const IDEMPOTENCY_KEY: &str = "Idempotency-Key"; pub(crate) const MESSAGE_SIGNATURE: &str = "Message-Signature"; pub(crate) const MERCHANT_ID: &str = "Merchant-ID"; + pub(crate) const REQUEST_ID: &str = "request-id"; pub(crate) const NONCE: &str = "nonce"; pub(crate) const TIMESTAMP: &str = "Timestamp"; pub(crate) const TOKEN: &str = "token"; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 8e6d74c83753..ccb563b06102 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1401,6 +1401,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { itaubank::transformers::ItaubankAuthType::try_from(self.auth_type)?; Ok(()) } + api_enums::Connector::Jpmorgan => { + jpmorgan::transformers::JpmorganAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Klarna => { klarna::transformers::KlarnaAuthType::try_from(self.auth_type)?; klarna::transformers::KlarnaConnectorMetadataObject::try_from( diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index c39ead5fbce8..df5d497ea8fa 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -446,9 +446,11 @@ impl ConnectorData { // Ok(ConnectorEnum::Old(Box::new(connector::Inespay::new()))) // } enums::Connector::Itaubank => { - //enums::Connector::Jpmorgan => Ok(ConnectorEnum::Old(Box::new(connector::Jpmorgan))), Ok(ConnectorEnum::Old(Box::new(connector::Itaubank::new()))) } + enums::Connector::Jpmorgan => { + Ok(ConnectorEnum::Old(Box::new(connector::Jpmorgan::new()))) + } enums::Connector::Klarna => { Ok(ConnectorEnum::Old(Box::new(connector::Klarna::new()))) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 1947a2a2dee1..1d9a86f0aff9 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -256,7 +256,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Iatapay => Self::Iatapay, // api_enums::Connector::Inespay => Self::Inespay, api_enums::Connector::Itaubank => Self::Itaubank, - //api_enums::Connector::Jpmorgan => Self::Jpmorgan, + api_enums::Connector::Jpmorgan => Self::Jpmorgan, api_enums::Connector::Klarna => Self::Klarna, api_enums::Connector::Mifinity => Self::Mifinity, api_enums::Connector::Mollie => Self::Mollie, diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index f2f849731d52..10db11bb3b0c 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -294,7 +294,8 @@ api_key="API Key" api_key="API Key" [jpmorgan] -api_key="API Key" +api_key="Client ID" +key1 ="Client Secret" [elavon] api_key="API Key" diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 37cc341c4540..7b6a1ba6b7a0 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -51,7 +51,7 @@ pub struct ConnectorAuthentication { pub iatapay: Option, pub inespay: Option, pub itaubank: Option, - pub jpmorgan: Option, + pub jpmorgan: Option, pub mifinity: Option, pub mollie: Option, pub multisafepay: Option, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js b/cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js new file mode 100644 index 000000000000..36e53ffd995f --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js @@ -0,0 +1,225 @@ +const successfulNo3DSCardDetails = { + card_number: "6011016011016011", + card_exp_month: "10", + card_exp_year: "2027", + card_holder_name: "John Doe", + card_cvc: "123", +}; + +export const connectorDetails = { + card_pm: { + PaymentIntent: { + Request: { + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + }, + }, + }, + "3DSManualCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "3DS payments is not supported by Jpmorgan", + code: "IR_00", + }, + }, + }, + }, + + "3DSAutoCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Three_ds payments is not supported by Jpmorgan", + code: "IR_00", + }, + }, + }, + }, + No3DSManualCapture: { + Request: { + currency: "USD", + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + No3DSAutoCapture: { + Request: { + currency: "USD", + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + Capture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + amount: 6500, + amount_capturable: 0, + amount_received: 6500, + }, + }, + }, + PartialCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "partially_captured", + amount: 6500, + amount_capturable: 0, + amount_received: 100, + }, + }, + }, + Refund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + manualPaymentRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + manualPaymentPartialRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + PartialRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + SyncRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Response: { + status: 404, + body: { + type: "invalid_request", + message: "Refund does not exist in our records.", + code: "HE_02", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index e432c37717bf..3b4c9e5decf0 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -15,6 +15,7 @@ import { connectorDetails as fiservemeaConnectorDetails } from "./Fiservemea.js" import { connectorDetails as fiuuConnectorDetails } from "./Fiuu.js"; import { connectorDetails as iatapayConnectorDetails } from "./Iatapay.js"; import { connectorDetails as itaubankConnectorDetails } from "./ItauBank.js"; +import { connectorDetails as jpmorganConnectorDetails } from "./Jpmorgan.js"; import { connectorDetails as nexixpayConnectorDetails } from "./Nexixpay.js"; import { connectorDetails as nmiConnectorDetails } from "./Nmi.js"; import { connectorDetails as noonConnectorDetails } from "./Noon.js"; @@ -36,6 +37,7 @@ const connectorDetails = { fiservemea: fiservemeaConnectorDetails, iatapay: iatapayConnectorDetails, itaubank: itaubankConnectorDetails, + jpmorgan: jpmorganConnectorDetails, nexixpay: nexixpayConnectorDetails, nmi: nmiConnectorDetails, novalnet: novalnetConnectorDetails, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index f33ab82aa0dd..3320a5516d07 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -120,6 +120,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/"