diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 02fac0fbfa..b48e6bce57 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -348,6 +348,7 @@ pub enum PayoutConnectors { Wise, Paypal, Ebanx, + Cybersource, } #[cfg(feature = "payouts")] @@ -359,6 +360,7 @@ impl From for RoutableConnectors { PayoutConnectors::Wise => Self::Wise, PayoutConnectors::Paypal => Self::Paypal, PayoutConnectors::Ebanx => Self::Ebanx, + PayoutConnectors::Cybersource => Self::Cybersource, } } } @@ -372,6 +374,7 @@ impl From for Connector { PayoutConnectors::Wise => Self::Wise, PayoutConnectors::Paypal => Self::Paypal, PayoutConnectors::Ebanx => Self::Ebanx, + PayoutConnectors::Cybersource => Self::Cybersource, } } } @@ -386,6 +389,7 @@ impl TryFrom for PayoutConnectors { Connector::Wise => Ok(Self::Wise), Connector::Paypal => Ok(Self::Paypal), Connector::Ebanx => Ok(Self::Ebanx), + Connector::Cybersource => Ok(Self::Cybersource), _ => Err(format!("Invalid payout connector {}", value)), } } diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index b9fbed2815..691d52b4fb 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -133,6 +133,8 @@ pub struct ConnectorConfig { pub coinbase: Option, pub cryptopay: Option, pub cybersource: Option, + #[cfg(feature = "payouts")] + pub cybersource_payout: Option, pub iatapay: Option, pub opennode: Option, pub bambora: Option, @@ -223,6 +225,7 @@ impl ConnectorConfig { PayoutConnectors::Wise => Ok(connector_data.wise_payout), PayoutConnectors::Paypal => Ok(connector_data.paypal_payout), PayoutConnectors::Ebanx => Ok(connector_data.ebanx_payout), + PayoutConnectors::Cybersource => Ok(connector_data.cybersource_payout), } } diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 39079fe9f9..d93a9405ae 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -304,6 +304,10 @@ impl api::PaymentsPreProcessing for Cybersource {} impl api::PaymentsCompleteAuthorize for Cybersource {} impl api::ConnectorMandateRevoke for Cybersource {} +impl api::Payouts for Cybersource {} +#[cfg(feature = "payouts")] +impl api::PayoutFulfill for Cybersource {} + impl ConnectorIntegration< api::PaymentMethodToken, @@ -965,6 +969,124 @@ impl ConnectorIntegration + for Cybersource +{ + fn get_url( + &self, + _req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}pts/v2/payouts", self.base_url(connectors))) + } + + fn get_headers( + &self, + req: &types::PayoutsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_request_body( + &self, + req: &types::PayoutsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + 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, + connectors: &settings::Connectors, + ) -> CustomResult, 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, + event_builder: Option<&mut ConnectorEvent>, + res: types::Response, + ) -> CustomResult, 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 { + self.build_error_response(res, event_builder) + } + + fn get_5xx_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + 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 ConnectorIntegration< api::CompleteAuthorize, diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 4c8e06b28e..8251de8ca3 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1,4 +1,9 @@ use api_models::payments; +#[cfg(feature = "payouts")] +use api_models::{ + payments::{AddressDetails, PhoneDetails}, + payouts::PayoutMethodData, +}; use base64::Engine; use common_enums::FutureUsage; use common_utils::{ext_traits::ValueExt, pii}; @@ -111,7 +116,7 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { number: ccard.card_number, expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, + security_code: Some(ccard.card_cvc), card_type, }, }), @@ -404,7 +409,7 @@ pub struct Card { number: cards::CardNumber, expiration_month: Secret, expiration_year: Secret, - security_code: Secret, + security_code: Option>, #[serde(rename = "type")] card_type: Option, } @@ -849,7 +854,7 @@ impl number: ccard.card_number, expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, + security_code: Some(ccard.card_cvc), card_type: card_type.clone(), }, }); @@ -900,7 +905,7 @@ impl number: ccard.card_number, expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, + security_code: Some(ccard.card_cvc), card_type, }, }); @@ -1278,7 +1283,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> number: ccard.card_number, expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, + security_code: Some(ccard.card_cvc), card_type, }, }); @@ -1982,7 +1987,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsPreProcessingRouterData>> number: ccard.card_number, expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, + security_code: Some(ccard.card_cvc), card_type, }, })) @@ -2775,6 +2780,223 @@ impl TryFrom, + last_name: Secret, + address1: Secret, + locality: String, + administrative_area: Secret, + postal_code: Secret, + country: api_enums::CountryAlpha2, + phone_number: Option>, +} + +#[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>> + for CybersourcePayoutFulfillRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PayoutsRouterData>, + ) -> Result { + 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; + fn try_from(item: (&AddressDetails, &PhoneDetails)) -> Result { + 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 for PaymentInformation { + type Error = error_stack::Report; + fn try_from(item: PayoutMethodData) -> Result { + 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 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 TryFrom> + for types::PayoutsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PayoutsResponseRouterData, + ) -> Result { + 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)] #[serde(rename_all = "camelCase")] pub struct CybersourceStandardErrorResponse { diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 2cb3aa436e..bb52d4885f 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; #[cfg(feature = "payouts")] -use api_models::payouts::PayoutVendorAccountDetails; +use api_models::payouts::{self, PayoutVendorAccountDetails}; use api_models::{ enums::{CanadaStatesAbbreviation, UsStatesAbbreviation}, payments::{self, OrderDetailsWithAmount}, @@ -1078,6 +1078,80 @@ pub trait CardData { fn get_expiry_year_as_i32(&self) -> Result, Error>; } +#[cfg(feature = "payouts")] +impl CardData for payouts::Card { + fn get_card_expiry_year_2_digit(&self) -> Result, 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 { + get_card_issuer(self.card_number.peek()) + } + fn get_card_expiry_month_year_2_digit_with_delimiter( + &self, + delimiter: String, + ) -> Result, 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 { + 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 { + 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 { + 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, 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, Error> { + self.expiry_month + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } + fn get_expiry_year_as_i32(&self) -> Result, Error> { + self.expiry_year + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } +} + impl CardData for domain::Card { fn get_card_expiry_year_2_digit(&self) -> Result, errors::ConnectorError> { let binding = self.card_exp_year.clone(); diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index c7ec86ac5e..0687f5986b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -975,7 +975,6 @@ default_imp_for_payouts!( connector::Cashtocode, connector::Checkout, connector::Cryptopay, - connector::Cybersource, connector::Coinbase, connector::Dlocal, connector::Fiserv, @@ -1232,7 +1231,6 @@ default_imp_for_payouts_fulfill!( connector::Cashtocode, connector::Checkout, connector::Cryptopay, - connector::Cybersource, connector::Coinbase, connector::Dlocal, connector::Fiserv, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 70e4715183..c45a4aaaa1 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -15721,7 +15721,8 @@ "stripe", "wise", "paypal", - "ebanx" + "ebanx", + "cybersource" ] }, "PayoutCreateRequest": {