mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
feat(recovery): add support for custom billing api for v2 (#8838)
Co-authored-by: Chikke Srujan <chikke.srujan@Chikke-Srujan-V9P7D4K9V0.local> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
use actix_web::{web, Responder};
|
||||
use api_models::payments as payments_api;
|
||||
use common_utils::id_type;
|
||||
use error_stack::ResultExt;
|
||||
use error_stack::{report, FutureExt, ResultExt};
|
||||
use hyperswitch_domain_models::{
|
||||
merchant_context::{Context, MerchantContext},
|
||||
payments as payments_domain,
|
||||
@ -12,11 +13,13 @@ use crate::{
|
||||
payments::{self, operations::Operation},
|
||||
webhooks::recovery_incoming,
|
||||
},
|
||||
db::errors::{RouterResponse, StorageErrorExt},
|
||||
logger,
|
||||
routes::SessionState,
|
||||
routes::{app::ReqState, SessionState},
|
||||
services,
|
||||
types::{
|
||||
api::payments as api_types,
|
||||
domain,
|
||||
storage::{self, revenue_recovery as revenue_recovery_types},
|
||||
},
|
||||
};
|
||||
@ -231,3 +234,126 @@ pub async fn record_internal_attempt_api(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn custom_revenue_recovery_core(
|
||||
state: SessionState,
|
||||
req_state: ReqState,
|
||||
merchant_context: MerchantContext,
|
||||
profile: domain::Profile,
|
||||
request: api_models::payments::RecoveryPaymentsCreate,
|
||||
) -> RouterResponse<payments_api::RecoveryPaymentsResponse> {
|
||||
let store = state.store.as_ref();
|
||||
let key_manager_state = &(&state).into();
|
||||
let payment_merchant_connector_account_id = request.payment_merchant_connector_id.to_owned();
|
||||
// Find the payment & billing merchant connector id at the top level to avoid multiple DB calls.
|
||||
let payment_merchant_connector_account = store
|
||||
.find_merchant_connector_account_by_id(
|
||||
key_manager_state,
|
||||
&payment_merchant_connector_account_id,
|
||||
merchant_context.get_merchant_key_store(),
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
||||
id: payment_merchant_connector_account_id
|
||||
.clone()
|
||||
.get_string_repr()
|
||||
.to_string(),
|
||||
})?;
|
||||
let billing_connector_account = store
|
||||
.find_merchant_connector_account_by_id(
|
||||
key_manager_state,
|
||||
&request.billing_merchant_connector_id.clone(),
|
||||
merchant_context.get_merchant_key_store(),
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
||||
id: request
|
||||
.billing_merchant_connector_id
|
||||
.clone()
|
||||
.get_string_repr()
|
||||
.to_string(),
|
||||
})?;
|
||||
|
||||
let recovery_intent =
|
||||
recovery_incoming::RevenueRecoveryInvoice::get_or_create_custom_recovery_intent(
|
||||
request.clone(),
|
||||
&state,
|
||||
&req_state,
|
||||
&merchant_context,
|
||||
&profile,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: format!(
|
||||
"Failed to load recovery intent for merchant reference id : {:?}",
|
||||
request.merchant_reference_id.to_owned()
|
||||
)
|
||||
.to_string(),
|
||||
})?;
|
||||
|
||||
let (revenue_recovery_attempt_data, updated_recovery_intent) =
|
||||
recovery_incoming::RevenueRecoveryAttempt::load_recovery_attempt_from_api(
|
||||
request.clone(),
|
||||
&state,
|
||||
&req_state,
|
||||
&merchant_context,
|
||||
&profile,
|
||||
recovery_intent.clone(),
|
||||
payment_merchant_connector_account,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: format!(
|
||||
"Failed to load recovery attempt for merchant reference id : {:?}",
|
||||
request.merchant_reference_id.to_owned()
|
||||
)
|
||||
.to_string(),
|
||||
})?;
|
||||
|
||||
let intent_retry_count = updated_recovery_intent
|
||||
.feature_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.get_retry_count())
|
||||
.ok_or(report!(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: "Failed to fetch retry count from intent feature metadata".to_string(),
|
||||
}))?;
|
||||
|
||||
router_env::logger::info!("Intent retry count: {:?}", intent_retry_count);
|
||||
let recovery_action = recovery_incoming::RecoveryAction {
|
||||
action: request.action.to_owned(),
|
||||
};
|
||||
let mca_retry_threshold = billing_connector_account
|
||||
.get_retry_threshold()
|
||||
.ok_or(report!(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: "Failed to fetch retry threshold from billing merchant connector account"
|
||||
.to_string(),
|
||||
}))?;
|
||||
|
||||
recovery_action
|
||||
.handle_action(
|
||||
&state,
|
||||
&profile,
|
||||
&merchant_context,
|
||||
&billing_connector_account,
|
||||
mca_retry_threshold,
|
||||
intent_retry_count,
|
||||
&(
|
||||
Some(revenue_recovery_attempt_data),
|
||||
updated_recovery_intent.clone(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::GenericNotFoundError {
|
||||
message: "Unexpected response from recovery core".to_string(),
|
||||
})?;
|
||||
|
||||
let response = api_models::payments::RecoveryPaymentsResponse {
|
||||
id: updated_recovery_intent.payment_id.to_owned(),
|
||||
intent_status: updated_recovery_intent.status.to_owned(),
|
||||
merchant_reference_id: updated_recovery_intent.merchant_reference_id.to_owned(),
|
||||
};
|
||||
|
||||
Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
|
||||
response,
|
||||
))
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use common_enums::AttemptStatus;
|
||||
use masking::PeekInterface;
|
||||
|
||||
use crate::{
|
||||
core::revenue_recovery::types::RevenueRecoveryPaymentsAttemptStatus,
|
||||
@ -42,3 +43,75 @@ impl ForeignFrom<AttemptStatus> for RevenueRecoveryPaymentsAttemptStatus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<api_models::payments::RecoveryPaymentsCreate>
|
||||
for hyperswitch_domain_models::revenue_recovery::RevenueRecoveryInvoiceData
|
||||
{
|
||||
fn foreign_from(data: api_models::payments::RecoveryPaymentsCreate) -> Self {
|
||||
Self {
|
||||
amount: data.amount_details.order_amount().into(),
|
||||
currency: data.amount_details.currency(),
|
||||
merchant_reference_id: data.merchant_reference_id,
|
||||
billing_address: data.billing,
|
||||
retry_count: None,
|
||||
next_billing_at: None,
|
||||
billing_started_at: data.billing_started_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<&api_models::payments::RecoveryPaymentsCreate>
|
||||
for hyperswitch_domain_models::revenue_recovery::RevenueRecoveryAttemptData
|
||||
{
|
||||
fn foreign_from(data: &api_models::payments::RecoveryPaymentsCreate) -> Self {
|
||||
let primary_token = &data
|
||||
.primary_processor_payment_method_token
|
||||
.peek()
|
||||
.to_string();
|
||||
let card_info = data.payment_method_units.units.get(primary_token);
|
||||
Self {
|
||||
amount: data.amount_details.order_amount().into(),
|
||||
currency: data.amount_details.currency(),
|
||||
merchant_reference_id: data.merchant_reference_id.to_owned(),
|
||||
connector_transaction_id: data.connector_transaction_id.as_ref().map(|txn_id| {
|
||||
common_utils::types::ConnectorTransactionId::TxnId(txn_id.peek().to_string())
|
||||
}),
|
||||
error_code: data.error.as_ref().map(|error| error.code.clone()),
|
||||
error_message: data.error.as_ref().map(|error| error.message.clone()),
|
||||
processor_payment_method_token: data
|
||||
.primary_processor_payment_method_token
|
||||
.peek()
|
||||
.to_string(),
|
||||
connector_customer_id: data.connector_customer_id.peek().to_string(),
|
||||
connector_account_reference_id: data
|
||||
.payment_merchant_connector_id
|
||||
.get_string_repr()
|
||||
.to_string(),
|
||||
transaction_created_at: data.transaction_created_at.to_owned(),
|
||||
status: data.attempt_status,
|
||||
payment_method_type: data.payment_method_type,
|
||||
payment_method_sub_type: data.payment_method_sub_type,
|
||||
network_advice_code: data
|
||||
.error
|
||||
.as_ref()
|
||||
.and_then(|error| error.network_advice_code.clone()),
|
||||
network_decline_code: data
|
||||
.error
|
||||
.as_ref()
|
||||
.and_then(|error| error.network_decline_code.clone()),
|
||||
network_error_message: data
|
||||
.error
|
||||
.as_ref()
|
||||
.and_then(|error| error.network_error_message.clone()),
|
||||
/// retry count will be updated whenever there is new attempt is created.
|
||||
retry_count: None,
|
||||
invoice_next_billing_time: None,
|
||||
invoice_billing_started_at_time: data.billing_started_at,
|
||||
card_network: card_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.card_network.clone()),
|
||||
card_isin: card_info.as_ref().and_then(|info| info.card_isin.clone()),
|
||||
charge_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -371,7 +371,6 @@ async fn incoming_webhooks_core<W: types::OutgoingWebhookType>(
|
||||
state.clone(),
|
||||
merchant_context,
|
||||
profile,
|
||||
webhook_details,
|
||||
source_verified,
|
||||
&connector,
|
||||
merchant_connector_account,
|
||||
|
||||
@ -14,7 +14,7 @@ use hyperswitch_domain_models::{
|
||||
};
|
||||
use hyperswitch_interfaces::webhooks as interface_webhooks;
|
||||
use masking::{PeekInterface, Secret};
|
||||
use router_env::{instrument, tracing};
|
||||
use router_env::{instrument, logger, tracing};
|
||||
use services::kafka;
|
||||
|
||||
use crate::{
|
||||
@ -29,7 +29,10 @@ use crate::{
|
||||
self,
|
||||
connector_integration_interface::{self, RouterDataConversion},
|
||||
},
|
||||
types::{self, api, domain, storage::revenue_recovery as storage_churn_recovery},
|
||||
types::{
|
||||
self, api, domain, storage::revenue_recovery as storage_churn_recovery,
|
||||
transformers::ForeignFrom,
|
||||
},
|
||||
workflows::revenue_recovery as revenue_recovery_flow,
|
||||
};
|
||||
|
||||
@ -40,7 +43,6 @@ pub async fn recovery_incoming_webhook_flow(
|
||||
state: SessionState,
|
||||
merchant_context: domain::MerchantContext,
|
||||
business_profile: domain::Profile,
|
||||
_webhook_details: api::IncomingWebhookDetails,
|
||||
source_verified: bool,
|
||||
connector_enum: &connector_integration_interface::ConnectorEnum,
|
||||
billing_connector_account: hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccount,
|
||||
@ -147,7 +149,7 @@ pub async fn recovery_incoming_webhook_flow(
|
||||
)
|
||||
.await
|
||||
{
|
||||
router_env::logger::error!(
|
||||
logger::error!(
|
||||
"Failed to publish revenue recovery event to kafka : {:?}",
|
||||
e
|
||||
);
|
||||
@ -158,7 +160,9 @@ pub async fn recovery_incoming_webhook_flow(
|
||||
.as_ref()
|
||||
.and_then(|attempt| attempt.get_attempt_triggered_by());
|
||||
|
||||
let action = revenue_recovery::RecoveryAction::get_action(event_type, attempt_triggered_by);
|
||||
let recovery_action = RecoveryAction {
|
||||
action: RecoveryAction::get_action(event_type, attempt_triggered_by),
|
||||
};
|
||||
|
||||
let mca_retry_threshold = billing_connector_account
|
||||
.get_retry_threshold()
|
||||
@ -172,67 +176,21 @@ pub async fn recovery_incoming_webhook_flow(
|
||||
.and_then(|metadata| metadata.get_retry_count())
|
||||
.ok_or(report!(errors::RevenueRecoveryError::RetryCountFetchFailed))?;
|
||||
|
||||
router_env::logger::info!("Intent retry count: {:?}", intent_retry_count);
|
||||
|
||||
match action {
|
||||
revenue_recovery::RecoveryAction::CancelInvoice => todo!(),
|
||||
revenue_recovery::RecoveryAction::ScheduleFailedPayment => {
|
||||
let recovery_algorithm_type = business_profile
|
||||
.revenue_recovery_retry_algorithm_type
|
||||
.ok_or(report!(
|
||||
errors::RevenueRecoveryError::RetryAlgorithmTypeNotFound
|
||||
))?;
|
||||
match recovery_algorithm_type {
|
||||
api_enums::RevenueRecoveryAlgorithmType::Monitoring => {
|
||||
handle_monitoring_threshold(
|
||||
&state,
|
||||
&business_profile,
|
||||
merchant_context.get_merchant_key_store(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
revenue_recovery_retry_type => {
|
||||
handle_schedule_failed_payment(
|
||||
&billing_connector_account,
|
||||
intent_retry_count,
|
||||
mca_retry_threshold,
|
||||
&state,
|
||||
&merchant_context,
|
||||
&(
|
||||
recovery_attempt_from_payment_attempt,
|
||||
recovery_intent_from_payment_attempt,
|
||||
),
|
||||
&business_profile,
|
||||
revenue_recovery_retry_type,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
revenue_recovery::RecoveryAction::SuccessPaymentExternal => {
|
||||
// Need to add recovery stop flow for this scenario
|
||||
router_env::logger::info!("Payment has been succeeded via external system");
|
||||
Ok(webhooks::WebhookResponseTracker::NoEffect)
|
||||
}
|
||||
revenue_recovery::RecoveryAction::PendingPayment => {
|
||||
router_env::logger::info!(
|
||||
"Pending transactions are not consumed by the revenue recovery webhooks"
|
||||
);
|
||||
Ok(webhooks::WebhookResponseTracker::NoEffect)
|
||||
}
|
||||
revenue_recovery::RecoveryAction::NoAction => {
|
||||
router_env::logger::info!(
|
||||
"No Recovery action is taken place for recovery event : {:?} and attempt triggered_by : {:?} ", event_type.clone(), attempt_triggered_by
|
||||
);
|
||||
Ok(webhooks::WebhookResponseTracker::NoEffect)
|
||||
}
|
||||
revenue_recovery::RecoveryAction::InvalidAction => {
|
||||
router_env::logger::error!(
|
||||
"Invalid Revenue recovery action state has been received, event : {:?}, triggered_by : {:?}", event_type, attempt_triggered_by
|
||||
);
|
||||
Ok(webhooks::WebhookResponseTracker::NoEffect)
|
||||
}
|
||||
}
|
||||
logger::info!("Intent retry count: {:?}", intent_retry_count);
|
||||
recovery_action
|
||||
.handle_action(
|
||||
&state,
|
||||
&business_profile,
|
||||
&merchant_context,
|
||||
&billing_connector_account,
|
||||
mca_retry_threshold,
|
||||
intent_retry_count,
|
||||
&(
|
||||
recovery_attempt_from_payment_attempt,
|
||||
recovery_intent_from_payment_attempt,
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_monitoring_threshold(
|
||||
@ -284,7 +242,7 @@ async fn handle_schedule_failed_payment(
|
||||
payment_attempt_with_recovery_intent;
|
||||
(intent_retry_count <= mca_retry_threshold)
|
||||
.then(|| {
|
||||
router_env::logger::error!(
|
||||
logger::error!(
|
||||
"Payment retry count {} is less than threshold {}",
|
||||
intent_retry_count,
|
||||
mca_retry_threshold
|
||||
@ -316,6 +274,27 @@ pub struct RevenueRecoveryInvoice(revenue_recovery::RevenueRecoveryInvoiceData);
|
||||
pub struct RevenueRecoveryAttempt(revenue_recovery::RevenueRecoveryAttemptData);
|
||||
|
||||
impl RevenueRecoveryInvoice {
|
||||
pub async fn get_or_create_custom_recovery_intent(
|
||||
data: api_models::payments::RecoveryPaymentsCreate,
|
||||
state: &SessionState,
|
||||
req_state: &ReqState,
|
||||
merchant_context: &domain::MerchantContext,
|
||||
profile: &domain::Profile,
|
||||
) -> CustomResult<revenue_recovery::RecoveryPaymentIntent, errors::RevenueRecoveryError> {
|
||||
let recovery_intent = Self(revenue_recovery::RevenueRecoveryInvoiceData::foreign_from(
|
||||
data,
|
||||
));
|
||||
recovery_intent
|
||||
.get_payment_intent(state, req_state, merchant_context, profile)
|
||||
.await
|
||||
.transpose()
|
||||
.async_unwrap_or_else(|| async {
|
||||
recovery_intent
|
||||
.create_payment_intent(state, req_state, merchant_context, profile)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
fn get_recovery_invoice_details(
|
||||
connector_enum: &connector_integration_interface::ConnectorEnum,
|
||||
request_details: &hyperswitch_interfaces::webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
@ -390,7 +369,7 @@ impl RevenueRecoveryInvoice {
|
||||
Ok(_) => Err(errors::RevenueRecoveryError::PaymentIntentFetchFailed)
|
||||
.attach_printable("Unexpected response from payment intent core"),
|
||||
error @ Err(_) => {
|
||||
router_env::logger::error!(?error);
|
||||
logger::error!(?error);
|
||||
Err(errors::RevenueRecoveryError::PaymentIntentFetchFailed)
|
||||
.attach_printable("failed to fetch payment intent recovery webhook flow")
|
||||
}
|
||||
@ -453,6 +432,44 @@ impl RevenueRecoveryInvoice {
|
||||
}
|
||||
|
||||
impl RevenueRecoveryAttempt {
|
||||
pub async fn load_recovery_attempt_from_api(
|
||||
data: api_models::payments::RecoveryPaymentsCreate,
|
||||
state: &SessionState,
|
||||
req_state: &ReqState,
|
||||
merchant_context: &domain::MerchantContext,
|
||||
profile: &domain::Profile,
|
||||
payment_intent: revenue_recovery::RecoveryPaymentIntent,
|
||||
payment_merchant_connector_account: domain::MerchantConnectorAccount,
|
||||
) -> CustomResult<
|
||||
(
|
||||
revenue_recovery::RecoveryPaymentAttempt,
|
||||
revenue_recovery::RecoveryPaymentIntent,
|
||||
),
|
||||
errors::RevenueRecoveryError,
|
||||
> {
|
||||
let recovery_attempt = Self(revenue_recovery::RevenueRecoveryAttemptData::foreign_from(
|
||||
&data,
|
||||
));
|
||||
recovery_attempt
|
||||
.get_payment_attempt(state, req_state, merchant_context, profile, &payment_intent)
|
||||
.await
|
||||
.transpose()
|
||||
.async_unwrap_or_else(|| async {
|
||||
recovery_attempt
|
||||
.record_payment_attempt(
|
||||
state,
|
||||
req_state,
|
||||
merchant_context,
|
||||
profile,
|
||||
&payment_intent,
|
||||
&data.billing_merchant_connector_id,
|
||||
Some(payment_merchant_connector_account),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn get_recovery_invoice_transaction_details(
|
||||
connector_enum: &connector_integration_interface::ConnectorEnum,
|
||||
request_details: &hyperswitch_interfaces::webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
@ -574,7 +591,7 @@ impl RevenueRecoveryAttempt {
|
||||
Ok(_) => Err(errors::RevenueRecoveryError::PaymentAttemptFetchFailed)
|
||||
.attach_printable("Unexpected response from payment intent core"),
|
||||
error @ Err(_) => {
|
||||
router_env::logger::error!(?error);
|
||||
logger::error!(?error);
|
||||
Err(errors::RevenueRecoveryError::PaymentAttemptFetchFailed)
|
||||
.attach_printable("failed to fetch payment attempt in recovery webhook flow")
|
||||
}
|
||||
@ -600,7 +617,7 @@ impl RevenueRecoveryAttempt {
|
||||
errors::RevenueRecoveryError,
|
||||
> {
|
||||
let payment_connector_id = payment_connector_account.as_ref().map(|account: &hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccount| account.id.clone());
|
||||
let request_payload = self
|
||||
let request_payload: api_payments::PaymentsAttemptRecordRequest = self
|
||||
.create_payment_record_request(
|
||||
state,
|
||||
billing_connector_account_id,
|
||||
@ -660,7 +677,7 @@ impl RevenueRecoveryAttempt {
|
||||
Ok(_) => Err(errors::RevenueRecoveryError::PaymentAttemptFetchFailed)
|
||||
.attach_printable("Unexpected response from record attempt core"),
|
||||
error @ Err(_) => {
|
||||
router_env::logger::error!(?error);
|
||||
logger::error!(?error);
|
||||
Err(errors::RevenueRecoveryError::PaymentAttemptFetchFailed)
|
||||
.attach_printable("failed to record attempt in recovery webhook flow")
|
||||
}
|
||||
@ -979,7 +996,7 @@ impl BillingConnectorPaymentsSyncResponseData {
|
||||
let additional_recovery_details = match response.response {
|
||||
Ok(response) => Ok(response),
|
||||
error @ Err(_) => {
|
||||
router_env::logger::error!(?error);
|
||||
logger::error!(?error);
|
||||
Err(errors::RevenueRecoveryError::BillingConnectorPaymentsSyncFailed)
|
||||
.attach_printable("Failed while fetching billing connector payment details")
|
||||
}
|
||||
@ -1147,7 +1164,7 @@ impl BillingConnectorInvoiceSyncResponseData {
|
||||
let additional_recovery_details = match response.response {
|
||||
Ok(response) => Ok(response),
|
||||
error @ Err(_) => {
|
||||
router_env::logger::error!(?error);
|
||||
logger::error!(?error);
|
||||
Err(errors::RevenueRecoveryError::BillingConnectorPaymentsSyncFailed)
|
||||
.attach_printable("Failed while fetching billing connector Invoice details")
|
||||
}
|
||||
@ -1353,3 +1370,149 @@ impl RecoveryPaymentTuple {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RecoveryAction {
|
||||
pub action: common_types::payments::RecoveryAction,
|
||||
}
|
||||
|
||||
impl RecoveryAction {
|
||||
pub fn get_action(
|
||||
event_type: webhooks::IncomingWebhookEvent,
|
||||
attempt_triggered_by: Option<common_enums::TriggeredBy>,
|
||||
) -> common_types::payments::RecoveryAction {
|
||||
match event_type {
|
||||
webhooks::IncomingWebhookEvent::PaymentIntentFailure
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentSuccess
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentProcessing
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentPartiallyFunded
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentCancelled
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentCancelFailure
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentAuthorizationSuccess
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentAuthorizationFailure
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentCaptureSuccess
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentCaptureFailure
|
||||
| webhooks::IncomingWebhookEvent::PaymentIntentExpired
|
||||
| webhooks::IncomingWebhookEvent::PaymentActionRequired
|
||||
| webhooks::IncomingWebhookEvent::EventNotSupported
|
||||
| webhooks::IncomingWebhookEvent::SourceChargeable
|
||||
| webhooks::IncomingWebhookEvent::SourceTransactionCreated
|
||||
| webhooks::IncomingWebhookEvent::RefundFailure
|
||||
| webhooks::IncomingWebhookEvent::RefundSuccess
|
||||
| webhooks::IncomingWebhookEvent::DisputeOpened
|
||||
| webhooks::IncomingWebhookEvent::DisputeExpired
|
||||
| webhooks::IncomingWebhookEvent::DisputeAccepted
|
||||
| webhooks::IncomingWebhookEvent::DisputeCancelled
|
||||
| webhooks::IncomingWebhookEvent::DisputeChallenged
|
||||
| webhooks::IncomingWebhookEvent::DisputeWon
|
||||
| webhooks::IncomingWebhookEvent::DisputeLost
|
||||
| webhooks::IncomingWebhookEvent::MandateActive
|
||||
| webhooks::IncomingWebhookEvent::MandateRevoked
|
||||
| webhooks::IncomingWebhookEvent::EndpointVerification
|
||||
| webhooks::IncomingWebhookEvent::ExternalAuthenticationARes
|
||||
| webhooks::IncomingWebhookEvent::FrmApproved
|
||||
| webhooks::IncomingWebhookEvent::FrmRejected
|
||||
| webhooks::IncomingWebhookEvent::PayoutSuccess
|
||||
| webhooks::IncomingWebhookEvent::PayoutFailure
|
||||
| webhooks::IncomingWebhookEvent::PayoutProcessing
|
||||
| webhooks::IncomingWebhookEvent::PayoutCancelled
|
||||
| webhooks::IncomingWebhookEvent::PayoutCreated
|
||||
| webhooks::IncomingWebhookEvent::PayoutExpired
|
||||
| webhooks::IncomingWebhookEvent::PayoutReversed => {
|
||||
common_types::payments::RecoveryAction::InvalidAction
|
||||
}
|
||||
webhooks::IncomingWebhookEvent::RecoveryPaymentFailure => match attempt_triggered_by {
|
||||
Some(common_enums::TriggeredBy::Internal) => {
|
||||
common_types::payments::RecoveryAction::NoAction
|
||||
}
|
||||
Some(common_enums::TriggeredBy::External) | None => {
|
||||
common_types::payments::RecoveryAction::ScheduleFailedPayment
|
||||
}
|
||||
},
|
||||
webhooks::IncomingWebhookEvent::RecoveryPaymentSuccess => match attempt_triggered_by {
|
||||
Some(common_enums::TriggeredBy::Internal) => {
|
||||
common_types::payments::RecoveryAction::NoAction
|
||||
}
|
||||
Some(common_enums::TriggeredBy::External) | None => {
|
||||
common_types::payments::RecoveryAction::SuccessPaymentExternal
|
||||
}
|
||||
},
|
||||
webhooks::IncomingWebhookEvent::RecoveryPaymentPending => {
|
||||
common_types::payments::RecoveryAction::PendingPayment
|
||||
}
|
||||
webhooks::IncomingWebhookEvent::RecoveryInvoiceCancel => {
|
||||
common_types::payments::RecoveryAction::CancelInvoice
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn handle_action(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
business_profile: &domain::Profile,
|
||||
merchant_context: &domain::MerchantContext,
|
||||
billing_connector_account: &hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccount,
|
||||
mca_retry_threshold: u16,
|
||||
intent_retry_count: u16,
|
||||
recovery_tuple: &(
|
||||
Option<revenue_recovery::RecoveryPaymentAttempt>,
|
||||
revenue_recovery::RecoveryPaymentIntent,
|
||||
),
|
||||
) -> CustomResult<webhooks::WebhookResponseTracker, errors::RevenueRecoveryError> {
|
||||
match self.action {
|
||||
common_types::payments::RecoveryAction::CancelInvoice => todo!(),
|
||||
common_types::payments::RecoveryAction::ScheduleFailedPayment => {
|
||||
let recovery_algorithm_type = business_profile
|
||||
.revenue_recovery_retry_algorithm_type
|
||||
.ok_or(report!(
|
||||
errors::RevenueRecoveryError::RetryAlgorithmTypeNotFound
|
||||
))?;
|
||||
match recovery_algorithm_type {
|
||||
api_enums::RevenueRecoveryAlgorithmType::Monitoring => {
|
||||
handle_monitoring_threshold(
|
||||
state,
|
||||
business_profile,
|
||||
merchant_context.get_merchant_key_store(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
revenue_recovery_retry_type => {
|
||||
handle_schedule_failed_payment(
|
||||
billing_connector_account,
|
||||
intent_retry_count,
|
||||
mca_retry_threshold,
|
||||
state,
|
||||
merchant_context,
|
||||
recovery_tuple,
|
||||
business_profile,
|
||||
revenue_recovery_retry_type,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
common_types::payments::RecoveryAction::SuccessPaymentExternal => {
|
||||
logger::info!("Payment has been succeeded via external system");
|
||||
Ok(webhooks::WebhookResponseTracker::NoEffect)
|
||||
}
|
||||
common_types::payments::RecoveryAction::PendingPayment => {
|
||||
logger::info!(
|
||||
"Pending transactions are not consumed by the revenue recovery webhooks"
|
||||
);
|
||||
Ok(webhooks::WebhookResponseTracker::NoEffect)
|
||||
}
|
||||
common_types::payments::RecoveryAction::NoAction => {
|
||||
logger::info!(
|
||||
"No Recovery action is taken place for recovery event and attempt triggered_by"
|
||||
);
|
||||
Ok(webhooks::WebhookResponseTracker::NoEffect)
|
||||
}
|
||||
common_types::payments::RecoveryAction::InvalidAction => {
|
||||
logger::error!("Invalid Revenue recovery action state has been received");
|
||||
Ok(webhooks::WebhookResponseTracker::NoEffect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -653,6 +653,10 @@ impl Payments {
|
||||
.service(
|
||||
web::resource("/aggregate").route(web::get().to(payments::get_payments_aggregates)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/recovery")
|
||||
.route(web::post().to(payments::recovery_payments_create)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/profile/aggregate")
|
||||
.route(web::get().to(payments::get_payments_aggregates_profile)),
|
||||
|
||||
@ -168,7 +168,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::PaymentStartRedirection
|
||||
| Flow::ProxyConfirmIntent
|
||||
| Flow::PaymentsRetrieveUsingMerchantReferenceId
|
||||
| Flow::PaymentAttemptsList => Self::Payments,
|
||||
| Flow::PaymentAttemptsList
|
||||
| Flow::RecoveryPaymentsCreate => Self::Payments,
|
||||
|
||||
Flow::PayoutsCreate
|
||||
| Flow::PayoutsRetrieve
|
||||
|
||||
@ -10,6 +10,8 @@ use masking::PeekInterface;
|
||||
use router_env::{env, instrument, logger, tracing, types, Flow};
|
||||
|
||||
use super::app::ReqState;
|
||||
#[cfg(feature = "v2")]
|
||||
use crate::core::revenue_recovery::api as recovery;
|
||||
use crate::{
|
||||
self as app,
|
||||
core::{
|
||||
@ -115,6 +117,40 @@ pub async fn payments_create(
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
pub async fn recovery_payments_create(
|
||||
state: web::Data<app::AppState>,
|
||||
req: actix_web::HttpRequest,
|
||||
json_payload: web::Json<payment_types::RecoveryPaymentsCreate>,
|
||||
) -> impl Responder {
|
||||
let flow = Flow::RecoveryPaymentsCreate;
|
||||
let mut payload = json_payload.into_inner();
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req.clone(),
|
||||
payload,
|
||||
|state, auth: auth::AuthenticationData, req_payload, req_state| {
|
||||
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
|
||||
domain::Context(auth.merchant_account, auth.key_store),
|
||||
));
|
||||
recovery::custom_revenue_recovery_core(
|
||||
state.to_owned(),
|
||||
req_state,
|
||||
merchant_context,
|
||||
auth.profile,
|
||||
req_payload,
|
||||
)
|
||||
},
|
||||
&auth::V2ApiKeyAuth {
|
||||
is_connected_allowed: false,
|
||||
is_platform_allowed: false,
|
||||
},
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::PaymentsCreateIntent, payment_id))]
|
||||
pub async fn payments_create_intent(
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
pub use api_models::payments::{
|
||||
PaymentAttemptListRequest, PaymentAttemptListResponse, PaymentsConfirmIntentRequest,
|
||||
PaymentsCreateIntentRequest, PaymentsIntentResponse, PaymentsUpdateIntentRequest,
|
||||
RecoveryPaymentsCreate,
|
||||
};
|
||||
#[cfg(feature = "v1")]
|
||||
pub use api_models::payments::{
|
||||
|
||||
Reference in New Issue
Block a user