feat(connector): [Cybersource] Add payout flows for Card (#4511)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sakil Mostak
2024-05-07 12:48:22 +05:30
committed by GitHub
parent df2c2ca22d
commit a72f040d92
7 changed files with 434 additions and 10 deletions

View File

@ -348,6 +348,7 @@ pub enum PayoutConnectors {
Wise, Wise,
Paypal, Paypal,
Ebanx, Ebanx,
Cybersource,
} }
#[cfg(feature = "payouts")] #[cfg(feature = "payouts")]
@ -359,6 +360,7 @@ impl From<PayoutConnectors> for RoutableConnectors {
PayoutConnectors::Wise => Self::Wise, PayoutConnectors::Wise => Self::Wise,
PayoutConnectors::Paypal => Self::Paypal, PayoutConnectors::Paypal => Self::Paypal,
PayoutConnectors::Ebanx => Self::Ebanx, PayoutConnectors::Ebanx => Self::Ebanx,
PayoutConnectors::Cybersource => Self::Cybersource,
} }
} }
} }
@ -372,6 +374,7 @@ impl From<PayoutConnectors> for Connector {
PayoutConnectors::Wise => Self::Wise, PayoutConnectors::Wise => Self::Wise,
PayoutConnectors::Paypal => Self::Paypal, PayoutConnectors::Paypal => Self::Paypal,
PayoutConnectors::Ebanx => Self::Ebanx, PayoutConnectors::Ebanx => Self::Ebanx,
PayoutConnectors::Cybersource => Self::Cybersource,
} }
} }
} }
@ -386,6 +389,7 @@ impl TryFrom<Connector> for PayoutConnectors {
Connector::Wise => Ok(Self::Wise), Connector::Wise => Ok(Self::Wise),
Connector::Paypal => Ok(Self::Paypal), Connector::Paypal => Ok(Self::Paypal),
Connector::Ebanx => Ok(Self::Ebanx), Connector::Ebanx => Ok(Self::Ebanx),
Connector::Cybersource => Ok(Self::Cybersource),
_ => Err(format!("Invalid payout connector {}", value)), _ => Err(format!("Invalid payout connector {}", value)),
} }
} }

View File

@ -133,6 +133,8 @@ pub struct ConnectorConfig {
pub coinbase: Option<ConnectorTomlConfig>, pub coinbase: Option<ConnectorTomlConfig>,
pub cryptopay: Option<ConnectorTomlConfig>, pub cryptopay: Option<ConnectorTomlConfig>,
pub cybersource: Option<ConnectorTomlConfig>, pub cybersource: Option<ConnectorTomlConfig>,
#[cfg(feature = "payouts")]
pub cybersource_payout: Option<ConnectorTomlConfig>,
pub iatapay: Option<ConnectorTomlConfig>, pub iatapay: Option<ConnectorTomlConfig>,
pub opennode: Option<ConnectorTomlConfig>, pub opennode: Option<ConnectorTomlConfig>,
pub bambora: Option<ConnectorTomlConfig>, pub bambora: Option<ConnectorTomlConfig>,
@ -223,6 +225,7 @@ impl ConnectorConfig {
PayoutConnectors::Wise => Ok(connector_data.wise_payout), PayoutConnectors::Wise => Ok(connector_data.wise_payout),
PayoutConnectors::Paypal => Ok(connector_data.paypal_payout), PayoutConnectors::Paypal => Ok(connector_data.paypal_payout),
PayoutConnectors::Ebanx => Ok(connector_data.ebanx_payout), PayoutConnectors::Ebanx => Ok(connector_data.ebanx_payout),
PayoutConnectors::Cybersource => Ok(connector_data.cybersource_payout),
} }
} }

View File

