diff --git a/config/config.example.toml b/config/config.example.toml index 5743d74052..023298e957 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -992,3 +992,7 @@ billing_connectors_which_require_payment_sync = "stripebilling, recurly" # List [open_router] enabled = true # Enable or disable Open Router url = "http://localhost:8080" # Open Router URL + + +[billing_connectors_invoice_sync] +billing_connectors_which_requires_invoice_sync_call = "recurly" # List of billing connectors which has invoice sync api call \ No newline at end of file diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index f65f1dba78..95a3362054 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -637,3 +637,6 @@ enabled = true [billing_connectors_payment_sync] billing_connectors_which_require_payment_sync = "stripebilling, recurly" + +[billing_connectors_invoice_sync] +billing_connectors_which_requires_invoice_sync_call = "recurly" \ No newline at end of file diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 846e27085c..67cdb439c1 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -653,5 +653,8 @@ enabled = false [billing_connectors_payment_sync] billing_connectors_which_require_payment_sync = "stripebilling, recurly" +[billing_connectors_invoice_sync] +billing_connectors_which_requires_invoice_sync_call = "recurly" + [authentication_providers] click_to_pay = {connector_list = "adyen, cybersource"} \ No newline at end of file diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 15efb264ac..26b10f0685 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -653,5 +653,8 @@ enabled = false [billing_connectors_payment_sync] billing_connectors_which_require_payment_sync = "stripebilling, recurly" +[billing_connectors_invoice_sync] +billing_connectors_which_requires_invoice_sync_call = "recurly" + [authentication_providers] click_to_pay = {connector_list = "adyen, cybersource"} \ No newline at end of file diff --git a/config/development.toml b/config/development.toml index a059466e43..7618acce81 100644 --- a/config/development.toml +++ b/config/development.toml @@ -817,6 +817,9 @@ connectors_with_webhook_source_verification_call = "paypal" [billing_connectors_payment_sync] billing_connectors_which_require_payment_sync = "stripebilling, recurly" +[billing_connectors_invoice_sync] +billing_connectors_which_requires_invoice_sync_call = "recurly" + [zero_mandates.supported_payment_methods] bank_debit.ach = { connector_list = "gocardless,adyen" } bank_debit.becs = { connector_list = "gocardless,adyen" } @@ -841,6 +844,7 @@ bank_redirect.bancontact_card.connector_list = "adyen" bank_redirect.trustly.connector_list = "adyen" bank_redirect.open_banking_uk.connector_list = "adyen" + [mandates.supported_payment_methods] bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index caf617ef71..00f5498127 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -329,6 +329,9 @@ connectors_with_webhook_source_verification_call = "paypal" [billing_connectors_payment_sync] billing_connectors_which_require_payment_sync = "stripebilling, recurly" +[billing_connectors_invoice_sync] +billing_connectors_which_requires_invoice_sync_call = "recurly" + [scheduler] stream = "SCHEDULER_STREAM" diff --git a/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs b/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs index ff3b6b422c..9d7a5d364e 100644 --- a/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs @@ -266,6 +266,7 @@ pub struct ChargebeeInvoiceBody { #[derive(Serialize, Deserialize, Debug)] pub struct ChargebeeInvoiceContent { pub invoice: ChargebeeInvoiceData, + pub subscription: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -555,11 +556,27 @@ impl TryFrom for revenue_recovery::RevenueRecoveryInvoiceD let merchant_reference_id = common_utils::id_type::PaymentReferenceId::from_str(&item.content.invoice.id) .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + // The retry count will never exceed u16 limit in a billing connector. It can have maximum of 12 in case of charge bee so its ok to suppress this + #[allow(clippy::as_conversions)] + let retry_count = item + .content + .invoice + .linked_payments + .as_ref() + .map(|linked_payments| linked_payments.len() as u16); + let invoice_next_billing_time = item + .content + .subscription + .as_ref() + .and_then(|subscription| subscription.next_billing_at); Ok(Self { amount: item.content.invoice.total, currency: item.content.invoice.currency_code, merchant_reference_id, billing_address: Some(api_models::payments::Address::from(item.content.invoice)), + retry_count, + next_billing_at: invoice_next_billing_time, }) } } diff --git a/crates/hyperswitch_connectors/src/connectors/recurly.rs b/crates/hyperswitch_connectors/src/connectors/recurly.rs index 18e0a357f4..02dea67d9b 100644 --- a/crates/hyperswitch_connectors/src/connectors/recurly.rs +++ b/crates/hyperswitch_connectors/src/connectors/recurly.rs @@ -1,58 +1,49 @@ pub mod transformers; use base64::Engine; -use common_utils::{ - consts, - errors::CustomResult, - ext_traits::BytesExt, - request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, -}; +#[cfg(all(feature = "v2", feature = "revenue_recovery"))] +use common_utils::request::{Method, Request, RequestBuilder, RequestContent}; +use common_utils::{consts, errors::CustomResult, ext_traits::BytesExt}; use error_stack::{report, ResultExt}; #[cfg(all(feature = "v2", feature = "revenue_recovery"))] use hyperswitch_domain_models::{ - revenue_recovery, router_flow_types::revenue_recovery as recovery_router_flows, + revenue_recovery, router_data_v2::flow_common_types as recovery_flow_common_types, + router_flow_types::revenue_recovery as recovery_router_flows, router_request_types::revenue_recovery as recovery_request_types, router_response_types::revenue_recovery as recovery_response_types, types as recovery_router_data_types, }; use hyperswitch_domain_models::{ - router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, - router_flow_types::{ - access_token_auth::AccessTokenAuth, - payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, - refunds::{Execute, RSync}, + router_data::{ConnectorAuthType, ErrorResponse}, + router_data_v2::UasFlowData, + router_flow_types::unified_authentication_service::{ + Authenticate, AuthenticationConfirmation, PostAuthenticate, PreAuthenticate, }, - router_request_types::{ - AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, - PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, - RefundsData, SetupMandateRequestData, - }, - router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + router_request_types::unified_authentication_service::{ + UasAuthenticationRequestData, UasAuthenticationResponseData, UasConfirmationRequestData, + UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, }; +#[cfg(all(feature = "v2", feature = "revenue_recovery"))] +use hyperswitch_interfaces::types; use hyperswitch_interfaces::{ - api::{ - self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorSpecifications, - ConnectorValidation, - }, + api::{self, ConnectorCommon, ConnectorSpecifications, ConnectorValidation}, configs::Connectors, + connector_integration_v2::ConnectorIntegrationV2, errors, events::connector_api_logs::ConnectorEvent, - types::{self, Response}, + types::Response, webhooks, }; use masking::{Mask, PeekInterface}; use transformers as recurly; +use crate::{connectors::recurly::transformers::RecurlyWebhookBody, constants::headers}; #[cfg(all(feature = "v2", feature = "revenue_recovery"))] -use crate::connectors::recurly::transformers::{RecurlyRecordStatus, RecurlyRecoveryDetailsData}; use crate::{ - connectors::recurly::transformers::RecurlyWebhookBody, constants::headers, - types::ResponseRouterData, utils, + connectors::recurly::transformers::{RecurlyRecordStatus, RecurlyRecoveryDetailsData}, + types::ResponseRouterDataV2, + utils, }; #[cfg(all(feature = "v2", feature = "revenue_recovery"))] const STATUS_SUCCESSFUL_ENDPOINT: &str = "mark_successful"; @@ -61,16 +52,15 @@ const STATUS_FAILED_ENDPOINT: &str = "mark_failed"; const RECURLY_API_VERSION: &str = "application/vnd.recurly.v2021-02-25"; +// We don't need an amount converter beacuse we are not using it anywhere in code, but it's important to note that Float Major Unit is the standard format used by Recurly. #[derive(Clone)] pub struct Recurly { - amount_converter: &'static (dyn AmountConvertor + Sync), + // amount_converter: &'static (dyn AmountConvertor + Sync), } impl Recurly { pub fn new() -> &'static Self { - &Self { - amount_converter: &StringMinorUnitForConnector, - } + &Self {} } fn get_signature_elements_from_header( @@ -91,47 +81,61 @@ impl Recurly { } } -impl api::Payment for Recurly {} -impl api::PaymentSession for Recurly {} -impl api::ConnectorAccessToken for Recurly {} -impl api::MandateSetup for Recurly {} -impl api::PaymentAuthorize for Recurly {} -impl api::PaymentSync for Recurly {} -impl api::PaymentCapture for Recurly {} -impl api::PaymentVoid for Recurly {} -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 {} -#[cfg(all(feature = "v2", feature = "revenue_recovery"))] -impl api::revenue_recovery::BillingConnectorPaymentsSyncIntegration for Recurly {} - -impl ConnectorIntegration - for Recurly +impl api::PayoutsV2 for Recurly {} +impl api::UnifiedAuthenticationServiceV2 for Recurly {} +impl api::UasPreAuthenticationV2 for Recurly {} +impl api::UasPostAuthenticationV2 for Recurly {} +impl api::UasAuthenticationV2 for Recurly {} +impl api::UasAuthenticationConfirmationV2 for Recurly {} +impl + ConnectorIntegrationV2< + PreAuthenticate, + UasFlowData, + UasPreAuthenticationRequestData, + UasAuthenticationResponseData, + > for Recurly { - // Not Implemented (R) + //TODO: implement sessions flow } -impl ConnectorCommonExt for Recurly -where - Self: ConnectorIntegration, +impl + ConnectorIntegrationV2< + PostAuthenticate, + UasFlowData, + UasPostAuthenticationRequestData, + UasAuthenticationResponseData, + > for Recurly { - fn build_headers( - &self, - req: &RouterData, - _connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - self.get_content_type().to_string().into(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) - } + //TODO: implement sessions flow } +impl + ConnectorIntegrationV2< + AuthenticationConfirmation, + UasFlowData, + UasConfirmationRequestData, + UasAuthenticationResponseData, + > for Recurly +{ + //TODO: implement sessions flow +} +impl + ConnectorIntegrationV2< + Authenticate, + UasFlowData, + UasAuthenticationRequestData, + UasAuthenticationResponseData, + > for Recurly +{ + //TODO: implement sessions flow +} + +impl api::revenue_recovery_v2::RevenueRecoveryV2 for Recurly {} +#[cfg(all(feature = "v2", feature = "revenue_recovery"))] +impl api::revenue_recovery_v2::RevenueRecoveryRecordBackV2 for Recurly {} +#[cfg(all(feature = "v2", feature = "revenue_recovery"))] +impl api::revenue_recovery_v2::BillingConnectorPaymentsSyncIntegrationV2 for Recurly {} +#[cfg(all(feature = "v2", feature = "revenue_recovery"))] +impl api::revenue_recovery_v2::BillingConnectorInvoiceSyncIntegrationV2 for Recurly {} impl ConnectorCommon for Recurly { fn id(&self) -> &'static str { @@ -206,452 +210,63 @@ impl ConnectorValidation for Recurly { //TODO: implement functions when support enabled } -impl ConnectorIntegration for Recurly { - //TODO: implement sessions flow -} - -impl ConnectorIntegration for Recurly {} - -impl ConnectorIntegration for Recurly {} - -impl ConnectorIntegration for Recurly { - fn get_headers( - &self, - req: &PaymentsAuthorizeRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &PaymentsAuthorizeRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn get_request_body( - &self, - req: &PaymentsAuthorizeRouterData, - _connectors: &Connectors, - ) -> CustomResult { - let amount = utils::convert_amount( - self.amount_converter, - req.request.minor_amount, - req.request.currency, - )?; - - let connector_router_data = recurly::RecurlyRouterData::from((amount, req)); - let connector_req = recurly::RecurlyPaymentsRequest::try_from(&connector_router_data)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - - fn build_request( - &self, - req: &PaymentsAuthorizeRouterData, - connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - RequestBuilder::new() - .method(Method::Post) - .url(&types::PaymentsAuthorizeType::get_url( - self, req, connectors, - )?) - .attach_default_headers() - .headers(types::PaymentsAuthorizeType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsAuthorizeType::get_request_body( - self, req, connectors, - )?) - .build(), - )) - } - - fn handle_response( - &self, - data: &PaymentsAuthorizeRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { - let response: recurly::RecurlyPaymentsResponse = res - .response - .parse_struct("Recurly PaymentsAuthorizeResponse") - .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) - } -} - -impl ConnectorIntegration for Recurly { - fn get_headers( - &self, - req: &PaymentsSyncRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn build_request( - &self, - req: &PaymentsSyncRouterData, - connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - RequestBuilder::new() - .method(Method::Get) - .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .build(), - )) - } - - fn handle_response( - &self, - data: &PaymentsSyncRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { - let response: recurly::RecurlyPaymentsResponse = res - .response - .parse_struct("recurly PaymentsSyncResponse") - .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) - } -} - -impl ConnectorIntegration for Recurly { - fn get_headers( - &self, - req: &PaymentsCaptureRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn get_request_body( - &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) - } - - fn build_request( - &self, - req: &PaymentsCaptureRouterData, - connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - RequestBuilder::new() - .method(Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCaptureType::get_request_body( - self, req, connectors, - )?) - .build(), - )) - } - - fn handle_response( - &self, - data: &PaymentsCaptureRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { - let response: recurly::RecurlyPaymentsResponse = res - .response - .parse_struct("Recurly PaymentsCaptureResponse") - .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) - } -} - -impl ConnectorIntegration for Recurly {} - -impl ConnectorIntegration for Recurly { - fn get_headers( - &self, - req: &RefundsRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &RefundsRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn get_request_body( - &self, - req: &RefundsRouterData, - _connectors: &Connectors, - ) -> CustomResult { - let refund_amount = utils::convert_amount( - self.amount_converter, - req.request.minor_refund_amount, - req.request.currency, - )?; - - let connector_router_data = recurly::RecurlyRouterData::from((refund_amount, req)); - let connector_req = recurly::RecurlyRefundRequest::try_from(&connector_router_data)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - - fn build_request( - &self, - req: &RefundsRouterData, - connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - let request = RequestBuilder::new() - .method(Method::Post) - .url(&types::RefundExecuteType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundExecuteType::get_headers( - self, req, connectors, - )?) - .set_body(types::RefundExecuteType::get_request_body( - self, req, connectors, - )?) - .build(); - Ok(Some(request)) - } - - fn handle_response( - &self, - data: &RefundsRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult, errors::ConnectorError> { - let response: recurly::RefundResponse = res - .response - .parse_struct("recurly RefundResponse") - .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) - } -} - -impl ConnectorIntegration for Recurly { - fn get_headers( - &self, - req: &RefundSyncRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn build_request( - &self, - req: &RefundSyncRouterData, - connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - RequestBuilder::new() - .method(Method::Get) - .url(&types::RefundSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .set_body(types::RefundSyncType::get_request_body( - self, req, connectors, - )?) - .build(), - )) - } - - fn handle_response( - &self, - data: &RefundSyncRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { - let response: recurly::RefundResponse = res - .response - .parse_struct("recurly RefundSyncResponse") - .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) - } -} - #[cfg(all(feature = "v2", feature = "revenue_recovery"))] impl - ConnectorIntegration< + ConnectorIntegrationV2< recovery_router_flows::BillingConnectorPaymentsSync, + recovery_flow_common_types::BillingConnectorPaymentsSyncFlowData, recovery_request_types::BillingConnectorPaymentsSyncRequest, recovery_response_types::BillingConnectorPaymentsSyncResponse, > for Recurly { fn get_headers( &self, - req: &recovery_router_data_types::BillingConnectorPaymentsSyncRouterData, - connectors: &Connectors, + req: &recovery_router_data_types::BillingConnectorPaymentsSyncRouterDataV2, ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.common_get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) } fn get_url( &self, - req: &recovery_router_data_types::BillingConnectorPaymentsSyncRouterData, - connectors: &Connectors, + req: &recovery_router_data_types::BillingConnectorPaymentsSyncRouterDataV2, ) -> CustomResult { let transaction_uuid = &req.request.billing_connector_psync_id; Ok(format!( "{}/transactions/uuid-{transaction_uuid}", - self.base_url(connectors), + req.request.connector_params.base_url, )) } - fn build_request( + fn build_request_v2( &self, - req: &recovery_router_data_types::BillingConnectorPaymentsSyncRouterData, - connectors: &Connectors, + req: &recovery_router_data_types::BillingConnectorPaymentsSyncRouterDataV2, ) -> CustomResult, errors::ConnectorError> { let request = RequestBuilder::new() .method(Method::Get) - .url(&types::BillingConnectorPaymentsSyncType::get_url( - self, req, connectors, + .url(&types::BillingConnectorPaymentsSyncTypeV2::get_url( + self, req, )?) .attach_default_headers() - .headers(types::BillingConnectorPaymentsSyncType::get_headers( - self, req, connectors, + .headers(types::BillingConnectorPaymentsSyncTypeV2::get_headers( + self, req, )?) .build(); Ok(Some(request)) } - fn handle_response( + fn handle_response_v2( &self, - data: &recovery_router_data_types::BillingConnectorPaymentsSyncRouterData, + data: &recovery_router_data_types::BillingConnectorPaymentsSyncRouterDataV2, event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult< - recovery_router_data_types::BillingConnectorPaymentsSyncRouterData, + recovery_router_data_types::BillingConnectorPaymentsSyncRouterDataV2, errors::ConnectorError, > { let response: RecurlyRecoveryDetailsData = res @@ -662,8 +277,8 @@ impl event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); - recovery_router_data_types::BillingConnectorPaymentsSyncRouterData::try_from( - ResponseRouterData { + recovery_router_data_types::BillingConnectorPaymentsSyncRouterDataV2::try_from( + ResponseRouterDataV2 { response, data: data.clone(), http_code: res.status_code, @@ -671,7 +286,7 @@ impl ) } - fn get_error_response( + fn get_error_response_v2( &self, res: Response, event_builder: Option<&mut ConnectorEvent>, @@ -682,23 +297,29 @@ impl #[cfg(all(feature = "v2", feature = "revenue_recovery"))] impl - ConnectorIntegration< + ConnectorIntegrationV2< recovery_router_flows::RecoveryRecordBack, + recovery_flow_common_types::RevenueRecoveryRecordBackData, recovery_request_types::RevenueRecoveryRecordBackRequest, recovery_response_types::RevenueRecoveryRecordBackResponse, > for Recurly { fn get_headers( &self, - req: &recovery_router_data_types::RevenueRecoveryRecordBackRouterData, - connectors: &Connectors, + req: &recovery_router_data_types::RevenueRecoveryRecordBackRouterDataV2, ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.common_get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) } + fn get_url( &self, - req: &recovery_router_data_types::RevenueRecoveryRecordBackRouterData, - connectors: &Connectors, + req: &recovery_router_data_types::RevenueRecoveryRecordBackRouterDataV2, ) -> CustomResult { let invoice_id = req .request @@ -715,41 +336,34 @@ impl Ok(format!( "{}/invoices/{invoice_id}/{status_endpoint}", - self.base_url(connectors) + req.request.connector_params.base_url, )) } - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn build_request( + fn build_request_v2( &self, - req: &recovery_router_data_types::RevenueRecoveryRecordBackRouterData, - connectors: &Connectors, + req: &recovery_router_data_types::RevenueRecoveryRecordBackRouterDataV2, ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() .method(Method::Put) - .url(&types::RevenueRecoveryRecordBackType::get_url( - self, req, connectors, - )?) + .url(&types::RevenueRecoveryRecordBackTypeV2::get_url(self, req)?) .attach_default_headers() - .headers(types::RevenueRecoveryRecordBackType::get_headers( - self, req, connectors, + .headers(types::RevenueRecoveryRecordBackTypeV2::get_headers( + self, req, )?) .header("Content-Length", "0") .build(), )) } - fn handle_response( + fn handle_response_v2( &self, - data: &recovery_router_data_types::RevenueRecoveryRecordBackRouterData, + data: &recovery_router_data_types::RevenueRecoveryRecordBackRouterDataV2, event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult< - recovery_router_data_types::RevenueRecoveryRecordBackRouterData, + recovery_router_data_types::RevenueRecoveryRecordBackRouterDataV2, errors::ConnectorError, > { let response: recurly::RecurlyRecordBackResponse = res @@ -758,14 +372,101 @@ impl .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, - }) + recovery_router_data_types::RevenueRecoveryRecordBackRouterDataV2::try_from( + ResponseRouterDataV2 { + response, + data: data.clone(), + http_code: res.status_code, + }, + ) } - fn get_error_response( + fn get_error_response_v2( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +#[cfg(all(feature = "v2", feature = "revenue_recovery"))] +impl + ConnectorIntegrationV2< + recovery_router_flows::BillingConnectorInvoiceSync, + recovery_flow_common_types::BillingConnectorInvoiceSyncFlowData, + recovery_request_types::BillingConnectorInvoiceSyncRequest, + recovery_response_types::BillingConnectorInvoiceSyncResponse, + > for Recurly +{ + fn get_headers( + &self, + req: &recovery_router_data_types::BillingConnectorInvoiceSyncRouterDataV2, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.common_get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } + + fn get_url( + &self, + req: &recovery_router_data_types::BillingConnectorInvoiceSyncRouterDataV2, + ) -> CustomResult { + let invoice_id = &req.request.billing_connector_invoice_id; + Ok(format!( + "{}/invoices/{invoice_id}", + req.request.connector_params.base_url, + )) + } + + fn build_request_v2( + &self, + req: &recovery_router_data_types::BillingConnectorInvoiceSyncRouterDataV2, + ) -> CustomResult, errors::ConnectorError> { + let request = RequestBuilder::new() + .method(Method::Get) + .url(&types::BillingConnectorInvoiceSyncTypeV2::get_url( + self, req, + )?) + .attach_default_headers() + .headers(types::BillingConnectorInvoiceSyncTypeV2::get_headers( + self, req, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response_v2( + &self, + data: &recovery_router_data_types::BillingConnectorInvoiceSyncRouterDataV2, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult< + recovery_router_data_types::BillingConnectorInvoiceSyncRouterDataV2, + errors::ConnectorError, + > { + let response: recurly::RecurlyInvoiceSyncResponse = res + .response + .parse_struct::("RecurlyInvoiceSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + recovery_router_data_types::BillingConnectorInvoiceSyncRouterDataV2::try_from( + ResponseRouterDataV2 { + response, + data: data.clone(), + http_code: res.status_code, + }, + ) + } + + fn get_error_response_v2( &self, res: Response, event_builder: Option<&mut ConnectorEvent>, diff --git a/crates/hyperswitch_connectors/src/connectors/recurly/transformers.rs b/crates/hyperswitch_connectors/src/connectors/recurly/transformers.rs index 6bcbdb74a2..0f674948ea 100644 --- a/crates/hyperswitch_connectors/src/connectors/recurly/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/recurly/transformers.rs @@ -11,16 +11,10 @@ use common_utils::{ types::{FloatMajorUnit, StringMinorUnit}, }; use error_stack::ResultExt; -use hyperswitch_domain_models::{ - payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, - router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, - router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, -}; +use hyperswitch_domain_models::router_data::ConnectorAuthType; #[cfg(all(feature = "v2", feature = "revenue_recovery"))] use hyperswitch_domain_models::{ + router_data_v2::flow_common_types as recovery_flow_common_types, router_flow_types::revenue_recovery as recovery_router_flows, router_request_types::revenue_recovery as recovery_request_types, router_response_types::revenue_recovery as recovery_response_types, @@ -33,12 +27,9 @@ use time::PrimitiveDateTime; #[cfg(all(feature = "v2", feature = "revenue_recovery"))] use crate::utils; -use crate::{ - types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, -}; +#[cfg(all(feature = "v2", feature = "revenue_recovery"))] +use crate::{types::ResponseRouterDataV2, utils::PaymentsAuthorizeRequestData}; -//TODO: Fill the struct with respective fields pub struct RecurlyRouterData { pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. pub router_data: T, @@ -54,47 +45,6 @@ impl From<(StringMinorUnit, T)> for RecurlyRouterData { } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] -pub struct RecurlyPaymentsRequest { - amount: StringMinorUnit, - card: RecurlyCard, -} - -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct RecurlyCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, -} - -impl TryFrom<&RecurlyRouterData<&PaymentsAuthorizeRouterData>> for RecurlyPaymentsRequest { - type Error = error_stack::Report; - fn try_from( - item: &RecurlyRouterData<&PaymentsAuthorizeRouterData>, - ) -> Result { - match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(req_card) => { - let card = RecurlyCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, - }; - Ok(Self { - amount: item.amount.clone(), - card, - }) - } - _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), - } - } -} - -//TODO: Fill the struct with respective fields // Auth Struct pub struct RecurlyAuthType { pub(super) api_key: Secret, @@ -111,133 +61,6 @@ impl TryFrom<&ConnectorAuthType> for RecurlyAuthType { } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Copy)] -#[serde(rename_all = "lowercase")] -pub enum RecurlyPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for common_enums::AttemptStatus { - fn from(item: RecurlyPaymentStatus) -> Self { - match item { - RecurlyPaymentStatus::Succeeded => Self::Charged, - RecurlyPaymentStatus::Failed => Self::Failure, - RecurlyPaymentStatus::Processing => Self::Authorizing, - } - } -} - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct RecurlyPaymentsResponse { - status: RecurlyPaymentStatus, - id: String, -} - -impl TryFrom> - for RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: ResponseRouterData, - ) -> Result { - Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), - response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: Box::new(None), - mandate_reference: Box::new(None), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charges: None, - }), - ..item.data - }) - } -} - -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] -pub struct RecurlyRefundRequest { - pub amount: StringMinorUnit, -} - -impl TryFrom<&RecurlyRouterData<&RefundsRouterData>> for RecurlyRefundRequest { - type Error = error_stack::Report; - fn try_from(item: &RecurlyRouterData<&RefundsRouterData>) -> Result { - Ok(Self { - amount: item.amount.to_owned(), - }) - } -} - -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone, Copy)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping - } - } -} - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, -} - -impl TryFrom> for RefundsRouterData { - type Error = error_stack::Report; - fn try_from( - item: RefundsResponseRouterData, - ) -> Result { - Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), - ..item.data - }) - } -} - -impl TryFrom> for RefundsRouterData { - type Error = error_stack::Report; - fn try_from( - item: RefundsResponseRouterData, - ) -> Result { - Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), - ..item.data - }) - } -} //TODO: Fill the struct with respective fields #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] @@ -333,19 +156,21 @@ pub struct PaymentGateway { #[cfg(all(feature = "v2", feature = "revenue_recovery"))] impl TryFrom< - ResponseRouterData< + ResponseRouterDataV2< recovery_router_flows::BillingConnectorPaymentsSync, RecurlyRecoveryDetailsData, + recovery_flow_common_types::BillingConnectorPaymentsSyncFlowData, recovery_request_types::BillingConnectorPaymentsSyncRequest, recovery_response_types::BillingConnectorPaymentsSyncResponse, >, - > for recovery_router_data_types::BillingConnectorPaymentsSyncRouterData + > for recovery_router_data_types::BillingConnectorPaymentsSyncRouterDataV2 { type Error = error_stack::Report; fn try_from( - item: ResponseRouterData< + item: ResponseRouterDataV2< recovery_router_flows::BillingConnectorPaymentsSync, RecurlyRecoveryDetailsData, + recovery_flow_common_types::BillingConnectorPaymentsSyncFlowData, recovery_request_types::BillingConnectorPaymentsSyncRequest, recovery_response_types::BillingConnectorPaymentsSyncResponse, >, @@ -468,19 +293,21 @@ pub struct RecurlyRecordBackResponse { #[cfg(all(feature = "v2", feature = "revenue_recovery"))] impl TryFrom< - ResponseRouterData< + ResponseRouterDataV2< recovery_router_flows::RecoveryRecordBack, RecurlyRecordBackResponse, + recovery_flow_common_types::RevenueRecoveryRecordBackData, recovery_request_types::RevenueRecoveryRecordBackRequest, recovery_response_types::RevenueRecoveryRecordBackResponse, >, - > for recovery_router_data_types::RevenueRecoveryRecordBackRouterData + > for recovery_router_data_types::RevenueRecoveryRecordBackRouterDataV2 { type Error = error_stack::Report; fn try_from( - item: ResponseRouterData< + item: ResponseRouterDataV2< recovery_router_flows::RecoveryRecordBack, RecurlyRecordBackResponse, + recovery_flow_common_types::RevenueRecoveryRecordBackData, recovery_request_types::RevenueRecoveryRecordBackRequest, recovery_response_types::RevenueRecoveryRecordBackResponse, >, @@ -494,3 +321,132 @@ impl }) } } + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RecurlyInvoiceSyncResponse { + pub id: String, + pub total: FloatMajorUnit, + pub currency: common_enums::Currency, + pub address: Option, + pub line_items: Vec, + pub transactions: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RecurlyInvoiceBillingAddress { + pub street1: Option>, + pub street2: Option>, + pub region: Option>, + pub country: Option, + pub postal_code: Option>, + pub city: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RecurlyLineItems { + #[serde(rename = "type")] + pub invoice_type: RecurlyInvoiceLineItemType, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub start_date: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub end_date: PrimitiveDateTime, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum RecurlyInvoiceLineItemType { + Credit, + Charge, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub struct RecurlyInvoiceTransactionsStatus { + pub status: String, +} + +#[cfg(all(feature = "v2", feature = "revenue_recovery"))] +impl + TryFrom< + ResponseRouterDataV2< + recovery_router_flows::BillingConnectorInvoiceSync, + RecurlyInvoiceSyncResponse, + recovery_flow_common_types::BillingConnectorInvoiceSyncFlowData, + recovery_request_types::BillingConnectorInvoiceSyncRequest, + recovery_response_types::BillingConnectorInvoiceSyncResponse, + >, + > for recovery_router_data_types::BillingConnectorInvoiceSyncRouterDataV2 +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterDataV2< + recovery_router_flows::BillingConnectorInvoiceSync, + RecurlyInvoiceSyncResponse, + recovery_flow_common_types::BillingConnectorInvoiceSyncFlowData, + recovery_request_types::BillingConnectorInvoiceSyncRequest, + recovery_response_types::BillingConnectorInvoiceSyncResponse, + >, + ) -> Result { + #[allow(clippy::as_conversions)] + // No of retries never exceeds u16 in recurly. So its better to suppress the clippy warning + let retry_count = item.response.transactions.len() as u16; + let merchant_reference_id = id_type::PaymentReferenceId::from_str(&item.response.id) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(Self { + response: Ok( + recovery_response_types::BillingConnectorInvoiceSyncResponse { + amount: utils::convert_back_amount_to_minor_units( + &FloatMajorUnitForConnector, + item.response.total, + item.response.currency, + )?, + currency: item.response.currency, + merchant_reference_id, + retry_count: Some(retry_count), + billing_address: Some(api_models::payments::Address { + address: Some(api_models::payments::AddressDetails { + city: item + .response + .address + .clone() + .and_then(|address| address.city), + state: item + .response + .address + .clone() + .and_then(|address| address.region), + country: item + .response + .address + .clone() + .and_then(|address| address.country), + line1: item + .response + .address + .clone() + .and_then(|address| address.street1), + line2: item + .response + .address + .clone() + .and_then(|address| address.street2), + line3: None, + zip: item + .response + .address + .clone() + .and_then(|address| address.postal_code), + first_name: None, + last_name: None, + }), + phone: None, + email: None, + }), + created_at: item.response.line_items.first().map(|line| line.start_date), + ends_at: item.response.line_items.first().map(|line| line.end_date), + }, + ), + ..item.data + }) + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs b/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs index 1056006642..c6c9c6c4aa 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs @@ -307,6 +307,7 @@ pub struct StripebillingInvoiceObject { pub currency: enums::Currency, #[serde(rename = "amount_remaining")] pub amount: common_utils::types::MinorUnit, + pub attempt_count: Option, } impl StripebillingWebhookBody { @@ -342,6 +343,8 @@ impl TryFrom for revenue_recovery::RevenueRecoveryInvo currency: item.data.object.currency, merchant_reference_id, billing_address: None, + retry_count: None, + next_billing_at: None, }) } } diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index 1dc8dcc28a..4a597eb628 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -4399,6 +4399,7 @@ default_imp_for_billing_connector_payment_sync!( connectors::Placetopay, connectors::Rapyd, connectors::Razorpay, + connectors::Recurly, connectors::Redsys, connectors::Riskified, connectors::Shift4, @@ -4502,6 +4503,7 @@ default_imp_for_revenue_recovery_record_back!( connectors::Placetopay, connectors::Rapyd, connectors::Razorpay, + connectors::Recurly, connectors::Redsys, connectors::Riskified, connectors::Shift4, diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index fb385f6ee3..0c17f4942f 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -2637,6 +2637,7 @@ default_imp_for_new_connector_integration_frm!( connectors::Multisafepay, connectors::Rapyd, connectors::Razorpay, + connectors::Recurly, connectors::Redsys, connectors::Riskified, connectors::Shift4, @@ -2743,6 +2744,7 @@ default_imp_for_new_connector_integration_connector_authentication!( connectors::Razorpay, connectors::Redsys, connectors::Riskified, + connectors::Recurly, connectors::Shift4, connectors::Stax, connectors::Square, diff --git a/crates/hyperswitch_connectors/src/types.rs b/crates/hyperswitch_connectors/src/types.rs index 209360d32b..4a36f22cb9 100644 --- a/crates/hyperswitch_connectors/src/types.rs +++ b/crates/hyperswitch_connectors/src/types.rs @@ -2,6 +2,7 @@ use hyperswitch_domain_models::types::{PayoutsData, PayoutsResponseData}; use hyperswitch_domain_models::{ router_data::{AccessToken, RouterData}, + router_data_v2::RouterDataV2, router_flow_types::{ Accept, AccessTokenAuth, Authorize, Capture, Checkout, Defend, Evidence, Fulfillment, PSync, PreProcessing, Session, Transaction, Upload, Void, @@ -71,3 +72,9 @@ pub type FrmFulfillmentType = dyn ConnectorIntegration; pub type FrmCheckoutRouterData = RouterData; + +pub struct ResponseRouterDataV2 { + pub response: R, + pub data: RouterDataV2, + pub http_code: u16, +} diff --git a/crates/hyperswitch_domain_models/src/configs.rs b/crates/hyperswitch_domain_models/src/configs.rs index 813a6ecbb4..a2ab95fe8c 100644 --- a/crates/hyperswitch_domain_models/src/configs.rs +++ b/crates/hyperswitch_domain_models/src/configs.rs @@ -1,8 +1,11 @@ //! Configs interface -use common_enums::ApplicationError; +use common_enums::{connector_enums, ApplicationError}; +use common_utils::errors::CustomResult; use masking::Secret; use router_derive; use serde::Deserialize; + +use crate::errors::api_error_response; // struct Connectors #[allow(missing_docs, missing_debug_implementations)] #[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] @@ -111,6 +114,20 @@ pub struct Connectors { pub zsl: ConnectorParams, } +impl Connectors { + pub fn get_connector_params( + &self, + connector: connector_enums::Connector, + ) -> CustomResult { + match connector { + connector_enums::Connector::Recurly => Ok(self.recurly.clone()), + connector_enums::Connector::Stripebilling => Ok(self.stripebilling.clone()), + connector_enums::Connector::Chargebee => Ok(self.chargebee.clone()), + _ => Err(api_error_response::ApiErrorResponse::IncorrectConnectorNameGiven.into()), + } + } +} + /// struct ConnectorParams #[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] #[serde(default)] diff --git a/crates/hyperswitch_domain_models/src/revenue_recovery.rs b/crates/hyperswitch_domain_models/src/revenue_recovery.rs index 81443abbaa..82692047e4 100644 --- a/crates/hyperswitch_domain_models/src/revenue_recovery.rs +++ b/crates/hyperswitch_domain_models/src/revenue_recovery.rs @@ -3,7 +3,9 @@ use common_enums::enums as common_enums; use common_utils::{id_type, types as util_types}; use time::PrimitiveDateTime; -use crate::router_response_types::revenue_recovery::BillingConnectorPaymentsSyncResponse; +use crate::router_response_types::revenue_recovery::{ + BillingConnectorInvoiceSyncResponse, BillingConnectorPaymentsSyncResponse, +}; /// Recovery payload is unified struct constructed from billing connectors #[derive(Debug)] @@ -49,7 +51,7 @@ pub struct RevenueRecoveryAttemptData { } /// This is unified struct for Revenue Recovery Invoice Data and it is constructed from billing connectors -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RevenueRecoveryInvoiceData { /// invoice amount at billing connector pub amount: util_types::MinorUnit, @@ -59,6 +61,10 @@ pub struct RevenueRecoveryInvoiceData { pub merchant_reference_id: id_type::PaymentReferenceId, /// billing address id of the invoice pub billing_address: Option, + /// Retry count of the invoice + pub retry_count: Option, + /// Ending date of the invoice or the Next billing time of the Subscription + pub next_billing_at: Option, } /// type of action that needs to taken after consuming recovery payload @@ -209,38 +215,62 @@ impl From<&RevenueRecoveryInvoiceData> for api_payments::PaymentsCreateIntentReq } } -impl From<&BillingConnectorPaymentsSyncResponse> for RevenueRecoveryInvoiceData { - fn from(data: &BillingConnectorPaymentsSyncResponse) -> Self { +impl From<&BillingConnectorInvoiceSyncResponse> for RevenueRecoveryInvoiceData { + fn from(data: &BillingConnectorInvoiceSyncResponse) -> Self { Self { amount: data.amount, currency: data.currency, merchant_reference_id: data.merchant_reference_id.clone(), - billing_address: None, + billing_address: data.billing_address.clone(), + retry_count: data.retry_count, + next_billing_at: data.ends_at, } } } -impl From<&BillingConnectorPaymentsSyncResponse> for RevenueRecoveryAttemptData { - fn from(data: &BillingConnectorPaymentsSyncResponse) -> Self { +impl + From<( + &BillingConnectorPaymentsSyncResponse, + &RevenueRecoveryInvoiceData, + )> for RevenueRecoveryAttemptData +{ + fn from( + data: ( + &BillingConnectorPaymentsSyncResponse, + &RevenueRecoveryInvoiceData, + ), + ) -> Self { + let billing_connector_payment_details = data.0; + let invoice_details = data.1; Self { - amount: data.amount, - currency: data.currency, - merchant_reference_id: data.merchant_reference_id.clone(), - connector_transaction_id: data.connector_transaction_id.clone(), - error_code: data.error_code.clone(), - error_message: data.error_message.clone(), - processor_payment_method_token: data.processor_payment_method_token.clone(), - connector_customer_id: data.connector_customer_id.clone(), - connector_account_reference_id: data.connector_account_reference_id.clone(), - transaction_created_at: data.transaction_created_at, - status: data.status, - payment_method_type: data.payment_method_type, - payment_method_sub_type: data.payment_method_sub_type, + amount: billing_connector_payment_details.amount, + currency: billing_connector_payment_details.currency, + merchant_reference_id: billing_connector_payment_details + .merchant_reference_id + .clone(), + connector_transaction_id: billing_connector_payment_details + .connector_transaction_id + .clone(), + error_code: billing_connector_payment_details.error_code.clone(), + error_message: billing_connector_payment_details.error_message.clone(), + processor_payment_method_token: billing_connector_payment_details + .processor_payment_method_token + .clone(), + connector_customer_id: billing_connector_payment_details + .connector_customer_id + .clone(), + connector_account_reference_id: billing_connector_payment_details + .connector_account_reference_id + .clone(), + transaction_created_at: billing_connector_payment_details.transaction_created_at, + status: billing_connector_payment_details.status, + payment_method_type: billing_connector_payment_details.payment_method_type, + payment_method_sub_type: billing_connector_payment_details.payment_method_sub_type, network_advice_code: None, network_decline_code: None, network_error_message: None, - retry_count: None, - invoice_next_billing_time: None, + retry_count: invoice_details.retry_count, + invoice_next_billing_time: invoice_details.next_billing_at, } } } diff --git a/crates/hyperswitch_domain_models/src/router_request_types/revenue_recovery.rs b/crates/hyperswitch_domain_models/src/router_request_types/revenue_recovery.rs index 4d5df3b6fb..402b1dd2e8 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types/revenue_recovery.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types/revenue_recovery.rs @@ -1,9 +1,13 @@ use common_enums::enums; +use crate::configs; + #[derive(Debug, Clone)] pub struct BillingConnectorPaymentsSyncRequest { /// unique id for making billing connector psync call pub billing_connector_psync_id: String, + /// connector params of the connector + pub connector_params: configs::ConnectorParams, } #[derive(Debug, Clone)] @@ -14,9 +18,13 @@ pub struct RevenueRecoveryRecordBackRequest { pub payment_method_type: Option, pub attempt_status: common_enums::AttemptStatus, pub connector_transaction_id: Option, + pub connector_params: configs::ConnectorParams, } #[derive(Debug, Clone)] pub struct BillingConnectorInvoiceSyncRequest { + /// Invoice id pub billing_connector_invoice_id: String, + /// connector params of the connector + pub connector_params: configs::ConnectorParams, } diff --git a/crates/hyperswitch_domain_models/src/router_response_types/revenue_recovery.rs b/crates/hyperswitch_domain_models/src/router_response_types/revenue_recovery.rs index 6877befa5d..598795fa11 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types/revenue_recovery.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types/revenue_recovery.rs @@ -35,6 +35,7 @@ pub struct RevenueRecoveryRecordBackResponse { pub merchant_reference_id: common_utils::id_type::PaymentReferenceId, } +#[derive(Debug, Clone)] pub struct BillingConnectorInvoiceSyncResponse { /// transaction amount against invoice, accepted in minor unit. pub amount: MinorUnit, diff --git a/crates/hyperswitch_domain_models/src/types.rs b/crates/hyperswitch_domain_models/src/types.rs index 234a1343c8..8fc7b8e29e 100644 --- a/crates/hyperswitch_domain_models/src/types.rs +++ b/crates/hyperswitch_domain_models/src/types.rs @@ -2,6 +2,7 @@ pub use diesel_models::types::OrderDetailsWithAmount; use crate::{ router_data::{AccessToken, RouterData}, + router_data_v2::{self, RouterDataV2}, router_flow_types::{ mandate_revoke::MandateRevoke, revenue_recovery::RecoveryRecordBack, AccessTokenAuth, Authenticate, AuthenticationConfirmation, Authorize, AuthorizeSessionToken, @@ -119,3 +120,24 @@ pub type BillingConnectorInvoiceSyncRouterData = RouterData< BillingConnectorInvoiceSyncRequest, BillingConnectorInvoiceSyncResponse, >; + +pub type BillingConnectorInvoiceSyncRouterDataV2 = RouterDataV2< + BillingConnectorInvoiceSync, + router_data_v2::flow_common_types::BillingConnectorInvoiceSyncFlowData, + BillingConnectorInvoiceSyncRequest, + BillingConnectorInvoiceSyncResponse, +>; + +pub type BillingConnectorPaymentsSyncRouterDataV2 = RouterDataV2< + BillingConnectorPaymentsSync, + router_data_v2::flow_common_types::BillingConnectorPaymentsSyncFlowData, + BillingConnectorPaymentsSyncRequest, + BillingConnectorPaymentsSyncResponse, +>; + +pub type RevenueRecoveryRecordBackRouterDataV2 = RouterDataV2< + RecoveryRecordBack, + router_data_v2::flow_common_types::RevenueRecoveryRecordBackData, + RevenueRecoveryRecordBackRequest, + RevenueRecoveryRecordBackResponse, +>; diff --git a/crates/hyperswitch_interfaces/src/api/revenue_recovery_v2.rs b/crates/hyperswitch_interfaces/src/api/revenue_recovery_v2.rs index b63021b8bf..11a854c6de 100644 --- a/crates/hyperswitch_interfaces/src/api/revenue_recovery_v2.rs +++ b/crates/hyperswitch_interfaces/src/api/revenue_recovery_v2.rs @@ -20,6 +20,7 @@ use hyperswitch_domain_models::{ use crate::connector_integration_v2::ConnectorIntegrationV2; +#[cfg(all(feature = "v2", feature = "revenue_recovery"))] /// trait RevenueRecoveryV2 pub trait RevenueRecoveryV2: BillingConnectorPaymentsSyncIntegrationV2 @@ -28,6 +29,10 @@ pub trait RevenueRecoveryV2: { } +#[cfg(not(all(feature = "v2", feature = "revenue_recovery")))] +/// trait RevenueRecoveryV2 +pub trait RevenueRecoveryV2 {} + /// trait BillingConnectorPaymentsSyncIntegrationV2 pub trait BillingConnectorPaymentsSyncIntegrationV2: ConnectorIntegrationV2< diff --git a/crates/hyperswitch_interfaces/src/conversion_impls.rs b/crates/hyperswitch_interfaces/src/conversion_impls.rs index c431121faa..e01cd5ae9e 100644 --- a/crates/hyperswitch_interfaces/src/conversion_impls.rs +++ b/crates/hyperswitch_interfaces/src/conversion_impls.rs @@ -9,9 +9,10 @@ use hyperswitch_domain_models::{ router_data::{self, RouterData}, router_data_v2::{ flow_common_types::{ - AccessTokenFlowData, BillingConnectorPaymentsSyncFlowData, DisputesFlowData, - ExternalAuthenticationFlowData, FilesFlowData, MandateRevokeFlowData, PaymentFlowData, - RefundFlowData, RevenueRecoveryRecordBackData, UasFlowData, WebhookSourceVerifyData, + AccessTokenFlowData, BillingConnectorInvoiceSyncFlowData, + BillingConnectorPaymentsSyncFlowData, DisputesFlowData, ExternalAuthenticationFlowData, + FilesFlowData, MandateRevokeFlowData, PaymentFlowData, RefundFlowData, + RevenueRecoveryRecordBackData, UasFlowData, WebhookSourceVerifyData, }, RouterDataV2, }, @@ -840,3 +841,42 @@ impl RouterDataConversion }) } } + +impl RouterDataConversion + for BillingConnectorInvoiceSyncFlowData +{ + fn from_old_router_data( + old_router_data: &RouterData, + ) -> CustomResult, ConnectorError> + where + Self: Sized, + { + let resource_common_data = Self {}; + Ok(RouterDataV2 { + flow: std::marker::PhantomData, + tenant_id: old_router_data.tenant_id.clone(), + resource_common_data, + connector_auth_type: old_router_data.connector_auth_type.clone(), + request: old_router_data.request.clone(), + response: old_router_data.response.clone(), + }) + } + + fn to_old_router_data( + new_router_data: RouterDataV2, + ) -> CustomResult, ConnectorError> + where + Self: Sized, + { + let router_data = get_default_router_data( + new_router_data.tenant_id.clone(), + "BillingConnectorInvoiceSync", + new_router_data.request, + new_router_data.response, + ); + Ok(RouterData { + connector_auth_type: new_router_data.connector_auth_type.clone(), + ..router_data + }) + } +} diff --git a/crates/hyperswitch_interfaces/src/types.rs b/crates/hyperswitch_interfaces/src/types.rs index b30ed8491a..27f50609da 100644 --- a/crates/hyperswitch_interfaces/src/types.rs +++ b/crates/hyperswitch_interfaces/src/types.rs @@ -2,6 +2,7 @@ use hyperswitch_domain_models::{ router_data::AccessToken, + router_data_v2::flow_common_types, router_flow_types::{ access_token_auth::AccessTokenAuth, dispute::{Accept, Defend, Evidence}, @@ -61,7 +62,7 @@ use hyperswitch_domain_models::{ router_response_types::PayoutsResponseData, }; -use crate::api::ConnectorIntegration; +use crate::{api::ConnectorIntegration, connector_integration_v2::ConnectorIntegrationV2}; /// struct Response #[derive(Clone, Debug)] pub struct Response { @@ -257,3 +258,27 @@ pub type BillingConnectorInvoiceSyncType = dyn ConnectorIntegration< BillingConnectorInvoiceSyncRequest, BillingConnectorInvoiceSyncResponse, >; + +/// Type alias for `ConnectorIntegrationV2` +pub type RevenueRecoveryRecordBackTypeV2 = dyn ConnectorIntegrationV2< + RecoveryRecordBack, + flow_common_types::RevenueRecoveryRecordBackData, + RevenueRecoveryRecordBackRequest, + RevenueRecoveryRecordBackResponse, +>; + +/// Type alias for `ConnectorIntegrationV2` +pub type BillingConnectorPaymentsSyncTypeV2 = dyn ConnectorIntegrationV2< + BillingConnectorPaymentsSync, + flow_common_types::BillingConnectorPaymentsSyncFlowData, + BillingConnectorPaymentsSyncRequest, + BillingConnectorPaymentsSyncResponse, +>; + +/// Type alias for `ConnectorIntegrationV2` +pub type BillingConnectorInvoiceSyncTypeV2 = dyn ConnectorIntegrationV2< + BillingConnectorInvoiceSync, + flow_common_types::BillingConnectorInvoiceSyncFlowData, + BillingConnectorInvoiceSyncRequest, + BillingConnectorInvoiceSyncResponse, +>; diff --git a/crates/hyperswitch_interfaces/src/webhooks.rs b/crates/hyperswitch_interfaces/src/webhooks.rs index a84c5a02fc..6a243cf09f 100644 --- a/crates/hyperswitch_interfaces/src/webhooks.rs +++ b/crates/hyperswitch_interfaces/src/webhooks.rs @@ -304,16 +304,4 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { ) .into()) } - - /// get billing address for invoice if present in the webhook - #[cfg(all(feature = "revenue_recovery", feature = "v2"))] - fn get_billing_address_for_invoice( - &self, - _request: &IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented( - "get_billing_address_for_invoice method".to_string(), - ) - .into()) - } } diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index a5aedab894..3ae171545b 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -490,6 +490,7 @@ pub(crate) async fn fetch_raw_secrets( delayed_session_response: conf.delayed_session_response, webhook_source_verification_call: conf.webhook_source_verification_call, billing_connectors_payment_sync: conf.billing_connectors_payment_sync, + billing_connectors_invoice_sync: conf.billing_connectors_invoice_sync, payment_method_auth, connector_request_reference_id_config: conf.connector_request_reference_id_config, #[cfg(feature = "payouts")] diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index c5ab8a3121..10a72c4e82 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -104,6 +104,7 @@ pub struct Settings { pub delayed_session_response: DelayedSessionConfig, pub webhook_source_verification_call: WebhookSourceVerificationCall, pub billing_connectors_payment_sync: BillingConnectorPaymentsSyncCall, + pub billing_connectors_invoice_sync: BillingConnectorInvoiceSyncCall, pub payment_method_auth: SecretStateContainer, pub connector_request_reference_id_config: ConnectorRequestReferenceIdConfig, #[cfg(feature = "payouts")] @@ -837,6 +838,12 @@ pub struct BillingConnectorPaymentsSyncCall { pub billing_connectors_which_require_payment_sync: HashSet, } +#[derive(Debug, Deserialize, Clone, Default)] +pub struct BillingConnectorInvoiceSyncCall { + #[serde(deserialize_with = "deserialize_hashset")] + pub billing_connectors_which_requires_invoice_sync_call: HashSet, +} + #[derive(Debug, Deserialize, Clone, Default)] pub struct ApplePayDecryptConfig { pub apple_pay_ppc: Secret, diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 927bfb4fdb..7f3fd98d77 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -516,6 +516,8 @@ pub enum RevenueRecoveryError { ProcessTrackerResponseError, #[error("Billing connector psync call failed")] BillingConnectorPaymentsSyncFailed, + #[error("Billing connector invoice sync call failed")] + BillingConnectorInvoiceSyncFailed, #[error("Failed to get the retry count for payment intent")] RetryCountFetchFailed, #[error("Failed to get the billing threshold retry count")] diff --git a/crates/router/src/core/revenue_recovery/types.rs b/crates/router/src/core/revenue_recovery/types.rs index a47e73ccf1..8b30011e02 100644 --- a/crates/router/src/core/revenue_recovery/types.rs +++ b/crates/router/src/core/revenue_recovery/types.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, str::FromStr}; use api_models::{ enums as api_enums, @@ -771,6 +771,20 @@ pub fn construct_recovery_record_back_router_data( .attach_printable( "Merchant reference id not found while recording back to billing connector", )?; + let connector_name = billing_mca.get_connector_name_as_string(); + let connector = common_enums::connector_enums::Connector::from_str(connector_name.as_str()) + .change_context(errors::RecoveryError::RecordBackToBillingConnectorFailed) + .attach_printable("Cannot find connector from the connector_name")?; + + let connector_params = hyperswitch_domain_models::configs::Connectors::get_connector_params( + &state.conf.connectors, + connector, + ) + .change_context(errors::RecoveryError::RecordBackToBillingConnectorFailed) + .attach_printable(format!( + "cannot find connector params for this connector {} in this flow", + connector + ))?; let router_data = router_data_v2::RouterDataV2 { flow: PhantomData::, @@ -787,6 +801,7 @@ pub fn construct_recovery_record_back_router_data( .connector_payment_id .as_ref() .map(|id| common_utils::types::ConnectorTransactionId::TxnId(id.clone())), + connector_params, }, response: Err(types::ErrorResponse::default()), }; diff --git a/crates/router/src/core/webhooks/recovery_incoming.rs b/crates/router/src/core/webhooks/recovery_incoming.rs index 2a15899036..89341614d9 100644 --- a/crates/router/src/core/webhooks/recovery_incoming.rs +++ b/crates/router/src/core/webhooks/recovery_incoming.rs @@ -58,6 +58,12 @@ pub async fn recovery_incoming_webhook_flow( .change_context(errors::RevenueRecoveryError::InvoiceWebhookProcessingFailed) .attach_printable_lazy(|| format!("unable to parse connector name {connector_name:?}"))?; + let billing_connectors_with_invoice_sync_call = &state.conf.billing_connectors_invoice_sync; + + let should_billing_connector_invoice_api_called = billing_connectors_with_invoice_sync_call + .billing_connectors_which_requires_invoice_sync_call + .contains(&connector); + let billing_connectors_with_payment_sync_call = &state.conf.billing_connectors_payment_sync; let should_billing_connector_payment_api_called = billing_connectors_with_payment_sync_call @@ -75,12 +81,26 @@ pub async fn recovery_incoming_webhook_flow( ) .await?; - // Checks whether we have data in recovery_details , If its there then it will use the data and convert it into required from or else fetches from Incoming webhook + let invoice_id = billing_connector_payment_details + .clone() + .map(|data| data.merchant_reference_id); + let billing_connector_invoice_details = + BillingConnectorInvoiceSyncResponseData::get_billing_connector_invoice_details( + should_billing_connector_invoice_api_called, + &state, + &merchant_context, + &billing_connector_account, + connector_name, + invoice_id, + ) + .await?; + + // Checks whether we have data in billing_connector_invoice_details , if it is there then we construct revenue recovery invoice from it else it takes from webhook let invoice_details = RevenueRecoveryInvoice::get_recovery_invoice_details( connector_enum, request_details, - billing_connector_payment_details.as_ref(), + billing_connector_invoice_details.as_ref(), )?; // Fetch the intent using merchant reference id, if not found create new intent. @@ -108,6 +128,7 @@ pub async fn recovery_incoming_webhook_flow( &merchant_context, &business_profile, &payment_intent, + &invoice_details.0, ) .await?; @@ -224,11 +245,11 @@ impl RevenueRecoveryInvoice { fn get_recovery_invoice_details( connector_enum: &connector_integration_interface::ConnectorEnum, request_details: &hyperswitch_interfaces::webhooks::IncomingWebhookRequestDetails<'_>, - billing_connector_payment_details: Option< - &revenue_recovery_response::BillingConnectorPaymentsSyncResponse, + billing_connector_invoice_details: Option< + &revenue_recovery_response::BillingConnectorInvoiceSyncResponse, >, ) -> CustomResult { - billing_connector_payment_details.map_or_else( + billing_connector_invoice_details.map_or_else( || { interface_webhooks::IncomingWebhook::get_revenue_recovery_invoice_details( connector_enum, @@ -343,6 +364,7 @@ impl RevenueRecoveryAttempt { billing_connector_payment_details: Option< &revenue_recovery_response::BillingConnectorPaymentsSyncResponse, >, + billing_connector_invoice_details: &revenue_recovery::RevenueRecoveryInvoiceData, ) -> CustomResult { billing_connector_payment_details.map_or_else( || { @@ -357,9 +379,10 @@ impl RevenueRecoveryAttempt { .map(RevenueRecoveryAttempt) }, |data| { - Ok(Self(revenue_recovery::RevenueRecoveryAttemptData::from( + Ok(Self(revenue_recovery::RevenueRecoveryAttemptData::from(( data, - ))) + billing_connector_invoice_details, + )))) }, ) } @@ -604,6 +627,7 @@ impl RevenueRecoveryAttempt { merchant_context: &domain::MerchantContext, business_profile: &domain::Profile, payment_intent: &revenue_recovery::RecoveryPaymentIntent, + invoice_details: &revenue_recovery::RevenueRecoveryInvoiceData, ) -> CustomResult< ( Option, @@ -617,6 +641,7 @@ impl RevenueRecoveryAttempt { connector_enum, request_details, billing_connector_payment_details, + invoice_details, )?; // Find the payment merchant connector ID at the top level to avoid multiple DB calls. @@ -859,12 +884,28 @@ impl BillingConnectorPaymentsSyncFlowRouterData { .parse_value("ConnectorAuthType") .change_context(errors::RevenueRecoveryError::BillingConnectorPaymentsSyncFailed)?; + let connector = common_enums::connector_enums::Connector::from_str(connector_name) + .change_context(errors::RevenueRecoveryError::BillingConnectorInvoiceSyncFailed) + .attach_printable("Cannot find connector from the connector_name")?; + + let connector_params = + hyperswitch_domain_models::configs::Connectors::get_connector_params( + &state.conf.connectors, + connector, + ) + .change_context(errors::RevenueRecoveryError::BillingConnectorPaymentsSyncFailed) + .attach_printable(format!( + "cannot find connector params for this connector {} in this flow", + connector + ))?; + let router_data = types::RouterDataV2 { flow: PhantomData::, tenant_id: state.tenant.tenant_id.clone(), resource_common_data: flow_common_types::BillingConnectorPaymentsSyncFlowData, connector_auth_type: auth_type, request: revenue_recovery_request::BillingConnectorPaymentsSyncRequest { + connector_params, billing_connector_psync_id: billing_connector_psync_id.to_string(), }, response: Err(types::ErrorResponse::default()), @@ -886,3 +927,169 @@ impl BillingConnectorPaymentsSyncFlowRouterData { self.0 } } + +pub struct BillingConnectorInvoiceSyncResponseData( + revenue_recovery_response::BillingConnectorInvoiceSyncResponse, +); +pub struct BillingConnectorInvoiceSyncFlowRouterData( + router_types::BillingConnectorInvoiceSyncRouterData, +); + +impl BillingConnectorInvoiceSyncResponseData { + async fn handle_billing_connector_invoice_sync_call( + state: &SessionState, + merchant_context: &domain::MerchantContext, + merchant_connector_account: &hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccount, + connector_name: &str, + id: &str, + ) -> CustomResult { + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector_name, + api::GetToken::Connector, + None, + ) + .change_context(errors::RevenueRecoveryError::BillingConnectorInvoiceSyncFailed) + .attach_printable("invalid connector name received in payment attempt")?; + + let connector_integration: services::BoxedBillingConnectorInvoiceSyncIntegrationInterface< + router_flow_types::BillingConnectorInvoiceSync, + revenue_recovery_request::BillingConnectorInvoiceSyncRequest, + revenue_recovery_response::BillingConnectorInvoiceSyncResponse, + > = connector_data.connector.get_connector_integration(); + + let router_data = + BillingConnectorInvoiceSyncFlowRouterData::construct_router_data_for_billing_connector_invoice_sync_call( + state, + connector_name, + merchant_connector_account, + merchant_context, + id, + ) + .await + .change_context(errors::RevenueRecoveryError::BillingConnectorInvoiceSyncFailed) + .attach_printable( + "Failed while constructing router data for billing connector psync call", + )? + .inner(); + + let response = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .change_context(errors::RevenueRecoveryError::BillingConnectorInvoiceSyncFailed) + .attach_printable("Failed while fetching billing connector Invoice details")?; + + let additional_recovery_details = match response.response { + Ok(response) => Ok(response), + error @ Err(_) => { + router_env::logger::error!(?error); + Err(errors::RevenueRecoveryError::BillingConnectorPaymentsSyncFailed) + .attach_printable("Failed while fetching billing connector Invoice details") + } + }?; + Ok(Self(additional_recovery_details)) + } + + async fn get_billing_connector_invoice_details( + should_billing_connector_invoice_api_called: bool, + state: &SessionState, + merchant_context: &domain::MerchantContext, + billing_connector_account: &hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccount, + connector_name: &str, + merchant_reference_id: Option, + ) -> CustomResult< + Option, + errors::RevenueRecoveryError, + > { + let response_data = match should_billing_connector_invoice_api_called { + true => { + let billing_connector_invoice_id = merchant_reference_id + .as_ref() + .map(|id| id.get_string_repr()) + .ok_or(errors::RevenueRecoveryError::BillingConnectorInvoiceSyncFailed)?; + + let billing_connector_invoice_details = + Self::handle_billing_connector_invoice_sync_call( + state, + merchant_context, + billing_connector_account, + connector_name, + billing_connector_invoice_id, + ) + .await?; + Some(billing_connector_invoice_details.inner()) + } + false => None, + }; + + Ok(response_data) + } + + fn inner(self) -> revenue_recovery_response::BillingConnectorInvoiceSyncResponse { + self.0 + } +} + +impl BillingConnectorInvoiceSyncFlowRouterData { + async fn construct_router_data_for_billing_connector_invoice_sync_call( + state: &SessionState, + connector_name: &str, + merchant_connector_account: &hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccount, + merchant_context: &domain::MerchantContext, + billing_connector_invoice_id: &str, + ) -> CustomResult { + let auth_type: types::ConnectorAuthType = helpers::MerchantConnectorAccountType::DbVal( + Box::new(merchant_connector_account.clone()), + ) + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::RevenueRecoveryError::BillingConnectorInvoiceSyncFailed)?; + + let connector = common_enums::connector_enums::Connector::from_str(connector_name) + .change_context(errors::RevenueRecoveryError::BillingConnectorInvoiceSyncFailed) + .attach_printable("Cannot find connector from the connector_name")?; + + let connector_params = + hyperswitch_domain_models::configs::Connectors::get_connector_params( + &state.conf.connectors, + connector, + ) + .change_context(errors::RevenueRecoveryError::BillingConnectorPaymentsSyncFailed) + .attach_printable(format!( + "cannot find connector params for this connector {} in this flow", + connector + ))?; + + let router_data = types::RouterDataV2 { + flow: PhantomData::, + tenant_id: state.tenant.tenant_id.clone(), + resource_common_data: flow_common_types::BillingConnectorInvoiceSyncFlowData, + connector_auth_type: auth_type, + request: revenue_recovery_request::BillingConnectorInvoiceSyncRequest { + billing_connector_invoice_id: billing_connector_invoice_id.to_string(), + connector_params, + }, + response: Err(types::ErrorResponse::default()), + }; + + let old_router_data = + flow_common_types::BillingConnectorInvoiceSyncFlowData::to_old_router_data( + router_data, + ) + .change_context(errors::RevenueRecoveryError::BillingConnectorInvoiceSyncFailed) + .attach_printable( + "Cannot construct router data for making the billing connector invoice api call", + )?; + + Ok(Self(old_router_data)) + } + + fn inner(self) -> router_types::BillingConnectorInvoiceSyncRouterData { + self.0 + } +} diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 6b74288014..de9eedb43e 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -499,7 +499,7 @@ impl ConnectorData { Ok(ConnectorEnum::Old(Box::new(connector::Rapyd::new()))) } enums::Connector::Recurly => { - Ok(ConnectorEnum::Old(Box::new(connector::Recurly::new()))) + Ok(ConnectorEnum::New(Box::new(connector::Recurly::new()))) } enums::Connector::Redsys => { Ok(ConnectorEnum::Old(Box::new(connector::Redsys::new()))) diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 018e4220d0..d6edc5c6b8 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -81,7 +81,6 @@ mod powertranz; mod prophetpay; mod rapyd; mod razorpay; -mod recurly; mod redsys; mod shift4; mod square; diff --git a/crates/router/tests/connectors/recurly.rs b/crates/router/tests/connectors/recurly.rs deleted file mode 100644 index a38ed49d41..0000000000 --- a/crates/router/tests/connectors/recurly.rs +++ /dev/null @@ -1,421 +0,0 @@ -use hyperswitch_domain_models::payment_method_data::{Card, PaymentMethodData}; -use masking::Secret; -use router::types::{self, api, storage::enums}; -use test_utils::connector_auth; - -use crate::utils::{self, ConnectorActions}; - -#[derive(Clone, Copy)] -struct RecurlyTest; -impl ConnectorActions for RecurlyTest {} -impl utils::Connector for RecurlyTest { - fn get_data(&self) -> api::ConnectorData { - use router::connector::Recurly; - utils::construct_connector_data_old( - Box::new(Recurly::new()), - types::Connector::Plaid, - api::GetToken::Connector, - None, - ) - } - - fn get_auth_token(&self) -> types::ConnectorAuthType { - utils::to_connector_auth_type( - connector_auth::ConnectorAuthentication::new() - .recurly - .expect("Missing connector authentication configuration") - .into(), - ) - } - - fn get_name(&self) -> String { - "Recurly".to_string() - } -} - -static CONNECTOR: RecurlyTest = RecurlyTest {}; - -fn get_default_payment_info() -> Option { - None -} - -fn payment_method_details() -> Option { - None -} - -// Cards Positive Tests -// Creates a payment using the manual capture flow (Non 3DS). -#[actix_web::test] -async fn should_only_authorize_payment() { - let response = CONNECTOR - .authorize_payment(payment_method_details(), get_default_payment_info()) - .await - .expect("Authorize payment response"); - assert_eq!(response.status, enums::AttemptStatus::Authorized); -} - -// Captures a payment using the manual capture flow (Non 3DS). -#[actix_web::test] -async fn should_capture_authorized_payment() { - let response = CONNECTOR - .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) - .await - .expect("Capture payment response"); - assert_eq!(response.status, enums::AttemptStatus::Charged); -} - -// Partially captures a payment using the manual capture flow (Non 3DS). -#[actix_web::test] -async fn should_partially_capture_authorized_payment() { - let response = CONNECTOR - .authorize_and_capture_payment( - payment_method_details(), - Some(types::PaymentsCaptureData { - amount_to_capture: 50, - ..utils::PaymentCaptureType::default().0 - }), - get_default_payment_info(), - ) - .await - .expect("Capture payment response"); - assert_eq!(response.status, enums::AttemptStatus::Charged); -} - -// Synchronizes a payment using the manual capture flow (Non 3DS). -#[actix_web::test] -async fn should_sync_authorized_payment() { - let authorize_response = CONNECTOR - .authorize_payment(payment_method_details(), get_default_payment_info()) - .await - .expect("Authorize payment response"); - let txn_id = utils::get_connector_transaction_id(authorize_response.response); - let response = CONNECTOR - .psync_retry_till_status_matches( - enums::AttemptStatus::Authorized, - Some(types::PaymentsSyncData { - connector_transaction_id: types::ResponseId::ConnectorTransactionId( - txn_id.unwrap(), - ), - ..Default::default() - }), - get_default_payment_info(), - ) - .await - .expect("PSync response"); - assert_eq!(response.status, enums::AttemptStatus::Authorized,); -} - -// Voids a payment using the manual capture flow (Non 3DS). -#[actix_web::test] -async fn should_void_authorized_payment() { - let response = CONNECTOR - .authorize_and_void_payment( - payment_method_details(), - Some(types::PaymentsCancelData { - connector_transaction_id: String::from(""), - cancellation_reason: Some("requested_by_customer".to_string()), - ..Default::default() - }), - get_default_payment_info(), - ) - .await - .expect("Void payment response"); - assert_eq!(response.status, enums::AttemptStatus::Voided); -} - -// Refunds a payment using the manual capture flow (Non 3DS). -#[actix_web::test] -async fn should_refund_manually_captured_payment() { - let response = CONNECTOR - .capture_payment_and_refund( - payment_method_details(), - None, - None, - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!( - response.response.unwrap().refund_status, - enums::RefundStatus::Success, - ); -} - -// Partially refunds a payment using the manual capture flow (Non 3DS). -#[actix_web::test] -async fn should_partially_refund_manually_captured_payment() { - let response = CONNECTOR - .capture_payment_and_refund( - payment_method_details(), - None, - Some(types::RefundsData { - refund_amount: 50, - ..utils::PaymentRefundType::default().0 - }), - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!( - response.response.unwrap().refund_status, - enums::RefundStatus::Success, - ); -} - -// Synchronizes a refund using the manual capture flow (Non 3DS). -#[actix_web::test] -async fn should_sync_manually_captured_refund() { - let refund_response = CONNECTOR - .capture_payment_and_refund( - payment_method_details(), - None, - None, - get_default_payment_info(), - ) - .await - .unwrap(); - let response = CONNECTOR - .rsync_retry_till_status_matches( - enums::RefundStatus::Success, - refund_response.response.unwrap().connector_refund_id, - None, - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!( - response.response.unwrap().refund_status, - enums::RefundStatus::Success, - ); -} - -// Creates a payment using the automatic capture flow (Non 3DS). -#[actix_web::test] -async fn should_make_payment() { - let authorize_response = CONNECTOR - .make_payment(payment_method_details(), get_default_payment_info()) - .await - .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); -} - -// Synchronizes a payment using the automatic capture flow (Non 3DS). -#[actix_web::test] -async fn should_sync_auto_captured_payment() { - let authorize_response = CONNECTOR - .make_payment(payment_method_details(), get_default_payment_info()) - .await - .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); - let txn_id = utils::get_connector_transaction_id(authorize_response.response); - assert_ne!(txn_id, None, "Empty connector transaction id"); - let response = CONNECTOR - .psync_retry_till_status_matches( - enums::AttemptStatus::Charged, - Some(types::PaymentsSyncData { - connector_transaction_id: types::ResponseId::ConnectorTransactionId( - txn_id.unwrap(), - ), - capture_method: Some(enums::CaptureMethod::Automatic), - ..Default::default() - }), - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!(response.status, enums::AttemptStatus::Charged,); -} - -// Refunds a payment using the automatic capture flow (Non 3DS). -#[actix_web::test] -async fn should_refund_auto_captured_payment() { - let response = CONNECTOR - .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) - .await - .unwrap(); - assert_eq!( - response.response.unwrap().refund_status, - enums::RefundStatus::Success, - ); -} - -// Partially refunds a payment using the automatic capture flow (Non 3DS). -#[actix_web::test] -async fn should_partially_refund_succeeded_payment() { - let refund_response = CONNECTOR - .make_payment_and_refund( - payment_method_details(), - Some(types::RefundsData { - refund_amount: 50, - ..utils::PaymentRefundType::default().0 - }), - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!( - refund_response.response.unwrap().refund_status, - enums::RefundStatus::Success, - ); -} - -// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). -#[actix_web::test] -async fn should_refund_succeeded_payment_multiple_times() { - CONNECTOR - .make_payment_and_multiple_refund( - payment_method_details(), - Some(types::RefundsData { - refund_amount: 50, - ..utils::PaymentRefundType::default().0 - }), - get_default_payment_info(), - ) - .await; -} - -// Synchronizes a refund using the automatic capture flow (Non 3DS). -#[actix_web::test] -async fn should_sync_refund() { - let refund_response = CONNECTOR - .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) - .await - .unwrap(); - let response = CONNECTOR - .rsync_retry_till_status_matches( - enums::RefundStatus::Success, - refund_response.response.unwrap().connector_refund_id, - None, - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!( - response.response.unwrap().refund_status, - enums::RefundStatus::Success, - ); -} - -// Cards Negative scenarios -// Creates a payment with incorrect CVC. -#[actix_web::test] -async fn should_fail_payment_for_incorrect_cvc() { - let response = CONNECTOR - .make_payment( - Some(types::PaymentsAuthorizeData { - payment_method_data: PaymentMethodData::Card(Card { - card_cvc: Secret::new("12345".to_string()), - ..utils::CCardType::default().0 - }), - ..utils::PaymentAuthorizeType::default().0 - }), - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!( - response.response.unwrap_err().message, - "Your card's security code is invalid.".to_string(), - ); -} - -// Creates a payment with incorrect expiry month. -#[actix_web::test] -async fn should_fail_payment_for_invalid_exp_month() { - let response = CONNECTOR - .make_payment( - Some(types::PaymentsAuthorizeData { - payment_method_data: PaymentMethodData::Card(Card { - card_exp_month: Secret::new("20".to_string()), - ..utils::CCardType::default().0 - }), - ..utils::PaymentAuthorizeType::default().0 - }), - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!( - response.response.unwrap_err().message, - "Your card's expiration month is invalid.".to_string(), - ); -} - -// Creates a payment with incorrect expiry year. -#[actix_web::test] -async fn should_fail_payment_for_incorrect_expiry_year() { - let response = CONNECTOR - .make_payment( - Some(types::PaymentsAuthorizeData { - payment_method_data: PaymentMethodData::Card(Card { - card_exp_year: Secret::new("2000".to_string()), - ..utils::CCardType::default().0 - }), - ..utils::PaymentAuthorizeType::default().0 - }), - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!( - response.response.unwrap_err().message, - "Your card's expiration year is invalid.".to_string(), - ); -} - -// Voids a payment using automatic capture flow (Non 3DS). -#[actix_web::test] -async fn should_fail_void_payment_for_auto_capture() { - let authorize_response = CONNECTOR - .make_payment(payment_method_details(), get_default_payment_info()) - .await - .unwrap(); - assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); - let txn_id = utils::get_connector_transaction_id(authorize_response.response); - assert_ne!(txn_id, None, "Empty connector transaction id"); - let void_response = CONNECTOR - .void_payment(txn_id.unwrap(), None, get_default_payment_info()) - .await - .unwrap(); - assert_eq!( - void_response.response.unwrap_err().message, - "You cannot cancel this PaymentIntent because it has a status of succeeded." - ); -} - -// Captures a payment using invalid connector payment id. -#[actix_web::test] -async fn should_fail_capture_for_invalid_payment() { - let capture_response = CONNECTOR - .capture_payment("123456789".to_string(), None, get_default_payment_info()) - .await - .unwrap(); - assert_eq!( - capture_response.response.unwrap_err().message, - String::from("No such payment_intent: '123456789'") - ); -} - -// Refunds a payment with refund amount higher than payment amount. -#[actix_web::test] -async fn should_fail_for_refund_amount_higher_than_payment_amount() { - let response = CONNECTOR - .make_payment_and_refund( - payment_method_details(), - Some(types::RefundsData { - refund_amount: 150, - ..utils::PaymentRefundType::default().0 - }), - get_default_payment_info(), - ) - .await - .unwrap(); - assert_eq!( - response.response.unwrap_err().message, - "Refund amount (₹1.50) is greater than charge amount (₹1.00)", - ); -} - -// Connector dependent test cases goes here - -// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 8bf92b3b07..225947be11 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -551,3 +551,5 @@ redsys = { payment_method = "card" } [billing_connectors_payment_sync] billing_connectors_which_require_payment_sync = "stripebilling, recurly" +[billing_connectors_invoice_sync] +billing_connectors_which_requires_invoice_sync_call = "recurly" \ No newline at end of file