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:
chikke srujan
2025-08-08 02:38:34 -07:00
committed by GitHub
parent b0b71935ca
commit 9e8df84590
15 changed files with 622 additions and 178 deletions

View File

@ -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,
))
}

View File

@ -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,
}
}
}

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
pub use api_models::payments::{
PaymentAttemptListRequest, PaymentAttemptListResponse, PaymentsConfirmIntentRequest,
PaymentsCreateIntentRequest, PaymentsIntentResponse, PaymentsUpdateIntentRequest,
RecoveryPaymentsCreate,
};
#[cfg(feature = "v1")]
pub use api_models::payments::{