feat(connector): add support for capture, void, psync, refund and rsync for cybersource (#381)

This commit is contained in:
Jagan
2023-01-17 13:55:51 +05:30
committed by GitHub
parent d08c35c77c
commit e1590e7bf8
9 changed files with 1064 additions and 229 deletions

View File

@ -14,8 +14,12 @@ use crate::{
configs::settings,
consts,
core::errors::{self, CustomResult},
headers, logger, services,
types::{self, api},
headers, logger,
services::{self, ConnectorIntegration},
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
},
utils::{self, BytesExt},
};
@ -33,23 +37,25 @@ impl Cybersource {
auth: cybersource::CybersourceAuthType,
host: String,
resource: &str,
payload: &str,
payload: &String,
date: OffsetDateTime,
http_method: services::Method,
) -> CustomResult<String, errors::ConnectorError> {
let cybersource::CybersourceAuthType {
api_key,
merchant_account,
api_secret,
} = auth;
let headers_for_post_method = "host date (request-target) digest v-c-merchant-id";
let is_post_method = matches!(http_method, services::Method::Post);
let digest_str = if is_post_method { "digest " } else { "" };
let headers = format!("host date (request-target) {digest_str}v-c-merchant-id");
let request_target = if is_post_method {
format!("(request-target): post {resource}\ndigest: SHA-256={payload}\n")
} else {
format!("(request-target): get {resource}\n")
};
let signature_string = format!(
"host: {host}\n\
date: {date}\n\
(request-target): post {resource}\n\
digest: SHA-256={}\n\
v-c-merchant-id: {merchant_account}",
self.generate_digest(payload.as_bytes())
"host: {host}\ndate: {date}\n{request_target}v-c-merchant-id: {merchant_account}"
);
let key_value = consts::BASE64_ENGINE
.decode(api_secret)
@ -59,25 +65,97 @@ impl Cybersource {
let signature_value =
consts::BASE64_ENGINE.encode(hmac::sign(&key, signature_string.as_bytes()).as_ref());
let signature_header = format!(
r#"keyid="{api_key}", algorithm="HmacSHA256", headers="{headers_for_post_method}", signature="{signature_value}""#
r#"keyid="{api_key}", algorithm="HmacSHA256", headers="{headers}", signature="{signature_value}""#
);
Ok(signature_header)
}
}
impl api::ConnectorCommon for Cybersource {
impl ConnectorCommon for Cybersource {
fn id(&self) -> &'static str {
"cybersource"
}
fn common_get_content_type(&self) -> &'static str {
"application/json"
"application/json;charset=utf-8"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.cybersource.base_url.as_ref()
}
fn build_error_response(
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: cybersource::ErrorResponse = res
.parse_struct("Cybersource ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(types::ErrorResponse {
code: consts::NO_ERROR_CODE.to_string(),
message: response.details.to_string(),
reason: None,
})
}
}
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Cybersource
where
Self: ConnectorIntegration<Flow, Request, Response>,
{
fn build_headers(
&self,
req: &types::RouterData<Flow, Request, Response>,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let date = OffsetDateTime::now_utc();
let cybersource_req = self.get_request_body(req)?;
let auth = cybersource::CybersourceAuthType::try_from(&req.connector_auth_type)?;
let merchant_account = auth.merchant_account.clone();
let base_url = connectors.cybersource.base_url.as_str();
let cybersource_host = Url::parse(base_url)
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let host = cybersource_host
.host_str()
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
let path: String = self
.get_url(req, connectors)?
.chars()
.skip(base_url.len() - 1)
.collect();
let sha256 =
self.generate_digest(cybersource_req.map_or("{}".to_string(), |s| s).as_bytes());
let http_method = self.get_http_method();
let signature = self.generate_signature(
auth,
host.to_string(),
path.as_str(),
&sha256,
date,
http_method,
)?;
let mut headers = vec![
(
headers::CONTENT_TYPE.to_string(),
self.get_content_type().to_string(),
),
(
headers::ACCEPT.to_string(),
"application/hal+json;charset=utf-8".to_string(),
),
("v-c-merchant-id".to_string(), merchant_account),
("Date".to_string(), date.to_string()),
("Host".to_string(), host.to_string()),
("Signature".to_string(), signature),
];
if matches!(http_method, services::Method::Post | services::Method::Put) {
headers.push(("Digest".to_string(), format!("SHA-256={}", sha256)));
}
Ok(headers)
}
}
impl api::Payment for Cybersource {}
@ -87,68 +165,197 @@ impl api::PaymentVoid for Cybersource {}
impl api::PaymentCapture for Cybersource {}
impl api::PreVerify for Cybersource {}
impl
services::ConnectorIntegration<
api::Verify,
types::VerifyRequestData,
types::PaymentsResponseData,
> for Cybersource
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
for Cybersource
{
}
impl api::PaymentSession for Cybersource {}
impl
services::ConnectorIntegration<
api::Session,
types::PaymentsSessionData,
types::PaymentsResponseData,
> for Cybersource
{
}
impl
services::ConnectorIntegration<
api::Capture,
types::PaymentsCaptureData,
types::PaymentsResponseData,
> for Cybersource
{
}
impl
services::ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Cybersource
{
}
impl
services::ConnectorIntegration<
api::Authorize,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
> for Cybersource
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Cybersource
{
fn get_headers(
&self,
_req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let headers = vec![
(
headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
),
(
headers::ACCEPT.to_string(),
"application/hal+json;charset=utf-8".to_string(),
),
];
Ok(headers)
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
"application/json;charset=utf-8"
self.common_get_content_type()
}
fn get_url(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}pts/v2/payments/{}/captures",
self.base_url(connectors),
connector_payment_id
))
}
fn get_request_body(
&self,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let req_obj = cybersource::CybersourcePaymentsRequest::try_from(req)?;
let req =
utils::Encode::<cybersource::CybersourcePaymentsRequest>::encode_to_string_of_json(
&req_obj,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(req))
}
fn build_request(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsCaptureType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: types::Response,
) -> CustomResult<
types::RouterData<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>,
errors::ConnectorError,
> {
let response: cybersource::CybersourcePaymentsResponse = res
.response
.parse_struct("Cybersource PaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(cybersourcepayments_create_response=?response);
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Cybersource
{
fn get_headers(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_http_method(&self) -> services::Method {
services::Method::Get
}
fn get_url(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req
.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
Ok(format!(
"{}tss/v2/transactions/{}",
self.base_url(connectors),
connector_payment_id
))
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_request_body(
&self,
_req: &types::PaymentsSyncRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
Ok(Some("{}".to_string()))
}
fn build_request(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Get)
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: cybersource::CybersourceTransactionResponse = res
.response
.parse_struct("Cybersource PaymentSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(cybersourcepayments_create_response=?response);
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Cybersource
{
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
@ -162,61 +369,36 @@ impl
))
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let req_obj = cybersource::CybersourcePaymentsRequest::try_from(req)?;
let cybersource_req =
utils::Encode::<cybersource::CybersourcePaymentsRequest>::encode_to_string_of_json(
&req_obj,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(cybersource_req))
}
fn build_request(
&self,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let date = OffsetDateTime::now_utc();
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsAuthorizeType::get_url(
self, req, connectors,
)?)
.headers(types::PaymentsAuthorizeType::get_headers(
self, req, connectors,
)?)
.body(self.get_request_body(req)?)
.build();
let cybersource_req =
utils::Encode::<cybersource::CybersourcePaymentsRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let auth: cybersource::CybersourceAuthType =
cybersource::CybersourceAuthType::try_from(&req.connector_auth_type)?;
let merchant_account = auth.merchant_account.clone();
let cybersource_host = Url::parse(connectors.cybersource.base_url.as_str())
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
match cybersource_host.host_str() {
Some(host) => {
let signature = self.generate_signature(
auth,
host.to_string(),
"/pts/v2/payments/",
&cybersource_req,
date,
)?;
let headers = vec![
(
"Digest".to_string(),
format!(
"SHA-256={}",
self.generate_digest(cybersource_req.as_bytes())
),
),
("v-c-merchant-id".to_string(), merchant_account),
("Date".to_string(), date.to_string()),
("Host".to_string(), host.to_string()),
("Signature".to_string(), signature),
];
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsAuthorizeType::get_url(
self, req, connectors,
)?)
.headers(headers)
.headers(types::PaymentsAuthorizeType::get_headers(
self, req, connectors,
)?)
.body(Some(cybersource_req))
.build();
Ok(Some(request))
}
None => Err(errors::ConnectorError::RequestEncodingFailed.into()),
}
Ok(Some(request))
}
fn handle_response(
@ -242,26 +424,85 @@ impl
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: cybersource::ErrorResponse = res
.parse_struct("Cybersource ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(types::ErrorResponse {
code: consts::NO_ERROR_CODE.to_string(),
message: response
.message
.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
reason: None,
})
self.build_error_response(res)
}
}
impl
services::ConnectorIntegration<
api::Void,
types::PaymentsCancelData,
types::PaymentsResponseData,
> for Cybersource
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Cybersource
{
fn get_headers(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_url(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}pts/v2/payments/{}/voids",
self.base_url(connectors),
connector_payment_id
))
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_request_body(
&self,
_req: &types::PaymentsCancelRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
Ok(Some("{}".to_string()))
}
fn build_request(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.body(self.get_request_body(req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
let response: cybersource::CybersourcePaymentsResponse = res
.response
.parse_struct("Cybersource PaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(cybersourcepayments_create_response=?response);
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::Refund for Cybersource {}
@ -269,15 +510,153 @@ impl api::RefundExecute for Cybersource {}
impl api::RefundSync for Cybersource {}
#[allow(dead_code)]
impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
for Cybersource
{
fn get_headers(
&self,
req: &types::RefundExecuteRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, 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::RefundExecuteRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}pts/v2/payments/{}/refunds",
self.base_url(connectors),
connector_payment_id
))
}
fn get_request_body(
&self,
req: &types::RefundExecuteRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let req_obj = cybersource::CybersourceRefundRequest::try_from(req)?;
let req = utils::Encode::<cybersource::CybersourceRefundRequest>::encode_to_string_of_json(
&req_obj,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(req))
}
fn build_request(
&self,
req: &types::RefundExecuteRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
.headers(types::RefundExecuteType::get_headers(
self, req, connectors,
)?)
.body(self.get_request_body(req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::RefundExecuteRouterData,
res: types::Response,
) -> CustomResult<types::RefundExecuteRouterData, errors::ConnectorError> {
let response: cybersource::CybersourcePaymentsResponse = res
.response
.parse_struct("Cybersource PaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(cybersource_refund_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
#[allow(dead_code)]
impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
for Cybersource
{
fn get_headers(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_http_method(&self) -> services::Method {
services::Method::Get
}
fn get_url(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}tss/v2/transactions/{}",
self.base_url(connectors),
req.request.connector_transaction_id
))
}
fn build_request(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Get)
.url(&types::RefundSyncType::get_url(self, req, connectors)?)
.headers(types::RefundSyncType::get_headers(self, req, connectors)?)
.body(types::RefundSyncType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::RefundSyncRouterData,
res: types::Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
let response: cybersource::CybersourceTransactionResponse = res
.response
.parse_struct("Cybersource PaymentSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(cybersourcepayments_create_response=?response);
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
#[async_trait::async_trait]

View File

@ -4,6 +4,8 @@ use masking::Secret;
use serde::{Deserialize, Serialize};
use crate::{
connector::utils::{self, AddressDetailsData, PaymentsRequestData, PhoneDetailsData},
consts,
core::errors,
pii::PeekInterface,
types::{self, api, storage::enums},
@ -18,8 +20,17 @@ pub struct CybersourcePaymentsRequest {
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProcessingInformation {
capture: bool,
capture_options: Option<CaptureOptions>,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CaptureOptions {
capture_sequence_number: u32,
total_capture_count: u32,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
@ -72,35 +83,25 @@ pub struct BillTo {
// for cybersource each item in Billing is mandatory
fn build_bill_to(
address_details: payments::Address,
address_details: &payments::Address,
email: Secret<String, pii::Email>,
phone_number: Secret<String>,
) -> Option<BillTo> {
if let Some(api_models::payments::AddressDetails {
first_name: Some(f_name),
last_name: Some(last_name),
line1: Some(address1),
city: Some(city),
line2: Some(administrative_area),
zip: Some(postal_code),
country: Some(country),
..
}) = address_details.address
{
Some(BillTo {
first_name: f_name,
last_name,
address1,
locality: city,
administrative_area,
postal_code,
country,
email,
phone_number,
})
} else {
None
}
) -> Result<BillTo, error_stack::Report<errors::ConnectorError>> {
let address = address_details
.address
.as_ref()
.ok_or_else(utils::missing_field_err("billing.address"))?;
Ok(BillTo {
first_name: address.get_first_name()?.to_owned(),
last_name: address.get_last_name()?.to_owned(),
address1: address.get_line1()?.to_owned(),
locality: address.get_city()?.to_owned(),
administrative_area: address.get_line2()?.to_owned(),
postal_code: address.get_zip()?.to_owned(),
country: address.get_country()?.to_owned(),
email,
phone_number,
})
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for CybersourcePaymentsRequest {
@ -108,30 +109,17 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for CybersourcePaymentsRequest
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data {
api::PaymentMethod::Card(ref ccard) => {
let address = item
.address
.billing
.clone()
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
let phone = address
.clone()
.phone
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
let phone_number = phone
.number
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
let country_code = phone
.country_code
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
let phone = item.get_billing_phone()?;
let phone_number = phone.get_number()?;
let country_code = phone.get_country_code()?;
let number_with_code =
Secret::new(format!("{}{}", country_code, phone_number.peek()));
let email = item
.request
.email
.clone()
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
let bill_to = build_bill_to(address, email, number_with_code)
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
.ok_or_else(utils::missing_field_err("email"))?;
let bill_to = build_bill_to(item.get_billing()?, email, number_with_code)?;
let order_information = OrderInformationWithBill {
amount_details: Amount {
@ -155,6 +143,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for CybersourcePaymentsRequest
item.request.capture_method,
Some(enums::CaptureMethod::Automatic) | None
),
capture_options: None,
};
Ok(Self {
@ -168,6 +157,49 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for CybersourcePaymentsRequest
}
}
impl TryFrom<&types::PaymentsCaptureRouterData> for CybersourcePaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(value: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
Ok(Self {
processing_information: ProcessingInformation {
capture_options: Some(CaptureOptions {
capture_sequence_number: 1,
total_capture_count: 1,
}),
..Default::default()
},
order_information: OrderInformationWithBill {
amount_details: Amount {
total_amount: value
.request
.amount_to_capture
.map(|amount| amount.to_string())
.ok_or_else(utils::missing_field_err("amount_to_capture"))?,
..Default::default()
},
..Default::default()
},
..Default::default()
})
}
}
impl TryFrom<&types::RefundExecuteRouterData> for CybersourcePaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(value: &types::RefundExecuteRouterData) -> Result<Self, Self::Error> {
Ok(Self {
order_information: OrderInformationWithBill {
amount_details: Amount {
total_amount: value.request.refund_amount.to_string(),
currency: value.request.currency.to_string(),
},
..Default::default()
},
..Default::default()
})
}
}
pub struct CybersourceAuthType {
pub(super) api_key: String,
pub(super) merchant_account: String,
@ -199,6 +231,11 @@ pub enum CybersourcePaymentStatus {
Authorized,
Succeeded,
Failed,
Voided,
Reversed,
Pending,
Declined,
Transmitted,
#[default]
Processing,
}
@ -207,28 +244,110 @@ impl From<CybersourcePaymentStatus> for enums::AttemptStatus {
fn from(item: CybersourcePaymentStatus) -> Self {
match item {
CybersourcePaymentStatus::Authorized => Self::Authorized,
CybersourcePaymentStatus::Succeeded => Self::Charged,
CybersourcePaymentStatus::Failed => Self::Failure,
CybersourcePaymentStatus::Succeeded | CybersourcePaymentStatus::Transmitted => {
Self::Charged
}
CybersourcePaymentStatus::Voided | CybersourcePaymentStatus::Reversed => Self::Voided,
CybersourcePaymentStatus::Failed | CybersourcePaymentStatus::Declined => Self::Failure,
CybersourcePaymentStatus::Processing => Self::Authorizing,
CybersourcePaymentStatus::Pending => Self::Pending,
}
}
}
impl From<CybersourcePaymentStatus> for enums::RefundStatus {
fn from(item: CybersourcePaymentStatus) -> Self {
match item {
CybersourcePaymentStatus::Succeeded => Self::Success,
CybersourcePaymentStatus::Failed => Self::Failure,
_ => Self::Pending,
}
}
}
#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CybersourcePaymentsResponse {
id: String,
status: CybersourcePaymentStatus,
error_information: Option<CybersourceErrorInformation>,
}
impl TryFrom<types::PaymentsResponseRouterData<CybersourcePaymentsResponse>>
for types::PaymentsAuthorizeRouterData
#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)]
pub struct CybersourceErrorInformation {
reason: String,
message: String,
}
impl<F, T>
TryFrom<
types::ResponseRouterData<F, CybersourcePaymentsResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::PaymentsResponseRouterData<CybersourcePaymentsResponse>,
item: types::ResponseRouterData<
F,
CybersourcePaymentsResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: item.response.status.into(),
response: match item.response.error_information {
Some(error) => Err(types::ErrorResponse {
code: consts::NO_ERROR_CODE.to_string(),
message: error.message,
reason: Some(error.reason),
}),
_ => Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
},
..item.data
})
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceTransactionResponse {
id: String,
application_information: ApplicationInformation,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApplicationInformation {
status: CybersourcePaymentStatus,
}
impl<F, T>
TryFrom<
types::ResponseRouterData<
F,
CybersourceTransactionResponse,
T,
types::PaymentsResponseData,
>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::ResponseRouterData<
F,
CybersourceTransactionResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: item.response.application_information.status.into(),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
redirection_data: None,
@ -247,6 +366,7 @@ pub struct ErrorResponse {
pub error_information: Option<ErrorInformation>,
pub status: String,
pub message: Option<String>,
pub details: serde_json::Value,
}
#[derive(Debug, Default, Deserialize)]
@ -256,12 +376,13 @@ pub struct ErrorInformation {
}
#[derive(Default, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CybersourceRefundRequest {
order_information: OrderInformation,
}
impl<F> TryFrom<&types::RefundsRouterData<F>> for CybersourceRefundRequest {
type Error = error_stack::Report<errors::ParsingError>;
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
Ok(Self {
order_information: OrderInformation {
@ -274,42 +395,37 @@ impl<F> TryFrom<&types::RefundsRouterData<F>> for CybersourceRefundRequest {
}
}
#[allow(dead_code)]
#[derive(Debug, Default, Deserialize, Clone)]
pub enum RefundStatus {
Succeeded,
Failed,
#[default]
Processing,
}
impl From<RefundStatus> for enums::RefundStatus {
fn from(item: RefundStatus) -> Self {
match item {
self::RefundStatus::Succeeded => Self::Success,
self::RefundStatus::Failed => Self::Failure,
self::RefundStatus::Processing => Self::Pending,
}
}
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct CybersourceRefundResponse {
pub id: String,
pub status: RefundStatus,
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, CybersourceRefundResponse>>
for types::RefundsRouterData<api::RSync>
impl TryFrom<types::RefundsResponseRouterData<api::Execute, CybersourcePaymentsResponse>>
for types::RefundsRouterData<api::Execute>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::RefundsResponseRouterData<api::RSync, CybersourceRefundResponse>,
item: types::RefundsResponseRouterData<api::Execute, CybersourcePaymentsResponse>,
) -> Result<Self, Self::Error> {
let refund_status = enums::RefundStatus::from(item.response.status);
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id,
refund_status: enums::RefundStatus::from(item.response.status),
refund_status,
}),
..item.data
})
}
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, CybersourceTransactionResponse>>
for types::RefundsRouterData<api::RSync>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::RefundsResponseRouterData<api::RSync, CybersourceTransactionResponse>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id,
refund_status: enums::RefundStatus::from(
item.response.application_information.status,
),
}),
..item.data
})

View File

@ -1,4 +1,3 @@
use error_stack::ResultExt;
use serde::{Deserialize, Serialize};
use super::{
@ -9,7 +8,6 @@ use crate::{
connector::utils::{self, CardData, PaymentsRequestData},
core::errors,
types::{self, api, storage::enums},
utils::OptionExt,
};
impl TryFrom<&types::PaymentsAuthorizeRouterData> for GlobalpayPaymentsRequest {
@ -18,8 +16,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for GlobalpayPaymentsRequest {
let metadata = item
.connector_meta_data
.to_owned()
.get_required_value("connector_meta_data")
.change_context(errors::ConnectorError::NoConnectorMetaData)?;
.ok_or_else(utils::missing_field_err("connector_meta"))?;
let account_name = metadata
.as_object()
.and_then(|o| o.get("account_name"))

View File

@ -1,3 +1,5 @@
use masking::Secret;
use crate::{
core::errors,
pii::PeekInterface,
@ -18,10 +20,50 @@ pub fn missing_field_err(
type Error = error_stack::Report<errors::ConnectorError>;
pub trait PaymentsRequestData {
fn get_attempt_id(&self) -> Result<String, Error>;
fn get_billing(&self) -> Result<&api::Address, Error>;
fn get_billing_country(&self) -> Result<String, Error>;
fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error>;
fn get_card(&self) -> Result<api::CCard, Error>;
}
impl PaymentsRequestData for types::PaymentsAuthorizeRouterData {
fn get_attempt_id(&self) -> Result<String, Error> {
self.attempt_id
.clone()
.ok_or_else(missing_field_err("attempt_id"))
}
fn get_billing_country(&self) -> Result<String, Error> {
self.address
.billing
.as_ref()
.and_then(|a| a.address.as_ref())
.and_then(|ad| ad.country.clone())
.ok_or_else(missing_field_err("billing.address.country"))
}
fn get_card(&self) -> Result<api::CCard, Error> {
match self.request.payment_method_data.clone() {
api::PaymentMethod::Card(card) => Ok(card),
_ => Err(missing_field_err("card")()),
}
}
fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error> {
self.address
.billing
.as_ref()
.and_then(|a| a.phone.as_ref())
.ok_or_else(missing_field_err("billing.phone"))
}
fn get_billing(&self) -> Result<&api::Address, Error> {
self.address
.billing
.as_ref()
.ok_or_else(missing_field_err("billing"))
}
}
pub trait CardData {
fn get_card_number(&self) -> String;
fn get_card_expiry_month(&self) -> String;
@ -29,6 +71,7 @@ pub trait CardData {
fn get_card_expiry_year_2_digit(&self) -> String;
fn get_card_cvc(&self) -> String;
}
impl CardData for api::CCard {
fn get_card_number(&self) -> String {
self.card_number.peek().clone()
@ -47,26 +90,74 @@ impl CardData for api::CCard {
self.card_cvc.peek().clone()
}
}
impl PaymentsRequestData for types::PaymentsAuthorizeRouterData {
fn get_attempt_id(&self) -> Result<String, Error> {
self.attempt_id
.clone()
.ok_or_else(missing_field_err("attempt_id"))
}
pub trait PhoneDetailsData {
fn get_number(&self) -> Result<Secret<String>, Error>;
fn get_country_code(&self) -> Result<String, Error>;
}
fn get_billing_country(&self) -> Result<String, Error> {
self.address
.billing
impl PhoneDetailsData for api::PhoneDetails {
fn get_country_code(&self) -> Result<String, Error> {
self.country_code
.clone()
.and_then(|a| a.address)
.and_then(|ad| ad.country)
.ok_or_else(missing_field_err("billing.country"))
.ok_or_else(missing_field_err("billing.phone.country_code"))
}
fn get_card(&self) -> Result<api::CCard, Error> {
match self.request.payment_method_data.clone() {
api::PaymentMethod::Card(card) => Ok(card),
_ => Err(missing_field_err("card")()),
}
fn get_number(&self) -> Result<Secret<String>, Error> {
self.number
.clone()
.ok_or_else(missing_field_err("billing.phone.number"))
}
}
pub trait AddressDetailsData {
fn get_first_name(&self) -> Result<&Secret<String>, Error>;
fn get_last_name(&self) -> Result<&Secret<String>, Error>;
fn get_line1(&self) -> Result<&Secret<String>, Error>;
fn get_city(&self) -> Result<&String, Error>;
fn get_line2(&self) -> Result<&Secret<String>, Error>;
fn get_zip(&self) -> Result<&Secret<String>, Error>;
fn get_country(&self) -> Result<&String, Error>;
}
impl AddressDetailsData for api::AddressDetails {
fn get_first_name(&self) -> Result<&Secret<String>, Error> {
self.first_name
.as_ref()
.ok_or_else(missing_field_err("address.first_name"))
}
fn get_last_name(&self) -> Result<&Secret<String>, Error> {
self.last_name
.as_ref()
.ok_or_else(missing_field_err("address.last_name"))
}
fn get_line1(&self) -> Result<&Secret<String>, Error> {
self.line1
.as_ref()
.ok_or_else(missing_field_err("address.line1"))
}
fn get_city(&self) -> Result<&String, Error> {
self.city
.as_ref()
.ok_or_else(missing_field_err("address.city"))
}
fn get_line2(&self) -> Result<&Secret<String>, Error> {
self.line2
.as_ref()
.ok_or_else(missing_field_err("address.line2"))
}
fn get_zip(&self) -> Result<&Secret<String>, Error> {
self.zip
.as_ref()
.ok_or_else(missing_field_err("address.zip"))
}
fn get_country(&self) -> Result<&String, Error> {
self.country
.as_ref()
.ok_or_else(missing_field_err("address.country"))
}
}

View File

@ -58,6 +58,11 @@ pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Re
mime::APPLICATION_JSON.essence_str()
}
/// primarily used when creating signature based on request method of payment flow
fn get_http_method(&self) -> Method {
Method::Post
}
fn get_url(
&self,
_req: &types::RouterData<T, Req, Resp>,