mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
feat(connectors): [Redsys] add Psync and Rsync support (#7586)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -3591,6 +3591,15 @@ dependencies = [
|
||||
"toml 0.5.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html-escape"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
|
||||
dependencies = [
|
||||
"utf8-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@ -3840,6 +3849,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"error-stack",
|
||||
"hex",
|
||||
"html-escape",
|
||||
"http 0.2.12",
|
||||
"hyperswitch_domain_models",
|
||||
"hyperswitch_interfaces",
|
||||
@ -9026,6 +9036,12 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
|
||||
@ -173,3 +173,6 @@ pub const DEFAULT_CUSTOMER_ID_BLOCKING_THRESHOLD: i32 = 5;
|
||||
|
||||
/// Default Card Testing Guard Redis Expiry in seconds
|
||||
pub const DEFAULT_CARD_TESTING_GUARD_EXPIRY_IN_SECS: i32 = 3600;
|
||||
|
||||
/// SOAP 1.1 Envelope Namespace
|
||||
pub const SOAP_ENV_NAMESPACE: &str = "http://schemas.xmlsoap.org/soap/envelope/";
|
||||
|
||||
@ -46,7 +46,7 @@ urlencoding = "2.1.3"
|
||||
uuid = { version = "1.8.0", features = ["v4"] }
|
||||
lazy_static = "1.4.0"
|
||||
unicode-normalization = "0.1.21"
|
||||
|
||||
html-escape = "0.2"
|
||||
# First party crates
|
||||
api_models = { version = "0.1.0", path = "../api_models", features = ["errors"], default-features = false }
|
||||
cards = { version = "0.1.0", path = "../cards" }
|
||||
|
||||
@ -4,7 +4,7 @@ use std::sync::LazyLock;
|
||||
|
||||
use common_utils::{
|
||||
errors::CustomResult,
|
||||
ext_traits::BytesExt,
|
||||
ext_traits::{BytesExt, XmlExt},
|
||||
request::{Method, Request, RequestBuilder, RequestContent},
|
||||
types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector},
|
||||
};
|
||||
@ -29,7 +29,7 @@ use hyperswitch_domain_models::{
|
||||
types::{
|
||||
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
|
||||
PaymentsCompleteAuthorizeRouterData, PaymentsPreProcessingRouterData,
|
||||
RefundExecuteRouterData,
|
||||
PaymentsSyncRouterData, RefundExecuteRouterData, RefundSyncRouterData,
|
||||
},
|
||||
};
|
||||
use hyperswitch_interfaces::{
|
||||
@ -42,13 +42,14 @@ use hyperswitch_interfaces::{
|
||||
events::connector_api_logs::ConnectorEvent,
|
||||
types::{
|
||||
PaymentsAuthorizeType, PaymentsCaptureType, PaymentsCompleteAuthorizeType,
|
||||
PaymentsPreProcessingType, PaymentsVoidType, RefundExecuteType, Response,
|
||||
PaymentsPreProcessingType, PaymentsSyncType, PaymentsVoidType, RefundExecuteType,
|
||||
RefundSyncType, Response,
|
||||
},
|
||||
webhooks,
|
||||
};
|
||||
use transformers as redsys;
|
||||
|
||||
use crate::{types::ResponseRouterData, utils as connector_utils};
|
||||
use crate::{constants::headers, types::ResponseRouterData, utils as connector_utils};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Redsys {
|
||||
@ -78,11 +79,6 @@ impl api::PaymentToken for Redsys {}
|
||||
impl api::PaymentsPreProcessing for Redsys {}
|
||||
impl api::PaymentsCompleteAuthorize for Redsys {}
|
||||
|
||||
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Redsys where
|
||||
Self: ConnectorIntegration<Flow, Request, Response>
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorCommon for Redsys {
|
||||
fn id(&self) -> &'static str {
|
||||
"redsys"
|
||||
@ -143,6 +139,29 @@ impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsRespons
|
||||
}
|
||||
}
|
||||
|
||||
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Redsys
|
||||
where
|
||||
Self: ConnectorIntegration<Flow, Request, Response>,
|
||||
{
|
||||
fn build_headers(
|
||||
&self,
|
||||
_req: &RouterData<Flow, Request, Response>,
|
||||
_connectors: &Connectors,
|
||||
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
|
||||
let headers = vec![
|
||||
(
|
||||
headers::CONTENT_TYPE.to_string(),
|
||||
"application/xml".to_string().into(),
|
||||
),
|
||||
(
|
||||
headers::SOAP_ACTION.to_string(),
|
||||
redsys::REDSYS_SOAP_ACTION.to_string().into(),
|
||||
),
|
||||
];
|
||||
Ok(headers)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<PreProcessing, PaymentsPreProcessingData, PaymentsResponseData>
|
||||
for Redsys
|
||||
{
|
||||
@ -621,9 +640,159 @@ impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Redsys
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Redsys {}
|
||||
impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Redsys {
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &PaymentsSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Redsys {}
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &PaymentsSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!(
|
||||
"{}/apl02/services/SerClsWSConsulta",
|
||||
self.base_url(connectors)
|
||||
))
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &PaymentsSyncRouterData,
|
||||
_connectors: &Connectors,
|
||||
) -> CustomResult<RequestContent, errors::ConnectorError> {
|
||||
let connector_req = redsys::build_payment_sync_request(req)?;
|
||||
Ok(RequestContent::RawBytes(connector_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &PaymentsSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Option<Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
RequestBuilder::new()
|
||||
.method(Method::Post)
|
||||
.url(&PaymentsSyncType::get_url(self, req, connectors)?)
|
||||
.attach_default_headers()
|
||||
.headers(PaymentsSyncType::get_headers(self, req, connectors)?)
|
||||
.set_body(self.get_request_body(req, connectors)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &PaymentsSyncRouterData,
|
||||
event_builder: Option<&mut ConnectorEvent>,
|
||||
res: Response,
|
||||
) -> CustomResult<PaymentsSyncRouterData, errors::ConnectorError> {
|
||||
let response = String::from_utf8(res.response.to_vec())
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
let response_data = html_escape::decode_html_entities(&response).to_ascii_lowercase();
|
||||
let response = response_data
|
||||
.parse_xml::<redsys::RedsysSyncResponse>()
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
event_builder.map(|i| i.set_response_body(&response));
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
RouterData::try_from(ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
event_builder: Option<&mut ConnectorEvent>,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res, event_builder)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Redsys {
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &RefundSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &RefundSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!(
|
||||
"{}/apl02/services/SerClsWSConsulta",
|
||||
self.base_url(connectors)
|
||||
))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &RefundSyncRouterData,
|
||||
_connectors: &Connectors,
|
||||
) -> CustomResult<RequestContent, errors::ConnectorError> {
|
||||
let connector_req = redsys::build_refund_sync_request(req)?;
|
||||
Ok(RequestContent::RawBytes(connector_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &RefundSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Option<Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
RequestBuilder::new()
|
||||
.method(Method::Post)
|
||||
.url(&RefundSyncType::get_url(self, req, connectors)?)
|
||||
.attach_default_headers()
|
||||
.headers(RefundSyncType::get_headers(self, req, connectors)?)
|
||||
.set_body(self.get_request_body(req, connectors)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &RefundSyncRouterData,
|
||||
event_builder: Option<&mut ConnectorEvent>,
|
||||
res: Response,
|
||||
) -> CustomResult<RefundSyncRouterData, errors::ConnectorError> {
|
||||
let response = String::from_utf8(res.response.to_vec())
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
let response_data = html_escape::decode_html_entities(&response).to_ascii_lowercase();
|
||||
let response = response_data
|
||||
.parse_xml::<redsys::RedsysSyncResponse>()
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
event_builder.map(|i| i.set_response_body(&response));
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
RouterData::try_from(ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
event_builder: Option<&mut ConnectorEvent>,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res, event_builder)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<PaymentMethodToken, PaymentMethodTokenizationData, PaymentsResponseData>
|
||||
for Redsys
|
||||
|
||||
@ -11,19 +11,20 @@ use hyperswitch_domain_models::{
|
||||
address::Address,
|
||||
payment_method_data::PaymentMethodData,
|
||||
router_data::{ConnectorAuthType, ErrorResponse, RouterData},
|
||||
router_flow_types::refunds::Execute,
|
||||
router_flow_types::refunds::{Execute, RSync},
|
||||
router_request_types::{
|
||||
BrowserInformation, CompleteAuthorizeData, PaymentsAuthorizeData, PaymentsCancelData,
|
||||
PaymentsCaptureData, PaymentsPreProcessingData, ResponseId,
|
||||
PaymentsCaptureData, PaymentsPreProcessingData, PaymentsSyncData, ResponseId,
|
||||
},
|
||||
router_response_types::{PaymentsResponseData, RedirectForm, RefundsResponseData},
|
||||
types::{
|
||||
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
|
||||
PaymentsCompleteAuthorizeRouterData, PaymentsPreProcessingRouterData, RefundsRouterData,
|
||||
PaymentsCompleteAuthorizeRouterData, PaymentsPreProcessingRouterData,
|
||||
PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData,
|
||||
},
|
||||
};
|
||||
use hyperswitch_interfaces::errors;
|
||||
use masking::{ExposeInterface, Secret};
|
||||
use masking::{ExposeInterface, PeekInterface, Secret};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
@ -36,6 +37,20 @@ use crate::{
|
||||
};
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
|
||||
const DS_VERSION: &str = "0.0";
|
||||
const SIGNATURE_VERSION: &str = "HMAC_SHA256_V1";
|
||||
const XMLNS_WEB_URL: &str = "http://webservices.apl02.redsys.es";
|
||||
pub const REDSYS_SOAP_ACTION: &str = "consultaOperaciones";
|
||||
|
||||
// Specifies the type of transaction for XML requests
|
||||
pub mod transaction_type {
|
||||
pub const PAYMENT: &str = "0";
|
||||
pub const PREAUTHORIZATION: &str = "1";
|
||||
pub const CONFIRMATION: &str = "2";
|
||||
pub const REFUND: &str = "3";
|
||||
pub const CANCELLATION: &str = "9";
|
||||
}
|
||||
|
||||
pub struct RedsysRouterData<T> {
|
||||
pub amount: StringMinorUnit,
|
||||
pub currency: api_models::enums::Currency,
|
||||
@ -753,8 +768,6 @@ fn handle_threeds_invoke_exempt<F>(
|
||||
})
|
||||
}
|
||||
|
||||
pub const SIGNATURE_VERSION: &str = "HMAC_SHA256_V1";
|
||||
|
||||
fn des_encrypt(
|
||||
message: &str,
|
||||
key: &str,
|
||||
@ -862,12 +875,12 @@ fn get_redsys_attempt_status(
|
||||
"0900" => Ok(enums::AttemptStatus::Charged),
|
||||
"0400" => Ok(enums::AttemptStatus::Voided),
|
||||
"0950" => Ok(enums::AttemptStatus::VoidFailed),
|
||||
"9998" | "9999" => Ok(enums::AttemptStatus::Pending),
|
||||
"9256" | "9257" => Ok(enums::AttemptStatus::AuthenticationFailed),
|
||||
"9998" => Ok(enums::AttemptStatus::AuthenticationPending),
|
||||
"9256" | "9257" | "0184" => Ok(enums::AttemptStatus::AuthenticationFailed),
|
||||
"0101" | "0102" | "0106" | "0125" | "0129" | "0172" | "0173" | "0174" | "0180"
|
||||
| "0184" | "0190" | "0191" | "0195" | "0202" | "0904" | "0909" | "0913" | "0944"
|
||||
| "9912" | "0912" | "9064" | "9078" | "9093" | "9094" | "9104" | "9218" | "9253"
|
||||
| "9261" | "9915" | "9997" => Ok(enums::AttemptStatus::Failure),
|
||||
| "0190" | "0191" | "0195" | "0202" | "0904" | "0909" | "0913" | "0944" | "9912"
|
||||
| "0912" | "9064" | "9078" | "9093" | "9094" | "9104" | "9218" | "9253" | "9261"
|
||||
| "9915" | "9997" | "9999" => Ok(enums::AttemptStatus::Failure),
|
||||
error => Err(errors::ConnectorError::ResponseHandlingFailed)
|
||||
.attach_printable(format!("Received Unknown Status:{}", error))?,
|
||||
}
|
||||
@ -1181,7 +1194,6 @@ pub struct RedsysOperationRequest {
|
||||
pub struct RedsysOperationsResponse {
|
||||
#[serde(rename = "Ds_Order")]
|
||||
ds_order: String,
|
||||
|
||||
#[serde(rename = "Ds_Response")]
|
||||
ds_response: DsResponse,
|
||||
#[serde(rename = "Ds_AuthorisationCode")]
|
||||
@ -1382,8 +1394,8 @@ impl TryFrom<DsResponse> for enums::RefundStatus {
|
||||
fn try_from(ds_response: DsResponse) -> Result<Self, Self::Error> {
|
||||
match ds_response.0.as_str() {
|
||||
"0900" => Ok(Self::Success),
|
||||
"9998" | "9999" => Ok(Self::Pending),
|
||||
"0950" => Ok(Self::Failure),
|
||||
"9999" => Ok(Self::Pending),
|
||||
"0950" | "0172" | "174" => Ok(Self::Failure),
|
||||
unknown_status => Err(errors::ConnectorError::ResponseHandlingFailed)
|
||||
.attach_printable(format!("Received unknown status:{}", unknown_status))?,
|
||||
}
|
||||
@ -1418,7 +1430,7 @@ impl TryFrom<RefundsResponseRouterData<Execute, RedsysResponse>> for RefundsRout
|
||||
} else {
|
||||
Ok(RefundsResponseData {
|
||||
connector_refund_id: response_data.ds_order,
|
||||
refund_status: enums::RefundStatus::try_from(response_data.ds_response)?,
|
||||
refund_status,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1503,3 +1515,384 @@ fn get_payments_response(
|
||||
Ok((response, enums::AttemptStatus::AuthenticationPending))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Messages {
|
||||
#[serde(rename = "Version")]
|
||||
version: VersionData,
|
||||
#[serde(rename = "Signature")]
|
||||
signature: String,
|
||||
#[serde(rename = "SignatureVersion")]
|
||||
signature_version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename = "Version")]
|
||||
pub struct VersionData {
|
||||
#[serde(rename = "@Ds_Version")]
|
||||
ds_version: String,
|
||||
#[serde(rename = "Message")]
|
||||
message: Message,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Message {
|
||||
#[serde(rename = "Transaction")]
|
||||
transaction: RedsysSyncRequest,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename = "Transaction")]
|
||||
pub struct RedsysSyncRequest {
|
||||
#[serde(rename = "Ds_MerchantCode")]
|
||||
ds_merchant_code: Secret<String>,
|
||||
#[serde(rename = "Ds_Terminal")]
|
||||
ds_terminal: Secret<String>,
|
||||
#[serde(rename = "Ds_Order")]
|
||||
ds_order: String,
|
||||
#[serde(rename = "Ds_TransactionType")]
|
||||
ds_transaction_type: String,
|
||||
}
|
||||
|
||||
fn get_transaction_type(
|
||||
status: enums::AttemptStatus,
|
||||
capture_method: Option<enums::CaptureMethod>,
|
||||
) -> Result<String, errors::ConnectorError> {
|
||||
match status {
|
||||
enums::AttemptStatus::AuthenticationPending
|
||||
| enums::AttemptStatus::AuthenticationSuccessful
|
||||
| enums::AttemptStatus::Started
|
||||
| enums::AttemptStatus::Authorizing
|
||||
| enums::AttemptStatus::DeviceDataCollectionPending => match capture_method {
|
||||
Some(enums::CaptureMethod::Automatic) | None => {
|
||||
Ok(transaction_type::PAYMENT.to_owned())
|
||||
}
|
||||
Some(enums::CaptureMethod::Manual) => Ok(transaction_type::PREAUTHORIZATION.to_owned()),
|
||||
Some(capture_method) => Err(errors::ConnectorError::NotSupported {
|
||||
message: capture_method.to_string(),
|
||||
connector: "redsys",
|
||||
}),
|
||||
},
|
||||
enums::AttemptStatus::VoidInitiated => Ok(transaction_type::CANCELLATION.to_owned()),
|
||||
enums::AttemptStatus::PartialChargedAndChargeable
|
||||
| enums::AttemptStatus::CaptureInitiated => Ok(transaction_type::CONFIRMATION.to_owned()),
|
||||
enums::AttemptStatus::Authorized | enums::AttemptStatus::Pending => match capture_method {
|
||||
Some(enums::CaptureMethod::Automatic) | None => {
|
||||
Ok(transaction_type::PAYMENT.to_owned())
|
||||
}
|
||||
Some(enums::CaptureMethod::Manual) => Ok(transaction_type::CONFIRMATION.to_owned()),
|
||||
Some(capture_method) => Err(errors::ConnectorError::NotSupported {
|
||||
message: capture_method.to_string(),
|
||||
connector: "redsys",
|
||||
}),
|
||||
},
|
||||
other_attempt_status => Err(errors::ConnectorError::NotSupported {
|
||||
message: format!(
|
||||
"Payment sync after terminal status: {} payment",
|
||||
other_attempt_status
|
||||
),
|
||||
connector: "redsys",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_sync_request(
|
||||
order_id: String,
|
||||
transaction_type: String,
|
||||
auth: RedsysAuthType,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let transaction_data = RedsysSyncRequest {
|
||||
ds_merchant_code: auth.merchant_id,
|
||||
ds_terminal: auth.terminal_id,
|
||||
ds_transaction_type: transaction_type,
|
||||
ds_order: order_id.clone(),
|
||||
};
|
||||
let version = VersionData {
|
||||
ds_version: DS_VERSION.to_owned(),
|
||||
message: Message {
|
||||
transaction: transaction_data,
|
||||
},
|
||||
};
|
||||
let version_data = quick_xml::se::to_string(&version)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
|
||||
let signature = get_signature(&order_id, &version_data, auth.sha256_pwd.peek())?;
|
||||
|
||||
let messages = Messages {
|
||||
version,
|
||||
signature,
|
||||
signature_version: SIGNATURE_VERSION.to_owned(),
|
||||
};
|
||||
|
||||
let cdata = quick_xml::se::to_string(&messages)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
|
||||
let body = format!(
|
||||
r#"<soapenv:Envelope xmlns:soapenv="{}" xmlns:web="{}"><soapenv:Header/><soapenv:Body><web:consultaOperaciones><cadenaXML><![CDATA[{}]]></cadenaXML></web:consultaOperaciones></soapenv:Body></soapenv:Envelope>"#,
|
||||
common_utils::consts::SOAP_ENV_NAMESPACE,
|
||||
XMLNS_WEB_URL,
|
||||
cdata
|
||||
);
|
||||
|
||||
Ok(body.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
pub fn build_payment_sync_request(item: &PaymentsSyncRouterData) -> Result<Vec<u8>, Error> {
|
||||
let transaction_type = get_transaction_type(item.status, item.request.capture_method)?;
|
||||
let auth = RedsysAuthType::try_from(&item.connector_auth_type)?;
|
||||
let connector_transaction_id = item
|
||||
.request
|
||||
.connector_transaction_id
|
||||
.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
construct_sync_request(connector_transaction_id, transaction_type, auth)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename = "soapenv:envelope")]
|
||||
pub struct RedsysSyncResponse {
|
||||
#[serde(rename = "@xmlns:soapenv")]
|
||||
xmlns_soapenv: String,
|
||||
#[serde(rename = "@xmlns:soapenc")]
|
||||
xmlns_soapenc: String,
|
||||
#[serde(rename = "@xmlns:xsd")]
|
||||
xmlns_xsd: String,
|
||||
#[serde(rename = "@xmlns:xsi")]
|
||||
xmlns_xsi: String,
|
||||
#[serde(rename = "header")]
|
||||
header: Option<SoapHeader>,
|
||||
#[serde(rename = "body")]
|
||||
body: SyncResponseBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SoapHeader {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct SyncResponseBody {
|
||||
consultaoperacionesresponse: ConsultaOperacionesResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ConsultaOperacionesResponse {
|
||||
#[serde(rename = "@xmlns:p259")]
|
||||
xmlns_p259: String,
|
||||
consultaoperacionesreturn: ConsultaOperacionesReturn,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ConsultaOperacionesReturn {
|
||||
messages: MessagesResponseData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct MessagesResponseData {
|
||||
version: VersionResponseData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct VersionResponseData {
|
||||
#[serde(rename = "@ds_version")]
|
||||
ds_version: String,
|
||||
message: MessageResponseType,
|
||||
}
|
||||
|
||||
// The response will contain either a sync transaction data or error data.
|
||||
// Since the XML parser does not support enums for this case, we use Option to handle both scenarios.
|
||||
// If both are present or both are absent, an error is thrown.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MessageResponseType {
|
||||
response: Option<RedsysSyncResponseData>,
|
||||
errormsg: Option<SyncErrorCode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SyncErrorCode {
|
||||
ds_errorcode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct RedsysSyncResponseData {
|
||||
ds_order: String,
|
||||
ds_transactiontype: String,
|
||||
ds_amount: Option<String>,
|
||||
ds_currency: Option<String>,
|
||||
ds_securepayment: Option<String>,
|
||||
ds_state: Option<String>,
|
||||
ds_response: Option<DsResponse>,
|
||||
}
|
||||
|
||||
impl<F> TryFrom<ResponseRouterData<F, RedsysSyncResponse, PaymentsSyncData, PaymentsResponseData>>
|
||||
for RouterData<F, PaymentsSyncData, PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
|
||||
fn try_from(
|
||||
item: ResponseRouterData<F, RedsysSyncResponse, PaymentsSyncData, PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let message_data = item
|
||||
.response
|
||||
.body
|
||||
.consultaoperacionesresponse
|
||||
.consultaoperacionesreturn
|
||||
.messages
|
||||
.version
|
||||
.message;
|
||||
let (status, response) = match (message_data.response, message_data.errormsg) {
|
||||
(Some(response), None) => {
|
||||
if let Some(ds_response) = response.ds_response {
|
||||
let status = get_redsys_attempt_status(
|
||||
ds_response.clone(),
|
||||
item.data.request.capture_method,
|
||||
)?;
|
||||
|
||||
if connector_utils::is_payment_failure(status) {
|
||||
let payment_response = Err(ErrorResponse {
|
||||
status_code: item.http_code,
|
||||
code: ds_response.0.clone(),
|
||||
message: ds_response.0.clone(),
|
||||
reason: Some(ds_response.0.clone()),
|
||||
attempt_status: None,
|
||||
connector_transaction_id: None,
|
||||
issuer_error_code: None,
|
||||
issuer_error_message: None,
|
||||
});
|
||||
(status, payment_response)
|
||||
} else {
|
||||
let payment_response = Ok(PaymentsResponseData::TransactionResponse {
|
||||
resource_id: ResponseId::ConnectorTransactionId(
|
||||
response.ds_order.clone(),
|
||||
),
|
||||
redirection_data: Box::new(None),
|
||||
mandate_reference: Box::new(None),
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: Some(response.ds_order.clone()),
|
||||
incremental_authorization_allowed: None,
|
||||
charges: None,
|
||||
});
|
||||
(status, payment_response)
|
||||
}
|
||||
} else {
|
||||
// When the payment is in authentication or still processing
|
||||
let payment_response = Ok(PaymentsResponseData::TransactionResponse {
|
||||
resource_id: ResponseId::ConnectorTransactionId(response.ds_order.clone()),
|
||||
redirection_data: Box::new(None),
|
||||
mandate_reference: Box::new(None),
|
||||
connector_metadata: None,
|
||||
network_txn_id: None,
|
||||
connector_response_reference_id: Some(response.ds_order.clone()),
|
||||
incremental_authorization_allowed: None,
|
||||
charges: None,
|
||||
});
|
||||
|
||||
(item.data.status, payment_response)
|
||||
}
|
||||
}
|
||||
(None, Some(errormsg)) => {
|
||||
let error_code = errormsg.ds_errorcode.clone();
|
||||
let response = Err(ErrorResponse {
|
||||
code: error_code.clone(),
|
||||
message: error_code.clone(),
|
||||
reason: Some(error_code),
|
||||
status_code: item.http_code,
|
||||
attempt_status: None,
|
||||
connector_transaction_id: None,
|
||||
issuer_error_code: None,
|
||||
issuer_error_message: None,
|
||||
});
|
||||
(item.data.status, response)
|
||||
}
|
||||
(Some(_), Some(_)) | (None, None) => {
|
||||
Err(errors::ConnectorError::ResponseHandlingFailed)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
status,
|
||||
response,
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_refund_sync_request(item: &RefundSyncRouterData) -> Result<Vec<u8>, Error> {
|
||||
let transaction_type = transaction_type::REFUND.to_owned();
|
||||
let auth = RedsysAuthType::try_from(&item.connector_auth_type)?;
|
||||
let connector_transaction_id = item.request.connector_transaction_id.clone();
|
||||
construct_sync_request(connector_transaction_id, transaction_type, auth)
|
||||
}
|
||||
|
||||
impl TryFrom<RefundsResponseRouterData<RSync, RedsysSyncResponse>> for RefundsRouterData<RSync> {
|
||||
type Error = Error;
|
||||
fn try_from(
|
||||
item: RefundsResponseRouterData<RSync, RedsysSyncResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let message_data = item
|
||||
.response
|
||||
.body
|
||||
.consultaoperacionesresponse
|
||||
.consultaoperacionesreturn
|
||||
.messages
|
||||
.version
|
||||
.message;
|
||||
let response = match (message_data.response, message_data.errormsg) {
|
||||
(None, Some(errormsg)) => {
|
||||
let error_code = errormsg.ds_errorcode.clone();
|
||||
Err(ErrorResponse {
|
||||
code: error_code.clone(),
|
||||
message: error_code.clone(),
|
||||
reason: Some(error_code),
|
||||
status_code: item.http_code,
|
||||
attempt_status: None,
|
||||
connector_transaction_id: None,
|
||||
issuer_error_code: None,
|
||||
issuer_error_message: None,
|
||||
})
|
||||
}
|
||||
(Some(response), None) => {
|
||||
if let Some(ds_response) = response.ds_response {
|
||||
let refund_status = enums::RefundStatus::try_from(ds_response.clone())?;
|
||||
|
||||
if connector_utils::is_refund_failure(refund_status) {
|
||||
Err(ErrorResponse {
|
||||
status_code: item.http_code,
|
||||
code: ds_response.0.clone(),
|
||||
message: ds_response.0.clone(),
|
||||
reason: Some(ds_response.0.clone()),
|
||||
attempt_status: None,
|
||||
connector_transaction_id: None,
|
||||
issuer_error_code: None,
|
||||
issuer_error_message: None,
|
||||
})
|
||||
} else {
|
||||
Ok(RefundsResponseData {
|
||||
connector_refund_id: response.ds_order,
|
||||
refund_status,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// When the refund is pending
|
||||
Ok(RefundsResponseData {
|
||||
connector_refund_id: response.ds_order,
|
||||
refund_status: enums::RefundStatus::Pending,
|
||||
})
|
||||
}
|
||||
}
|
||||
(Some(_), Some(_)) | (None, None) => {
|
||||
Err(errors::ConnectorError::ResponseHandlingFailed)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
response,
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ pub(crate) mod headers {
|
||||
pub(crate) const USER_AGENT: &str = "User-Agent";
|
||||
pub(crate) const KEY: &str = "key";
|
||||
pub(crate) const X_SIGNATURE: &str = "X-Signature";
|
||||
pub(crate) const SOAP_ACTION: &str = "SOAPAction";
|
||||
}
|
||||
|
||||
/// Unsupported response type error message
|
||||
|
||||
@ -6062,7 +6062,9 @@ pub fn generate_12_digit_number() -> u64 {
|
||||
pub fn normalize_string(value: String) -> Result<String, regex::Error> {
|
||||
let nfkd_value = value.nfkd().collect::<String>();
|
||||
let lowercase_value = nfkd_value.to_lowercase();
|
||||
let re = Regex::new(r"[^a-z0-9]")?;
|
||||
let normalized = re.replace_all(&lowercase_value, "").to_string();
|
||||
static REGEX: std::sync::LazyLock<Result<Regex, regex::Error>> =
|
||||
std::sync::LazyLock::new(|| Regex::new(r"[^a-z0-9]"));
|
||||
let regex = REGEX.as_ref().map_err(|e| e.clone())?;
|
||||
let normalized = regex.replace_all(&lowercase_value, "").to_string();
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user