From 5584f1131ae4180020be23d4c735b8356482c22d Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:25:59 +0530 Subject: [PATCH 1/8] refactor(connector): [adyen] add more fields in the payments request (#4010) --- config/deployments/production.toml | 2 +- config/development.toml | 6 +- .../src/connector/adyen/transformers.rs | 235 ++++++++++++++---- 3 files changed, 195 insertions(+), 48 deletions(-) diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 382ad018f1dd..7c543feed7e6 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -17,7 +17,7 @@ payout_connector_list = "wise" [connectors] aci.base_url = "https://eu-test.oppwa.com/" -adyen.base_url = "https://{{merchant_endpoint_prefix}}-checkout-live.adyenpayments.com/" +adyen.base_url = "https://{{merchant_endpoint_prefix}}-checkout-live.adyenpayments.com/checkout" adyen.secondary_base_url = "https://{{merchant_endpoint_prefix}}-pal-live.adyenpayments.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" diff --git a/config/development.toml b/config/development.toml index 6d4914af8b44..41497021c3af 100644 --- a/config/development.toml +++ b/config/development.toml @@ -88,7 +88,7 @@ vault_private_key = "" tunnel_private_key = "" [connectors.supported] -wallets = ["klarna", "braintree", "applepay"] +wallets = ["klarna", "braintree", "applepay", "adyen"] rewards = ["cashtocode", "zen"] cards = [ "aci", @@ -323,8 +323,8 @@ pay_bright = { country = "CA", currency = "CAD" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } giropay = { country = "DE", currency = "EUR" } eps = { country = "AT", currency = "EUR" } -sofort = { country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} -ideal = { country = "NL", currency = "EUR" } +sofort = {not_available_flows = { capture_method = "manual" }, country = "AT,BE,DE,ES,CH,NL", currency = "CHF,EUR"} +ideal = { not_available_flows = { capture_method = "manual" }, country = "NL", currency = "EUR" } blik = {country = "PL", currency = "PLN"} trustly = {country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK"} online_banking_czech_republic = {country = "CZ", currency = "EUR,CZK"} diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index c44d590e2f1c..8af99420fa45 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -9,12 +9,10 @@ use reqwest::Url; use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime, PrimitiveDateTime}; -#[cfg(feature = "payouts")] -use crate::{connector::utils::AddressDetailsData, types::api::payouts, utils::OptionExt}; use crate::{ connector::utils::{ - self, BrowserInformationData, CardData, MandateReferenceData, PaymentsAuthorizeRequestData, - RouterData, + self, AddressDetailsData, BrowserInformationData, CardData, MandateReferenceData, + PaymentsAuthorizeRequestData, RouterData, }, consts, core::errors, @@ -29,6 +27,8 @@ use crate::{ }, utils as crate_utils, }; +#[cfg(feature = "payouts")] +use crate::{types::api::payouts, utils::OptionExt}; type Error = error_stack::Report; @@ -130,12 +130,12 @@ pub struct ShopperName { #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Address { - city: Option, - country: Option, - house_number_or_name: Option>, - postal_code: Option>, + city: String, + country: api_enums::CountryAlpha2, + house_number_or_name: Secret, + postal_code: Secret, state_or_province: Option>, - street: Option>, + street: Secret, } #[derive(Debug, Serialize)] @@ -165,8 +165,11 @@ pub struct AdyenPaymentRequest<'a> { shopper_reference: Option, store_payment_method: Option, shopper_name: Option, + #[serde(rename = "shopperIP")] + shopper_ip: Option>, shopper_locale: Option, shopper_email: Option, + shopper_statement: Option, social_security_number: Option>, telephone_number: Option>, billing_address: Option
, @@ -174,6 +177,7 @@ pub struct AdyenPaymentRequest<'a> { country_code: Option, line_items: Option>, channel: Option, + metadata: Option, } #[derive(Debug, Serialize)] @@ -1729,16 +1733,22 @@ fn get_amount_data(item: &AdyenRouterData<&types::PaymentsAuthorizeRouterData>) } } -fn get_address_info(address: Option<&api_models::payments::Address>) -> Option
{ +fn get_address_info( + address: Option<&api_models::payments::Address>, +) -> Option>> { address.and_then(|add| { - add.address.as_ref().map(|a| Address { - city: a.city.clone(), - country: a.country, - house_number_or_name: a.line1.clone(), - postal_code: a.zip.clone(), - state_or_province: a.state.clone(), - street: a.line2.clone(), - }) + add.address.as_ref().map( + |a| -> Result> { + Ok(Address { + city: a.get_city()?.to_owned(), + country: a.get_country()?.to_owned(), + house_number_or_name: a.get_line1()?.to_owned(), + postal_code: a.get_zip()?.to_owned(), + state_or_province: a.state.clone(), + street: a.get_line2()?.to_owned(), + }) + }, + ) }) } @@ -1823,6 +1833,12 @@ fn get_social_security_number( } } +fn build_shopper_reference(customer_id: &Option, merchant_id: String) -> Option { + customer_id + .clone() + .map(|c_id| format!("{}_{}", merchant_id, c_id)) +} + impl<'a> TryFrom<&api_models::payments::BankDebitData> for AdyenPaymentMethod<'a> { type Error = Error; fn try_from( @@ -2125,27 +2141,81 @@ impl<'a> TryFrom<&api::WalletData> for AdyenPaymentMethod<'a> { } } -impl<'a> TryFrom<(&api::PayLaterData, Option)> - for AdyenPaymentMethod<'a> +pub fn check_required_field<'a, T>( + field: &'a Option, + message: &'static str, +) -> Result<&'a T, errors::ConnectorError> { + field + .as_ref() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: message, + }) +} + +impl<'a> + TryFrom<( + &api::PayLaterData, + &Option, + &Option, + &Option, + &Option, + &Option>, + &Option
, + &Option
, + )> for AdyenPaymentMethod<'a> { type Error = Error; fn try_from( - value: (&api::PayLaterData, Option), + value: ( + &api::PayLaterData, + &Option, + &Option, + &Option, + &Option, + &Option>, + &Option
, + &Option
, + ), ) -> Result { - let (pay_later_data, country_code) = value; + let ( + pay_later_data, + country_code, + shopper_email, + shopper_reference, + shopper_name, + telephone_number, + billing_address, + delivery_address, + ) = value; match pay_later_data { api_models::payments::PayLaterData::KlarnaRedirect { .. } => { let klarna = PmdForPaymentType { payment_type: PaymentType::Klarna, }; + check_required_field(shopper_email, "email")?; + check_required_field(shopper_reference, "customer_id")?; + check_required_field(country_code, "billing.country")?; + Ok(AdyenPaymentMethod::AdyenKlarna(Box::new(klarna))) } - api_models::payments::PayLaterData::AffirmRedirect { .. } => Ok( - AdyenPaymentMethod::AdyenAffirm(Box::new(PmdForPaymentType { - payment_type: PaymentType::Affirm, - })), - ), + api_models::payments::PayLaterData::AffirmRedirect { .. } => { + check_required_field(shopper_email, "email")?; + check_required_field(shopper_name, "billing.first_name, billing.last_name")?; + check_required_field(telephone_number, "billing.phone")?; + check_required_field(billing_address, "billing")?; + + Ok(AdyenPaymentMethod::AdyenAffirm(Box::new( + PmdForPaymentType { + payment_type: PaymentType::Affirm, + }, + ))) + } api_models::payments::PayLaterData::AfterpayClearpayRedirect { .. } => { + check_required_field(shopper_email, "email")?; + check_required_field(shopper_name, "billing.first_name, billing.last_name")?; + check_required_field(delivery_address, "shipping")?; + check_required_field(billing_address, "billing")?; + if let Some(country) = country_code { match country { api_enums::CountryAlpha2::IT @@ -2163,17 +2233,36 @@ impl<'a> TryFrom<(&api::PayLaterData, Option)> } } api_models::payments::PayLaterData::PayBrightRedirect { .. } => { + check_required_field(shopper_name, "billing.first_name, billing.last_name")?; + check_required_field(telephone_number, "billing.phone")?; + check_required_field(shopper_email, "email")?; + check_required_field(billing_address, "billing")?; + check_required_field(delivery_address, "shipping")?; + check_required_field(country_code, "billing.country")?; Ok(AdyenPaymentMethod::PayBright) } api_models::payments::PayLaterData::WalleyRedirect { .. } => { + //[TODO: Line items specific sub-fields are mandatory] + check_required_field(telephone_number, "billing.phone")?; + check_required_field(shopper_email, "email")?; Ok(AdyenPaymentMethod::Walley) } - api_models::payments::PayLaterData::AlmaRedirect { .. } => Ok( - AdyenPaymentMethod::AlmaPayLater(Box::new(PmdForPaymentType { - payment_type: PaymentType::Alma, - })), - ), + api_models::payments::PayLaterData::AlmaRedirect { .. } => { + check_required_field(telephone_number, "billing.phone")?; + check_required_field(shopper_email, "email")?; + check_required_field(billing_address, "billing")?; + check_required_field(delivery_address, "shipping")?; + Ok(AdyenPaymentMethod::AlmaPayLater(Box::new( + PmdForPaymentType { + payment_type: PaymentType::Alma, + }, + ))) + } api_models::payments::PayLaterData::AtomeRedirect { .. } => { + check_required_field(shopper_email, "email")?; + check_required_field(shopper_name, "billing.first_name, billing.last_name")?; + check_required_field(telephone_number, "billing.phone")?; + check_required_field(billing_address, "billing")?; Ok(AdyenPaymentMethod::Atome) } payments::PayLaterData::KlarnaSdk { .. } => Err(errors::ConnectorError::NotSupported { @@ -2544,6 +2633,9 @@ impl<'a> shopper_reference, store_payment_method, channel: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }) } } @@ -2564,12 +2656,22 @@ impl<'a> let amount = get_amount_data(item); let auth_type = AdyenAuthType::try_from(&item.router_data.connector_auth_type)?; let shopper_interaction = AdyenShopperInteraction::from(item.router_data); - let (recurring_processing_model, store_payment_method, shopper_reference) = + let shopper_reference = build_shopper_reference( + &item.router_data.customer_id, + item.router_data.merchant_id.clone(), + ); + let (recurring_processing_model, store_payment_method, _) = get_recurring_processing_model(item.router_data)?; let browser_info = get_browser_info(item.router_data)?; + let billing_address = + get_address_info(item.router_data.address.billing.as_ref()).transpose()?; + let country_code = get_country_code(item.router_data.address.billing.as_ref()); let additional_data = get_additional_data(item.router_data); let return_url = item.router_data.request.get_return_url()?; let payment_method = AdyenPaymentMethod::try_from(card_data)?; + let shopper_email = item.router_data.request.email.clone(); + let shopper_name = get_shopper_name(item.router_data.address.billing.as_ref()); + Ok(AdyenPaymentRequest { amount, merchant_account: auth_type.merchant_account, @@ -2581,17 +2683,20 @@ impl<'a> browser_info, additional_data, telephone_number: None, - shopper_name: None, - shopper_email: None, + shopper_name, + shopper_email, shopper_locale: None, social_security_number: None, - billing_address: None, + billing_address, delivery_address: None, - country_code: None, + country_code, line_items: None, shopper_reference, store_payment_method, channel: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }) } } @@ -2642,6 +2747,9 @@ impl<'a> shopper_reference: None, store_payment_method: None, channel: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }; Ok(request) } @@ -2693,6 +2801,9 @@ impl<'a> shopper_reference: None, store_payment_method: None, channel: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }; Ok(request) } @@ -2740,6 +2851,9 @@ impl<'a> shopper_reference: None, store_payment_method: None, channel: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }; Ok(request) } @@ -2787,6 +2901,9 @@ impl<'a> store_payment_method: None, channel: None, social_security_number: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }; Ok(request) } @@ -2841,6 +2958,9 @@ impl<'a> shopper_reference, store_payment_method, channel: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }) } } @@ -2935,6 +3055,9 @@ impl<'a> shopper_reference, store_payment_method, channel, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }) } } @@ -2958,18 +3081,33 @@ impl<'a> let browser_info = get_browser_info(item.router_data)?; let additional_data = get_additional_data(item.router_data); let country_code = get_country_code(item.router_data.address.billing.as_ref()); - let payment_method = AdyenPaymentMethod::try_from((paylater_data, country_code))?; let shopper_interaction = AdyenShopperInteraction::from(item.router_data); - let (recurring_processing_model, store_payment_method, shopper_reference) = + let shopper_reference = build_shopper_reference( + &item.router_data.customer_id, + item.router_data.merchant_id.clone(), + ); + let (recurring_processing_model, store_payment_method, _) = get_recurring_processing_model(item.router_data)?; let return_url = item.router_data.request.get_return_url()?; let shopper_name: Option = get_shopper_name(item.router_data.address.billing.as_ref()); let shopper_email = item.router_data.request.email.clone(); - let billing_address = get_address_info(item.router_data.address.billing.as_ref()); - let delivery_address = get_address_info(item.router_data.address.shipping.as_ref()); + let billing_address = + get_address_info(item.router_data.address.billing.as_ref()).transpose()?; + let delivery_address = + get_address_info(item.router_data.address.shipping.as_ref()).transpose()?; let line_items = Some(get_line_items(item)); let telephone_number = get_telephone_number(item.router_data); + let payment_method = AdyenPaymentMethod::try_from(( + paylater_data, + &country_code, + &shopper_email, + &shopper_reference, + &shopper_name, + &telephone_number, + &billing_address, + &delivery_address, + ))?; Ok(AdyenPaymentRequest { amount, merchant_account: auth_type.merchant_account, @@ -2992,6 +3130,9 @@ impl<'a> shopper_reference, store_payment_method, channel: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }) } } @@ -3047,6 +3188,9 @@ impl<'a> store_payment_method: None, channel: None, social_security_number: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone(), }) } } @@ -4522,7 +4666,8 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC date_of_birth: None, entity_type: Some(item.router_data.request.entity_type), nationality: get_country_code(item.router_data.address.billing.as_ref()), - billing_address: get_address_info(item.router_data.address.billing.as_ref()), + billing_address: get_address_info(item.router_data.address.billing.as_ref()) + .transpose()?, }) } PayoutMethodData::Wallet(wallet_data) => { @@ -4561,7 +4706,8 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC date_of_birth: None, entity_type: Some(item.router_data.request.entity_type), nationality: get_country_code(item.router_data.address.billing.as_ref()), - billing_address: get_address_info(item.router_data.address.billing.as_ref()), + billing_address: get_address_info(item.router_data.address.billing.as_ref()) + .transpose()?, }) } } @@ -4598,7 +4744,8 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutF currency: item.router_data.request.destination_currency, }, card: PayoutCardDetails::try_from(&item.router_data.get_payout_method_data()?)?, - billing_address: get_address_info(item.router_data.get_billing().ok()), + billing_address: get_address_info(item.router_data.get_billing().ok()) + .transpose()?, merchant_account, reference: item.router_data.request.payout_id.clone(), shopper_name: ShopperName { From 43ebfbc47f03eaaaf274847290861dcb00db26a5 Mon Sep 17 00:00:00 2001 From: Shanks Date: Mon, 11 Mar 2024 15:26:07 +0530 Subject: [PATCH 2/8] feat(router): add routing support for token-based mit payments (#4012) --- config/development.toml | 2 +- crates/api_models/src/payments.rs | 4 +- .../src/connector/dlocal/transformers.rs | 2 +- crates/router/src/core/mandate.rs | 82 +++---- crates/router/src/core/payments.rs | 211 +++++++++++++----- .../src/core/payments/flows/authorize_flow.rs | 4 + .../core/payments/flows/setup_mandate_flow.rs | 6 + crates/router/src/core/payments/helpers.rs | 147 +++++++----- .../payments/operations/payment_approve.rs | 2 + .../payments/operations/payment_cancel.rs | 2 + .../payments/operations/payment_capture.rs | 2 + .../operations/payment_complete_authorize.rs | 8 + .../payments/operations/payment_confirm.rs | 19 ++ .../payments/operations/payment_create.rs | 8 +- .../payments/operations/payment_reject.rs | 2 + .../payments/operations/payment_response.rs | 4 +- .../payments/operations/payment_session.rs | 2 + .../core/payments/operations/payment_start.rs | 11 + .../payments/operations/payment_status.rs | 4 +- .../payments/operations/payment_update.rs | 14 +- .../payments_incremental_authorization.rs | 2 + crates/router/src/core/payments/retry.rs | 2 +- .../router/src/core/payments/tokenization.rs | 29 ++- .../router/src/core/payments/transformers.rs | 4 +- crates/router/src/core/payouts/helpers.rs | 2 +- crates/router/src/core/routing.rs | 3 +- .../src/types/storage/payment_method.rs | 31 ++- 27 files changed, 429 insertions(+), 180 deletions(-) diff --git a/config/development.toml b/config/development.toml index 41497021c3af..16851c90ccaa 100644 --- a/config/development.toml +++ b/config/development.toml @@ -581,4 +581,4 @@ enabled = true file_storage_backend = "file_system" [unmasked_headers] -keys = "user-agent" \ No newline at end of file +keys = "user-agent" diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 6f97a87fbe9c..33d355d255ab 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -740,7 +740,7 @@ pub enum MandateTransactionType { #[derive(Default, Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct MandateIds { - pub mandate_id: String, + pub mandate_id: Option, pub mandate_reference_id: Option, } @@ -767,7 +767,7 @@ pub struct UpdateHistory { impl MandateIds { pub fn new(mandate_id: String) -> Self { Self { - mandate_id, + mandate_id: Some(mandate_id), mandate_reference_id: None, } } diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index 5eb682d4cfa4..c1b8731ee893 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -139,7 +139,7 @@ impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalP .request .mandate_id .as_ref() - .map(|ids| ids.mandate_id.clone()), + .and_then(|ids| ids.mandate_id.clone()), // [#595[FEATURE] Pass Mandate history information in payment flows/request] installments: item .router_data diff --git a/crates/router/src/core/mandate.rs b/crates/router/src/core/mandate.rs index d07a8814c888..2838edfbf91d 100644 --- a/crates/router/src/core/mandate.rs +++ b/crates/router/src/core/mandate.rs @@ -326,48 +326,52 @@ where Err(_) => {} Ok(_) => match resp.request.get_mandate_id() { Some(mandate_id) => { - let mandate_id = &mandate_id.mandate_id; - let mandate = state - .store - .find_mandate_by_merchant_id_mandate_id(resp.merchant_id.as_ref(), mandate_id) - .await - .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; - let mandate = match mandate.mandate_type { - storage_enums::MandateType::SingleUse => state + if let Some(ref mandate_id) = mandate_id.mandate_id { + let mandate = state .store - .update_mandate_by_merchant_id_mandate_id( - &resp.merchant_id, + .find_mandate_by_merchant_id_mandate_id( + resp.merchant_id.as_ref(), mandate_id, - storage::MandateUpdate::StatusUpdate { - mandate_status: storage_enums::MandateStatus::Revoked, - }, ) .await - .change_context(errors::ApiErrorResponse::MandateUpdateFailed), - storage_enums::MandateType::MultiUse => state - .store - .update_mandate_by_merchant_id_mandate_id( - &resp.merchant_id, - mandate_id, - storage::MandateUpdate::CaptureAmountUpdate { - amount_captured: Some( - mandate.amount_captured.unwrap_or(0) - + resp.request.get_amount(), - ), - }, - ) - .await - .change_context(errors::ApiErrorResponse::MandateUpdateFailed), - }?; - metrics::SUBSEQUENT_MANDATE_PAYMENT.add( - &metrics::CONTEXT, - 1, - &[metrics::request::add_attributes( - "connector", - mandate.connector, - )], - ); - resp.payment_method_id = Some(mandate.payment_method_id); + .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; + let mandate = match mandate.mandate_type { + storage_enums::MandateType::SingleUse => state + .store + .update_mandate_by_merchant_id_mandate_id( + &resp.merchant_id, + mandate_id, + storage::MandateUpdate::StatusUpdate { + mandate_status: storage_enums::MandateStatus::Revoked, + }, + ) + .await + .change_context(errors::ApiErrorResponse::MandateUpdateFailed), + storage_enums::MandateType::MultiUse => state + .store + .update_mandate_by_merchant_id_mandate_id( + &resp.merchant_id, + mandate_id, + storage::MandateUpdate::CaptureAmountUpdate { + amount_captured: Some( + mandate.amount_captured.unwrap_or(0) + + resp.request.get_amount(), + ), + }, + ) + .await + .change_context(errors::ApiErrorResponse::MandateUpdateFailed), + }?; + metrics::SUBSEQUENT_MANDATE_PAYMENT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes( + "connector", + mandate.connector, + )], + ); + resp.payment_method_id = Some(mandate.payment_method_id); + } } None => { if resp.request.get_setup_mandate_details().is_some() { @@ -409,7 +413,7 @@ where logger::debug!("{:?}", new_mandate_data); resp.request .set_mandate_id(Some(api_models::payments::MandateIds { - mandate_id: new_mandate_data.mandate_id.clone(), + mandate_id: Some(new_mandate_data.mandate_id.clone()), mandate_reference_id: new_mandate_data .connector_mandate_ids .clone() diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index ca3bbfd06519..2544bffda720 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -13,7 +13,10 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; -use api_models::{self, enums, payments::HeaderPayload}; +use api_models::{ + self, enums, + payments::{self as payments_api, HeaderPayload}, +}; use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge}; use data_models::mandates::{CustomerAcceptance, MandateData}; use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; @@ -2061,9 +2064,11 @@ where pub customer_acceptance: Option, pub address: PaymentAddress, pub token: Option, + pub token_data: Option, pub confirm: Option, pub force_sync: Option, pub payment_method_data: Option, + pub payment_method_info: Option, pub refunds: Vec, pub disputes: Vec, pub attempts: Option>, @@ -2652,7 +2657,10 @@ where .as_ref() .zip(payment_data.payment_attempt.payment_method_type.as_ref()) { - if let Some(choice) = pre_routing_results.get(storage_pm_type) { + if let (Some(choice), None) = ( + pre_routing_results.get(storage_pm_type), + &payment_data.token_data, + ) { let connector_data = api::ConnectorData::get_connector_by_name( &state.conf.connectors, &choice.connector.to_string(), @@ -2687,6 +2695,12 @@ where .attach_printable("Failed execution of straight through routing")?; if check_eligibility { + #[cfg(feature = "business_profile_routing")] + let profile_id = payment_data.payment_intent.profile_id.clone(); + + #[cfg(not(feature = "business_profile_routing"))] + let _profile_id: Option = None; + connectors = routing::perform_eligibility_analysis_with_fallback( &state.clone(), key_store, @@ -2695,20 +2709,13 @@ where &TransactionData::Payment(payment_data), eligible_connectors, #[cfg(feature = "business_profile_routing")] - payment_data.payment_intent.profile_id.clone(), + profile_id, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed eligibility analysis and fallback")?; } - let first_connector_choice = connectors - .first() - .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) - .into_report() - .attach_printable("Empty connector list returned")? - .clone(); - let connector_data = connectors .into_iter() .map(|conn| { @@ -2726,17 +2733,11 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Invalid connector name received")?; - routing_data.routed_through = Some(first_connector_choice.connector.to_string()); - #[cfg(feature = "connector_choice_mca_id")] - { - routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; - } - #[cfg(not(feature = "connector_choice_mca_id"))] - { - routing_data.business_sub_label = first_connector_choice.sub_label.clone(); - } - routing_data.routing_info.algorithm = Some(routing_algorithm); - return Ok(api::ConnectorCallType::Retryable(connector_data)); + return decide_connector_for_token_based_mit_flow( + payment_data, + routing_data, + connector_data, + ); } if let Some(ref routing_algorithm) = routing_data.routing_info.algorithm { @@ -2748,6 +2749,12 @@ where .attach_printable("Failed execution of straight through routing")?; if check_eligibility { + #[cfg(feature = "business_profile_routing")] + let profile_id = payment_data.payment_intent.profile_id.clone(); + + #[cfg(not(feature = "business_profile_routing"))] + let _profile_id: Option = None; + connectors = routing::perform_eligibility_analysis_with_fallback( &state, key_store, @@ -2756,20 +2763,13 @@ where &TransactionData::Payment(payment_data), eligible_connectors, #[cfg(feature = "business_profile_routing")] - payment_data.payment_intent.profile_id.clone(), + profile_id, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed eligibility analysis and fallback")?; } - let first_connector_choice = connectors - .first() - .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) - .into_report() - .attach_printable("Empty connector list returned")? - .clone(); - let connector_data = connectors .into_iter() .map(|conn| { @@ -2787,16 +2787,11 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Invalid connector name received")?; - routing_data.routed_through = Some(first_connector_choice.connector.to_string()); - #[cfg(feature = "connector_choice_mca_id")] - { - routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; - } - #[cfg(not(feature = "connector_choice_mca_id"))] - { - routing_data.business_sub_label = first_connector_choice.sub_label; - } - return Ok(api::ConnectorCallType::Retryable(connector_data)); + return decide_connector_for_token_based_mit_flow( + payment_data, + routing_data, + connector_data, + ); } route_connector_v1( @@ -2804,13 +2799,107 @@ where merchant_account, business_profile, key_store, - &TransactionData::Payment(payment_data), + TransactionData::Payment(payment_data), routing_data, eligible_connectors, ) .await } +pub fn decide_connector_for_token_based_mit_flow( + payment_data: &mut PaymentData, + routing_data: &mut storage::RoutingData, + connectors: Vec, +) -> RouterResult { + if let Some((storage_enums::FutureUsage::OffSession, _)) = payment_data + .payment_intent + .setup_future_usage + .zip(payment_data.token_data.as_ref()) + { + logger::debug!("performing routing for token-based MIT flow"); + + let payment_method_info = payment_data + .payment_method_info + .as_ref() + .get_required_value("payment_method_info")?; + + let connector_mandate_details = payment_method_info + .connector_mandate_details + .clone() + .map(|details| { + details.parse_value::("connector_mandate_details") + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize connector mandate details")? + .get_required_value("connector_mandate_details") + .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .attach_printable("no eligible connector found for token-based MIT flow since there were no connector mandate details")?; + + let mut connector_choice = None; + for connector_data in connectors { + if let Some(merchant_connector_id) = connector_data.merchant_connector_id.as_ref() { + if let Some(mandate_reference_record) = + connector_mandate_details.get(merchant_connector_id) + { + connector_choice = Some((connector_data, mandate_reference_record.clone())); + break; + } + } + } + + let (chosen_connector_data, mandate_reference_record) = connector_choice + .get_required_value("connector_choice") + .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .attach_printable("no eligible connector found for token-based MIT payment")?; + + routing_data.routed_through = Some(chosen_connector_data.connector_name.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = + chosen_connector_data.merchant_connector_id.clone(); + } + + payment_data.mandate_id = Some(payments_api::MandateIds { + mandate_id: None, + mandate_reference_id: Some(payments_api::MandateReferenceId::ConnectorMandateId( + payments_api::ConnectorMandateReferenceId { + connector_mandate_id: Some( + mandate_reference_record.connector_mandate_id.clone(), + ), + payment_method_id: Some(payment_method_info.payment_method_id.clone()), + update_history: None, + }, + )), + }); + + payment_data.recurring_mandate_payment_data = Some(RecurringMandatePaymentData { + payment_method_type: mandate_reference_record.payment_method_type, + original_payment_authorized_amount: mandate_reference_record + .original_payment_authorized_amount, + original_payment_authorized_currency: mandate_reference_record + .original_payment_authorized_currency, + }); + + Ok(api::ConnectorCallType::PreDetermined(chosen_connector_data)) + } else { + let first_choice = connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .into_report() + .attach_printable("no eligible connector found for payment")? + .clone(); + + routing_data.routed_through = Some(first_choice.connector_name.to_string()); + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = first_choice.merchant_connector_id; + } + + Ok(api::ConnectorCallType::Retryable(connectors)) + } +} + pub fn should_add_task_to_process_tracker(payment_data: &PaymentData) -> bool { let connector = payment_data.payment_attempt.connector.as_deref(); @@ -2940,7 +3029,7 @@ pub async fn route_connector_v1( merchant_account: &domain::MerchantAccount, business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, - transaction_data: &TransactionData<'_, F>, + transaction_data: TransactionData<'_, F>, routing_data: &mut storage::RoutingData, eligible_connectors: Option>, ) -> RouterResult @@ -2948,7 +3037,7 @@ where F: Send + Clone, { #[allow(unused_variables)] - let (profile_id, routing_algorithm) = match transaction_data { + let (profile_id, routing_algorithm) = match &transaction_data { TransactionData::Payment(payment_data) => { if cfg!(feature = "business_profile_routing") { ( @@ -2983,7 +3072,7 @@ where state, &merchant_account.merchant_id, algorithm_ref, - transaction_data, + &transaction_data, ) .await .change_context(errors::ApiErrorResponse::InternalServerError)?; @@ -2993,7 +3082,7 @@ where key_store, merchant_account.modified_at.assume_utc().unix_timestamp(), connectors, - transaction_data, + &transaction_data, eligible_connectors, #[cfg(feature = "business_profile_routing")] profile_id, @@ -3002,6 +3091,7 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed eligibility analysis and fallback")?; + #[cfg(feature = "payouts")] let first_connector_choice = connectors .first() .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) @@ -3009,17 +3099,6 @@ where .attach_printable("Empty connector list returned")? .clone(); - routing_data.routed_through = Some(first_connector_choice.connector.to_string()); - - #[cfg(feature = "connector_choice_mca_id")] - { - routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; - } - #[cfg(not(feature = "connector_choice_mca_id"))] - { - routing_data.business_sub_label = first_connector_choice.sub_label; - } - let connector_data = connectors .into_iter() .map(|conn| { @@ -3037,7 +3116,27 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Invalid connector name received")?; - Ok(ConnectorCallType::Retryable(connector_data)) + match transaction_data { + TransactionData::Payment(payment_data) => { + decide_connector_for_token_based_mit_flow(payment_data, routing_data, connector_data) + } + + #[cfg(feature = "payouts")] + TransactionData::Payout(_) => { + routing_data.routed_through = Some(first_connector_choice.connector.to_string()); + + #[cfg(feature = "connector_choice_mca_id")] + { + routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id; + } + #[cfg(not(feature = "connector_choice_mca_id"))] + { + routing_data.business_sub_label = first_connector_choice.sub_label; + } + + Ok(ConnectorCallType::Retryable(connector_data)) + } + } } #[instrument(skip_all)] diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 212e694ac926..efc0e8852da0 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -100,6 +100,8 @@ impl Feature for types::PaymentsAu merchant_account, self.request.payment_method_type, key_store, + Some(resp.request.amount), + Some(resp.request.currency), )) .await?; @@ -134,6 +136,8 @@ impl Feature for types::PaymentsAu &merchant_account, self.request.payment_method_type, &key_store, + Some(resp.request.amount), + Some(resp.request.currency), )) .await; diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index 65342a1f06bf..189a8a0a3eda 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -107,6 +107,8 @@ impl Feature for types::Setup merchant_account, self.request.payment_method_type, key_store, + resp.request.amount, + Some(resp.request.currency), )) .await?; @@ -244,6 +246,8 @@ impl types::SetupMandateRouterData { merchant_account, payment_method_type, key_store, + resp.request.amount, + Some(resp.request.currency), )) .await?; @@ -330,6 +334,8 @@ impl types::SetupMandateRouterData { merchant_account, self.request.payment_method_type, key_store, + resp.request.amount, + Some(resp.request.currency), )) .await? .0; diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 76ffed499d1f..68821ee50328 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1602,6 +1602,90 @@ pub async fn retrieve_card_with_permanent_token( Ok(api::PaymentMethodData::Card(api_card)) } +pub async fn retrieve_payment_method_from_db_with_token_data( + state: &AppState, + token_data: &storage::PaymentTokenData, +) -> RouterResult> { + match token_data { + storage::PaymentTokenData::PermanentCard(data) => { + if let Some(ref payment_method_id) = data.payment_method_id { + state + .store + .find_payment_method(payment_method_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("error retrieving payment method from DB") + .map(Some) + } else { + Ok(None) + } + } + + storage::PaymentTokenData::WalletToken(data) => state + .store + .find_payment_method(&data.payment_method_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("error retrieveing payment method from DB") + .map(Some), + + storage::PaymentTokenData::Temporary(_) + | storage::PaymentTokenData::TemporaryGeneric(_) + | storage::PaymentTokenData::Permanent(_) + | storage::PaymentTokenData::AuthBankDebit(_) => Ok(None), + } +} + +pub async fn retrieve_payment_token_data( + state: &AppState, + token: String, + payment_method: Option, +) -> RouterResult { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let key = format!( + "pm_token_{}_{}_hyperswitch", + token, + payment_method.get_required_value("payment_method")? + ); + + let token_data_string = redis_conn + .get_key::>(&key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch the token from redis")? + .ok_or(error_stack::Report::new( + errors::ApiErrorResponse::UnprocessableEntity { + message: "Token is invalid or expired".to_owned(), + }, + ))?; + + let token_data_result = token_data_string + .clone() + .parse_struct("PaymentTokenData") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to deserialize hyperswitch token data"); + + let token_data = match token_data_result { + Ok(data) => data, + Err(e) => { + // The purpose of this logic is backwards compatibility to support tokens + // in redis that might be following the old format. + if token_data_string.starts_with('{') { + return Err(e); + } else { + storage::PaymentTokenData::temporary_generic(token_data_string) + } + } + }; + + Ok(token_data) +} + pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( operation: BoxedOperation<'a, F, R, Ctx>, state: &'a AppState, @@ -1630,72 +1714,13 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( } } - let token = payment_data.token.clone(); - - let hyperswitch_token = match payment_data.mandate_id { - Some(_) => token.map(storage::PaymentTokenData::temporary_generic), - None => { - if let Some(token) = token { - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - - let key = format!( - "pm_token_{}_{}_hyperswitch", - token, - payment_data - .payment_attempt - .payment_method - .to_owned() - .get_required_value("payment_method")?, - ); - - let token_data_string = redis_conn - .get_key::>(&key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch the token from redis")? - .ok_or(error_stack::Report::new( - errors::ApiErrorResponse::UnprocessableEntity { - message: "Token is invalid or expired".to_owned(), - }, - ))?; - - let token_data_result = token_data_string - .clone() - .parse_struct("PaymentTokenData") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to deserialize hyperswitch token data"); - - let token_data = match token_data_result { - Ok(data) => data, - Err(e) => { - // The purpose of this logic is backwards compatibility to support tokens - // in redis that might be following the old format. - if token_data_string.starts_with('{') { - return Err(e); - } else { - storage::PaymentTokenData::temporary_generic(token_data_string) - } - } - }; - - Some(token_data) - } else { - None - } - } - }; - // TODO: Handle case where payment method and token both are present in request properly. - let (payment_method, pm_id) = match (request, hyperswitch_token) { + let (payment_method, pm_id) = match (request, payment_data.token_data.as_ref()) { (_, Some(hyperswitch_token)) => { let pm_data = Ctx::retrieve_payment_method_with_token( state, merchant_key_store, - &hyperswitch_token, + hyperswitch_token, &payment_data.payment_intent, card_token_data.as_ref(), customer, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index ca6ed3e4ebd5..6ad14d8fc4e8 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -149,6 +149,7 @@ impl setup_mandate: None, customer_acceptance: None, token: None, + token_data: None, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), @@ -158,6 +159,7 @@ impl }, confirm: None, payment_method_data: None, + payment_method_info: None, force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 7d5bf73fc1f1..2439646b3da8 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -157,6 +157,7 @@ impl setup_mandate: None, customer_acceptance: None, token: None, + token_data: None, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), @@ -166,6 +167,7 @@ impl }, confirm: None, payment_method_data: None, + payment_method_info: None, force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index d67151897aae..d643c86698d7 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -202,6 +202,7 @@ impl setup_mandate: None, customer_acceptance: None, token: None, + token_data: None, address: payments::PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), @@ -211,6 +212,7 @@ impl }, confirm: None, payment_method_data: None, + payment_method_info: None, refunds: vec![], disputes: vec![], attempts: None, diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 22ba50c07b96..79701db575b6 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -131,6 +131,12 @@ impl } } + let token_data = if let Some(token) = token.clone() { + Some(helpers::retrieve_payment_token_data(state, token, payment_method).await?) + } else { + None + }; + payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); payment_attempt.browser_info = browser_info; payment_attempt.payment_method_type = @@ -247,6 +253,7 @@ impl setup_mandate, customer_acceptance: None, token, + token_data, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), @@ -259,6 +266,7 @@ impl .payment_method_data .as_ref() .map(|pmd| pmd.payment_method_data.clone()), + payment_method_info: None, force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 2f548129a65a..96de0cc924bd 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -383,6 +383,23 @@ impl &token, )?; + let (token_data, payment_method_info) = if let Some(token) = token.clone() { + let token_data = helpers::retrieve_payment_token_data( + state, + token, + payment_method.or(payment_attempt.payment_method), + ) + .await?; + + let payment_method_info = + helpers::retrieve_payment_method_from_db_with_token_data(state, &token_data) + .await?; + + (Some(token_data), payment_method_info) + } else { + (None, None) + }; + payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); payment_attempt.browser_info = browser_info; payment_attempt.payment_method_type = @@ -548,6 +565,7 @@ impl setup_mandate, customer_acceptance, token, + token_data, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), @@ -557,6 +575,7 @@ impl }, confirm: request.confirm, payment_method_data: payment_method_data_after_card_bin_call, + payment_method_info, force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index b4ae0285dd1d..bcc13ea1cef2 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -302,7 +302,7 @@ impl mandate_obj.connector_mandate_ids, ) { (Some(network_tx_id), _) => Ok(api_models::payments::MandateIds { - mandate_id: mandate_obj.mandate_id, + mandate_id: Some(mandate_obj.mandate_id), mandate_reference_id: Some( api_models::payments::MandateReferenceId::NetworkMandateId( network_tx_id, @@ -314,7 +314,7 @@ impl .change_context(errors::ApiErrorResponse::MandateNotFound) .map(|connector_id: api_models::payments::ConnectorMandateReferenceId| { api_models::payments::MandateIds { - mandate_id: mandate_obj.mandate_id, + mandate_id: Some(mandate_obj.mandate_id), mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId( api_models::payments::ConnectorMandateReferenceId{ connector_mandate_id: connector_id.connector_mandate_id, @@ -325,7 +325,7 @@ impl } }), (_, _) => Ok(api_models::payments::MandateIds { - mandate_id: mandate_obj.mandate_id, + mandate_id: Some(mandate_obj.mandate_id), mandate_reference_id: None, }), } @@ -390,6 +390,7 @@ impl setup_mandate, customer_acceptance, token, + token_data: None, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), @@ -399,6 +400,7 @@ impl }, confirm: request.confirm, payment_method_data: payment_method_data_after_card_bin_call, + payment_method_info: None, refunds: vec![], disputes: vec![], attempts: None, diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 66bab03d0477..36197a9463be 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -145,6 +145,7 @@ impl setup_mandate: None, customer_acceptance: None, token: None, + token_data: None, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), @@ -154,6 +155,7 @@ impl }, confirm: None, payment_method_data: None, + payment_method_info: None, force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 18e568eb9b89..eaded6b5e659 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -654,7 +654,7 @@ async fn payment_response_update_tracker( mandate_id: payment_data .mandate_id .clone() - .map(|mandate| mandate.mandate_id), + .and_then(|mandate| mandate.mandate_id), connector_metadata, payment_token: None, error_code: error_status.clone(), @@ -844,7 +844,7 @@ async fn payment_response_update_tracker( .or(payment_data .mandate_id .clone() - .map(|mandate_ids| mandate_ids.mandate_id)); + .and_then(|mandate_ids| mandate_ids.mandate_id)); let m_router_data_response = router_data.response.clone(); let mandate_update_fut = tokio::spawn( async move { diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index e64b41ed383d..d498e1baca4c 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -169,6 +169,7 @@ impl mandate_connector: None, customer_acceptance: None, token: None, + token_data: None, setup_mandate: None, address: payments::PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), @@ -179,6 +180,7 @@ impl }, confirm: None, payment_method_data: None, + payment_method_info: None, force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 885d2593ba81..f1264868ff78 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -114,6 +114,15 @@ impl ) .await?; + let token_data = if let Some(token) = payment_attempt.payment_token.clone() { + Some( + helpers::retrieve_payment_token_data(state, token, payment_attempt.payment_method) + .await?, + ) + } else { + None + }; + payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); @@ -147,6 +156,7 @@ impl setup_mandate: None, customer_acceptance: None, token: payment_attempt.payment_token.clone(), + token_data, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), @@ -157,6 +167,7 @@ impl confirm: Some(payment_attempt.confirm), payment_attempt, payment_method_data: None, + payment_method_info: None, force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index f36a2eea81d0..66f0527c35d2 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -410,13 +410,14 @@ async fn get_tracker_for_sync< .mandate_id .clone() .map(|id| api_models::payments::MandateIds { - mandate_id: id, + mandate_id: Some(id), mandate_reference_id: None, }), mandate_connector: None, setup_mandate: None, customer_acceptance: None, token: None, + token_data: None, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), @@ -426,6 +427,7 @@ async fn get_tracker_for_sync< }, confirm: Some(request.force_sync), payment_method_data: None, + payment_method_info: None, force_sync: Some( request.force_sync && (helpers::check_force_psync_precondition(&payment_attempt.status) diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index c5fda22ed87a..0ce77dfcd13a 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -254,6 +254,12 @@ impl )?; } + let token_data = if let Some(token) = token.clone() { + Some(helpers::retrieve_payment_token_data(state, token, payment_method).await?) + } else { + None + }; + let mandate_id = request .mandate_id .as_ref() @@ -268,7 +274,7 @@ impl mandate_obj.connector_mandate_ids, ) { (Some(network_tx_id), _) => Ok(api_models::payments::MandateIds { - mandate_id: mandate_obj.mandate_id, + mandate_id: Some(mandate_obj.mandate_id), mandate_reference_id: Some( api_models::payments::MandateReferenceId::NetworkMandateId( network_tx_id, @@ -280,14 +286,14 @@ impl .change_context(errors::ApiErrorResponse::MandateNotFound) .map(|connector_id: api_models::payments::ConnectorMandateReferenceId| { api_models::payments::MandateIds { - mandate_id: mandate_obj.mandate_id, + mandate_id: Some(mandate_obj.mandate_id), mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId( api_models::payments::ConnectorMandateReferenceId {connector_mandate_id:connector_id.connector_mandate_id,payment_method_id:connector_id.payment_method_id, update_history: None }, )) } }), (_, _) => Ok(api_models::payments::MandateIds { - mandate_id: mandate_obj.mandate_id, + mandate_id: Some(mandate_obj.mandate_id), mandate_reference_id: None, }), } @@ -383,6 +389,7 @@ impl mandate_id, mandate_connector, token, + token_data, setup_mandate, customer_acceptance, address: PaymentAddress { @@ -395,6 +402,7 @@ impl .payment_method_data .as_ref() .map(|pmd| pmd.payment_method_data.clone()), + payment_method_info: None, force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs index dbdf1c062acf..0157822cdf81 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -122,6 +122,7 @@ impl setup_mandate: None, customer_acceptance: None, token: None, + token_data: None, address: PaymentAddress { billing: None, shipping: None, @@ -129,6 +130,7 @@ impl }, confirm: None, payment_method_data: None, + payment_method_info: None, force_sync: None, refunds: vec![], disputes: vec![], diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 91b6061c1112..2af73f29909a 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -377,7 +377,7 @@ where mandate_id: payment_data .mandate_id .clone() - .map(|mandate| mandate.mandate_id), + .and_then(|mandate| mandate.mandate_id), connector_metadata, payment_token: None, error_code: None, diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 7aaaaed92aad..4628f027a052 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -39,6 +39,8 @@ pub async fn save_payment_method( merchant_account: &domain::MerchantAccount, payment_method_type: Option, key_store: &domain::MerchantKeyStore, + amount: Option, + currency: Option, ) -> RouterResult<(Option, Option)> where FData: mandate::MandateBehaviour, @@ -94,7 +96,13 @@ where .map(|future_usage| future_usage == storage_enums::FutureUsage::OffSession) .unwrap_or(false) { - add_connector_mandate_details_in_payment_method(responses, connector) + add_connector_mandate_details_in_payment_method( + responses, + payment_method_type, + amount, + currency, + connector, + ) } else { None } @@ -648,6 +656,9 @@ pub async fn add_payment_method_token( fn add_connector_mandate_details_in_payment_method( resp: types::PaymentsResponseData, + payment_method_type: Option, + authorized_amount: Option, + authorized_currency: Option, connector: &api::ConnectorData, ) -> Option { let mut mandate_details = HashMap::new(); @@ -665,8 +676,20 @@ fn add_connector_mandate_details_in_payment_method( _ => None, }; - if let Some(mca_id) = connector.merchant_connector_id.clone() { - mandate_details.insert(mca_id, connector_mandate_id); + if let Some((mca_id, connector_mandate_id)) = connector + .merchant_connector_id + .clone() + .zip(connector_mandate_id) + { + mandate_details.insert( + mca_id, + storage::PaymentsMandateReferenceRecord { + connector_mandate_id, + payment_method_type, + original_payment_authorized_amount: authorized_amount, + original_payment_authorized_currency: authorized_currency, + }, + ); Some(storage::PaymentsMandateReference(mandate_details)) } else { None diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 8e5b2c9199e3..0ae04aaddcec 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -327,7 +327,9 @@ where phone: customer .as_ref() .and_then(|cus| cus.phone.as_ref().map(|s| s.to_owned())), - mandate_id: data.mandate_id.map(|mandate_ids| mandate_ids.mandate_id), + mandate_id: data + .mandate_id + .and_then(|mandate_ids| mandate_ids.mandate_id), payment_method: data.payment_attempt.payment_method, payment_method_data: payment_method_data_response, payment_token: data.token, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 9fd7458155a0..847c555a86d8 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -600,7 +600,7 @@ pub async fn decide_payout_connector( merchant_account, &payout_data.business_profile, key_store, - &TransactionData::<()>::Payout(payout_data), + TransactionData::<()>::Payout(payout_data), routing_data, eligible_connectors, ) diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 31a2e05dd797..0d9c7f4f62ce 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -36,12 +36,11 @@ use crate::{core::errors, services::api as service_api, types::storage}; #[cfg(feature = "business_profile_routing")] use crate::{errors, services::api as service_api}; -#[derive(Clone)] pub enum TransactionData<'a, F> where F: Clone, { - Payment(&'a payments::PaymentData), + Payment(&'a mut payments::PaymentData), #[cfg(feature = "payouts")] Payout(&'a payouts::PayoutData), } diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 8edfb8b36d8d..d3339b7c4be4 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -1,4 +1,7 @@ -use std::collections::HashMap; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, +}; use api_models::payment_methods; use diesel_models::enums; @@ -40,7 +43,7 @@ pub struct WalletTokenData { pub payment_method_id: String, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum PaymentTokenData { // The variants 'Temporary' and 'Permanent' are added for backwards compatibility @@ -76,4 +79,26 @@ impl PaymentTokenData { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct PaymentsMandateReference(pub HashMap>); +pub struct PaymentsMandateReferenceRecord { + pub connector_mandate_id: String, + pub payment_method_type: Option, + pub original_payment_authorized_amount: Option, + pub original_payment_authorized_currency: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentsMandateReference(pub HashMap); + +impl Deref for PaymentsMandateReference { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for PaymentsMandateReference { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} From a2f179350a7e5819db1b6088b75ff3a7b1ad9bad Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:17:54 +0000 Subject: [PATCH 3/8] chore(version): 2024.03.11.1 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b135c61a506..4356b94bbf21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.03.11.1 + +### Features + +- **router:** Add routing support for token-based mit payments ([#4012](https://github.com/juspay/hyperswitch/pull/4012)) ([`43ebfbc`](https://github.com/juspay/hyperswitch/commit/43ebfbc47f03eaaaf274847290861dcb00db26a5)) +- **users:** Implemented Set-Cookie ([#3865](https://github.com/juspay/hyperswitch/pull/3865)) ([`44eef46`](https://github.com/juspay/hyperswitch/commit/44eef46e5d7f0a198be80602ceae1c843449319c)) + +### Refactors + +- **connector:** + - [Multisafepay] Mask PII data ([#3869](https://github.com/juspay/hyperswitch/pull/3869)) ([`c2b1561`](https://github.com/juspay/hyperswitch/commit/c2b15615e3c61e6f497180be8fa66d008ed150bb)) + - [Globalpay] Mask PII data ([#3840](https://github.com/juspay/hyperswitch/pull/3840)) ([`13f6d6c`](https://github.com/juspay/hyperswitch/commit/13f6d6c10ce421329a7eb8b494fbb3bd31aed91f)) + - [Iatapay] Mask PII data ([#3850](https://github.com/juspay/hyperswitch/pull/3850)) ([`bd7accb`](https://github.com/juspay/hyperswitch/commit/bd7accb2c250b5f330b6bbb87f6f6edf4a479a61)) + - [Payme][Payeezy] Mask PII data ([#3926](https://github.com/juspay/hyperswitch/pull/3926)) ([`ffcb2bc`](https://github.com/juspay/hyperswitch/commit/ffcb2bcf2b7a26d8fc7fc45f9878d41ba74d2fe0)) + - [Nexinets] Mask PII data ([#3874](https://github.com/juspay/hyperswitch/pull/3874)) ([`9ea5310`](https://github.com/juspay/hyperswitch/commit/9ea531068d87b76e8f41ee7d9e9d26fd755bced4)) + - [Noon] Mask PII data ([#3879](https://github.com/juspay/hyperswitch/pull/3879)) ([`96efc2a`](https://github.com/juspay/hyperswitch/commit/96efc2abf94e3e9174f625bee2270236bad50278)) + - [stripe] capture error_code and error_message for psync ([#3771](https://github.com/juspay/hyperswitch/pull/3771)) ([`614182a`](https://github.com/juspay/hyperswitch/commit/614182ae4cdc7a762e0ce90d1336b1ff16fc9fa3)) + - [Trustpay][Volt] Mask PII data ([#3932](https://github.com/juspay/hyperswitch/pull/3932)) ([`a179b9c`](https://github.com/juspay/hyperswitch/commit/a179b9c90c2b9a419f1ce394d06158f80c29ee45)) + - [Nuvie] Mask PII data ([#3924](https://github.com/juspay/hyperswitch/pull/3924)) ([`6b2f71c`](https://github.com/juspay/hyperswitch/commit/6b2f71c850ff2ea36365375a81a7026fd8c87ebc)) + - [adyen] add more fields in the payments request ([#4010](https://github.com/juspay/hyperswitch/pull/4010)) ([`5584f11`](https://github.com/juspay/hyperswitch/commit/5584f1131ae4180020be23d4c735b8356482c22d)) +- **core:** Updated payments response with payment_method_id & payment_method_status ([#3883](https://github.com/juspay/hyperswitch/pull/3883)) ([`7391416`](https://github.com/juspay/hyperswitch/commit/7391416e2473eab0474bd01bb155a9ecc96da263)) + +**Full Changelog:** [`2024.03.11.0...2024.03.11.1`](https://github.com/juspay/hyperswitch/compare/2024.03.11.0...2024.03.11.1) + +- - - + ## 2024.03.11.0 ### Features From e87f2ea8c5669473940df8bc2f5c61fdf3f218ff Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Mon, 11 Mar 2024 19:50:38 +0530 Subject: [PATCH 4/8] Refactor(core): status handling for payment_method_status (#3965) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/diesel_models/src/payment_method.rs | 13 ++++++++ crates/router/src/core/payments/helpers.rs | 6 ++-- .../payments/operations/payment_response.rs | 32 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 6191a768efe6..ef153c23a7b9 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -123,6 +123,9 @@ pub enum PaymentMethodUpdate { LastUsedUpdate { last_used_at: PrimitiveDateTime, }, + StatusUpdate { + status: Option, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -131,6 +134,7 @@ pub struct PaymentMethodUpdateInternal { metadata: Option, payment_method_data: Option, last_used_at: Option, + status: Option, } impl PaymentMethodUpdateInternal { @@ -148,6 +152,7 @@ impl From for PaymentMethodUpdateInternal { metadata, payment_method_data: None, last_used_at: None, + status: None, }, PaymentMethodUpdate::PaymentMethodDataUpdate { payment_method_data, @@ -155,11 +160,19 @@ impl From for PaymentMethodUpdateInternal { metadata: None, payment_method_data, last_used_at: None, + status: None, }, PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { metadata: None, payment_method_data: None, last_used_at: Some(last_used_at), + status: None, + }, + PaymentMethodUpdate::StatusUpdate { status } => Self { + metadata: None, + payment_method_data: None, + last_used_at: None, + status, }, } } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 68821ee50328..40ba633bdb97 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1744,7 +1744,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( } (Some(_), _) => { - let payment_method_data = Ctx::retrieve_payment_method( + let (payment_method_data, payment_token) = Ctx::retrieve_payment_method( request, state, &payment_data.payment_intent, @@ -1753,9 +1753,9 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( ) .await?; - payment_data.token = payment_method_data.1; + payment_data.token = payment_token; - Ok((payment_method_data.0, None)) + Ok((payment_method_data, None)) } _ => Ok((None, None)), }?; diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index eaded6b5e659..2de4ddff34e3 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -441,6 +441,7 @@ async fn payment_response_update_tracker( router_data: types::RouterData, storage_scheme: enums::MerchantStorageScheme, ) -> RouterResult> { + payment_data.payment_method_status = router_data.payment_method_status; let (capture_update, mut payment_attempt_update) = match router_data.response.clone() { Err(err) => { let (capture_update, attempt_update) = match payment_data.multiple_capture_data { @@ -816,6 +817,7 @@ async fn payment_response_update_tracker( }, }; + update_payment_method_status(state, &mut payment_data, router_data.status).await?; let m_db = state.clone().store; let m_payment_data_payment_intent = payment_data.payment_intent.clone(); let m_payment_intent_update = payment_intent_update.clone(); @@ -871,6 +873,36 @@ async fn payment_response_update_tracker( Ok(payment_data) } +async fn update_payment_method_status( + state: &AppState, + payment_data: &mut PaymentData, + attempt_status: common_enums::AttemptStatus, +) -> RouterResult<()> { + if let Some(id) = &payment_data.payment_attempt.payment_method_id { + let pm = state + .store + .find_payment_method(id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + if pm.status != attempt_status.into() { + let updated_pm_status = common_enums::PaymentMethodStatus::from(attempt_status); + + payment_data.payment_method_status = Some(updated_pm_status); + let pm_update = storage::PaymentMethodUpdate::StatusUpdate { + status: Some(updated_pm_status), + }; + state + .store + .update_payment_method(pm, pm_update) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method in db")?; + } + }; + Ok(()) +} + fn response_to_capture_update( multiple_capture_data: &MultipleCaptureData, response_list: HashMap, From d9f84232a4a29814a1f9a792ebc74923862a1da6 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 12 Mar 2024 01:01:15 +0530 Subject: [PATCH 5/8] chore: add threedsecureio base url in deployment config files (#4039) --- config/deployments/integration_test.toml | 1 + config/deployments/production.toml | 1 + config/deployments/sandbox.toml | 1 + 3 files changed, 3 insertions(+) diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 3ed64840b785..9b805f0462a0 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -76,6 +76,7 @@ worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" zen.base_url = "https://api.zen-test.com/" zen.secondary_base_url = "https://secure.zen-test.com/" +threedsecureio.base_url = "https://service.sandbox.3dsecure.io" [dummy_connector] enabled = true diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 7c543feed7e6..a02e0a300835 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -80,6 +80,7 @@ worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" zen.base_url = "https://api.zen.com/" zen.secondary_base_url = "https://secure.zen.com/" +threedsecureio.base_url = "https://service.sandbox.3dsecure.io" [delayed_session_response] connectors_with_delayed_session_response = "trustpay,payme" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 396fd8b8628c..43cd9136b1bd 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -80,6 +80,7 @@ worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" zen.base_url = "https://api.zen-test.com/" zen.secondary_base_url = "https://secure.zen-test.com/" +threedsecureio.base_url = "https://service.sandbox.3dsecure.io" [delayed_session_response] connectors_with_delayed_session_response = "trustpay,payme" From 21810e48d20dbb15895eb8f4d2f455478198eb2b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 04:26:41 +0000 Subject: [PATCH 6/8] chore(version): 2024.03.12.0 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4356b94bbf21..78d2d68a5701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.03.12.0 + +### Refactors + +- **core:** Status handling for payment_method_status ([#3965](https://github.com/juspay/hyperswitch/pull/3965)) ([`e87f2ea`](https://github.com/juspay/hyperswitch/commit/e87f2ea8c5669473940df8bc2f5c61fdf3f218ff)) + +### Miscellaneous Tasks + +- Add threedsecureio base url in deployment config files ([#4039](https://github.com/juspay/hyperswitch/pull/4039)) ([`d9f8423`](https://github.com/juspay/hyperswitch/commit/d9f84232a4a29814a1f9a792ebc74923862a1da6)) + +**Full Changelog:** [`2024.03.11.1...2024.03.12.0`](https://github.com/juspay/hyperswitch/compare/2024.03.11.1...2024.03.12.0) + +- - - + ## 2024.03.11.1 ### Features From abe9c2ac17a0783f3625dd7fde5d28e285012ec3 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:48:25 +0530 Subject: [PATCH 7/8] refactor(payment_methods): filter wallet payment method from mca based on customer pm (#4038) --- .../router/src/core/payment_methods/cards.rs | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index fccacfef45d6..985ddcfe849c 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1344,42 +1344,50 @@ pub async fn list_payment_methods( .await?; } - // Filter out applepay payment method from mca if customer has already saved it - response - .iter() - .position(|pm| { - pm.payment_method == enums::PaymentMethod::Wallet - && pm.payment_method_type == enums::PaymentMethodType::ApplePay - }) + // Filter out wallet payment method from mca if customer has already saved it + customer .as_ref() - .zip(customer.as_ref()) - .async_map(|(index, customer)| async { - match db - .find_payment_method_by_customer_id_merchant_id_list( - &customer.customer_id, - &merchant_account.merchant_id, - None, - ) - .await - { - Ok(customer_payment_methods) => { - if customer_payment_methods.iter().any(|pm| { - pm.payment_method == enums::PaymentMethod::Wallet - && pm.payment_method_type == Some(enums::PaymentMethodType::ApplePay) - }) { - response.remove(*index); - } - Ok(()) - } - Err(error) => { - if error.current_context().is_db_not_found() { + .async_map(|customer| async { + let wallet_pm_exists = response + .iter() + .any(|mca| mca.payment_method == enums::PaymentMethod::Wallet); + if wallet_pm_exists { + match db + .find_payment_method_by_customer_id_merchant_id_list( + &customer.customer_id, + &merchant_account.merchant_id, + None, + ) + .await + { + Ok(customer_payment_methods) => { + let customer_wallet_pm = customer_payment_methods + .iter() + .filter(|cust_pm| { + cust_pm.payment_method == enums::PaymentMethod::Wallet + }) + .collect::>(); + + response.retain(|mca| { + !(mca.payment_method == enums::PaymentMethod::Wallet + && customer_wallet_pm.iter().any(|cust_pm| { + cust_pm.payment_method_type == Some(mca.payment_method_type) + })) + }); Ok(()) - } else { - Err(error) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to find payment methods for a customer") + } + Err(error) => { + if error.current_context().is_db_not_found() { + Ok(()) + } else { + Err(error) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to find payment methods for a customer") + } } } + } else { + Ok(()) } }) .await From 45ed56f16516c44acbe75b75c0621b78ccdb9894 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:49:00 +0530 Subject: [PATCH 8/8] refactor(payment_methods): allow deletion of default payment method for a customer if only one pm exists (#4027) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/diesel_models/src/customers.rs | 2 +- crates/diesel_models/src/query/generics.rs | 1 + .../diesel_models/src/query/payment_method.rs | 35 +++++++++++++- .../router/src/core/payment_methods/cards.rs | 31 +++++++++++- crates/router/src/db/kafka_store.rs | 15 ++++++ crates/router/src/db/payment_method.rs | 47 +++++++++++++++++++ crates/router/src/types/domain/customer.rs | 2 +- 7 files changed, 128 insertions(+), 5 deletions(-) diff --git a/crates/diesel_models/src/customers.rs b/crates/diesel_models/src/customers.rs index cefb0c240ec5..bda7af157de6 100644 --- a/crates/diesel_models/src/customers.rs +++ b/crates/diesel_models/src/customers.rs @@ -52,5 +52,5 @@ pub struct CustomerUpdateInternal { pub modified_at: Option, pub connector_customer: Option, pub address_id: Option, - pub default_payment_method_id: Option, + pub default_payment_method_id: Option>, } diff --git a/crates/diesel_models/src/query/generics.rs b/crates/diesel_models/src/query/generics.rs index 8f2e391df6ca..a1dd40cd4007 100644 --- a/crates/diesel_models/src/query/generics.rs +++ b/crates/diesel_models/src/query/generics.rs @@ -40,6 +40,7 @@ pub mod db_metrics { DeleteWithResult, UpdateWithResults, UpdateOne, + Count, } #[inline] diff --git a/crates/diesel_models/src/query/payment_method.rs b/crates/diesel_models/src/query/payment_method.rs index bed4d0790107..a27a2ae89502 100644 --- a/crates/diesel_models/src/query/payment_method.rs +++ b/crates/diesel_models/src/query/payment_method.rs @@ -1,4 +1,9 @@ -use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table}; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{ + associations::HasTable, debug_query, pg::Pg, BoolExpressionMethods, ExpressionMethods, + QueryDsl, Table, +}; +use error_stack::{IntoReport, ResultExt}; use super::generics; use crate::{ @@ -96,6 +101,34 @@ impl PaymentMethod { .await } + pub async fn get_count_by_customer_id_merchant_id_status( + conn: &PgPooledConn, + customer_id: &str, + merchant_id: &str, + status: common_enums::PaymentMethodStatus, + ) -> StorageResult { + let filter = ::table() + .count() + .filter( + dsl::customer_id + .eq(customer_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_owned())) + .and(dsl::status.eq(status.to_owned())), + ) + .into_boxed(); + + router_env::logger::debug!(query = %debug_query::(&filter).to_string()); + + generics::db_metrics::track_database_call::<::Table, _, _>( + filter.get_result_async::(conn), + generics::db_metrics::DatabaseOperation::Count, + ) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Failed to get a count of payment methods") + } + pub async fn find_by_customer_id_merchant_id_status( conn: &PgPooledConn, customer_id: &str, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 985ddcfe849c..847950d4f451 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -3201,7 +3201,7 @@ pub async fn set_default_payment_method( )?; let customer_update = CustomerUpdate::UpdateDefaultPaymentMethod { - default_payment_method_id: Some(payment_method_id.to_owned()), + default_payment_method_id: Some(Some(payment_method_id.to_owned())), }; // update the db with the default payment method id @@ -3450,6 +3450,16 @@ pub async fn delete_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + let payment_methods_count = db + .get_payment_method_count_by_customer_id_merchant_id_status( + &key.customer_id, + &merchant_account.merchant_id, + api_enums::PaymentMethodStatus::Active, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get a count of payment methods for a customer")?; + let customer = db .find_customer_by_customer_id_merchant_id( &key.customer_id, @@ -3461,7 +3471,8 @@ pub async fn delete_payment_method( .attach_printable("Customer not found for the payment method")?; utils::when( - customer.default_payment_method_id.as_ref() == Some(&pm_id.payment_method_id), + customer.default_payment_method_id.as_ref() == Some(&pm_id.payment_method_id) + && payment_methods_count > 1, || Err(errors::ApiErrorResponse::PaymentMethodDeleteFailed), )?; @@ -3489,6 +3500,22 @@ pub async fn delete_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + if customer.default_payment_method_id.as_ref() == Some(&pm_id.payment_method_id) { + let customer_update = CustomerUpdate::UpdateDefaultPaymentMethod { + default_payment_method_id: Some(None), + }; + + db.update_customer_by_customer_id_merchant_id( + key.customer_id, + key.merchant_id, + customer_update, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update the default payment method id for the customer")?; + }; + Ok(services::ApplicationResponse::Json( api::PaymentMethodDeleteResponse { payment_method_id: key.payment_method_id, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 4509da8f0ff0..ff0363200989 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1299,6 +1299,21 @@ impl PaymentMethodInterface for KafkaStore { .await } + async fn get_payment_method_count_by_customer_id_merchant_id_status( + &self, + customer_id: &str, + merchant_id: &str, + status: common_enums::PaymentMethodStatus, + ) -> CustomResult { + self.diesel_store + .get_payment_method_count_by_customer_id_merchant_id_status( + customer_id, + merchant_id, + status, + ) + .await + } + async fn find_payment_method_by_locker_id( &self, locker_id: &str, diff --git a/crates/router/src/db/payment_method.rs b/crates/router/src/db/payment_method.rs index ef471fbd2464..94ebee178934 100644 --- a/crates/router/src/db/payment_method.rs +++ b/crates/router/src/db/payment_method.rs @@ -36,6 +36,13 @@ pub trait PaymentMethodInterface { limit: Option, ) -> CustomResult, errors::StorageError>; + async fn get_payment_method_count_by_customer_id_merchant_id_status( + &self, + customer_id: &str, + merchant_id: &str, + status: common_enums::PaymentMethodStatus, + ) -> CustomResult; + async fn insert_payment_method( &self, payment_method_new: storage::PaymentMethodNew, @@ -80,6 +87,25 @@ impl PaymentMethodInterface for Store { .into_report() } + #[instrument(skip_all)] + async fn get_payment_method_count_by_customer_id_merchant_id_status( + &self, + customer_id: &str, + merchant_id: &str, + status: common_enums::PaymentMethodStatus, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::PaymentMethod::get_count_by_customer_id_merchant_id_status( + &conn, + customer_id, + merchant_id, + status, + ) + .await + .map_err(Into::into) + .into_report() + } + #[instrument(skip_all)] async fn insert_payment_method( &self, @@ -204,6 +230,27 @@ impl PaymentMethodInterface for MockDb { } } + async fn get_payment_method_count_by_customer_id_merchant_id_status( + &self, + customer_id: &str, + merchant_id: &str, + status: common_enums::PaymentMethodStatus, + ) -> CustomResult { + let payment_methods = self.payment_methods.lock().await; + let count = payment_methods + .iter() + .filter(|pm| { + pm.customer_id == customer_id + && pm.merchant_id == merchant_id + && pm.status == status + }) + .count(); + count + .try_into() + .into_report() + .change_context(errors::StorageError::MockDbError) + } + async fn insert_payment_method( &self, payment_method_new: storage::PaymentMethodNew, diff --git a/crates/router/src/types/domain/customer.rs b/crates/router/src/types/domain/customer.rs index 5437d06a2e65..d5f05944d6cd 100644 --- a/crates/router/src/types/domain/customer.rs +++ b/crates/router/src/types/domain/customer.rs @@ -118,7 +118,7 @@ pub enum CustomerUpdate { connector_customer: Option, }, UpdateDefaultPaymentMethod { - default_payment_method_id: Option, + default_payment_method_id: Option>, }, }