Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 16 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions api-reference-v2/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -8365,10 +8365,12 @@
"CAD",
"CDF",
"CHF",
"CLF",
"CLP",
"CNY",
"COP",
"CRC",
"CUC",
"CUP",
"CVE",
"CZK",
Expand Down Expand Up @@ -8464,6 +8466,7 @@
"SOS",
"SRD",
"SSP",
"STD",
"STN",
"SVC",
"SYP",
Expand Down Expand Up @@ -9969,6 +9972,12 @@
}
}
},
{
"type": "string",
"enum": [
"user_social_security_number"
]
},
{
"type": "string",
"enum": [
Expand Down
9 changes: 9 additions & 0 deletions api-reference/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -10493,10 +10493,12 @@
"CAD",
"CDF",
"CHF",
"CLF",
"CLP",
"CNY",
"COP",
"CRC",
"CUC",
"CUP",
"CVE",
"CZK",
Expand Down Expand Up @@ -10592,6 +10594,7 @@
"SOS",
"SRD",
"SSP",
"STD",
"STN",
"SVC",
"SYP",
Expand Down Expand Up @@ -12170,6 +12173,12 @@
}
}
},
{
"type": "string",
"enum": [
"user_social_security_number"
]
},
{
"type": "string",
"enum": [
Expand Down
106 changes: 105 additions & 1 deletion crates/hyperswitch_connectors/src/connectors/recurly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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};
Expand Down Expand Up @@ -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
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3740,7 +3740,6 @@ default_imp_for_revenue_recovery_record_back!(
connectors::Placetopay,
connectors::Rapyd,
connectors::Razorpay,
connectors::Recurly,
connectors::Redsys,
connectors::Shift4,
connectors::Stax,
Expand Down
Loading