diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 0ae089c5ea0..0d10ad5cbd9 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -8365,10 +8365,12 @@ "CAD", "CDF", "CHF", + "CLF", "CLP", "CNY", "COP", "CRC", + "CUC", "CUP", "CVE", "CZK", @@ -8464,6 +8466,7 @@ "SOS", "SRD", "SSP", + "STD", "STN", "SVC", "SYP", @@ -9969,6 +9972,12 @@ } } }, + { + "type": "string", + "enum": [ + "user_social_security_number" + ] + }, { "type": "string", "enum": [ diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index fa4ce7ae5eb..c4c18f6738f 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -10493,10 +10493,12 @@ "CAD", "CDF", "CHF", + "CLF", "CLP", "CNY", "COP", "CRC", + "CUC", "CUP", "CVE", "CZK", @@ -10592,6 +10594,7 @@ "SOS", "SRD", "SSP", + "STD", "STN", "SVC", "SYP", @@ -12170,6 +12173,12 @@ } } }, + { + "type": "string", + "enum": [ + "user_social_security_number" + ] + }, { "type": "string", "enum": [ diff --git a/crates/hyperswitch_connectors/src/connectors/recurly.rs b/crates/hyperswitch_connectors/src/connectors/recurly.rs index 450a5ed3097..e094e24e23a 100644 --- a/crates/hyperswitch_connectors/src/connectors/recurly.rs +++ b/crates/hyperswitch_connectors/src/connectors/recurly.rs @@ -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 for Recurly { @@ -561,6 +575,96 @@ impl ConnectorIntegration 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)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + fn get_url( + &self, + req: &RevenueRecoveryRecordBackRouterData, + connectors: &Connectors, + ) -> CustomResult { + 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, 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 { + 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 { + self.build_error_response(res, event_builder) + } +} #[async_trait::async_trait] impl webhooks::IncomingWebhook for Recurly { diff --git a/crates/hyperswitch_connectors/src/connectors/recurly/transformers.rs b/crates/hyperswitch_connectors/src/connectors/recurly/transformers.rs index 8b7c9b65c69..23a0daa45b4 100644 --- a/crates/hyperswitch_connectors/src/connectors/recurly/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/recurly/transformers.rs @@ -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 for RecurlyRecordStatus { + type Error = error_stack::Report; + fn try_from(status: enums::AttemptStatus) -> Result { + 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; + fn try_from( + item: ResponseRouterData< + RecoveryRecordBack, + RecurlyRecordbackResponse, + RevenueRecoveryRecordBackRequest, + RevenueRecoveryRecordBackResponse, + >, + ) -> Result { + let merchant_reference_id = item.response.id; + Ok(Self { + response: Ok(RevenueRecoveryRecordBackResponse { + merchant_reference_id, + }), + ..item.data + }) + } +} diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index e2faeaa1a30..3e7bfc79161 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -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,