diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 83c888b030..80a27e09a9 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -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 { 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 { + 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 ConnectorCommonExt for Cybersource +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 + 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 +impl ConnectorIntegration for Cybersource { } -impl - services::ConnectorIntegration< - api::Authorize, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > for Cybersource +impl ConnectorIntegration + for Cybersource { fn get_headers( &self, - _req: &types::PaymentsAuthorizeRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, ) -> CustomResult, 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 { + 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, errors::ConnectorError> { + let req_obj = cybersource::CybersourcePaymentsRequest::try_from(req)?; + let req = + utils::Encode::::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, 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, + 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 { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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, errors::ConnectorError> { + Ok(Some("{}".to_string())) + } + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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 { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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, errors::ConnectorError> { + let req_obj = cybersource::CybersourcePaymentsRequest::try_from(req)?; + let cybersource_req = + utils::Encode::::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, 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::::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 { - 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 + for Cybersource { + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + 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, errors::ConnectorError> { + Ok(Some("{}".to_string())) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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 { + 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 +impl ConnectorIntegration for Cybersource { + fn get_headers( + &self, + req: &types::RefundExecuteRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::RefundExecuteRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + 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, errors::ConnectorError> { + let req_obj = cybersource::CybersourceRefundRequest::try_from(req)?; + let req = utils::Encode::::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, 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 { + 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 { + self.build_error_response(res) + } } #[allow(dead_code)] -impl services::ConnectorIntegration +impl ConnectorIntegration for Cybersource { + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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, 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 { + 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 { + self.build_error_response(res) + } } #[async_trait::async_trait] diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 6881bda460..8e84d5687d 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -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, +} + +#[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, phone_number: Secret, -) -> Option { - 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> { + 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 { 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; + fn try_from(value: &types::PaymentsCaptureRouterData) -> Result { + 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; + fn try_from(value: &types::RefundExecuteRouterData) -> Result { + 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 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 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, } -impl TryFrom> - for types::PaymentsAuthorizeRouterData +#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct CybersourceErrorInformation { + reason: String, + message: String, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::PaymentsResponseRouterData, + item: types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + T, + types::PaymentsResponseData, + >, ) -> Result { 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 + TryFrom< + types::ResponseRouterData< + F, + CybersourceTransactionResponse, + T, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourceTransactionResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + 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, pub status: String, pub message: Option, + 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 TryFrom<&types::RefundsRouterData> for CybersourceRefundRequest { - type Error = error_stack::Report; + type Error = error_stack::Report; fn try_from(item: &types::RefundsRouterData) -> Result { Ok(Self { order_information: OrderInformation { @@ -274,42 +395,37 @@ impl TryFrom<&types::RefundsRouterData> for CybersourceRefundRequest { } } -#[allow(dead_code)] -#[derive(Debug, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From 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> - for types::RefundsRouterData +impl TryFrom> + for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { + 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> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id, + refund_status: enums::RefundStatus::from( + item.response.application_information.status, + ), }), ..item.data }) diff --git a/crates/router/src/connector/globalpay/transformers.rs b/crates/router/src/connector/globalpay/transformers.rs index 8c3fce9b62..0c1531f47b 100644 --- a/crates/router/src/connector/globalpay/transformers.rs +++ b/crates/router/src/connector/globalpay/transformers.rs @@ -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")) diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index a1ed892829..4b8c095cd8 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -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; pub trait PaymentsRequestData { fn get_attempt_id(&self) -> Result; + fn get_billing(&self) -> Result<&api::Address, Error>; fn get_billing_country(&self) -> Result; + fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error>; fn get_card(&self) -> Result; } +impl PaymentsRequestData for types::PaymentsAuthorizeRouterData { + fn get_attempt_id(&self) -> Result { + self.attempt_id + .clone() + .ok_or_else(missing_field_err("attempt_id")) + } + + fn get_billing_country(&self) -> Result { + 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 { + 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 { - self.attempt_id - .clone() - .ok_or_else(missing_field_err("attempt_id")) - } +pub trait PhoneDetailsData { + fn get_number(&self) -> Result, Error>; + fn get_country_code(&self) -> Result; +} - fn get_billing_country(&self) -> Result { - self.address - .billing +impl PhoneDetailsData for api::PhoneDetails { + fn get_country_code(&self) -> Result { + 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 { - match self.request.payment_method_data.clone() { - api::PaymentMethod::Card(card) => Ok(card), - _ => Err(missing_field_err("card")()), - } + fn get_number(&self) -> Result, Error> { + self.number + .clone() + .ok_or_else(missing_field_err("billing.phone.number")) + } +} + +pub trait AddressDetailsData { + fn get_first_name(&self) -> Result<&Secret, Error>; + fn get_last_name(&self) -> Result<&Secret, Error>; + fn get_line1(&self) -> Result<&Secret, Error>; + fn get_city(&self) -> Result<&String, Error>; + fn get_line2(&self) -> Result<&Secret, Error>; + fn get_zip(&self) -> Result<&Secret, Error>; + fn get_country(&self) -> Result<&String, Error>; +} + +impl AddressDetailsData for api::AddressDetails { + fn get_first_name(&self) -> Result<&Secret, Error> { + self.first_name + .as_ref() + .ok_or_else(missing_field_err("address.first_name")) + } + + fn get_last_name(&self) -> Result<&Secret, Error> { + self.last_name + .as_ref() + .ok_or_else(missing_field_err("address.last_name")) + } + + fn get_line1(&self) -> Result<&Secret, 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, Error> { + self.line2 + .as_ref() + .ok_or_else(missing_field_err("address.line2")) + } + + fn get_zip(&self) -> Result<&Secret, 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")) } } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 1cb9c7a4e5..ee19000eaa 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -58,6 +58,11 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny Method { + Method::Post + } + fn get_url( &self, _req: &types::RouterData, diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 0e74e936fd..cd696c9e0b 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -6,6 +6,7 @@ pub(crate) struct ConnectorAuthentication { pub aci: Option, pub authorizedotnet: Option, pub checkout: Option, + pub cybersource: Option, pub fiserv: Option, pub globalpay: Option, pub payu: Option, diff --git a/crates/router/tests/connectors/cybersource.rs b/crates/router/tests/connectors/cybersource.rs new file mode 100644 index 0000000000..e9bd28222b --- /dev/null +++ b/crates/router/tests/connectors/cybersource.rs @@ -0,0 +1,240 @@ +use futures::future::OptionFuture; +use masking::Secret; +use router::types::{self, api, storage::enums}; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions, PaymentAuthorizeType}, +}; + +struct Cybersource; +impl ConnectorActions for Cybersource {} +impl utils::Connector for Cybersource { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Cybersource; + types::api::ConnectorData { + connector: Box::new(&Cybersource), + connector_name: types::Connector::Cybersource, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .cybersource + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "cybersource".to_string() + } +} + +fn get_default_payment_info() -> Option { + Some(utils::PaymentInfo { + address: Some(types::PaymentAddress { + billing: Some(api::Address { + address: Some(api::AddressDetails { + first_name: Some(Secret::new("first".to_string())), + last_name: Some(Secret::new("last".to_string())), + line1: Some(Secret::new("line1".to_string())), + line2: Some(Secret::new("line2".to_string())), + city: Some("city".to_string()), + zip: Some(Secret::new("zip".to_string())), + country: Some("IN".to_string()), + ..Default::default() + }), + phone: Some(api::PhoneDetails { + number: Some(Secret::new("1234567890".to_string())), + country_code: Some("+91".to_string()), + }), + }), + ..Default::default() + }), + ..Default::default() + }) +} + +fn get_default_payment_authorize_data() -> Option { + Some(types::PaymentsAuthorizeData { + email: Some(Secret::new("abc@gmail.com".to_string())), + ..PaymentAuthorizeType::default().0 + }) +} +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = Cybersource {} + .authorize_payment( + get_default_payment_authorize_data(), + get_default_payment_info(), + ) + .await; + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +#[actix_web::test] +async fn should_authorize_and_capture_payment() { + let connector = Cybersource {}; + let response = connector + .make_payment( + get_default_payment_authorize_data(), + get_default_payment_info(), + ) + .await; + let sync_response = connector + .sync_payment( + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + utils::get_connector_transaction_id(response).unwrap(), + ), + encoded_data: None, + }), + get_default_payment_info(), + ) + .await; + //cybersource takes sometime to settle the transaction,so it will be in pending for long time + assert_eq!(sync_response.status, enums::AttemptStatus::Pending); +} + +#[actix_web::test] +async fn should_sync_capture_payment() { + let sync_response = Cybersource {} + .sync_payment( + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + "6736046645576085004953".to_string(), + ), + encoded_data: None, + }), + get_default_payment_info(), + ) + .await; + assert_eq!(sync_response.status, enums::AttemptStatus::Charged); +} + +#[actix_web::test] +async fn should_capture_already_authorized_payment() { + let connector = Cybersource {}; + let authorize_response = connector + .authorize_payment( + get_default_payment_authorize_data(), + get_default_payment_info(), + ) + .await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); + let txn_id = utils::get_connector_transaction_id(authorize_response); + let response: OptionFuture<_> = txn_id + .map(|transaction_id| async move { + connector + .capture_payment(transaction_id, None, get_default_payment_info()) + .await + .status + }) + .into(); + //cybersource takes sometime to settle the transaction,so it will be in pending for long time + assert_eq!(response.await, Some(enums::AttemptStatus::Pending)); +} + +#[actix_web::test] +async fn should_void_already_authorized_payment() { + let connector = Cybersource {}; + let authorize_response = connector + .authorize_payment( + get_default_payment_authorize_data(), + get_default_payment_info(), + ) + .await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized); + let txn_id = utils::get_connector_transaction_id(authorize_response); + let response: OptionFuture<_> = txn_id + .map(|transaction_id| async move { + connector + .void_payment(transaction_id, None, get_default_payment_info()) + .await + .status + }) + .into(); + assert_eq!(response.await, Some(enums::AttemptStatus::Voided)); +} + +#[actix_web::test] +async fn should_refund_succeeded_payment() { + let connector = Cybersource {}; + //make a successful payment + let response = connector + .make_payment( + get_default_payment_authorize_data(), + get_default_payment_info(), + ) + .await; + + //try refund for previous payment + let transaction_id = utils::get_connector_transaction_id(response).unwrap(); + let response = connector + .refund_payment(transaction_id, None, get_default_payment_info()) + .await; + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Pending, //cybersource takes sometime to refund the transaction,so it will be in pending state for long time + ); +} + +#[actix_web::test] +async fn should_sync_refund() { + let connector = Cybersource {}; + let response = connector + .sync_refund( + "6738063831816571404953".to_string(), + None, + get_default_payment_info(), + ) + .await; + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Pending, //cybersource takes sometime to refund the transaction,so it will be in pending state for long time + ); +} + +#[actix_web::test] +async fn should_fail_payment_for_incorrect_card_number() { + let response = Cybersource {} + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::CCard { + card_number: Secret::new("424242442424242".to_string()), + ..utils::CCardType::default().0 + }), + ..get_default_payment_authorize_data().unwrap() + }), + get_default_payment_info(), + ) + .await; + assert_eq!(response.status, enums::AttemptStatus::Failure); + let x = response.response.unwrap_err(); + assert_eq!(x.message, "Decline - Invalid account number".to_string(),); +} + +#[actix_web::test] +async fn should_fail_payment_for_incorrect_exp_month() { + let response = Cybersource {} + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::CCard { + card_number: Secret::new("4242424242424242".to_string()), + card_exp_month: Secret::new("101".to_string()), + ..utils::CCardType::default().0 + }), + ..get_default_payment_authorize_data().unwrap() + }), + get_default_payment_info(), + ) + .await; + let x = response.response.unwrap_err(); + assert_eq!( + x.message, + r#"[{"field":"paymentInformation.card.expirationMonth","reason":"INVALID_DATA"}]"# + .to_string(), + ); +} diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 2cd9aa7d24..cca173d602 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -4,6 +4,7 @@ mod aci; mod authorizedotnet; mod checkout; mod connector_auth; +mod cybersource; mod fiserv; mod globalpay; mod payu; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index fcb5c75df2..a9587db05a 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -13,6 +13,11 @@ key1 = "MyTransactionKey" api_key = "Bearer MyApiKey" key1 = "MyProcessingChannelId" +[cybersource] +api_key = "Bearer MyApiKey" +key1 = "Merchant id" +api_secret = "Secret key" + [shift4] api_key = "Bearer MyApiKey"