@ -304,6 +304,10 @@ impl api::PaymentsPreProcessing for Cybersource {}
impl api::PaymentsCompleteAuthorize for Cybersource {} impl api::PaymentsCompleteAuthorize for Cybersource {}
impl api::ConnectorMandateRevoke for Cybersource {} impl api::ConnectorMandateRevoke for Cybersource {}
impl api::Payouts for Cybersource {}
#[cfg(feature = "payouts")]
impl api::PayoutFulfill for Cybersource {}
impl impl
ConnectorIntegration< ConnectorIntegration<
api::PaymentMethodToken, api::PaymentMethodToken,
@ -965,6 +969,124 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
} }
} }
#[cfg(feature = "payouts")]
impl ConnectorIntegration<api::PoFulfill, types::PayoutsData, types::PayoutsResponseData>
for Cybersource
{
fn get_url(
&self,
_req: &types::PayoutsRouterData<api::PoFulfill>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}pts/v2/payouts", self.base_url(connectors)))
}
fn get_headers(
&self,
req: &types::PayoutsRouterData<api::PoFulfill>,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_request_body(
&self,
req: &types::PayoutsRouterData<api::PoFulfill>,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_router_data = cybersource::CybersourceRouterData::try_from((
&self.get_currency_unit(),
req.request.destination_currency,
req.request.amount,
req,
))?;
let connector_req =
cybersource::CybersourcePayoutFulfillRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::PayoutsRouterData<api::PoFulfill>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PayoutFulfillType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::PayoutFulfillType::get_headers(
self, req, connectors,
)?)
.set_body(types::PayoutFulfillType::get_request_body(
self, req, connectors,
)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::PayoutsRouterData<api::PoFulfill>,
event_builder: Option<&mut ConnectorEvent>,
res: types::Response,
) -> CustomResult<types::PayoutsRouterData<api::PoFulfill>, errors::ConnectorError> {
let response: cybersource::CybersourceFulfillResponse = res
.response
.parse_struct("CybersourceFulfillResponse")
.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: types::Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
fn get_5xx_error_response(
&self,
res: types::Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: cybersource::CybersourceServerErrorResponse = res
.response
.parse_struct("CybersourceServerErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|event| event.set_response_body(&response));
router_env::logger::info!(error_response=?response);
let attempt_status = match response.reason {
Some(reason) => match reason {
transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure),
transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None,
},
None => None,
};
Ok(types::ErrorResponse {
status_code: res.status_code,
reason: response.status.clone(),
code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: response
.message
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
attempt_status,
connector_transaction_id: None,
})
}
}
impl impl
ConnectorIntegration< ConnectorIntegration<
api::CompleteAuthorize, api::CompleteAuthorize,

View File

@ -1,4 +1,9 @@
use api_models::payments; use api_models::payments;
#[cfg(feature = "payouts")]
use api_models::{
payments::{AddressDetails, PhoneDetails},
payouts::PayoutMethodData,
};
use base64::Engine; use base64::Engine;
use common_enums::FutureUsage; use common_enums::FutureUsage;
use common_utils::{ext_traits::ValueExt, pii}; use common_utils::{ext_traits::ValueExt, pii};
@ -111,7 +116,7 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest {
number: ccard.card_number, number: ccard.card_number,
expiration_month: ccard.card_exp_month, expiration_month: ccard.card_exp_month,
expiration_year: ccard.card_exp_year, expiration_year: ccard.card_exp_year,
security_code: ccard.card_cvc, security_code: Some(ccard.card_cvc),
card_type, card_type,
}, },
}), }),
@ -404,7 +409,7 @@ pub struct Card {
number: cards::CardNumber, number: cards::CardNumber,
expiration_month: Secret<String>, expiration_month: Secret<String>,
expiration_year: Secret<String>, expiration_year: Secret<String>,
security_code: Secret<String>, security_code: Option<Secret<String>>,
#[serde(rename = "type")] #[serde(rename = "type")]
card_type: Option<String>, card_type: Option<String>,
} }
@ -849,7 +854,7 @@ impl
number: ccard.card_number, number: ccard.card_number,
expiration_month: ccard.card_exp_month, expiration_month: ccard.card_exp_month,
expiration_year: ccard.card_exp_year, expiration_year: ccard.card_exp_year,
security_code: ccard.card_cvc, security_code: Some(ccard.card_cvc),
card_type: card_type.clone(), card_type: card_type.clone(),
}, },
}); });
@ -900,7 +905,7 @@ impl
number: ccard.card_number, number: ccard.card_number,
expiration_month: ccard.card_exp_month, expiration_month: ccard.card_exp_month,
expiration_year: ccard.card_exp_year, expiration_year: ccard.card_exp_year,
security_code: ccard.card_cvc, security_code: Some(ccard.card_cvc),
card_type, card_type,
}, },
}); });
@ -1278,7 +1283,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>>
number: ccard.card_number, number: ccard.card_number,
expiration_month: ccard.card_exp_month, expiration_month: ccard.card_exp_month,
expiration_year: ccard.card_exp_year, expiration_year: ccard.card_exp_year,
security_code: ccard.card_cvc, security_code: Some(ccard.card_cvc),
card_type, card_type,
}, },
}); });
@ -1982,7 +1987,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsPreProcessingRouterData>>
number: ccard.card_number, number: ccard.card_number,
expiration_month: ccard.card_exp_month, expiration_month: ccard.card_exp_month,
expiration_year: ccard.card_exp_year, expiration_year: ccard.card_exp_year,
security_code: ccard.card_cvc, security_code: Some(ccard.card_cvc),
card_type, card_type,
}, },
})) }))
@ -2775,6 +2780,223 @@ impl TryFrom<types::RefundsResponseRouterData<api::RSync, CybersourceRsyncRespon
} }
} }
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourcePayoutFulfillRequest {
client_reference_information: ClientReferenceInformation,
order_information: OrderInformation,
recipient_information: CybersourceRecipientInfo,
sender_information: CybersourceSenderInfo,
processing_information: CybersourceProcessingInfo,
payment_information: PaymentInformation,
}
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceRecipientInfo {
first_name: Secret<String>,
last_name: Secret<String>,
address1: Secret<String>,
locality: String,
administrative_area: Secret<String>,
postal_code: Secret<String>,
country: api_enums::CountryAlpha2,
phone_number: Option<Secret<String>>,
}
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceSenderInfo {
reference_number: String,
account: CybersourceAccountInfo,
}
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceAccountInfo {
funds_source: CybersourcePayoutFundSourceType,
}
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize)]
pub enum CybersourcePayoutFundSourceType {
#[serde(rename = "05")]
Disbursement,
}
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceProcessingInfo {
business_application_id: CybersourcePayoutBusinessType,
}
#[cfg(feature = "payouts")]
#[derive(Debug, Serialize)]
pub enum CybersourcePayoutBusinessType {
#[serde(rename = "PP")]
PersonToPerson,
#[serde(rename = "AA")]
AccountToAccount,
}
#[cfg(feature = "payouts")]
impl TryFrom<&CybersourceRouterData<&types::PayoutsRouterData<api::PoFulfill>>>
for CybersourcePayoutFulfillRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &CybersourceRouterData<&types::PayoutsRouterData<api::PoFulfill>>,
) -> Result<Self, Self::Error> {
match item.router_data.request.payout_type {
enums::PayoutType::Card => {
let client_reference_information = ClientReferenceInformation {
code: Some(item.router_data.request.payout_id.clone()),
};
let order_information = OrderInformation {
amount_details: Amount {
total_amount: item.amount.to_owned(),
currency: item.router_data.request.destination_currency,
},
};
let billing_address = item.router_data.get_billing_address()?;
let phone_address = item.router_data.get_billing_phone()?;
let recipient_information =
CybersourceRecipientInfo::try_from((billing_address, phone_address))?;
let sender_information = CybersourceSenderInfo {
reference_number: item.router_data.request.payout_id.clone(),
account: CybersourceAccountInfo {
funds_source: CybersourcePayoutFundSourceType::Disbursement,
},
};
let processing_information = CybersourceProcessingInfo {
business_application_id: CybersourcePayoutBusinessType::PersonToPerson, // this means sender and receiver are different
};
let payout_method_data = item.router_data.get_payout_method_data()?;
let payment_information = PaymentInformation::try_from(payout_method_data)?;
Ok(Self {
client_reference_information,
order_information,
recipient_information,
sender_information,
processing_information,
payment_information,
})
}
enums::PayoutType::Bank | enums::PayoutType::Wallet => {
Err(errors::ConnectorError::NotSupported {
message: "PayoutType is not supported".to_string(),
connector: "Cybersource",
})?
}
}
}
}
#[cfg(feature = "payouts")]
impl TryFrom<(&AddressDetails, &PhoneDetails)> for CybersourceRecipientInfo {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: (&AddressDetails, &PhoneDetails)) -> Result<Self, Self::Error> {
let (billing_address, phone_address) = item;
Ok(Self {
first_name: billing_address.get_first_name()?.to_owned(),
last_name: billing_address.get_last_name()?.to_owned(),
address1: billing_address.get_line1()?.to_owned(),
locality: billing_address.get_city()?.to_owned(),
administrative_area: billing_address.get_state()?.to_owned(),
postal_code: billing_address.get_zip()?.to_owned(),
country: billing_address.get_country()?.to_owned(),
phone_number: phone_address.number.clone(),
})
}
}
#[cfg(feature = "payouts")]
impl TryFrom<PayoutMethodData> for PaymentInformation {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: PayoutMethodData) -> Result<Self, Self::Error> {
match item {
PayoutMethodData::Card(card_details) => {
let card_issuer = card_details.get_card_issuer().ok();
let card_type = card_issuer.map(String::from);
let card = Card {
number: card_details.card_number,
expiration_month: card_details.expiry_month,
expiration_year: card_details.expiry_year,
security_code: None,
card_type,
};
Ok(Self::Cards(CardPaymentInformation { card }))
}
PayoutMethodData::Bank(_) | PayoutMethodData::Wallet(_) => {
Err(errors::ConnectorError::NotSupported {
message: "PayoutMethod is not supported".to_string(),
connector: "Cybersource",
})?
}
}
}
}
#[cfg(feature = "payouts")]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceFulfillResponse {
id: String,
status: CybersourcePayoutStatus,
}
#[cfg(feature = "payouts")]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CybersourcePayoutStatus {
Accepted,
Declined,
InvalidRequest,
}
#[cfg(feature = "payouts")]
impl ForeignFrom<CybersourcePayoutStatus> for enums::PayoutStatus {
fn foreign_from(status: CybersourcePayoutStatus) -> Self {
match status {
CybersourcePayoutStatus::Accepted => Self::Success,
CybersourcePayoutStatus::Declined | CybersourcePayoutStatus::InvalidRequest => {
Self::Failed
}
}
}
}
#[cfg(feature = "payouts")]
impl<F> TryFrom<types::PayoutsResponseRouterData<F, CybersourceFulfillResponse>>
for types::PayoutsRouterData<F>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::PayoutsResponseRouterData<F, CybersourceFulfillResponse>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(types::PayoutsResponseData {
status: Some(enums::PayoutStatus::foreign_from(item.response.status)),
connector_payout_id: item.response.id,
payout_eligible: None,
should_add_next_step_to_process_tracker: false,
}),
..item.data
})
}
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CybersourceStandardErrorResponse { pub struct CybersourceStandardErrorResponse {

View File

@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(feature = "payouts")] #[cfg(feature = "payouts")]
use api_models::payouts::PayoutVendorAccountDetails; use api_models::payouts::{self, PayoutVendorAccountDetails};
use api_models::{ use api_models::{
enums::{CanadaStatesAbbreviation, UsStatesAbbreviation}, enums::{CanadaStatesAbbreviation, UsStatesAbbreviation},
payments::{self, OrderDetailsWithAmount}, payments::{self, OrderDetailsWithAmount},
@ -1078,6 +1078,80 @@ pub trait CardData {
fn get_expiry_year_as_i32(&self) -> Result<Secret<i32>, Error>; fn get_expiry_year_as_i32(&self) -> Result<Secret<i32>, Error>;
} }
#[cfg(feature = "payouts")]
impl CardData for payouts::Card {
fn get_card_expiry_year_2_digit(&self) -> Result<Secret<String>, errors::ConnectorError> {
let binding = self.expiry_year.clone();
let year = binding.peek();
Ok(Secret::new(
year.get(year.len() - 2..)
.ok_or(errors::ConnectorError::RequestEncodingFailed)?
.to_string(),
))
}
fn get_card_issuer(&self) -> Result<CardIssuer, Error> {
get_card_issuer(self.card_number.peek())
}
fn get_card_expiry_month_year_2_digit_with_delimiter(
&self,
delimiter: String,
) -> Result<Secret<String>, errors::ConnectorError> {
let year = self.get_card_expiry_year_2_digit()?;
Ok(Secret::new(format!(
"{}{}{}",
self.expiry_month.peek(),
delimiter,
year.peek()
)))
}
fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret<String> {
let year = self.get_expiry_year_4_digit();
Secret::new(format!(
"{}{}{}",
year.peek(),
delimiter,
self.expiry_month.peek()
))
}
fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret<String> {
let year = self.get_expiry_year_4_digit();
Secret::new(format!(
"{}{}{}",
self.expiry_month.peek(),
delimiter,
year.peek()
))
}
fn get_expiry_year_4_digit(&self) -> Secret<String> {
let mut year = self.expiry_year.peek().clone();
if year.len() == 2 {
year = format!("20{}", year);
}
Secret::new(year)
}
fn get_expiry_date_as_yymm(&self) -> Result<Secret<String>, errors::ConnectorError> {
let year = self.get_card_expiry_year_2_digit()?.expose();
let month = self.expiry_month.clone().expose();
Ok(Secret::new(format!("{year}{month}")))
}
fn get_expiry_month_as_i8(&self) -> Result<Secret<i8>, Error> {
self.expiry_month
.peek()
.clone()
.parse::<i8>()
.change_context(errors::ConnectorError::ResponseDeserializationFailed)
.map(Secret::new)
}
fn get_expiry_year_as_i32(&self) -> Result<Secret<i32>, Error> {
self.expiry_year
.peek()
.clone()
.parse::<i32>()
.change_context(errors::ConnectorError::ResponseDeserializationFailed)
.map(Secret::new)
}
}
impl CardData for domain::Card { impl CardData for domain::Card {
fn get_card_expiry_year_2_digit(&self) -> Result<Secret<String>, errors::ConnectorError> { fn get_card_expiry_year_2_digit(&self) -> Result<Secret<String>, errors::ConnectorError> {
let binding = self.card_exp_year.clone(); let binding = self.card_exp_year.clone();

View File

@ -975,7 +975,6 @@ default_imp_for_payouts!(
connector::Cashtocode, connector::Cashtocode,
connector::Checkout, connector::Checkout,
connector::Cryptopay, connector::Cryptopay,
connector::Cybersource,
connector::Coinbase, connector::Coinbase,
connector::Dlocal, connector::Dlocal,
connector::Fiserv, connector::Fiserv,
@ -1232,7 +1231,6 @@ default_imp_for_payouts_fulfill!(
connector::Cashtocode, connector::Cashtocode,
connector::Checkout, connector::Checkout,
connector::Cryptopay, connector::Cryptopay,
connector::Cybersource,
connector::Coinbase, connector::Coinbase,
connector::Dlocal, connector::Dlocal,
connector::Fiserv, connector::Fiserv,

View File

@ -15721,7 +15721,8 @@
"stripe", "stripe",
"wise", "wise",
"paypal", "paypal",
"ebanx" "ebanx",
"cybersource"
] ]
}, },
"PayoutCreateRequest": { "PayoutCreateRequest": {