Skip to content

Commit

Permalink
fix: storage of generic payment methods in permanent locker (#1799)
Browse files Browse the repository at this point in the history
Co-authored-by: Kashif <[email protected]>
  • Loading branch information
kashif-m and kashif-m authored Aug 22, 2023
1 parent bb9c34e commit 19ee324
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 133 deletions.
1 change: 1 addition & 0 deletions crates/api_models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod files;
pub mod mandates;
pub mod payment_methods;
pub mod payments;
#[cfg(feature = "payouts")]
pub mod payouts;
pub mod refunds;
pub mod webhooks;
19 changes: 0 additions & 19 deletions crates/api_models/src/payouts.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
#[cfg(feature = "payouts")]
use cards::CardNumber;
#[cfg(feature = "payouts")]
use common_utils::{
crypto,
pii::{self, Email},
};
#[cfg(feature = "payouts")]
use masking::Secret;
#[cfg(feature = "payouts")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "payouts")]
use utoipa::ToSchema;

#[cfg(feature = "payouts")]
use crate::{admin, enums as api_enums, payments};

#[cfg(feature = "payouts")]
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
pub enum PayoutRequest {
PayoutActionRequest(PayoutActionRequest),
PayoutCreateRequest(PayoutCreateRequest),
PayoutRetrieveRequest(PayoutRetrieveRequest),
}

#[cfg(feature = "payouts")]
#[derive(Default, Debug, Deserialize, Serialize, Clone, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct PayoutCreateRequest {
Expand Down Expand Up @@ -156,22 +148,19 @@ pub struct PayoutCreateRequest {
pub payout_token: Option<String>,
}

#[cfg(feature = "payouts")]
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum PayoutMethodData {
Card(Card),
Bank(Bank),
}

#[cfg(feature = "payouts")]
impl Default for PayoutMethodData {
fn default() -> Self {
Self::Card(Card::default())
}
}

#[cfg(feature = "payouts")]
#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct Card {
/// The card number
Expand All @@ -191,7 +180,6 @@ pub struct Card {
pub card_holder_name: Secret<String>,
}

#[cfg(feature = "payouts")]
#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)]
#[serde(untagged)]
pub enum Bank {
Expand All @@ -200,7 +188,6 @@ pub enum Bank {
Sepa(SepaBankTransfer),
}

#[cfg(feature = "payouts")]
#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct AchBankTransfer {
/// Bank name
Expand All @@ -224,7 +211,6 @@ pub struct AchBankTransfer {
pub bank_routing_number: Secret<String>,
}

#[cfg(feature = "payouts")]
#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct BacsBankTransfer {
/// Bank name
Expand All @@ -248,7 +234,6 @@ pub struct BacsBankTransfer {
pub bank_sort_code: Secret<String>,
}

#[cfg(feature = "payouts")]
#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)]
// The SEPA (Single Euro Payments Area) is a pan-European network that allows you to send and receive payments in euros between two cross-border bank accounts in the eurozone.
pub struct SepaBankTransfer {
Expand All @@ -273,7 +258,6 @@ pub struct SepaBankTransfer {
pub bic: Option<Secret<String>>,
}

#[cfg(feature = "payouts")]
#[derive(Debug, ToSchema, Clone, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PayoutCreateResponse {
Expand Down Expand Up @@ -394,13 +378,11 @@ pub struct PayoutCreateResponse {
pub error_code: Option<String>,
}

#[cfg(feature = "payouts")]
#[derive(Default, Debug, Clone, Deserialize, ToSchema)]
pub struct PayoutRetrieveBody {
pub force_sync: Option<bool>,
}

#[cfg(feature = "payouts")]
#[derive(Default, Debug, Serialize, ToSchema, Clone, Deserialize)]
pub struct PayoutRetrieveRequest {
/// Unique identifier for the payout. This ensures idempotency for multiple payouts
Expand All @@ -419,7 +401,6 @@ pub struct PayoutRetrieveRequest {
pub force_sync: Option<bool>,
}

#[cfg(feature = "payouts")]
#[derive(Default, Debug, Serialize, ToSchema, Clone, Deserialize)]
pub struct PayoutActionRequest {
/// Unique identifier for the payout. This ensures idempotency for multiple payouts
Expand Down
137 changes: 80 additions & 57 deletions crates/router/src/core/payment_methods/cards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ use api_models::{
},
payments::BankCodeResponse,
};
#[cfg(feature = "payouts")]
use common_utils::ext_traits::ByteSliceExt;
use common_utils::{
consts,
ext_traits::{AsyncExt, StringExt, ValueExt},
Expand Down Expand Up @@ -257,8 +255,20 @@ pub async fn add_card_hs(
customer_id: String,
merchant_account: &domain::MerchantAccount,
) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> {
let store_card_payload =
call_to_card_hs(state, &card, None, &customer_id, merchant_account).await?;
let payload = payment_methods::StoreLockerReq::LockerCard(payment_methods::StoreCardReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: customer_id.to_owned(),
card: payment_methods::Card {
card_number: card.card_number.to_owned(),
name_on_card: card.card_holder_name.to_owned(),
card_exp_month: card.card_exp_month.to_owned(),
card_exp_year: card.card_exp_year.to_owned(),
card_brand: None,
card_isin: None,
nick_name: card.nick_name.as_ref().map(masking::Secret::peek).cloned(),
},
});
let store_card_payload = call_to_locker_hs(state, &payload, &customer_id).await?;

let payment_method_resp = payment_methods::mk_add_card_response_hs(
card,
Expand All @@ -272,6 +282,28 @@ pub async fn add_card_hs(
))
}

#[instrument(skip_all)]
pub async fn decode_and_decrypt_locker_data(
key_store: &domain::MerchantKeyStore,
enc_card_data: String,
) -> errors::CustomResult<Secret<String>, errors::VaultError> {
// Fetch key
let key = key_store.key.get_inner().peek();
// Decode
let decoded_bytes = hex::decode(&enc_card_data)
.into_report()
.change_context(errors::VaultError::ResponseDeserializationFailed)
.attach_printable("Failed to decode hex string into bytes")?;
// Decrypt
decrypt(Some(Encryption::new(decoded_bytes.into())), key)
.await
.change_context(errors::VaultError::FetchPaymentMethodFailed)?
.map_or(
Err(report!(errors::VaultError::FetchPaymentMethodFailed)),
|d| Ok(d.into_inner()),
)
}

#[instrument(skip_all)]
pub async fn get_payment_method_from_hs_locker<'a>(
state: &'a routes::AppState,
Expand All @@ -286,7 +318,7 @@ pub async fn get_payment_method_from_hs_locker<'a>(
#[cfg(feature = "kms")]
let jwekey = &state.kms_secrets;

if !locker.mock_locker {
let payment_method_data = if !locker.mock_locker {
let request = payment_methods::mk_get_card_request_hs(
jwekey,
locker,
Expand Down Expand Up @@ -315,44 +347,34 @@ pub async fn get_payment_method_from_hs_locker<'a>(
.payload
.get_required_value("RetrieveCardRespPayload")
.change_context(errors::VaultError::FetchPaymentMethodFailed)?;
retrieve_card_resp
let enc_card_data = retrieve_card_resp
.enc_card_data
.get_required_value("enc_card_data")
.change_context(errors::VaultError::FetchPaymentMethodFailed)
.change_context(errors::VaultError::FetchPaymentMethodFailed)?;
decode_and_decrypt_locker_data(key_store, enc_card_data.peek().to_string()).await?
} else {
let get_card_resp =
mock_get_payment_method(&*state.store, key_store, payment_method_reference).await?;
Ok(get_card_resp.payment_method.payment_method_data)
}
mock_get_payment_method(&*state.store, key_store, payment_method_reference)
.await?
.payment_method
.payment_method_data
};
Ok(payment_method_data)
}

#[instrument(skip_all)]
pub async fn call_to_card_hs(
pub async fn call_to_locker_hs<'a>(
state: &routes::AppState,
card: &api::CardDetail,
enc_value: Option<&str>,
payload: &payment_methods::StoreLockerReq<'a>,
customer_id: &str,
merchant_account: &domain::MerchantAccount,
) -> errors::CustomResult<payment_methods::StoreCardRespPayload, errors::VaultError> {
let locker = &state.conf.locker;
#[cfg(not(feature = "kms"))]
let jwekey = &state.conf.jwekey;
#[cfg(feature = "kms")]
let jwekey = &state.kms_secrets;

let db = &*state.store;
let merchant_id = &merchant_account.merchant_id;

let stored_card_response = if !locker.mock_locker {
let request = payment_methods::mk_add_card_request_hs(
jwekey,
locker,
card,
enc_value,
customer_id,
merchant_id,
)
.await?;
let request = payment_methods::mk_add_locker_request_hs(jwekey, locker, payload).await?;
let response = services::call_connector_api(state, request)
.await
.change_context(errors::VaultError::SaveCardFailed);
Expand All @@ -371,7 +393,7 @@ pub async fn call_to_card_hs(
stored_card_resp
} else {
let card_id = generate_id(consts::ID_LENGTH, "card");
mock_add_card_hs(db, &card_id, card, None, enc_value, None, Some(customer_id)).await?
mock_call_to_locker_hs(db, &card_id, payload, None, None, Some(customer_id)).await?
};

let stored_card = stored_card_response
Expand Down Expand Up @@ -495,31 +517,47 @@ pub async fn delete_card_from_hs_locker<'a>(
}

///Mock api for local testing
#[instrument(skip_all)]
pub async fn mock_add_card_hs(
pub async fn mock_call_to_locker_hs<'a>(
db: &dyn db::StorageInterface,
card_id: &str,
card: &api::CardDetail,
payload: &payment_methods::StoreLockerReq<'a>,
card_cvc: Option<String>,
enc_val: Option<&str>,
payment_method_id: Option<String>,
customer_id: Option<&str>,
) -> errors::CustomResult<payment_methods::StoreCardResp, errors::VaultError> {
let locker_mock_up = storage::LockerMockUpNew {
let mut locker_mock_up = storage::LockerMockUpNew {
card_id: card_id.to_string(),
external_id: uuid::Uuid::new_v4().to_string(),
card_fingerprint: uuid::Uuid::new_v4().to_string(),
card_global_fingerprint: uuid::Uuid::new_v4().to_string(),
merchant_id: "mm01".to_string(),
card_number: card.card_number.peek().to_string(),
card_exp_year: card.card_exp_year.peek().to_string(),
card_exp_month: card.card_exp_month.peek().to_string(),
merchant_id: "".to_string(),
card_number: "4111111111111111".to_string(),
card_exp_year: "2099".to_string(),
card_exp_month: "12".to_string(),
card_cvc,
payment_method_id,
customer_id: customer_id.map(str::to_string),
name_on_card: card.card_holder_name.to_owned().expose_option(),
nickname: card.nick_name.to_owned().map(masking::Secret::expose),
enc_card_data: enc_val.map(|e| e.to_string()),
name_on_card: None,
nickname: None,
enc_card_data: None,
};
locker_mock_up = match payload {
payment_methods::StoreLockerReq::LockerCard(store_card_req) => storage::LockerMockUpNew {
merchant_id: store_card_req.merchant_id.to_string(),
card_number: store_card_req.card.card_number.peek().to_string(),
card_exp_year: store_card_req.card.card_exp_year.peek().to_string(),
card_exp_month: store_card_req.card.card_exp_month.peek().to_string(),
name_on_card: store_card_req.card.name_on_card.to_owned().expose_option(),
nickname: store_card_req.card.nick_name.to_owned(),
..locker_mock_up
},
payment_methods::StoreLockerReq::LockerGeneric(store_generic_req) => {
storage::LockerMockUpNew {
merchant_id: store_generic_req.merchant_id.to_string(),
enc_card_data: Some(store_generic_req.enc_data.to_owned()),
..locker_mock_up
}
}
};

let response = db
Expand Down Expand Up @@ -588,21 +626,7 @@ pub async fn mock_get_payment_method<'a>(
.await
.change_context(errors::VaultError::FetchPaymentMethodFailed)?;
let dec_data = if let Some(e) = locker_mock_up.enc_card_data {
// Fetch key
let key = key_store.key.get_inner().peek();
// Decode
let decoded_bytes = hex::decode(e)
.into_report()
.change_context(errors::VaultError::ResponseDeserializationFailed)
.attach_printable("Failed to decode hex string into bytes")?;
// Decrypt
async { decrypt(Some(Encryption::new(decoded_bytes.into())), key).await }
.await
.change_context(errors::VaultError::FetchPaymentMethodFailed)?
.map_or(
Err(report!(errors::VaultError::FetchPaymentMethodFailed)),
|d| Ok(d.into_inner()),
)
decode_and_decrypt_locker_data(key_store, e).await
} else {
Err(report!(errors::VaultError::FetchPaymentMethodFailed))
}?;
Expand Down Expand Up @@ -1894,8 +1918,7 @@ pub async fn get_lookup_key_for_payout_method(
.attach_printable("Error getting payment method from locker")?;
let pm_parsed: api::PayoutMethodData = payment_method
.peek()
.as_bytes()
.to_vec()
.to_string()
.parse_struct("PayoutMethodData")
.change_context(errors::ApiErrorResponse::InternalServerError)?;
match &pm_parsed {
Expand Down
Loading

0 comments on commit 19ee324

Please sign in to comment.