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 network_transaction_id: Option<String>,
pub connector_mandate_details: Option<pii::SecretSerdeValue>,
pub updated_payment_method_data: Option<bool>,
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
@ -2689,7 +2690,9 @@ pub struct UpdatePaymentMethodRecord {
pub network_transaction_id: Option<String>,
pub line_number: Option<i64>,
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)]
@ -2701,6 +2704,7 @@ pub struct PaymentMethodUpdateResponse {
pub update_status: UpdateStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_error: Option<String>,
pub updated_payment_method_data: Option<bool>,
pub line_number: Option<i64>,
}
@ -2841,6 +2845,7 @@ impl From<PaymentMethodUpdateResponseType> for PaymentMethodUpdateResponse {
status: Some(res.status),
network_transaction_id: res.network_transaction_id,
connector_mandate_details: res.connector_mandate_details,
updated_payment_method_data: res.updated_payment_method_data,
update_status: UpdateStatus::Success,
update_error: None,
line_number: record.line_number,
@ -2850,6 +2855,7 @@ impl From<PaymentMethodUpdateResponseType> for PaymentMethodUpdateResponse {
status: record.status,
network_transaction_id: record.network_transaction_id,
connector_mandate_details: None,
updated_payment_method_data: None,
update_status: UpdateStatus::Failed,
update_error: Some(e),
line_number: record.line_number,

View File

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

View File

@ -77,10 +77,10 @@ pub struct PaymentMethodsMigrateForm {
pub merchant_connector_ids: Option<text::Text<String>>,
}
struct MerchantConnectorValidator;
pub struct MerchantConnectorValidator;
impl MerchantConnectorValidator {
fn parse_comma_separated_ids(
pub fn parse_comma_separated_ids(
ids_string: &str,
) -> 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,
payment_methods::PaymentMethodUpdate,
};
use masking::PeekInterface;
use masking::{ExposeInterface, PeekInterface};
use payment_methods::core::migration::MerchantConnectorValidator;
use rdkafka::message::ToBytes;
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>;
#[cfg(feature = "v1")]
@ -62,6 +65,8 @@ pub async fn update_payment_method_record(
let payment_method_id = req.payment_method_id.clone();
let network_transaction_id = req.network_transaction_id.clone();
let status = req.status;
let key_manager_state = state.into();
let mut updated_card_expiry = false;
let payment_method = db
.find_payment_method(
@ -79,94 +84,143 @@ pub async fn update_payment_method_record(
}.into());
}
// Process mandate details when both payment_instrument_id and merchant_connector_id are present
let pm_update = match (&req.payment_instrument_id, &req.merchant_connector_id) {
(Some(payment_instrument_id), Some(merchant_connector_id)) => {
let updated_payment_method_data = match payment_method.payment_method_data.as_ref() {
Some(data) => {
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
.get_common_mandate_reference()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to deserialize to Payment Mandate Reference ")?;
let mca = db
.find_by_merchant_connector_account_merchant_id_merchant_connector_id(
&state.into(),
merchant_context.get_merchant_account().get_id(),
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(),
},
)?;
let mut existing_payments_mandate = mandate_details
.payments
.clone()
.unwrap_or(PaymentsMandateReference(HashMap::new()));
let mut existing_payouts_mandate = mandate_details
.payouts
.clone()
.unwrap_or(PayoutsMandateReference(HashMap::new()));
let updated_connector_mandate_details = match mca.connector_type {
enums::ConnectorType::PayoutProcessor => {
// Handle PayoutsMandateReference
let mut existing_payouts_mandate = mandate_details
.payouts
.unwrap_or_else(|| PayoutsMandateReference(HashMap::new()));
for merchant_connector_id in parsed_mca_ids {
let mca = db
.find_by_merchant_connector_account_merchant_id_merchant_connector_id(
&state.into(),
merchant_context.get_merchant_account().get_id(),
&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
let new_payout_record = PayoutsMandateReferenceRecord {
transfer_method_id: Some(payment_instrument_id.peek().to_string()),
};
match mca.connector_type {
enums::ConnectorType::PayoutProcessor => {
// 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
if let Some(existing_record) =
existing_payouts_mandate.0.get_mut(merchant_connector_id)
{
if let Some(transfer_method_id) = &new_payout_record.transfer_method_id {
existing_record.transfer_method_id = Some(transfer_method_id.clone());
// Check if record exists for this merchant_connector_id
if let Some(existing_record) =
existing_payouts_mandate.0.get_mut(&merchant_connector_id)
{
if let Some(transfer_method_id) = &new_payout_record.transfer_method_id
{
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
CommonMandateReference {
payments: mandate_details.payments,
payouts: Some(existing_payouts_mandate),
_ => {
// Handle PaymentsMandateReference
// Check if record exists for this merchant_connector_id
if let Some(existing_record) =
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
if let Some(existing_record) =
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,
},
);
}
// Create updated CommonMandateReference preserving payouts section
CommonMandateReference {
payments: Some(existing_payments_mandate),
payouts: mandate_details.payouts,
}
}
let updated_connector_mandate_details = CommonMandateReference {
payments: if !existing_payments_mandate.0.is_empty() {
Some(existing_payments_mandate)
} else {
mandate_details.payments
},
payouts: if !existing_payouts_mandate.0.is_empty() {
Some(existing_payouts_mandate)
} else {
mandate_details.payouts
},
};
let connector_mandate_details_value = updated_connector_mandate_details
@ -176,19 +230,25 @@ pub async fn update_payment_method_record(
errors::ApiErrorResponse::MandateUpdateFailed
})?;
PaymentMethodUpdate::ConnectorNetworkTransactionIdStatusAndMandateDetailsUpdate {
PaymentMethodUpdate::PaymentMethodBatchUpdate {
connector_mandate_details: Some(pii::SecretSerdeValue::new(
connector_mandate_details_value,
)),
network_transaction_id,
status,
payment_method_data: updated_payment_method_data.clone(),
}
}
_ => {
// Update only network_transaction_id and status
PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate {
network_transaction_id,
status,
if updated_payment_method_data.is_some() {
PaymentMethodUpdate::PaymentMethodDataUpdate {
payment_method_data: updated_payment_method_data,
}
} 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
.map(pii::SecretSerdeValue::new),
updated_payment_method_data: Some(updated_card_expiry),
},
))
}