feat(connector): Plaid connector Integration (#3952)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sarthak Soni
2024-07-19 13:35:27 +05:30
committed by GitHub
parent 33298b3808
commit eb01680284
26 changed files with 1403 additions and 56 deletions

View File

@ -233,6 +233,7 @@ payone.base_url = "https://payment.preprod.payone.com/"
paypal.base_url = "https://api-m.sandbox.paypal.com/"
payu.base_url = "https://secure.snd.payu.com/"
placetopay.base_url = "https://test.placetopay.com/rest/gateway"
plaid.base_url = "https://sandbox.plaid.com"
powertranz.base_url = "https://staging.ptranz.com/api/"
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
rapyd.base_url = "https://sandboxapi.rapyd.net"
@ -488,6 +489,9 @@ open_banking_uk = { country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,M
[pm_filters.razorpay]
upi_collect = { country = "IN", currency = "INR" }
[pm_filters.plaid]
open_banking_pis = {currency = "EUR,GBP"}
[pm_filters.zen]
credit = { not_available_flows = { capture_method = "manual" } }
debit = { not_available_flows = { capture_method = "manual" } }

View File

@ -72,6 +72,7 @@ payone.base_url = "https://payment.preprod.payone.com/"
paypal.base_url = "https://api-m.sandbox.paypal.com/"
payu.base_url = "https://secure.snd.payu.com/"
placetopay.base_url = "https://test.placetopay.com/rest/gateway"
plaid.base_url = "https://sandbox.plaid.com"
powertranz.base_url = "https://staging.ptranz.com/api/"
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
rapyd.base_url = "https://sandboxapi.rapyd.net"
@ -293,6 +294,9 @@ open_banking_uk = {country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,MT
[pm_filters.razorpay]
upi_collect = {country = "IN", currency = "INR"}
[pm_filters.plaid]
open_banking_pis = {currency = "EUR,GBP"}
[pm_filters.worldpay]
apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US"
google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN"

View File

@ -76,6 +76,7 @@ payone.base_url = "https://payment.payone.com/"
paypal.base_url = "https://api-m.paypal.com/"
payu.base_url = "https://secure.payu.com/api/"
placetopay.base_url = "https://checkout.placetopay.com/rest/gateway"
plaid.base_url = "https://production.plaid.com"
powertranz.base_url = "https://staging.ptranz.com/api/"
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
rapyd.base_url = "https://sandboxapi.rapyd.net"
@ -312,6 +313,9 @@ open_banking_uk = {country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,MT
[pm_filters.razorpay]
upi_collect = {country = "IN", currency = "INR"}
[pm_filters.plaid]
open_banking_pis = {currency = "EUR,GBP"}
[pm_filters.worldpay]
apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US"
google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN"

View File

@ -76,6 +76,7 @@ payone.base_url = "https://payment.preprod.payone.com/"
paypal.base_url = "https://api-m.sandbox.paypal.com/"
payu.base_url = "https://secure.snd.payu.com/"
placetopay.base_url = "https://test.placetopay.com/rest/gateway"
plaid.base_url = "https://sandbox.plaid.com"
powertranz.base_url = "https://staging.ptranz.com/api/"
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
rapyd.base_url = "https://sandboxapi.rapyd.net"
@ -316,6 +317,9 @@ open_banking_uk = { country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,M
[pm_filters.razorpay]
upi_collect = {country = "IN", currency = "INR"}
[pm_filters.plaid]
open_banking_pis = {currency = "EUR,GBP"}
[pm_filters.worldpay]
apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US"
google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN"

View File

@ -141,6 +141,7 @@ cards = [
"paypal",
"payu",
"placetopay",
"plaid",
"powertranz",
"prophetpay",
"shift4",
@ -227,6 +228,7 @@ payone.base_url = "https://payment.preprod.payone.com/"
paypal.base_url = "https://api-m.sandbox.paypal.com/"
payu.base_url = "https://secure.snd.payu.com/"
placetopay.base_url = "https://test.placetopay.com/rest/gateway"
plaid.base_url = "https://sandbox.plaid.com"
powertranz.base_url = "https://staging.ptranz.com/api/"
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
rapyd.base_url = "https://sandboxapi.rapyd.net"
@ -345,6 +347,9 @@ open_banking_uk = { country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,M
[pm_filters.razorpay]
upi_collect = {country = "IN", currency = "INR"}
[pm_filters.plaid]
open_banking_pis = {currency = "EUR,GBP"}
[pm_filters.adyen]
google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" }
apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,GB,SE,NO,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" }

View File

@ -162,6 +162,7 @@ payone.base_url = "https://payment.preprod.payone.com/"
paypal.base_url = "https://api-m.sandbox.paypal.com/"
payu.base_url = "https://secure.snd.payu.com/"
placetopay.base_url = "https://test.placetopay.com/rest/gateway"
plaid.base_url = "https://sandbox.plaid.com"
powertranz.base_url = "https://staging.ptranz.com/api/"
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
rapyd.base_url = "https://sandboxapi.rapyd.net"
@ -239,6 +240,7 @@ cards = [
"paypal",
"payu",
"placetopay",
"plaid",
"powertranz",
"prophetpay",
"shift4",
@ -355,6 +357,9 @@ open_banking_uk = { country = "DE,GB,AT,BE,CY,EE,ES,FI,FR,GR,HR,IE,IT,LT,LU,LV,M
[pm_filters.razorpay]
upi_collect = { country = "IN", currency = "INR" }
[pm_filters.plaid]
open_banking_pis = {currency = "EUR,GBP"}
[pm_filters.zen]
credit = { not_available_flows = { capture_method = "manual" } }
debit = { not_available_flows = { capture_method = "manual" } }

View File

@ -342,6 +342,7 @@ pub struct PaymentsPostProcessingData {
pub payment_method_data: PaymentMethodData,
pub customer_id: Option<id_type::CustomerId>,
pub connector_transaction_id: Option<String>,
pub country: Option<common_enums::CountryAlpha2>,
}
impl<F> TryFrom<RouterData<F, PaymentsAuthorizeData, response_types::PaymentsResponseData>>
@ -362,6 +363,11 @@ impl<F> TryFrom<RouterData<F, PaymentsAuthorizeData, response_types::PaymentsRes
_ => None,
},
customer_id: data.request.customer_id,
country: data
.address
.get_payment_billing()
.and_then(|bl| bl.address.as_ref())
.and_then(|address| address.country),
})
}
}

View File

@ -62,6 +62,7 @@ pub struct Connectors {
pub paypal: ConnectorParams,
pub payu: ConnectorParams,
pub placetopay: ConnectorParams,
pub plaid: ConnectorParams,
pub powertranz: ConnectorParams,
pub prophetpay: ConnectorParams,
pub rapyd: ConnectorParams,

View File

@ -448,7 +448,6 @@ impl<F, T>
pub struct PlaidAuthType {
pub client_id: Secret<String>,
pub secret: Secret<String>,
pub merchant_data: Option<types::MerchantRecipientData>,
}
impl TryFrom<&types::ConnectorAuthType> for PlaidAuthType {
@ -458,16 +457,6 @@ impl TryFrom<&types::ConnectorAuthType> for PlaidAuthType {
types::ConnectorAuthType::BodyKey { client_id, secret } => Ok(Self {
client_id: client_id.to_owned(),
secret: secret.to_owned(),
merchant_data: None,
}),
types::ConnectorAuthType::OpenBankingAuth {
api_key,
key1,
merchant_data,
} => Ok(Self {
client_id: api_key.to_owned(),
secret: key1.to_owned(),
merchant_data: Some(merchant_data.clone()),
}),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
}

View File

@ -218,11 +218,6 @@ pub enum ConnectorAuthType {
client_id: Secret<String>,
secret: Secret<String>,
},
OpenBankingAuth {
api_key: Secret<String>,
key1: Secret<String>,
merchant_data: MerchantRecipientData,
},
#[default]
NoKey,
}

View File

@ -47,6 +47,7 @@ pub mod payone;
pub mod paypal;
pub mod payu;
pub mod placetopay;
pub mod plaid;
pub mod powertranz;
pub mod prophetpay;
pub mod rapyd;
@ -81,7 +82,7 @@ pub use self::{
iatapay::Iatapay, itaubank::Itaubank, klarna::Klarna, mifinity::Mifinity, mollie::Mollie,
multisafepay::Multisafepay, netcetera::Netcetera, nexinets::Nexinets, nmi::Nmi, noon::Noon,
nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, payone::Payone,
paypal::Paypal, payu::Payu, placetopay::Placetopay, powertranz::Powertranz,
paypal::Paypal, payu::Payu, placetopay::Placetopay, plaid::Plaid, powertranz::Powertranz,
prophetpay::Prophetpay, rapyd::Rapyd, razorpay::Razorpay, riskified::Riskified, shift4::Shift4,
signifyd::Signifyd, square::Square, stax::Stax, stripe::Stripe, threedsecureio::Threedsecureio,
trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline,

View File

@ -0,0 +1,458 @@
pub mod transformers;
use common_utils::types::{AmountConvertor, FloatMajorUnit, FloatMajorUnitForConnector};
use error_stack::ResultExt;
use transformers as plaid;
use crate::{
configs::settings,
connector::utils as connector_utils,
core::errors::{self, CustomResult},
events::connector_api_logs::ConnectorEvent,
headers,
services::{
self,
request::{self, Mask},
ConnectorIntegration, ConnectorValidation,
},
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
ErrorResponse, RequestContent, Response,
},
utils::BytesExt,
};
#[derive(Clone)]
pub struct Plaid {
amount_converter: &'static (dyn AmountConvertor<Output = FloatMajorUnit> + Sync),
}
impl Plaid {
pub fn new() -> &'static Self {
&Self {
amount_converter: &FloatMajorUnitForConnector,
}
}
}
impl api::Payment for Plaid {}
impl api::PaymentSession for Plaid {}
impl api::ConnectorAccessToken for Plaid {}
impl api::MandateSetup for Plaid {}
impl api::PaymentAuthorize for Plaid {}
impl api::PaymentSync for Plaid {}
impl api::PaymentCapture for Plaid {}
impl api::PaymentVoid for Plaid {}
impl api::Refund for Plaid {}
impl api::RefundExecute for Plaid {}
impl api::RefundSync for Plaid {}
impl api::PaymentToken for Plaid {}
impl api::PaymentsPostProcessing for Plaid {}
impl
ConnectorIntegration<
api::PaymentMethodToken,
types::PaymentMethodTokenizationData,
types::PaymentsResponseData,
> for Plaid
{
// Not Implemented (R)
}
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Plaid
where
Self: ConnectorIntegration<Flow, Request, Response>,
{
fn build_headers(
&self,
req: &types::RouterData<Flow, Request, Response>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let mut header = vec![(
headers::CONTENT_TYPE.to_string(),
self.get_content_type().to_string().into(),
)];
let mut auth = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut auth);
Ok(header)
}
}
impl ConnectorCommon for Plaid {
fn id(&self) -> &'static str {
"plaid"
}
fn get_currency_unit(&self) -> api::CurrencyUnit {
api::CurrencyUnit::Base
}
fn common_get_content_type(&self) -> &'static str {
"application/json"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.plaid.base_url.as_ref()
}
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
let auth = plaid::PlaidAuthType::try_from(auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
let client_id = auth.client_id.into_masked();
let secret = auth.secret.into_masked();
Ok(vec![
("PLAID-CLIENT-ID".to_string(), client_id),
("PLAID-SECRET".to_string(), secret),
])
}
fn build_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: plaid::PlaidErrorResponse =
res.response
.parse_struct("PlaidErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
Ok(ErrorResponse {
status_code: res.status_code,
code: response
.error_code
.unwrap_or(crate::consts::NO_ERROR_CODE.to_string()),
message: response.error_message,
reason: response.display_message,
attempt_status: None,
connector_transaction_id: None,
})
}
}
impl ConnectorValidation for Plaid {
//TODO: implement functions when support enabled
}
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Plaid
{
//TODO: implement sessions flow
}
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
for Plaid
{
}
impl
ConnectorIntegration<
api::SetupMandate,
types::SetupMandateRequestData,
types::PaymentsResponseData,
> for Plaid
{
}
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Plaid
{
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
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> {
Ok(format!(
"{}/payment_initiation/payment/create",
self.base_url(connectors)
))
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let amount = connector_utils::convert_amount(
self.amount_converter,
req.request.minor_amount,
req.request.currency,
)?;
let connector_router_data = plaid::PlaidRouterData::from((amount, req));
let connector_req = plaid::PlaidPaymentsRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_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,
)?)
.attach_default_headers()
.headers(types::PaymentsAuthorizeType::get_headers(
self, req, connectors,
)?)
.set_body(types::PaymentsAuthorizeType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsAuthorizeRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: plaid::PlaidPaymentsResponse = res
.response
.parse_struct("PlaidPaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Plaid
{
fn get_headers(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_request_body(
&self,
req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = plaid::PlaidSyncRequest::try_from(req)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn get_url(
&self,
_req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}/payment_initiation/payment/get",
self.base_url(connectors)
))
}
fn build_request(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
.set_body(types::PaymentsSyncType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: plaid::PlaidSyncResponse = res
.response
.parse_struct("PlaidSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl
ConnectorIntegration<
api::PostProcessing,
types::PaymentsPostProcessingData,
types::PaymentsResponseData,
> for Plaid
{
fn get_headers(
&self,
req: &types::PaymentsPostProcessingRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::PaymentsPostProcessingRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}/link/token/create", self.base_url(connectors)))
}
fn get_request_body(
&self,
req: &types::PaymentsPostProcessingRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = plaid::PlaidLinkTokenRequest::try_from(req)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::PaymentsPostProcessingRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsPostProcessingType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::PaymentsPostProcessingType::get_headers(
self, req, connectors,
)?)
.set_body(types::PaymentsPostProcessingType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsPostProcessingRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsPostProcessingRouterData, errors::ConnectorError> {
let response: plaid::PlaidLinkTokenResponse = res
.response
.parse_struct("PlaidLinkTokenResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Plaid
{
}
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Plaid
{
}
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData> for Plaid {}
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Plaid {}
#[async_trait::async_trait]
impl api::IncomingWebhook for Plaid {
fn get_webhook_object_reference_id(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::webhooks::ObjectReferenceId, errors::ConnectorError> {
Err((errors::ConnectorError::WebhooksNotImplemented).into())
}
fn get_webhook_event_type(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
Err((errors::ConnectorError::WebhooksNotImplemented).into())
}
fn get_webhook_resource_object(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
Err((errors::ConnectorError::WebhooksNotImplemented).into())
}
}

View File

@ -0,0 +1,389 @@
use common_enums::Currency;
use common_utils::types::FloatMajorUnit;
use error_stack::ResultExt;
use masking::{PeekInterface, Secret};
use serde::{Deserialize, Serialize};
use crate::{
connector::utils::is_payment_failure,
core::errors,
types::{self, api, domain, storage::enums},
};
pub struct PlaidRouterData<T> {
pub amount: FloatMajorUnit,
pub router_data: T,
}
impl<T> From<(FloatMajorUnit, T)> for PlaidRouterData<T> {
fn from((amount, item): (FloatMajorUnit, T)) -> Self {
Self {
amount,
router_data: item,
}
}
}
#[derive(Default, Debug, Serialize)]
pub struct PlaidPaymentsRequest {
amount: PlaidAmount,
recipient_id: String,
reference: String,
#[serde(skip_serializing_if = "Option::is_none")]
schedule: Option<PlaidSchedule>,
#[serde(skip_serializing_if = "Option::is_none")]
options: Option<PlaidOptions>,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct PlaidAmount {
currency: Currency,
value: FloatMajorUnit,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct PlaidSchedule {
interval: String,
interval_execution_day: String,
start_date: String,
end_date: Option<String>,
adjusted_start_date: Option<String>,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct PlaidOptions {
request_refund_details: bool,
iban: Option<Secret<String>>,
bacs: Option<PlaidBacs>,
scheme: String,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct PlaidBacs {
account: Secret<String>,
sort_code: Secret<String>,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct PlaidLinkTokenRequest {
client_name: String,
country_codes: Vec<String>,
language: String,
products: Vec<String>,
user: User,
payment_initiation: PlaidPaymentInitiation,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct User {
pub client_user_id: String,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct PlaidPaymentInitiation {
payment_id: String,
}
impl TryFrom<&PlaidRouterData<&types::PaymentsAuthorizeRouterData>> for PlaidPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &PlaidRouterData<&types::PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
match item.router_data.request.payment_method_data.clone() {
domain::PaymentMethodData::OpenBanking(ref data) => match data {
domain::OpenBankingData::OpenBankingPIS { .. } => {
let amount = item.amount;
let currency = item.router_data.request.currency;
let payment_id = item.router_data.payment_id.clone();
let id_len = payment_id.len();
let reference = if id_len > 18 {
payment_id.get(id_len - 18..id_len).map(|id| id.to_string())
} else {
Some(payment_id)
}
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "payment_id",
})?;
let recipient_val = item
.router_data
.connector_meta_data
.as_ref()
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "connector_customer",
})?
.peek()
.clone();
let recipient_type =
serde_json::from_value::<types::MerchantRecipientData>(recipient_val)
.change_context(errors::ConnectorError::ParsingFailed)?;
let recipient_id = match recipient_type {
types::MerchantRecipientData::ConnectorRecipientId(id) => {
Ok(id.peek().to_string())
}
_ => Err(errors::ConnectorError::MissingRequiredField {
field_name: "ConnectorRecipientId",
}),
}?;
Ok(Self {
amount: PlaidAmount {
currency,
value: amount,
},
reference,
recipient_id,
schedule: None,
options: None,
})
}
},
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
}
}
}
impl TryFrom<&types::PaymentsSyncRouterData> for PlaidSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsSyncRouterData) -> Result<Self, Self::Error> {
match item.request.connector_transaction_id {
types::ResponseId::ConnectorTransactionId(ref id) => Ok(Self {
payment_id: id.clone(),
}),
_ => Err((errors::ConnectorError::MissingConnectorTransactionID).into()),
}
}
}
impl TryFrom<&types::PaymentsPostProcessingRouterData> for PlaidLinkTokenRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsPostProcessingRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data.clone() {
domain::PaymentMethodData::OpenBanking(ref data) => match data {
domain::OpenBankingData::OpenBankingPIS { .. } => Ok(Self {
client_name: "Hyperswitch".to_string(),
country_codes: item
.request
.country
.map(|code| vec![code.to_string()])
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "billing.address.country",
})?,
language: "en".to_string(),
products: vec!["payment_initiation".to_string()],
user: User {
client_user_id: item
.request
.customer_id
.clone()
.map(|id| id.get_string_repr().to_string())
.unwrap_or("default cust".to_string()),
},
payment_initiation: PlaidPaymentInitiation {
payment_id: item
.request
.connector_transaction_id
.clone()
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?,
},
}),
},
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
}
}
}
pub struct PlaidAuthType {
pub client_id: Secret<String>,
pub secret: Secret<String>,
}
impl TryFrom<&types::ConnectorAuthType> for PlaidAuthType {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
match auth_type {
types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self {
client_id: api_key.to_owned(),
secret: key1.to_owned(),
}),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[derive(strum::Display)]
pub enum PlaidPaymentStatus {
PaymentStatusInputNeeded,
PaymentStatusInitiated,
PaymentStatusInsufficientFunds,
PaymentStatusFailed,
PaymentStatusBlocked,
PaymentStatusCancelled,
PaymentStatusExecuted,
PaymentStatusSettled,
PaymentStatusEstablished,
PaymentStatusRejected,
PaymentStatusAuthorising,
}
impl From<PlaidPaymentStatus> for enums::AttemptStatus {
fn from(item: PlaidPaymentStatus) -> Self {
match item {
PlaidPaymentStatus::PaymentStatusAuthorising => Self::Authorizing,
PlaidPaymentStatus::PaymentStatusBlocked => Self::AuthorizationFailed,
PlaidPaymentStatus::PaymentStatusCancelled => Self::Voided,
PlaidPaymentStatus::PaymentStatusEstablished => Self::Authorized,
PlaidPaymentStatus::PaymentStatusExecuted => Self::Authorized,
PlaidPaymentStatus::PaymentStatusFailed => Self::Failure,
PlaidPaymentStatus::PaymentStatusInitiated => Self::AuthenticationPending,
PlaidPaymentStatus::PaymentStatusInputNeeded => Self::AuthenticationPending,
PlaidPaymentStatus::PaymentStatusInsufficientFunds => Self::AuthorizationFailed,
PlaidPaymentStatus::PaymentStatusRejected => Self::AuthorizationFailed,
PlaidPaymentStatus::PaymentStatusSettled => Self::Charged,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PlaidPaymentsResponse {
status: PlaidPaymentStatus,
payment_id: String,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, PlaidPaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, PlaidPaymentsResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let status = enums::AttemptStatus::from(item.response.status.clone());
Ok(Self {
status,
response: if is_payment_failure(status) {
Err(types::ErrorResponse {
// populating status everywhere as plaid only sends back a status
code: item.response.status.clone().to_string(),
message: item.response.status.clone().to_string(),
reason: Some(item.response.status.to_string()),
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: Some(item.response.payment_id),
})
} else {
Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
item.response.payment_id.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: Some(item.response.payment_id),
incremental_authorization_allowed: None,
charge_id: None,
})
},
..item.data
})
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct PlaidLinkTokenResponse {
link_token: String,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, PlaidLinkTokenResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, PlaidLinkTokenResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let session_token = Some(api::OpenBankingSessionToken {
open_banking_session_token: item.response.link_token,
});
Ok(Self {
status: enums::AttemptStatus::AuthenticationPending,
response: Ok(types::PaymentsResponseData::PostProcessingResponse { session_token }),
..item.data
})
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct PlaidSyncRequest {
payment_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PlaidSyncResponse {
payment_id: String,
amount: PlaidAmount,
status: PlaidPaymentStatus,
recipient_id: String,
reference: String,
last_status_update: String,
adjusted_reference: Option<String>,
schedule: Option<PlaidSchedule>,
iban: Option<Secret<String>>,
bacs: Option<PlaidBacs>,
scheme: Option<String>,
adjusted_scheme: Option<String>,
request_id: String,
}
impl<F, T> TryFrom<types::ResponseRouterData<F, PlaidSyncResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, PlaidSyncResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let status = enums::AttemptStatus::from(item.response.status.clone());
Ok(Self {
status,
response: if is_payment_failure(status) {
Err(types::ErrorResponse {
// populating status everywhere as plaid only sends back a status
code: item.response.status.clone().to_string(),
message: item.response.status.clone().to_string(),
reason: Some(item.response.status.to_string()),
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: Some(item.response.payment_id),
})
} else {
Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
item.response.payment_id.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: Some(item.response.payment_id),
incremental_authorization_allowed: None,
charge_id: None,
})
},
..item.data
})
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct PlaidErrorResponse {
pub display_message: Option<String>,
pub error_code: Option<String>,
pub error_message: String,
pub error_type: Option<String>,
}

View File

@ -1256,6 +1256,8 @@ pub async fn create_payment_connector(
expected_format: "auth_type and api_key".to_string(),
})?;
validate_auth_and_metadata_type(req.connector_name, &auth, &req.metadata)?;
let merchant_recipient_data = if let Some(data) = &req.additional_merchant_data {
Some(
process_open_banking_connectors(
@ -1280,8 +1282,6 @@ pub async fn create_payment_connector(
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get MerchantRecipientData")?;
validate_auth_and_metadata_type(req.connector_name, &auth, &req.metadata)?;
let frm_configs = get_frm_config_as_secret(req.frm_configs);
// The purpose of this merchant account update is just to update the

View File

@ -699,7 +699,8 @@ default_imp_for_new_connector_integration_payment!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_refund {
@ -784,7 +785,8 @@ default_imp_for_new_connector_integration_refund!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_connector_access_token {
@ -864,7 +866,8 @@ default_imp_for_new_connector_integration_connector_access_token!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_accept_dispute {
@ -966,7 +969,8 @@ default_imp_for_new_connector_integration_accept_dispute!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_defend_dispute {
@ -1050,7 +1054,8 @@ default_imp_for_new_connector_integration_defend_dispute!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
default_imp_for_new_connector_integration_submit_evidence!(
connector::Aci,
@ -1118,7 +1123,8 @@ default_imp_for_new_connector_integration_submit_evidence!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_file_upload {
@ -1213,7 +1219,8 @@ default_imp_for_new_connector_integration_file_upload!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_payouts {
@ -1290,7 +1297,8 @@ default_imp_for_new_connector_integration_payouts!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "payouts")]
@ -1377,7 +1385,8 @@ default_imp_for_new_connector_integration_payouts_create!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "payouts")]
@ -1464,7 +1473,8 @@ default_imp_for_new_connector_integration_payouts_eligibility!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "payouts")]
@ -1551,7 +1561,8 @@ default_imp_for_new_connector_integration_payouts_fulfill!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "payouts")]
@ -1638,7 +1649,8 @@ default_imp_for_new_connector_integration_payouts_cancel!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "payouts")]
@ -1725,7 +1737,8 @@ default_imp_for_new_connector_integration_payouts_quote!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "payouts")]
@ -1812,7 +1825,8 @@ default_imp_for_new_connector_integration_payouts_recipient!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "payouts")]
@ -1899,7 +1913,8 @@ default_imp_for_new_connector_integration_payouts_sync!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "payouts")]
@ -1986,7 +2001,8 @@ default_imp_for_new_connector_integration_payouts_recipient_account!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_webhook_source_verification {
@ -2071,7 +2087,8 @@ default_imp_for_new_connector_integration_webhook_source_verification!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_frm {
@ -2148,7 +2165,8 @@ default_imp_for_new_connector_integration_frm!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "frm")]
@ -2235,7 +2253,8 @@ default_imp_for_new_connector_integration_frm_sale!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "frm")]
@ -2322,7 +2341,8 @@ default_imp_for_new_connector_integration_frm_checkout!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "frm")]
@ -2409,7 +2429,8 @@ default_imp_for_new_connector_integration_frm_transaction!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "frm")]
@ -2496,7 +2517,8 @@ default_imp_for_new_connector_integration_frm_fulfillment!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
#[cfg(feature = "frm")]
@ -2583,7 +2605,8 @@ default_imp_for_new_connector_integration_frm_record_return!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_revoking_mandates {
@ -2667,7 +2690,8 @@ default_imp_for_new_connector_integration_revoking_mandates!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);
macro_rules! default_imp_for_new_connector_integration_connector_authentication {
@ -2779,5 +2803,6 @@ default_imp_for_new_connector_integration_connector_authentication!(
connector::Worldline,
connector::Worldpay,
connector::Zen,
connector::Zsl
connector::Zsl,
connector::Plaid
);

View File

@ -221,6 +221,7 @@ default_imp_for_complete_authorize!(
connector::Payone,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Rapyd,
connector::Razorpay,
connector::Riskified,
@ -311,6 +312,7 @@ default_imp_for_webhook_source_verification!(
connector::Payone,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -406,6 +408,7 @@ default_imp_for_create_customer!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -489,6 +492,7 @@ default_imp_for_connector_redirect_response!(
connector::Payone,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -565,6 +569,7 @@ default_imp_for_connector_request_id!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -662,6 +667,7 @@ default_imp_for_accept_dispute!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -778,6 +784,7 @@ default_imp_for_file_upload!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -872,6 +879,7 @@ default_imp_for_submit_evidence!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -966,6 +974,7 @@ default_imp_for_defend_dispute!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1070,6 +1079,7 @@ default_imp_for_pre_processing_steps!(
connector::Payone,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1222,6 +1232,7 @@ default_imp_for_payouts!(
connector::Payme,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1314,6 +1325,7 @@ default_imp_for_payouts_create!(
connector::Payone,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1408,6 +1420,7 @@ default_imp_for_payouts_retrieve!(
connector::Payone,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1506,6 +1519,7 @@ default_imp_for_payouts_eligibility!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1596,6 +1610,7 @@ default_imp_for_payouts_fulfill!(
connector::Payme,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1689,6 +1704,7 @@ default_imp_for_payouts_cancel!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1783,6 +1799,7 @@ default_imp_for_payouts_quote!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1878,6 +1895,7 @@ default_imp_for_payouts_recipient!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -1976,6 +1994,7 @@ default_imp_for_payouts_recipient_account!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2071,6 +2090,7 @@ default_imp_for_approve!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2167,6 +2187,7 @@ default_imp_for_reject!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2247,6 +2268,7 @@ default_imp_for_fraud_check!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2343,6 +2365,7 @@ default_imp_for_frm_sale!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2439,6 +2462,7 @@ default_imp_for_frm_checkout!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2535,6 +2559,7 @@ default_imp_for_frm_transaction!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2631,6 +2656,7 @@ default_imp_for_frm_fulfillment!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2727,6 +2753,7 @@ default_imp_for_frm_record_return!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2820,6 +2847,7 @@ default_imp_for_incremental_authorization!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -2912,6 +2940,7 @@ default_imp_for_revoking_mandates!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -3065,6 +3094,7 @@ default_imp_for_connector_authentication!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,
@ -3156,6 +3186,7 @@ default_imp_for_authorize_session_token!(
connector::Paypal,
connector::Payu,
connector::Placetopay,
connector::Plaid,
connector::Powertranz,
connector::Prophetpay,
connector::Rapyd,

View File

@ -210,7 +210,6 @@ impl ForeignTryFrom<&types::ConnectorAuthType> for PlaidAuthType {
Ok::<Self, errors::ConnectorError>(Self {
client_id: api_key.to_owned(),
secret: key1.to_owned(),
merchant_data: None,
})
}
_ => Err(errors::ConnectorError::FailedToObtainAuthType),

View File

@ -500,8 +500,10 @@ impl ConnectorData {
enums::Connector::Volt => Ok(ConnectorEnum::Old(Box::new(&connector::Volt))),
enums::Connector::Zen => Ok(ConnectorEnum::Old(Box::new(&connector::Zen))),
enums::Connector::Zsl => Ok(ConnectorEnum::Old(Box::new(&connector::Zsl))),
enums::Connector::Plaid => {
Ok(ConnectorEnum::Old(Box::new(connector::Plaid::new())))
}
enums::Connector::Signifyd
| enums::Connector::Plaid
| enums::Connector::Riskified
| enums::Connector::Gpayments
| enums::Connector::Threedsecureio => {

View File

@ -4,8 +4,8 @@ pub use hyperswitch_domain_models::payment_method_data::{
CardToken, CashappQr, CryptoData, GcashRedirection, GiftCardData, GiftCardDetails,
GoPayRedirection, GooglePayPaymentMethodInfo, GooglePayRedirectData,
GooglePayThirdPartySdkData, GooglePayWalletData, GpayTokenizationData, IndomaretVoucherData,
KakaoPayRedirection, MbWayRedirection, MifinityData, PayLaterData, PaymentMethodData,
RealTimePaymentData, SamsungPayWalletData, SepaAndBacsBillingDetails, SwishQrData,
TouchNGoRedirection, UpiCollectData, UpiData, UpiIntentData, VoucherData, WalletData,
WeChatPayQr,
KakaoPayRedirection, MbWayRedirection, MifinityData, OpenBankingData, PayLaterData,
PaymentMethodData, RealTimePaymentData, SamsungPayWalletData, SepaAndBacsBillingDetails,
SwishQrData, TouchNGoRedirection, UpiCollectData, UpiData, UpiIntentData, VoucherData,
WalletData, WeChatPayQr,
};

View File

@ -274,11 +274,7 @@ impl ForeignTryFrom<api_enums::Connector> for common_enums::RoutableConnectors {
api_enums::Connector::Paypal => Self::Paypal,
api_enums::Connector::Payu => Self::Payu,
api_models::enums::Connector::Placetopay => Self::Placetopay,
api_enums::Connector::Plaid => {
Err(common_utils::errors::ValidationError::InvalidValue {
message: "plaid is not a routable connector".to_string(),
})?
}
api_enums::Connector::Plaid => Self::Plaid,
api_enums::Connector::Powertranz => Self::Powertranz,
api_enums::Connector::Prophetpay => Self::Prophetpay,
api_enums::Connector::Rapyd => Self::Rapyd,

View File

@ -57,6 +57,7 @@ mod payone;
mod paypal;
mod payu;
mod placetopay;
mod plaid;
mod powertranz;
#[cfg(feature = "dummy_connector")]
mod prophetpay;

View File

@ -0,0 +1,421 @@
use masking::Secret;
use router::types::{self, api, domain, storage::enums};
use test_utils::connector_auth;
use crate::utils::{self, Connector, ConnectorActions};
#[derive(Clone, Copy)]
struct PlaidTest;
impl ConnectorActions for PlaidTest {}
static CONNECTOR: PlaidTest = PlaidTest {};
impl Connector for PlaidTest {
fn get_data(&self) -> api::ConnectorData {
use router::connector::Plaid;
utils::construct_connector_data_old(
Box::new(Plaid::new()),
types::Connector::Plaid,
api::GetToken::Connector,
None,
)
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
utils::to_connector_auth_type(
connector_auth::ConnectorAuthentication::new()
.plaid
.expect("Missing connector authentication configuration")
.into(),
)
}
fn get_name(&self) -> String {
"plaid".to_string()
}
}
fn get_default_payment_info() -> Option<utils::PaymentInfo> {
None
}
fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
None
}
// Cards Positive Tests
// Creates a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_only_authorize_payment() {
let response = CONNECTOR
.authorize_payment(payment_method_details(), get_default_payment_info())
.await
.expect("Authorize payment response");
assert_eq!(response.status, enums::AttemptStatus::Authorized);
}
// Captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info())
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
// Partially captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(
payment_method_details(),
Some(types::PaymentsCaptureData {
amount_to_capture: 50,
..utils::PaymentCaptureType::default().0
}),
get_default_payment_info(),
)
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
// Synchronizes a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_authorized_payment() {
let authorize_response = CONNECTOR
.authorize_payment(payment_method_details(), get_default_payment_info())
.await
.expect("Authorize payment response");
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Authorized,
Some(types::PaymentsSyncData {
connector_transaction_id: types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
..Default::default()
}),
get_default_payment_info(),
)
.await
.expect("PSync response");
assert_eq!(response.status, enums::AttemptStatus::Authorized,);
}
// Voids a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_void_authorized_payment() {
let response = CONNECTOR
.authorize_and_void_payment(
payment_method_details(),
Some(types::PaymentsCancelData {
connector_transaction_id: String::from(""),
cancellation_reason: Some("requested_by_customer".to_string()),
..Default::default()
}),
get_default_payment_info(),
)
.await
.expect("Void payment response");
assert_eq!(response.status, enums::AttemptStatus::Voided);
}
// Refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_manually_captured_payment() {
let response = CONNECTOR
.capture_payment_and_refund(
payment_method_details(),
None,
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Partially refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_manually_captured_payment() {
let response = CONNECTOR
.capture_payment_and_refund(
payment_method_details(),
None,
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Synchronizes a refund using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_manually_captured_refund() {
let refund_response = CONNECTOR
.capture_payment_and_refund(
payment_method_details(),
None,
None,
get_default_payment_info(),
)
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Creates a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_make_payment() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
}
// Synchronizes a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_auto_captured_payment() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Charged,
Some(types::PaymentsSyncData {
connector_transaction_id: types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
capture_method: Some(enums::CaptureMethod::Automatic),
..Default::default()
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Charged,);
}
// Refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_auto_captured_payment() {
let response = CONNECTOR
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Partially refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_succeeded_payment() {
let refund_response = CONNECTOR
.make_payment_and_refund(
payment_method_details(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
refund_response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_succeeded_payment_multiple_times() {
CONNECTOR
.make_payment_and_multiple_refund(
payment_method_details(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await;
}
// Synchronizes a refund using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_refund() {
let refund_response = CONNECTOR
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Cards Negative scenarios
// Creates a payment with incorrect CVC.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_cvc() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: domain::PaymentMethodData::Card(domain::Card {
card_cvc: Secret::new("12345".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Invalid card cvc.".to_string(),
);
}
// Creates a payment with incorrect expiry month.
#[actix_web::test]
async fn should_fail_payment_for_invalid_exp_month() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: domain::PaymentMethodData::Card(domain::Card {
card_exp_month: Secret::new("20".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Your card's expiration month is invalid.".to_string(),
);
}
// Creates a payment with incorrect expiry year.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_expiry_year() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: domain::PaymentMethodData::Card(domain::Card {
card_exp_year: Secret::new("2000".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Your card's expiration year is invalid.".to_string(),
);
}
// Voids a payment using automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_fail_void_payment_for_auto_capture() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
let void_response = CONNECTOR
.void_payment(txn_id.unwrap(), None, get_default_payment_info())
.await
.unwrap();
assert_eq!(
void_response.response.unwrap_err().message,
"You cannot cancel this PaymentIntent because it has a status of succeeded."
);
}
// Captures a payment using invalid connector payment id.
#[actix_web::test]
async fn should_fail_capture_for_invalid_payment() {
let capture_response = CONNECTOR
.capture_payment("123456789".to_string(), None, get_default_payment_info())
.await
.unwrap();
assert_eq!(
capture_response.response.unwrap_err().message,
String::from("No such payment_intent: '123456789'")
);
}
// Refunds a payment with refund amount higher than payment amount.
#[actix_web::test]
async fn should_fail_for_refund_amount_higher_than_payment_amount() {
let response = CONNECTOR
.make_payment_and_refund(
payment_method_details(),
Some(types::RefundsData {
refund_amount: 150,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Refund amount (₹1.50) is greater than charge amount (₹1.00)",
);
}
// Connector dependent test cases goes here
// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests

View File

@ -195,6 +195,10 @@ api_key= "Login"
key1= "Trankey"
[plaid]
api_key="Client Id"
key1= "Secret"
[threedsecureio]
api_key="API Key"

View File

@ -60,6 +60,7 @@ pub struct ConnectorAuthentication {
pub paypal: Option<BodyKey>,
pub payu: Option<BodyKey>,
pub placetopay: Option<BodyKey>,
pub plaid: Option<BodyKey>,
pub powertranz: Option<BodyKey>,
pub prophetpay: Option<HeaderKey>,
pub rapyd: Option<BodyKey>,

View File

@ -127,6 +127,7 @@ payone.base_url = "https://payment.preprod.payone.com/"
paypal.base_url = "https://api-m.sandbox.paypal.com/"
payu.base_url = "https://secure.snd.payu.com/"
placetopay.base_url = "https://test.placetopay.com/rest/gateway"
plaid.base_url = "https://sandbox.plaid.com"
powertranz.base_url = "https://staging.ptranz.com/api/"
prophetpay.base_url = "https://ccm-thirdparty.cps.golf/"
rapyd.base_url = "https://sandboxapi.rapyd.net"
@ -204,6 +205,7 @@ cards = [
"paypal",
"payu",
"placetopay",
"plaid",
"powertranz",
"prophetpay",
"shift4",

View File

@ -6,7 +6,7 @@ function find_prev_connector() {
git checkout $self
cp $self $self.tmp
# Add new connector to existing list and sort it
connectors=(aci adyen adyenplatform airwallex applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource datatrans dlocal dummyconnector ebanx fiserv forte globalpay globepay gocardless gpayments helcim iatapay itaubank klarna mifinity mollie multisafepay netcetera nexinets noon nuvei opayo opennode payeezy payme payone paypal payu placetopay powertranz prophetpay rapyd razorpay shift4 square stax stripe threedsecureio trustpay tsys volt wise worldline worldpay zsl "$1")
connectors=(aci adyen adyenplatform airwallex applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource datatrans dlocal dummyconnector ebanx fiserv forte globalpay globepay gocardless gpayments helcim iatapay itaubank klarna mifinity mollie multisafepay netcetera nexinets noon nuvei opayo opennode payeezy payme payone paypal payu placetopay plaid powertranz prophetpay rapyd razorpay shift4 square stax stripe threedsecureio trustpay tsys volt wise worldline worldpay zsl "$1")
IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS
res=`echo ${sorted[@]}`
sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp