feat(router): add card_discovery in payment_attempt (#7039)

Co-authored-by: hrithikesh026 <hrithikesh.vm@juspay.in>
This commit is contained in:
Sai Harsha Vardhan
2025-02-04 22:43:02 +05:30
committed by GitHub
parent e2ddcc26b8
commit b9aa3ab445
19 changed files with 127 additions and 3 deletions

View File

@@ -181,6 +181,31 @@ impl AttemptStatus {
}
}
/// Indicates the method by which a card is discovered during a payment
#[derive(
Clone,
Copy,
Debug,
Default,
Hash,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumString,
ToSchema,
)]
#[router_derive::diesel_enum(storage_type = "db_enum")]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum CardDiscovery {
#[default]
Manual,
SavedCard,
ClickToPay,
}
/// Pass this parameter to force 3DS or non 3DS auth for this payment. Some connectors will still force 3DS auth even in case of passing 'no_three_ds' here and vice versa. Default value is 'no_three_ds' if not set
#[derive(
Clone,

View File

@@ -4,8 +4,8 @@ pub mod diesel_exports {
DbApiVersion as ApiVersion, DbAttemptStatus as AttemptStatus,
DbAuthenticationType as AuthenticationType, DbBlocklistDataKind as BlocklistDataKind,
DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus,
DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType,
DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency,
DbCardDiscovery as CardDiscovery, DbConnectorStatus as ConnectorStatus,
DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency,
DbDashboardMetadata as DashboardMetadata, DbDeleteStatus as DeleteStatus,
DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus,
DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType,

View File

@@ -94,6 +94,7 @@ pub struct PaymentAttempt {
pub shipping_cost: Option<MinorUnit>,
pub order_tax_amount: Option<MinorUnit>,
pub connector_mandate_detail: Option<ConnectorMandateReferenceId>,
pub card_discovery: Option<storage_enums::CardDiscovery>,
}
#[cfg(feature = "v1")]
@@ -172,6 +173,7 @@ pub struct PaymentAttempt {
pub order_tax_amount: Option<MinorUnit>,
pub connector_transaction_data: Option<String>,
pub connector_mandate_detail: Option<ConnectorMandateReferenceId>,
pub card_discovery: Option<storage_enums::CardDiscovery>,
}
#[cfg(feature = "v1")]
@@ -278,6 +280,7 @@ pub struct PaymentAttemptNew {
pub payment_method_subtype: storage_enums::PaymentMethodType,
pub id: id_type::GlobalAttemptId,
pub connector_mandate_detail: Option<ConnectorMandateReferenceId>,
pub card_discovery: Option<storage_enums::CardDiscovery>,
}
#[cfg(feature = "v1")]
@@ -351,6 +354,7 @@ pub struct PaymentAttemptNew {
pub shipping_cost: Option<MinorUnit>,
pub order_tax_amount: Option<MinorUnit>,
pub connector_mandate_detail: Option<ConnectorMandateReferenceId>,
pub card_discovery: Option<storage_enums::CardDiscovery>,
}
#[cfg(feature = "v1")]
@@ -423,6 +427,7 @@ pub enum PaymentAttemptUpdate {
shipping_cost: Option<MinorUnit>,
order_tax_amount: Option<MinorUnit>,
connector_mandate_detail: Option<ConnectorMandateReferenceId>,
card_discovery: Option<storage_enums::CardDiscovery>,
},
VoidUpdate {
status: storage_enums::AttemptStatus,
@@ -848,6 +853,7 @@ pub struct PaymentAttemptUpdateInternal {
pub order_tax_amount: Option<MinorUnit>,
pub connector_transaction_data: Option<String>,
pub connector_mandate_detail: Option<ConnectorMandateReferenceId>,
pub card_discovery: Option<common_enums::CardDiscovery>,
}
#[cfg(feature = "v1")]
@@ -1031,6 +1037,7 @@ impl PaymentAttemptUpdate {
order_tax_amount,
connector_transaction_data,
connector_mandate_detail,
card_discovery,
} = PaymentAttemptUpdateInternal::from(self).populate_derived_fields(&source);
PaymentAttempt {
amount: amount.unwrap_or(source.amount),
@@ -1089,6 +1096,7 @@ impl PaymentAttemptUpdate {
connector_transaction_data: connector_transaction_data
.or(source.connector_transaction_data),
connector_mandate_detail: connector_mandate_detail.or(source.connector_mandate_detail),
card_discovery: card_discovery.or(source.card_discovery),
..source
}
}
@@ -2141,6 +2149,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::AuthenticationTypeUpdate {
authentication_type,
@@ -2197,6 +2206,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::ConfirmUpdate {
amount,
@@ -2232,6 +2242,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
shipping_cost,
order_tax_amount,
connector_mandate_detail,
card_discovery,
} => Self {
amount: Some(amount),
currency: Some(currency),
@@ -2284,6 +2295,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount,
connector_transaction_data: None,
connector_mandate_detail,
card_discovery,
},
PaymentAttemptUpdate::VoidUpdate {
status,
@@ -2341,6 +2353,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::RejectUpdate {
status,
@@ -2399,6 +2412,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::BlocklistUpdate {
status,
@@ -2457,6 +2471,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::ConnectorMandateDetailUpdate {
connector_mandate_detail,
@@ -2513,6 +2528,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail,
card_discovery: None,
},
PaymentAttemptUpdate::PaymentMethodDetailsUpdate {
payment_method_id,
@@ -2569,6 +2585,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::ResponseUpdate {
status,
@@ -2650,6 +2667,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
shipping_cost: None,
order_tax_amount: None,
connector_mandate_detail,
card_discovery: None,
}
}
PaymentAttemptUpdate::ErrorUpdate {
@@ -2723,6 +2741,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
shipping_cost: None,
order_tax_amount: None,
connector_mandate_detail: None,
card_discovery: None,
}
}
PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self {
@@ -2777,6 +2796,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::UpdateTrackers {
payment_token,
@@ -2839,6 +2859,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::UnresolvedResponseUpdate {
status,
@@ -2908,6 +2929,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
shipping_cost: None,
order_tax_amount: None,
connector_mandate_detail: None,
card_discovery: None,
}
}
PaymentAttemptUpdate::PreprocessingUpdate {
@@ -2976,6 +2998,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
shipping_cost: None,
order_tax_amount: None,
connector_mandate_detail: None,
card_discovery: None,
}
}
PaymentAttemptUpdate::CaptureUpdate {
@@ -3034,6 +3057,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::AmountToCaptureUpdate {
status,
@@ -3091,6 +3115,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::ConnectorResponse {
authentication_data,
@@ -3157,6 +3182,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
shipping_cost: None,
order_tax_amount: None,
connector_mandate_detail: None,
card_discovery: None,
}
}
PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate {
@@ -3214,6 +3240,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::AuthenticationUpdate {
status,
@@ -3273,6 +3300,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
PaymentAttemptUpdate::ManualUpdate {
status,
@@ -3341,6 +3369,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
shipping_cost: None,
order_tax_amount: None,
connector_mandate_detail: None,
card_discovery: None,
}
}
PaymentAttemptUpdate::PostSessionTokensUpdate {
@@ -3398,6 +3427,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
order_tax_amount: None,
connector_transaction_data: None,
connector_mandate_detail: None,
card_discovery: None,
},
}
}

View File

@@ -907,6 +907,7 @@ diesel::table! {
#[max_length = 512]
connector_transaction_data -> Nullable<Varchar>,
connector_mandate_detail -> Nullable<Jsonb>,
card_discovery -> Nullable<CardDiscovery>,
}
}

View File

@@ -877,6 +877,7 @@ diesel::table! {
shipping_cost -> Nullable<Int8>,
order_tax_amount -> Nullable<Int8>,
connector_mandate_detail -> Nullable<Jsonb>,
card_discovery -> Nullable<CardDiscovery>,
}
}

View File

@@ -203,6 +203,7 @@ pub struct PaymentAttemptBatchNew {
pub order_tax_amount: Option<MinorUnit>,
pub connector_transaction_data: Option<String>,
pub connector_mandate_detail: Option<ConnectorMandateReferenceId>,
pub card_discovery: Option<common_enums::CardDiscovery>,
}
#[cfg(feature = "v1")]
@@ -282,6 +283,7 @@ impl PaymentAttemptBatchNew {
shipping_cost: self.shipping_cost,
order_tax_amount: self.order_tax_amount,
connector_mandate_detail: self.connector_mandate_detail,
card_discovery: self.card_discovery,
}
}
}

View File

@@ -402,6 +402,8 @@ pub struct PaymentAttempt {
pub id: id_type::GlobalAttemptId,
/// The connector mandate details which are stored temporarily
pub connector_mandate_detail: Option<ConnectorMandateReferenceId>,
/// Indicates the method by which a card is discovered during a payment
pub card_discovery: Option<common_enums::CardDiscovery>,
}
impl PaymentAttempt {
@@ -520,6 +522,7 @@ impl PaymentAttempt {
error: None,
connector_mandate_detail: None,
id,
card_discovery: None,
})
}
}
@@ -590,6 +593,7 @@ pub struct PaymentAttempt {
pub profile_id: id_type::ProfileId,
pub organization_id: id_type::OrganizationId,
pub connector_mandate_detail: Option<ConnectorMandateReferenceId>,
pub card_discovery: Option<common_enums::CardDiscovery>,
}
#[cfg(feature = "v1")]
@@ -836,6 +840,7 @@ pub struct PaymentAttemptNew {
pub profile_id: id_type::ProfileId,
pub organization_id: id_type::OrganizationId,
pub connector_mandate_detail: Option<ConnectorMandateReferenceId>,
pub card_discovery: Option<common_enums::CardDiscovery>,
}
#[cfg(feature = "v1")]
@@ -902,6 +907,7 @@ pub enum PaymentAttemptUpdate {
client_version: Option<String>,
customer_acceptance: Option<pii::SecretSerdeValue>,
connector_mandate_detail: Option<ConnectorMandateReferenceId>,
card_discovery: Option<common_enums::CardDiscovery>,
},
RejectUpdate {
status: storage_enums::AttemptStatus,
@@ -1154,6 +1160,7 @@ impl PaymentAttemptUpdate {
client_version,
customer_acceptance,
connector_mandate_detail,
card_discovery,
} => DieselPaymentAttemptUpdate::ConfirmUpdate {
amount: net_amount.get_order_amount(),
currency,
@@ -1188,6 +1195,7 @@ impl PaymentAttemptUpdate {
shipping_cost: net_amount.get_shipping_cost(),
order_tax_amount: net_amount.get_order_tax_amount(),
connector_mandate_detail,
card_discovery,
},
Self::VoidUpdate {
status,
@@ -1551,6 +1559,7 @@ impl behaviour::Conversion for PaymentAttempt {
order_tax_amount: self.net_amount.get_order_tax_amount(),
shipping_cost: self.net_amount.get_shipping_cost(),
connector_mandate_detail: self.connector_mandate_detail,
card_discovery: self.card_discovery,
})
}
@@ -1632,6 +1641,7 @@ impl behaviour::Conversion for PaymentAttempt {
profile_id: storage_model.profile_id,
organization_id: storage_model.organization_id,
connector_mandate_detail: storage_model.connector_mandate_detail,
card_discovery: storage_model.card_discovery,
})
}
.await
@@ -1714,6 +1724,7 @@ impl behaviour::Conversion for PaymentAttempt {
order_tax_amount: self.net_amount.get_order_tax_amount(),
shipping_cost: self.net_amount.get_shipping_cost(),
connector_mandate_detail: self.connector_mandate_detail,
card_discovery: self.card_discovery,
})
}
}
@@ -1781,6 +1792,7 @@ impl behaviour::Conversion for PaymentAttempt {
payment_method_billing_address,
connector,
connector_mandate_detail,
card_discovery,
} = self;
let AttemptAmountDetails {
@@ -1858,6 +1870,7 @@ impl behaviour::Conversion for PaymentAttempt {
payment_method_billing_address: payment_method_billing_address.map(Encryption::from),
connector_payment_data,
connector_mandate_detail,
card_discovery,
})
}
@@ -1969,6 +1982,7 @@ impl behaviour::Conversion for PaymentAttempt {
connector: storage_model.connector,
payment_method_billing_address,
connector_mandate_detail: storage_model.connector_mandate_detail,
card_discovery: storage_model.card_discovery,
})
}
.await
@@ -2053,6 +2067,7 @@ impl behaviour::Conversion for PaymentAttempt {
payment_method_type_v2: self.payment_method_type,
id: self.id,
connector_mandate_detail: self.connector_mandate_detail,
card_discovery: self.card_discovery,
})
}
}

