feat(connector): [Recurly] Add record back support for recurly [V2] (#7544)

Co-authored-by: Aniket Burman <aniket.burman@192.168.1.4>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Aniket Burman <aniket.burman@192.168.1.5>
This commit is contained in:
Aniket Burman
2025-03-24 16:57:43 +05:30
committed by GitHub
parent 7010799087
commit 2b70c94540
3 changed files with 197 additions and 3 deletions

View File

@ -24,6 +24,13 @@ use hyperswitch_domain_models::{
RefundSyncRouterData, RefundsRouterData,
},
};
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
use hyperswitch_domain_models::{
router_flow_types::RecoveryRecordBack,
router_request_types::revenue_recovery::RevenueRecoveryRecordBackRequest,
router_response_types::revenue_recovery::RevenueRecoveryRecordBackResponse,
types::RevenueRecoveryRecordBackRouterData,
};
use hyperswitch_interfaces::{
api::{
self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorSpecifications,
@ -38,10 +45,16 @@ use hyperswitch_interfaces::{
use masking::{ExposeInterface, Mask};
use transformers as recurly;
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
use crate::connectors::recurly::transformers::RecurlyRecordStatus;
use crate::{
connectors::recurly::transformers::RecurlyWebhookBody, constants::headers,
types::ResponseRouterData, utils,
};
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
const STATUS_SUCCESSFUL_ENDPOINT: &str = "mark_successful";
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
const STATUS_FAILED_ENDPOINT: &str = "mark_failed";
#[derive(Clone)]
pub struct Recurly {
@ -85,7 +98,8 @@ impl api::Refund for Recurly {}
impl api::RefundExecute for Recurly {}
impl api::RefundSync for Recurly {}
impl api::PaymentToken for Recurly {}
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
impl api::revenue_recovery::RevenueRecoveryRecordBack for Recurly {}
impl ConnectorIntegration<PaymentMethodToken, PaymentMethodTokenizationData, PaymentsResponseData>
for Recurly
{
@ -561,6 +575,96 @@ impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Recurly {
self.build_error_response(res, event_builder)
}
}
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
impl
ConnectorIntegration<
RecoveryRecordBack,
RevenueRecoveryRecordBackRequest,
RevenueRecoveryRecordBackResponse,
> for Recurly
{
fn get_headers(
&self,
req: &RevenueRecoveryRecordBackRouterData,
connectors: &Connectors,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_url(
&self,
req: &RevenueRecoveryRecordBackRouterData,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let invoice_id = req
.request
.merchant_reference_id
.get_string_repr()
.to_string();
let status = RecurlyRecordStatus::try_from(req.request.attempt_status)?;
let status_endpoint = match status {
RecurlyRecordStatus::Success => STATUS_SUCCESSFUL_ENDPOINT,
RecurlyRecordStatus::Failure => STATUS_FAILED_ENDPOINT,
};
Ok(format!(
"{}/invoices/{invoice_id}/{status_endpoint}",
self.base_url(connectors)
))
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn build_request(
&self,
req: &RevenueRecoveryRecordBackRouterData,
connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Ok(Some(
RequestBuilder::new()
.method(Method::Put)
.url(&types::RevenueRecoveryRecordBackType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::RevenueRecoveryRecordBackType::get_headers(
self, req, connectors,
)?)
.header("Content-Length", "0")
.build(),
))
}
fn handle_response(
&self,
data: &RevenueRecoveryRecordBackRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<RevenueRecoveryRecordBackRouterData, errors::ConnectorError> {
let response: recurly::RecurlyRecordbackResponse = res
.response
.parse_struct("recurly RecurlyRecordbackResponse")
.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)
}
}
#[async_trait::async_trait]
impl webhooks::IncomingWebhook for Recurly {

View File

@ -1,5 +1,7 @@
use common_enums::enums;
use common_utils::{errors::CustomResult, ext_traits::ByteSliceExt, types::StringMinorUnit};
use common_utils::{
errors::CustomResult, ext_traits::ByteSliceExt, id_type, types::StringMinorUnit,
};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
payment_method_data::PaymentMethodData,
@ -9,6 +11,13 @@ use hyperswitch_domain_models::{
router_response_types::{PaymentsResponseData, RefundsResponseData},
types::{PaymentsAuthorizeRouterData, RefundsRouterData},
};
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
use hyperswitch_domain_models::{
router_flow_types::RecoveryRecordBack,
router_request_types::revenue_recovery::RevenueRecoveryRecordBackRequest,
router_response_types::revenue_recovery::RevenueRecoveryRecordBackResponse,
types::RevenueRecoveryRecordBackRouterData,
};
use hyperswitch_interfaces::errors;
use masking::Secret;
use serde::{Deserialize, Serialize};
@ -251,3 +260,85 @@ impl RecurlyWebhookBody {
Ok(webhook_body)
}
}
#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum RecurlyRecordStatus {
Success,
Failure,
}
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
impl TryFrom<enums::AttemptStatus> for RecurlyRecordStatus {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(status: enums::AttemptStatus) -> Result<Self, Self::Error> {
match status {
enums::AttemptStatus::Charged
| enums::AttemptStatus::PartialCharged
| enums::AttemptStatus::PartialChargedAndChargeable => Ok(Self::Success),
enums::AttemptStatus::Failure
| enums::AttemptStatus::CaptureFailed
| enums::AttemptStatus::RouterDeclined => Ok(Self::Failure),
enums::AttemptStatus::AuthenticationFailed
| enums::AttemptStatus::Started
| enums::AttemptStatus::AuthenticationPending
| enums::AttemptStatus::AuthenticationSuccessful
| enums::AttemptStatus::Authorized
| enums::AttemptStatus::AuthorizationFailed
| enums::AttemptStatus::Authorizing
| enums::AttemptStatus::CodInitiated
| enums::AttemptStatus::Voided
| enums::AttemptStatus::VoidInitiated
| enums::AttemptStatus::CaptureInitiated
| enums::AttemptStatus::VoidFailed
| enums::AttemptStatus::AutoRefunded
| enums::AttemptStatus::Unresolved
| enums::AttemptStatus::Pending
| enums::AttemptStatus::PaymentMethodAwaited
| enums::AttemptStatus::ConfirmationAwaited
| enums::AttemptStatus::DeviceDataCollectionPending => {
Err(errors::ConnectorError::NotSupported {
message: "Record back flow is only supported for terminal status".to_string(),
connector: "recurly",
}
.into())
}
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RecurlyRecordbackResponse {
// inovice id
pub id: id_type::PaymentReferenceId,
}
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
impl
TryFrom<
ResponseRouterData<
RecoveryRecordBack,
RecurlyRecordbackResponse,
RevenueRecoveryRecordBackRequest,
RevenueRecoveryRecordBackResponse,
>,
> for RevenueRecoveryRecordBackRouterData
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: ResponseRouterData<
RecoveryRecordBack,
RecurlyRecordbackResponse,
RevenueRecoveryRecordBackRequest,
RevenueRecoveryRecordBackResponse,
>,
) -> Result<Self, Self::Error> {
let merchant_reference_id = item.response.id;
Ok(Self {
response: Ok(RevenueRecoveryRecordBackResponse {
merchant_reference_id,
}),
..item.data
})
}
}

View File

@ -3738,7 +3738,6 @@ default_imp_for_revenue_recovery_record_back!(
connectors::Placetopay,
connectors::Rapyd,
connectors::Razorpay,
connectors::Recurly,
connectors::Redsys,
connectors::Shift4,
connectors::Stax,