Skip to content

Commit

Permalink
feat(payment_methods_v2): Update and Retrieve payment method APIs for…
Browse files Browse the repository at this point in the history
… v2 (#5939)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
Sarthak1799 and hyperswitch-bot[bot] authored Sep 26, 2024
1 parent 8c12862 commit f096992
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 67 deletions.
23 changes: 22 additions & 1 deletion crates/api_models/src/payment_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ pub struct PaymentMethodUpdate {
#[serde(deny_unknown_fields)]
pub struct PaymentMethodUpdate {
/// payment method data to be passed
pub payment_method_data: Option<PaymentMethodUpdateData>,
pub payment_method_data: PaymentMethodUpdateData,

/// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK
#[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893eiu2d")]
Expand Down Expand Up @@ -990,6 +990,27 @@ impl From<CardDetailsPaymentMethod> for CardDetailFromLocker {
}
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
impl From<CardDetail> for CardDetailFromLocker {
fn from(item: CardDetail) -> Self {
Self {
issuer_country: item.card_issuing_country,
last4_digits: Some(item.card_number.get_last4()),
card_number: Some(item.card_number),
expiry_month: Some(item.card_exp_month),
expiry_year: Some(item.card_exp_year),
card_holder_name: item.card_holder_name,
nick_name: item.nick_name,
card_isin: None,
card_issuer: item.card_issuer,
card_network: item.card_network,
card_type: item.card_type.map(|card| card.to_string()),
saved_to_locker: true,
card_fingerprint: None,
}
}
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
impl From<CardDetail> for CardDetailsPaymentMethod {
fn from(item: CardDetail) -> Self {
Expand Down
4 changes: 4 additions & 0 deletions crates/router/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ pub const ADD_VAULT_REQUEST_URL: &str = "/vault/add";
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
pub const VAULT_FINGERPRINT_REQUEST_URL: &str = "/fingerprint";

/// Vault Retrieve request url
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
pub const VAULT_RETRIEVE_REQUEST_URL: &str = "/vault/retrieve";

/// Vault Header content type
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
pub const VAULT_HEADER_CONTENT_TYPE: &str = "application/json";
199 changes: 184 additions & 15 deletions crates/router/src/core/payment_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use common_utils::{consts::DEFAULT_LOCALE, id_type};
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use common_utils::{
crypto::{self, Encryptable},
ext_traits::{AsyncExt, Encode, ValueExt},
ext_traits::{AsyncExt, Encode, StringExt, ValueExt},
fp_utils::when,
generate_id,
request::RequestContent,
Expand Down Expand Up @@ -80,7 +80,7 @@ const PAYMENT_METHOD_STATUS_UPDATE_TASK: &str = "PAYMENT_METHOD_STATUS_UPDATE";
const PAYMENT_METHOD_STATUS_TAG: &str = "PAYMENT_METHOD_STATUS";

#[instrument(skip_all)]
pub async fn retrieve_payment_method(
pub async fn retrieve_payment_method_core(
pm_data: &Option<domain::PaymentMethodData>,
state: &SessionState,
payment_intent: &PaymentIntent,
Expand Down Expand Up @@ -877,16 +877,24 @@ pub async fn create_payment_method(
.await
.attach_printable("Failed to add Payment method to DB")?;

let vaulting_result =
vault_payment_method(state, &req.payment_method_data, merchant_account, key_store).await;
let payment_method_data = pm_types::PaymentMethodVaultingData::from(req.payment_method_data);

let vaulting_result = vault_payment_method(
state,
&payment_method_data,
merchant_account,
key_store,
None,
)
.await;

let response = match vaulting_result {
Ok(resp) => {
let pm_update = create_pm_additional_data_update(
&req.payment_method_data,
&payment_method_data,
state,
key_store,
Some(resp.vault_id),
Some(resp.vault_id.get_string_repr().clone()),
Some(req.payment_method),
Some(req.payment_method_type),
)
Expand Down Expand Up @@ -1041,16 +1049,24 @@ pub async fn payment_method_intent_confirm(
.await
.to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?;

let vaulting_result =
vault_payment_method(state, &req.payment_method_data, merchant_account, key_store).await;
let payment_method_data = pm_types::PaymentMethodVaultingData::from(req.payment_method_data);

let vaulting_result = vault_payment_method(
state,
&payment_method_data,
merchant_account,
key_store,
None,
)
.await;

let response = match vaulting_result {
Ok(resp) => {
let pm_update = create_pm_additional_data_update(
&req.payment_method_data,
&payment_method_data,
state,
key_store,
Some(resp.vault_id),
Some(resp.vault_id.get_string_repr().clone()),
Some(req.payment_method),
Some(req.payment_method_type),
)
Expand Down Expand Up @@ -1220,15 +1236,17 @@ pub async fn create_payment_method_for_intent(

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
pub async fn create_pm_additional_data_update(
pmd: &api::PaymentMethodCreateData,
pmd: &pm_types::PaymentMethodVaultingData,
state: &SessionState,
key_store: &domain::MerchantKeyStore,
vault_id: Option<String>,
payment_method: Option<api_enums::PaymentMethod>,
payment_method_type: Option<api_enums::PaymentMethodType>,
) -> RouterResult<storage::PaymentMethodUpdate> {
let card = match pmd.clone() {
api::PaymentMethodCreateData::Card(card) => api::PaymentMethodsData::Card(card.into()),
let card = match pmd {
pm_types::PaymentMethodVaultingData::Card(card) => {
api::PaymentMethodsData::Card(card.clone().into())
}
};

let pmd: Encryptable<Secret<serde_json::Value>> =
Expand All @@ -1255,15 +1273,17 @@ pub async fn create_pm_additional_data_update(
#[instrument(skip_all)]
pub async fn vault_payment_method(
state: &SessionState,
pmd: &api::PaymentMethodCreateData,
pmd: &pm_types::PaymentMethodVaultingData,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
existing_vault_id: Option<String>,
) -> RouterResult<pm_types::AddVaultResponse> {
let db = &*state.store;

// get fingerprint_id from locker
let fingerprint_id_from_locker = cards::get_fingerprint_id_from_locker(state, pmd)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get fingerprint_id from vault")?;

// throw back error if payment method is duplicated
Expand All @@ -1286,7 +1306,10 @@ pub async fn vault_payment_method(
)?;

let resp_from_locker =
cards::vault_payment_method_in_locker(state, merchant_account, pmd).await?;
cards::vault_payment_method_in_locker(state, merchant_account, pmd, existing_vault_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to vault payment method in locker")?;

Ok(resp_from_locker)
}
Expand Down Expand Up @@ -1656,6 +1679,152 @@ async fn generate_saved_pm_response(
Ok(pma)
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
#[instrument(skip_all)]
pub async fn retrieve_payment_method(
state: SessionState,
pm: api::PaymentMethodId,
key_store: domain::MerchantKeyStore,
merchant_account: domain::MerchantAccount,
) -> RouterResponse<api::PaymentMethodResponse> {
let db = state.store.as_ref();
let pm_id = id_type::GlobalPaymentMethodId::generate_from_string(pm.payment_method_id)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to generate GlobalPaymentMethodId")?;

let payment_method = db
.find_payment_method(
&((&state).into()),
&key_store,
&pm_id,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;

let pmd = payment_method
.payment_method_data
.clone()
.map(|x| x.into_inner().expose())
.and_then(|v| serde_json::from_value::<api::PaymentMethodsData>(v).ok())
.and_then(|pmd| match pmd {
api::PaymentMethodsData::Card(card) => {
Some(api::PaymentMethodResponseData::Card(card.into()))
}
_ => None,
});

let resp = api::PaymentMethodResponse {
merchant_id: payment_method.merchant_id.to_owned(),
customer_id: payment_method.customer_id.to_owned(),
payment_method_id: payment_method.id.get_string_repr().to_string(),
payment_method: payment_method.payment_method,
payment_method_type: payment_method.payment_method_type,
metadata: payment_method.metadata.clone(),
created: Some(payment_method.created_at),
recurring_enabled: false,
last_used_at: Some(payment_method.last_used_at),
client_secret: payment_method.client_secret.clone(),
payment_method_data: pmd,
};

Ok(services::ApplicationResponse::Json(resp))
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
#[instrument(skip_all)]
pub async fn update_payment_method(
state: SessionState,
merchant_account: domain::MerchantAccount,
req: api::PaymentMethodUpdate,
payment_method_id: &str,
key_store: domain::MerchantKeyStore,
) -> RouterResponse<api::PaymentMethodResponse> {
let db = state.store.as_ref();

let pm_id = id_type::GlobalPaymentMethodId::generate_from_string(payment_method_id.to_string())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to generate GlobalPaymentMethodId")?;

let payment_method = db
.find_payment_method(
&((&state).into()),
&key_store,
&pm_id,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
let current_vault_id = payment_method.locker_id.clone();

when(
payment_method.status == enums::PaymentMethodStatus::AwaitingData,
|| {
Err(errors::ApiErrorResponse::InvalidRequestData {
message: "This Payment method is awaiting data and hence cannot be updated"
.to_string(),
})
},
)?;

let pmd: pm_types::PaymentMethodVaultingData = cards::retrieve_payment_method_from_vault(
&state,
&merchant_account,
&payment_method.customer_id,
&payment_method,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to retrieve payment method from vault")?
.data
.expose()
.parse_struct("PaymentMethodCreateData")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse PaymentMethodCreateData")?;

let vault_request_data =
pm_transforms::generate_pm_vaulting_req_from_update_request(pmd, req.payment_method_data);

let vaulting_response = vault_payment_method(
&state,
&vault_request_data,
&merchant_account,
&key_store,
current_vault_id, // using current vault_id for now, will have to refactor this
) // to generate new one on each vaulting later on
.await
.attach_printable("Failed to add payment method in vault")?;

let pm_update = create_pm_additional_data_update(
&vault_request_data,
&state,
&key_store,
Some(vaulting_response.vault_id.get_string_repr().clone()),
payment_method.payment_method,
payment_method.payment_method_type,
)
.await
.attach_printable("Unable to create Payment method data")?;

let payment_method = db
.update_payment_method(
&((&state).into()),
&key_store,
payment_method,
pm_update,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update payment method in db")?;

let response = pm_transforms::generate_payment_method_response(&payment_method)?;

// Add a PT task to handle payment_method delete from vault

Ok(services::ApplicationResponse::Json(response))
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
impl pm_types::SavedPMLPaymentsInfo {
pub async fn form_payments_info(
Expand Down
Loading

0 comments on commit f096992

Please # to comment.