feat(core): Add support to update card exp in update payment methods api (#9688)

This commit is contained in:
Mrudul Vajpayee
2025-10-07 17:11:04 +05:30
committed by GitHub
parent 7f6bed3f8e
commit ad37499785
4 changed files with 158 additions and 89 deletions

View File

@ -294,6 +294,7 @@ pub struct PaymentMethodRecordUpdateResponse {
pub status: common_enums::PaymentMethodStatus, pub status: common_enums::PaymentMethodStatus,
pub network_transaction_id: Option<String>, pub network_transaction_id: Option<String>,
pub connector_mandate_details: Option<pii::SecretSerdeValue>, pub connector_mandate_details: Option<pii::SecretSerdeValue>,
pub updated_payment_method_data: Option<bool>,
} }
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
@ -2689,7 +2690,9 @@ pub struct UpdatePaymentMethodRecord {
pub network_transaction_id: Option<String>, pub network_transaction_id: Option<String>,
pub line_number: Option<i64>, pub line_number: Option<i64>,
pub payment_instrument_id: Option<masking::Secret<String>>, pub payment_instrument_id: Option<masking::Secret<String>>,
pub merchant_connector_id: Option<id_type::MerchantConnectorAccountId>, pub merchant_connector_ids: Option<String>,
pub card_expiry_month: Option<masking::Secret<String>>,
pub card_expiry_year: Option<masking::Secret<String>>,
} }
#[derive(Debug, serde::Serialize)] #[derive(Debug, serde::Serialize)]
@ -2701,6 +2704,7 @@ pub struct PaymentMethodUpdateResponse {
pub update_status: UpdateStatus, pub update_status: UpdateStatus,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub update_error: Option<String>, pub update_error: Option<String>,
pub updated_payment_method_data: Option<bool>,
pub line_number: Option<i64>, pub line_number: Option<i64>,
} }
@ -2841,6 +2845,7 @@ impl From<PaymentMethodUpdateResponseType> for PaymentMethodUpdateResponse {
status: Some(res.status), status: Some(res.status),
network_transaction_id: res.network_transaction_id, network_transaction_id: res.network_transaction_id,
connector_mandate_details: res.connector_mandate_details, connector_mandate_details: res.connector_mandate_details,
updated_payment_method_data: res.updated_payment_method_data,
update_status: UpdateStatus::Success, update_status: UpdateStatus::Success,
update_error: None, update_error: None,
line_number: record.line_number, line_number: record.line_number,
@ -2850,6 +2855,7 @@ impl From<PaymentMethodUpdateResponseType> for PaymentMethodUpdateResponse {
status: record.status, status: record.status,
network_transaction_id: record.network_transaction_id, network_transaction_id: record.network_transaction_id,
connector_mandate_details: None, connector_mandate_details: None,
updated_payment_method_data: None,
update_status: UpdateStatus::Failed, update_status: UpdateStatus::Failed,
update_error: Some(e), update_error: Some(e),
line_number: record.line_number, line_number: record.line_number,

View File

@ -251,10 +251,11 @@ pub enum PaymentMethodUpdate {
connector_mandate_details: Option<pii::SecretSerdeValue>, connector_mandate_details: Option<pii::SecretSerdeValue>,
network_transaction_id: Option<Secret<String>>, network_transaction_id: Option<Secret<String>>,
}, },
ConnectorNetworkTransactionIdStatusAndMandateDetailsUpdate { PaymentMethodBatchUpdate {
connector_mandate_details: Option<pii::SecretSerdeValue>, connector_mandate_details: Option<pii::SecretSerdeValue>,
network_transaction_id: Option<String>, network_transaction_id: Option<String>,
status: Option<storage_enums::PaymentMethodStatus>, status: Option<storage_enums::PaymentMethodStatus>,
payment_method_data: Option<Encryption>,
}, },
} }
@ -687,13 +688,13 @@ impl From<PaymentMethodUpdate> for PaymentMethodUpdateInternal {
network_token_payment_method_data: None, network_token_payment_method_data: None,
scheme: None, scheme: None,
}, },
PaymentMethodUpdate::ConnectorNetworkTransactionIdStatusAndMandateDetailsUpdate { PaymentMethodUpdate::PaymentMethodBatchUpdate {
connector_mandate_details, connector_mandate_details,
network_transaction_id, network_transaction_id,
status, status,
payment_method_data,
} => Self { } => Self {
metadata: None, metadata: None,
payment_method_data: None,
last_used_at: None, last_used_at: None,
status, status,
locker_id: None, locker_id: None,
@ -709,6 +710,7 @@ impl From<PaymentMethodUpdate> for PaymentMethodUpdateInternal {
network_token_locker_id: None, network_token_locker_id: None,
network_token_payment_method_data: None, network_token_payment_method_data: None,
scheme: None, scheme: None,
payment_method_data,
}, },
} }
} }

View File

