feat(router): add card_info in payment_attempt table if not provided in request (#1538)

Co-authored-by: Sahkal Poddar <sahkal.poddar@juspay.in>
This commit is contained in:
Sahkal Poddar
2023-07-05 18:28:00 +05:30
committed by GitHub
parent cf7b67286c
commit 5628985c40
23 changed files with 230 additions and 46 deletions

View File

@ -551,6 +551,14 @@ pub struct Card {
#[schema(value_type = Option<CardNetwork>, example = "Visa")] #[schema(value_type = Option<CardNetwork>, example = "Visa")]
pub card_network: Option<api_enums::CardNetwork>, pub card_network: Option<api_enums::CardNetwork>,
#[schema(example = "CREDIT")]
pub card_type: Option<String>,
#[schema(example = "INDIA")]
pub card_issuing_country: Option<String>,
#[schema(example = "JP_AMEX")]
pub bank_code: Option<String>,
/// The card holder's nick name /// The card holder's nick name
#[schema(value_type = Option<String>, example = "John Test")] #[schema(value_type = Option<String>, example = "John Test")]
pub nick_name: Option<Secret<String>>, pub nick_name: Option<Secret<String>>,
@ -663,7 +671,10 @@ pub enum PaymentMethodData {
pub enum AdditionalPaymentData { pub enum AdditionalPaymentData {
Card { Card {
card_issuer: Option<String>, card_issuer: Option<String>,
card_network: Option<String>, card_network: Option<api_enums::CardNetwork>,
card_type: Option<String>,
card_issuing_country: Option<String>,
bank_code: Option<String>,
}, },
BankRedirect { BankRedirect {
bank_name: Option<api_enums::BankNames>, bank_name: Option<api_enums::BankNames>,
@ -678,37 +689,6 @@ pub enum AdditionalPaymentData {
Upi {}, Upi {},
} }
impl From<&PaymentMethodData> for AdditionalPaymentData {
fn from(pm_data: &PaymentMethodData) -> Self {
match pm_data {
PaymentMethodData::Card(card_data) => Self::Card {
card_issuer: card_data.card_issuer.to_owned(),
card_network: card_data
.card_network
.as_ref()
.map(|card_network| card_network.to_string()),
},
PaymentMethodData::BankRedirect(bank_redirect_data) => match bank_redirect_data {
BankRedirectData::Eps { bank_name, .. } => Self::BankRedirect {
bank_name: bank_name.to_owned(),
},
BankRedirectData::Ideal { bank_name, .. } => Self::BankRedirect {
bank_name: bank_name.to_owned(),
},
_ => Self::BankRedirect { bank_name: None },
},
PaymentMethodData::Wallet(_) => Self::Wallet {},
PaymentMethodData::PayLater(_) => Self::PayLater {},
PaymentMethodData::BankTransfer(_) => Self::BankTransfer {},
PaymentMethodData::Crypto(_) => Self::Crypto {},
PaymentMethodData::BankDebit(_) => Self::BankDebit {},
PaymentMethodData::MandatePayment => Self::MandatePayment {},
PaymentMethodData::Reward(_) => Self::Reward {},
PaymentMethodData::Upi(_) => Self::Upi {},
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum BankRedirectData { pub enum BankRedirectData {

View File

@ -1,6 +1,6 @@
use std::{fmt, ops::Deref, str::FromStr}; use std::{fmt, ops::Deref, str::FromStr};
use masking::{Strategy, StrongSecret, WithType}; use masking::{PeekInterface, Strategy, StrongSecret, WithType};
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error; use thiserror::Error;
@ -18,6 +18,12 @@ impl From<core::convert::Infallible> for CCValError {
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
pub struct CardNumber(StrongSecret<String, CardNumberStrategy>); pub struct CardNumber(StrongSecret<String, CardNumberStrategy>);
impl CardNumber {
pub fn get_card_isin(self) -> String {
self.0.peek().chars().take(6).collect::<String>()
}
}
impl FromStr for CardNumber { impl FromStr for CardNumber {
type Err = CCValError; type Err = CCValError;

View File

@ -98,6 +98,9 @@ impl From<StripeCard> for payments::Card {
card_cvc: card.cvc, card_cvc: card.cvc,
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
bank_code: None,
card_issuing_country: None,
card_type: None,
nick_name: None, nick_name: None,
} }
} }

View File

@ -89,6 +89,9 @@ impl From<StripeCard> for payments::Card {
card_cvc: card.cvc, card_cvc: card.cvc,
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
bank_code: None,
card_issuing_country: None,
card_type: None,
nick_name: None, nick_name: None,
} }
} }

View File

@ -106,6 +106,9 @@ impl Vaultable for api::Card {
card_cvc: value2.card_security_code.unwrap_or_default().into(), card_cvc: value2.card_security_code.unwrap_or_default().into(),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
bank_code: None,
card_issuing_country: None,
card_type: None,
nick_name: value1.nickname.map(masking::Secret::new), nick_name: value1.nickname.map(masking::Secret::new),
}; };

View File

@ -2449,3 +2449,93 @@ mod test {
); );
} }
} }
pub async fn get_additional_payment_data(
pm_data: &api_models::payments::PaymentMethodData,
db: &dyn StorageInterface,
) -> api_models::payments::AdditionalPaymentData {
match pm_data {
api_models::payments::PaymentMethodData::Card(card_data) => {
if card_data.card_issuer.is_some()
&& card_data.card_network.is_some()
&& card_data.card_type.is_some()
&& card_data.card_issuing_country.is_some()
&& card_data.bank_code.is_some()
{
api_models::payments::AdditionalPaymentData::Card {
card_issuer: card_data.card_issuer.to_owned(),
card_network: card_data.card_network.clone(),
card_type: card_data.card_type.to_owned(),
card_issuing_country: card_data.card_issuing_country.to_owned(),
bank_code: card_data.bank_code.to_owned(),
}
} else {
let card_number = card_data.clone().card_number;
let card_info = db
.get_card_info(&card_number.get_card_isin())
.await
.map_err(|error| services::logger::warn!(card_info_error=?error))
.ok()
.flatten()
.map(
|card_info| api_models::payments::AdditionalPaymentData::Card {
card_issuer: card_info.card_issuer,
card_network: card_info
.card_network
.clone()
.map(|network| network.foreign_into()),
bank_code: card_info.bank_code,
card_type: card_info.card_type,
card_issuing_country: card_info.card_issuing_country,
},
);
card_info.unwrap_or(api_models::payments::AdditionalPaymentData::Card {
card_issuer: None,
card_network: None,
bank_code: None,
card_type: None,
card_issuing_country: None,
})
}
}
api_models::payments::PaymentMethodData::BankRedirect(bank_redirect_data) => {
match bank_redirect_data {
api_models::payments::BankRedirectData::Eps { bank_name, .. } => {
api_models::payments::AdditionalPaymentData::BankRedirect {
bank_name: bank_name.to_owned(),
}
}
api_models::payments::BankRedirectData::Ideal { bank_name, .. } => {
api_models::payments::AdditionalPaymentData::BankRedirect {
bank_name: bank_name.to_owned(),
}
}
_ => api_models::payments::AdditionalPaymentData::BankRedirect { bank_name: None },
}
}
api_models::payments::PaymentMethodData::Wallet(_) => {
api_models::payments::AdditionalPaymentData::Wallet {}
}
api_models::payments::PaymentMethodData::PayLater(_) => {
api_models::payments::AdditionalPaymentData::PayLater {}
}
api_models::payments::PaymentMethodData::BankTransfer(_) => {
api_models::payments::AdditionalPaymentData::BankTransfer {}
}
api_models::payments::PaymentMethodData::Crypto(_) => {
api_models::payments::AdditionalPaymentData::Crypto {}
}
api_models::payments::PaymentMethodData::BankDebit(_) => {
api_models::payments::AdditionalPaymentData::BankDebit {}
}
api_models::payments::PaymentMethodData::MandatePayment => {
api_models::payments::AdditionalPaymentData::MandatePayment {}
}
api_models::payments::PaymentMethodData::Reward(_) => {
api_models::payments::AdditionalPaymentData::Reward {}
}
api_models::payments::PaymentMethodData::Upi(_) => {
api_models::payments::AdditionalPaymentData::Upi {}
}
}
}

View File

@ -397,7 +397,10 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
let additional_pm_data = payment_data let additional_pm_data = payment_data
.payment_method_data .payment_method_data
.as_ref() .as_ref()
.map(api_models::payments::AdditionalPaymentData::from) .async_map(|payment_method_data| async {
helpers::get_additional_payment_data(payment_method_data, db).await
})
.await
.as_ref() .as_ref()
.map(Encode::<api_models::payments::AdditionalPaymentData>::encode_to_value) .map(Encode::<api_models::payments::AdditionalPaymentData>::encode_to_value)
.transpose() .transpose()

View File

@ -55,7 +55,6 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await; let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await;
let merchant_id = &merchant_account.merchant_id; let merchant_id = &merchant_account.merchant_id;
let storage_scheme = merchant_account.storage_scheme; let storage_scheme = merchant_account.storage_scheme;
let (payment_intent, payment_attempt, connector_response); let (payment_intent, payment_attempt, connector_response);
let money @ (amount, currency) = payments_create_request_validation(request)?; let money @ (amount, currency) = payments_create_request_validation(request)?;
@ -116,7 +115,9 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
payment_method_type, payment_method_type,
request, request,
browser_info, browser_info,
)?, db,
)
.await?,
storage_scheme, storage_scheme,
) )
.await .await
@ -486,7 +487,8 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentCreate
impl PaymentCreate { impl PaymentCreate {
#[instrument(skip_all)] #[instrument(skip_all)]
fn make_payment_attempt( #[allow(clippy::too_many_arguments)]
pub async fn make_payment_attempt(
payment_id: &str, payment_id: &str,
merchant_id: &str, merchant_id: &str,
money: (api::Amount, enums::Currency), money: (api::Amount, enums::Currency),
@ -494,6 +496,7 @@ impl PaymentCreate {
payment_method_type: Option<enums::PaymentMethodType>, payment_method_type: Option<enums::PaymentMethodType>,
request: &api::PaymentsRequest, request: &api::PaymentsRequest,
browser_info: Option<serde_json::Value>, browser_info: Option<serde_json::Value>,
db: &dyn StorageInterface,
) -> RouterResult<storage::PaymentAttemptNew> { ) -> RouterResult<storage::PaymentAttemptNew> {
let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now());
let status = let status =
@ -503,7 +506,10 @@ impl PaymentCreate {
let additional_pm_data = request let additional_pm_data = request
.payment_method_data .payment_method_data
.as_ref() .as_ref()
.map(api_models::payments::AdditionalPaymentData::from) .async_map(|payment_method_data| async {
helpers::get_additional_payment_data(payment_method_data, db).await
})
.await
.as_ref() .as_ref()
.map(Encode::<api_models::payments::AdditionalPaymentData>::encode_to_value) .map(Encode::<api_models::payments::AdditionalPaymentData>::encode_to_value)
.transpose() .transpose()

View File

@ -429,7 +429,10 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
let additional_pm_data = payment_data let additional_pm_data = payment_data
.payment_method_data .payment_method_data
.as_ref() .as_ref()
.map(api_models::payments::AdditionalPaymentData::from) .async_map(|payment_method_data| async {
helpers::get_additional_payment_data(payment_method_data, db).await
})
.await
.as_ref() .as_ref()
.map(Encode::<api_models::payments::AdditionalPaymentData>::encode_to_value) .map(Encode::<api_models::payments::AdditionalPaymentData>::encode_to_value)
.transpose() .transpose()

View File

@ -233,6 +233,9 @@ mod payments_test {
card_cvc: "123".to_string().into(), card_cvc: "123".to_string().into(),
card_issuer: Some("HDFC".to_string()), card_issuer: Some("HDFC".to_string()),
card_network: Some(api_models::enums::CardNetwork::Visa), card_network: Some(api_models::enums::CardNetwork::Visa),
bank_code: None,
card_issuing_country: None,
card_type: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
} }
} }

View File

@ -174,6 +174,7 @@ impl ForeignFrom<api_models::payments::MandateType> for storage_enums::MandateDa
} }
} }
} }
impl ForeignFrom<storage_enums::MandateDataType> for api_models::payments::MandateType { impl ForeignFrom<storage_enums::MandateDataType> for api_models::payments::MandateType {
fn foreign_from(from: storage_enums::MandateDataType) -> Self { fn foreign_from(from: storage_enums::MandateDataType) -> Self {
match from { match from {
@ -572,7 +573,7 @@ impl ForeignFrom<storage_models::cards_info::CardInfo>
card_iin: item.card_iin, card_iin: item.card_iin,
card_type: item.card_type, card_type: item.card_type,
card_sub_type: item.card_subtype, card_sub_type: item.card_subtype,
card_network: item.card_network, card_network: item.card_network.map(|x| x.to_string()),
card_issuer: item.card_issuer, card_issuer: item.card_issuer,
card_issuing_country: item.card_issuing_country, card_issuing_country: item.card_issuing_country,
} }
@ -619,3 +620,9 @@ impl TryFrom<domain::MerchantConnectorAccount> for api_models::admin::MerchantCo
}) })
} }
} }
impl ForeignFrom<storage_models::enums::CardNetwork> for api_models::enums::CardNetwork {
fn foreign_from(source: storage_models::enums::CardNetwork) -> Self {
frunk::labelled_convert_from(source)
}
}