View File

@@ -4450,6 +4450,28 @@ pub struct PaymentEvent {
}
impl<F: Clone> PaymentData<F> {
// Get the method by which a card is discovered during a payment
#[cfg(feature = "v1")]
fn get_card_discovery_for_card_payment_method(&self) -> Option<common_enums::CardDiscovery> {
match self.payment_attempt.payment_method {
Some(storage_enums::PaymentMethod::Card) => {
if self
.token_data
.as_ref()
.map(storage::PaymentTokenData::is_permanent_card)
.unwrap_or(false)
{
Some(common_enums::CardDiscovery::SavedCard)
} else if self.service_details.is_some() {
Some(common_enums::CardDiscovery::ClickToPay)
} else {
Some(common_enums::CardDiscovery::Manual)
}
}
_ => None,
}
}
fn to_event(&self) -> PaymentEvent {
PaymentEvent {
payment_intent: self.payment_intent.clone(),

View File

@@ -4289,6 +4289,7 @@ impl AttemptType {
organization_id: old_payment_attempt.organization_id,
profile_id: old_payment_attempt.profile_id,
connector_mandate_detail: None,
card_discovery: None,
}
}

View File

@@ -1585,7 +1585,7 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for
let m_payment_token = payment_token.clone();
let m_additional_pm_data = encoded_additional_pm_data
.clone()
.or(payment_data.payment_attempt.payment_method_data);
.or(payment_data.payment_attempt.payment_method_data.clone());
let m_business_sub_label = business_sub_label.clone();
let m_straight_through_algorithm = straight_through_algorithm.clone();
let m_error_code = error_code.clone();
@@ -1614,6 +1614,8 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for
None => (None, None, None),
};
let card_discovery = payment_data.get_card_discovery_for_card_payment_method();
let payment_attempt_fut = tokio::spawn(
async move {
m_db.update_payment_attempt_with_attempt_id(
@@ -1661,6 +1663,7 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for
connector_mandate_detail: payment_data
.payment_attempt
.connector_mandate_detail,
card_discovery,
},
storage_scheme,
)

View File

@@ -1306,6 +1306,7 @@ impl PaymentCreate {
organization_id: organization_id.clone(),
profile_id,
connector_mandate_detail: None,
card_discovery: None,
},
additional_pm_data,

View File

@@ -654,6 +654,7 @@ pub fn make_new_payment_attempt(
charge_id: Default::default(),
customer_acceptance: Default::default(),
connector_mandate_detail: Default::default(),
card_discovery: old_payment_attempt.card_discovery,
}
}

View File

@@ -217,6 +217,7 @@ mod tests {
profile_id: common_utils::generate_profile_id_of_default_length(),
organization_id: Default::default(),
connector_mandate_detail: Default::default(),
card_discovery: Default::default(),
};
let store = state
@@ -301,6 +302,7 @@ mod tests {
profile_id: common_utils::generate_profile_id_of_default_length(),
organization_id: Default::default(),
connector_mandate_detail: Default::default(),
card_discovery: Default::default(),
};
let store = state
.stores
@@ -398,6 +400,7 @@ mod tests {
profile_id: common_utils::generate_profile_id_of_default_length(),
organization_id: Default::default(),
connector_mandate_detail: Default::default(),
card_discovery: Default::default(),
};
let store = state
.stores

View File

@@ -103,6 +103,10 @@ impl PaymentTokenData {
pub fn wallet_token(payment_method_id: String) -> Self {
Self::WalletToken(WalletTokenData { payment_method_id })
}
pub fn is_permanent_card(&self) -> bool {
matches!(self, Self::PermanentCard(_) | Self::Permanent(_))
}
}
#[cfg(all(

View File

@@ -361,6 +361,7 @@ pub async fn generate_sample_data(
order_tax_amount: None,
connector_transaction_data,
connector_mandate_detail: None,
card_discovery: None,
};
let refund = if refunds_count < number_of_refunds && !is_failed_payment {

View File

@@ -195,6 +195,7 @@ impl PaymentAttemptInterface for MockDb {
organization_id: payment_attempt.organization_id,
profile_id: payment_attempt.profile_id,
connector_mandate_detail: payment_attempt.connector_mandate_detail,
card_discovery: payment_attempt.card_discovery,
};
payment_attempts.push(payment_attempt.clone());
Ok(payment_attempt)

View File

@@ -564,6 +564,7 @@ impl<T: DatabaseStore> PaymentAttemptInterface for KVRouterStore<T> {
organization_id: payment_attempt.organization_id.clone(),
profile_id: payment_attempt.profile_id.clone(),
connector_mandate_detail: payment_attempt.connector_mandate_detail.clone(),
card_discovery: payment_attempt.card_discovery,
};
let field = format!("pa_{}", created_attempt.attempt_id);
@@ -1511,6 +1512,7 @@ impl DataModelExt for PaymentAttempt {
shipping_cost: self.net_amount.get_shipping_cost(),
order_tax_amount: self.net_amount.get_order_tax_amount(),
connector_mandate_detail: self.connector_mandate_detail,
card_discovery: self.card_discovery,
}
}
@@ -1587,6 +1589,7 @@ impl DataModelExt for PaymentAttempt {
organization_id: storage_model.organization_id,
profile_id: storage_model.profile_id,
connector_mandate_detail: storage_model.connector_mandate_detail,
card_discovery: storage_model.card_discovery,
}
}
}
@@ -1670,6 +1673,7 @@ impl DataModelExt for PaymentAttemptNew {
shipping_cost: self.net_amount.get_shipping_cost(),
order_tax_amount: self.net_amount.get_order_tax_amount(),
connector_mandate_detail: self.connector_mandate_detail,
card_discovery: self.card_discovery,
}
}
@@ -1742,6 +1746,7 @@ impl DataModelExt for PaymentAttemptNew {
organization_id: storage_model.organization_id,
profile_id: storage_model.profile_id,
connector_mandate_detail: storage_model.connector_mandate_detail,
card_discovery: storage_model.card_discovery,
}
}
}

View File

@@ -0,0 +1,4 @@
-- This file should undo anything in `up.sql`
ALTER TABLE payment_attempt DROP COLUMN IF EXISTS card_discovery;
DROP TYPE IF EXISTS "CardDiscovery";

View File

@@ -0,0 +1,4 @@
-- Your SQL goes here
CREATE TYPE "CardDiscovery" AS ENUM ('manual', 'saved_card', 'click_to_pay');
ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS card_discovery "CardDiscovery";