feat(core/connector): [SANTANDER] Send back connector refund id in refund response and end_to_end_id in payments response (#11244)

Co-authored-by: Sayak Bhattacharya <sayak.b@juspay.in>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sayak Bhattacharya
2026-03-03 21:58:40 +05:30
committed by GitHub
parent b78375b646
commit d70567b5e9
14 changed files with 156 additions and 13 deletions

View File

@@ -15247,6 +15247,21 @@
}
}
},
"ConnectorMetadataResponse": {
"oneOf": [
{
"type": "object",
"required": [
"santander"
],
"properties": {
"santander": {
"$ref": "#/components/schemas/SantanderData"
}
}
}
]
},
"ConnectorSelection": {
"oneOf": [
{
@@ -29314,6 +29329,14 @@
],
"nullable": true
},
"connector_response_metadata": {
"allOf": [
{
"$ref": "#/components/schemas/ConnectorMetadataResponse"
}
],
"nullable": true
},
"feature_metadata": {
"allOf": [
{
@@ -30957,6 +30980,14 @@
],
"nullable": true
},
"connector_response_metadata": {
"allOf": [
{
"$ref": "#/components/schemas/ConnectorMetadataResponse"
}
],
"nullable": true
},
"feature_metadata": {
"allOf": [
{
@@ -35706,6 +35737,11 @@
"type": "string",
"description": "Contains whole connector response",
"nullable": true
},
"connector_refund_id": {
"type": "string",
"description": "A unique identifier for a payment provided by the connector",
"nullable": true
}
}
},
@@ -37263,6 +37299,16 @@
}
}
},
"SantanderData": {
"type": "object",
"properties": {
"end_to_end_id": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"ScaExemptionType": {
"type": "string",
"description": "SCA Exemptions types available for authentication",

View File

@@ -10795,6 +10795,21 @@
}
}
},
"ConnectorMetadataResponse": {
"oneOf": [
{
"type": "object",
"required": [
"santander"
],
"properties": {
"santander": {
"$ref": "#/components/schemas/SantanderData"
}
}
}
]
},
"ConnectorSelection": {
"oneOf": [
{
@@ -28169,6 +28184,16 @@
}
}
},
"SantanderData": {
"type": "object",
"properties": {
"end_to_end_id": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"ScaExemptionType": {
"type": "string",
"description": "SCA Exemptions types available for authentication",

View File

@@ -7461,6 +7461,11 @@ pub struct PaymentsResponse {
#[smithy(value_type = "Option<ConnectorMetadata>")]
pub connector_metadata: Option<serde_json::Value>, // This is Value because it is fetched from DB and before putting in DB the type is validated
/// Returns additional provider-specific metadata for certain connectors
#[schema(value_type = Option<ConnectorMetadataResponse>)]
#[smithy(value_type = "Option<ConnectorMetadataResponse>")]
pub connector_response_metadata: Option<ConnectorMetadataResponse>,
/// Additional data that might be required by hyperswitch, to enable some specific features.
#[schema(value_type = Option<FeatureMetadata>)]
#[smithy(value_type = "Option<FeatureMetadata>")]
@@ -9834,6 +9839,21 @@ pub struct ConnectorMetadata {
pub peachpayments: Option<PeachpaymentsData>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema, SmithyModel)]
#[smithy(namespace = "com.hyperswitch.smithy.types")]
#[serde(rename_all = "snake_case")]
pub enum ConnectorMetadataResponse {
Santander(SantanderData),
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, ToSchema, SmithyModel)]
#[smithy(namespace = "com.hyperswitch.smithy.types")]
#[serde(deny_unknown_fields)]
pub struct SantanderData {
#[serde(skip_serializing_if = "Option::is_none")]
pub end_to_end_id: Option<String>,
}
impl ConnectorMetadata {
pub fn from_value(
value: pii::SecretSerdeValue,

View File

@@ -351,6 +351,9 @@ pub struct RefundResponse {
/// Contains whole connector response
#[schema(value_type = Option<String>)]
pub raw_connector_response: Option<masking::Secret<String>>,
/// A unique identifier for a payment provided by the connector
#[smithy(value_type = "Option<String>")]
pub connector_refund_id: Option<String>,
}
#[cfg(feature = "v1")]

View File

@@ -160,6 +160,7 @@ pub struct ImmediateExpirationTime {
#[diesel(sql_type = Json)]
pub struct ScheduledExpirationTime {
/// Expiration time in terms of date, format: YYYY-MM-DD
#[serde(with = "common_utils::custom_serde::date_only")]
pub date: time::PrimitiveDateTime,
/// Days after expiration date for which the QR code remains valid
pub validity_after_expiration: Option<u32>,

View File

@@ -1,4 +1,4 @@
use api_models::payments::{QrCodeInformation, VoucherNextStepData};
use api_models::payments::{QrCodeInformation, SantanderData, VoucherNextStepData};
use common_enums::{
enums, AttemptStatus, BoletoDocumentKind, BoletoPaymentType, ExpiryType, PixKey,
};
@@ -802,13 +802,16 @@ impl<F, T> TryFrom<ResponseRouterData<F, SantanderPaymentsSyncResponse, T, Payme
_ => {
let connector_metadata = pix_data
.pix
.ok_or_else(|| errors::ConnectorError::ParsingFailed)?
.first()
.as_ref()
.and_then(|pix_list| pix_list.first())
.map(|pix| {
serde_json::json!({
"end_to_end_id": pix.end_to_end_id.clone().expose()
})
});
let data = SantanderData {
end_to_end_id: Some(pix.end_to_end_id.clone().expose()),
};
serde_json::to_value(data)
.change_context(errors::ConnectorError::ParsingFailed)
})
.transpose()?;
Ok(Self {
status: AttemptStatus::from(pix_data.status),
response: Ok(PaymentsResponseData::TransactionResponse {
@@ -927,7 +930,6 @@ impl<F, T> TryFrom<ResponseRouterData<F, SantanderPaymentsResponse, T, PaymentsR
}
SantanderPaymentsResponse::Boleto(boleto_data) => {
let qr_code_url = if let Some(data) = boleto_data.qr_code_pix.clone() {
router_env::logger::debug!("Data to be converted into QR code: {}", data);
let qr_image = QrImage::new_from_data(data)
.change_context(errors::ConnectorError::ResponseHandlingFailed)?;
let url_str = &qr_image.data;
@@ -1137,11 +1139,6 @@ fn convert_pix_data_to_value(
data: String,
variant: Option<ExpiryType>,
) -> CustomResult<Option<Value>, errors::ConnectorError> {
if router_env::which() != router_env::env::Env::Production {
// The data string is the EMV string which is used to generate the QR code. We are generating the QR code and then converting it to a data URL to be sent to the client. This is because the client can directly use the data URL to display the QR code without needing to generate it on their end.
// We are logging it because in dev env, we won't be able to scan this QR and complete the payment. We would need to send this EMV to the Santander Support Team to finish the payment on their end so that we can test other flows like PSync/Refund/RSync etc
router_env::logger::debug!("Data to be converted into QR code: {}", data);
}
let image_data = QrImage::new_from_data(data.clone())
.change_context(errors::ConnectorError::ResponseHandlingFailed)?;

View File

@@ -1,3 +1,5 @@
use std::str::FromStr;
#[cfg(all(feature = "v1", feature = "olap"))]
use api_models::enums::Connector;
#[cfg(feature = "v2")]
@@ -1831,6 +1833,31 @@ impl PaymentAttempt {
.is_some_and(|unsupported_set| unsupported_set.contains(&pm_type))
})
}
/// Extract connector response metadata from the payment attempt metadata based on the connector's configuration
pub fn get_connector_response_metadata_from_attempt_metadata(
&self,
) -> Option<api_models::payments::ConnectorMetadataResponse> {
let connector = self
.connector
.as_deref()
.and_then(|s| Connector::from_str(s).ok())?;
self.connector_metadata
.clone()
.and_then(|metadata| match connector {
Connector::Santander => metadata
.parse_value::<api_models::payments::SantanderData>("SantanderData")
.map_err(|_| {
router_env::logger::warn!(
"Failed to parse payment_attempt.connector_metadata to SantanderData"
)
})
.ok()
.map(api_models::payments::ConnectorMetadataResponse::Santander),
_ => None,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]

View File

@@ -525,6 +525,8 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payments::BankRedirectBilling,
api_models::payments::BankRedirectBilling,
api_models::payments::ConnectorMetadata,
api_models::payments::ConnectorMetadataResponse,
api_models::payments::SantanderData,
api_models::payments::FeatureMetadata,
api_models::payments::ApplepayConnectorMetadataRequest,
api_models::payments::SessionTokenInfo,

View File

@@ -472,6 +472,8 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payments::BankRedirectBilling,
api_models::payments::BankRedirectBilling,
api_models::payments::ConnectorMetadata,
api_models::payments::ConnectorMetadataResponse,
api_models::payments::SantanderData,
api_models::payments::FeatureMetadata,
api_models::payments::SdkType,
api_models::payments::ApplepayConnectorMetadataRequest,

View File

@@ -3995,6 +3995,9 @@ where
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse feature metadata")?;
let connector_response_metadata =
payment_attempt.get_connector_response_metadata_from_attempt_metadata();
let payments_response = api::PaymentsResponse {
payment_id: payment_intent.payment_id,
merchant_id: payment_intent.merchant_id,
@@ -4137,6 +4140,7 @@ where
partner_merchant_identifier_details: payment_intent.partner_merchant_identifier_details,
payment_method_tokenization_details,
installment_options: payment_intent.installment_options,
connector_response_metadata,
};
services::ApplicationResponse::JsonWithHeaders((payments_response, headers))
@@ -4301,6 +4305,7 @@ impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::Pay
fn foreign_from((pi, pa): (storage::PaymentIntent, storage::PaymentAttempt)) -> Self {
let connector_transaction_id = pa.get_connector_payment_id().map(ToString::to_string);
Self {
connector_response_metadata: pa.get_connector_response_metadata_from_attempt_metadata(),
payment_id: pi.payment_id,
merchant_id: pi.merchant_id,
status: pi.status,

View File

@@ -1790,6 +1790,11 @@ impl ForeignFrom<diesel_refund::Refund> for api::RefundResponse {
issuer_error_code: refund.issuer_error_code,
issuer_error_message: refund.issuer_error_message,
raw_connector_response: None,
connector_refund_id: refund
.connector_refund_id
.as_ref()
.map(ConnectorTransactionId::get_id)
.map(ToOwned::to_owned),
}
}
}
@@ -1819,6 +1824,11 @@ impl ForeignFrom<(diesel_refund::Refund, Option<masking::Secret<String>>)> for a
issuer_error_code: refund.issuer_error_code,
issuer_error_message: refund.issuer_error_message,
raw_connector_response,
connector_refund_id: refund
.connector_refund_id
.as_ref()
.map(ConnectorTransactionId::get_id)
.map(ToOwned::to_owned),
}
}
}

View File

@@ -1531,6 +1531,7 @@ mod tests {
error_details: None,
installment_options: None,
state_metadata: None,
connector_response_metadata: None,
};
let content =
api_webhooks::OutgoingWebhookContent::PaymentDetails(Box::new(expected_response));

View File

@@ -482,6 +482,7 @@ async fn payments_create_core() {
error_details: None,
installment_options: None,
state_metadata: None,
connector_response_metadata: None,
};
let expected_response =
services::ApplicationResponse::JsonWithHeaders((expected_response, vec![]));
@@ -782,6 +783,7 @@ async fn payments_create_core_adyen_no_redirect() {
error_details: None,
installment_options: None,
state_metadata: None,
connector_response_metadata: None,
},
vec![],
));

View File

@@ -242,6 +242,7 @@ async fn payments_create_core() {
error_details: None,
installment_options: None,
state_metadata: None,
connector_response_metadata: None,
};
let expected_response =
@@ -551,6 +552,7 @@ async fn payments_create_core_adyen_no_redirect() {
error_details: None,
installment_options: None,
state_metadata: None,
connector_response_metadata: None,
},
vec![],
));