feat(core): update card_details for an existing mandate (#3452)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Amisha Prabhat
2024-01-30 12:35:42 +05:30
committed by GitHub
parent c9d41e2169
commit 02074dfc23
22 changed files with 451 additions and 72 deletions

View File

@ -753,6 +753,7 @@ impl ForeignTryFrom<(Option<MandateData>, Option<String>)> for Option<payments::
user_agent: online.user_agent,
}),
}),
update_mandate_id: None,
});
Ok(mandate_data)
}

View File

@ -1,13 +1,13 @@
pub mod helpers;
pub mod utils;
use api_models::payments;
use common_utils::{ext_traits::Encode, pii};
use diesel_models::enums as storage_enums;
use diesel_models::{enums as storage_enums, Mandate};
use error_stack::{report, IntoReport, ResultExt};
use futures::future;
use router_env::{instrument, logger, tracing};
use super::payments::helpers;
use super::payments::helpers as payment_helper;
use crate::{
core::{
errors::{self, RouterResponse, StorageErrorExt},
@ -64,34 +64,17 @@ pub async fn revoke_mandate(
common_enums::MandateStatus::Active
| common_enums::MandateStatus::Inactive
| common_enums::MandateStatus::Pending => {
let profile_id = if let Some(ref payment_id) = mandate.original_payment_id {
let pi = db
.find_payment_intent_by_payment_id_merchant_id(
payment_id,
&merchant_account.merchant_id,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::PaymentNotFound)?;
let profile_id = pi.profile_id.clone().ok_or(
errors::ApiErrorResponse::BusinessProfileNotFound {
id: pi
.profile_id
.unwrap_or_else(|| "Profile id is Null".to_string()),
},
)?;
Ok(profile_id)
} else {
Err(errors::ApiErrorResponse::PaymentNotFound)
}?;
let profile_id =
helpers::get_profile_id_for_mandate(&state, &merchant_account, mandate.clone())
.await?;
let merchant_connector_account = helpers::get_merchant_connector_account(
let merchant_connector_account = payment_helper::get_merchant_connector_account(
&state,
&merchant_account.merchant_id,
None,
&key_store,
&profile_id,
&mandate.connector,
&mandate.connector.clone(),
mandate.merchant_connector_id.as_ref(),
)
.await?;
@ -243,7 +226,72 @@ where
_ => Some(router_data.request.get_payment_method_data()),
}
}
pub async fn update_mandate_procedure<F, FData>(
state: &AppState,
resp: types::RouterData<F, FData, types::PaymentsResponseData>,
mandate: Mandate,
merchant_id: &str,
pm_id: Option<String>,
) -> errors::RouterResult<types::RouterData<F, FData, types::PaymentsResponseData>>
where
FData: MandateBehaviour,
{
let mandate_details = match &resp.response {
Ok(types::PaymentsResponseData::TransactionResponse {
mandate_reference, ..
}) => mandate_reference,
Ok(_) => Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Unexpected response received")?,
Err(_) => return Ok(resp),
};
let old_record = payments::UpdateHistory {
connector_mandate_id: mandate.connector_mandate_id,
payment_method_id: mandate.payment_method_id,
original_payment_id: mandate.original_payment_id,
};
let mandate_ref = mandate
.connector_mandate_ids
.parse_value::<payments::ConnectorMandateReferenceId>("Connector Reference Id")
.change_context(errors::ApiErrorResponse::MandateDeserializationFailed)?;
let mut update_history = mandate_ref.update_history.unwrap_or_default();
update_history.push(old_record);
let updated_mandate_ref = payments::ConnectorMandateReferenceId {
connector_mandate_id: mandate_details
.as_ref()
.and_then(|mandate_ref| mandate_ref.connector_mandate_id.clone()),
payment_method_id: pm_id.clone(),
update_history: Some(update_history),
};
let connector_mandate_ids =
Encode::<types::MandateReference>::encode_to_value(&updated_mandate_ref)
.change_context(errors::ApiErrorResponse::InternalServerError)
.map(masking::Secret::new)?;
let _update_mandate_details = state
.store
.update_mandate_by_merchant_id_mandate_id(
merchant_id,
&mandate.mandate_id,
diesel_models::MandateUpdate::ConnectorMandateIdUpdate {
connector_mandate_id: mandate_details
.as_ref()
.and_then(|man_ref| man_ref.connector_mandate_id.clone()),
connector_mandate_ids: Some(connector_mandate_ids),
payment_method_id: pm_id
.unwrap_or("Error retrieving the payment_method_id".to_string()),
original_payment_id: Some(resp.payment_id.clone()),
},
)
.await
.change_context(errors::ApiErrorResponse::MandateUpdateFailed)?;
Ok(resp)
}
pub async fn mandate_procedure<F, FData>(
state: &AppState,
mut resp: types::RouterData<F, FData, types::PaymentsResponseData>,
@ -324,7 +372,7 @@ where
})
.transpose()?;
if let Some(new_mandate_data) = helpers::generate_mandate(
if let Some(new_mandate_data) = payment_helper::generate_mandate(
resp.merchant_id.clone(),
resp.payment_id.clone(),
resp.connector.clone(),
@ -363,6 +411,8 @@ where
api_models::payments::ConnectorMandateReferenceId {
connector_mandate_id: connector_id.connector_mandate_id,
payment_method_id: connector_id.payment_method_id,
update_history:None,
}
)))
}));

