refactor(relay): add manual multiple capture for relay and fix database deserialization (#11264)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sakil Mostak
2026-03-02 12:40:08 +05:30
committed by GitHub
parent 6ccc09e47c
commit 37bf9bc237
6 changed files with 182 additions and 38 deletions

View File

@@ -35753,6 +35753,14 @@
},
"currency": {
"$ref": "#/components/schemas/Currency"
},
"capture_method": {
"allOf": [
{
"$ref": "#/components/schemas/CaptureMethod"
}
],
"nullable": true
}
}
},

View File

@@ -54,6 +54,9 @@ pub struct RelayCaptureRequestData {
/// The currency in which the amount is being captured
#[schema(value_type = Currency)]
pub currency: api_enums::Currency,
/// type of capture for the relay
#[schema(value_type = Option<CaptureMethod>)]
pub capture_method: Option<api_enums::CaptureMethod>,
}
#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)]

View File

@@ -2957,6 +2957,40 @@ pub enum RelayStatus {
Failure,
}
impl RelayStatus {
pub fn get_void_status(attempt_status: AttemptStatus) -> Self {
match attempt_status {
AttemptStatus::Failure
| AttemptStatus::AuthenticationFailed
| AttemptStatus::RouterDeclined
| AttemptStatus::AuthorizationFailed
| AttemptStatus::CaptureFailed
| AttemptStatus::VoidFailed
| AttemptStatus::IntegrityFailure
| AttemptStatus::AutoRefunded
| AttemptStatus::Expired => Self::Failure,
AttemptStatus::Pending
| AttemptStatus::PaymentMethodAwaited
| AttemptStatus::Authorized
| AttemptStatus::PartiallyAuthorized
| AttemptStatus::AuthenticationSuccessful
| AttemptStatus::ConfirmationAwaited
| AttemptStatus::DeviceDataCollectionPending
| AttemptStatus::VoidInitiated
| AttemptStatus::Unresolved
| AttemptStatus::Charged
| AttemptStatus::PartialChargedAndChargeable
| AttemptStatus::CodInitiated
| AttemptStatus::PartialCharged
| AttemptStatus::Authorizing
| AttemptStatus::CaptureInitiated
| AttemptStatus::AuthenticationPending
| AttemptStatus::Started => Self::Pending,
AttemptStatus::Voided | AttemptStatus::VoidedPostCharge => Self::Success,
}
}
}
#[derive(
Clone,
Copy,

View File