View File

@ -42,6 +42,9 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
card_cvc: Secret::new("999".to_string()), card_cvc: Secret::new("999".to_string()),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
}), }),
confirm: true, confirm: true,
@ -188,6 +191,9 @@ async fn payments_create_failure() {
card_cvc: Secret::new("99".to_string()), card_cvc: Secret::new("99".to_string()),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
}); });

View File

@ -70,6 +70,9 @@ impl AdyenTest {
card_cvc: Secret::new(card_cvc.to_string()), card_cvc: Secret::new(card_cvc.to_string()),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
}), }),
confirm: true, confirm: true,

View File

@ -62,6 +62,9 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
card_cvc: Secret::new("123".to_string()), card_cvc: Secret::new("123".to_string()),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
}), }),
capture_method: Some(storage_models::enums::CaptureMethod::Manual), capture_method: Some(storage_models::enums::CaptureMethod::Manual),

View File

@ -48,6 +48,9 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
card_cvc: Secret::new("123".to_string()), card_cvc: Secret::new("123".to_string()),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
}), }),
capture_method: Some(storage_models::enums::CaptureMethod::Manual), capture_method: Some(storage_models::enums::CaptureMethod::Manual),

View File

@ -48,6 +48,9 @@ async fn should_only_authorize_payment() {
card_cvc: Secret::new("123".to_string()), card_cvc: Secret::new("123".to_string()),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
}), }),
capture_method: Some(storage_models::enums::CaptureMethod::Manual), capture_method: Some(storage_models::enums::CaptureMethod::Manual),
@ -73,6 +76,9 @@ async fn should_authorize_and_capture_payment() {
card_cvc: Secret::new("123".to_string()), card_cvc: Secret::new("123".to_string()),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
}), }),
..utils::PaymentAuthorizeType::default().0 ..utils::PaymentAuthorizeType::default().0