View File

@ -0,0 +1,35 @@
use common_utils::errors::CustomResult;
use diesel_models::Mandate;
use error_stack::ResultExt;
use crate::{core::errors, routes::AppState, types::domain};
pub async fn get_profile_id_for_mandate(
state: &AppState,
merchant_account: &domain::MerchantAccount,
mandate: Mandate,
) -> CustomResult<String, errors::ApiErrorResponse> {
let profile_id = if let Some(ref payment_id) = mandate.original_payment_id {
let pi = state
.store
.find_payment_intent_by_payment_id_merchant_id(
payment_id,
&merchant_account.merchant_id,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::PaymentNotFound)?;
let profile_id =
pi.profile_id
.clone()
.ok_or(errors::ApiErrorResponse::BusinessProfileNotFound {
id: pi
.profile_id
.unwrap_or_else(|| "Profile id is Null".to_string()),
})?;
Ok(profile_id)
} else {
Err(errors::ApiErrorResponse::PaymentNotFound)
}?;
Ok(profile_id)
}

View File

@ -1823,14 +1823,24 @@ pub async fn list_payment_methods(
} else {
api_surcharge_decision_configs::MerchantSurchargeConfigs::default()
};
print!("PAMT{:?}", payment_attempt);
Ok(services::ApplicationResponse::Json(
api::PaymentMethodListResponse {
redirect_url: merchant_account.return_url,
merchant_name: merchant_account.merchant_name,
payment_type,
payment_methods: payment_method_responses,
mandate_payment: payment_attempt.and_then(|inner| inner.mandate_details).map(
|d| match d {
mandate_payment: payment_attempt
.and_then(|inner| inner.mandate_details)
.and_then(|man_type_details| match man_type_details {
data_models::mandates::MandateTypeDetails::MandateType(mandate_type) => {
Some(mandate_type)
}
data_models::mandates::MandateTypeDetails::MandateDetails(mandate_details) => {
mandate_details.mandate_type
}
})
.map(|d| match d {
data_models::mandates::MandateDataType::SingleUse(i) => {
api::MandateType::SingleUse(api::MandateAmountData {
amount: i.amount,
@ -1852,8 +1862,7 @@ pub async fn list_payment_methods(
data_models::mandates::MandateDataType::MultiUse(None) => {
api::MandateType::MultiUse(None)
}
},
),
}),
show_surcharge_breakup_screen: merchant_surcharge_configs
.show_surcharge_breakup_screen
.unwrap_or_default(),

View File

@ -1,9 +1,10 @@
use async_trait::async_trait;
use error_stack::{IntoReport, ResultExt};
use super::{ConstructFlowSpecificData, Feature};
use crate::{
core::{
errors::{self, ConnectorErrorExt, RouterResult},
errors::{self, ConnectorErrorExt, RouterResult, StorageErrorExt},
mandate,
payments::{
self, access_token, customers, helpers, tokenization, transformers, PaymentData,
@ -65,16 +66,16 @@ impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::Setup
types::SetupMandateRequestData,
types::PaymentsResponseData,
> = connector.connector.get_connector_integration();
let resp = services::execute_connector_processing_step(
state,
connector_integration,
&self,
call_connector_action,
call_connector_action.clone(),
connector_request,
)
.await
.to_setup_mandate_failed_response()?;
let pm_id = Box::pin(tokenization::save_payment_method(
state,
connector,
@ -86,14 +87,84 @@ impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::Setup
))
.await?;
mandate::mandate_procedure(
state,
resp,
maybe_customer,
pm_id,
connector.merchant_connector_id.clone(),
)
.await
if let Some(mandate_id) = self
.request
.setup_mandate_details
.as_ref()
.and_then(|mandate_data| mandate_data.update_mandate_id.clone())
{
let mandate = state
.store
.find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, &mandate_id)
.await
.to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?;
let profile_id = mandate::helpers::get_profile_id_for_mandate(
state,
merchant_account,
mandate.clone(),
)
.await?;
match resp.response {
Ok(types::PaymentsResponseData::TransactionResponse { .. }) => {
let connector_integration: services::BoxedConnectorIntegration<
'_,
types::api::MandateRevoke,
types::MandateRevokeRequestData,
types::MandateRevokeResponseData,
> = connector.connector.get_connector_integration();
let merchant_connector_account = helpers::get_merchant_connector_account(
state,
&merchant_account.merchant_id,
None,
key_store,
&profile_id,
&mandate.connector,
mandate.merchant_connector_id.as_ref(),
)
.await?;
let router_data = mandate::utils::construct_mandate_revoke_router_data(
merchant_connector_account,
merchant_account,
mandate.clone(),
)
.await?;
let _response = services::execute_connector_processing_step(
state,
connector_integration,
&router_data,
call_connector_action,
None,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
// TODO:Add the revoke mandate task to process tracker
mandate::update_mandate_procedure(
state,
resp,
mandate,
&merchant_account.merchant_id,
pm_id,
)
.await
}
Ok(_) => Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Unexpected response received")?,
Err(_) => Ok(resp),
}
} else {
mandate::mandate_procedure(
state,
resp,
maybe_customer,
pm_id,
connector.merchant_connector_id.clone(),
)
.await
}
}
async fn add_access_token<'a>(

View File

@ -840,7 +840,7 @@ fn validate_new_mandate_request(
let mandate_details = match mandate_data.mandate_type {
Some(api_models::payments::MandateType::SingleUse(details)) => Some(details),
Some(api_models::payments::MandateType::MultiUse(details)) => details,
None => None,
_ => None,
};
mandate_details.and_then(|md| md.start_date.zip(md.end_date)).map(|(start_date, end_date)|
utils::when (start_date >= end_date, || {

View File

@ -431,7 +431,29 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
// The operation merges mandate data from both request and payment_attempt
setup_mandate = setup_mandate.map(|mut sm| {
sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type);
sm.mandate_type = payment_attempt
.mandate_details
.clone()
.and_then(|mandate| match mandate {
data_models::mandates::MandateTypeDetails::MandateType(mandate_type) => {
Some(mandate_type)
}
data_models::mandates::MandateTypeDetails::MandateDetails(mandate_details) => {
mandate_details.mandate_type
}
})
.or(sm.mandate_type);
sm.update_mandate_id = payment_attempt
.mandate_details
.clone()
.and_then(|mandate| match mandate {
data_models::mandates::MandateTypeDetails::MandateType(_) => None,
data_models::mandates::MandateTypeDetails::MandateDetails(update_id) => {
Some(update_id.update_mandate_id)
}
})
.flatten()
.or(sm.update_mandate_id);
sm
});

View File

@ -3,7 +3,10 @@ use std::marker::PhantomData;
use api_models::enums::FrmSuggestion;
use async_trait::async_trait;
use common_utils::ext_traits::{AsyncExt, Encode, ValueExt};
use data_models::{mandates::MandateData, payments::payment_attempt::PaymentAttempt};
use data_models::{
mandates::{MandateData, MandateDetails, MandateTypeDetails},
payments::payment_attempt::PaymentAttempt,
};
use diesel_models::ephemeral_key;
use error_stack::{self, ResultExt};
use masking::PeekInterface;
@ -255,7 +258,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
.to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment {
payment_id: payment_id.clone(),
})?;
// connector mandate reference update history
let mandate_id = request
.mandate_id
.as_ref()
@ -284,10 +287,11 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
api_models::payments::MandateIds {
mandate_id: mandate_obj.mandate_id,
mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId(
api_models::payments::ConnectorMandateReferenceId {
connector_mandate_id: connector_id.connector_mandate_id,
payment_method_id: connector_id.payment_method_id,
},
api_models::payments::ConnectorMandateReferenceId{
connector_mandate_id: connector_id.connector_mandate_id,
payment_method_id: connector_id.payment_method_id,
update_history: None
}
))
}
}),
@ -701,6 +705,35 @@ impl PaymentCreate {
.surcharge_details
.and_then(|surcharge_details| surcharge_details.tax_amount);
if request.mandate_data.as_ref().map_or(false, |mandate_data| {
mandate_data.update_mandate_id.is_some() && mandate_data.mandate_type.is_some()
}) {
Err(errors::ApiErrorResponse::InvalidRequestData {message:"Only one field out of 'mandate_type' and 'update_mandate_id' was expected, found both".to_string()})?
}
let mandate_dets = if let Some(update_id) = request
.mandate_data
.as_ref()
.and_then(|inner| inner.update_mandate_id.clone())
{
let mandate_data = MandateDetails {
update_mandate_id: Some(update_id),
mandate_type: None,
};
Some(MandateTypeDetails::MandateDetails(mandate_data))
} else {
// let mandate_type: data_models::mandates::MandateDataType =
let mandate_data = MandateDetails {
update_mandate_id: None,
mandate_type: request
.mandate_data
.as_ref()
.and_then(|inner| inner.mandate_type.clone().map(Into::into)),
};
Some(MandateTypeDetails::MandateDetails(mandate_data))
};
Ok((
storage::PaymentAttemptNew {
payment_id: payment_id.to_string(),
@ -727,10 +760,7 @@ impl PaymentCreate {
business_sub_label: request.business_sub_label.clone(),
surcharge_amount,
tax_amount,
mandate_details: request
.mandate_data
.as_ref()
.and_then(|inner| inner.mandate_type.clone().map(Into::into)),
mandate_details: mandate_dets,
..storage::PaymentAttemptNew::default()
},
additional_pm_data,

View File

@ -255,10 +255,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
api_models::payments::MandateIds {
mandate_id: mandate_obj.mandate_id,
mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId(
api_models::payments::ConnectorMandateReferenceId {
connector_mandate_id: connector_id.connector_mandate_id,
payment_method_id: connector_id.payment_method_id,
},
api_models::payments::ConnectorMandateReferenceId {connector_mandate_id:connector_id.connector_mandate_id,payment_method_id:connector_id.payment_method_id, update_history: None },
))
}
}),

View File

@ -636,6 +636,7 @@ where
api::MandateType::MultiUse(None)
}
}),
update_mandate_id: d.update_mandate_id,
}),
auth_flow == services::AuthFlow::Merchant,
)

View File

@ -202,6 +202,18 @@ impl MandateInterface for MockDb {
} => {
mandate.connector_mandate_ids = connector_mandate_ids;
}
diesel_models::MandateUpdate::ConnectorMandateIdUpdate {
connector_mandate_id,
connector_mandate_ids,
payment_method_id,
original_payment_id,
} => {
mandate.connector_mandate_ids = connector_mandate_ids;
mandate.connector_mandate_id = connector_mandate_id;
mandate.payment_method_id = payment_method_id;
mandate.original_payment_id = original_payment_id
}
}
Ok(mandate.clone())
}

View File

@ -1674,7 +1674,7 @@ pub fn build_redirection_form(
// Initialize the ThreeDSService
const threeDS = gateway.get3DSecure();
const options = {{
customerVaultId: '{customer_vault_id}',
currency: '{currency}',

View File

@ -323,6 +323,7 @@ impl ForeignFrom<api_models::payments::MandateData> for data_models::mandates::M
data_models::mandates::MandateDataType::MultiUse(None)
}
}),
update_mandate_id: d.update_mandate_id,
}
}
}