@@ -2,6 +2,7 @@ use common_enums::enums;
use common_utils::{
self,
errors::{CustomResult, ValidationError},
ext_traits::ValueExt,
id_type::{self, GenerateId},
pii,
types::{keymanager, MinorUnit},
@@ -77,6 +78,7 @@ impl From<api_models::relay::RelayData> for RelayData {
authorized_amount: relay_capture_request.authorized_amount,
amount_to_capture: relay_capture_request.amount_to_capture,
currency: relay_capture_request.currency,
capture_method: relay_capture_request.capture_method,
})
}
api_models::relay::RelayData::IncrementalAuthorization(
@@ -111,6 +113,7 @@ impl From<api_models::relay::RelayCaptureRequestData> for RelayCaptureData {
authorized_amount: relay.authorized_amount,
amount_to_capture: relay.amount_to_capture,
currency: relay.currency,
capture_method: relay.capture_method,
}
}
}
@@ -162,11 +165,23 @@ impl RelayUpdate {
),
) -> CustomResult<Self, ApiErrorResponse> {
match response {
Err(error) => Ok(Self::ErrorUpdate {
error_code: error.code,
error_message: error.reason.unwrap_or(error.message),
status: common_enums::RelayStatus::Failure,
}),
Err(error) => {
let relay_status = common_enums::RelayStatus::from(status);
match relay_status {
common_enums::RelayStatus::Failure => Ok(Self::ErrorUpdate {
error_code: error.code,
error_message: error.reason.unwrap_or(error.message),
status: relay_status,
}),
common_enums::RelayStatus::Created
| common_enums::RelayStatus::Pending
| common_enums::RelayStatus::Success => Ok(Self::StatusUpdate {
connector_reference_id: None,
status: relay_status,
}),
}
}
Ok(response) => match response {
router_response_types::PaymentsResponseData::TransactionResponse {
resource_id,
@@ -262,7 +277,7 @@ impl RelayUpdate {
..
} => Ok(Self::StatusUpdate {
connector_reference_id: resource_id.get_optional_response_id(),
status: common_enums::RelayStatus::from(status),
status: common_enums::RelayStatus::get_void_status(status),
}),
_ => Err(ApiErrorResponse::InternalServerError)
.attach_printable("Payment Response Not Supported"),
@@ -286,6 +301,7 @@ impl From<RelayData> for api_models::relay::RelayData {
authorized_amount: relay_capture_request.authorized_amount,
amount_to_capture: relay_capture_request.amount_to_capture,
currency: relay_capture_request.currency,
capture_method: relay_capture_request.capture_method,
})
}
RelayData::IncrementalAuthorization(relay_incremental_authorization_request) => {
@@ -334,6 +350,7 @@ impl From<Relay> for api_models::relay::RelayResponse {
authorized_amount: relay_capture_request.authorized_amount,
amount_to_capture: relay_capture_request.amount_to_capture,
currency: relay_capture_request.currency,
capture_method: relay_capture_request.capture_method,
})
}
RelayData::IncrementalAuthorization(relay_incremental_authorization_request) => {
@@ -378,6 +395,31 @@ pub enum RelayData {
}
impl RelayData {
pub fn parse_relay_data(
value: Option<pii::SecretSerdeValue>,
relay_type: enums::RelayType,
) -> CustomResult<Option<Self>, ValidationError> {
match value {
Some(data) => match relay_type {
enums::RelayType::Capture => Ok(Some(Self::Capture(RelayCaptureData::from_value(
data.expose(),
)?))),
enums::RelayType::Refund => Ok(Some(Self::Refund(RelayRefundData::from_value(
data.expose(),
)?))),
enums::RelayType::IncrementalAuthorization => {
Ok(Some(Self::IncrementalAuthorization(
RelayIncrementalAuthorizationData::from_value(data.expose())?,
)))
}
enums::RelayType::Void => {
Ok(Some(Self::Void(RelayVoidData::from_value(data.expose())?)))
}
},
None => Ok(None),
}
}
pub fn get_refund_data(&self) -> CustomResult<RelayRefundData, ApiErrorResponse> {
match self.clone() {
Self::Refund(refund_data) => Ok(refund_data),
@@ -415,10 +457,10 @@ impl RelayData {
pub fn get_void_data(&self) -> CustomResult<RelayVoidData, ApiErrorResponse> {
match self.clone() {
Self::Void(void_data) => Ok(void_data),
Self::Refund(_) | Self::Capture(_) | Self::IncrementalAuthorization(_) => Err(
ApiErrorResponse::InternalServerError,
)
.attach_printable("relay data does not contain relay incremental authorization data"),
Self::Refund(_) | Self::Capture(_) | Self::IncrementalAuthorization(_) => {
Err(ApiErrorResponse::InternalServerError)
.attach_printable("relay data does not contain relay void data")
}
}
}
}
@@ -430,11 +472,32 @@ pub struct RelayRefundData {
pub reason: Option<String>,
}
impl RelayRefundData {
pub fn from_value(value: serde_json::Value) -> CustomResult<Self, ValidationError> {
value
.parse_value("RelayRefundData")
.change_context(ValidationError::InvalidValue {
message: "Failed while deserializing RelayRefundData".to_string(),
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RelayCaptureData {
pub authorized_amount: MinorUnit,
pub amount_to_capture: MinorUnit,
pub currency: enums::Currency,
pub capture_method: Option<enums::CaptureMethod>,
}
impl RelayCaptureData {
pub fn from_value(value: serde_json::Value) -> CustomResult<Self, ValidationError> {
value
.parse_value("RelayCaptureData")
.change_context(ValidationError::InvalidValue {
message: "Failed while deserializing RelayCaptureData".to_string(),
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@@ -444,6 +507,16 @@ pub struct RelayIncrementalAuthorizationData {
pub currency: enums::Currency,
}
impl RelayIncrementalAuthorizationData {
pub fn from_value(value: serde_json::Value) -> CustomResult<Self, ValidationError> {
value
.parse_value("RelayIncrementalAuthorizationData")
.change_context(ValidationError::InvalidValue {
message: "Failed while deserializing RelayIncrementalAuthorizationData".to_string(),
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RelayVoidData {
pub amount: Option<MinorUnit>,
@@ -451,6 +524,16 @@ pub struct RelayVoidData {
pub cancellation_reason: Option<String>,
}
impl RelayVoidData {
pub fn from_value(value: serde_json::Value) -> CustomResult<Self, ValidationError> {
value
.parse_value("RelayVoidData")
.change_context(ValidationError::InvalidValue {
message: "Failed while deserializing RelayVoidData".to_string(),
})
}
}
#[derive(Debug)]
pub enum RelayUpdate {
ErrorUpdate {
@@ -537,16 +620,7 @@ impl super::behaviour::Conversion for Relay {
profile_id: item.profile_id,
merchant_id: item.merchant_id,
relay_type: item.relay_type,
request_data: item
.request_data
.map(|data| {
serde_json::from_value(data.expose()).change_context(
ValidationError::InvalidValue {
message: "Failed while decrypting business profile data".to_string(),
},
)
})
.transpose()?,
request_data: RelayData::parse_relay_data(item.request_data, item.relay_type)?,
status: item.status,
connector_reference_id: item.connector_reference_id,
error_code: item.error_code,

View File

@@ -12,9 +12,10 @@ use hyperswitch_domain_models::relay;
use super::errors::{self, ConnectorErrorExt, RouterResponse, RouterResult, StorageErrorExt};
use crate::{
connector::utils::RouterData,
core::payments,
routes::SessionState,
services,
services::{self, api::ConnectorValidation},
types::{
api::{self},
domain,
@@ -971,16 +972,34 @@ pub async fn sync_relay_capture_with_gateway(
)
.await?;
let router_data_res = services::execute_connector_processing_step(
state,
connector_integration,
&router_data,
payments::CallConnectorAction::Trigger,
None,
None,
)
.await
.to_payment_failed_response()?;
//validate_psync_reference_id if call_connector_action is trigger
let router_data_res = if connector_data
.connector
.validate_psync_reference_id(
&router_data.request,
router_data.is_three_ds(),
router_data.status,
router_data.connector_meta_data.clone(),
)
.is_err()
{
router_env::logger::warn!(
"validate_psync_reference_id failed, hence skipping call to connector"
);
router_data
} else {
services::execute_connector_processing_step(
state,
connector_integration,
&router_data,
payments::CallConnectorAction::Trigger,
None,
None,
)
.await
.to_payment_failed_response()?
};
let relay_response = relay::RelayUpdate::try_from_capture_response((
router_data_res.status,

View File

@@ -77,7 +77,7 @@ pub async fn construct_relay_refund_router_data<F>(
connector: connector_name.to_string(),
payment_id: IRRELEVANT_PAYMENT_INTENT_ID.to_string(),
attempt_id: IRRELEVANT_PAYMENT_ATTEMPT_ID.to_string(),
status: common_enums::AttemptStatus::Charged,
status: common_enums::AttemptStatus::Pending,
payment_method: common_enums::PaymentMethod::default(),
payment_method_type: None,
connector_auth_type,
@@ -213,7 +213,7 @@ pub async fn construct_relay_capture_router_data(
connector: connector_name.to_string(),
payment_id: IRRELEVANT_PAYMENT_INTENT_ID.to_string(),
attempt_id: IRRELEVANT_PAYMENT_ATTEMPT_ID.to_string(),
status: common_enums::AttemptStatus::Charged,
status: common_enums::AttemptStatus::Pending,
payment_method: common_enums::PaymentMethod::default(),
payment_method_type: None,
connector_auth_type,
@@ -230,7 +230,13 @@ pub async fn construct_relay_capture_router_data(
currency: relay_capture_data.currency,
connector_transaction_id: relay_record.connector_resource_id.clone(),
payment_amount: relay_capture_data.authorized_amount.get_amount_as_i64(),
multiple_capture_data: None,
multiple_capture_data: Some(
// for relay, each manual multiple capture is a separate entity i.e not related
hyperswitch_domain_models::router_request_types::MultipleCaptureRequestData {
capture_sequence: 1,
capture_reference: relay_id_string.clone(),
},
),
connector_meta: None,
browser_info: None,
metadata: None,
@@ -340,7 +346,7 @@ pub async fn construct_relay_incremental_authorization_router_data(
connector: connector_name.to_string(),
payment_id: IRRELEVANT_PAYMENT_INTENT_ID.to_string(),
attempt_id: IRRELEVANT_PAYMENT_ATTEMPT_ID.to_string(),
status: common_enums::AttemptStatus::Charged,
status: common_enums::AttemptStatus::Pending,
payment_method: common_enums::PaymentMethod::default(),
payment_method_type: None,
connector_auth_type,
@@ -466,7 +472,7 @@ pub async fn construct_relay_void_router_data(
connector: connector_name.to_string(),
payment_id: IRRELEVANT_PAYMENT_INTENT_ID.to_string(),
attempt_id: IRRELEVANT_PAYMENT_ATTEMPT_ID.to_string(),
status: common_enums::AttemptStatus::Charged,
status: common_enums::AttemptStatus::Pending,
payment_method: common_enums::PaymentMethod::default(),
payment_method_type: None,
connector_auth_type,
@@ -488,7 +494,7 @@ pub async fn construct_relay_void_router_data(
connector_meta: None,
browser_info: None,
metadata: None,
minor_amount: None,
minor_amount: relay_void_data.amount,
webhook_url,
capture_method: None,
split_payments: None,
@@ -609,7 +615,7 @@ pub async fn construct_relay_payments_retrieve_router_data(
connector: connector_name.to_string(),
payment_id: IRRELEVANT_PAYMENT_INTENT_ID.to_string(),
attempt_id: IRRELEVANT_PAYMENT_ATTEMPT_ID.to_string(),
status: common_enums::AttemptStatus::Charged,
status: common_enums::AttemptStatus::Pending,
payment_method: common_enums::PaymentMethod::default(),
payment_method_type: None,
connector_auth_type,