mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 19:42:27 +08:00
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
490 lines
20 KiB
Rust
490 lines
20 KiB
Rust
pub mod helpers;
|
|
pub mod utils;
|
|
use api_models::payments;
|
|
use common_utils::{ext_traits::Encode, pii};
|
|
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 as payment_helper;
|
|
use crate::{
|
|
core::{
|
|
errors::{self, RouterResponse, StorageErrorExt},
|
|
payments::CallConnectorAction,
|
|
},
|
|
db::StorageInterface,
|
|
routes::{metrics, AppState},
|
|
services,
|
|
types::{
|
|
self,
|
|
api::{
|
|
customers,
|
|
mandates::{self, MandateResponseExt},
|
|
ConnectorData, GetToken,
|
|
},
|
|
domain, storage,
|
|
transformers::ForeignTryFrom,
|
|
},
|
|
utils::OptionExt,
|
|
};
|
|
|
|
#[instrument(skip(state))]
|
|
pub async fn get_mandate(
|
|
state: AppState,
|
|
merchant_account: domain::MerchantAccount,
|
|
key_store: domain::MerchantKeyStore,
|
|
req: mandates::MandateId,
|
|
) -> RouterResponse<mandates::MandateResponse> {
|
|
let mandate = state
|
|
.store
|
|
.as_ref()
|
|
.find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, &req.mandate_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?;
|
|
Ok(services::ApplicationResponse::Json(
|
|
mandates::MandateResponse::from_db_mandate(&state, key_store, mandate).await?,
|
|
))
|
|
}
|
|
|
|
#[instrument(skip(state))]
|
|
pub async fn revoke_mandate(
|
|
state: AppState,
|
|
merchant_account: domain::MerchantAccount,
|
|
key_store: domain::MerchantKeyStore,
|
|
req: mandates::MandateId,
|
|
) -> RouterResponse<mandates::MandateRevokedResponse> {
|
|
let db = state.store.as_ref();
|
|
let mandate = db
|
|
.find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, &req.mandate_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?;
|
|
|
|
let mandate_revoke_status = match mandate.mandate_status {
|
|
common_enums::MandateStatus::Active
|
|
| common_enums::MandateStatus::Inactive
|
|
| common_enums::MandateStatus::Pending => {
|
|
let profile_id =
|
|
helpers::get_profile_id_for_mandate(&state, &merchant_account, mandate.clone())
|
|
.await?;
|
|
|
|
let merchant_connector_account = payment_helper::get_merchant_connector_account(
|
|
&state,
|
|
&merchant_account.merchant_id,
|
|
None,
|
|
&key_store,
|
|
&profile_id,
|
|
&mandate.connector.clone(),
|
|
mandate.merchant_connector_id.as_ref(),
|
|
)
|
|
.await?;
|
|
|
|
let connector_data = ConnectorData::get_connector_by_name(
|
|
&state.conf.connectors,
|
|
&mandate.connector,
|
|
GetToken::Connector,
|
|
mandate.merchant_connector_id.clone(),
|
|
)?;
|
|
let connector_integration: services::BoxedConnectorIntegration<
|
|
'_,
|
|
types::api::MandateRevoke,
|
|
types::MandateRevokeRequestData,
|
|
types::MandateRevokeResponseData,
|
|
> = connector_data.connector.get_connector_integration();
|
|
|
|
let router_data = 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,
|
|
CallConnectorAction::Trigger,
|
|
None,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
|
|
|
match response.response {
|
|
Ok(_) => {
|
|
let update_mandate = db
|
|
.update_mandate_by_merchant_id_mandate_id(
|
|
&merchant_account.merchant_id,
|
|
&req.mandate_id,
|
|
storage::MandateUpdate::StatusUpdate {
|
|
mandate_status: storage::enums::MandateStatus::Revoked,
|
|
},
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?;
|
|
Ok(services::ApplicationResponse::Json(
|
|
mandates::MandateRevokedResponse {
|
|
mandate_id: update_mandate.mandate_id,
|
|
status: update_mandate.mandate_status,
|
|
error_code: None,
|
|
error_message: None,
|
|
},
|
|
))
|
|
}
|
|
|
|
Err(err) => Err(errors::ApiErrorResponse::ExternalConnectorError {
|
|
code: err.code,
|
|
message: err.message,
|
|
connector: mandate.connector,
|
|
status_code: err.status_code,
|
|
reason: err.reason,
|
|
})
|
|
.into_report(),
|
|
}
|
|
}
|
|
common_enums::MandateStatus::Revoked => {
|
|
Err(errors::ApiErrorResponse::MandateValidationFailed {
|
|
reason: "Mandate has already been revoked".to_string(),
|
|
})
|
|
.into_report()
|
|
}
|
|
};
|
|
mandate_revoke_status
|
|
}
|
|
|
|
#[instrument(skip(db))]
|
|
pub async fn update_connector_mandate_id(
|
|
db: &dyn StorageInterface,
|
|
merchant_account: String,
|
|
mandate_ids_opt: Option<api_models::payments::MandateIds>,
|
|
resp: Result<types::PaymentsResponseData, types::ErrorResponse>,
|
|
) -> RouterResponse<mandates::MandateResponse> {
|
|
let connector_mandate_id = Option::foreign_try_from(resp)?;
|
|
//Ignore updation if the payment_attempt mandate_id or connector_mandate_id is not present
|
|
if let Some((mandate_ids, connector_id)) = mandate_ids_opt.zip(connector_mandate_id) {
|
|
let mandate_id = &mandate_ids.mandate_id;
|
|
let mandate = db
|
|
.find_mandate_by_merchant_id_mandate_id(&merchant_account, mandate_id)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::MandateNotFound)?;
|
|
// only update the connector_mandate_id if existing is none
|
|
if mandate.connector_mandate_id.is_none() {
|
|
db.update_mandate_by_merchant_id_mandate_id(
|
|
&merchant_account,
|
|
mandate_id,
|
|
storage::MandateUpdate::ConnectorReferenceUpdate {
|
|
connector_mandate_ids: Some(connector_id),
|
|
},
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::MandateUpdateFailed)?;
|
|
}
|
|
}
|
|
Ok(services::ApplicationResponse::StatusOk)
|
|
}
|
|
|
|
#[instrument(skip(state))]
|
|
pub async fn get_customer_mandates(
|
|
state: AppState,
|
|
merchant_account: domain::MerchantAccount,
|
|
key_store: domain::MerchantKeyStore,
|
|
req: customers::CustomerId,
|
|
) -> RouterResponse<Vec<mandates::MandateResponse>> {
|
|
let mandates = state
|
|
.store
|
|
.find_mandate_by_merchant_id_customer_id(&merchant_account.merchant_id, &req.customer_id)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable_lazy(|| {
|
|
format!(
|
|
"Failed while finding mandate: merchant_id: {}, customer_id: {}",
|
|
merchant_account.merchant_id, req.customer_id
|
|
)
|
|
})?;
|
|
|
|
if mandates.is_empty() {
|
|
Err(report!(errors::ApiErrorResponse::MandateNotFound).attach_printable("No Mandate found"))
|
|
} else {
|
|
let mut response_vec = Vec::with_capacity(mandates.len());
|
|
for mandate in mandates {
|
|
response_vec.push(
|
|
mandates::MandateResponse::from_db_mandate(&state, key_store.clone(), mandate)
|
|
.await?,
|
|
);
|
|
}
|
|
Ok(services::ApplicationResponse::Json(response_vec))
|
|
}
|
|
}
|
|
|
|
fn get_insensitive_payment_method_data_if_exists<F, FData>(
|
|
router_data: &types::RouterData<F, FData, types::PaymentsResponseData>,
|
|
) -> Option<payments::PaymentMethodData>
|
|
where
|
|
FData: MandateBehaviour,
|
|
{
|
|
match &router_data.request.get_payment_method_data() {
|
|
api_models::payments::PaymentMethodData::Card(_) => None,
|
|
_ => 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>,
|
|
maybe_customer: &Option<domain::Customer>,
|
|
pm_id: Option<String>,
|
|
merchant_connector_id: Option<String>,
|
|
) -> errors::RouterResult<types::RouterData<F, FData, types::PaymentsResponseData>>
|
|
where
|
|
FData: MandateBehaviour,
|
|
{
|
|
match resp.response {
|
|
Err(_) => {}
|
|
Ok(_) => match resp.request.get_mandate_id() {
|
|
Some(mandate_id) => {
|
|
let mandate_id = &mandate_id.mandate_id;
|
|
let mandate = state
|
|
.store
|
|
.find_mandate_by_merchant_id_mandate_id(resp.merchant_id.as_ref(), mandate_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?;
|
|
let mandate = match mandate.mandate_type {
|
|
storage_enums::MandateType::SingleUse => state
|
|
.store
|
|
.update_mandate_by_merchant_id_mandate_id(
|
|
&resp.merchant_id,
|
|
mandate_id,
|
|
storage::MandateUpdate::StatusUpdate {
|
|
mandate_status: storage_enums::MandateStatus::Revoked,
|
|
},
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::MandateUpdateFailed),
|
|
storage_enums::MandateType::MultiUse => state
|
|
.store
|
|
.update_mandate_by_merchant_id_mandate_id(
|
|
&resp.merchant_id,
|
|
mandate_id,
|
|
storage::MandateUpdate::CaptureAmountUpdate {
|
|
amount_captured: Some(
|
|
mandate.amount_captured.unwrap_or(0)
|
|
+ resp.request.get_amount(),
|
|
),
|
|
},
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::MandateUpdateFailed),
|
|
}?;
|
|
metrics::SUBSEQUENT_MANDATE_PAYMENT.add(
|
|
&metrics::CONTEXT,
|
|
1,
|
|
&[metrics::request::add_attributes(
|
|
"connector",
|
|
mandate.connector,
|
|
)],
|
|
);
|
|
resp.payment_method_id = Some(mandate.payment_method_id);
|
|
}
|
|
None => {
|
|
if resp.request.get_setup_mandate_details().is_some() {
|
|
resp.payment_method_id = pm_id.clone();
|
|
let (mandate_reference, network_txn_id) = match resp.response.as_ref().ok() {
|
|
Some(types::PaymentsResponseData::TransactionResponse {
|
|
mandate_reference,
|
|
network_txn_id,
|
|
..
|
|
}) => (mandate_reference.clone(), network_txn_id.clone()),
|
|
_ => (None, None),
|
|
};
|
|
|
|
let mandate_ids = mandate_reference
|
|
.as_ref()
|
|
.map(|md| {
|
|
Encode::<types::MandateReference>::encode_to_value(&md)
|
|
.change_context(
|
|
errors::ApiErrorResponse::MandateSerializationFailed,
|
|
)
|
|
.map(masking::Secret::new)
|
|
})
|
|
.transpose()?;
|
|
|
|
if let Some(new_mandate_data) = payment_helper::generate_mandate(
|
|
resp.merchant_id.clone(),
|
|
resp.payment_id.clone(),
|
|
resp.connector.clone(),
|
|
resp.request.get_setup_mandate_details().map(Clone::clone),
|
|
maybe_customer,
|
|
pm_id.get_required_value("payment_method_id")?,
|
|
mandate_ids,
|
|
network_txn_id,
|
|
get_insensitive_payment_method_data_if_exists(&resp),
|
|
mandate_reference,
|
|
merchant_connector_id,
|
|
)? {
|
|
let connector = new_mandate_data.connector.clone();
|
|
logger::debug!("{:?}", new_mandate_data);
|
|
resp.request
|
|
.set_mandate_id(Some(api_models::payments::MandateIds {
|
|
mandate_id: new_mandate_data.mandate_id.clone(),
|
|
mandate_reference_id: new_mandate_data
|
|
.connector_mandate_ids
|
|
.clone()
|
|
.map(|ids| {
|
|
Some(ids)
|
|
.parse_value::<api_models::payments::ConnectorMandateReferenceId>(
|
|
"ConnectorMandateId",
|
|
)
|
|
.change_context(errors::ApiErrorResponse::MandateDeserializationFailed)
|
|
})
|
|
.transpose()?
|
|
.map_or(
|
|
new_mandate_data.network_transaction_id.clone().map(|id| {
|
|
api_models::payments::MandateReferenceId::NetworkMandateId(
|
|
id,
|
|
)
|
|
}),
|
|
|connector_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,
|
|
update_history:None,
|
|
|
|
}
|
|
)))
|
|
}));
|
|
state
|
|
.store
|
|
.insert_mandate(new_mandate_data)
|
|
.await
|
|
.to_duplicate_response(errors::ApiErrorResponse::DuplicateMandate)?;
|
|
metrics::MANDATE_COUNT.add(
|
|
&metrics::CONTEXT,
|
|
1,
|
|
&[metrics::request::add_attributes("connector", connector)],
|
|
);
|
|
};
|
|
}
|
|
}
|
|
},
|
|
}
|
|
Ok(resp)
|
|
}
|
|
|
|
#[instrument(skip(state))]
|
|
pub async fn retrieve_mandates_list(
|
|
state: AppState,
|
|
merchant_account: domain::MerchantAccount,
|
|
key_store: domain::MerchantKeyStore,
|
|
constraints: api_models::mandates::MandateListConstraints,
|
|
) -> RouterResponse<Vec<api_models::mandates::MandateResponse>> {
|
|
let mandates = state
|
|
.store
|
|
.as_ref()
|
|
.find_mandates_by_merchant_id(&merchant_account.merchant_id, constraints)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unable to retrieve mandates")?;
|
|
let mandates_list = future::try_join_all(mandates.into_iter().map(|mandate| {
|
|
mandates::MandateResponse::from_db_mandate(&state, key_store.clone(), mandate)
|
|
}))
|
|
.await?;
|
|
Ok(services::ApplicationResponse::Json(mandates_list))
|
|
}
|
|
|
|
impl ForeignTryFrom<Result<types::PaymentsResponseData, types::ErrorResponse>>
|
|
for Option<pii::SecretSerdeValue>
|
|
{
|
|
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
|
fn foreign_try_from(
|
|
resp: Result<types::PaymentsResponseData, types::ErrorResponse>,
|
|
) -> errors::RouterResult<Self> {
|
|
let mandate_details = match resp {
|
|
Ok(types::PaymentsResponseData::TransactionResponse {
|
|
mandate_reference, ..
|
|
}) => mandate_reference,
|
|
_ => None,
|
|
};
|
|
|
|
mandate_details
|
|
.map(|md| {
|
|
Encode::<types::MandateReference>::encode_to_value(&md)
|
|
.change_context(errors::ApiErrorResponse::MandateNotFound)
|
|
.map(masking::Secret::new)
|
|
})
|
|
.transpose()
|
|
}
|
|
}
|
|
|
|
pub trait MandateBehaviour {
|
|
fn get_amount(&self) -> i64;
|
|
fn get_setup_future_usage(&self) -> Option<diesel_models::enums::FutureUsage>;
|
|
fn get_mandate_id(&self) -> Option<&api_models::payments::MandateIds>;
|
|
fn set_mandate_id(&mut self, new_mandate_id: Option<api_models::payments::MandateIds>);
|
|
fn get_payment_method_data(&self) -> api_models::payments::PaymentMethodData;
|
|
fn get_setup_mandate_details(&self) -> Option<&data_models::mandates::MandateData>;
|
|
}
|