fix: storage of generic payment methods in permanent locker (#1799)

Co-authored-by: Kashif <mohammed.kashif@juspay.in>
This commit is contained in:
Kashif
2023-08-22 12:19:43 +05:30
committed by GitHub
parent bb9c34e8b3
commit 19ee324d37
5 changed files with 140 additions and 133 deletions

View File

@ -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;

View File

@ -1,21 +1,14 @@
#[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),
@ -23,7 +16,6 @@ pub enum PayoutRequest {
PayoutRetrieveRequest(PayoutRetrieveRequest),
}
#[cfg(feature = "payouts")]
#[derive(Default, Debug, Deserialize, Serialize, Clone, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct PayoutCreateRequest {
@ -156,7 +148,6 @@ 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 {
@ -164,14 +155,12 @@ pub enum PayoutMethodData {
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
@ -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 {
@ -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
@ -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
@ -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 {
@ -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 {
@ -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
@ -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

View File

@ -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},
@ -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,
@ -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,
@ -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,
@ -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);
@ -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
@ -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
@ -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))
}?;
@ -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 {

View File

@ -15,12 +15,26 @@ use crate::{
utils::{self, OptionExt},
};
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum StoreLockerReq<'a> {
LockerCard(StoreCardReq<'a>),
LockerGeneric(StoreGenericReq<'a>),
}
#[derive(Debug, Deserialize, Serialize)]
pub struct StoreCardReq<'a> {
pub merchant_id: &'a str,
pub merchant_customer_id: String,
pub card: Card,
pub enc_card_data: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct StoreGenericReq<'a> {
pub merchant_id: &'a str,
pub merchant_customer_id: String,
#[serde(rename = "enc_card_data")]
pub enc_data: String,
}
#[derive(Debug, Deserialize, Serialize)]
@ -253,32 +267,13 @@ pub async fn mk_basilisk_req(
Ok(jwe_body)
}
pub async fn mk_add_card_request_hs(
pub async fn mk_add_locker_request_hs<'a>(
#[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey,
#[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets,
locker: &settings::Locker,
card: &api::CardDetail,
enc_value: Option<&str>,
customer_id: &str,
merchant_id: &str,
payload: &StoreLockerReq<'a>,
) -> CustomResult<services::Request, errors::VaultError> {
let merchant_customer_id = customer_id.to_owned();
let card = 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.to_owned().map(masking::Secret::expose),
};
let store_card_req = StoreCardReq {
merchant_id,
merchant_customer_id,
card,
enc_card_data: enc_value.map(|e| e.to_string()),
};
let payload = utils::Encode::<StoreCardReq<'_>>::encode_to_vec(&store_card_req)
let payload = utils::Encode::<StoreCardReq<'_>>::encode_to_vec(&payload)
.change_context(errors::VaultError::RequestEncodingFailed)?;
#[cfg(feature = "kms")]

View File

@ -1,6 +1,3 @@
use std::str::FromStr;
use ::cards::CardNumber;
use common_utils::{errors::CustomResult, ext_traits::ValueExt};
use diesel_models::encryption::Encryption;
use error_stack::{IntoReport, ResultExt};
@ -9,7 +6,11 @@ use masking::{ExposeInterface, PeekInterface, Secret};
use crate::{
core::{
errors::{self, RouterResult},
payment_methods::{cards, vault},
payment_methods::{
cards, transformers,
transformers::{StoreCardReq, StoreGenericReq, StoreLockerReq},
vault,
},
payments::{customers::get_connector_customer_details_if_present, CustomerDetails},
utils as core_utils,
},
@ -125,21 +126,37 @@ pub async fn save_payout_data_to_locker(
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<()> {
let mut enc_card_data = None;
let (card_details, payment_method_type) = match payout_method_data {
api_models::payouts::PayoutMethodData::Card(card) => (
api::CardDetail {
let (locker_req, card_details, payment_method_type) = match payout_method_data {
api_models::payouts::PayoutMethodData::Card(card) => {
let card_detail = api::CardDetail {
card_number: card.card_number.to_owned(),
card_holder_name: Some(card.card_holder_name.to_owned()),
card_exp_month: card.expiry_month.to_owned(),
card_exp_year: card.expiry_year.to_owned(),
card_holder_name: Some(card.card_holder_name.to_owned()),
nick_name: None,
},
api_enums::PaymentMethodType::Debit,
),
};
let payload = StoreLockerReq::LockerCard(StoreCardReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: payout_attempt.customer_id.to_owned(),
card: transformers::Card {
card_number: card.card_number.to_owned(),
name_on_card: Some(card.card_holder_name.to_owned()),
card_exp_month: card.expiry_month.to_owned(),
card_exp_year: card.expiry_year.to_owned(),
card_brand: None,
card_isin: None,
nick_name: None,
},
});
(
payload,
Some(card_detail),
api_enums::PaymentMethodType::Debit,
)
}
api_models::payouts::PayoutMethodData::Bank(bank) => {
let key = key_store.key.get_inner().peek();
let enc_str = async {
let enc_data = async {
serde_json::to_value(payout_method_data.to_owned())
.into_report()
.change_context(errors::ApiErrorResponse::InternalServerError)
@ -160,32 +177,22 @@ pub async fn save_payout_data_to_locker(
.map_or(Err(errors::ApiErrorResponse::InternalServerError), |e| {
Ok(hex::encode(e.peek()))
})?;
enc_card_data = Some(enc_str);
let payload = StoreLockerReq::LockerGeneric(StoreGenericReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: payout_attempt.customer_id.to_owned(),
enc_data,
});
(
api::CardDetail {
card_number: CardNumber::from_str("4111111111111111")
.into_report()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to form a sample card number")?,
card_exp_month: "12".to_string().into(),
card_exp_year: "99".to_string().into(),
card_holder_name: None,
nick_name: None,
},
payload,
None,
api_enums::PaymentMethodType::foreign_from(bank.to_owned()),
)
}
};
// Store payout method in locker
let stored_resp = cards::call_to_card_hs(
state,
&card_details,
enc_card_data.as_deref(),
&payout_attempt.customer_id,
merchant_account,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let stored_resp = cards::call_to_locker_hs(state, &locker_req, &payout_attempt.customer_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
// Store card_reference in payouts table
let db = &*state.store;
@ -208,7 +215,7 @@ pub async fn save_payout_data_to_locker(
payment_method_type: Some(payment_method_type),
payment_method_issuer: None,
payment_method_issuer_code: None,
card: Some(card_details),
card: card_details,
metadata: None,
customer_id: Some(payout_attempt.customer_id.to_owned()),
card_network: None,