@ -77,10 +77,10 @@ pub struct PaymentMethodsMigrateForm {
pub merchant_connector_ids: Option<text::Text<String>>, pub merchant_connector_ids: Option<text::Text<String>>,
} }
struct MerchantConnectorValidator; pub struct MerchantConnectorValidator;
impl MerchantConnectorValidator { impl MerchantConnectorValidator {
fn parse_comma_separated_ids( pub fn parse_comma_separated_ids(
ids_string: &str, ids_string: &str,
) -> Result<Vec<common_utils::id_type::MerchantConnectorAccountId>, errors::ApiErrorResponse> ) -> Result<Vec<common_utils::id_type::MerchantConnectorAccountId>, errors::ApiErrorResponse>
{ {

View File

@ -7,12 +7,15 @@ use hyperswitch_domain_models::{
api::ApplicationResponse, errors::api_error_response as errors, merchant_context, api::ApplicationResponse, errors::api_error_response as errors, merchant_context,
payment_methods::PaymentMethodUpdate, payment_methods::PaymentMethodUpdate,
}; };
use masking::PeekInterface; use masking::{ExposeInterface, PeekInterface};
use payment_methods::core::migration::MerchantConnectorValidator;
use rdkafka::message::ToBytes; use rdkafka::message::ToBytes;
use router_env::logger; use router_env::logger;
use crate::{core::errors::StorageErrorExt, routes::SessionState}; use crate::{
core::{errors::StorageErrorExt, payment_methods::cards::create_encrypted_data},
routes::SessionState,
};
type PmMigrationResult<T> = CustomResult<ApplicationResponse<T>, errors::ApiErrorResponse>; type PmMigrationResult<T> = CustomResult<ApplicationResponse<T>, errors::ApiErrorResponse>;
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
@ -62,6 +65,8 @@ pub async fn update_payment_method_record(
let payment_method_id = req.payment_method_id.clone(); let payment_method_id = req.payment_method_id.clone();
let network_transaction_id = req.network_transaction_id.clone(); let network_transaction_id = req.network_transaction_id.clone();
let status = req.status; let status = req.status;
let key_manager_state = state.into();
let mut updated_card_expiry = false;
let payment_method = db let payment_method = db
.find_payment_method( .find_payment_method(
@ -79,94 +84,143 @@ pub async fn update_payment_method_record(
}.into()); }.into());
} }
// Process mandate details when both payment_instrument_id and merchant_connector_id are present let updated_payment_method_data = match payment_method.payment_method_data.as_ref() {
let pm_update = match (&req.payment_instrument_id, &req.merchant_connector_id) { Some(data) => {
(Some(payment_instrument_id), Some(merchant_connector_id)) => { match serde_json::from_value::<pm_api::PaymentMethodsData>(
data.clone().into_inner().expose(),
) {
Ok(pm_api::PaymentMethodsData::Card(mut card_data)) => {
if let Some(new_month) = &req.card_expiry_month {
card_data.expiry_month = Some(new_month.clone());
updated_card_expiry = true;
}
if let Some(new_year) = &req.card_expiry_year {
card_data.expiry_year = Some(new_year.clone());
updated_card_expiry = true;
}
if updated_card_expiry {
Some(
create_encrypted_data(
&key_manager_state,
merchant_context.get_merchant_key_store(),
pm_api::PaymentMethodsData::Card(card_data),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to encrypt payment method data")
.map(Into::into),
)
} else {
None
}
}
_ => None,
}
}
None => None,
}
.transpose()?;
// Process mandate details when both payment_instrument_id and merchant_connector_ids are present
let pm_update = match (
&req.payment_instrument_id,
&req.merchant_connector_ids.clone(),
) {
(Some(payment_instrument_id), Some(merchant_connector_ids)) => {
let parsed_mca_ids =
MerchantConnectorValidator::parse_comma_separated_ids(merchant_connector_ids)?;
let mandate_details = payment_method let mandate_details = payment_method
.get_common_mandate_reference() .get_common_mandate_reference()
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to deserialize to Payment Mandate Reference ")?; .attach_printable("Failed to deserialize to Payment Mandate Reference ")?;
let mca = db let mut existing_payments_mandate = mandate_details
.find_by_merchant_connector_account_merchant_id_merchant_connector_id( .payments
&state.into(), .clone()
merchant_context.get_merchant_account().get_id(), .unwrap_or(PaymentsMandateReference(HashMap::new()));
merchant_connector_id, let mut existing_payouts_mandate = mandate_details
merchant_context.get_merchant_key_store(), .payouts
) .clone()
.await .unwrap_or(PayoutsMandateReference(HashMap::new()));
.to_not_found_response(
errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
id: merchant_connector_id.get_string_repr().to_string(),
},
)?;
let updated_connector_mandate_details = match mca.connector_type { for merchant_connector_id in parsed_mca_ids {
enums::ConnectorType::PayoutProcessor => { let mca = db
// Handle PayoutsMandateReference .find_by_merchant_connector_account_merchant_id_merchant_connector_id(
let mut existing_payouts_mandate = mandate_details &state.into(),
.payouts merchant_context.get_merchant_account().get_id(),
.unwrap_or_else(|| PayoutsMandateReference(HashMap::new())); &merchant_connector_id,
merchant_context.get_merchant_key_store(),
)
.await
.to_not_found_response(
errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
id: merchant_connector_id.get_string_repr().to_string(),
},
)?;
// Create new payout mandate record match mca.connector_type {
let new_payout_record = PayoutsMandateReferenceRecord { enums::ConnectorType::PayoutProcessor => {
transfer_method_id: Some(payment_instrument_id.peek().to_string()), // Handle PayoutsMandateReference
}; let new_payout_record = PayoutsMandateReferenceRecord {
transfer_method_id: Some(payment_instrument_id.peek().to_string()),
};
// Check if record exists for this merchant_connector_id // Check if record exists for this merchant_connector_id
if let Some(existing_record) = if let Some(existing_record) =
existing_payouts_mandate.0.get_mut(merchant_connector_id) existing_payouts_mandate.0.get_mut(&merchant_connector_id)
{ {
if let Some(transfer_method_id) = &new_payout_record.transfer_method_id { if let Some(transfer_method_id) = &new_payout_record.transfer_method_id
existing_record.transfer_method_id = Some(transfer_method_id.clone()); {
existing_record.transfer_method_id =
Some(transfer_method_id.clone());
}
} else {
// Insert new record in connector_mandate_details
existing_payouts_mandate
.0
.insert(merchant_connector_id.clone(), new_payout_record);
} }
} else {
// Insert new record in connector_mandate_details
existing_payouts_mandate
.0
.insert(merchant_connector_id.clone(), new_payout_record);
} }
_ => {
// Create updated CommonMandateReference preserving payments section // Handle PaymentsMandateReference
CommonMandateReference { // Check if record exists for this merchant_connector_id
payments: mandate_details.payments, if let Some(existing_record) =
payouts: Some(existing_payouts_mandate), existing_payments_mandate.0.get_mut(&merchant_connector_id)
{
existing_record.connector_mandate_id =
payment_instrument_id.peek().to_string();
} else {
// Insert new record in connector_mandate_details
existing_payments_mandate.0.insert(
merchant_connector_id.clone(),
PaymentsMandateReferenceRecord {
connector_mandate_id: payment_instrument_id.peek().to_string(),
payment_method_type: None,
original_payment_authorized_amount: None,
original_payment_authorized_currency: None,
mandate_metadata: None,
connector_mandate_status: None,
connector_mandate_request_reference_id: None,
},
);
}
} }
} }
_ => { }
// Handle PaymentsMandateReference
let mut existing_payments_mandate = mandate_details
.payments
.unwrap_or_else(|| PaymentsMandateReference(HashMap::new()));
// Check if record exists for this merchant_connector_id let updated_connector_mandate_details = CommonMandateReference {
if let Some(existing_record) = payments: if !existing_payments_mandate.0.is_empty() {
existing_payments_mandate.0.get_mut(merchant_connector_id) Some(existing_payments_mandate)
{ } else {
existing_record.connector_mandate_id = mandate_details.payments
payment_instrument_id.peek().to_string(); },
} else { payouts: if !existing_payouts_mandate.0.is_empty() {
// Insert new record in connector_mandate_details Some(existing_payouts_mandate)
existing_payments_mandate.0.insert( } else {
merchant_connector_id.clone(), mandate_details.payouts
PaymentsMandateReferenceRecord { },
connector_mandate_id: payment_instrument_id.peek().to_string(),
payment_method_type: None,
original_payment_authorized_amount: None,
original_payment_authorized_currency: None,
mandate_metadata: None,
connector_mandate_status: None,
connector_mandate_request_reference_id: None,
},
);
}
// Create updated CommonMandateReference preserving payouts section
CommonMandateReference {
payments: Some(existing_payments_mandate),
payouts: mandate_details.payouts,
}
}
}; };
let connector_mandate_details_value = updated_connector_mandate_details let connector_mandate_details_value = updated_connector_mandate_details
@ -176,19 +230,25 @@ pub async fn update_payment_method_record(
errors::ApiErrorResponse::MandateUpdateFailed errors::ApiErrorResponse::MandateUpdateFailed
})?; })?;
PaymentMethodUpdate::ConnectorNetworkTransactionIdStatusAndMandateDetailsUpdate { PaymentMethodUpdate::PaymentMethodBatchUpdate {
connector_mandate_details: Some(pii::SecretSerdeValue::new( connector_mandate_details: Some(pii::SecretSerdeValue::new(
connector_mandate_details_value, connector_mandate_details_value,
)), )),
network_transaction_id, network_transaction_id,
status, status,
payment_method_data: updated_payment_method_data.clone(),
} }
} }
_ => { _ => {
// Update only network_transaction_id and status if updated_payment_method_data.is_some() {
PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate { PaymentMethodUpdate::PaymentMethodDataUpdate {
network_transaction_id, payment_method_data: updated_payment_method_data,
status, }
} else {
PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate {
network_transaction_id,
status,
}
} }
} }
}; };
@ -217,6 +277,7 @@ pub async fn update_payment_method_record(
connector_mandate_details: response connector_mandate_details: response
.connector_mandate_details .connector_mandate_details
.map(pii::SecretSerdeValue::new), .map(pii::SecretSerdeValue::new),
updated_payment_method_data: Some(updated_card_expiry),
}, },
)) ))
} }