feat(payment_methods): Store necessary payment method data in payment_methods table (#2073)

Co-authored-by: Sarthak Soni <sarthak.soni@juspay.in>
This commit is contained in:
Sarthak Soni
2023-09-06 19:25:25 +05:30
committed by GitHub
parent 9cae5de5ff
commit 3c93552101
23 changed files with 204 additions and 16 deletions

View File

@@ -144,6 +144,20 @@ pub struct PaymentMethodResponse {
pub created: Option<time::PrimitiveDateTime>,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum PaymentMethodsData {
Card(CardDetailsPaymentMethod),
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CardDetailsPaymentMethod {
pub last4_digits: Option<String>,
pub issuer_country: Option<String>,
pub expiry_month: Option<masking::Secret<String>>,
pub expiry_year: Option<masking::Secret<String>>,
pub nick_name: Option<masking::Secret<String>>,
pub card_holder_name: Option<masking::Secret<String>>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
pub struct CardDetailFromLocker {
pub scheme: Option<String>,
@@ -172,6 +186,36 @@ pub struct CardDetailFromLocker {
pub nick_name: Option<masking::Secret<String>>,
}
impl From<CardDetailsPaymentMethod> for CardDetailFromLocker {
fn from(item: CardDetailsPaymentMethod) -> Self {
Self {
scheme: None,
issuer_country: item.issuer_country,
last4_digits: item.last4_digits,
card_number: None,
expiry_month: item.expiry_month,
expiry_year: item.expiry_year,
card_token: None,
card_holder_name: item.card_holder_name,
card_fingerprint: None,
nick_name: item.nick_name,
}
}
}
impl From<CardDetailFromLocker> for CardDetailsPaymentMethod {
fn from(item: CardDetailFromLocker) -> Self {
Self {
issuer_country: item.issuer_country,
last4_digits: item.last4_digits,
expiry_month: item.expiry_month,
expiry_year: item.expiry_year,
nick_name: item.nick_name,
card_holder_name: item.card_holder_name,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, PartialEq, Eq)]
pub struct PaymentExperienceTypes {
/// The payment experience enabled

View File

@@ -7,7 +7,7 @@ use diesel::{
};
use masking::Secret;
#[derive(Debug, AsExpression, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, AsExpression, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
#[diesel(sql_type = diesel::sql_types::Binary)]
#[repr(transparent)]
pub struct Encryption {

View File

@@ -4,7 +4,7 @@ use masking::Secret;
use serde::{Deserialize, Serialize};
use time::PrimitiveDateTime;
use crate::{enums as storage_enums, schema::payment_methods};
use crate::{encryption::Encryption, enums as storage_enums, schema::payment_methods};
#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable)]
#[diesel(table_name = payment_methods)]
@@ -32,6 +32,7 @@ pub struct PaymentMethod {
pub payment_method_issuer: Option<String>,
pub payment_method_issuer_code: Option<storage_enums::PaymentMethodIssuerCode>,
pub metadata: Option<pii::SecretSerdeValue>,
pub payment_method_data: Option<Encryption>,
}
#[derive(Clone, Debug, Eq, PartialEq, Insertable, Queryable, router_derive::DebugAsDisplay)]
@@ -57,6 +58,7 @@ pub struct PaymentMethodNew {
pub created_at: PrimitiveDateTime,
pub last_modified: PrimitiveDateTime,
pub metadata: Option<pii::SecretSerdeValue>,
pub payment_method_data: Option<Encryption>,
}
impl Default for PaymentMethodNew {
@@ -84,6 +86,7 @@ impl Default for PaymentMethodNew {
created_at: now,
last_modified: now,
metadata: Option::default(),
payment_method_data: Option::default(),
}
}
}

View File

@@ -638,6 +638,7 @@ diesel::table! {
payment_method_issuer -> Nullable<Varchar>,
payment_method_issuer_code -> Nullable<PaymentMethodIssuerCode>,
metadata -> Nullable<Json>,
payment_method_data -> Nullable<Bytea>,
}
}

View File

@@ -7,9 +7,9 @@ use api_models::{
admin::{self, PaymentMethodsEnabled},
enums::{self as api_enums},
payment_methods::{
CardNetworkTypes, PaymentExperienceTypes, RequestPaymentMethodTypes, RequiredFieldInfo,
ResponsePaymentMethodIntermediate, ResponsePaymentMethodTypes,
ResponsePaymentMethodsEnabled,
CardDetailsPaymentMethod, CardNetworkTypes, PaymentExperienceTypes, PaymentMethodsData,
RequestPaymentMethodTypes, RequiredFieldInfo, ResponsePaymentMethodIntermediate,
ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled,
},
payments::BankCodeResponse,
};
@@ -43,7 +43,10 @@ use crate::{
services,
types::{
api::{self, PaymentMethodCreateExt},
domain::{self, types::decrypt},
domain::{
self,
types::{decrypt, encrypt_optional, AsyncLift},
},
storage::{self, enums},
transformers::{ForeignFrom, ForeignInto},
},
@@ -58,6 +61,7 @@ pub async fn create_payment_method(
payment_method_id: &str,
merchant_id: &str,
pm_metadata: Option<serde_json::Value>,
payment_method_data: Option<Encryption>,
) -> errors::CustomResult<storage::PaymentMethod, errors::StorageError> {
let response = db
.insert_payment_method(storage::PaymentMethodNew {
@@ -69,6 +73,7 @@ pub async fn create_payment_method(
payment_method_issuer: req.payment_method_issuer.clone(),
scheme: req.card_network.clone(),
metadata: pm_metadata.map(masking::Secret::new),
payment_method_data,
..storage::PaymentMethodNew::default()
})
.await?;
@@ -81,6 +86,7 @@ pub async fn add_payment_method(
state: &routes::AppState,
req: api::PaymentMethodCreate,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
) -> errors::RouterResponse<api::PaymentMethodResponse> {
req.validate()?;
let merchant_id = &merchant_account.merchant_id;
@@ -118,6 +124,15 @@ pub async fn add_payment_method(
let (resp, is_duplicate) = response?;
if !is_duplicate {
let pm_metadata = resp.metadata.as_ref().map(|data| data.peek());
let pm_card_details = resp
.card
.as_ref()
.map(|card| PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())));
let pm_data_encrypted =
create_encrypted_payment_method_data(key_store, pm_card_details).await;
create_payment_method(
&*state.store,
&req,
@@ -125,6 +140,7 @@ pub async fn add_payment_method(
&resp.payment_method_id,
&resp.merchant_id,
pm_metadata.cloned(),
pm_data_encrypted,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
@@ -140,6 +156,7 @@ pub async fn update_customer_payment_method(
merchant_account: domain::MerchantAccount,
req: api::PaymentMethodUpdate,
payment_method_id: &str,
key_store: domain::MerchantKeyStore,
) -> errors::RouterResponse<api::PaymentMethodResponse> {
let db = &*state.store;
let pm = db
@@ -171,7 +188,7 @@ pub async fn update_customer_payment_method(
.as_ref()
.map(|card_network| card_network.to_string()),
};
add_payment_method(state, new_pm, &merchant_account).await
add_payment_method(state, new_pm, &merchant_account, &key_store).await
}
// Wrapper function to switch lockers
@@ -1751,6 +1768,8 @@ pub async fn list_customer_payment_method(
.await
.to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?;
let key = key_store.key.get_inner().peek();
let is_requires_cvv = db
.find_config_by_key(format!("{}_requires_cvv", merchant_account.merchant_id).as_str())
.await;
@@ -1782,11 +1801,13 @@ pub async fn list_customer_payment_method(
for pm in resp.into_iter() {
let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token");
let hyperswitch_token = generate_id(consts::ID_LENGTH, "token");
let card = if pm.payment_method == enums::PaymentMethod::Card {
Some(get_lookup_key_from_locker(state, &hyperswitch_token, &pm).await?)
get_card_details(&pm, key, state, &hyperswitch_token).await?
} else {
None
};
#[cfg(feature = "payouts")]
let pmd = if pm.payment_method == enums::PaymentMethod::BankTransfer {
Some(
@@ -1872,6 +1893,34 @@ pub async fn list_customer_payment_method(
Ok(services::ApplicationResponse::Json(response))
}
async fn get_card_details(
pm: &payment_method::PaymentMethod,
key: &[u8],
state: &routes::AppState,
hyperswitch_token: &str,
) -> errors::RouterResult<Option<api::CardDetailFromLocker>> {
let mut card_decrypted =
decrypt::<serde_json::Value, masking::WithType>(pm.payment_method_data.clone(), key)
.await
.change_context(errors::StorageError::DecryptionError)
.attach_printable("unable to decrypt card details")
.ok()
.flatten()
.map(|x| x.into_inner().expose())
.and_then(|v| serde_json::from_value::<PaymentMethodsData>(v).ok())
.map(|pmd| match pmd {
PaymentMethodsData::Card(crd) => api::CardDetailFromLocker::from(crd),
});
card_decrypted = if let Some(mut crd) = card_decrypted {
crd.scheme = pm.scheme.clone();
Some(crd)
} else {
Some(get_lookup_key_from_locker(state, hyperswitch_token, pm).await?)
};
Ok(card_decrypted)
}
pub async fn get_lookup_key_from_locker(
state: &routes::AppState,
payment_token: &str,
@@ -2191,3 +2240,33 @@ pub async fn delete_payment_method(
},
))
}
pub async fn create_encrypted_payment_method_data(
key_store: &domain::MerchantKeyStore,
pm_data: Option<PaymentMethodsData>,
) -> Option<Encryption> {
let key = key_store.key.get_inner().peek();
let pm_data_encrypted: Option<Encryption> = pm_data
.as_ref()
.map(utils::Encode::<PaymentMethodsData>::encode_to_value)
.transpose()
.change_context(errors::StorageError::SerializationFailed)
.attach_printable("Unable to convert payment method data to a value")
.unwrap_or_else(|err| {
logger::error!(err=?err);
None
})
.map(masking::Secret::<_, masking::WithType>::new)
.async_lift(|inner| encrypt_optional(inner, key))
.await
.change_context(errors::StorageError::EncryptionError)
.attach_printable("Unable to encrypt payment method data")
.unwrap_or_else(|err| {
logger::error!(err=?err);
None
})
.map(|details| details.into());
pm_data_encrypted
}

View File

@@ -668,6 +668,7 @@ where
call_connector_action,
merchant_account,
connector_request,
key_store,
)
.await
} else {
@@ -721,6 +722,7 @@ where
CallConnectorAction::Trigger,
merchant_account,
None,
key_store,
);
join_handlers.push(res);

View File

@@ -33,6 +33,7 @@ pub trait ConstructFlowSpecificData<F, Req, Res> {
) -> RouterResult<types::RouterData<F, Req, Res>>;
}
#[allow(clippy::too_many_arguments)]
#[async_trait]
pub trait Feature<F, T> {
async fn decide_flows<'a>(
@@ -43,6 +44,7 @@ pub trait Feature<F, T> {
call_connector_action: payments::CallConnectorAction,
merchant_account: &domain::MerchantAccount,
connector_request: Option<services::Request>,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self>
where
Self: Sized,

View File

@@ -48,6 +48,7 @@ impl Feature<api::Approve, types::PaymentsApproveData>
_call_connector_action: payments::CallConnectorAction,
_merchant_account: &domain::MerchantAccount,
_connector_request: Option<services::Request>,
_key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
Err(ApiErrorResponse::NotImplemented {
message: NotImplementedMessage::Reason("Flow not supported".to_string()),

View File

@@ -57,6 +57,7 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
call_connector_action: payments::CallConnectorAction,
merchant_account: &domain::MerchantAccount,
connector_request: Option<services::Request>,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
let connector_integration: services::BoxedConnectorIntegration<
'_,
@@ -87,6 +88,7 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
maybe_customer,
merchant_account,
self.request.payment_method_type,
key_store,
)
.await;

View File

@@ -47,6 +47,7 @@ impl Feature<api::Void, types::PaymentsCancelData>
call_connector_action: payments::CallConnectorAction,
_merchant_account: &domain::MerchantAccount,
connector_request: Option<services::Request>,
_key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
metrics::PAYMENT_CANCEL_COUNT.add(
&metrics::CONTEXT,

View File

@@ -48,6 +48,7 @@ impl Feature<api::Capture, types::PaymentsCaptureData>
call_connector_action: payments::CallConnectorAction,
_merchant_account: &domain::MerchantAccount,
connector_request: Option<services::Request>,
_key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
let connector_integration: services::BoxedConnectorIntegration<
'_,

View File

@@ -65,6 +65,7 @@ impl Feature<api::CompleteAuthorize, types::CompleteAuthorizeData>
call_connector_action: payments::CallConnectorAction,
_merchant_account: &domain::MerchantAccount,
connector_request: Option<services::Request>,
_key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
let connector_integration: services::BoxedConnectorIntegration<
'_,

View File

@@ -51,6 +51,7 @@ impl Feature<api::PSync, types::PaymentsSyncData>
call_connector_action: payments::CallConnectorAction,
_merchant_account: &domain::MerchantAccount,
connector_request: Option<services::Request>,
_key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
let connector_integration: services::BoxedConnectorIntegration<
'_,

View File

@@ -47,6 +47,7 @@ impl Feature<api::Reject, types::PaymentsRejectData>
_call_connector_action: payments::CallConnectorAction,
_merchant_account: &domain::MerchantAccount,
_connector_request: Option<services::Request>,
_key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
Err(ApiErrorResponse::NotImplemented {
message: NotImplementedMessage::Reason("Flow not supported".to_string()),

View File

@@ -51,6 +51,7 @@ impl Feature<api::Session, types::PaymentsSessionData> for types::PaymentsSessio
call_connector_action: payments::CallConnectorAction,
_merchant_account: &domain::MerchantAccount,
_connector_request: Option<services::Request>,
_key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
metrics::SESSION_TOKEN_CREATED.add(
&metrics::CONTEXT,

View File

@@ -46,6 +46,7 @@ impl Feature<api::Verify, types::VerifyRequestData> for types::VerifyRouterData
call_connector_action: payments::CallConnectorAction,
merchant_account: &domain::MerchantAccount,
connector_request: Option<services::Request>,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
let connector_integration: services::BoxedConnectorIntegration<
'_,
@@ -70,6 +71,7 @@ impl Feature<api::Verify, types::VerifyRequestData> for types::VerifyRouterData
maybe_customer,
merchant_account,
self.request.payment_method_type,
key_store,
)
.await?;
@@ -156,6 +158,7 @@ impl TryFrom<types::VerifyRequestData> for types::ConnectorCustomerData {
}
}
#[allow(clippy::too_many_arguments)]
impl types::VerifyRouterData {
pub async fn decide_flow<'a, 'b>(
&'b self,
@@ -165,6 +168,7 @@ impl types::VerifyRouterData {
confirm: Option<bool>,
call_connector_action: payments::CallConnectorAction,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self> {
match confirm {
Some(true) => {
@@ -192,6 +196,7 @@ impl types::VerifyRouterData {
maybe_customer,
merchant_account,
payment_method_type,
key_store,
)
.await?;

View File

@@ -13,7 +13,7 @@ use crate::{
services,
types::{
self,
api::{self, PaymentMethodCreateExt},
api::{self, CardDetailsPaymentMethod, PaymentMethodCreateExt},
domain,
storage::enums as storage_enums,
},
@@ -27,6 +27,7 @@ pub async fn save_payment_method<F: Clone, FData>(
maybe_customer: &Option<domain::Customer>,
merchant_account: &domain::MerchantAccount,
payment_method_type: Option<storage_enums::PaymentMethodType>,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<Option<String>>
where
FData: mandate::MandateBehaviour,
@@ -71,6 +72,19 @@ where
.await?;
let is_duplicate = locker_response.1;
let pm_card_details = locker_response.0.card.as_ref().map(|card| {
api::payment_methods::PaymentMethodsData::Card(CardDetailsPaymentMethod::from(
card.clone(),
))
});
let pm_data_encrypted =
payment_methods::cards::create_encrypted_payment_method_data(
key_store,
pm_card_details,
)
.await;
if is_duplicate {
let existing_pm = db
.find_payment_method(&locker_response.0.payment_method_id)
@@ -103,6 +117,7 @@ where
&locker_response.0.payment_method_id,
merchant_id,
pm_metadata,
pm_data_encrypted,
)
.await
.change_context(
@@ -131,6 +146,7 @@ where
&locker_response.0.payment_method_id,
merchant_id,
pm_metadata,
pm_data_encrypted,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)

View File

@@ -209,6 +209,24 @@ pub async fn save_payout_data_to_locker(
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error updating payouts in saved payout method")?;
let pm_data = api::payment_methods::PaymentMethodsData::Card(
api::payment_methods::CardDetailsPaymentMethod {
last4_digits: card_details
.as_ref()
.map(|c| c.card_number.clone().get_last4()),
issuer_country: None,
expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()),
expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()),
nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()),
card_holder_name: card_details
.as_ref()
.and_then(|c| c.card_holder_name.clone()),
},
);
let card_details_encrypted =
cards::create_encrypted_payment_method_data(key_store, Some(pm_data)).await;
// Insert in payment_method table
let payment_method = api::PaymentMethodCreate {
payment_method: api_enums::PaymentMethod::foreign_from(payout_method_data.to_owned()),
@@ -220,6 +238,7 @@ pub async fn save_payout_data_to_locker(
customer_id: Some(payout_attempt.customer_id.to_owned()),
card_network: None,
};
cards::create_payment_method(
db,
&payment_method,
@@ -227,6 +246,7 @@ pub async fn save_payout_data_to_locker(
&stored_resp.card_reference,
&merchant_account.merchant_id,
None,
card_details_encrypted,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)

View File

@@ -159,6 +159,7 @@ impl PaymentMethodInterface for MockDb {
payment_method_issuer: payment_method_new.payment_method_issuer,
payment_method_issuer_code: payment_method_new.payment_method_issuer_code,
metadata: payment_method_new.metadata,
payment_method_data: payment_method_new.payment_method_data,
};
payment_methods.push(payment_method.clone());
Ok(payment_method)

View File

@@ -40,7 +40,7 @@ pub async fn create_payment_method_api(
&req,
json_payload.into_inner(),
|state, auth, req| async move {
cards::add_payment_method(state, req, &auth.merchant_account).await
cards::add_payment_method(state, req, &auth.merchant_account, &auth.key_store).await
},
&auth::ApiKeyAuth,
)
@@ -289,6 +289,7 @@ pub async fn payment_method_update_api(
auth.merchant_account,
payload,
&payment_method_id,
auth.key_store,
)
},
&auth::ApiKeyAuth,

View File

@@ -1,11 +1,12 @@
use api_models::enums as api_enums;
pub use api_models::payment_methods::{
CardDetail, CardDetailFromLocker, CustomerPaymentMethod, CustomerPaymentMethodsListResponse,
DeleteTokenizeByDateRequest, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest,
GetTokenizePayloadResponse, PaymentMethodCreate, PaymentMethodDeleteResponse, PaymentMethodId,
PaymentMethodList, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodResponse,
PaymentMethodUpdate, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1,
TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2,
CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod,
CustomerPaymentMethodsListResponse, DeleteTokenizeByDateRequest, DeleteTokenizeByTokenRequest,
GetTokenizePayloadRequest, GetTokenizePayloadResponse, PaymentMethodCreate,
PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodList, PaymentMethodListRequest,
PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData,
TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2,
TokenizedWalletValue1, TokenizedWalletValue2,
};
use error_stack::report;

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE payment_methods DROP COLUMN IF EXISTS payment_method_data;

View File

@@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS payment_method_data BYTEA DEFAULT NULL;