View File

@ -489,6 +489,9 @@ impl Default for CCardType {
card_cvc: Secret::new("999".to_string()), card_cvc: Secret::new("999".to_string()),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
}) })
} }

View File

@ -73,6 +73,9 @@ impl WorldlineTest {
card_cvc: Secret::new(card_cvc.to_string()), card_cvc: Secret::new(card_cvc.to_string()),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
}), }),
confirm: true, confirm: true,

View File

@ -318,6 +318,9 @@ async fn payments_create_core() {
card_cvc: "123".to_string().into(), card_cvc: "123".to_string().into(),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
})), })),
payment_method: Some(api_enums::PaymentMethod::Card), payment_method: Some(api_enums::PaymentMethod::Card),
@ -476,6 +479,9 @@ async fn payments_create_core_adyen_no_redirect() {
card_cvc: "737".to_string().into(), card_cvc: "737".to_string().into(),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
})), })),
payment_method: Some(api_enums::PaymentMethod::Card), payment_method: Some(api_enums::PaymentMethod::Card),

View File

@ -78,6 +78,9 @@ async fn payments_create_core() {
card_cvc: "123".to_string().into(), card_cvc: "123".to_string().into(),
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
bank_code: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
})), })),
payment_method: Some(api_enums::PaymentMethod::Card), payment_method: Some(api_enums::PaymentMethod::Card),
@ -240,15 +243,14 @@ async fn payments_create_core_adyen_no_redirect() {
card_exp_year: "2030".to_string().into(), card_exp_year: "2030".to_string().into(),
card_holder_name: "JohnDoe".to_string().into(), card_holder_name: "JohnDoe".to_string().into(),
card_cvc: "737".to_string().into(), card_cvc: "737".to_string().into(),
bank_code: None,
card_issuer: None, card_issuer: None,
card_network: None, card_network: None,
card_type: None,
card_issuing_country: None,
nick_name: Some(masking::Secret::new("nick_name".into())), nick_name: Some(masking::Secret::new("nick_name".into())),
})), })),
payment_method: Some(api_enums::PaymentMethod::Card), payment_method: Some(api_enums::PaymentMethod::Card),
shipping: Some(api::Address {
address: None,
phone: None,
}),
billing: Some(api::Address { billing: Some(api::Address {
address: None, address: None,
phone: None, phone: None,

View File

@ -1,14 +1,14 @@
use diesel::{Identifiable, Queryable}; use diesel::{Identifiable, Queryable};
use time::PrimitiveDateTime; use time::PrimitiveDateTime;
use crate::schema::cards_info; use crate::{enums as storage_enums, schema::cards_info};
#[derive(Clone, Debug, Queryable, Identifiable, serde::Deserialize, serde::Serialize)] #[derive(Clone, Debug, Queryable, Identifiable, serde::Deserialize, serde::Serialize)]
#[diesel(table_name = cards_info, primary_key(card_iin))] #[diesel(table_name = cards_info, primary_key(card_iin))]
pub struct CardInfo { pub struct CardInfo {
pub card_iin: String, pub card_iin: String,
pub card_issuer: Option<String>, pub card_issuer: Option<String>,
pub card_network: Option<String>, pub card_network: Option<storage_enums::CardNetwork>,
pub card_type: Option<String>, pub card_type: Option<String>,
pub card_subtype: Option<String>, pub card_subtype: Option<String>,
pub card_issuing_country: Option<String>, pub card_issuing_country: Option<String>,

View File

@ -805,6 +805,33 @@ pub enum BankNames {
Boz, Boz,
} }
#[derive(
Clone,
Debug,
Eq,
Hash,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumString,
frunk::LabelledGeneric,
)]
#[router_derive::diesel_enum(storage_type = "text")]
pub enum CardNetwork {
Visa,
Mastercard,
AmericanExpress,
JCB,
DinersClub,
Discover,
CartesBancaires,
UnionPay,
Interac,
RuPay,
Maestro,
}
#[derive( #[derive(
Eq, Eq,
PartialEq, PartialEq,

View File

@ -2764,6 +2764,21 @@
], ],
"nullable": true "nullable": true
}, },
"card_type": {
"type": "string",
"example": "CREDIT",
"nullable": true
},
"card_issuing_country": {
"type": "string",
"example": "INDIA",
"nullable": true
},
"bank_code": {
"type": "string",
"example": "JP_AMEX",
"nullable": true
},
"nick_name": { "nick_name": {
"type": "string", "type": "string",
"description": "The card holder's nick name", "description": "The card holder's nick name",