feat(connector): [Recurly] add invoice sync support along with transaction monitoring (#7867)

Co-authored-by: Nishanth Challa <nishanth.challa@Nishanth-Challa-C0WGKCFHLF.local>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com>
This commit is contained in:
CHALLA NISHANTH BABU
2025-05-02 17:10:48 +05:30
committed by GitHub
parent af5e56ef9d
commit bcc57ebb2d
31 changed files with 824 additions and 1168 deletions

View File

@ -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")]

View File

@ -104,6 +104,7 @@ pub struct Settings<S: SecretState> {
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<PaymentMethodAuth, S>,
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<enums::Connector>,
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct BillingConnectorInvoiceSyncCall {
#[serde(deserialize_with = "deserialize_hashset")]
pub billing_connectors_which_requires_invoice_sync_call: HashSet<enums::Connector>,
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct ApplePayDecryptConfig {
pub apple_pay_ppc: Secret<String>,

View File

@ -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")]

View File

@ -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::<router_flow_types::RecoveryRecordBack>,
@ -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()),
};

View File

@ -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<Self, errors::RevenueRecoveryError> {
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<Self, errors::RevenueRecoveryError> {
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<revenue_recovery::RecoveryPaymentAttempt>,
@ -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::<router_flow_types::BillingConnectorPaymentsSync>,
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<Self, errors::RevenueRecoveryError> {
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<id_type::PaymentReferenceId>,
) -> CustomResult<
Option<revenue_recovery_response::BillingConnectorInvoiceSyncResponse>,
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<Self, errors::RevenueRecoveryError> {
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::<router_flow_types::BillingConnectorInvoiceSync>,
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
}
}

View File

@ -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())))