mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 21:07:58 +08:00
feat(core): Add record attempt operation for revenue recovery webhooks (#7236)
Co-authored-by: Chikke Srujan <chikke.srujan@Chikke-Srujan-N7WRTY72X7.local> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -483,4 +483,6 @@ pub enum RevenueRecoveryError {
|
||||
PaymentIntentCreateFailed,
|
||||
#[error("Source verification failed for billing connector")]
|
||||
WebhookAuthenticationFailed,
|
||||
#[error("Payment merchant connector account not found using acccount reference id")]
|
||||
PaymentMerchantConnectorAccountNotFound,
|
||||
}
|
||||
|
||||
@ -1725,6 +1725,76 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn record_attempt_core(
|
||||
state: SessionState,
|
||||
req_state: ReqState,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
profile: domain::Profile,
|
||||
key_store: domain::MerchantKeyStore,
|
||||
req: api_models::payments::PaymentsAttemptRecordRequest,
|
||||
payment_id: id_type::GlobalPaymentId,
|
||||
header_payload: HeaderPayload,
|
||||
platform_merchant_account: Option<domain::MerchantAccount>,
|
||||
) -> RouterResponse<api_models::payments::PaymentAttemptResponse> {
|
||||
tracing::Span::current().record("merchant_id", merchant_account.get_id().get_string_repr());
|
||||
|
||||
let operation: &operations::payment_attempt_record::PaymentAttemptRecord =
|
||||
&operations::payment_attempt_record::PaymentAttemptRecord;
|
||||
let boxed_operation: BoxedOperation<
|
||||
'_,
|
||||
api::RecordAttempt,
|
||||
api_models::payments::PaymentsAttemptRecordRequest,
|
||||
domain_payments::PaymentAttemptRecordData<api::RecordAttempt>,
|
||||
> = Box::new(operation);
|
||||
|
||||
let _validate_result = boxed_operation
|
||||
.to_validate_request()?
|
||||
.validate_request(&req, &merchant_account)?;
|
||||
|
||||
tracing::Span::current().record("global_payment_id", payment_id.get_string_repr());
|
||||
|
||||
let operations::GetTrackerResponse { payment_data } = boxed_operation
|
||||
.to_get_tracker()?
|
||||
.get_trackers(
|
||||
&state,
|
||||
&payment_id,
|
||||
&req,
|
||||
&merchant_account,
|
||||
&profile,
|
||||
&key_store,
|
||||
&header_payload,
|
||||
platform_merchant_account.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (_operation, payment_data) = boxed_operation
|
||||
.to_update_tracker()?
|
||||
.update_trackers(
|
||||
&state,
|
||||
req_state,
|
||||
payment_data,
|
||||
None,
|
||||
merchant_account.storage_scheme,
|
||||
None,
|
||||
&key_store,
|
||||
None,
|
||||
header_payload.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
transformers::GenerateResponse::generate_response(
|
||||
payment_data,
|
||||
&state,
|
||||
None,
|
||||
None,
|
||||
header_payload.x_hs_latency,
|
||||
&merchant_account,
|
||||
&profile,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn payments_intent_core<F, Res, Req, Op, D>(
|
||||
|
||||
@ -30,6 +30,8 @@ pub mod payments_incremental_authorization;
|
||||
#[cfg(feature = "v1")]
|
||||
pub mod tax_calculation;
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
pub mod payment_attempt_record;
|
||||
#[cfg(feature = "v2")]
|
||||
pub mod payment_confirm_intent;
|
||||
#[cfg(feature = "v2")]
|
||||
|
||||
@ -0,0 +1,323 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use api_models::{enums::FrmSuggestion, payments::PaymentsAttemptRecordRequest};
|
||||
use async_trait::async_trait;
|
||||
use common_utils::{
|
||||
errors::CustomResult,
|
||||
ext_traits::{AsyncExt, Encode, ValueExt},
|
||||
types::keymanager::ToEncryptable,
|
||||
};
|
||||
use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::payments::PaymentAttemptRecordData;
|
||||
use masking::PeekInterface;
|
||||
use router_env::{instrument, tracing};
|
||||
|
||||
use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest};
|
||||
use crate::{
|
||||
core::{
|
||||
errors::{self, StorageErrorExt},
|
||||
payments::{
|
||||
self,
|
||||
cards::create_encrypted_data,
|
||||
helpers,
|
||||
operations::{self, ValidateStatusForOperation},
|
||||
},
|
||||
},
|
||||
db::{domain::types, errors::RouterResult},
|
||||
routes::{app::ReqState, SessionState},
|
||||
services,
|
||||
types::{
|
||||
api,
|
||||
domain::{self, types as domain_types},
|
||||
storage::{self, enums},
|
||||
},
|
||||
utils::{self, OptionExt},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PaymentAttemptRecord;
|
||||
|
||||
type PaymentsAttemptRecordOperation<'b, F> =
|
||||
BoxedOperation<'b, F, PaymentsAttemptRecordRequest, PaymentAttemptRecordData<F>>;
|
||||
|
||||
impl<F: Send + Clone + Sync> Operation<F, PaymentsAttemptRecordRequest> for &PaymentAttemptRecord {
|
||||
type Data = PaymentAttemptRecordData<F>;
|
||||
fn to_validate_request(
|
||||
&self,
|
||||
) -> RouterResult<
|
||||
&(dyn ValidateRequest<F, PaymentsAttemptRecordRequest, Self::Data> + Send + Sync),
|
||||
> {
|
||||
Ok(*self)
|
||||
}
|
||||
fn to_get_tracker(
|
||||
&self,
|
||||
) -> RouterResult<&(dyn GetTracker<F, Self::Data, PaymentsAttemptRecordRequest> + Send + Sync)>
|
||||
{
|
||||
Ok(*self)
|
||||
}
|
||||
fn to_domain(
|
||||
&self,
|
||||
) -> RouterResult<&(dyn Domain<F, PaymentsAttemptRecordRequest, Self::Data>)> {
|
||||
Ok(*self)
|
||||
}
|
||||
fn to_update_tracker(
|
||||
&self,
|
||||
) -> RouterResult<&(dyn UpdateTracker<F, Self::Data, PaymentsAttemptRecordRequest> + Send + Sync)>
|
||||
{
|
||||
Ok(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidateStatusForOperation for PaymentAttemptRecord {
|
||||
fn validate_status_for_operation(
|
||||
&self,
|
||||
intent_status: common_enums::IntentStatus,
|
||||
) -> Result<(), errors::ApiErrorResponse> {
|
||||
// need to verify this?
|
||||
match intent_status {
|
||||
// Payment attempt can be recorded for failed payment as well in revenue recovery flow.
|
||||
common_enums::IntentStatus::RequiresPaymentMethod
|
||||
| common_enums::IntentStatus::Failed => Ok(()),
|
||||
common_enums::IntentStatus::Succeeded
|
||||
| common_enums::IntentStatus::Cancelled
|
||||
| common_enums::IntentStatus::Processing
|
||||
| common_enums::IntentStatus::RequiresCustomerAction
|
||||
| common_enums::IntentStatus::RequiresMerchantAction
|
||||
| common_enums::IntentStatus::RequiresCapture
|
||||
| common_enums::IntentStatus::PartiallyCaptured
|
||||
| common_enums::IntentStatus::RequiresConfirmation
|
||||
| common_enums::IntentStatus::PartiallyCapturedAndCapturable => {
|
||||
Err(errors::ApiErrorResponse::PaymentUnexpectedState {
|
||||
current_flow: format!("{self:?}"),
|
||||
field_name: "status".to_string(),
|
||||
current_value: intent_status.to_string(),
|
||||
states: [
|
||||
common_enums::IntentStatus::RequiresPaymentMethod,
|
||||
common_enums::IntentStatus::Failed,
|
||||
]
|
||||
.map(|enum_value| enum_value.to_string())
|
||||
.join(", "),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<F: Send + Clone + Sync>
|
||||
GetTracker<F, PaymentAttemptRecordData<F>, PaymentsAttemptRecordRequest>
|
||||
for PaymentAttemptRecord
|
||||
{
|
||||
#[instrument(skip_all)]
|
||||
async fn get_trackers<'a>(
|
||||
&'a self,
|
||||
state: &'a SessionState,
|
||||
payment_id: &common_utils::id_type::GlobalPaymentId,
|
||||
request: &PaymentsAttemptRecordRequest,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
_profile: &domain::Profile,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
_header_payload: &hyperswitch_domain_models::payments::HeaderPayload,
|
||||
_platform_merchant_account: Option<&domain::MerchantAccount>,
|
||||
) -> RouterResult<operations::GetTrackerResponse<PaymentAttemptRecordData<F>>> {
|
||||
let db = &*state.store;
|
||||
let key_manager_state = &state.into();
|
||||
|
||||
let storage_scheme = merchant_account.storage_scheme;
|
||||
|
||||
let payment_intent = db
|
||||
.find_payment_intent_by_id(key_manager_state, payment_id, key_store, storage_scheme)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
|
||||
self.validate_status_for_operation(payment_intent.status)?;
|
||||
let payment_method_billing_address = request
|
||||
.payment_method_data
|
||||
.as_ref()
|
||||
.and_then(|data| {
|
||||
data.billing
|
||||
.as_ref()
|
||||
.map(|address| address.clone().encode_to_value())
|
||||
})
|
||||
.transpose()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to encode payment_method_billing address")?
|
||||
.map(masking::Secret::new);
|
||||
|
||||
let batch_encrypted_data = domain_types::crypto_operation(
|
||||
key_manager_state,
|
||||
common_utils::type_name!(hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt),
|
||||
domain_types::CryptoOperation::BatchEncrypt(
|
||||
hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt::to_encryptable(
|
||||
hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt {
|
||||
payment_method_billing_address,
|
||||
},
|
||||
),
|
||||
),
|
||||
common_utils::types::keymanager::Identifier::Merchant(merchant_account.get_id().to_owned()),
|
||||
key_store.key.peek(),
|
||||
)
|
||||
.await
|
||||
.and_then(|val| val.try_into_batchoperation())
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed while encrypting payment intent details".to_string())?;
|
||||
|
||||
let encrypted_data =
|
||||
hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt::from_encryptable(batch_encrypted_data)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed while encrypting payment intent details")?;
|
||||
let cell_id = state.conf.cell_information.id.clone();
|
||||
|
||||
let payment_attempt_domain_model =
|
||||
hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt::create_domain_model_using_record_request(
|
||||
&payment_intent,
|
||||
cell_id,
|
||||
storage_scheme,
|
||||
request,
|
||||
encrypted_data,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let payment_attempt = db
|
||||
.insert_payment_attempt(
|
||||
key_manager_state,
|
||||
key_store,
|
||||
payment_attempt_domain_model,
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not insert payment attempt")?;
|
||||
let revenue_recovery_data = hyperswitch_domain_models::payments::RevenueRecoveryData {
|
||||
billing_connector_id: request.billing_connector_id.clone(),
|
||||
processor_payment_method_token: request.processor_payment_method_token.clone(),
|
||||
connector_customer_id: request.connector_customer_id.clone(),
|
||||
};
|
||||
|
||||
let payment_data = PaymentAttemptRecordData {
|
||||
flow: PhantomData,
|
||||
payment_intent,
|
||||
payment_attempt,
|
||||
revenue_recovery_data,
|
||||
};
|
||||
|
||||
let get_trackers_response = operations::GetTrackerResponse { payment_data };
|
||||
|
||||
Ok(get_trackers_response)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<F: Clone + Sync> UpdateTracker<F, PaymentAttemptRecordData<F>, PaymentsAttemptRecordRequest>
|
||||
for PaymentAttemptRecord
|
||||
{
|
||||
#[instrument(skip_all)]
|
||||
async fn update_trackers<'b>(
|
||||
&'b self,
|
||||
state: &'b SessionState,
|
||||
_req_state: ReqState,
|
||||
mut payment_data: PaymentAttemptRecordData<F>,
|
||||
_customer: Option<domain::Customer>,
|
||||
storage_scheme: enums::MerchantStorageScheme,
|
||||
_updated_customer: Option<storage::CustomerUpdate>,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
_frm_suggestion: Option<FrmSuggestion>,
|
||||
_header_payload: hyperswitch_domain_models::payments::HeaderPayload,
|
||||
) -> RouterResult<(
|
||||
PaymentsAttemptRecordOperation<'b, F>,
|
||||
PaymentAttemptRecordData<F>,
|
||||
)>
|
||||
where
|
||||
F: 'b + Send,
|
||||
{
|
||||
let feature_metadata = payment_data.get_updated_feature_metadata()?;
|
||||
let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::RecordUpdate
|
||||
{
|
||||
status: common_enums::IntentStatus::from(payment_data.payment_attempt.status),
|
||||
feature_metadata: Box::new(feature_metadata),
|
||||
active_attempt_id: payment_data.payment_attempt.id.clone(),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
};
|
||||
payment_data.payment_intent = state
|
||||
.store
|
||||
.update_payment_intent(
|
||||
&state.into(),
|
||||
payment_data.payment_intent,
|
||||
payment_intent_update,
|
||||
key_store,
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
Ok((Box::new(self), payment_data))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Send + Clone> ValidateRequest<F, PaymentsAttemptRecordRequest, PaymentAttemptRecordData<F>>
|
||||
for PaymentAttemptRecord
|
||||
{
|
||||
#[instrument(skip_all)]
|
||||
fn validate_request<'a, 'b>(
|
||||
&'b self,
|
||||
_request: &PaymentsAttemptRecordRequest,
|
||||
merchant_account: &'a domain::MerchantAccount,
|
||||
) -> RouterResult<operations::ValidateResult> {
|
||||
Ok(operations::ValidateResult {
|
||||
merchant_id: merchant_account.get_id().to_owned(),
|
||||
storage_scheme: merchant_account.storage_scheme,
|
||||
requeue: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<F: Clone + Send + Sync> Domain<F, PaymentsAttemptRecordRequest, PaymentAttemptRecordData<F>>
|
||||
for PaymentAttemptRecord
|
||||
{
|
||||
#[instrument(skip_all)]
|
||||
async fn get_customer_details<'a>(
|
||||
&'a self,
|
||||
_state: &SessionState,
|
||||
_payment_data: &mut PaymentAttemptRecordData<F>,
|
||||
_merchant_key_store: &domain::MerchantKeyStore,
|
||||
_storage_scheme: enums::MerchantStorageScheme,
|
||||
) -> CustomResult<
|
||||
(
|
||||
BoxedOperation<'a, F, PaymentsAttemptRecordRequest, PaymentAttemptRecordData<F>>,
|
||||
Option<domain::Customer>,
|
||||
),
|
||||
errors::StorageError,
|
||||
> {
|
||||
Ok((Box::new(self), None))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn make_pm_data<'a>(
|
||||
&'a self,
|
||||
_state: &'a SessionState,
|
||||
_payment_data: &mut PaymentAttemptRecordData<F>,
|
||||
_storage_scheme: enums::MerchantStorageScheme,
|
||||
_merchant_key_store: &domain::MerchantKeyStore,
|
||||
_customer: &Option<domain::Customer>,
|
||||
_business_profile: &domain::Profile,
|
||||
_should_retry_with_pan: bool,
|
||||
) -> RouterResult<(
|
||||
PaymentsAttemptRecordOperation<'a, F>,
|
||||
Option<domain::PaymentMethodData>,
|
||||
Option<String>,
|
||||
)> {
|
||||
Ok((Box::new(self), None, None))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn perform_routing<'a>(
|
||||
&'a self,
|
||||
_merchant_account: &domain::MerchantAccount,
|
||||
_business_profile: &domain::Profile,
|
||||
_state: &SessionState,
|
||||
_payment_data: &mut PaymentAttemptRecordData<F>,
|
||||
_mechant_key_store: &domain::MerchantKeyStore,
|
||||
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
|
||||
Ok(api::ConnectorCallType::Skip)
|
||||
}
|
||||
}
|
||||
@ -1796,6 +1796,30 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
impl<F> GenerateResponse<api_models::payments::PaymentAttemptResponse>
|
||||
for hyperswitch_domain_models::payments::PaymentAttemptRecordData<F>
|
||||
where
|
||||
F: Clone,
|
||||
{
|
||||
fn generate_response(
|
||||
self,
|
||||
_state: &SessionState,
|
||||
_connector_http_status_code: Option<u16>,
|
||||
_external_latency: Option<u128>,
|
||||
_is_latency_header_enabled: Option<bool>,
|
||||
_merchant_account: &domain::MerchantAccount,
|
||||
_profile: &domain::Profile,
|
||||
) -> RouterResponse<api_models::payments::PaymentAttemptResponse> {
|
||||
let payment_attempt = self.payment_attempt;
|
||||
let response = api_models::payments::PaymentAttemptResponse::foreign_from(&payment_attempt);
|
||||
Ok(services::ApplicationResponse::JsonWithHeaders((
|
||||
response,
|
||||
vec![],
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
impl<F, Op, D> ToResponse<F, D, Op> for api::PaymentsPostSessionTokensResponse
|
||||
where
|
||||
|
||||
@ -364,6 +364,7 @@ async fn incoming_webhooks_core<W: types::OutgoingWebhookType>(
|
||||
&request_details,
|
||||
event_type,
|
||||
req_state,
|
||||
merchant_connector_account,
|
||||
))
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use api_models::webhooks;
|
||||
use api_models::{payments as api_payments, webhooks};
|
||||
use common_utils::ext_traits::AsyncExt;
|
||||
use error_stack::{report, ResultExt};
|
||||
use hyperswitch_domain_models::revenue_recovery;
|
||||
@ -29,6 +29,7 @@ pub async fn recovery_incoming_webhook_flow(
|
||||
request_details: &hyperswitch_interfaces::webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
event_type: webhooks::IncomingWebhookEvent,
|
||||
req_state: ReqState,
|
||||
billing_connector_account: domain::MerchantConnectorAccount,
|
||||
) -> CustomResult<webhooks::WebhookResponseTracker, errors::RevenueRecoveryError> {
|
||||
// Source verification is necessary for revenue recovery webhooks flow since We don't have payment intent/attempt object created before in our system.
|
||||
|
||||
@ -79,17 +80,43 @@ pub async fn recovery_incoming_webhook_flow(
|
||||
)
|
||||
.change_context(errors::RevenueRecoveryError::TransactionWebhookProcessingFailed)?,
|
||||
);
|
||||
|
||||
invoice_transaction_details
|
||||
.get_payment_attempt(
|
||||
// Find the payment merchant connector ID at the top level to avoid multiple DB calls.
|
||||
let payment_merchant_connector_account = invoice_transaction_details
|
||||
.find_payment_merchant_connector_account(
|
||||
&state,
|
||||
&req_state,
|
||||
&merchant_account,
|
||||
&business_profile,
|
||||
&key_store,
|
||||
payment_intent.payment_id.clone(),
|
||||
&billing_connector_account,
|
||||
)
|
||||
.await?
|
||||
.await?;
|
||||
|
||||
Some(
|
||||
invoice_transaction_details
|
||||
.get_payment_attempt(
|
||||
&state,
|
||||
&req_state,
|
||||
&merchant_account,
|
||||
&business_profile,
|
||||
&key_store,
|
||||
payment_intent.payment_id.clone(),
|
||||
)
|
||||
.await
|
||||
.transpose()
|
||||
.async_unwrap_or_else(|| async {
|
||||
invoice_transaction_details
|
||||
.record_payment_attempt(
|
||||
&state,
|
||||
&req_state,
|
||||
&merchant_account,
|
||||
&business_profile,
|
||||
&key_store,
|
||||
payment_intent.payment_id.clone(),
|
||||
&billing_connector_account.id,
|
||||
payment_merchant_connector_account,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
@ -189,13 +216,13 @@ impl RevenueRecoveryInvoice {
|
||||
profile: &domain::Profile,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
) -> CustomResult<revenue_recovery::RecoveryPaymentIntent, errors::RevenueRecoveryError> {
|
||||
let payload = api_models::payments::PaymentsCreateIntentRequest::from(&self.0);
|
||||
let payload = api_payments::PaymentsCreateIntentRequest::from(&self.0);
|
||||
let global_payment_id =
|
||||
common_utils::id_type::GlobalPaymentId::generate(&state.conf.cell_information.id);
|
||||
|
||||
let create_intent_response = Box::pin(payments::payments_intent_core::<
|
||||
hyperswitch_domain_models::router_flow_types::payments::PaymentCreateIntent,
|
||||
api_models::payments::PaymentsIntentResponse,
|
||||
api_payments::PaymentsIntentResponse,
|
||||
_,
|
||||
_,
|
||||
hyperswitch_domain_models::payments::PaymentIntentData<
|
||||
@ -242,7 +269,7 @@ impl RevenueRecoveryAttempt {
|
||||
{
|
||||
let attempt_response = Box::pin(payments::payments_core::<
|
||||
hyperswitch_domain_models::router_flow_types::payments::PSync,
|
||||
api_models::payments::PaymentsResponse,
|
||||
api_payments::PaymentsResponse,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
@ -256,7 +283,7 @@ impl RevenueRecoveryAttempt {
|
||||
profile.clone(),
|
||||
key_store.clone(),
|
||||
payments::operations::PaymentGet,
|
||||
api_models::payments::PaymentsRetrieveRequest {
|
||||
api_payments::PaymentsRetrieveRequest {
|
||||
force_sync: false,
|
||||
expand_attempts: true,
|
||||
param: None,
|
||||
@ -286,25 +313,122 @@ impl RevenueRecoveryAttempt {
|
||||
});
|
||||
Ok(payment_attempt)
|
||||
}
|
||||
Ok(_) => Err(errors::RevenueRecoveryError::PaymentIntentFetchFailed)
|
||||
Ok(_) => Err(errors::RevenueRecoveryError::PaymentAttemptFetchFailed)
|
||||
.attach_printable("Unexpected response from payment intent core"),
|
||||
error @ Err(_) => {
|
||||
router_env::logger::error!(?error);
|
||||
Err(errors::RevenueRecoveryError::PaymentIntentFetchFailed)
|
||||
.attach_printable("failed to fetch payment intent recovery webhook flow")
|
||||
Err(errors::RevenueRecoveryError::PaymentAttemptFetchFailed)
|
||||
.attach_printable("failed to fetch payment attempt in recovery webhook flow")
|
||||
}
|
||||
}?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn record_payment_attempt(
|
||||
&self,
|
||||
_state: &SessionState,
|
||||
_req_state: &ReqState,
|
||||
_merchant_account: &domain::MerchantAccount,
|
||||
_profile: &domain::Profile,
|
||||
_key_store: &domain::MerchantKeyStore,
|
||||
_payment_id: common_utils::id_type::GlobalPaymentId,
|
||||
state: &SessionState,
|
||||
req_state: &ReqState,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
profile: &domain::Profile,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
payment_id: common_utils::id_type::GlobalPaymentId,
|
||||
billing_connector_account_id: &common_utils::id_type::MerchantConnectorAccountId,
|
||||
payment_connector_account: Option<domain::MerchantConnectorAccount>,
|
||||
) -> CustomResult<revenue_recovery::RecoveryPaymentAttempt, errors::RevenueRecoveryError> {
|
||||
todo!()
|
||||
let request_payload = self
|
||||
.create_payment_record_request(billing_connector_account_id, payment_connector_account);
|
||||
let attempt_response = Box::pin(payments::record_attempt_core(
|
||||
state.clone(),
|
||||
req_state.clone(),
|
||||
merchant_account.clone(),
|
||||
profile.clone(),
|
||||
key_store.clone(),
|
||||
request_payload,
|
||||
payment_id.clone(),
|
||||
hyperswitch_domain_models::payments::HeaderPayload::default(),
|
||||
None,
|
||||
))
|
||||
.await;
|
||||
|
||||
let response = match attempt_response {
|
||||
Ok(services::ApplicationResponse::JsonWithHeaders((attempt_response, _))) => {
|
||||
Ok(revenue_recovery::RecoveryPaymentAttempt {
|
||||
attempt_id: attempt_response.id,
|
||||
attempt_status: attempt_response.status,
|
||||
feature_metadata: attempt_response.feature_metadata,
|
||||
})
|
||||
}
|
||||
Ok(_) => Err(errors::RevenueRecoveryError::PaymentAttemptFetchFailed)
|
||||
.attach_printable("Unexpected response from record attempt core"),
|
||||
error @ Err(_) => {
|
||||
router_env::logger::error!(?error);
|
||||
Err(errors::RevenueRecoveryError::PaymentAttemptFetchFailed)
|
||||
.attach_printable("failed to record attempt in recovery webhook flow")
|
||||
}
|
||||
}?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn create_payment_record_request(
|
||||
&self,
|
||||
billing_merchant_connector_account_id: &common_utils::id_type::MerchantConnectorAccountId,
|
||||
payment_merchant_connector_account: Option<domain::MerchantConnectorAccount>,
|
||||
) -> api_payments::PaymentsAttemptRecordRequest {
|
||||
let amount_details = api_payments::PaymentAttemptAmountDetails::from(&self.0);
|
||||
let feature_metadata = api_payments::PaymentAttemptFeatureMetadata {
|
||||
revenue_recovery: Some(api_payments::PaymentAttemptRevenueRecoveryData {
|
||||
// Since we are recording the external paymenmt attempt, this is hardcoded to External
|
||||
attempt_triggered_by: common_enums::TriggeredBy::External,
|
||||
}),
|
||||
};
|
||||
let error = Option::<api_payments::RecordAttemptErrorDetails>::from(&self.0);
|
||||
api_payments::PaymentsAttemptRecordRequest {
|
||||
amount_details,
|
||||
status: self.0.status,
|
||||
billing: None,
|
||||
shipping: None,
|
||||
connector : payment_merchant_connector_account.as_ref().map(|account| account.connector_name),
|
||||
payment_merchant_connector_id: payment_merchant_connector_account.as_ref().map(|account: &hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccount| account.id.clone()),
|
||||
error,
|
||||
description: None,
|
||||
connector_transaction_id: self.0.connector_transaction_id.clone(),
|
||||
payment_method_type: self.0.payment_method_type,
|
||||
billing_connector_id: billing_merchant_connector_account_id.clone(),
|
||||
payment_method_subtype: self.0.payment_method_sub_type,
|
||||
payment_method_data: None,
|
||||
metadata: None,
|
||||
feature_metadata: Some(feature_metadata),
|
||||
transaction_created_at: self.0.transaction_created_at,
|
||||
processor_payment_method_token: self.0.processor_payment_method_token.clone(),
|
||||
connector_customer_id: self.0.connector_customer_id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_payment_merchant_connector_account(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
billing_connector_account: &domain::MerchantConnectorAccount,
|
||||
) -> CustomResult<Option<domain::MerchantConnectorAccount>, errors::RevenueRecoveryError> {
|
||||
let payment_merchant_connector_account_id = billing_connector_account
|
||||
.get_payment_merchant_connector_account_id_using_account_reference_id(
|
||||
self.0.connector_account_reference_id.clone(),
|
||||
);
|
||||
let db = &*state.store;
|
||||
let key_manager_state = &(state).into();
|
||||
let payment_merchant_connector_account = payment_merchant_connector_account_id
|
||||
.as_ref()
|
||||
.async_map(|mca_id| async move {
|
||||
db.find_merchant_connector_account_by_id(key_manager_state, mca_id, key_store)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.transpose()
|
||||
.change_context(errors::RevenueRecoveryError::PaymentMerchantConnectorAccountNotFound)
|
||||
.attach_printable(
|
||||
"failed to fetch payment merchant connector id using account reference id",
|
||||
)?;
|
||||
Ok(payment_merchant_connector_account)
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ pub use hyperswitch_domain_models::router_flow_types::payments::{
|
||||
Approve, Authorize, AuthorizeSessionToken, Balance, CalculateTax, Capture, CompleteAuthorize,
|
||||
CreateConnectorCustomer, IncrementalAuthorization, InitPayment, PSync, PaymentCreateIntent,
|
||||
PaymentGetIntent, PaymentMethodToken, PaymentUpdateIntent, PostProcessing, PostSessionTokens,
|
||||
PreProcessing, Reject, SdkSessionUpdate, Session, SetupMandate, Void,
|
||||
PreProcessing, RecordAttempt, Reject, SdkSessionUpdate, Session, SetupMandate, Void,
|
||||
};
|
||||
pub use hyperswitch_interfaces::api::payments::{
|
||||
ConnectorCustomer, MandateSetup, Payment, PaymentApprove, PaymentAuthorize,
|
||||
|
||||
Reference in New Issue
Block a user