From 9f2ce05b2591da0d757b267800f42b69fc38e3ee Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:03:12 +0530 Subject: [PATCH 01/11] fix(connector): [STRIPE] fix stripe mandate (#6899) --- .../router/src/connector/stripe/transformers.rs | 15 ++++++++++++--- crates/router/src/core/payments/transformers.rs | 15 +++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 262b0348b5ae..cb6c04c601f5 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1703,7 +1703,13 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent }; let mut payment_method_options = None; - let (mut payment_data, payment_method, billing_address, payment_method_types) = { + let ( + mut payment_data, + payment_method, + billing_address, + payment_method_types, + setup_future_usage, + ) = { match item .request .mandate_id @@ -1717,6 +1723,7 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent connector_mandate_ids.get_connector_mandate_id(), StripeBillingAddress::default(), get_payment_method_type_for_saved_payment_method_payment(item)?, + None, ), Some(api_models::payments::MandateReferenceId::NetworkMandateId( network_transaction_id, @@ -1782,9 +1789,10 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent None, StripeBillingAddress::default(), None, + None, ) } - _ => { + Some(api_models::payments::MandateReferenceId::NetworkTokenWithNTI(_)) | None => { let (payment_method_data, payment_method_type, billing_address) = create_stripe_payment_method( &item.request.payment_method_data, @@ -1806,6 +1814,7 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent None, billing_address, payment_method_type, + item.request.setup_future_usage, ) } } @@ -1969,7 +1978,7 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent customer, setup_mandate_details, off_session: item.request.off_session, - setup_future_usage: item.request.setup_future_usage, + setup_future_usage, payment_method_types, expand: Some(ExpandableObjects::LatestCharge), browser_info, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 1e39831af174..8652844623f1 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -2622,10 +2622,7 @@ impl TryFrom> for types::PaymentsAuthoriz payment_method_data: (payment_method_data.get_required_value("payment_method_data")?), setup_future_usage: payment_data.payment_intent.setup_future_usage, mandate_id: payment_data.mandate_id.clone(), - off_session: payment_data - .mandate_id - .as_ref() - .and(payment_data.payment_intent.off_session), + off_session: payment_data.mandate_id.as_ref().map(|_| true), setup_mandate_details: payment_data.setup_mandate.clone(), confirm: payment_data.payment_attempt.confirm, statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, @@ -3360,10 +3357,7 @@ impl TryFrom> for types::SetupMandateRequ .get_required_value("payment_method_data")?), statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, setup_future_usage: payment_data.payment_intent.setup_future_usage, - off_session: payment_data - .mandate_id - .as_ref() - .and(payment_data.payment_intent.off_session), + off_session: payment_data.mandate_id.as_ref().map(|_| true), mandate_id: payment_data.mandate_id.clone(), setup_mandate_details: payment_data.setup_mandate, customer_acceptance: payment_data.customer_acceptance, @@ -3478,10 +3472,7 @@ impl TryFrom> for types::CompleteAuthoriz Ok(Self { setup_future_usage: payment_data.payment_intent.setup_future_usage, mandate_id: payment_data.mandate_id.clone(), - off_session: payment_data - .mandate_id - .as_ref() - .and(payment_data.payment_intent.off_session), + off_session: payment_data.mandate_id.as_ref().map(|_| true), setup_mandate_details: payment_data.setup_mandate.clone(), confirm: payment_data.payment_attempt.confirm, statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, From 02f0824d303fb9a36ee54123f52176014613a992 Mon Sep 17 00:00:00 2001 From: awasthi21 <107559116+awasthi21@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:04:26 +0530 Subject: [PATCH 02/11] fix(connector): Update mandate PMT configs for Mandate Supported Connectors (#6903) --- config/deployments/integration_test.toml | 38 ++++++++++++++++-------- config/deployments/production.toml | 38 ++++++++++++++++-------- config/deployments/sandbox.toml | 38 ++++++++++++++++-------- config/development.toml | 38 ++++++++++++++++-------- loadtest/config/development.toml | 38 ++++++++++++++++-------- 5 files changed, 125 insertions(+), 65 deletions(-) diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index fbf80ced5f17..56dd0ef7451e 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -159,19 +159,31 @@ payout_connector_list = "stripe,wise" connectors_with_delayed_session_response = "trustpay,payme" # List of connectors which have delayed session response [mandates.supported_payment_methods] -bank_debit.ach = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit -bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit -bank_debit.bacs = { connector_list = "adyen" } # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit -card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -pay_later.klarna.connector_list = "adyen" # Mandate supported payment method type and connector for pay_later -wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.paypal.connector_list = "adyen" # Mandate supported payment method type and connector for wallets -bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [mandates.update_mandate_supported] card.credit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card diff --git a/config/deployments/production.toml b/config/deployments/production.toml index a22b618bfa21..464f9bcba32e 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -159,19 +159,31 @@ force_cookies = false enabled = false [mandates.supported_payment_methods] -bank_debit.ach = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit -bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit -bank_debit.bacs = { connector_list = "adyen" } # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit -card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -pay_later.klarna.connector_list = "adyen" # Mandate supported payment method type and connector for pay_later -wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.paypal.connector_list = "adyen" # Mandate supported payment method type and connector for wallets -bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [mandates.update_mandate_supported] card.credit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 2defc5729cf2..fb210a28862c 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -159,19 +159,31 @@ force_cookies = false enabled = true [mandates.supported_payment_methods] -bank_debit.ach = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit -bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit -bank_debit.bacs = { connector_list = "adyen" } # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit -card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -pay_later.klarna.connector_list = "adyen" # Mandate supported payment method type and connector for pay_later -wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.paypal.connector_list = "adyen" # Mandate supported payment method type and connector for wallets -bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [mandates.update_mandate_supported] card.credit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card diff --git a/config/development.toml b/config/development.toml index 6250e431bd4f..7008af1f28a2 100644 --- a/config/development.toml +++ b/config/development.toml @@ -614,19 +614,31 @@ connectors_with_delayed_session_response = "trustpay,payme" connectors_with_webhook_source_verification_call = "paypal" [mandates.supported_payment_methods] -pay_later.klarna = { connector_list = "adyen" } -wallet.google_pay = { connector_list = "stripe,adyen,cybersource,bankofamerica" } -wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon,bankofamerica" } -wallet.paypal = { connector_list = "adyen" } -card.credit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,fiuu" } -card.debit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,fiuu" } -bank_debit.ach = { connector_list = "gocardless,adyen" } -bank_debit.becs = { connector_list = "gocardless" } -bank_debit.bacs = { connector_list = "adyen" } -bank_debit.sepa = { connector_list = "gocardless,adyen" } -bank_redirect.ideal = { connector_list = "stripe,adyen,globalpay,multisafepay" } -bank_redirect.sofort = { connector_list = "stripe,adyen,globalpay" } -bank_redirect.giropay = { connector_list = "adyen,globalpay,multisafepay" } +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [mandates.update_mandate_supported] card.credit = { connector_list = "cybersource" } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 09b52bc357d2..f33ab82aa0dd 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -327,19 +327,31 @@ discord_invite_url = "https://discord.gg/wJZ7DVW8mm" payout_eligibility = true [mandates.supported_payment_methods] -pay_later.klarna = {connector_list = "adyen"} -wallet.google_pay = {connector_list = "stripe,adyen,bankofamerica"} -wallet.apple_pay = {connector_list = "stripe,adyen,bankofamerica"} -wallet.paypal = {connector_list = "adyen"} -card.credit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree"} -card.debit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree"} -bank_debit.ach = { connector_list = "gocardless,adyen" } -bank_debit.becs = { connector_list = "gocardless" } -bank_debit.bacs = { connector_list = "adyen" } -bank_debit.sepa = { connector_list = "gocardless,adyen" } -bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} -bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} -bank_redirect.giropay = {connector_list = "adyen,globalpay"} +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [cors] From 22de8ad132811b636fdb2594649e40b90810f564 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:36:54 +0530 Subject: [PATCH 03/11] feat(router): add /relay endpoint (#6870) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../api-reference/relay/relay--retrieve.mdx | 3 + api-reference/api-reference/relay/relay.mdx | 3 + api-reference/mint.json | 7 + api-reference/openapi_spec.json | 272 ++++++++++++++++++ crates/api_models/src/lib.rs | 1 + crates/api_models/src/relay.rs | 103 +++++++ crates/common_utils/src/id_type.rs | 2 + crates/common_utils/src/id_type/relay.rs | 7 + crates/openapi/src/openapi.rs | 11 + crates/openapi/src/routes.rs | 3 +- crates/openapi/src/routes/relay.rs | 57 ++++ 11 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 api-reference/api-reference/relay/relay--retrieve.mdx create mode 100644 api-reference/api-reference/relay/relay.mdx create mode 100644 crates/api_models/src/relay.rs create mode 100644 crates/common_utils/src/id_type/relay.rs create mode 100644 crates/openapi/src/routes/relay.rs diff --git a/api-reference/api-reference/relay/relay--retrieve.mdx b/api-reference/api-reference/relay/relay--retrieve.mdx new file mode 100644 index 000000000000..d65e62d31d74 --- /dev/null +++ b/api-reference/api-reference/relay/relay--retrieve.mdx @@ -0,0 +1,3 @@ +--- +openapi: openapi_spec get /relay/{relay_id} +--- \ No newline at end of file diff --git a/api-reference/api-reference/relay/relay.mdx b/api-reference/api-reference/relay/relay.mdx new file mode 100644 index 000000000000..a6b5962740a6 --- /dev/null +++ b/api-reference/api-reference/relay/relay.mdx @@ -0,0 +1,3 @@ +--- +openapi: openapi_spec post /relay +--- \ No newline at end of file diff --git a/api-reference/mint.json b/api-reference/mint.json index 04b16682e3c4..5e9c9c6d36c6 100644 --- a/api-reference/mint.json +++ b/api-reference/mint.json @@ -234,6 +234,13 @@ "api-reference/routing/routing--activate-config" ] }, + { + "group": "Relay", + "pages": [ + "api-reference/relay/relay", + "api-reference/relay/relay--retrieve" + ] + }, { "group": "Schemas", "pages": ["api-reference/schemas/outgoing--webhook"] diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 4956df4713cc..746883028adb 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -950,6 +950,123 @@ ] } }, + "/relay": { + "post": { + "tags": [ + "Relay" + ], + "summary": "Relay - Create", + "description": "Creates a relay request.", + "operationId": "Relay Request", + "parameters": [ + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID for authentication", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Idempotency-Key", + "in": "header", + "description": "Idempotency Key for relay request", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelayRequest" + }, + "examples": { + "Create a relay request": { + "value": { + "connector_id": "mca_5apGeP94tMts6rg3U3kR", + "connector_resource_id": "7256228702616471803954", + "data": { + "amount": 6540, + "currency": "USD" + }, + "type": "refund" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Relay request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelayResponse" + } + } + } + }, + "400": { + "description": "Invalid data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/relay/{relay_id}": { + "get": { + "tags": [ + "Relay" + ], + "summary": "Relay - Retrieve", + "description": "Retrieves a relay details.", + "operationId": "Retrieve a Relay details", + "parameters": [ + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID for authentication", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Relay Retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelayResponse" + } + } + } + }, + "404": { + "description": "Relay details was not found" + } + }, + "security": [ + { + "api_key": [] + }, + { + "ephemeral_key": [] + } + ] + } + }, "/refunds": { "post": { "tags": [ @@ -23093,6 +23210,161 @@ }, "additionalProperties": false }, + "RelayData": { + "oneOf": [ + { + "$ref": "#/components/schemas/RelayRefundRequest" + } + ] + }, + "RelayError": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "The error code" + }, + "message": { + "type": "string", + "description": "The error message" + } + } + }, + "RelayRefundRequest": { + "type": "object", + "required": [ + "amount", + "currency" + ], + "properties": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The amount that is being refunded", + "example": 6540 + }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "reason": { + "type": "string", + "description": "The reason for the refund", + "example": "Customer returned the product", + "nullable": true, + "maxLength": 255 + } + } + }, + "RelayRequest": { + "type": "object", + "required": [ + "connector_resource_id", + "connector_id", + "type" + ], + "properties": { + "connector_resource_id": { + "type": "string", + "description": "The identifier that is associated to a resource at the connector to which the relay request is being made", + "example": "7256228702616471803954" + }, + "connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) to which relay request is being made", + "example": "mca_5apGeP94tMts6rg3U3kR" + }, + "type": { + "$ref": "#/components/schemas/RelayType" + }, + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayData" + } + ], + "nullable": true + } + } + }, + "RelayResponse": { + "type": "object", + "required": [ + "id", + "status", + "connector_resource_id", + "connector_id", + "profile_id", + "type" + ], + "properties": { + "id": { + "type": "string", + "description": "The unique identifier for the Relay", + "example": "relay_mbabizu24mvu3mela5njyhpit4" + }, + "status": { + "$ref": "#/components/schemas/RelayStatus" + }, + "connector_reference_id": { + "type": "string", + "description": "The reference identifier provided by the connector for the relay request", + "example": "pi_3MKEivSFNglxLpam0ZaL98q9", + "nullable": true + }, + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayError" + } + ], + "nullable": true + }, + "connector_resource_id": { + "type": "string", + "description": "The identifier that is associated to a resource at the connector to which the relay request is being made", + "example": "7256228702616471803954" + }, + "connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) to which relay request is being made", + "example": "mca_5apGeP94tMts6rg3U3kR" + }, + "profile_id": { + "type": "string", + "description": "The business profile that is associated with this relay request", + "example": "pro_abcdefghijklmnopqrstuvwxyz" + }, + "type": { + "$ref": "#/components/schemas/RelayType" + }, + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayData" + } + ], + "nullable": true + } + } + }, + "RelayStatus": { + "type": "string", + "enum": [ + "success", + "processing", + "failure" + ] + }, + "RelayType": { + "type": "string", + "enum": [ + "refund" + ] + }, "RequestPaymentMethodTypes": { "type": "object", "required": [ diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index a28332e7fea0..2d1e87ebf01e 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -31,6 +31,7 @@ pub mod poll; #[cfg(feature = "recon")] pub mod recon; pub mod refunds; +pub mod relay; pub mod routing; pub mod surcharge_decision_configs; pub mod user; diff --git a/crates/api_models/src/relay.rs b/crates/api_models/src/relay.rs new file mode 100644 index 000000000000..7e094f283607 --- /dev/null +++ b/crates/api_models/src/relay.rs @@ -0,0 +1,103 @@ +pub use common_utils::types::MinorUnit; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::enums; + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRequest { + /// The identifier that is associated to a resource at the connector to which the relay request is being made + #[schema(example = "7256228702616471803954")] + pub connector_resource_id: String, + /// Identifier of the connector ( merchant connector account ) to which relay request is being made + #[schema(example = "mca_5apGeP94tMts6rg3U3kR", value_type = String)] + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + /// The type of relay request + #[serde(rename = "type")] + pub relay_type: RelayType, + /// The data that is associated with the relay request + pub data: Option, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RelayType { + /// The relay request is for a refund + Refund, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case", untagged)] +pub enum RelayData { + /// The data that is associated with a refund relay request + Refund(RelayRefundRequest), +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRefundRequest { + /// The amount that is being refunded + #[schema(value_type = i64 , example = 6540)] + pub amount: MinorUnit, + /// The currency in which the amount is being refunded + #[schema(value_type = Currency)] + pub currency: enums::Currency, + /// The reason for the refund + #[schema(max_length = 255, example = "Customer returned the product")] + pub reason: Option, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayResponse { + /// The unique identifier for the Relay + #[schema(example = "relay_mbabizu24mvu3mela5njyhpit4", value_type = String)] + pub id: common_utils::id_type::RelayId, + /// The status of the relay request + pub status: RelayStatus, + /// The reference identifier provided by the connector for the relay request + #[schema(example = "pi_3MKEivSFNglxLpam0ZaL98q9")] + pub connector_reference_id: Option, + /// The error details if the relay request failed + pub error: Option, + /// The identifier that is associated to a resource at the connector to which the relay request is being made + #[schema(example = "7256228702616471803954")] + pub connector_resource_id: String, + /// Identifier of the connector ( merchant connector account ) to which relay request is being made + #[schema(example = "mca_5apGeP94tMts6rg3U3kR", value_type = String)] + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + /// The business profile that is associated with this relay request + #[schema(example = "pro_abcdefghijklmnopqrstuvwxyz", value_type = String)] + pub profile_id: common_utils::id_type::ProfileId, + /// The type of relay request + #[serde(rename = "type")] + pub relay_type: RelayType, + /// The data that is associated with the relay request + pub data: Option, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RelayStatus { + /// The relay request is successful + Success, + /// The relay request is being processed + Processing, + /// The relay request has failed + Failure, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayError { + /// The error code + pub code: String, + /// The error message + pub message: String, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRetrieveRequest { + /// The unique identifier for the Relay + #[serde(default)] + pub force_sync: bool, + /// The unique identifier for the Relay + pub id: String, +} diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index 594078bb4057..c7393155226b 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -11,6 +11,7 @@ mod organization; mod payment; mod profile; mod refunds; +mod relay; mod routing; mod tenant; @@ -43,6 +44,7 @@ pub use self::{ payment::{PaymentId, PaymentReferenceId}, profile::ProfileId, refunds::RefundReferenceId, + relay::RelayId, routing::RoutingId, tenant::TenantId, }; diff --git a/crates/common_utils/src/id_type/relay.rs b/crates/common_utils/src/id_type/relay.rs new file mode 100644 index 000000000000..5db48c492f9c --- /dev/null +++ b/crates/common_utils/src/id_type/relay.rs @@ -0,0 +1,7 @@ +crate::id_type!( + RelayId, + "A type for relay_id that can be used for relay ids" +); +crate::impl_id_type_methods!(RelayId, "relay_id"); + +crate::impl_debug_id_type!(RelayId); diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index a55f6ed4214c..7b093ffd9612 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -84,6 +84,10 @@ Never share your secret api keys. Keep them guarded and secure. routes::payments::payments_complete_authorize, routes::payments::payments_post_session_tokens, + // Routes for relay + routes::relay, + routes::relay_retrieve, + // Routes for refunds routes::refunds::refunds_create, routes::refunds::refunds_retrieve, @@ -520,6 +524,13 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodCollectLinkResponse, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, + api_models::relay::RelayRequest, + api_models::relay::RelayType, + api_models::relay::RelayData, + api_models::relay::RelayRefundRequest, + api_models::relay::RelayResponse, + api_models::relay::RelayStatus, + api_models::relay::RelayError, api_models::payments::AmountFilter, api_models::mandates::MandateRevokedResponse, api_models::mandates::MandateResponse, diff --git a/crates/openapi/src/routes.rs b/crates/openapi/src/routes.rs index 453f7f557d34..2c1c58244124 100644 --- a/crates/openapi/src/routes.rs +++ b/crates/openapi/src/routes.rs @@ -16,10 +16,11 @@ pub mod payouts; pub mod poll; pub mod profile; pub mod refunds; +pub mod relay; pub mod routing; pub mod webhook_events; pub use self::{ customers::*, mandates::*, merchant_account::*, merchant_connector_account::*, organization::*, - payment_method::*, payments::*, poll::*, refunds::*, routing::*, webhook_events::*, + payment_method::*, payments::*, poll::*, refunds::*, relay::*, routing::*, webhook_events::*, }; diff --git a/crates/openapi/src/routes/relay.rs b/crates/openapi/src/routes/relay.rs new file mode 100644 index 000000000000..9100bf47f759 --- /dev/null +++ b/crates/openapi/src/routes/relay.rs @@ -0,0 +1,57 @@ +/// Relay - Create +/// +/// Creates a relay request. +#[utoipa::path( + post, + path = "/relay", + request_body( + content = RelayRequest, + examples(( + "Create a relay request" = ( + value = json!({ + "connector_resource_id": "7256228702616471803954", + "connector_id": "mca_5apGeP94tMts6rg3U3kR", + "type": "refund", + "data": { + "amount": 6540, + "currency": "USD" + } + }) + ) + )) + ), + responses( + (status = 200, description = "Relay request", body = RelayResponse), + (status = 400, description = "Invalid data") + ), + params( + ("X-Profile-Id" = String, Header, description = "Profile ID for authentication"), + ("X-Idempotency-Key" = String, Header, description = "Idempotency Key for relay request") + ), + tag = "Relay", + operation_id = "Relay Request", + security(("api_key" = [])) +)] + +pub async fn relay() {} + +/// Relay - Retrieve +/// +/// Retrieves a relay details. +#[utoipa::path( + get, + path = "/relay/{relay_id}", + params (("id" = String, Path, description = "The unique identifier for the Relay")), + responses( + (status = 200, description = "Relay Retrieved", body = RelayResponse), + (status = 404, description = "Relay details was not found") + ), + params( + ("X-Profile-Id" = String, Header, description = "Profile ID for authentication") + ), + tag = "Relay", + operation_id = "Retrieve a Relay details", + security(("api_key" = []), ("ephemeral_key" = [])) +)] + +pub async fn relay_retrieve() {} From 977cb704e7dcf35d0fa6bc0e3c6d335ad0601521 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:54:57 +0530 Subject: [PATCH 04/11] feat(payments_v2): implement payments capture v2 (#6722) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 202 ++++---- api-reference/openapi_spec.json | 2 +- crates/api_models/src/events/payment.rs | 11 +- crates/api_models/src/payments.rs | 31 +- crates/diesel_models/src/payment_attempt.rs | 4 +- crates/diesel_models/src/payment_intent.rs | 20 +- .../hyperswitch_domain_models/src/payments.rs | 15 +- .../src/payments/payment_attempt.rs | 155 +++++- .../src/payments/payment_intent.rs | 64 ++- .../src/router_data.rs | 444 +++++++++++++++++- crates/openapi/src/openapi_v2.rs | 2 +- crates/router/src/core/payments.rs | 242 +++++++++- .../src/core/payments/flows/capture_flow.rs | 38 +- crates/router/src/core/payments/operations.rs | 5 +- .../payments/operations/payment_capture_v2.rs | 324 +++++++++++++ .../core/payments/operations/payment_get.rs | 65 +-- .../payments/operations/payment_response.rs | 94 +++- .../router/src/core/payments/transformers.rs | 404 +++++++++++++--- .../router/src/core/webhooks/incoming_v2.rs | 13 +- crates/router/src/routes/app.rs | 3 + crates/router/src/routes/payments.rs | 72 +++ crates/router/src/types.rs | 2 +- 22 files changed, 1925 insertions(+), 287 deletions(-) create mode 100644 crates/router/src/core/payments/operations/payment_capture_v2.rs diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 63777d5283a7..3f1a0ae01de8 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6495,88 +6495,6 @@ "greater_than_equal" ] }, - "ConfirmIntentAmountDetailsResponse": { - "type": "object", - "required": [ - "currency", - "external_tax_calculation", - "surcharge_calculation", - "net_amount", - "amount_capturable" - ], - "properties": { - "order_amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and 1 for 1¥ since ¥ is a zero-decimal currency. Read more about [the Decimal and Non-Decimal Currencies](https://github.com/juspay/hyperswitch/wiki/Decimal-and-Non%E2%80%90Decimal-Currencies)", - "example": 6540, - "minimum": 0 - }, - "currency": { - "$ref": "#/components/schemas/Currency" - }, - "shipping_cost": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "order_tax_amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "external_tax_calculation": { - "$ref": "#/components/schemas/TaxCalculationOverride" - }, - "surcharge_calculation": { - "$ref": "#/components/schemas/SurchargeCalculationOverride" - }, - "surcharge_amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "tax_on_surcharge": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "net_amount": { - "$ref": "#/components/schemas/MinorUnit" - }, - "amount_to_capture": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "amount_capturable": { - "$ref": "#/components/schemas/MinorUnit" - }, - "amount_captured": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - } - } - }, "Connector": { "type": "string", "description": "A connector is an integration to fulfill payments", @@ -12605,6 +12523,88 @@ } } }, + "PaymentAmountDetailsResponse": { + "type": "object", + "required": [ + "currency", + "external_tax_calculation", + "surcharge_calculation", + "net_amount", + "amount_capturable" + ], + "properties": { + "order_amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and 1 for 1¥ since ¥ is a zero-decimal currency. Read more about [the Decimal and Non-Decimal Currencies](https://github.com/juspay/hyperswitch/wiki/Decimal-and-Non%E2%80%90Decimal-Currencies)", + "example": 6540, + "minimum": 0 + }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "shipping_cost": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "order_tax_amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "external_tax_calculation": { + "$ref": "#/components/schemas/TaxCalculationOverride" + }, + "surcharge_calculation": { + "$ref": "#/components/schemas/SurchargeCalculationOverride" + }, + "surcharge_amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "tax_on_surcharge": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "net_amount": { + "$ref": "#/components/schemas/MinorUnit" + }, + "amount_to_capture": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "amount_capturable": { + "$ref": "#/components/schemas/MinorUnit" + }, + "amount_captured": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + } + } + }, "PaymentAttemptResponse": { "type": "object", "required": [ @@ -14467,42 +14467,12 @@ }, "PaymentsCaptureRequest": { "type": "object", - "required": [ - "amount_to_capture" - ], "properties": { - "merchant_id": { - "type": "string", - "description": "The unique identifier for the merchant", - "nullable": true - }, "amount_to_capture": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the user's payment method.", - "example": 6540 - }, - "refund_uncaptured_amount": { - "type": "boolean", - "description": "Decider to refund the uncaptured amount", - "nullable": true - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements.", - "nullable": true - }, - "statement_descriptor_prefix": { - "type": "string", - "description": "Concatenated with the statement descriptor suffix that’s set on the account to form the complete statement descriptor.", - "nullable": true - }, - "merchant_connector_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" - } - ], + "description": "The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured.", + "example": 6540, "nullable": true } } @@ -14605,7 +14575,7 @@ "$ref": "#/components/schemas/IntentStatus" }, "amount": { - "$ref": "#/components/schemas/ConfirmIntentAmountDetailsResponse" + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" }, "customer_id": { "type": "string", @@ -16354,7 +16324,7 @@ "$ref": "#/components/schemas/IntentStatus" }, "amount": { - "$ref": "#/components/schemas/ConfirmIntentAmountDetailsResponse" + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" }, "customer_id": { "type": "string", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 746883028adb..0d750ea7bf53 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -17372,7 +17372,7 @@ "amount_to_capture": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the user's payment method.", + "description": "The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured.", "example": 6540 }, "refund_uncaptured_amount": { diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 6015682f7c38..a78aaf483aa0 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -11,7 +11,7 @@ use super::{ ))] use crate::payment_methods::CustomerPaymentMethodsListResponse; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use crate::payment_methods::CustomerPaymentMethodsListResponse; +use crate::{events, payment_methods::CustomerPaymentMethodsListResponse}; use crate::{ payment_methods::{ CustomerDefaultPaymentMethodResponse, DefaultPaymentMethod, ListCountriesCurrenciesRequest, @@ -418,3 +418,12 @@ impl ApiEventMetric for PaymentStartRedirectionRequest { }) } } + +#[cfg(feature = "v2")] +impl ApiEventMetric for events::PaymentsCaptureResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.id.clone(), + }) + } +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 0ae906f3dc0f..a7211e6f82ad 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -621,7 +621,7 @@ pub struct AmountDetailsResponse { #[cfg(feature = "v2")] #[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] -pub struct ConfirmIntentAmountDetailsResponse { +pub struct PaymentAmountDetailsResponse { /// The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and 1 for 1¥ since ¥ is a zero-decimal currency. Read more about [the Decimal and Non-Decimal Currencies](https://github.com/juspay/hyperswitch/wiki/Decimal-and-Non%E2%80%90Decimal-Currencies) #[schema(value_type = u64, example = 6540)] #[serde(default, deserialize_with = "amount::deserialize")] @@ -4077,6 +4077,7 @@ pub struct PhoneDetails { pub country_code: Option, } +#[cfg(feature = "v1")] #[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] pub struct PaymentsCaptureRequest { /// The unique identifier for the payment @@ -4085,7 +4086,7 @@ pub struct PaymentsCaptureRequest { /// The unique identifier for the merchant #[schema(value_type = Option)] pub merchant_id: Option, - /// The Amount to be captured/ debited from the user's payment method. + /// The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured. #[schema(value_type = i64, example = 6540)] pub amount_to_capture: Option, /// Decider to refund the uncaptured amount @@ -4099,6 +4100,28 @@ pub struct PaymentsCaptureRequest { pub merchant_connector_details: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PaymentsCaptureRequest { + /// The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured. + #[schema(value_type = Option, example = 6540)] + pub amount_to_capture: Option, +} + +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Serialize, ToSchema)] +pub struct PaymentsCaptureResponse { + /// The unique identifier for the payment + pub id: id_type::GlobalPaymentId, + + /// Status of the payment + #[schema(value_type = IntentStatus, example = "succeeded")] + pub status: common_enums::IntentStatus, + + /// Amount details related to the payment + pub amount: PaymentAmountDetailsResponse, +} + #[derive(Default, Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct UrlDetails { pub url: String, @@ -4801,7 +4824,7 @@ pub struct PaymentsConfirmIntentResponse { pub status: api_enums::IntentStatus, /// Amount related information for this payment and attempt - pub amount: ConfirmIntentAmountDetailsResponse, + pub amount: PaymentAmountDetailsResponse, /// The identifier for the customer #[schema( @@ -4879,7 +4902,7 @@ pub struct PaymentsRetrieveResponse { pub status: api_enums::IntentStatus, /// Amount related information for this payment and attempt - pub amount: ConfirmIntentAmountDetailsResponse, + pub amount: PaymentAmountDetailsResponse, /// The identifier for the customer #[schema( diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 6ddc26d49bdb..03facc2ebab4 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -754,7 +754,6 @@ pub enum PaymentAttemptUpdate { #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] #[diesel(table_name = payment_attempt)] pub struct PaymentAttemptUpdateInternal { - // net_amount: Option, pub status: Option, // authentication_type: Option, pub error_message: Option, @@ -774,7 +773,8 @@ pub struct PaymentAttemptUpdateInternal { // multiple_capture_count: Option, // pub surcharge_amount: Option, // tax_on_surcharge: Option, - // amount_capturable: Option, + pub amount_capturable: Option, + pub amount_to_capture: Option, pub updated_by: String, pub merchant_connector_id: Option, pub connector: Option, diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 57549ed201f2..8b834ee5d827 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -369,6 +369,23 @@ pub struct PaymentIntentNew { pub split_payments: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PaymentIntentUpdate { + /// Update the payment intent details on payment intent confirmation, before calling the connector + ConfirmIntent { + status: storage_enums::IntentStatus, + active_attempt_id: common_utils::id_type::GlobalAttemptId, + updated_by: String, + }, + /// Update the payment intent details on payment intent confirmation, after calling the connector + ConfirmIntentPostUpdate { + status: storage_enums::IntentStatus, + amount_captured: Option, + updated_by: String, + }, +} + #[cfg(feature = "v1")] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PaymentIntentUpdate { @@ -514,8 +531,9 @@ pub struct PaymentIntentUpdateFields { #[diesel(table_name = payment_intent)] pub struct PaymentIntentUpdateInternal { pub status: Option, - pub active_attempt_id: Option, + pub amount_captured: Option, pub modified_at: PrimitiveDateTime, + pub active_attempt_id: Option, pub amount: Option, pub currency: Option, pub shipping_cost: Option, diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 0243c2549204..40ab88663c9e 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -226,7 +226,7 @@ impl AmountDetails { common_enums::TaxCalculationOverride::Calculate => None, }; - payment_attempt::AttemptAmountDetails { + payment_attempt::AttemptAmountDetails::from(payment_attempt::AttemptAmountDetailsSetter { net_amount, amount_to_capture: None, surcharge_amount, @@ -235,7 +235,7 @@ impl AmountDetails { amount_capturable: MinorUnit::zero(), shipping_cost: self.shipping_cost, order_tax_amount, - } + }) } pub fn update_from_request(self, req: &api_models::payments::AmountDetailsUpdate) -> Self { @@ -599,6 +599,17 @@ where pub should_sync_with_connector: bool, } +#[cfg(feature = "v2")] +#[derive(Clone)] +pub struct PaymentCaptureData +where + F: Clone, +{ + pub flow: PhantomData, + pub payment_intent: PaymentIntent, + pub payment_attempt: PaymentAttempt, +} + #[cfg(feature = "v2")] impl PaymentStatusData where diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 591ccc4ff981..0a7ada39ecdb 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -189,6 +189,27 @@ pub trait PaymentAttemptInterface { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct AttemptAmountDetails { + /// The total amount for this payment attempt. This includes all the surcharge and tax amounts. + net_amount: MinorUnit, + /// The amount that has to be captured, + amount_to_capture: Option, + /// Surcharge amount for the payment attempt. + /// This is either derived by surcharge rules, or sent by the merchant + surcharge_amount: Option, + /// Tax amount for the payment attempt + /// This is either derived by surcharge rules, or sent by the merchant + tax_on_surcharge: Option, + /// The total amount that can be captured for this payment attempt. + amount_capturable: MinorUnit, + /// Shipping cost for the payment attempt. + shipping_cost: Option, + /// Tax amount for the order. + /// This is either derived by calling an external tax processor, or sent by the merchant + order_tax_amount: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct AttemptAmountDetailsSetter { /// The total amount for this payment attempt. This includes all the surcharge and tax amounts. pub net_amount: MinorUnit, /// The amount that has to be captured, @@ -208,6 +229,67 @@ pub struct AttemptAmountDetails { pub order_tax_amount: Option, } +/// Set the fields of amount details, since the fields are not public +impl From for AttemptAmountDetails { + fn from(setter: AttemptAmountDetailsSetter) -> Self { + Self { + net_amount: setter.net_amount, + amount_to_capture: setter.amount_to_capture, + surcharge_amount: setter.surcharge_amount, + tax_on_surcharge: setter.tax_on_surcharge, + amount_capturable: setter.amount_capturable, + shipping_cost: setter.shipping_cost, + order_tax_amount: setter.order_tax_amount, + } + } +} + +impl AttemptAmountDetails { + pub fn get_net_amount(&self) -> MinorUnit { + self.net_amount + } + + pub fn get_amount_to_capture(&self) -> Option { + self.amount_to_capture + } + + pub fn get_surcharge_amount(&self) -> Option { + self.surcharge_amount + } + + pub fn get_tax_on_surcharge(&self) -> Option { + self.tax_on_surcharge + } + + pub fn get_amount_capturable(&self) -> MinorUnit { + self.amount_capturable + } + + pub fn get_shipping_cost(&self) -> Option { + self.shipping_cost + } + + pub fn get_order_tax_amount(&self) -> Option { + self.order_tax_amount + } + + pub fn set_amount_to_capture(&mut self, amount_to_capture: MinorUnit) { + self.amount_to_capture = Some(amount_to_capture); + } + + /// Validate the amount to capture that is sent in the request + pub fn validate_amount_to_capture( + &self, + request_amount_to_capture: MinorUnit, + ) -> Result<(), ValidationError> { + common_utils::fp_utils::when(request_amount_to_capture > self.get_net_amount(), || { + Err(ValidationError::IncorrectValueProvided { + field_name: "amount_to_capture", + }) + }) + } +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct ErrorDetails { /// The error code that was returned by the connector. @@ -657,6 +739,7 @@ impl NetAmount { #[cfg(feature = "v2")] impl PaymentAttempt { + #[track_caller] pub fn get_total_amount(&self) -> MinorUnit { todo!(); } @@ -1339,15 +1422,28 @@ pub enum PaymentAttemptUpdate { updated_by: String, redirection_data: Option, connector_metadata: Option, + amount_capturable: Option, }, /// Update the payment attempt after force syncing with the connector SyncUpdate { status: storage_enums::AttemptStatus, + amount_capturable: Option, + updated_by: String, + }, + PreCaptureUpdate { + amount_to_capture: Option, + updated_by: String, + }, + /// Update the payment after attempting capture with the connector + CaptureUpdate { + status: storage_enums::AttemptStatus, + amount_capturable: Option, updated_by: String, }, /// Update the payment attempt on confirming the intent, after calling the connector on error response ErrorUpdate { status: storage_enums::AttemptStatus, + amount_capturable: Option, error: ErrorDetails, updated_by: String, connector_payment_id: Option, @@ -1985,11 +2081,14 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: Some(connector), redirection_data: None, connector_metadata: None, + amount_capturable: None, + amount_to_capture: None, }, PaymentAttemptUpdate::ErrorUpdate { status, error, connector_payment_id, + amount_capturable, updated_by, } => Self { status: Some(status), @@ -2006,6 +2105,8 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: None, redirection_data: None, connector_metadata: None, + amount_capturable, + amount_to_capture: None, }, PaymentAttemptUpdate::ConfirmIntentResponse { status, @@ -2013,8 +2114,10 @@ impl From for diesel_models::PaymentAttemptUpdateInternal updated_by, redirection_data, connector_metadata, + amount_capturable, } => Self { status: Some(status), + amount_capturable, error_message: None, error_code: None, modified_at: common_utils::date_time::now(), @@ -2029,9 +2132,15 @@ impl From for diesel_models::PaymentAttemptUpdateInternal redirection_data: redirection_data .map(diesel_models::payment_attempt::RedirectForm::from), connector_metadata, + amount_to_capture: None, }, - PaymentAttemptUpdate::SyncUpdate { status, updated_by } => Self { + PaymentAttemptUpdate::SyncUpdate { + status, + amount_capturable, + updated_by, + } => Self { status: Some(status), + amount_capturable, error_message: None, error_code: None, modified_at: common_utils::date_time::now(), @@ -2045,6 +2154,50 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: None, redirection_data: None, connector_metadata: None, + amount_to_capture: None, + }, + PaymentAttemptUpdate::CaptureUpdate { + status, + amount_capturable, + updated_by, + } => Self { + status: Some(status), + amount_capturable, + amount_to_capture: None, + error_message: None, + error_code: None, + modified_at: common_utils::date_time::now(), + browser_info: None, + error_reason: None, + updated_by, + merchant_connector_id: None, + unified_code: None, + unified_message: None, + connector_payment_id: None, + connector: None, + redirection_data: None, + connector_metadata: None, + }, + PaymentAttemptUpdate::PreCaptureUpdate { + amount_to_capture, + updated_by, + } => Self { + amount_to_capture, + error_message: None, + modified_at: common_utils::date_time::now(), + browser_info: None, + error_code: None, + error_reason: None, + updated_by, + merchant_connector_id: None, + unified_code: None, + unified_message: None, + connector_payment_id: None, + connector: None, + redirection_data: None, + status: None, + connector_metadata: None, + amount_capturable: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 4fd861acb65c..cbd3a8137ca9 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -292,11 +292,18 @@ pub enum PaymentIntentUpdate { /// PostUpdate tracker of ConfirmIntent ConfirmIntentPostUpdate { status: common_enums::IntentStatus, + amount_captured: Option, updated_by: String, }, /// SyncUpdate of ConfirmIntent in PostUpdateTrackers SyncUpdate { status: common_enums::IntentStatus, + amount_captured: Option, + updated_by: String, + }, + CaptureUpdate { + status: common_enums::IntentStatus, + amount_captured: Option, updated_by: String, }, /// UpdateIntent @@ -361,6 +368,7 @@ impl From for diesel_models::PaymentIntentUpdateInternal { active_attempt_id: Some(active_attempt_id), modified_at: common_utils::date_time::now(), amount: None, + amount_captured: None, currency: None, shipping_cost: None, tax_details: None, @@ -392,12 +400,57 @@ impl From for diesel_models::PaymentIntentUpdateInternal { updated_by, }, - PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { + PaymentIntentUpdate::ConfirmIntentPostUpdate { + status, + updated_by, + amount_captured, + } => Self { + status: Some(status), + active_attempt_id: None, + modified_at: common_utils::date_time::now(), + amount_captured, + amount: None, + currency: None, + shipping_cost: None, + tax_details: None, + skip_external_tax_calculation: None, + surcharge_applicable: None, + surcharge_amount: None, + tax_on_surcharge: None, + routing_algorithm_id: None, + capture_method: None, + authentication_type: None, + billing_address: None, + shipping_address: None, + customer_present: None, + description: None, + return_url: None, + setup_future_usage: None, + apply_mit_exemption: None, + statement_descriptor: None, + order_details: None, + allowed_payment_method_types: None, + metadata: None, + connector_metadata: None, + feature_metadata: None, + payment_link_config: None, + request_incremental_authorization: None, + session_expiry: None, + frm_metadata: None, + request_external_three_ds_authentication: None, + updated_by, + }, + PaymentIntentUpdate::SyncUpdate { + status, + amount_captured, + updated_by, + } => Self { status: Some(status), active_attempt_id: None, modified_at: common_utils::date_time::now(), amount: None, currency: None, + amount_captured, shipping_cost: None, tax_details: None, skip_external_tax_calculation: None, @@ -427,8 +480,13 @@ impl From for diesel_models::PaymentIntentUpdateInternal { request_external_three_ds_authentication: None, updated_by, }, - PaymentIntentUpdate::SyncUpdate { status, updated_by } => Self { + PaymentIntentUpdate::CaptureUpdate { + status, + amount_captured, + updated_by, + } => Self { status: Some(status), + amount_captured, active_attempt_id: None, modified_at: common_utils::date_time::now(), amount: None, @@ -499,7 +557,7 @@ impl From for diesel_models::PaymentIntentUpdateInternal { status: None, active_attempt_id: None, modified_at: common_utils::date_time::now(), - + amount_captured: None, amount, currency, shipping_cost, diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index b070c6bc848a..f69c1db0e10b 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -405,15 +405,30 @@ use crate::{ /// Get updatable trakcer objects of payment intent and payment attempt #[cfg(feature = "v2")] -pub trait TrackerPostUpdateObjects { +pub trait TrackerPostUpdateObjects { fn get_payment_intent_update( &self, + payment_data: &D, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate; + fn get_payment_attempt_update( &self, + payment_data: &D, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate; + + /// Get the amount that can be captured for the payment + fn get_amount_capturable(&self, payment_data: &D) -> Option; + + /// Get the amount that has been captured for the payment + fn get_captured_amount(&self, payment_data: &D) -> Option; + + /// Get the attempt status based on parameters like captured amount and amount capturable + fn get_attempt_status_for_db_update( + &self, + payment_data: &D, + ) -> common_enums::enums::AttemptStatus; } #[cfg(feature = "v2")] @@ -421,6 +436,7 @@ impl TrackerPostUpdateObjects< router_flow_types::Authorize, router_request_types::PaymentsAuthorizeData, + payments::PaymentConfirmData, > for RouterData< router_flow_types::Authorize, @@ -430,11 +446,16 @@ impl { fn get_payment_intent_update( &self, + payment_data: &payments::PaymentConfirmData, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); match self.response { Ok(ref _response) => PaymentIntentUpdate::ConfirmIntentPostUpdate { - status: common_enums::IntentStatus::from(self.status), + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, updated_by: storage_scheme.to_string(), }, Err(ref error) => PaymentIntentUpdate::ConfirmIntentPostUpdate { @@ -442,6 +463,7 @@ impl .attempt_status .map(common_enums::IntentStatus::from) .unwrap_or(common_enums::IntentStatus::Failed), + amount_captured, updated_by: storage_scheme.to_string(), }, } @@ -449,8 +471,11 @@ impl fn get_payment_attempt_update( &self, + payment_data: &payments::PaymentConfirmData, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + match self.response { Ok(ref response_router_data) => match response_router_data { router_response_types::PaymentsResponseData::TransactionResponse { @@ -463,7 +488,8 @@ impl incremental_authorization_allowed, charge_id, } => { - let attempt_status = self.status; + let attempt_status = self.get_attempt_status_for_db_update(payment_data); + let connector_payment_id = match resource_id { router_request_types::ResponseId::NoResponseId => None, router_request_types::ResponseId::ConnectorTransactionId(id) @@ -475,6 +501,7 @@ impl connector_payment_id, updated_by: storage_scheme.to_string(), redirection_data: *redirection_data.clone(), + amount_capturable, connector_metadata: connector_metadata.clone().map(Secret::new), } } @@ -528,16 +555,310 @@ impl PaymentAttemptUpdate::ErrorUpdate { status: attempt_status, error: error_details, + amount_capturable, connector_payment_id: connector_transaction_id, updated_by: storage_scheme.to_string(), } } } } + + fn get_attempt_status_for_db_update( + &self, + _payment_data: &payments::PaymentConfirmData, + ) -> common_enums::AttemptStatus { + // For this step, consider whatever status was given by the connector module + // We do not need to check for amount captured or amount capturable here because we are authorizing the whole amount + self.status + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> Option { + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is already succeeded / failed we cannot capture any more amount + // So set the amount capturable to zero + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // If status is requires capture, get the total amount that can be captured + // This is in cases where the capture method will be `manual` or `manual_multiple` + // We do not need to handle the case where amount_to_capture is provided here as it cannot be passed in authroize flow + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow, after doing authorization this state is invalid + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> Option { + // Based on the status of the response, we can determine the amount that was captured + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is succeeded then we have captured the whole amount + // we need not check for `amount_to_capture` here because passing `amount_to_capture` when authorizing is not supported + common_enums::IntentStatus::Succeeded => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // No amount has been captured yet + common_enums::IntentStatus::RequiresCapture => Some(MinorUnit::zero()), + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } } #[cfg(feature = "v2")] -impl TrackerPostUpdateObjects +impl + TrackerPostUpdateObjects< + router_flow_types::Capture, + router_request_types::PaymentsCaptureData, + payments::PaymentCaptureData, + > + for RouterData< + router_flow_types::Capture, + router_request_types::PaymentsCaptureData, + router_response_types::PaymentsResponseData, + > +{ + fn get_payment_intent_update( + &self, + payment_data: &payments::PaymentCaptureData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); + match self.response { + Ok(ref _response) => PaymentIntentUpdate::CaptureUpdate { + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + Err(ref error) => PaymentIntentUpdate::CaptureUpdate { + status: error + .attempt_status + .map(common_enums::IntentStatus::from) + .unwrap_or(common_enums::IntentStatus::Failed), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + } + } + + fn get_payment_attempt_update( + &self, + payment_data: &payments::PaymentCaptureData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + + match self.response { + Ok(ref response_router_data) => match response_router_data { + router_response_types::PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data, + mandate_reference, + connector_metadata, + network_txn_id, + connector_response_reference_id, + incremental_authorization_allowed, + charge_id, + } => { + let attempt_status = self.status; + + PaymentAttemptUpdate::CaptureUpdate { + status: attempt_status, + amount_capturable, + updated_by: storage_scheme.to_string(), + } + } + router_response_types::PaymentsResponseData::MultipleCaptureResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::SessionTokenResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::TransactionUnresolvedResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::TokenizationResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::ConnectorCustomerResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::ThreeDSEnrollmentResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PreProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::IncrementalAuthorizationResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PostProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionUpdateResponse { .. } => { + todo!() + } + }, + Err(ref error_response) => { + let ErrorResponse { + code, + message, + reason, + status_code: _, + attempt_status, + connector_transaction_id, + } = error_response.clone(); + let attempt_status = attempt_status.unwrap_or(self.status); + + let error_details = ErrorDetails { + code, + message, + reason, + unified_code: None, + unified_message: None, + }; + + PaymentAttemptUpdate::ErrorUpdate { + status: attempt_status, + error: error_details, + amount_capturable, + connector_payment_id: connector_transaction_id, + updated_by: storage_scheme.to_string(), + } + } + } + } + + fn get_attempt_status_for_db_update( + &self, + payment_data: &payments::PaymentCaptureData, + ) -> common_enums::AttemptStatus { + match self.status { + common_enums::AttemptStatus::Charged => { + let amount_captured = self + .get_captured_amount(payment_data) + .unwrap_or(MinorUnit::zero()); + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + + if amount_captured == total_amount { + common_enums::AttemptStatus::Charged + } else { + common_enums::AttemptStatus::PartialCharged + } + } + _ => self.status, + } + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentCaptureData, + ) -> Option { + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is already succeeded / failed we cannot capture any more amount + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentCaptureData, + ) -> Option { + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is succeeded then we have captured the whole amount + common_enums::IntentStatus::Succeeded => { + let amount_to_capture = payment_data + .payment_attempt + .amount_details + .get_amount_to_capture(); + + let amount_captured = amount_to_capture + .unwrap_or(payment_data.payment_attempt.amount_details.get_net_amount()); + + Some(amount_captured) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => { + todo!() + } + } + } +} + +#[cfg(feature = "v2")] +impl + TrackerPostUpdateObjects< + router_flow_types::PSync, + router_request_types::PaymentsSyncData, + payments::PaymentStatusData, + > for RouterData< router_flow_types::PSync, router_request_types::PaymentsSyncData, @@ -546,11 +867,16 @@ impl TrackerPostUpdateObjects, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); match self.response { Ok(ref _response) => PaymentIntentUpdate::SyncUpdate { - status: common_enums::IntentStatus::from(self.status), + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, updated_by: storage_scheme.to_string(), }, Err(ref error) => PaymentIntentUpdate::SyncUpdate { @@ -558,6 +884,7 @@ impl TrackerPostUpdateObjects, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + match self.response { Ok(ref response_router_data) => match response_router_data { router_response_types::PaymentsResponseData::TransactionResponse { @@ -579,15 +909,11 @@ impl TrackerPostUpdateObjects { - let attempt_status = self.status; - let connector_payment_id = match resource_id { - router_request_types::ResponseId::NoResponseId => None, - router_request_types::ResponseId::ConnectorTransactionId(id) - | router_request_types::ResponseId::EncodedData(id) => Some(id.to_owned()), - }; + let attempt_status = self.get_attempt_status_for_db_update(payment_data); PaymentAttemptUpdate::SyncUpdate { status: attempt_status, + amount_capturable, updated_by: storage_scheme.to_string(), } } @@ -641,10 +967,106 @@ impl TrackerPostUpdateObjects, + ) -> common_enums::AttemptStatus { + match self.status { + common_enums::AttemptStatus::Charged => { + let amount_captured = self + .get_captured_amount(payment_data) + .unwrap_or(MinorUnit::zero()); + + let total_amount = payment_data + .payment_attempt + .as_ref() + .map(|attempt| attempt.amount_details.get_net_amount()) + .unwrap_or(MinorUnit::zero()); + + if amount_captured == total_amount { + common_enums::AttemptStatus::Charged + } else { + common_enums::AttemptStatus::PartialCharged + } + } + _ => self.status, + } + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentStatusData, + ) -> Option { + let payment_attempt = payment_data.payment_attempt.as_ref()?; + + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is already succeeded / failed we cannot capture any more amount + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentStatusData, + ) -> Option { + let payment_attempt = payment_data.payment_attempt.as_ref()?; + + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is succeeded then we have captured the whole amount or amount_to_capture + common_enums::IntentStatus::Succeeded => { + let amount_to_capture = payment_attempt.amount_details.get_amount_to_capture(); + + let amount_captured = + amount_to_capture.unwrap_or(payment_attempt.amount_details.get_net_amount()); + + Some(amount_captured) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } } diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index c7425aed2a51..7553a780c272 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -639,7 +639,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ErrorDetails, api_models::payments::CtpServiceDetails, common_utils::types::BrowserInformation, - api_models::payments::ConfirmIntentAmountDetailsResponse, + api_models::payments::PaymentAmountDetailsResponse, routes::payments::ForceSync, )), modifiers(&SecurityAddon) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index f758dc0b0e55..e68bd09ba462 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -40,7 +40,7 @@ use helpers::{decrypt_paze_token, ApplePayData}; use hyperswitch_domain_models::payments::payment_intent::CustomerData; #[cfg(feature = "v2")] use hyperswitch_domain_models::payments::{ - PaymentConfirmData, PaymentIntentData, PaymentStatusData, + PaymentCaptureData, PaymentConfirmData, PaymentIntentData, PaymentStatusData, }; #[cfg(feature = "v2")] use hyperswitch_domain_models::router_response_types::RedirectForm; @@ -149,7 +149,7 @@ where services::api::ConnectorIntegration, RouterData: - hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, // To perform router related operation for PaymentResponse PaymentResponse: Operation, @@ -1519,8 +1519,12 @@ where FData: Send + Sync + Clone, Op: Operation + ValidateStatusForOperation + Send + Sync + Clone, Req: Debug, - D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, - Res: transformers::ToResponse, + D: OperationSessionGetters + + OperationSessionSetters + + transformers::GenerateResponse + + Send + + Sync + + Clone, // To create connector flow specific interface data D: ConstructFlowSpecificData, RouterData: Feature, @@ -1534,13 +1538,14 @@ where // To create updatable objects in post update tracker RouterData: - hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, { // Validate the request fields - let validate_result = operation + operation .to_validate_request()? .validate_request(&req, &merchant_account)?; + // Get the tracker related information. This includes payment intent and payment attempt let get_tracker_response = operation .to_get_tracker()? .get_trackers( @@ -1569,12 +1574,8 @@ where ) .await?; - Res::generate_response( - payment_data, - customer, - &state.base_url, - operation, - &state.conf.connector_request_reference_id_config, + payment_data.generate_response( + &state, connector_http_status_code, external_latency, header_payload.x_hs_latency, @@ -6640,7 +6641,7 @@ pub async fn payment_start_redirection( services::RedirectionFormData { redirect_form: redirection_data, payment_method_data: None, - amount: payment_attempt.amount_details.net_amount.to_string(), + amount: payment_attempt.amount_details.get_net_amount().to_string(), currency: payment_intent.amount_details.currency.to_string(), }, ))) @@ -7800,3 +7801,218 @@ impl OperationSessionSetters for PaymentStatusData { todo!() } } + +#[cfg(feature = "v2")] +impl OperationSessionGetters for PaymentCaptureData { + #[track_caller] + fn get_payment_attempt(&self) -> &storage::PaymentAttempt { + &self.payment_attempt + } + + fn get_payment_intent(&self) -> &storage::PaymentIntent { + &self.payment_intent + } + + fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod> { + todo!() + } + + fn get_mandate_id(&self) -> Option<&payments_api::MandateIds> { + todo!() + } + + // what is this address find out and not required remove this + fn get_address(&self) -> &PaymentAddress { + todo!() + } + + fn get_creds_identifier(&self) -> Option<&str> { + None + } + + fn get_token(&self) -> Option<&str> { + todo!() + } + + fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData> { + todo!() + } + + fn get_payment_link_data(&self) -> Option { + todo!() + } + + fn get_ephemeral_key(&self) -> Option { + todo!() + } + + fn get_setup_mandate(&self) -> Option<&MandateData> { + todo!() + } + + fn get_poll_config(&self) -> Option { + todo!() + } + + fn get_authentication(&self) -> Option<&storage::Authentication> { + todo!() + } + + fn get_frm_message(&self) -> Option { + todo!() + } + + fn get_refunds(&self) -> Vec { + todo!() + } + + fn get_disputes(&self) -> Vec { + todo!() + } + + fn get_authorizations(&self) -> Vec { + todo!() + } + + fn get_attempts(&self) -> Option> { + todo!() + } + + fn get_recurring_details(&self) -> Option<&RecurringDetails> { + todo!() + } + + fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> { + Some(&self.payment_intent.profile_id) + } + + fn get_currency(&self) -> storage_enums::Currency { + self.payment_intent.amount_details.currency + } + + fn get_amount(&self) -> api::Amount { + todo!() + } + + fn get_payment_attempt_connector(&self) -> Option<&str> { + todo!() + } + + fn get_billing_address(&self) -> Option { + todo!() + } + + fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData> { + todo!() + } + + fn get_sessions_token(&self) -> Vec { + todo!() + } + + fn get_token_data(&self) -> Option<&storage::PaymentTokenData> { + todo!() + } + + fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails> { + todo!() + } + + fn get_force_sync(&self) -> Option { + todo!() + } + + fn get_capture_method(&self) -> Option { + todo!() + } + + #[cfg(feature = "v2")] + fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt> { + Some(&self.payment_attempt) + } +} + +#[cfg(feature = "v2")] +impl OperationSessionSetters for PaymentCaptureData { + fn set_payment_intent(&mut self, payment_intent: storage::PaymentIntent) { + self.payment_intent = payment_intent; + } + + fn set_payment_attempt(&mut self, payment_attempt: storage::PaymentAttempt) { + self.payment_attempt = payment_attempt; + } + + fn set_payment_method_data(&mut self, _payment_method_data: Option) { + todo!() + } + + fn set_payment_method_id_in_attempt(&mut self, _payment_method_id: Option) { + todo!() + } + + fn set_email_if_not_present(&mut self, _email: pii::Email) { + todo!() + } + + fn set_pm_token(&mut self, _token: String) { + todo!() + } + + fn set_connector_customer_id(&mut self, _customer_id: Option) { + // TODO: handle this case. Should we add connector_customer_id in paymentConfirmData? + } + + fn push_sessions_token(&mut self, _token: api::SessionToken) { + todo!() + } + + fn set_surcharge_details(&mut self, _surcharge_details: Option) { + todo!() + } + + #[track_caller] + fn set_merchant_connector_id_in_attempt( + &mut self, + merchant_connector_id: Option, + ) { + todo!() + } + + fn set_frm_message(&mut self, _frm_message: FraudCheck) { + todo!() + } + + fn set_payment_intent_status(&mut self, status: storage_enums::IntentStatus) { + self.payment_intent.status = status; + } + + fn set_authentication_type_in_attempt( + &mut self, + _authentication_type: Option, + ) { + todo!() + } + + fn set_recurring_mandate_payment_data( + &mut self, + _recurring_mandate_payment_data: + hyperswitch_domain_models::router_data::RecurringMandatePaymentData, + ) { + todo!() + } + + fn set_mandate_id(&mut self, _mandate_id: api_models::payments::MandateIds) { + todo!() + } + + fn set_setup_future_usage_in_payment_intent( + &mut self, + setup_future_usage: storage_enums::FutureUsage, + ) { + self.payment_intent.setup_future_usage = setup_future_usage; + } + + fn set_connector_in_payment_attempt(&mut self, connector: Option) { + todo!() + } +} diff --git a/crates/router/src/core/payments/flows/capture_flow.rs b/crates/router/src/core/payments/flows/capture_flow.rs index 8b6fa24fbfd7..1863d673ee9c 100644 --- a/crates/router/src/core/payments/flows/capture_flow.rs +++ b/crates/router/src/core/payments/flows/capture_flow.rs @@ -11,12 +11,12 @@ use crate::{ types::{self, api, domain}, }; +#[cfg(feature = "v1")] #[async_trait] impl ConstructFlowSpecificData for PaymentData { - #[cfg(feature = "v1")] async fn construct_router_data<'a>( &self, state: &SessionState, @@ -45,7 +45,24 @@ impl .await } - #[cfg(feature = "v2")] + async fn get_merchant_recipient_data<'a>( + &self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _merchant_connector_account: &helpers::MerchantConnectorAccountType, + _connector: &api::ConnectorData, + ) -> RouterResult> { + Ok(None) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl + ConstructFlowSpecificData + for hyperswitch_domain_models::payments::PaymentCaptureData +{ async fn construct_router_data<'a>( &self, state: &SessionState, @@ -56,8 +73,21 @@ impl merchant_connector_account: &domain::MerchantConnectorAccount, merchant_recipient_data: Option, header_payload: Option, - ) -> RouterResult { - todo!() + ) -> RouterResult< + types::RouterData, + > { + Box::pin(transformers::construct_payment_router_data_for_capture( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + merchant_recipient_data, + header_payload, + )) + .await } async fn get_merchant_recipient_data<'a>( diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index a7e60274d135..38911bb2d6df 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -42,6 +42,9 @@ pub mod payment_update_intent; #[cfg(feature = "v2")] pub mod payment_get; +#[cfg(feature = "v2")] +pub mod payment_capture_v2; + use api_models::enums::FrmSuggestion; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] use api_models::routing::RoutableConnectorChoice; @@ -439,7 +442,7 @@ pub trait PostUpdateTracker: Send { where F: 'b + Send + Sync, types::RouterData: - hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; async fn save_pm_and_mandate<'b>( &self, diff --git a/crates/router/src/core/payments/operations/payment_capture_v2.rs b/crates/router/src/core/payments/operations/payment_capture_v2.rs new file mode 100644 index 000000000000..0ee62ea73c1f --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_capture_v2.rs @@ -0,0 +1,324 @@ +use api_models::{enums::FrmSuggestion, payments::PaymentsCaptureRequest}; +use async_trait::async_trait; +use error_stack::ResultExt; +use hyperswitch_domain_models::payments::PaymentCaptureData; +use router_env::{instrument, tracing}; + +use super::{Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, CustomResult, RouterResult, StorageErrorExt}, + payments::operations::{self, ValidateStatusForOperation}, + }, + routes::{app::ReqState, SessionState}, + types::{ + api::{self, ConnectorCallType}, + domain::{self}, + storage::{self, enums as storage_enums}, + }, + utils::OptionExt, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PaymentsCapture; + +impl ValidateStatusForOperation for PaymentsCapture { + /// Validate if the current operation can be performed on the current status of the payment intent + fn validate_status_for_operation( + &self, + intent_status: common_enums::IntentStatus, + ) -> Result<(), errors::ApiErrorResponse> { + match intent_status { + common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => Ok(()), + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Processing + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresConfirmation => { + Err(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: format!("{self:?}"), + field_name: "status".to_string(), + current_value: intent_status.to_string(), + states: [ + common_enums::IntentStatus::RequiresCapture, + common_enums::IntentStatus::PartiallyCapturedAndCapturable, + ] + .map(|enum_value| enum_value.to_string()) + .join(", "), + }) + } + } + } +} + +type BoxedConfirmOperation<'b, F> = + super::BoxedOperation<'b, F, PaymentsCaptureRequest, PaymentCaptureData>; + +// TODO: change the macro to include changes for v2 +// TODO: PaymentData in the macro should be an input +impl Operation for &PaymentsCapture { + type Data = PaymentCaptureData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(*self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> + { + Ok(*self) + } +} +#[automatically_derived] +impl Operation for PaymentsCapture { + type Data = PaymentCaptureData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&dyn Domain> { + Ok(self) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> + { + Ok(self) + } +} + +impl ValidateRequest> + for PaymentsCapture +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + _request: &PaymentsCaptureRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult { + let validate_result = operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }; + + Ok(validate_result) + } +} + +#[async_trait] +impl GetTracker, PaymentsCaptureRequest> + for PaymentsCapture +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a SessionState, + payment_id: &common_utils::id_type::GlobalPaymentId, + request: &PaymentsCaptureRequest, + merchant_account: &domain::MerchantAccount, + _profile: &domain::Profile, + key_store: &domain::MerchantKeyStore, + _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult>> { + let db = &*state.store; + let key_manager_state = &state.into(); + + let storage_scheme = merchant_account.storage_scheme; + + let payment_intent = db + .find_payment_intent_by_id(key_manager_state, payment_id, key_store, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + self.validate_status_for_operation(payment_intent.status)?; + + let active_attempt_id = payment_intent + .active_attempt_id + .as_ref() + .get_required_value("active_attempt_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Active attempt id is none when capturing the payment")?; + + let mut payment_attempt = db + .find_payment_attempt_by_id( + key_manager_state, + key_store, + active_attempt_id, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not find payment attempt given the attempt id")?; + + if let Some(amount_to_capture) = request.amount_to_capture { + payment_attempt + .amount_details + .validate_amount_to_capture(amount_to_capture) + .change_context(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "`amount_to_capture` is greater than the net amount {}", + payment_attempt.amount_details.get_net_amount() + ), + })?; + + payment_attempt + .amount_details + .set_amount_to_capture(amount_to_capture); + } + + let payment_data = PaymentCaptureData { + flow: std::marker::PhantomData, + payment_intent, + payment_attempt, + }; + + let get_trackers_response = operations::GetTrackerResponse { payment_data }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl Domain> for PaymentsCapture { + async fn get_customer_details<'a>( + &'a self, + state: &SessionState, + payment_data: &mut PaymentCaptureData, + merchant_key_store: &domain::MerchantKeyStore, + storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult<(BoxedConfirmOperation<'a, F>, Option), errors::StorageError> + { + match payment_data.payment_intent.customer_id.clone() { + Some(id) => { + let customer = state + .store + .find_customer_by_global_id( + &state.into(), + &id, + &payment_data.payment_intent.merchant_id, + merchant_key_store, + storage_scheme, + ) + .await?; + + Ok((Box::new(self), Some(customer))) + } + None => Ok((Box::new(self), None)), + } + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a SessionState, + _payment_data: &mut PaymentCaptureData, + _storage_scheme: storage_enums::MerchantStorageScheme, + _key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, + ) -> RouterResult<( + BoxedConfirmOperation<'a, F>, + Option, + Option, + )> { + Ok((Box::new(self), None, None)) + } + + #[instrument(skip_all)] + async fn perform_routing<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, + state: &SessionState, + // TODO: do not take the whole payment data here + payment_data: &mut PaymentCaptureData, + _mechant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + let payment_attempt = &payment_data.payment_attempt; + let connector = payment_attempt + .connector + .as_ref() + .get_required_value("connector") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Connector is none when constructing response")?; + + let merchant_connector_id = payment_attempt + .merchant_connector_id + .as_ref() + .get_required_value("merchant_connector_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Merchant connector id is none when constructing response")?; + + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector, + api::GetToken::Connector, + Some(merchant_connector_id.to_owned()), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + Ok(ConnectorCallType::PreDetermined(connector_data)) + } +} + +#[async_trait] +impl UpdateTracker, PaymentsCaptureRequest> for PaymentsCapture { + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + state: &'b SessionState, + _req_state: ReqState, + mut payment_data: PaymentCaptureData, + _customer: Option, + storage_scheme: storage_enums::MerchantStorageScheme, + _updated_customer: Option, + key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult<(BoxedConfirmOperation<'b, F>, PaymentCaptureData)> + where + F: 'b + Send, + { + let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::PreCaptureUpdate { amount_to_capture: payment_data.payment_attempt.amount_details.get_amount_to_capture(), updated_by: storage_scheme.to_string() }; + + let payment_attempt = state + .store + .update_payment_attempt( + &state.into(), + key_store, + payment_data.payment_attempt.clone(), + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not update payment attempt")?; + + payment_data.payment_attempt = payment_attempt; + Ok((Box::new(self), payment_data)) + } +} diff --git a/crates/router/src/core/payments/operations/payment_get.rs b/crates/router/src/core/payments/operations/payment_get.rs index 92c8240d187c..403ee5443148 100644 --- a/crates/router/src/core/payments/operations/payment_get.rs +++ b/crates/router/src/core/payments/operations/payment_get.rs @@ -1,38 +1,23 @@ -use api_models::{ - admin::ExtendedCardInfoConfig, - enums::FrmSuggestion, - payments::{ExtendedCardInfo, GetAddressFromPaymentMethodData, PaymentsRetrieveRequest}, -}; +use api_models::{enums::FrmSuggestion, payments::PaymentsRetrieveRequest}; use async_trait::async_trait; use common_utils::ext_traits::AsyncExt; use error_stack::ResultExt; -use hyperswitch_domain_models::payments::{ - payment_attempt::PaymentAttempt, PaymentIntent, PaymentStatusData, -}; +use hyperswitch_domain_models::payments::PaymentStatusData; use router_env::{instrument, tracing}; -use tracing_futures::Instrument; use super::{Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ core::{ - authentication, errors::{self, CustomResult, RouterResult, StorageErrorExt}, - payments::{ - self, helpers, - operations::{self, ValidateStatusForOperation}, - populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, - }, - utils as core_utils, + payments::operations::{self, ValidateStatusForOperation}, }, routes::{app::ReqState, SessionState}, - services, types::{ - self, - api::{self, ConnectorCallType, PaymentIdTypeExt}, + api::{self, ConnectorCallType}, domain::{self}, storage::{self, enums as storage_enums}, }, - utils::{self, OptionExt}, + utils::OptionExt, }; #[derive(Debug, Clone, Copy)] @@ -107,7 +92,7 @@ impl ValidateRequest( &'b self, - request: &PaymentsRetrieveRequest, + _request: &PaymentsRetrieveRequest, merchant_account: &'a domain::MerchantAccount, ) -> RouterResult { let validate_result = operations::ValidateResult { @@ -133,7 +118,7 @@ impl GetTracker, PaymentsRetriev merchant_account: &domain::MerchantAccount, _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, - header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); @@ -231,12 +216,12 @@ impl Domain( &'a self, - state: &'a SessionState, - payment_data: &mut PaymentStatusData, - storage_scheme: storage_enums::MerchantStorageScheme, - key_store: &domain::MerchantKeyStore, - customer: &Option, - business_profile: &domain::Profile, + _state: &'a SessionState, + _payment_data: &mut PaymentStatusData, + _storage_scheme: storage_enums::MerchantStorageScheme, + _key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, ) -> RouterResult<( BoxedConfirmOperation<'a, F>, Option, @@ -248,12 +233,12 @@ impl Domain( &'a self, - merchant_account: &domain::MerchantAccount, - business_profile: &domain::Profile, + _merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, state: &SessionState, // TODO: do not take the whole payment data here payment_data: &mut PaymentStatusData, - mechant_key_store: &domain::MerchantKeyStore, + _mechant_key_store: &domain::MerchantKeyStore, ) -> CustomResult { match &payment_data.payment_attempt { Some(payment_attempt) if payment_data.should_sync_with_connector => { @@ -294,15 +279,15 @@ impl UpdateTracker, PaymentsRetrieveReq #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - state: &'b SessionState, - req_state: ReqState, - mut payment_data: PaymentStatusData, - customer: Option, - storage_scheme: storage_enums::MerchantStorageScheme, - updated_customer: Option, - key_store: &domain::MerchantKeyStore, - frm_suggestion: Option, - header_payload: hyperswitch_domain_models::payments::HeaderPayload, + _state: &'b SessionState, + _req_state: ReqState, + payment_data: PaymentStatusData, + _customer: Option, + _storage_scheme: storage_enums::MerchantStorageScheme, + _updated_customer: Option, + _key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: hyperswitch_domain_models::payments::HeaderPayload, ) -> RouterResult<(BoxedConfirmOperation<'b, F>, PaymentStatusData)> where F: 'b + Send, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 01a4e0860918..988429a0eecb 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2200,6 +2200,88 @@ impl Operation for PaymentResp } } +#[cfg(feature = "v2")] +impl Operation for PaymentResponse { + type Data = hyperswitch_domain_models::payments::PaymentCaptureData; + fn to_post_update_tracker( + &self, + ) -> RouterResult< + &(dyn PostUpdateTracker + Send + Sync), + > { + Ok(self) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl + PostUpdateTracker< + F, + hyperswitch_domain_models::payments::PaymentCaptureData, + types::PaymentsCaptureData, + > for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + state: &'b SessionState, + mut payment_data: hyperswitch_domain_models::payments::PaymentCaptureData, + response: types::RouterData, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send + Sync, + types::RouterData: + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< + F, + types::PaymentsCaptureData, + hyperswitch_domain_models::payments::PaymentCaptureData, + >, + { + use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; + + let db = &*state.store; + let key_manager_state = &state.into(); + + let response_router_data = response; + + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); + + let payment_attempt_update = + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); + + let updated_payment_intent = db + .update_payment_intent( + key_manager_state, + payment_data.payment_intent, + payment_intent_update, + key_store, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment intent")?; + + let updated_payment_attempt = db + .update_payment_attempt( + key_manager_state, + key_store, + payment_data.payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment attempt")?; + + payment_data.payment_intent = updated_payment_intent; + payment_data.payment_attempt = updated_payment_attempt; + + Ok(payment_data) + } +} + #[cfg(feature = "v2")] #[async_trait] impl PostUpdateTracker, types::PaymentsAuthorizeData> @@ -2219,6 +2301,7 @@ impl PostUpdateTracker, types::PaymentsAuthor hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< F, types::PaymentsAuthorizeData, + PaymentConfirmData, >, { use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; @@ -2228,9 +2311,10 @@ impl PostUpdateTracker, types::PaymentsAuthor let response_router_data = response; - let payment_intent_update = response_router_data.get_payment_intent_update(storage_scheme); + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); let payment_attempt_update = - response_router_data.get_payment_attempt_update(storage_scheme); + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); let updated_payment_intent = db .update_payment_intent( @@ -2293,6 +2377,7 @@ impl PostUpdateTracker, types::PaymentsSyncDat hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< F, types::PaymentsSyncData, + PaymentStatusData, >, { use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; @@ -2302,9 +2387,10 @@ impl PostUpdateTracker, types::PaymentsSyncDat let response_router_data = response; - let payment_intent_update = response_router_data.get_payment_intent_update(storage_scheme); + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); let payment_attempt_update = - response_router_data.get_payment_attempt_update(storage_scheme); + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); let payment_attempt = payment_data .payment_attempt diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 8652844623f1..d4c3877e5d97 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -19,8 +19,6 @@ use diesel_models::{ }; use error_stack::{report, ResultExt}; #[cfg(feature = "v2")] -use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentIntentData}; -#[cfg(feature = "v2")] use hyperswitch_domain_models::ApiModelToDieselModelConvertor; use hyperswitch_domain_models::{payments::payment_intent::CustomerData, router_request_types}; #[cfg(feature = "v2")] @@ -183,7 +181,7 @@ where #[allow(clippy::too_many_arguments)] pub async fn construct_payment_router_data_for_authorize<'a>( state: &'a SessionState, - payment_data: PaymentConfirmData, + payment_data: hyperswitch_domain_models::payments::PaymentConfirmData, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -259,9 +257,9 @@ pub async fn construct_payment_router_data_for_authorize<'a>( amount: payment_data .payment_attempt .amount_details - .net_amount + .get_net_amount() .get_amount_as_i64(), - minor_amount: payment_data.payment_attempt.amount_details.net_amount, + minor_amount: payment_data.payment_attempt.amount_details.get_net_amount(), order_tax_amount: None, currency: payment_data.payment_intent.amount_details.currency, browser_info: None, @@ -382,6 +380,174 @@ pub async fn construct_payment_router_data_for_authorize<'a>( Ok(router_data) } +#[cfg(feature = "v2")] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn construct_payment_router_data_for_capture<'a>( + state: &'a SessionState, + payment_data: hyperswitch_domain_models::payments::PaymentCaptureData, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &'a Option, + merchant_connector_account: &domain::MerchantConnectorAccount, + _merchant_recipient_data: Option, + header_payload: Option, +) -> RouterResult { + use masking::ExposeOptionInterface; + + fp_utils::when(merchant_connector_account.is_disabled(), || { + Err(errors::ApiErrorResponse::MerchantConnectorAccountDisabled) + })?; + + let auth_type = merchant_connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + let customer_id = customer + .to_owned() + .map(|customer| common_utils::id_type::CustomerId::try_from(customer.id.clone())) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Invalid global customer generated, not able to convert to reference id", + )?; + + let payment_method = payment_data.payment_attempt.payment_method_type; + + let connector_request_reference_id = payment_data + .payment_intent + .merchant_reference_id + .map(|id| id.get_string_repr().to_owned()) + .unwrap_or(payment_data.payment_attempt.id.get_string_repr().to_owned()); + + let connector_mandate_request_reference_id = payment_data + .payment_attempt + .connector_mandate_detail + .as_ref() + .and_then(|detail| detail.get_connector_mandate_request_reference_id()); + + let connector = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector_id, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + + let amount_to_capture = payment_data + .payment_attempt + .amount_details + .get_amount_to_capture() + .unwrap_or(payment_data.payment_attempt.amount_details.get_net_amount()); + + let amount = payment_data.payment_attempt.amount_details.get_net_amount(); + let request = types::PaymentsCaptureData { + capture_method: Some(payment_data.payment_intent.capture_method), + amount_to_capture: amount_to_capture.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_amount_to_capture: amount_to_capture, + currency: payment_data.payment_intent.amount_details.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + payment_amount: amount.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_payment_amount: amount, + connector_meta: payment_data + .payment_attempt + .connector_metadata + .clone() + .expose_option(), + // TODO: add multiple capture data + multiple_capture_data: None, + // TODO: why do we need browser info during capture? + browser_info: None, + metadata: payment_data.payment_intent.metadata.expose_option(), + integrity_object: None, + }; + + // TODO: evaluate the fields in router data, if they are required or not + let router_data = types::RouterData { + flow: PhantomData, + merchant_id: merchant_account.get_id().clone(), + // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. + customer_id, + connector: connector_id.to_owned(), + // TODO: evaluate why we need payment id at the connector level. We already have connector reference id + payment_id: payment_data + .payment_attempt + .payment_id + .get_string_repr() + .to_owned(), + // TODO: evaluate why we need attempt id at the connector level. We already have connector reference id + attempt_id: payment_data + .payment_attempt + .get_id() + .get_string_repr() + .to_owned(), + status: payment_data.payment_attempt.status, + payment_method, + connector_auth_type: auth_type, + description: payment_data + .payment_intent + .description + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: evaluate why we need to send merchant's return url here + // This should be the return url of application, since application takes care of the redirection + return_url: payment_data + .payment_intent + .return_url + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: Create unified address + address: hyperswitch_domain_models::payment_address::PaymentAddress::default(), + auth_type: payment_data.payment_attempt.authentication_type, + connector_meta_data: None, + connector_wallets_details: None, + request, + response: Err(hyperswitch_domain_models::router_data::ErrorResponse::default()), + amount_captured: None, + minor_amount_captured: None, + access_token: None, + session_token: None, + reference_id: None, + payment_method_status: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + // TODO: This has to be generated as the reference id based on the connector configuration + // Some connectros might not accept accept the global id. This has to be done when generating the reference id + connector_request_reference_id, + preprocessing_id: payment_data.payment_attempt.preprocessing_step_id, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + // TODO: take this based on the env + test_mode: Some(true), + payment_method_balance: None, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload, + connector_mandate_request_reference_id, + psd2_sca_exemption_type: None, + authentication_id: None, + }; + + Ok(router_data) +} + #[cfg(feature = "v2")] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] @@ -424,7 +590,7 @@ pub async fn construct_router_data_for_psync<'a>( .unwrap_or(attempt.id.get_string_repr().to_owned()); let request = types::PaymentsSyncData { - amount: attempt.amount_details.net_amount, + amount: attempt.amount_details.get_net_amount(), integrity_object: None, mandate_id: None, connector_transaction_id: match attempt.get_connector_payment_id() { @@ -521,7 +687,7 @@ pub async fn construct_router_data_for_psync<'a>( #[allow(clippy::too_many_arguments)] pub async fn construct_payment_router_data_for_sdk_session<'a>( _state: &'a SessionState, - payment_data: PaymentIntentData, + payment_data: hyperswitch_domain_models::payments::PaymentIntentData, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -915,6 +1081,57 @@ where ) -> RouterResponse; } +/// Generate a response from the given Data. This should be implemented on a payment data object +pub trait GenerateResponse +where + Self: Sized, +{ + #[cfg(feature = "v2")] + fn generate_response( + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse; +} + +#[cfg(feature = "v2")] +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentCaptureData +where + F: Clone, +{ + fn generate_response( + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse { + let payment_intent = &self.payment_intent; + let payment_attempt = &self.payment_attempt; + + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( + &payment_intent.amount_details, + &payment_attempt.amount_details, + )); + + let response = api_models::payments::PaymentsCaptureResponse { + id: payment_intent.id.clone(), + amount, + status: payment_intent.status, + }; + + Ok(services::ApplicationResponse::JsonWithHeaders(( + response, + vec![], + ))) + } +} + #[cfg(feature = "v1")] impl ToResponse for api::PaymentsResponse where @@ -1170,28 +1387,23 @@ where } #[cfg(feature = "v2")] -impl ToResponse for api_models::payments::PaymentsConfirmIntentResponse +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentConfirmData where F: Clone, - Op: Debug, - D: OperationSessionGetters, { - #[allow(clippy::too_many_arguments)] fn generate_response( - payment_data: D, - _customer: Option, - base_url: &str, - operation: Op, - _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, - _connector_http_status_code: Option, - _external_latency: Option, - _is_latency_header_enabled: Option, + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, merchant_account: &domain::MerchantAccount, - ) -> RouterResponse { - let payment_intent = payment_data.get_payment_intent(); - let payment_attempt = payment_data.get_payment_attempt(); + ) -> RouterResponse { + let payment_intent = self.payment_intent; + let payment_attempt = self.payment_attempt; - let amount = api_models::payments::ConfirmIntentAmountDetailsResponse::foreign_from(( + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( &payment_intent.amount_details, &payment_attempt.amount_details, )); @@ -1215,7 +1427,7 @@ where .clone() .map(api_models::payments::ErrorDetails::foreign_from); - let payment_address = payment_data.get_address(); + let payment_address = self.payment_address; let payment_method_data = Some(api_models::payments::PaymentMethodDataResponseWithBilling { @@ -1227,14 +1439,17 @@ where }); // TODO: Add support for other next actions, currently only supporting redirect to url - let redirect_to_url = payment_intent - .create_start_redirection_url(base_url, merchant_account.publishable_key.clone())?; + let redirect_to_url = payment_intent.create_start_redirection_url( + &state.base_url, + merchant_account.publishable_key.clone(), + )?; + let next_action = payment_attempt .redirection_data .as_ref() .map(|_| api_models::payments::NextActionData::RedirectToUrl { redirect_to_url }); - let response = Self { + let response = api_models::payments::PaymentsConfirmIntentResponse { id: payment_intent.id.clone(), status: payment_intent.status, amount, @@ -1261,70 +1476,73 @@ where } #[cfg(feature = "v2")] -impl ToResponse for api_models::payments::PaymentsRetrieveResponse +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentStatusData where F: Clone, - Op: Debug, - D: OperationSessionGetters, { - #[allow(clippy::too_many_arguments)] fn generate_response( - payment_data: D, - _customer: Option, - _base_url: &str, - operation: Op, - _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, - _connector_http_status_code: Option, - _external_latency: Option, - _is_latency_header_enabled: Option, - _merchant_account: &domain::MerchantAccount, - ) -> RouterResponse { - let payment_intent = payment_data.get_payment_intent(); - let payment_attempt = payment_data.get_optional_payment_attempt(); + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse { + let payment_intent = self.payment_intent; + let optional_payment_attempt = self.payment_attempt.as_ref(); - let amount = api_models::payments::ConfirmIntentAmountDetailsResponse::foreign_from(( + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( &payment_intent.amount_details, - payment_attempt.map(|payment_attempt| &payment_attempt.amount_details), + optional_payment_attempt.map(|payment_attempt| &payment_attempt.amount_details), )); let connector = - payment_attempt.and_then(|payment_attempt| payment_attempt.connector.clone()); + optional_payment_attempt.and_then(|payment_attempt| payment_attempt.connector.clone()); - let merchant_connector_id = payment_attempt + let merchant_connector_id = optional_payment_attempt .and_then(|payment_attempt| payment_attempt.merchant_connector_id.clone()); - let error = payment_attempt + let error = optional_payment_attempt .and_then(|payment_attempt| payment_attempt.error.clone()) .map(api_models::payments::ErrorDetails::foreign_from); - let payment_address = payment_data.get_address(); - let payment_method_data = Some(api_models::payments::PaymentMethodDataResponseWithBilling { payment_method_data: None, - billing: payment_address + billing: self + .payment_address .get_request_payment_method_billing() .cloned() .map(From::from), }); - let response = Self { + let response = api_models::payments::PaymentsRetrieveResponse { id: payment_intent.id.clone(), status: payment_intent.status, amount, customer_id: payment_intent.customer_id.clone(), connector, - billing: payment_address + billing: self + .payment_address .get_payment_billing() .cloned() .map(From::from), - shipping: payment_address.get_shipping().cloned().map(From::from), + shipping: self.payment_address.get_shipping().cloned().map(From::from), client_secret: payment_intent.client_secret.clone(), created: payment_intent.created_at, payment_method_data, - payment_method_type: payment_attempt.map(|attempt| attempt.payment_method_type), - payment_method_subtype: payment_attempt.map(|attempt| attempt.payment_method_subtype), - connector_transaction_id: payment_attempt + payment_method_type: self + .payment_attempt + .as_ref() + .map(|attempt| attempt.payment_method_type), + payment_method_subtype: self + .payment_attempt + .as_ref() + .map(|attempt| attempt.payment_method_subtype), + connector_transaction_id: self + .payment_attempt + .as_ref() .and_then(|attempt| attempt.connector_payment_id.clone()), connector_reference_id: None, merchant_connector_id, @@ -2838,7 +3056,44 @@ impl TryFrom> for types::PaymentsCaptureD type Error = error_stack::Report; fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { - todo!() + use masking::ExposeOptionInterface; + + let payment_data = additional_data.payment_data; + let connector = api::ConnectorData::get_connector_by_name( + &additional_data.state.conf.connectors, + &additional_data.connector_name, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + let amount_to_capture = payment_data + .payment_attempt + .amount_details + .get_amount_to_capture() + .unwrap_or(payment_data.payment_attempt.get_total_amount()); + + let amount = payment_data.payment_attempt.get_total_amount(); + Ok(Self { + capture_method: Some(payment_data.payment_intent.capture_method), + amount_to_capture: amount_to_capture.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_amount_to_capture: amount_to_capture, + currency: payment_data.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + payment_amount: amount.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_payment_amount: amount, + connector_meta: payment_data + .payment_attempt + .connector_metadata + .expose_option(), + // TODO: add multiple capture data + multiple_capture_data: None, + // TODO: why do we need browser info during capture? + browser_info: None, + metadata: payment_data.payment_intent.metadata.expose_option(), + integrity_object: None, + }) } } @@ -3631,7 +3886,7 @@ impl ForeignFrom<( &hyperswitch_domain_models::payments::AmountDetails, &hyperswitch_domain_models::payments::payment_attempt::AttemptAmountDetails, - )> for api_models::payments::ConfirmIntentAmountDetailsResponse + )> for api_models::payments::PaymentAmountDetailsResponse { fn foreign_from( (intent_amount_details, attempt_amount_details): ( @@ -3642,15 +3897,15 @@ impl Self { order_amount: intent_amount_details.order_amount, currency: intent_amount_details.currency, - shipping_cost: attempt_amount_details.shipping_cost, - order_tax_amount: attempt_amount_details.order_tax_amount, + shipping_cost: attempt_amount_details.get_shipping_cost(), + order_tax_amount: attempt_amount_details.get_order_tax_amount(), external_tax_calculation: intent_amount_details.skip_external_tax_calculation, surcharge_calculation: intent_amount_details.skip_surcharge_calculation, - surcharge_amount: attempt_amount_details.surcharge_amount, - tax_on_surcharge: attempt_amount_details.tax_on_surcharge, - net_amount: attempt_amount_details.net_amount, - amount_to_capture: attempt_amount_details.amount_to_capture, - amount_capturable: attempt_amount_details.amount_capturable, + surcharge_amount: attempt_amount_details.get_surcharge_amount(), + tax_on_surcharge: attempt_amount_details.get_tax_on_surcharge(), + net_amount: attempt_amount_details.get_net_amount(), + amount_to_capture: attempt_amount_details.get_amount_to_capture(), + amount_capturable: attempt_amount_details.get_amount_capturable(), amount_captured: intent_amount_details.amount_captured, } } @@ -3663,7 +3918,7 @@ impl ForeignFrom<( &hyperswitch_domain_models::payments::AmountDetails, Option<&hyperswitch_domain_models::payments::payment_attempt::AttemptAmountDetails>, - )> for api_models::payments::ConfirmIntentAmountDetailsResponse + )> for api_models::payments::PaymentAmountDetailsResponse { fn foreign_from( (intent_amount_details, attempt_amount_details): ( @@ -3675,10 +3930,10 @@ impl order_amount: intent_amount_details.order_amount, currency: intent_amount_details.currency, shipping_cost: attempt_amount_details - .and_then(|attempt_amount| attempt_amount.shipping_cost) + .and_then(|attempt_amount| attempt_amount.get_shipping_cost()) .or(intent_amount_details.shipping_cost), order_tax_amount: attempt_amount_details - .and_then(|attempt_amount| attempt_amount.order_tax_amount) + .and_then(|attempt_amount| attempt_amount.get_order_tax_amount()) .or(intent_amount_details .tax_details .as_ref() @@ -3686,17 +3941,18 @@ impl external_tax_calculation: intent_amount_details.skip_external_tax_calculation, surcharge_calculation: intent_amount_details.skip_surcharge_calculation, surcharge_amount: attempt_amount_details - .and_then(|attempt| attempt.surcharge_amount) + .and_then(|attempt| attempt.get_surcharge_amount()) .or(intent_amount_details.surcharge_amount), tax_on_surcharge: attempt_amount_details - .and_then(|attempt| attempt.tax_on_surcharge) + .and_then(|attempt| attempt.get_tax_on_surcharge()) .or(intent_amount_details.tax_on_surcharge), net_amount: attempt_amount_details - .map(|attempt| attempt.net_amount) + .map(|attempt| attempt.get_net_amount()) .unwrap_or(intent_amount_details.calculate_net_amount()), - amount_to_capture: attempt_amount_details.and_then(|attempt| attempt.amount_to_capture), + amount_to_capture: attempt_amount_details + .and_then(|attempt| attempt.get_amount_to_capture()), amount_capturable: attempt_amount_details - .map(|attempt| attempt.amount_capturable) + .map(|attempt| attempt.get_amount_capturable()) .unwrap_or(MinorUnit::zero()), amount_captured: intent_amount_details.amount_captured, } diff --git a/crates/router/src/core/webhooks/incoming_v2.rs b/crates/router/src/core/webhooks/incoming_v2.rs index 139ae18c1717..569cd330a079 100644 --- a/crates/router/src/core/webhooks/incoming_v2.rs +++ b/crates/router/src/core/webhooks/incoming_v2.rs @@ -20,7 +20,10 @@ use crate::{ api_locking, errors::{self, ConnectorErrorExt, CustomResult, RouterResponse, StorageErrorExt}, metrics, - payments::{self, transformers::ToResponse}, + payments::{ + self, + transformers::{GenerateResponse, ToResponse}, + }, webhooks::utils::construct_webhook_router_data, }, db::StorageInterface, @@ -433,12 +436,8 @@ async fn payments_incoming_webhook_flow( )) .await?; - let response = api_models::payments::PaymentsRetrieveResponse::generate_response( - payment_data, - customer, - &state.base_url, - payments::operations::PaymentGet, - &state.conf.connector_request_reference_id_config, + let response = payment_data.generate_response( + &state, connector_http_status_code, external_latency, None, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 766679491c54..852fd0fbd7eb 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -575,6 +575,9 @@ impl Payments { .service( web::resource("/finish-redirection/{publishable_key}/{profile_id}") .route(web::get().to(payments::payments_finish_redirection)), + ) + .service( + web::resource("/capture").route(web::post().to(payments::payments_capture)), ), ); diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 2893cb8fa044..a9005644bebb 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -2447,3 +2447,75 @@ pub async fn payments_finish_redirection( ) .await } + +#[cfg(feature = "v2")] +#[instrument(skip(state, req), fields(flow, payment_id))] +pub async fn payments_capture( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Json, + path: web::Path, +) -> impl Responder { + use hyperswitch_domain_models::payments::PaymentCaptureData; + let flow = Flow::PaymentsCapture; + + let global_payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", global_payment_id.get_string_repr()); + + let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId { + global_payment_id, + payload: payload.into_inner(), + }; + + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let locking_action = internal_payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + internal_payload, + |state, auth: auth::AuthenticationData, req, req_state| async { + let payment_id = req.global_payment_id; + let request = req.payload; + + let operation = payments::operations::payment_capture_v2::PaymentsCapture; + + Box::pin(payments::payments_core::< + api_types::Capture, + api_models::payments::PaymentsCaptureResponse, + _, + _, + _, + PaymentCaptureData, + >( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + operation, + request, + payment_id, + payments::CallConnectorAction::Trigger, + header_payload.clone(), + )) + .await + }, + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::ProfileAccountWrite, + }, + req.headers(), + ), + locking_action, + )) + .await +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index de4c063c41aa..5ee7094700c8 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -470,7 +470,7 @@ impl Capturable for PaymentsSyncData { payment_data .payment_attempt .amount_details - .amount_to_capture + .get_amount_to_capture() .or_else(|| Some(payment_data.payment_attempt.get_total_amount())) .map(|amt| amt.get_amount_as_i64()) } From 0f8b0b3bc854be62942a77d08340510312157c67 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Sun, 22 Dec 2024 22:41:28 +0530 Subject: [PATCH 05/11] feat(router): add db interface for `/relay` (#6879) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference/openapi_spec.json | 33 ++- crates/api_models/src/relay.rs | 53 ++-- crates/common_enums/src/enums.rs | 67 +++++ crates/common_utils/src/id_type/relay.rs | 6 + crates/diesel_models/src/enums.rs | 3 +- crates/diesel_models/src/lib.rs | 1 + crates/diesel_models/src/query.rs | 1 + crates/diesel_models/src/query/relay.rs | 38 +++ crates/diesel_models/src/relay.rs | 77 +++++ crates/diesel_models/src/schema.rs | 30 ++ crates/diesel_models/src/schema_v2.rs | 30 ++ crates/hyperswitch_domain_models/src/lib.rs | 1 + .../src/merchant_connector_account.rs | 26 +- crates/hyperswitch_domain_models/src/relay.rs | 275 ++++++++++++++++++ crates/openapi/src/openapi.rs | 6 +- crates/router/src/core.rs | 2 + crates/router/src/core/relay.rs | 177 +++++++++++ crates/router/src/core/relay/utils.rs | 139 +++++++++ crates/router/src/db.rs | 2 + crates/router/src/db/relay.rs | 135 +++++++++ crates/router/src/lib.rs | 3 +- crates/router/src/routes.rs | 4 +- crates/router/src/routes/app.rs | 13 +- crates/router/src/routes/lock_utils.rs | 2 + crates/router/src/routes/relay.rs | 40 +++ crates/router/src/services/authentication.rs | 15 +- crates/router_env/src/logger/types.rs | 2 + .../down.sql | 7 + .../2024-12-17-141811_add_relay_table/up.sql | 22 ++ 29 files changed, 1152 insertions(+), 58 deletions(-) create mode 100644 crates/diesel_models/src/query/relay.rs create mode 100644 crates/diesel_models/src/relay.rs create mode 100644 crates/hyperswitch_domain_models/src/relay.rs create mode 100644 crates/router/src/core/relay.rs create mode 100644 crates/router/src/core/relay/utils.rs create mode 100644 crates/router/src/db/relay.rs create mode 100644 crates/router/src/routes/relay.rs create mode 100644 migrations/2024-12-17-141811_add_relay_table/down.sql create mode 100644 migrations/2024-12-17-141811_add_relay_table/up.sql diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 0d750ea7bf53..2f34aa9ec614 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -23213,7 +23213,15 @@ "RelayData": { "oneOf": [ { - "$ref": "#/components/schemas/RelayRefundRequest" + "type": "object", + "required": [ + "refund" + ], + "properties": { + "refund": { + "$ref": "#/components/schemas/RelayRefundRequest" + } + } } ] }, @@ -23269,12 +23277,12 @@ "properties": { "connector_resource_id": { "type": "string", - "description": "The identifier that is associated to a resource at the connector to which the relay request is being made", + "description": "The identifier that is associated to a resource at the connector reference to which the relay request is being made", "example": "7256228702616471803954" }, "connector_id": { "type": "string", - "description": "Identifier of the connector ( merchant connector account ) to which relay request is being made", + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", "example": "mca_5apGeP94tMts6rg3U3kR" }, "type": { @@ -23309,11 +23317,10 @@ "status": { "$ref": "#/components/schemas/RelayStatus" }, - "connector_reference_id": { + "connector_resource_id": { "type": "string", - "description": "The reference identifier provided by the connector for the relay request", - "example": "pi_3MKEivSFNglxLpam0ZaL98q9", - "nullable": true + "description": "The identifier that is associated to a resource at the connector reference to which the relay request is being made", + "example": "pi_3MKEivSFNglxLpam0ZaL98q9" }, "error": { "allOf": [ @@ -23323,19 +23330,20 @@ ], "nullable": true }, - "connector_resource_id": { + "connector_reference_id": { "type": "string", "description": "The identifier that is associated to a resource at the connector to which the relay request is being made", - "example": "7256228702616471803954" + "example": "re_3QY4TnEOqOywnAIx1Mm1p7GQ", + "nullable": true }, "connector_id": { "type": "string", - "description": "Identifier of the connector ( merchant connector account ) to which relay request is being made", + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", "example": "mca_5apGeP94tMts6rg3U3kR" }, "profile_id": { "type": "string", - "description": "The business profile that is associated with this relay request", + "description": "The business profile that is associated with this relay request.", "example": "pro_abcdefghijklmnopqrstuvwxyz" }, "type": { @@ -23354,8 +23362,9 @@ "RelayStatus": { "type": "string", "enum": [ + "created", + "pending", "success", - "processing", "failure" ] }, diff --git a/crates/api_models/src/relay.rs b/crates/api_models/src/relay.rs index 7e094f283607..e4bc607fc0da 100644 --- a/crates/api_models/src/relay.rs +++ b/crates/api_models/src/relay.rs @@ -1,33 +1,27 @@ -pub use common_utils::types::MinorUnit; +use common_utils::types::MinorUnit; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::enums; +use crate::enums as api_enums; #[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] pub struct RelayRequest { - /// The identifier that is associated to a resource at the connector to which the relay request is being made + /// The identifier that is associated to a resource at the connector reference to which the relay request is being made #[schema(example = "7256228702616471803954")] pub connector_resource_id: String, - /// Identifier of the connector ( merchant connector account ) to which relay request is being made + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment #[schema(example = "mca_5apGeP94tMts6rg3U3kR", value_type = String)] pub connector_id: common_utils::id_type::MerchantConnectorAccountId, /// The type of relay request #[serde(rename = "type")] - pub relay_type: RelayType, + #[schema(value_type = RelayType)] + pub relay_type: api_enums::RelayType, /// The data that is associated with the relay request pub data: Option, } #[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] -pub enum RelayType { - /// The relay request is for a refund - Refund, -} - -#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] -#[serde(rename_all = "snake_case", untagged)] pub enum RelayData { /// The data that is associated with a refund relay request Refund(RelayRefundRequest), @@ -40,7 +34,7 @@ pub struct RelayRefundRequest { pub amount: MinorUnit, /// The currency in which the amount is being refunded #[schema(value_type = Currency)] - pub currency: enums::Currency, + pub currency: api_enums::Currency, /// The reason for the refund #[schema(max_length = 255, example = "Customer returned the product")] pub reason: Option, @@ -52,39 +46,30 @@ pub struct RelayResponse { #[schema(example = "relay_mbabizu24mvu3mela5njyhpit4", value_type = String)] pub id: common_utils::id_type::RelayId, /// The status of the relay request - pub status: RelayStatus, - /// The reference identifier provided by the connector for the relay request + #[schema(value_type = RelayStatus)] + pub status: api_enums::RelayStatus, + /// The identifier that is associated to a resource at the connector reference to which the relay request is being made #[schema(example = "pi_3MKEivSFNglxLpam0ZaL98q9")] - pub connector_reference_id: Option, + pub connector_resource_id: String, /// The error details if the relay request failed pub error: Option, /// The identifier that is associated to a resource at the connector to which the relay request is being made - #[schema(example = "7256228702616471803954")] - pub connector_resource_id: String, - /// Identifier of the connector ( merchant connector account ) to which relay request is being made + #[schema(example = "re_3QY4TnEOqOywnAIx1Mm1p7GQ")] + pub connector_reference_id: Option, + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment #[schema(example = "mca_5apGeP94tMts6rg3U3kR", value_type = String)] pub connector_id: common_utils::id_type::MerchantConnectorAccountId, - /// The business profile that is associated with this relay request + /// The business profile that is associated with this relay request. #[schema(example = "pro_abcdefghijklmnopqrstuvwxyz", value_type = String)] pub profile_id: common_utils::id_type::ProfileId, /// The type of relay request #[serde(rename = "type")] - pub relay_type: RelayType, + #[schema(value_type = RelayType)] + pub relay_type: api_enums::RelayType, /// The data that is associated with the relay request pub data: Option, } -#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum RelayStatus { - /// The relay request is successful - Success, - /// The relay request is being processed - Processing, - /// The relay request has failed - Failure, -} - #[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] pub struct RelayError { /// The error code @@ -101,3 +86,7 @@ pub struct RelayRetrieveRequest { /// The unique identifier for the Relay pub id: String, } + +impl common_utils::events::ApiEventMetric for RelayRequest {} + +impl common_utils::events::ApiEventMetric for RelayResponse {} diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 5aac1a07457b..193998c3fd1c 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1728,6 +1728,53 @@ pub enum RefundStatus { TransactionFailure, } +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + Hash, + PartialEq, + strum::Display, + strum::EnumString, + strum::EnumIter, + serde::Serialize, + serde::Deserialize, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum RelayStatus { + Created, + #[default] + Pending, + Success, + Failure, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + strum::Display, + strum::EnumString, + strum::EnumIter, + serde::Serialize, + serde::Deserialize, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum RelayType { + Refund, +} + #[derive( Clone, Copy, @@ -3372,6 +3419,26 @@ impl From for TransactionType { } } +impl From for RelayStatus { + fn from(refund_status: RefundStatus) -> Self { + match refund_status { + RefundStatus::Failure | RefundStatus::TransactionFailure => Self::Failure, + RefundStatus::ManualReview | RefundStatus::Pending => Self::Pending, + RefundStatus::Success => Self::Success, + } + } +} + +impl From for RefundStatus { + fn from(relay_status: RelayStatus) -> Self { + match relay_status { + RelayStatus::Failure => Self::Failure, + RelayStatus::Pending | RelayStatus::Created => Self::Pending, + RelayStatus::Success => Self::Success, + } + } +} + #[derive( Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize, Default, ToSchema, )] diff --git a/crates/common_utils/src/id_type/relay.rs b/crates/common_utils/src/id_type/relay.rs index 5db48c492f9c..3ad64729fb73 100644 --- a/crates/common_utils/src/id_type/relay.rs +++ b/crates/common_utils/src/id_type/relay.rs @@ -4,4 +4,10 @@ crate::id_type!( ); crate::impl_id_type_methods!(RelayId, "relay_id"); +crate::impl_try_from_cow_str_id_type!(RelayId, "relay_id"); +crate::impl_generate_id_id_type!(RelayId, "relay"); +crate::impl_serializable_secret_id_type!(RelayId); +crate::impl_queryable_id_type!(RelayId); +crate::impl_to_sql_from_sql_id_type!(RelayId); + crate::impl_debug_id_type!(RelayId); diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 19f3bf0a3823..ec6e91a2ecb0 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -17,7 +17,8 @@ pub mod diesel_exports { DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentSource as PaymentSource, DbPaymentType as PaymentType, DbPayoutStatus as PayoutStatus, DbPayoutType as PayoutType, DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, - DbRefundStatus as RefundStatus, DbRefundType as RefundType, + DbRefundStatus as RefundStatus, DbRefundType as RefundType, DbRelayStatus as RelayStatus, + DbRelayType as RelayType, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRoleScope as RoleScope, DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbScaExemptionType as ScaExemptionType, diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index cc3dc1361545..1369368a8099 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -39,6 +39,7 @@ pub mod payouts; pub mod process_tracker; pub mod query; pub mod refund; +pub mod relay; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index ab044b5c6e69..8eb0a44f5dd7 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -34,6 +34,7 @@ pub mod payout_attempt; pub mod payouts; pub mod process_tracker; pub mod refund; +pub mod relay; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; diff --git a/crates/diesel_models/src/query/relay.rs b/crates/diesel_models/src/query/relay.rs new file mode 100644 index 000000000000..28ede3cd61a8 --- /dev/null +++ b/crates/diesel_models/src/query/relay.rs @@ -0,0 +1,38 @@ +use diesel::{associations::HasTable, ExpressionMethods}; + +use super::generics; +use crate::{ + errors, + relay::{Relay, RelayNew, RelayUpdateInternal}, + schema::relay::dsl, + PgPooledConn, StorageResult, +}; + +impl RelayNew { + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Relay { + pub async fn update( + self, + conn: &PgPooledConn, + relay: RelayUpdateInternal, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >(conn, dsl::id.eq(self.id.to_owned()), relay) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NoFieldsToUpdate => Ok(self), + _ => Err(error), + }, + result => result, + } + } +} diff --git a/crates/diesel_models/src/relay.rs b/crates/diesel_models/src/relay.rs new file mode 100644 index 000000000000..24a6ab8b034a --- /dev/null +++ b/crates/diesel_models/src/relay.rs @@ -0,0 +1,77 @@ +use common_utils::pii; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; +use time::PrimitiveDateTime; + +use crate::{enums as storage_enums, schema::relay}; + +#[derive( + Clone, + Debug, + Eq, + Identifiable, + Queryable, + Selectable, + PartialEq, + serde::Serialize, + serde::Deserialize, +)] +#[diesel(table_name = relay)] +pub struct Relay { + pub id: common_utils::id_type::RelayId, + pub connector_resource_id: String, + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + pub profile_id: common_utils::id_type::ProfileId, + pub merchant_id: common_utils::id_type::MerchantId, + pub relay_type: storage_enums::RelayType, + pub request_data: Option, + pub status: storage_enums::RelayStatus, + pub connector_reference_id: Option, + pub error_code: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub response_data: Option, +} + +#[derive( + Clone, + Debug, + Eq, + PartialEq, + Insertable, + router_derive::DebugAsDisplay, + serde::Serialize, + serde::Deserialize, + router_derive::Setter, +)] +#[diesel(table_name = relay)] +pub struct RelayNew { + pub id: common_utils::id_type::RelayId, + pub connector_resource_id: String, + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + pub profile_id: common_utils::id_type::ProfileId, + pub merchant_id: common_utils::id_type::MerchantId, + pub relay_type: storage_enums::RelayType, + pub request_data: Option, + pub status: storage_enums::RelayStatus, + pub connector_reference_id: Option, + pub error_code: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub response_data: Option, +} + +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[table_name = "relay"] +pub struct RelayUpdateInternal { + pub connector_reference_id: Option, + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub modified_at: PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 61a8a7e19b9e..366c917d2d11 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1235,6 +1235,35 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + relay (id) { + #[max_length = 64] + id -> Varchar, + #[max_length = 128] + connector_resource_id -> Varchar, + #[max_length = 64] + connector_id -> Varchar, + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + relay_type -> RelayType, + request_data -> Nullable, + status -> RelayStatus, + #[max_length = 128] + connector_reference_id -> Nullable, + #[max_length = 64] + error_code -> Nullable, + error_message -> Nullable, + created_at -> Timestamp, + modified_at -> Timestamp, + response_data -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -1479,6 +1508,7 @@ diesel::allow_tables_to_appear_in_same_query!( payouts, process_tracker, refund, + relay, reverse_lookup, roles, routing_algorithm, diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 6569c0353654..fcfe05e5731c 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1181,6 +1181,35 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + relay (id) { + #[max_length = 64] + id -> Varchar, + #[max_length = 128] + connector_resource_id -> Varchar, + #[max_length = 64] + connector_id -> Varchar, + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + relay_type -> RelayType, + request_data -> Nullable, + status -> RelayStatus, + #[max_length = 128] + connector_reference_id -> Nullable, + #[max_length = 64] + error_code -> Nullable, + error_message -> Nullable, + created_at -> Timestamp, + modified_at -> Timestamp, + response_data -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -1427,6 +1456,7 @@ diesel::allow_tables_to_appear_in_same_query!( payouts, process_tracker, refund, + relay, reverse_lookup, roles, routing_algorithm, diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index fe152e876ccf..7a170f918efc 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -17,6 +17,7 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +pub mod relay; pub mod router_data; pub mod router_data_v2; pub mod router_flow_types; diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index 51c75113dcda..baec0e59f8a4 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -1,12 +1,11 @@ #[cfg(feature = "v2")] use api_models::admin; -#[cfg(feature = "v2")] -use common_utils::ext_traits::ValueExt; use common_utils::{ crypto::Encryptable, date_time, encryption::Encryption, errors::{CustomResult, ValidationError}, + ext_traits::ValueExt, id_type, pii, type_name, types::keymanager::{Identifier, KeyManagerState, ToEncryptable}, }; @@ -21,9 +20,10 @@ use serde_json::Value; use super::behaviour; #[cfg(feature = "v2")] use crate::errors::api_error_response::ApiErrorResponse; -#[cfg(feature = "v2")] -use crate::router_data; -use crate::type_encryption::{crypto_operation, CryptoOperation}; +use crate::{ + router_data, + type_encryption::{crypto_operation, CryptoOperation}, +}; #[cfg(feature = "v1")] #[derive(Clone, Debug, router_derive::ToEncryption)] @@ -62,6 +62,19 @@ impl MerchantConnectorAccount { pub fn get_id(&self) -> id_type::MerchantConnectorAccountId { self.merchant_connector_id.clone() } + + pub fn get_connector_account_details( + &self, + ) -> error_stack::Result + { + self.connector_account_details + .get_inner() + .clone() + .parse_value("ConnectorAuthType") + } + pub fn get_connector_test_mode(&self) -> Option { + self.test_mode + } } #[cfg(feature = "v2")] @@ -134,6 +147,9 @@ impl MerchantConnectorAccount { .clone() .parse_value("ConnectorAuthType") } + pub fn get_connector_test_mode(&self) -> Option { + todo!() + } } #[cfg(feature = "v1")] diff --git a/crates/hyperswitch_domain_models/src/relay.rs b/crates/hyperswitch_domain_models/src/relay.rs new file mode 100644 index 000000000000..959ac8e7f612 --- /dev/null +++ b/crates/hyperswitch_domain_models/src/relay.rs @@ -0,0 +1,275 @@ +use common_enums::enums; +use common_utils::{ + self, + errors::{CustomResult, ValidationError}, + id_type::{self, GenerateId}, + pii, + types::{keymanager, MinorUnit}, +}; +use diesel_models::relay::RelayUpdateInternal; +use error_stack::ResultExt; +use masking::{ExposeInterface, Secret}; +use serde::{self, Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{router_data::ErrorResponse, router_response_types}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Relay { + pub id: id_type::RelayId, + pub connector_resource_id: String, + pub connector_id: id_type::MerchantConnectorAccountId, + pub profile_id: id_type::ProfileId, + pub merchant_id: id_type::MerchantId, + pub relay_type: enums::RelayType, + pub request_data: Option, + pub status: enums::RelayStatus, + pub connector_reference_id: Option, + pub error_code: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub response_data: Option, +} + +impl Relay { + pub fn new( + relay_request: &api_models::relay::RelayRequest, + merchant_id: &id_type::MerchantId, + profile_id: &id_type::ProfileId, + ) -> Self { + let relay_id = id_type::RelayId::generate(); + Self { + id: relay_id.clone(), + connector_resource_id: relay_request.connector_resource_id.clone(), + connector_id: relay_request.connector_id.clone(), + profile_id: profile_id.clone(), + merchant_id: merchant_id.clone(), + relay_type: common_enums::RelayType::Refund, + request_data: relay_request.data.clone().map(From::from), + status: common_enums::RelayStatus::Created, + connector_reference_id: None, + error_code: None, + error_message: None, + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + response_data: None, + } + } +} + +impl From for RelayData { + fn from(relay: api_models::relay::RelayData) -> Self { + match relay { + api_models::relay::RelayData::Refund(relay_refund_request) => { + Self::Refund(RelayRefundData { + amount: relay_refund_request.amount, + currency: relay_refund_request.currency, + reason: relay_refund_request.reason, + }) + } + } + } +} + +impl RelayUpdate { + pub fn from( + response: Result, + ) -> Self { + match response { + Err(error) => Self::ErrorUpdate { + error_code: error.code, + error_message: error.message, + status: common_enums::RelayStatus::Failure, + }, + Ok(response) => Self::StatusUpdate { + connector_reference_id: Some(response.connector_refund_id), + status: common_enums::RelayStatus::from(response.refund_status), + }, + } + } +} + +impl From for api_models::relay::RelayResponse { + fn from(value: Relay) -> Self { + let error = value + .error_code + .zip(value.error_message) + .map( + |(error_code, error_message)| api_models::relay::RelayError { + code: error_code, + message: error_message, + }, + ); + + let data = value.request_data.map(|relay_data| match relay_data { + RelayData::Refund(relay_refund_request) => { + api_models::relay::RelayData::Refund(api_models::relay::RelayRefundRequest { + amount: relay_refund_request.amount, + currency: relay_refund_request.currency, + reason: relay_refund_request.reason, + }) + } + }); + Self { + id: value.id, + status: value.status, + error, + connector_resource_id: value.connector_resource_id, + connector_id: value.connector_id, + profile_id: value.profile_id, + relay_type: value.relay_type, + data, + connector_reference_id: value.connector_reference_id, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case", untagged)] +pub enum RelayData { + Refund(RelayRefundData), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RelayRefundData { + pub amount: MinorUnit, + pub currency: enums::Currency, + pub reason: Option, +} + +#[derive(Debug)] +pub enum RelayUpdate { + ErrorUpdate { + error_code: String, + error_message: String, + status: enums::RelayStatus, + }, + StatusUpdate { + connector_reference_id: Option, + status: common_enums::RelayStatus, + }, +} + +impl From for RelayUpdateInternal { + fn from(value: RelayUpdate) -> Self { + match value { + RelayUpdate::ErrorUpdate { + error_code, + error_message, + status, + } => Self { + error_code: Some(error_code), + error_message: Some(error_message), + connector_reference_id: None, + status: Some(status), + modified_at: common_utils::date_time::now(), + }, + RelayUpdate::StatusUpdate { + connector_reference_id, + status, + } => Self { + connector_reference_id, + status: Some(status), + error_code: None, + error_message: None, + modified_at: common_utils::date_time::now(), + }, + } + } +} + +#[async_trait::async_trait] +impl super::behaviour::Conversion for Relay { + type DstType = diesel_models::relay::Relay; + type NewDstType = diesel_models::relay::RelayNew; + + async fn convert(self) -> CustomResult { + Ok(diesel_models::relay::Relay { + id: self.id, + connector_resource_id: self.connector_resource_id, + connector_id: self.connector_id, + profile_id: self.profile_id, + merchant_id: self.merchant_id, + relay_type: self.relay_type, + request_data: self + .request_data + .map(|data| { + serde_json::to_value(data).change_context(ValidationError::InvalidValue { + message: "Failed while decrypting business profile data".to_string(), + }) + }) + .transpose()? + .map(Secret::new), + status: self.status, + connector_reference_id: self.connector_reference_id, + error_code: self.error_code, + error_message: self.error_message, + created_at: self.created_at, + modified_at: self.modified_at, + response_data: self.response_data, + }) + } + + async fn convert_back( + _state: &keymanager::KeyManagerState, + item: Self::DstType, + _key: &Secret>, + _key_manager_identifier: keymanager::Identifier, + ) -> CustomResult { + Ok(Self { + id: item.id, + connector_resource_id: item.connector_resource_id, + connector_id: item.connector_id, + profile_id: item.profile_id, + merchant_id: item.merchant_id, + relay_type: enums::RelayType::Refund, + request_data: item + .request_data + .map(|data| { + serde_json::from_value(data.expose()).change_context( + ValidationError::InvalidValue { + message: "Failed while decrypting business profile data".to_string(), + }, + ) + }) + .transpose()?, + status: item.status, + connector_reference_id: item.connector_reference_id, + error_code: item.error_code, + error_message: item.error_message, + created_at: item.created_at, + modified_at: item.modified_at, + response_data: item.response_data, + }) + } + + async fn construct_new(self) -> CustomResult { + Ok(diesel_models::relay::RelayNew { + id: self.id, + connector_resource_id: self.connector_resource_id, + connector_id: self.connector_id, + profile_id: self.profile_id, + merchant_id: self.merchant_id, + relay_type: self.relay_type, + request_data: self + .request_data + .map(|data| { + serde_json::to_value(data).change_context(ValidationError::InvalidValue { + message: "Failed while decrypting business profile data".to_string(), + }) + }) + .transpose()? + .map(Secret::new), + status: self.status, + connector_reference_id: self.connector_reference_id, + error_code: self.error_code, + error_message: self.error_message, + created_at: self.created_at, + modified_at: self.modified_at, + response_data: self.response_data, + }) + } +} diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 7b093ffd9612..daf43fded07e 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -525,11 +525,11 @@ Never share your secret api keys. Keep them guarded and secure. api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::relay::RelayRequest, - api_models::relay::RelayType, + api_models::relay::RelayResponse, + api_models::enums::RelayType, api_models::relay::RelayData, api_models::relay::RelayRefundRequest, - api_models::relay::RelayResponse, - api_models::relay::RelayStatus, + api_models::enums::RelayStatus, api_models::relay::RelayError, api_models::payments::AmountFilter, api_models::mandates::MandateRevokedResponse, diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index f6fe1d85b5de..c22cecc20f9f 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -54,3 +54,5 @@ pub mod verify_connector; pub mod webhooks; pub mod unified_authentication_service; + +pub mod relay; diff --git a/crates/router/src/core/relay.rs b/crates/router/src/core/relay.rs new file mode 100644 index 000000000000..812709269d83 --- /dev/null +++ b/crates/router/src/core/relay.rs @@ -0,0 +1,177 @@ +use api_models::relay as relay_models; +use common_utils::{self, ext_traits::OptionExt, id_type}; +use error_stack::ResultExt; + +use super::errors::{self, ConnectorErrorExt, RouterResponse, RouterResult, StorageErrorExt}; +use crate::{ + core::payments, + routes::SessionState, + services, + types::{ + api::{self}, + domain, + }, +}; + +pub mod utils; + +pub async fn relay( + state: SessionState, + merchant_account: domain::MerchantAccount, + profile_id_optional: Option, + key_store: domain::MerchantKeyStore, + req: relay_models::RelayRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + let merchant_id = merchant_account.get_id(); + let connector_id = &req.connector_id; + + let profile_id_from_auth_layer = profile_id_optional + .get_required_value("ProfileId") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "profile id", + })?; + + let profile = db + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + merchant_id, + &profile_id_from_auth_layer, + ) + .await + .change_context(errors::ApiErrorResponse::ProfileNotFound { + id: profile_id_from_auth_layer.get_string_repr().to_owned(), + })?; + + #[cfg(feature = "v1")] + let connector_account = db + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + key_manager_state, + merchant_id, + connector_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: connector_id.get_string_repr().to_string(), + })?; + + #[cfg(feature = "v2")] + let connector_account = db + .find_merchant_connector_account_by_id(key_manager_state, connector_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: connector_id.get_string_repr().to_string(), + })?; + + validate_relay_refund_request(&req).attach_printable("Invalid relay refund request")?; + + let relay_domain = + hyperswitch_domain_models::relay::Relay::new(&req, merchant_id, profile.get_id()); + + let relay_record = db + .insert_relay(key_manager_state, &key_store, relay_domain) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to insert a relay record in db")?; + + let relay_response = match req.relay_type { + common_enums::RelayType::Refund => { + Box::pin(relay_refund( + &state, + merchant_account, + connector_account, + &relay_record, + )) + .await? + } + }; + + let relay_update_record = db + .update_relay(key_manager_state, &key_store, relay_record, relay_response) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let response = relay_models::RelayResponse::from(relay_update_record); + + Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( + response, + )) +} + +pub async fn relay_refund( + state: &SessionState, + merchant_account: domain::MerchantAccount, + connector_account: domain::MerchantConnectorAccount, + relay_record: &hyperswitch_domain_models::relay::Relay, +) -> RouterResult { + let connector_id = &relay_record.connector_id; + + let merchant_id = merchant_account.get_id(); + + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &connector_account.connector_name, + api::GetToken::Connector, + Some(connector_id.clone()), + )?; + + let connector_integration: services::BoxedRefundConnectorIntegrationInterface< + api::Execute, + hyperswitch_domain_models::router_request_types::RefundsData, + hyperswitch_domain_models::router_response_types::RefundsResponseData, + > = connector_data.connector.get_connector_integration(); + + let router_data = utils::construct_relay_refund_router_data( + state, + &connector_account.connector_name, + merchant_id, + &connector_account, + relay_record, + ) + .await?; + + let router_data_res = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_refund_failed_response()?; + + let relay_response = + hyperswitch_domain_models::relay::RelayUpdate::from(router_data_res.response); + + Ok(relay_response) +} + +// validate relay request +pub fn validate_relay_refund_request( + relay_request: &relay_models::RelayRequest, +) -> RouterResult<()> { + match (relay_request.relay_type, &relay_request.data) { + (common_enums::RelayType::Refund, Some(relay_models::RelayData::Refund(ref_data))) => { + validate_relay_refund_data(ref_data) + } + (common_enums::RelayType::Refund, None) => { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Relay data is required for refund relay".to_string(), + })? + } + } +} + +pub fn validate_relay_refund_data( + refund_data: &relay_models::RelayRefundRequest, +) -> RouterResult<()> { + if refund_data.amount.get_amount_as_i64() <= 0 { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Amount should be greater than 0".to_string(), + })? + } + Ok(()) +} diff --git a/crates/router/src/core/relay/utils.rs b/crates/router/src/core/relay/utils.rs new file mode 100644 index 000000000000..0753be1ec435 --- /dev/null +++ b/crates/router/src/core/relay/utils.rs @@ -0,0 +1,139 @@ +use std::str::FromStr; + +use common_utils::{ext_traits::OptionExt, id_type}; +use error_stack::ResultExt; +use hyperswitch_domain_models::{router_data::ErrorResponse, types}; + +use crate::{ + core::payments, + db::{ + domain, + errors::{self, RouterResult}, + }, + routes::SessionState, +}; + +const IRRELEVANT_PAYMENT_INTENT_ID: &str = "irrelevant_payment_intent_id"; + +const IRRELEVANT_PAYMENT_ATTEMPT_ID: &str = "irrelevant_payment_attempt_id"; + +pub async fn construct_relay_refund_router_data<'a, F>( + state: &'a SessionState, + connector_name: &str, + merchant_id: &id_type::MerchantId, + connector_account: &domain::MerchantConnectorAccount, + relay_record: &hyperswitch_domain_models::relay::Relay, +) -> RouterResult> { + let connector_auth_type = connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + let webhook_url = Some(payments::helpers::create_webhook_url( + &state.base_url.clone(), + merchant_id, + connector_name, + )); + + let supported_connector = &state + .conf + .multiple_api_version_supported_connectors + .supported_connectors; + + let connector_enum = api_models::enums::Connector::from_str(connector_name) + .change_context(errors::ConnectorError::InvalidConnectorName) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| format!("unable to parse connector name {connector_name:?}"))?; + + let connector_api_version = if supported_connector.contains(&connector_enum) { + state + .store + .find_config_by_key(&format!("connector_api_version_{connector_name}")) + .await + .map(|value| value.config) + .ok() + } else { + None + }; + + let hyperswitch_domain_models::relay::RelayData::Refund(relay_refund_data) = relay_record + .request_data + .clone() + .get_required_value("refund relay data") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to obtain relay data to construct relay refund data")?; + + let relay_id_string = relay_record.id.get_string_repr().to_string(); + + let router_data = hyperswitch_domain_models::router_data::RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_id.clone(), + customer_id: None, + connector: connector_name.to_string(), + payment_id: IRRELEVANT_PAYMENT_INTENT_ID.to_string(), + attempt_id: IRRELEVANT_PAYMENT_ATTEMPT_ID.to_string(), + status: common_enums::AttemptStatus::Charged, + payment_method: common_enums::PaymentMethod::default(), + connector_auth_type, + description: None, + return_url: None, + address: hyperswitch_domain_models::payment_address::PaymentAddress::default(), + auth_type: common_enums::AuthenticationType::default(), + connector_meta_data: None, + connector_wallets_details: None, + amount_captured: None, + payment_method_status: None, + minor_amount_captured: None, + request: hyperswitch_domain_models::router_request_types::RefundsData { + refund_id: relay_id_string.clone(), + connector_transaction_id: relay_record.connector_resource_id.clone(), + refund_amount: relay_refund_data.amount.get_amount_as_i64(), + minor_refund_amount: relay_refund_data.amount, + currency: relay_refund_data.currency, + payment_amount: relay_refund_data.amount.get_amount_as_i64(), + minor_payment_amount: relay_refund_data.amount, + webhook_url, + connector_metadata: None, + reason: relay_refund_data.reason, + connector_refund_id: relay_record.connector_reference_id.clone(), + browser_info: None, + split_refunds: None, + integrity_object: None, + refund_status: common_enums::RefundStatus::from(relay_record.status), + }, + + response: Err(ErrorResponse::default()), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + preprocessing_id: None, + connector_request_reference_id: relay_id_string.clone(), + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + test_mode: connector_account.get_connector_test_mode(), + payment_method_balance: None, + connector_api_version, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: Some(relay_id_string), + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload: None, + connector_mandate_request_reference_id: None, + authentication_id: None, + psd2_sca_exemption_type: None, + }; + + Ok(router_data) +} diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index fbb75b6af603..eef84f6ff983 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -30,6 +30,7 @@ pub mod organization; pub mod payment_link; pub mod payment_method; pub mod refund; +pub mod relay; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; @@ -131,6 +132,7 @@ pub trait StorageInterface: + user_authentication_method::UserAuthenticationMethodInterface + authentication::AuthenticationInterface + generic_link::GenericLinkInterface + + relay::RelayInterface + user::theme::ThemeInterface + 'static { diff --git a/crates/router/src/db/relay.rs b/crates/router/src/db/relay.rs new file mode 100644 index 000000000000..e869165aa339 --- /dev/null +++ b/crates/router/src/db/relay.rs @@ -0,0 +1,135 @@ +use common_utils::types::keymanager::KeyManagerState; +use diesel_models; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::behaviour::{Conversion, ReverseConversion}; +use storage_impl::MockDb; + +use super::domain; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + services::Store, +}; + +#[async_trait::async_trait] +pub trait RelayInterface { + async fn insert_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult; + + async fn update_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_state: hyperswitch_domain_models::relay::Relay, + relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl RelayInterface for Store { + async fn insert_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + new.construct_new() + .await + .change_context(errors::StorageError::EncryptionError)? + .insert(&conn) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } + + async fn update_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_state: hyperswitch_domain_models::relay::Relay, + relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + Conversion::convert(current_state) + .await + .change_context(errors::StorageError::EncryptionError)? + .update( + &conn, + diesel_models::relay::RelayUpdateInternal::from(relay_update), + ) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } +} + +#[async_trait::async_trait] +impl RelayInterface for MockDb { + async fn insert_relay( + &self, + _key_manager_state: &KeyManagerState, + _merchant_key_store: &domain::MerchantKeyStore, + _new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn update_relay( + &self, + _key_manager_state: &KeyManagerState, + _merchant_key_store: &domain::MerchantKeyStore, + _current_state: hyperswitch_domain_models::relay::Relay, + _relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl RelayInterface for KafkaStore { + async fn insert_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult { + self.diesel_store + .insert_relay(key_manager_state, merchant_key_store, new) + .await + } + + async fn update_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_state: hyperswitch_domain_models::relay::Relay, + relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult { + self.diesel_store + .update_relay( + key_manager_state, + merchant_key_store, + current_state, + relay_update, + ) + .await + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index d43fa0543138..9100ea3ebaa5 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -140,7 +140,8 @@ pub fn mk_app( .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) - .service(routes::Webhooks::server(state.clone())); + .service(routes::Webhooks::server(state.clone())) + .service(routes::Relay::server(state.clone())); #[cfg(feature = "oltp")] { diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index aaadcaa4eb51..53e5d5aac71d 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -58,6 +58,8 @@ pub mod verify_connector; pub mod webhook_events; pub mod webhooks; +pub mod relay; + #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(all(feature = "olap", feature = "recon", feature = "v1"))] @@ -66,7 +68,7 @@ pub use self::app::{ ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding, Customers, Disputes, EphemeralKey, Files, Forex, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, Profile, ProfileNew, - Refunds, SessionState, User, Webhooks, + Refunds, Relay, SessionState, User, Webhooks, }; #[cfg(feature = "olap")] pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 852fd0fbd7eb..a070afe09a48 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -55,7 +55,7 @@ use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_ve use super::webhooks::*; use super::{ admin, api_keys, cache::*, connector_onboarding, disputes, files, gsm, health::*, profiles, - user, user_role, + relay, user, user_role, }; #[cfg(feature = "v1")] use super::{apple_pay_certificates_migration, blocklist, payment_link, webhook_events}; @@ -585,6 +585,17 @@ impl Payments { } } +pub struct Relay; + +#[cfg(feature = "oltp")] +impl Relay { + pub fn server(state: AppState) -> Scope { + web::scope("/relay") + .app_data(web::Data::new(state)) + .service(web::resource("").route(web::post().to(relay::relay))) + } +} + #[cfg(feature = "v1")] impl Payments { pub fn server(state: AppState) -> Scope { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b904cb343a53..4b43c42f3bc1 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -37,6 +37,7 @@ pub enum ApiIdentifier { Recon, Poll, ApplePayCertificatesMigration, + Relay, } impl From for ApiIdentifier { @@ -164,6 +165,7 @@ impl From for ApiIdentifier { | Flow::RefundsFilters | Flow::RefundsAggregate | Flow::RefundsManualUpdate => Self::Refunds, + Flow::Relay => Self::Relay, Flow::FrmFulfillment | Flow::IncomingWebhookReceive diff --git a/crates/router/src/routes/relay.rs b/crates/router/src/routes/relay.rs new file mode 100644 index 000000000000..13c92eb19a6e --- /dev/null +++ b/crates/router/src/routes/relay.rs @@ -0,0 +1,40 @@ +use actix_web::{web, Responder}; +use router_env::{instrument, tracing, Flow}; + +use crate::{ + self as app, + core::{api_locking, relay}, + services::{api, authentication as auth}, +}; + +#[instrument(skip_all, fields(flow = ?Flow::Relay))] +#[cfg(feature = "oltp")] +pub async fn relay( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Json, +) -> impl Responder { + let flow = Flow::Relay; + let payload = payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, req, _| { + relay::relay( + state, + auth.merchant_account, + #[cfg(feature = "v1")] + auth.profile_id, + #[cfg(feature = "v2")] + Some(auth.profile.get_id().clone()), + auth.key_store, + req, + ) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index e8435243ff4d..88608316640a 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use actix_web::http::header::HeaderMap; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -538,6 +540,15 @@ where .change_context(errors::ApiErrorResponse::Unauthorized) .attach_printable("Failed to fetch merchant key store for the merchant id")?; + let profile_id = + get_header_value_by_key(headers::X_PROFILE_ID.to_string(), request_headers)? + .map(id_type::ProfileId::from_str) + .transpose() + .change_context(errors::ValidationError::IncorrectValueProvided { + field_name: "X-Profile-Id", + }) + .change_context(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state .store() .find_merchant_account_by_merchant_id( @@ -551,7 +562,7 @@ where let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: None, + profile_id, }; Ok(( auth.clone(), @@ -3109,7 +3120,7 @@ pub fn get_header_value_by_key(key: String, headers: &HeaderMap) -> RouterResult }) .transpose() } -pub fn get_id_type_by_key_from_headers( +pub fn get_id_type_by_key_from_headers( key: String, headers: &HeaderMap, ) -> RouterResult> { diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 84e322872a9c..de2577e6c34d 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -529,6 +529,8 @@ pub enum Flow { PaymentStartRedirection, /// Volume split on the routing type VolumeSplitOnRoutingType, + /// Relay flow + Relay, } /// Trait for providing generic behaviour to flow metric diff --git a/migrations/2024-12-17-141811_add_relay_table/down.sql b/migrations/2024-12-17-141811_add_relay_table/down.sql new file mode 100644 index 000000000000..47b6682c6791 --- /dev/null +++ b/migrations/2024-12-17-141811_add_relay_table/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` +DROP TABLE relay; + +DROP TYPE IF EXISTS "RelayStatus"; + +DROP TYPE IF EXISTS "RelayType"; + diff --git a/migrations/2024-12-17-141811_add_relay_table/up.sql b/migrations/2024-12-17-141811_add_relay_table/up.sql new file mode 100644 index 000000000000..e9f0017bd049 --- /dev/null +++ b/migrations/2024-12-17-141811_add_relay_table/up.sql @@ -0,0 +1,22 @@ +-- Your SQL goes here +CREATE TYPE "RelayStatus" AS ENUM ('created', 'pending', 'failure', 'success'); + +CREATE TYPE "RelayType" AS ENUM ('refund'); + +CREATE TABLE relay ( + id VARCHAR(64) PRIMARY KEY, + connector_resource_id VARCHAR(128) NOT NULL, + connector_id VARCHAR(64) NOT NULL, + profile_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + relay_type "RelayType" NOT NULL, + request_data JSONB DEFAULT NULL, + status "RelayStatus" NOT NULL, + connector_reference_id VARCHAR(128), + error_code VARCHAR(64), + error_message TEXT, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + response_data JSONB DEFAULT NULL +); + From 573974b3a5d53b279bd3959e400ac682aaacd474 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:01:51 +0530 Subject: [PATCH 06/11] refactor(connector): [Airwallex] add device_data in payment request (#6881) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 15 ++++ api-reference/openapi_spec.json | 15 ++++ crates/api_models/src/payments.rs | 9 +++ crates/common_utils/src/types.rs | 9 +++ .../src/connectors/airwallex/transformers.rs | 77 ++++++++++++++++++- crates/hyperswitch_connectors/src/utils.rs | 18 +++++ .../src/router_request_types.rs | 3 + .../src/connector/trustpay/transformers.rs | 3 + crates/router/src/routes/payments/helpers.rs | 3 + crates/router/tests/connectors/trustpay.rs | 3 + crates/router/tests/connectors/utils.rs | 3 + 11 files changed, 156 insertions(+), 2 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 3f1a0ae01de8..b1c33c253af2 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -5613,6 +5613,21 @@ "type": "string", "description": "User-agent of the browser", "nullable": true + }, + "os_type": { + "type": "string", + "description": "The os type of the client device", + "nullable": true + }, + "os_version": { + "type": "string", + "description": "The os version of the client device", + "nullable": true + }, + "device_model": { + "type": "string", + "description": "The device model of the client", + "nullable": true } } }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 2f34aa9ec614..c7527405a5f2 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -8325,6 +8325,21 @@ "type": "string", "description": "User-agent of the browser", "nullable": true + }, + "os_type": { + "type": "string", + "description": "The os type of the client device", + "nullable": true + }, + "os_version": { + "type": "string", + "description": "The os version of the client device", + "nullable": true + }, + "device_model": { + "type": "string", + "description": "The device model of the client", + "nullable": true } } }, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index a7211e6f82ad..ed537c3efeea 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1282,6 +1282,15 @@ pub struct BrowserInformation { /// User-agent of the browser pub user_agent: Option, + + /// The os type of the client device + pub os_type: Option, + + /// The os version of the client device + pub os_version: Option, + + /// The device model of the client + pub device_model: Option, } impl RequestSurchargeDetails { diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 0d39841fe09b..9e71ca76733f 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -1399,6 +1399,15 @@ pub struct BrowserInformation { /// User-agent of the browser pub user_agent: Option, + + /// The os type of the client device + pub os_type: Option, + + /// The os version of the client device + pub os_version: Option, + + /// The device model of the client + pub device_model: Option, } #[cfg(feature = "v2")] diff --git a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs index e8ab479c978c..075a7b070d6d 100644 --- a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs @@ -1,5 +1,5 @@ use common_enums::enums; -use common_utils::{errors::ParsingError, request::Method}; +use common_utils::{errors::ParsingError, pii::IpAddress, request::Method}; use error_stack::ResultExt; use hyperswitch_domain_models::{ payment_method_data::{PaymentMethodData, WalletData}, @@ -21,7 +21,7 @@ use uuid::Uuid; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, - utils::{self, CardData as _}, + utils::{self, BrowserInformationData, CardData as _, PaymentsAuthorizeRequestData}, }; pub struct AirwallexAuthType { @@ -124,6 +124,40 @@ pub struct AirwallexPaymentsRequest { payment_method: AirwallexPaymentMethod, payment_method_options: Option, return_url: Option, + device_data: DeviceData, +} + +#[derive(Debug, Serialize)] +pub struct DeviceData { + accept_header: String, + browser: Browser, + ip_address: Secret, + language: String, + mobile: Option, + screen_color_depth: u8, + screen_height: u32, + screen_width: u32, + timezone: String, +} + +#[derive(Debug, Serialize)] +pub struct Browser { + java_enabled: bool, + javascript_enabled: bool, + user_agent: String, +} + +#[derive(Debug, Serialize)] +pub struct Location { + lat: String, + lon: String, +} + +#[derive(Debug, Serialize)] +pub struct Mobile { + device_model: Option, + os_type: Option, + os_version: Option, } #[derive(Debug, Serialize)] @@ -242,16 +276,55 @@ impl TryFrom<&AirwallexRouterData<&types::PaymentsAuthorizeRouterData>> )) } }?; + let device_data = get_device_data(item.router_data)?; Ok(Self { request_id: Uuid::new_v4().to_string(), payment_method, payment_method_options, return_url: request.complete_authorize_url.clone(), + device_data, }) } } +fn get_device_data( + item: &types::PaymentsAuthorizeRouterData, +) -> Result> { + let info = item.request.get_browser_info()?; + let browser = Browser { + java_enabled: info.get_java_enabled()?, + javascript_enabled: info.get_java_script_enabled()?, + user_agent: info.get_user_agent()?, + }; + let mobile = { + let device_model = info.get_device_model().ok(); + let os_type = info.get_os_type().ok(); + let os_version = info.get_os_version().ok(); + + if device_model.is_some() || os_type.is_some() || os_version.is_some() { + Some(Mobile { + device_model, + os_type, + os_version, + }) + } else { + None + } + }; + Ok(DeviceData { + accept_header: info.get_accept_header()?, + browser, + ip_address: info.get_ip_address()?, + mobile, + screen_color_depth: info.get_color_depth()?, + screen_height: info.get_screen_height()?, + screen_width: info.get_screen_width()?, + timezone: info.get_time_zone()?.to_string(), + language: info.get_language()?, + }) +} + fn get_wallet_details( wallet_data: &WalletData, ) -> Result { diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index cf32d966e056..d1205dd8bb89 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1759,6 +1759,9 @@ pub trait BrowserInformationData { fn get_java_enabled(&self) -> Result; fn get_java_script_enabled(&self) -> Result; fn get_ip_address(&self) -> Result, Error>; + fn get_os_type(&self) -> Result; + fn get_os_version(&self) -> Result; + fn get_device_model(&self) -> Result; } impl BrowserInformationData for BrowserInformation { @@ -1807,6 +1810,21 @@ impl BrowserInformationData for BrowserInformation { self.java_script_enabled .ok_or_else(missing_field_err("browser_info.java_script_enabled")) } + fn get_os_type(&self) -> Result { + self.os_type + .clone() + .ok_or_else(missing_field_err("browser_info.os_type")) + } + fn get_os_version(&self) -> Result { + self.os_version + .clone() + .ok_or_else(missing_field_err("browser_info.os_version")) + } + fn get_device_model(&self) -> Result { + self.device_model + .clone() + .ok_or_else(missing_field_err("browser_info.device_model")) + } } pub fn get_header_key_value<'a>( diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 55624af9f728..aa9fb2b5f1f0 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -486,6 +486,9 @@ pub struct BrowserInformation { pub ip_address: Option, pub accept_header: Option, pub user_agent: Option, + pub os_type: Option, + pub os_version: Option, + pub device_model: Option, } #[derive(Debug, Clone, Default, Serialize)] diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index c20cc1aeb3aa..3006a73a3ee4 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -398,6 +398,9 @@ impl TryFrom<&TrustpayRouterData<&types::PaymentsAuthorizeRouterData>> for Trust accept_header: Some(browser_info.accept_header.unwrap_or("*".to_string())), user_agent: browser_info.user_agent, ip_address: browser_info.ip_address, + os_type: None, + os_version: None, + device_model: None, }; let params = get_mandatory_fields(item.router_data)?; let amount = item.amount.to_owned(); diff --git a/crates/router/src/routes/payments/helpers.rs b/crates/router/src/routes/payments/helpers.rs index 5b0ef815b289..529475a3584d 100644 --- a/crates/router/src/routes/payments/helpers.rs +++ b/crates/router/src/routes/payments/helpers.rs @@ -31,6 +31,9 @@ pub fn populate_ip_into_browser_info( accept_header: None, user_agent: None, ip_address: None, + os_type: None, + os_version: None, + device_model: None, }); let ip_address = req diff --git a/crates/router/tests/connectors/trustpay.rs b/crates/router/tests/connectors/trustpay.rs index 308bf5135855..2e5ea6d50f1a 100644 --- a/crates/router/tests/connectors/trustpay.rs +++ b/crates/router/tests/connectors/trustpay.rs @@ -49,6 +49,9 @@ fn get_default_browser_info() -> BrowserInformation { accept_header: Some("*".to_string()), user_agent: Some("none".to_string()), ip_address: None, + os_type: None, + os_version: None, + device_model: None, } } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 489b55a227b3..361cda63a9f9 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -1013,6 +1013,9 @@ impl Default for BrowserInfoType { java_enabled: Some(true), java_script_enabled: Some(true), ip_address: Some("127.0.0.1".parse().unwrap()), + device_model: Some("Apple IPHONE 7".to_string()), + os_type: Some("IOS or ANDROID".to_string()), + os_version: Some("IOS 14.5".to_string()), }; Self(data) } From 7540b7434766ff9dfa1aa2a56013ac89429dd1e6 Mon Sep 17 00:00:00 2001 From: Kashif Date: Sun, 22 Dec 2024 23:04:01 +0530 Subject: [PATCH 07/11] chore(cypress): payout - fix test cases for adyenplatform bank (#6887) --- .github/workflows/cypress-tests-runner.yml | 3 ++- cypress-tests/cypress/e2e/PayoutUtils/AdyenPlatform.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cypress-tests-runner.yml b/.github/workflows/cypress-tests-runner.yml index bb22a5692ade..bc83c2388f7e 100644 --- a/.github/workflows/cypress-tests-runner.yml +++ b/.github/workflows/cypress-tests-runner.yml @@ -14,6 +14,7 @@ env: CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 PAYMENTS_CONNECTORS: "cybersource stripe" + PAYOUTS_CONNECTORS: "adyenplatform wise" RUST_BACKTRACE: short RUSTUP_MAX_RETRIES: 10 RUN_TESTS: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} @@ -127,7 +128,7 @@ jobs: CONNECTOR_AUTH_PASSPHRASE: ${{ secrets.CONNECTOR_AUTH_PASSPHRASE }} CONNECTOR_CREDS_S3_BUCKET_URI: ${{ secrets.CONNECTOR_CREDS_S3_BUCKET_URI}} DESTINATION_FILE_NAME: "creds.json.gpg" - S3_SOURCE_FILE_NAME: "6f8289a9-6da0-433b-8a24-18d4d7257b7f.json.gpg" + S3_SOURCE_FILE_NAME: "aa328308-b34e-41b7-a590-4fe45cfe7991.json.gpg" shell: bash run: | mkdir -p ".github/secrets" ".github/test" diff --git a/cypress-tests/cypress/e2e/PayoutUtils/AdyenPlatform.js b/cypress-tests/cypress/e2e/PayoutUtils/AdyenPlatform.js index 75b45aac2046..0048daf45b88 100644 --- a/cypress-tests/cypress/e2e/PayoutUtils/AdyenPlatform.js +++ b/cypress-tests/cypress/e2e/PayoutUtils/AdyenPlatform.js @@ -71,7 +71,7 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "success", + status: "initiated", payout_type: "bank", }, }, @@ -101,7 +101,7 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "success", + status: "initiated", payout_type: "bank", }, }, From 24401bc16f9677ce0f5fa70d739e5e6885c7e907 Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:04:45 +0530 Subject: [PATCH 08/11] feat(payment_methods_v2): Added Ephemeral auth for v2 (#6813) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 15 +- crates/api_models/src/ephemeral_key.rs | 43 +++++ crates/api_models/src/payment_methods.rs | 14 +- crates/common_utils/src/events.rs | 3 + crates/common_utils/src/id_type.rs | 2 + .../common_utils/src/id_type/ephemeral_key.rs | 31 +++ crates/diesel_models/src/ephemeral_key.rs | 48 +++++ crates/diesel_models/src/payment_method.rs | 13 +- .../hyperswitch_domain_models/src/payments.rs | 5 +- crates/router/src/consts.rs | 8 +- crates/router/src/core/payment_methods.rs | 127 ++++++------ .../src/core/payment_methods/transformers.rs | 3 +- .../router/src/core/payment_methods/vault.rs | 9 +- crates/router/src/core/payments/helpers.rs | 108 ++++++++++- crates/router/src/db/ephemeral_key.rs | 180 ++++++++++++++++++ crates/router/src/db/kafka_store.rs | 32 ++++ crates/router/src/lib.rs | 9 +- crates/router/src/routes/app.rs | 23 ++- crates/router/src/routes/ephemeral_key.rs | 34 +++- crates/router/src/routes/payment_methods.rs | 12 +- crates/router/src/services/authentication.rs | 66 ++++++- crates/router/src/types/payment_methods.rs | 12 +- .../router/src/types/storage/ephemeral_key.rs | 17 ++ 23 files changed, 688 insertions(+), 126 deletions(-) create mode 100644 crates/common_utils/src/id_type/ephemeral_key.rs diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index b1c33c253af2..14e624233c36 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -13870,16 +13870,11 @@ "PaymentMethodIntentConfirm": { "type": "object", "required": [ - "client_secret", "payment_method_data", "payment_method_type", "payment_method_subtype" ], "properties": { - "client_secret": { - "type": "string", - "description": "For SDK based calls, client_secret would be required" - }, "customer_id": { "type": "string", "description": "The unique identifier of the customer.", @@ -14165,7 +14160,7 @@ "example": "2024-02-24T11:04:09.922Z", "nullable": true }, - "client_secret": { + "ephemeral_key": { "type": "string", "description": "For Client based calls", "nullable": true @@ -14314,14 +14309,6 @@ "properties": { "payment_method_data": { "$ref": "#/components/schemas/PaymentMethodUpdateData" - }, - "client_secret": { - "type": "string", - "description": "This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK", - "example": "secret_k2uj3he2893eiu2d", - "nullable": true, - "maxLength": 30, - "minLength": 30 } }, "additionalProperties": false diff --git a/crates/api_models/src/ephemeral_key.rs b/crates/api_models/src/ephemeral_key.rs index d06490d6bac2..fa61642e353a 100644 --- a/crates/api_models/src/ephemeral_key.rs +++ b/crates/api_models/src/ephemeral_key.rs @@ -1,7 +1,10 @@ use common_utils::id_type; +#[cfg(feature = "v2")] +use masking::Secret; use serde; use utoipa::ToSchema; +#[cfg(feature = "v1")] /// Information required to create an ephemeral key. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct EphemeralKeyCreateRequest { @@ -15,12 +18,52 @@ pub struct EphemeralKeyCreateRequest { pub customer_id: id_type::CustomerId, } +#[cfg(feature = "v2")] +/// Information required to create an ephemeral key. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct EphemeralKeyCreateRequest { + /// Customer ID for which an ephemeral key must be created + #[schema( + min_length = 32, + max_length = 64, + value_type = String, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8" + )] + pub customer_id: id_type::GlobalCustomerId, +} + +#[cfg(feature = "v2")] +/// ephemeral_key for the customer_id mentioned +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, ToSchema)] +pub struct EphemeralKeyResponse { + /// Ephemeral key id + #[schema(value_type = String, max_length = 32, min_length = 1)] + pub id: id_type::EphemeralKeyId, + /// customer_id to which this ephemeral key belongs to + #[schema(value_type = String, max_length = 64, min_length = 32, example = "12345_cus_01926c58bc6e77c09e809964e72af8c8")] + pub customer_id: id_type::GlobalCustomerId, + /// time at which this ephemeral key was created + pub created_at: time::PrimitiveDateTime, + /// time at which this ephemeral key would expire + pub expires: time::PrimitiveDateTime, + #[schema(value_type=String)] + /// ephemeral key + pub secret: Secret, +} + impl common_utils::events::ApiEventMetric for EphemeralKeyCreateRequest { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } +#[cfg(feature = "v2")] +impl common_utils::events::ApiEventMetric for EphemeralKeyResponse { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} + /// ephemeral_key for the customer_id mentioned #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, ToSchema)] pub struct EphemeralKeyCreateResponse { diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index c821feaff575..fe5cb2933f9e 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -163,9 +163,6 @@ pub struct PaymentMethodIntentCreate { #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct PaymentMethodIntentConfirm { - /// For SDK based calls, client_secret would be required - pub client_secret: String, - /// The unique identifier of the customer. #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, @@ -211,9 +208,6 @@ pub struct PaymentMethodIntentConfirmInternal { #[schema(value_type = PaymentMethodType,example = "credit")] pub payment_method_subtype: api_enums::PaymentMethodType, - /// For SDK based calls, client_secret would be required - pub client_secret: String, - /// The unique identifier of the customer. #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, @@ -226,7 +220,6 @@ pub struct PaymentMethodIntentConfirmInternal { impl From for PaymentMethodIntentConfirm { fn from(item: PaymentMethodIntentConfirmInternal) -> Self { Self { - client_secret: item.client_secret, payment_method_type: item.payment_method_type, payment_method_subtype: item.payment_method_subtype, customer_id: item.customer_id, @@ -408,10 +401,6 @@ pub struct PaymentMethodUpdate { pub struct PaymentMethodUpdate { /// payment method data to be passed pub payment_method_data: PaymentMethodUpdateData, - - /// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK - #[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893eiu2d")] - pub client_secret: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -826,7 +815,8 @@ pub struct PaymentMethodResponse { pub last_used_at: Option, /// For Client based calls - pub client_secret: Option, + #[schema(value_type=Option)] + pub ephemeral_key: Option>, pub payment_method_data: Option, } diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index b9af92786f58..9494b8209e75 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -102,6 +102,9 @@ pub enum ApiEventsType { poll_id: String, }, Analytics, + EphemeralKey { + key_id: id_type::EphemeralKeyId, + }, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index c7393155226b..ea90c00121e8 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -3,6 +3,7 @@ mod api_key; mod customer; +mod ephemeral_key; #[cfg(feature = "v2")] mod global_id; mod merchant; @@ -38,6 +39,7 @@ pub use self::global_id::{ pub use self::{ api_key::ApiKeyId, customer::CustomerId, + ephemeral_key::EphemeralKeyId, merchant::MerchantId, merchant_connector_account::MerchantConnectorAccountId, organization::OrganizationId, diff --git a/crates/common_utils/src/id_type/ephemeral_key.rs b/crates/common_utils/src/id_type/ephemeral_key.rs new file mode 100644 index 000000000000..071980fc6a46 --- /dev/null +++ b/crates/common_utils/src/id_type/ephemeral_key.rs @@ -0,0 +1,31 @@ +crate::id_type!( + EphemeralKeyId, + "A type for key_id that can be used for Ephemeral key IDs" +); +crate::impl_id_type_methods!(EphemeralKeyId, "key_id"); + +// This is to display the `EphemeralKeyId` as EphemeralKeyId(abcd) +crate::impl_debug_id_type!(EphemeralKeyId); +crate::impl_try_from_cow_str_id_type!(EphemeralKeyId, "key_id"); + +crate::impl_generate_id_id_type!(EphemeralKeyId, "eki"); +crate::impl_serializable_secret_id_type!(EphemeralKeyId); +crate::impl_queryable_id_type!(EphemeralKeyId); +crate::impl_to_sql_from_sql_id_type!(EphemeralKeyId); + +impl crate::events::ApiEventMetric for EphemeralKeyId { + fn get_api_event_type(&self) -> Option { + Some(crate::events::ApiEventsType::EphemeralKey { + key_id: self.clone(), + }) + } +} + +crate::impl_default_id_type!(EphemeralKeyId, "key"); + +impl EphemeralKeyId { + /// Generate a key for redis + pub fn generate_redis_key(&self) -> String { + format!("epkey_{}", self.get_string_repr()) + } +} diff --git a/crates/diesel_models/src/ephemeral_key.rs b/crates/diesel_models/src/ephemeral_key.rs index d398ecdf784a..c7fc103ed09b 100644 --- a/crates/diesel_models/src/ephemeral_key.rs +++ b/crates/diesel_models/src/ephemeral_key.rs @@ -1,3 +1,33 @@ +#[cfg(feature = "v2")] +use masking::{PeekInterface, Secret}; +#[cfg(feature = "v2")] +pub struct EphemeralKeyTypeNew { + pub id: common_utils::id_type::EphemeralKeyId, + pub merchant_id: common_utils::id_type::MerchantId, + pub customer_id: common_utils::id_type::GlobalCustomerId, + pub secret: Secret, + pub resource_type: ResourceType, +} + +#[cfg(feature = "v2")] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct EphemeralKeyType { + pub id: common_utils::id_type::EphemeralKeyId, + pub merchant_id: common_utils::id_type::MerchantId, + pub customer_id: common_utils::id_type::GlobalCustomerId, + pub resource_type: ResourceType, + pub created_at: time::PrimitiveDateTime, + pub expires: time::PrimitiveDateTime, + pub secret: Secret, +} + +#[cfg(feature = "v2")] +impl EphemeralKeyType { + pub fn generate_secret_key(&self) -> String { + format!("epkey_{}", self.secret.peek()) + } +} + pub struct EphemeralKeyNew { pub id: String, pub merchant_id: common_utils::id_type::MerchantId, @@ -20,3 +50,21 @@ impl common_utils::events::ApiEventMetric for EphemeralKey { Some(common_utils::events::ApiEventsType::Miscellaneous) } } + +#[derive( + Clone, + Copy, + Debug, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + PartialEq, + Eq, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ResourceType { + Payment, + PaymentMethod, +} diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 8ae34a6f89cf..76613984363d 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -290,6 +290,7 @@ pub enum PaymentMethodUpdate { network_token_requestor_reference_id: Option, network_token_locker_id: Option, network_token_payment_method_data: Option, + locker_fingerprint_id: Option, }, ConnectorMandateDetailsUpdate { connector_mandate_details: Option, @@ -324,6 +325,7 @@ pub struct PaymentMethodUpdateInternal { network_token_requestor_reference_id: Option, network_token_locker_id: Option, network_token_payment_method_data: Option, + locker_fingerprint_id: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -343,6 +345,7 @@ impl PaymentMethodUpdateInternal { network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, + locker_fingerprint_id, } = self; PaymentMethod { @@ -361,7 +364,7 @@ impl PaymentMethodUpdateInternal { client_secret: source.client_secret, payment_method_billing_address: source.payment_method_billing_address, updated_by: updated_by.or(source.updated_by), - locker_fingerprint_id: source.locker_fingerprint_id, + locker_fingerprint_id: locker_fingerprint_id.or(source.locker_fingerprint_id), payment_method_type_v2: payment_method_type_v2.or(source.payment_method_type_v2), payment_method_subtype: payment_method_subtype.or(source.payment_method_subtype), id: source.id, @@ -710,6 +713,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { payment_method_data: None, @@ -725,6 +729,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::UpdatePaymentMethodDataAndLastUsed { payment_method_data, @@ -744,6 +749,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate { network_transaction_id, @@ -762,6 +768,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::StatusUpdate { status } => Self { payment_method_data: None, @@ -777,6 +784,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::AdditionalDataUpdate { payment_method_data, @@ -787,6 +795,7 @@ impl From for PaymentMethodUpdateInternal { network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, + locker_fingerprint_id, } => Self { payment_method_data, last_used_at: None, @@ -801,6 +810,7 @@ impl From for PaymentMethodUpdateInternal { network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, + locker_fingerprint_id, }, PaymentMethodUpdate::ConnectorMandateDetailsUpdate { connector_mandate_details, @@ -818,6 +828,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 40ab88663c9e..1c88b83d087e 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -29,7 +29,10 @@ pub mod payment_intent; use common_enums as storage_enums; #[cfg(feature = "v2")] -use diesel_models::types::{FeatureMetadata, OrderDetailsWithAmount}; +use diesel_models::{ + ephemeral_key, + types::{FeatureMetadata, OrderDetailsWithAmount}, +}; use self::payment_attempt::PaymentAttempt; #[cfg(feature = "v1")] diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 823f0a4bf04f..e2784b968ad1 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -171,19 +171,19 @@ pub const DEFAULT_SDK_LAYOUT: &str = "tabs"; /// Vault Add request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const ADD_VAULT_REQUEST_URL: &str = "/vault/add"; +pub const ADD_VAULT_REQUEST_URL: &str = "/api/v2/vault/add"; /// Vault Get Fingerprint request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const VAULT_FINGERPRINT_REQUEST_URL: &str = "/fingerprint"; +pub const VAULT_FINGERPRINT_REQUEST_URL: &str = "/api/v2/vault/fingerprint"; /// Vault Retrieve request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const VAULT_RETRIEVE_REQUEST_URL: &str = "/vault/retrieve"; +pub const VAULT_RETRIEVE_REQUEST_URL: &str = "/api/v2/vault/retrieve"; /// Vault Delete request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const VAULT_DELETE_REQUEST_URL: &str = "/vault/delete"; +pub const VAULT_DELETE_REQUEST_URL: &str = "/api/v2/vault/delete"; /// Vault Header content type #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 61dc70ac4799..6cdadf07320c 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -62,7 +62,7 @@ use crate::{ types::{ api::{self, payment_methods::PaymentMethodCreateExt}, payment_methods as pm_types, - storage::PaymentMethodListContext, + storage::{ephemeral_key, PaymentMethodListContext}, }, utils::ext_traits::OptionExt, }; @@ -851,21 +851,23 @@ pub async fn create_payment_method( let db = &*state.store; let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); + let key_manager_state = &(state).into(); db.find_customer_by_global_id( - &(state.into()), + key_manager_state, &customer_id, merchant_account.get_id(), key_store, merchant_account.storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - let key_manager_state = state.into(); + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) + .attach_printable("Customer not found for the payment method")?; + let payment_method_billing_address: Option>> = req .billing .clone() - .async_map(|billing| cards::create_encrypted_data(&key_manager_state, key_store, billing)) + .async_map(|billing| cards::create_encrypted_data(key_manager_state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -877,7 +879,7 @@ pub async fn create_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; - let payment_method = create_payment_method_for_intent( + let (payment_method, ephemeral_key) = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, @@ -902,14 +904,15 @@ pub async fn create_payment_method( .await; let response = match vaulting_result { - Ok(resp) => { + Ok((vaulting_resp, fingerprint_id)) => { let pm_update = create_pm_additional_data_update( &payment_method_data, state, key_store, - Some(resp.vault_id.get_string_repr().clone()), + Some(vaulting_resp.vault_id.get_string_repr().clone()), Some(req.payment_method_type), Some(req.payment_method_subtype), + Some(fingerprint_id), ) .await .attach_printable("Unable to create Payment method data")?; @@ -926,7 +929,10 @@ pub async fn create_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + let resp = pm_transforms::generate_payment_method_response( + &payment_method, + Some(ephemeral_key), + )?; Ok(resp) } @@ -964,21 +970,23 @@ pub async fn payment_method_intent_create( let db = &*state.store; let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); + let key_manager_state = &(state).into(); db.find_customer_by_global_id( - &(state.into()), + key_manager_state, &customer_id, merchant_account.get_id(), key_store, merchant_account.storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - let key_manager_state = state.into(); + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) + .attach_printable("Customer not found for the payment method")?; + let payment_method_billing_address: Option>> = req .billing .clone() - .async_map(|billing| cards::create_encrypted_data(&key_manager_state, key_store, billing)) + .async_map(|billing| cards::create_encrypted_data(key_manager_state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -991,7 +999,7 @@ pub async fn payment_method_intent_create( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; - let payment_method = create_payment_method_for_intent( + let (payment_method, ephemeral_key) = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, @@ -1004,7 +1012,8 @@ pub async fn payment_method_intent_create( .await .attach_printable("Failed to add Payment method to DB")?; - let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + let resp = + pm_transforms::generate_payment_method_response(&payment_method, Some(ephemeral_key))?; Ok(services::ApplicationResponse::Json(resp)) } @@ -1018,10 +1027,10 @@ pub async fn payment_method_intent_confirm( key_store: &domain::MerchantKeyStore, pm_id: String, ) -> RouterResponse { + let key_manager_state = &(state).into(); req.validate()?; let db = &*state.store; - let client_secret = req.client_secret.clone(); let pm_id = id_type::GlobalPaymentMethodId::generate_from_string(pm_id) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; @@ -1037,11 +1046,6 @@ pub async fn payment_method_intent_confirm( .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) .attach_printable("Unable to find payment method")?; - when( - cards::authenticate_pm_client_secret_and_check_expiry(&client_secret, &payment_method)?, - || Err(errors::ApiErrorResponse::ClientSecretExpired), - )?; - when( payment_method.status != enums::PaymentMethodStatus::AwaitingData, || { @@ -1054,7 +1058,7 @@ pub async fn payment_method_intent_confirm( let customer_id = payment_method.customer_id.to_owned(); db.find_customer_by_global_id( - &(state.into()), + key_manager_state, &customer_id, merchant_account.get_id(), key_store, @@ -1075,14 +1079,15 @@ pub async fn payment_method_intent_confirm( .await; let response = match vaulting_result { - Ok(resp) => { + Ok((vaulting_resp, fingerprint_id)) => { let pm_update = create_pm_additional_data_update( &payment_method_data, state, key_store, - Some(resp.vault_id.get_string_repr().clone()), + Some(vaulting_resp.vault_id.get_string_repr().clone()), Some(req.payment_method_type), Some(req.payment_method_subtype), + Some(fingerprint_id), ) .await .attach_printable("Unable to create Payment method data")?; @@ -1099,7 +1104,7 @@ pub async fn payment_method_intent_confirm( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + let resp = pm_transforms::generate_payment_method_response(&payment_method, None)?; Ok(resp) } @@ -1153,7 +1158,6 @@ pub async fn create_payment_method_in_db( card_scheme: Option, ) -> errors::CustomResult { let db = &*state.store; - let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); let current_time = common_utils::date_time::now(); let response = db @@ -1170,7 +1174,7 @@ pub async fn create_payment_method_in_db( payment_method_data, connector_mandate_details, customer_acceptance, - client_secret: Some(client_secret), + client_secret: None, status: status.unwrap_or(enums::PaymentMethodStatus::Active), network_transaction_id: network_transaction_id.to_owned(), created_at: current_time, @@ -1205,9 +1209,18 @@ pub async fn create_payment_method_for_intent( key_store: &domain::MerchantKeyStore, storage_scheme: enums::MerchantStorageScheme, payment_method_billing_address: crypto::OptionalEncryptableValue, -) -> errors::CustomResult { +) -> errors::CustomResult<(domain::PaymentMethod, Secret), errors::ApiErrorResponse> { let db = &*state.store; - let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); + let ephemeral_key = payment_helpers::create_ephemeral_key( + state, + customer_id, + merchant_id, + ephemeral_key::ResourceType::PaymentMethod, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to create ephemeral_key")?; + let current_time = common_utils::date_time::now(); let response = db @@ -1224,7 +1237,7 @@ pub async fn create_payment_method_for_intent( payment_method_data: None, connector_mandate_details: None, customer_acceptance: None, - client_secret: Some(client_secret), + client_secret: None, status: enums::PaymentMethodStatus::AwaitingData, network_transaction_id: None, created_at: current_time, @@ -1244,7 +1257,7 @@ pub async fn create_payment_method_for_intent( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in db")?; - Ok(response) + Ok((response, ephemeral_key.secret)) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -1255,15 +1268,16 @@ pub async fn create_pm_additional_data_update( vault_id: Option, payment_method_type: Option, payment_method_subtype: Option, + vault_fingerprint_id: Option, ) -> RouterResult { let card = match pmd { pm_types::PaymentMethodVaultingData::Card(card) => { api::PaymentMethodsData::Card(card.clone().into()) } }; - let key_manager_state = state.into(); + let key_manager_state = &(state).into(); let pmd: Encryptable> = - cards::create_encrypted_data(&key_manager_state, key_store, card) + cards::create_encrypted_data(key_manager_state, key_store, card) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt Payment method data")?; @@ -1277,6 +1291,7 @@ pub async fn create_pm_additional_data_update( network_token_requestor_reference_id: None, network_token_locker_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: vault_fingerprint_id, }; Ok(pm_update) @@ -1290,7 +1305,7 @@ pub async fn vault_payment_method( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, existing_vault_id: Option, -) -> RouterResult { +) -> RouterResult<(pm_types::AddVaultResponse, String)> { let db = &*state.store; // get fingerprint_id from vault @@ -1301,17 +1316,16 @@ pub async fn vault_payment_method( // throw back error if payment method is duplicated when( - Some( - db.find_payment_method_by_fingerprint_id( - &(state.into()), - key_store, - &fingerprint_id_from_vault, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to find payment method by fingerprint_id")?, + db.find_payment_method_by_fingerprint_id( + &(state.into()), + key_store, + &fingerprint_id_from_vault, ) - .is_some(), + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to find payment method by fingerprint_id") + .inspect_err(|e| logger::error!("Vault Fingerprint_id error: {:?}", e)) + .is_ok(), || { Err(report!(errors::ApiErrorResponse::DuplicatePaymentMethod) .attach_printable("Cannot vault duplicate payment method")) @@ -1324,7 +1338,7 @@ pub async fn vault_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in vault")?; - Ok(resp_from_vault) + Ok((resp_from_vault, fingerprint_id_from_vault)) } #[cfg(all( @@ -1754,6 +1768,11 @@ pub async fn retrieve_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + when( + payment_method.status == enums::PaymentMethodStatus::Inactive, + || Err(errors::ApiErrorResponse::PaymentMethodNotFound), + )?; + let pmd = payment_method .payment_method_data .clone() @@ -1774,7 +1793,7 @@ pub async fn retrieve_payment_method( created: Some(payment_method.created_at), recurring_enabled: false, last_used_at: Some(payment_method.last_used_at), - client_secret: payment_method.client_secret.clone(), + ephemeral_key: None, payment_method_data: pmd, }; @@ -1822,16 +1841,12 @@ pub async fn update_payment_method( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to retrieve payment method from vault")? - .data - .expose() - .parse_struct("PaymentMethodVaultingData") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to parse PaymentMethodVaultingData")?; + .data; let vault_request_data = pm_transforms::generate_pm_vaulting_req_from_update_request(pmd, req.payment_method_data); - let vaulting_response = vault_payment_method( + let (vaulting_response, fingerprint_id) = vault_payment_method( &state, &vault_request_data, &merchant_account, @@ -1848,6 +1863,7 @@ pub async fn update_payment_method( Some(vaulting_response.vault_id.get_string_repr().clone()), payment_method.get_payment_method_type(), payment_method.get_payment_method_subtype(), + Some(fingerprint_id), ) .await .attach_printable("Unable to create Payment method data")?; @@ -1864,7 +1880,7 @@ pub async fn update_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let response = pm_transforms::generate_payment_method_response(&payment_method)?; + let response = pm_transforms::generate_payment_method_response(&payment_method, None)?; // Add a PT task to handle payment_method delete from vault @@ -1896,6 +1912,11 @@ pub async fn delete_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + when( + payment_method.status == enums::PaymentMethodStatus::Inactive, + || Err(errors::ApiErrorResponse::PaymentMethodNotFound), + )?; + let vault_id = payment_method .locker_id .clone() diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index c3fbfd8afbff..ce95096a0fb3 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -551,6 +551,7 @@ pub fn generate_pm_vaulting_req_from_update_request( #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub fn generate_payment_method_response( pm: &domain::PaymentMethod, + ephemeral_key: Option>, ) -> errors::RouterResult { let pmd = pm .payment_method_data @@ -572,7 +573,7 @@ pub fn generate_payment_method_response( created: Some(pm.created_at), recurring_enabled: false, last_used_at: Some(pm.last_used_at), - client_secret: pm.client_secret.clone(), + ephemeral_key, payment_method_data: pmd, }; diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index fcff711f6c9a..65ef16cb4496 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -1186,6 +1186,7 @@ async fn create_vault_request( jwekey: &settings::Jwekey, locker: &settings::Locker, payload: Vec, + tenant_id: id_type::TenantId, ) -> CustomResult { let private_key = jwekey.vault_private_key.peek().as_bytes(); @@ -1206,6 +1207,10 @@ async fn create_vault_request( headers::CONTENT_TYPE, consts::VAULT_HEADER_CONTENT_TYPE.into(), ); + request.add_header( + headers::X_TENANT_ID, + tenant_id.get_string_repr().to_owned().into(), + ); request.set_body(request::RequestContent::Json(Box::new(jwe_payload))); Ok(request) } @@ -1219,7 +1224,9 @@ pub async fn call_to_vault( let locker = &state.conf.locker; let jwekey = state.conf.jwekey.get_inner(); - let request = create_vault_request::(jwekey, locker, payload).await?; + let request = + create_vault_request::(jwekey, locker, payload, state.tenant.tenant_id.to_owned()) + .await?; let response = services::call_connector_api(state, request, V::get_vaulting_flow_name()) .await .change_context(errors::VaultError::VaultAPIError); diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 78cfdda33b49..c243686a76cd 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,11 +1,15 @@ use std::{borrow::Cow, str::FromStr}; +#[cfg(feature = "v2")] +use api_models::ephemeral_key::EphemeralKeyResponse; use api_models::{ mandates::RecurringDetails, payments::{additional_info as payment_additional_types, RequestSurchargeDetails}, }; use base64::Engine; use common_enums::ConnectorType; +#[cfg(feature = "v2")] +use common_utils::id_type::GenerateId; use common_utils::{ crypto::Encryptable, ext_traits::{AsyncExt, ByteSliceExt, Encode, ValueExt}, @@ -40,6 +44,8 @@ use openssl::{ pkey::PKey, symm::{decrypt_aead, Cipher}, }; +#[cfg(feature = "v2")] +use redis_interface::errors::RedisError; use router_env::{instrument, logger, tracing}; use uuid::Uuid; use x509_parser::parse_x509_certificate; @@ -48,8 +54,6 @@ use super::{ operations::{BoxedOperation, Operation, PaymentResponse}, CustomerDetails, PaymentData, }; -#[cfg(feature = "v2")] -use crate::core::admin as core_admin; use crate::{ configs::settings::{ConnectorRequestReferenceIdConfig, TempLockerEnableConfig}, connector, @@ -84,6 +88,8 @@ use crate::{ OptionExt, StringExt, }, }; +#[cfg(feature = "v2")] +use crate::{core::admin as core_admin, headers}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use crate::{ core::payment_methods::cards::create_encrypted_data, types::storage::CustomerUpdate::Update, @@ -3021,6 +3027,7 @@ pub fn make_merchant_url_with_response( Ok(merchant_url_with_response.to_string()) } +#[cfg(feature = "v1")] pub async fn make_ephemeral_key( state: SessionState, customer_id: id_type::CustomerId, @@ -3043,6 +3050,80 @@ pub async fn make_ephemeral_key( Ok(services::ApplicationResponse::Json(ek)) } +#[cfg(feature = "v2")] +pub async fn make_ephemeral_key( + state: SessionState, + customer_id: id_type::GlobalCustomerId, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + headers: &actix_web::http::header::HeaderMap, +) -> errors::RouterResponse { + let db = &state.store; + let key_manager_state = &((&state).into()); + db.find_customer_by_global_id( + key_manager_state, + &customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let resource_type = services::authentication::get_header_value_by_key( + headers::X_RESOURCE_TYPE.to_string(), + headers, + )? + .map(ephemeral_key::ResourceType::from_str) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid", headers::X_RESOURCE_TYPE), + })? + .get_required_value("ResourceType") + .attach_printable("Failed to convert ResourceType from string")?; + + let ephemeral_key = create_ephemeral_key( + &state, + &customer_id, + merchant_account.get_id(), + resource_type, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to create ephemeral key")?; + + let response = EphemeralKeyResponse::foreign_from(ephemeral_key); + Ok(services::ApplicationResponse::Json(response)) +} + +#[cfg(feature = "v2")] +pub async fn create_ephemeral_key( + state: &SessionState, + customer_id: &id_type::GlobalCustomerId, + merchant_id: &id_type::MerchantId, + resource_type: ephemeral_key::ResourceType, +) -> RouterResult { + use common_utils::generate_time_ordered_id; + + let store = &state.store; + let id = id_type::EphemeralKeyId::generate(); + let secret = masking::Secret::new(generate_time_ordered_id("epk")); + let ephemeral_key = ephemeral_key::EphemeralKeyTypeNew { + id, + customer_id: customer_id.to_owned(), + merchant_id: merchant_id.to_owned(), + secret, + resource_type, + }; + let ephemeral_key = store + .create_ephemeral_key(ephemeral_key, state.conf.eph_key.validity) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to create ephemeral key")?; + Ok(ephemeral_key) +} + +#[cfg(feature = "v1")] pub async fn delete_ephemeral_key( state: SessionState, ek_id: String, @@ -3056,6 +3137,29 @@ pub async fn delete_ephemeral_key( Ok(services::ApplicationResponse::Json(ek)) } +#[cfg(feature = "v2")] +pub async fn delete_ephemeral_key( + state: SessionState, + ephemeral_key_id: String, +) -> errors::RouterResponse { + let db = state.store.as_ref(); + let ephemeral_key = db + .delete_ephemeral_key(&ephemeral_key_id) + .await + .map_err(|err| match err.current_context() { + errors::StorageError::ValueNotFound(_) => { + err.change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "Ephemeral Key not found".to_string(), + }) + } + _ => err.change_context(errors::ApiErrorResponse::InternalServerError), + }) + .attach_printable("Unable to delete ephemeral key")?; + + let response = EphemeralKeyResponse::foreign_from(ephemeral_key); + Ok(services::ApplicationResponse::Json(response)) +} + pub fn make_pg_redirect_response( payment_id: id_type::PaymentId, response: &api::PaymentsResponse, diff --git a/crates/router/src/db/ephemeral_key.rs b/crates/router/src/db/ephemeral_key.rs index a1d43e83b871..a77995e4e58e 100644 --- a/crates/router/src/db/ephemeral_key.rs +++ b/crates/router/src/db/ephemeral_key.rs @@ -1,5 +1,9 @@ +#[cfg(feature = "v2")] +use common_utils::id_type; use time::ext::NumericalDuration; +#[cfg(feature = "v2")] +use crate::types::storage::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew, ResourceType}; use crate::{ core::errors::{self, CustomResult}, db::MockDb, @@ -8,30 +12,64 @@ use crate::{ #[async_trait::async_trait] pub trait EphemeralKeyInterface { + #[cfg(feature = "v1")] async fn create_ephemeral_key( &self, _ek: EphemeralKeyNew, _validity: i64, ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn create_ephemeral_key( + &self, + _ek: EphemeralKeyTypeNew, + _validity: i64, + ) -> CustomResult; + + #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, _key: &str, ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn get_ephemeral_key( + &self, + _key: &str, + ) -> CustomResult; + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, _id: &str, ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + _id: &str, + ) -> CustomResult; } mod storage { use common_utils::date_time; + #[cfg(feature = "v2")] + use common_utils::id_type; use error_stack::ResultExt; + #[cfg(feature = "v2")] + use masking::PeekInterface; + #[cfg(feature = "v2")] + use redis_interface::errors::RedisError; use redis_interface::HsetnxReply; use router_env::{instrument, tracing}; use storage_impl::redis::kv_store::RedisConnInterface; use time::ext::NumericalDuration; use super::EphemeralKeyInterface; + #[cfg(feature = "v2")] + use crate::types::storage::ephemeral_key::{ + EphemeralKeyType, EphemeralKeyTypeNew, ResourceType, + }; use crate::{ core::errors::{self, CustomResult}, services::Store, @@ -40,6 +78,7 @@ mod storage { #[async_trait::async_trait] impl EphemeralKeyInterface for Store { + #[cfg(feature = "v1")] #[instrument(skip_all)] async fn create_ephemeral_key( &self, @@ -94,6 +133,68 @@ mod storage { Err(er) => Err(er).change_context(errors::StorageError::KVError), } } + + #[cfg(feature = "v2")] + #[instrument(skip_all)] + async fn create_ephemeral_key( + &self, + new: EphemeralKeyTypeNew, + validity: i64, + ) -> CustomResult { + let created_at = date_time::now(); + let expires = created_at.saturating_add(validity.hours()); + let id_key = new.id.generate_redis_key(); + + let created_ephemeral_key = EphemeralKeyType { + id: new.id, + created_at, + expires, + customer_id: new.customer_id, + merchant_id: new.merchant_id, + secret: new.secret, + resource_type: new.resource_type, + }; + let secret_key = created_ephemeral_key.generate_secret_key(); + + match self + .get_redis_conn() + .map_err(Into::::into)? + .serialize_and_set_multiple_hash_field_if_not_exist( + &[ + (&secret_key, &created_ephemeral_key), + (&id_key, &created_ephemeral_key), + ], + "ephkey", + None, + ) + .await + { + Ok(v) if v.contains(&HsetnxReply::KeyNotSet) => { + Err(errors::StorageError::DuplicateValue { + entity: "ephemeral key", + key: None, + } + .into()) + } + Ok(_) => { + let expire_at = expires.assume_utc().unix_timestamp(); + self.get_redis_conn() + .map_err(Into::::into)? + .set_expire_at(&secret_key, expire_at) + .await + .change_context(errors::StorageError::KVError)?; + self.get_redis_conn() + .map_err(Into::::into)? + .set_expire_at(&id_key, expire_at) + .await + .change_context(errors::StorageError::KVError)?; + Ok(created_ephemeral_key) + } + Err(er) => Err(er).change_context(errors::StorageError::KVError), + } + } + + #[cfg(feature = "v1")] #[instrument(skip_all)] async fn get_ephemeral_key( &self, @@ -106,6 +207,22 @@ mod storage { .await .change_context(errors::StorageError::KVError) } + + #[cfg(feature = "v2")] + #[instrument(skip_all)] + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + let key = format!("epkey_{key}"); + self.get_redis_conn() + .map_err(Into::::into)? + .get_hash_field_and_deserialize(&key, "ephkey", "EphemeralKeyType") + .await + .change_context(errors::StorageError::KVError) + } + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, id: &str, @@ -125,11 +242,45 @@ mod storage { .change_context(errors::StorageError::KVError)?; Ok(ek) } + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + let ephemeral_key = self.get_ephemeral_key(id).await?; + let redis_id_key = ephemeral_key.id.generate_redis_key(); + let secret_key = ephemeral_key.generate_secret_key(); + + self.get_redis_conn() + .map_err(Into::::into)? + .delete_key(&redis_id_key) + .await + .map_err(|err| match err.current_context() { + RedisError::NotFound => { + err.change_context(errors::StorageError::ValueNotFound(redis_id_key)) + } + _ => err.change_context(errors::StorageError::KVError), + })?; + + self.get_redis_conn() + .map_err(Into::::into)? + .delete_key(&secret_key) + .await + .map_err(|err| match err.current_context() { + RedisError::NotFound => { + err.change_context(errors::StorageError::ValueNotFound(secret_key)) + } + _ => err.change_context(errors::StorageError::KVError), + })?; + Ok(ephemeral_key) + } } } #[async_trait::async_trait] impl EphemeralKeyInterface for MockDb { + #[cfg(feature = "v1")] async fn create_ephemeral_key( &self, ek: EphemeralKeyNew, @@ -150,6 +301,17 @@ impl EphemeralKeyInterface for MockDb { ephemeral_keys.push(ephemeral_key.clone()); Ok(ephemeral_key) } + + #[cfg(feature = "v2")] + async fn create_ephemeral_key( + &self, + ek: EphemeralKeyTypeNew, + validity: i64, + ) -> CustomResult { + todo!() + } + + #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, key: &str, @@ -167,6 +329,16 @@ impl EphemeralKeyInterface for MockDb { ), } } + + #[cfg(feature = "v2")] + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + todo!() + } + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, id: &str, @@ -181,4 +353,12 @@ impl EphemeralKeyInterface for MockDb { ); } } + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + todo!() + } } diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index f49173b35496..4155bbf3ff9a 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -6,6 +6,8 @@ use common_utils::{ id_type, types::{keymanager::KeyManagerState, theme::ThemeLineage}, }; +#[cfg(feature = "v2")] +use diesel_models::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew}; use diesel_models::{ enums, enums::ProcessTrackerStatus, @@ -646,6 +648,7 @@ impl DisputeInterface for KafkaStore { #[async_trait::async_trait] impl EphemeralKeyInterface for KafkaStore { + #[cfg(feature = "v1")] async fn create_ephemeral_key( &self, ek: EphemeralKeyNew, @@ -653,18 +656,47 @@ impl EphemeralKeyInterface for KafkaStore { ) -> CustomResult { self.diesel_store.create_ephemeral_key(ek, validity).await } + + #[cfg(feature = "v2")] + async fn create_ephemeral_key( + &self, + ek: EphemeralKeyTypeNew, + validity: i64, + ) -> CustomResult { + self.diesel_store.create_ephemeral_key(ek, validity).await + } + + #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, key: &str, ) -> CustomResult { self.diesel_store.get_ephemeral_key(key).await } + + #[cfg(feature = "v2")] + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.get_ephemeral_key(key).await + } + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, id: &str, ) -> CustomResult { self.diesel_store.delete_ephemeral_key(id).await } + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + self.diesel_store.delete_ephemeral_key(id).await + } } #[async_trait::async_trait] diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 9100ea3ebaa5..625a4d9c95b5 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -90,6 +90,7 @@ pub mod headers { pub const X_REDIRECT_URI: &str = "x-redirect-uri"; pub const X_TENANT_ID: &str = "x-tenant-id"; pub const X_CLIENT_SECRET: &str = "X-Client-Secret"; + pub const X_RESOURCE_TYPE: &str = "X-Resource-Type"; } pub mod pii { @@ -156,15 +157,17 @@ pub fn mk_app( } } + #[cfg(all(feature = "oltp", any(feature = "v1", feature = "v2"),))] + { + server_app = server_app.service(routes::EphemeralKey::server(state.clone())) + } #[cfg(all( feature = "oltp", any(feature = "v1", feature = "v2"), not(feature = "customer_v2") ))] { - server_app = server_app - .service(routes::EphemeralKey::server(state.clone())) - .service(routes::Poll::server(state.clone())) + server_app = server_app.service(routes::Poll::server(state.clone())) } #[cfg(feature = "olap")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index a070afe09a48..0b5d481e3da3 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -27,11 +27,7 @@ use self::settings::Tenant; use super::currency; #[cfg(feature = "dummy_connector")] use super::dummy_connector::*; -#[cfg(all( - any(feature = "v1", feature = "v2"), - not(feature = "customer_v2"), - feature = "oltp" -))] +#[cfg(all(any(feature = "v1", feature = "v2"), feature = "oltp"))] use super::ephemeral_key::*; #[cfg(any(feature = "olap", feature = "oltp"))] use super::payment_methods::*; @@ -1155,8 +1151,11 @@ impl PaymentMethods { web::resource("/{id}/update-saved-payment-method") .route(web::patch().to(payment_method_update_api)), ) - .service(web::resource("/{id}").route(web::get().to(payment_method_retrieve_api))) - .service(web::resource("/{id}").route(web::delete().to(payment_method_delete_api))); + .service( + web::resource("/{id}") + .route(web::get().to(payment_method_retrieve_api)) + .route(web::delete().to(payment_method_delete_api)), + ); route } @@ -1430,6 +1429,16 @@ impl EphemeralKey { } } +#[cfg(feature = "v2")] +impl EphemeralKey { + pub fn server(config: AppState) -> Scope { + web::scope("/v2/ephemeral-keys") + .app_data(web::Data::new(config)) + .service(web::resource("").route(web::post().to(ephemeral_key_create))) + .service(web::resource("/{id}").route(web::delete().to(ephemeral_key_delete))) + } +} + pub struct Mandates; #[cfg(all(any(feature = "olap", feature = "oltp"), feature = "v1"))] diff --git a/crates/router/src/routes/ephemeral_key.rs b/crates/router/src/routes/ephemeral_key.rs index e6a33c59192e..0330482e81a9 100644 --- a/crates/router/src/routes/ephemeral_key.rs +++ b/crates/router/src/routes/ephemeral_key.rs @@ -1,11 +1,7 @@ -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use actix_web::{web, HttpRequest, HttpResponse}; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use router_env::{instrument, tracing, Flow}; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use super::AppState; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use crate::{ core::{api_locking, payments::helpers}, services::{api, authentication as auth}, @@ -38,7 +34,35 @@ pub async fn ephemeral_key_create( .await } -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyCreate))] +pub async fn ephemeral_key_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::EphemeralKeyCreate; + let payload = json_payload.into_inner(); + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, payload, _| { + helpers::make_ephemeral_key( + state, + payload.customer_id.to_owned(), + auth.merchant_account, + auth.key_store, + req.headers(), + ) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + ) + .await +} + #[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyDelete))] pub async fn ephemeral_key_delete( state: web::Data, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index c27a25677af2..729a35aa8b7a 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -141,8 +141,8 @@ pub async fn confirm_payment_method_intent_api( let pm_id = path.into_inner(); let payload = json_payload.into_inner(); - let (auth, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { - Ok((auth, _auth_flow)) => (auth, _auth_flow), + let auth = match auth::is_ephemeral_or_publishible_auth(req.headers()) { + Ok(auth) => auth, Err(e) => return api::log_and_return_error_response(e), }; @@ -150,7 +150,6 @@ pub async fn confirm_payment_method_intent_api( id: pm_id.clone(), payment_method_type: payload.payment_method_type, payment_method_subtype: payload.payment_method_subtype, - client_secret: payload.client_secret.clone(), customer_id: payload.customer_id.to_owned(), payment_method_data: payload.payment_method_data.clone(), }; @@ -191,6 +190,11 @@ pub async fn payment_method_update_api( let payment_method_id = path.into_inner(); let payload = json_payload.into_inner(); + let auth = match auth::is_ephemeral_or_publishible_auth(req.headers()) { + Ok(auth) => auth, + Err(e) => return api::log_and_return_error_response(e), + }; + Box::pin(api::server_wrap( flow, state, @@ -205,7 +209,7 @@ pub async fn payment_method_update_api( auth.key_store, ) }, - &auth::HeaderAuth(auth::ApiKeyAuth), + &*auth, api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 88608316640a..e5412c20fe29 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -13,7 +13,11 @@ use api_models::payouts; use api_models::{payment_methods::PaymentMethodListRequest, payments}; use async_trait::async_trait; use common_enums::TokenPurpose; +#[cfg(feature = "v2")] +use common_utils::fp_utils; use common_utils::{date_time, id_type}; +#[cfg(feature = "v2")] +use diesel_models::ephemeral_key; use error_stack::{report, ResultExt}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use masking::PeekInterface; @@ -1340,6 +1344,7 @@ where #[derive(Debug)] pub struct EphemeralKeyAuth; +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for EphemeralKeyAuth where @@ -1363,6 +1368,45 @@ where .await } } + +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for EphemeralKeyAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let api_key = + get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; + let ephemeral_key = state + .store() + .get_ephemeral_key(api_key) + .await + .change_context(errors::ApiErrorResponse::Unauthorized)?; + + let resource_type = HeaderMapStruct::new(request_headers) + .get_mandatory_header_value_by_key(headers::X_RESOURCE_TYPE) + .and_then(|val| { + ephemeral_key::ResourceType::from_str(val).change_context( + errors::ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid", headers::X_RESOURCE_TYPE), + }, + ) + })?; + + fp_utils::when(resource_type != ephemeral_key.resource_type, || { + Err(errors::ApiErrorResponse::Unauthorized) + })?; + + MerchantIdAuth(ephemeral_key.merchant_id) + .authenticate_and_fetch(request_headers, state) + .await + } +} #[derive(Debug)] pub struct MerchantIdAuth(pub id_type::MerchantId); @@ -2949,13 +2993,6 @@ impl ClientSecretFetch for PaymentMethodCreate { } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl ClientSecretFetch for PaymentMethodIntentConfirm { - fn get_client_secret(&self) -> Option<&String> { - Some(&self.client_secret) - } -} - impl ClientSecretFetch for api_models::cards_info::CardsInfoRequest { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() @@ -2980,6 +3017,7 @@ impl ClientSecretFetch for api_models::pm_auth::ExchangeTokenCreateRequest { } } +#[cfg(feature = "v1")] impl ClientSecretFetch for api_models::payment_methods::PaymentMethodUpdate { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() @@ -3070,6 +3108,20 @@ where } } +pub fn is_ephemeral_or_publishible_auth( + headers: &HeaderMap, +) -> RouterResult>> { + let api_key = get_api_key(headers)?; + + if api_key.starts_with("epk") { + Ok(Box::new(EphemeralKeyAuth)) + } else if api_key.starts_with("pk_") { + Ok(Box::new(HeaderAuth(PublishableKeyAuth))) + } else { + Ok(Box::new(HeaderAuth(ApiKeyAuth))) + } +} + pub fn is_ephemeral_auth( headers: &HeaderMap, ) -> RouterResult>> { diff --git a/crates/router/src/types/payment_methods.rs b/crates/router/src/types/payment_methods.rs index 1a6b9053dcb9..c40d6fedfe73 100644 --- a/crates/router/src/types/payment_methods.rs +++ b/crates/router/src/types/payment_methods.rs @@ -126,16 +126,6 @@ impl VaultingDataInterface for PaymentMethodVaultingData { } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub struct PaymentMethodClientSecret; - -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl PaymentMethodClientSecret { - pub fn generate(payment_method_id: &common_utils::id_type::GlobalPaymentMethodId) -> String { - todo!() - } -} - #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub struct SavedPMLPaymentsInfo { pub payment_intent: storage::PaymentIntent, @@ -155,7 +145,7 @@ pub struct VaultRetrieveRequest { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct VaultRetrieveResponse { - pub data: Secret, + pub data: PaymentMethodVaultingData, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] diff --git a/crates/router/src/types/storage/ephemeral_key.rs b/crates/router/src/types/storage/ephemeral_key.rs index 9927e16b3fad..c4b8e2ba701a 100644 --- a/crates/router/src/types/storage/ephemeral_key.rs +++ b/crates/router/src/types/storage/ephemeral_key.rs @@ -1 +1,18 @@ pub use diesel_models::ephemeral_key::{EphemeralKey, EphemeralKeyNew}; +#[cfg(feature = "v2")] +pub use diesel_models::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew, ResourceType}; + +#[cfg(feature = "v2")] +use crate::types::transformers::ForeignFrom; +#[cfg(feature = "v2")] +impl ForeignFrom for api_models::ephemeral_key::EphemeralKeyResponse { + fn foreign_from(from: EphemeralKeyType) -> Self { + Self { + customer_id: from.customer_id, + created_at: from.created_at, + expires: from.expires, + secret: from.secret, + id: from.id, + } + } +} From dc0a92dc108c91d6c5f998af417e382aa7a0d9f1 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:10:03 +0530 Subject: [PATCH 09/11] refactor(grpc): send `x-tenant-id` and `x-request-id` in grpc headers (#6904) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/common_utils/src/consts.rs | 3 ++ crates/external_services/src/grpc_client.rs | 52 +++++++++++++++++++ .../elimination_rate_client.rs | 19 +++++-- .../dynamic_routing/success_rate_client.rs | 19 +++++-- crates/router/src/core/payments/routing.rs | 1 + crates/router/src/core/routing.rs | 5 +- crates/router/src/core/routing/helpers.rs | 2 + crates/router/src/routes/app.rs | 11 +++- 8 files changed, 104 insertions(+), 8 deletions(-) diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 39e5483700a5..3b437b703bef 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -146,3 +146,6 @@ pub const CONNECTOR_TRANSACTION_ID_HASH_BYTES: usize = 25; /// Apple Pay validation url pub const APPLEPAY_VALIDATION_URL: &str = "https://apple-pay-gateway-cert.apple.com/paymentservices/startSession"; + +/// Request ID +pub const X_REQUEST_ID: &str = "x-request-id"; diff --git a/crates/external_services/src/grpc_client.rs b/crates/external_services/src/grpc_client.rs index 8981a1094d69..404685025ed3 100644 --- a/crates/external_services/src/grpc_client.rs +++ b/crates/external_services/src/grpc_client.rs @@ -6,6 +6,8 @@ pub mod dynamic_routing; pub mod health_check_client; use std::{fmt::Debug, sync::Arc}; +#[cfg(feature = "dynamic_routing")] +use common_utils::consts; #[cfg(feature = "dynamic_routing")] use dynamic_routing::{DynamicRoutingClientConfig, RoutingStrategy}; #[cfg(feature = "dynamic_routing")] @@ -16,6 +18,8 @@ use http_body_util::combinators::UnsyncBoxBody; use hyper::body::Bytes; #[cfg(feature = "dynamic_routing")] use hyper_util::client::legacy::connect::HttpConnector; +#[cfg(feature = "dynamic_routing")] +use router_env::logger; use serde; #[cfg(feature = "dynamic_routing")] use tonic::Status; @@ -76,3 +80,51 @@ impl GrpcClientSettings { }) } } + +/// Contains grpc headers +#[derive(Debug)] +pub struct GrpcHeaders { + /// Tenant id + pub tenant_id: String, + /// Request id + pub request_id: Option, +} + +#[cfg(feature = "dynamic_routing")] +/// Trait to add necessary headers to the tonic Request +pub(crate) trait AddHeaders { + /// Add necessary header fields to the tonic Request + fn add_headers_to_grpc_request(&mut self, headers: GrpcHeaders); +} + +#[cfg(feature = "dynamic_routing")] +impl AddHeaders for tonic::Request { + #[track_caller] + fn add_headers_to_grpc_request(&mut self, headers: GrpcHeaders) { + headers.tenant_id + .parse() + .map(|tenant_id| { + self + .metadata_mut() + .append(consts::TENANT_HEADER, tenant_id) + }) + .inspect_err( + |err| logger::warn!(header_parse_error=?err,"invalid {} received",consts::TENANT_HEADER), + ) + .ok(); + + headers.request_id.map(|request_id| { + request_id + .parse() + .map(|request_id| { + self + .metadata_mut() + .append(consts::X_REQUEST_ID, request_id) + }) + .inspect_err( + |err| logger::warn!(header_parse_error=?err,"invalid {} received",consts::X_REQUEST_ID), + ) + .ok(); + }); + } +} diff --git a/crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs b/crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs index 6587b7941f4c..bc5ce4997270 100644 --- a/crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs +++ b/crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs @@ -21,6 +21,7 @@ pub mod elimination_rate { } use super::{Client, DynamicRoutingError, DynamicRoutingResult}; +use crate::grpc_client::{AddHeaders, GrpcHeaders}; /// The trait Elimination Based Routing would have the functions required to support performance, calculation and invalidation bucket #[async_trait::async_trait] @@ -32,6 +33,7 @@ pub trait EliminationBasedRouting: dyn_clone::DynClone + Send + Sync { params: String, labels: Vec, configs: Option, + headers: GrpcHeaders, ) -> DynamicRoutingResult; /// To update the bucket size and ttl for list of connectors with its respective bucket name async fn update_elimination_bucket_config( @@ -40,11 +42,13 @@ pub trait EliminationBasedRouting: dyn_clone::DynClone + Send + Sync { params: String, report: Vec, config: Option, + headers: GrpcHeaders, ) -> DynamicRoutingResult; /// To invalidate the previous id's bucket async fn invalidate_elimination_bucket( &self, id: String, + headers: GrpcHeaders, ) -> DynamicRoutingResult; } @@ -56,6 +60,7 @@ impl EliminationBasedRouting for EliminationAnalyserClient { params: String, label_input: Vec, configs: Option, + headers: GrpcHeaders, ) -> DynamicRoutingResult { let labels = label_input .into_iter() @@ -64,13 +69,15 @@ impl EliminationBasedRouting for EliminationAnalyserClient { let config = configs.map(ForeignTryFrom::foreign_try_from).transpose()?; - let request = tonic::Request::new(EliminationRequest { + let mut request = tonic::Request::new(EliminationRequest { id, params, labels, config, }); + request.add_headers_to_grpc_request(headers); + let response = self .clone() .get_elimination_status(request) @@ -89,6 +96,7 @@ impl EliminationBasedRouting for EliminationAnalyserClient { params: String, report: Vec, configs: Option, + headers: GrpcHeaders, ) -> DynamicRoutingResult { let config = configs.map(ForeignTryFrom::foreign_try_from).transpose()?; @@ -102,13 +110,15 @@ impl EliminationBasedRouting for EliminationAnalyserClient { }) .collect::>(); - let request = tonic::Request::new(UpdateEliminationBucketRequest { + let mut request = tonic::Request::new(UpdateEliminationBucketRequest { id, params, labels_with_bucket_name, config, }); + request.add_headers_to_grpc_request(headers); + let response = self .clone() .update_elimination_bucket(request) @@ -122,8 +132,11 @@ impl EliminationBasedRouting for EliminationAnalyserClient { async fn invalidate_elimination_bucket( &self, id: String, + headers: GrpcHeaders, ) -> DynamicRoutingResult { - let request = tonic::Request::new(InvalidateBucketRequest { id }); + let mut request = tonic::Request::new(InvalidateBucketRequest { id }); + + request.add_headers_to_grpc_request(headers); let response = self .clone() diff --git a/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs b/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs index f6d3efb88769..3cf06ab63beb 100644 --- a/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs +++ b/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs @@ -21,6 +21,7 @@ pub mod success_rate { tonic::include_proto!("success_rate"); } use super::{Client, DynamicRoutingError, DynamicRoutingResult}; +use crate::grpc_client::{AddHeaders, GrpcHeaders}; /// The trait Success Based Dynamic Routing would have the functions required to support the calculation and updation window #[async_trait::async_trait] pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { @@ -31,6 +32,7 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { success_rate_based_config: SuccessBasedRoutingConfig, params: String, label_input: Vec, + headers: GrpcHeaders, ) -> DynamicRoutingResult; /// To update the success rate with the given label async fn update_success_rate( @@ -39,11 +41,13 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { success_rate_based_config: SuccessBasedRoutingConfig, params: String, response: Vec, + headers: GrpcHeaders, ) -> DynamicRoutingResult; /// To invalidates the success rate routing keys async fn invalidate_success_rate_routing_keys( &self, id: String, + headers: GrpcHeaders, ) -> DynamicRoutingResult; } @@ -55,6 +59,7 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { success_rate_based_config: SuccessBasedRoutingConfig, params: String, label_input: Vec, + headers: GrpcHeaders, ) -> DynamicRoutingResult { let labels = label_input .into_iter() @@ -66,13 +71,15 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { .map(ForeignTryFrom::foreign_try_from) .transpose()?; - let request = tonic::Request::new(CalSuccessRateRequest { + let mut request = tonic::Request::new(CalSuccessRateRequest { id, params, labels, config, }); + request.add_headers_to_grpc_request(headers); + let response = self .clone() .fetch_success_rate(request) @@ -91,6 +98,7 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { success_rate_based_config: SuccessBasedRoutingConfig, params: String, label_input: Vec, + headers: GrpcHeaders, ) -> DynamicRoutingResult { let config = success_rate_based_config .config @@ -105,13 +113,15 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { }) .collect(); - let request = tonic::Request::new(UpdateSuccessRateWindowRequest { + let mut request = tonic::Request::new(UpdateSuccessRateWindowRequest { id, params, labels_with_status, config, }); + request.add_headers_to_grpc_request(headers); + let response = self .clone() .update_success_rate_window(request) @@ -126,8 +136,11 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { async fn invalidate_success_rate_routing_keys( &self, id: String, + headers: GrpcHeaders, ) -> DynamicRoutingResult { - let request = tonic::Request::new(InvalidateWindowsRequest { id }); + let mut request = tonic::Request::new(InvalidateWindowsRequest { id }); + + request.add_headers_to_grpc_request(headers); let response = self .clone() diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 746b6b7fb409..6d3c0973283c 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -1343,6 +1343,7 @@ pub async fn perform_success_based_routing( success_based_routing_configs, success_based_routing_config_params, routable_connectors, + state.get_grpc_headers(), ) .await .change_context(errors::RoutingError::SuccessRateCalculationError) diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 36bcb233d02a..5fc54fe96e52 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -1408,7 +1408,10 @@ pub async fn success_based_routing_update_configs( .as_ref() .async_map(|sr_client| async { sr_client - .invalidate_success_rate_routing_keys(prefix_of_dynamic_routing_keys) + .invalidate_success_rate_routing_keys( + prefix_of_dynamic_routing_keys, + state.get_grpc_headers(), + ) .await .change_context(errors::ApiErrorResponse::GenericNotFoundError { message: "Failed to invalidate the routing keys".to_string(), diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 2abb5e64cd4b..489c3122a8e9 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -719,6 +719,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( success_based_routing_configs.clone(), success_based_routing_config_params.clone(), routable_connectors.clone(), + state.get_grpc_headers(), ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -855,6 +856,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( }, payment_status_attribute == common_enums::AttemptStatus::Charged, )], + state.get_grpc_headers(), ) .await .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0b5d481e3da3..2decddb806cb 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -12,7 +12,10 @@ use common_utils::id_type; use external_services::email::{ no_email::NoEmailClient, ses::AwsSes, smtp::SmtpServer, EmailClientConfigs, EmailService, }; -use external_services::{file_storage::FileStorageInterface, grpc_client::GrpcClients}; +use external_services::{ + file_storage::FileStorageInterface, + grpc_client::{GrpcClients, GrpcHeaders}, +}; use hyperswitch_interfaces::{ encryption_interface::EncryptionManagementInterface, secrets_interface::secret_state::{RawSecret, SecuredSecret}, @@ -119,6 +122,12 @@ impl SessionState { event_context: events::EventContext::new(self.event_handler.clone()), } } + pub fn get_grpc_headers(&self) -> GrpcHeaders { + GrpcHeaders { + tenant_id: self.tenant.tenant_id.get_string_repr().to_string(), + request_id: self.request_id.map(|req_id| (*req_id).to_string()), + } + } } pub trait SessionStateInfo { From adcddd643c002a5fe3e7c50c0f78fa5a46f210e7 Mon Sep 17 00:00:00 2001 From: Sayak Bhattacharya Date: Sun, 22 Dec 2024 23:13:11 +0530 Subject: [PATCH 10/11] feat(connector): [JPMORGAN] add Payment flows for cards (#6668) Co-authored-by: Sayak Bhattacharya --- api-reference-v2/openapi_spec.json | 2 + api-reference/openapi_spec.json | 2 + config/config.example.toml | 1 + config/deployments/integration_test.toml | 5 + config/deployments/production.toml | 5 + config/deployments/sandbox.toml | 5 + config/development.toml | 5 + config/docker_compose.toml | 1 + crates/api_models/src/connector_enums.rs | 5 +- crates/common_enums/src/connector_enums.rs | 2 +- crates/connector_configs/src/connector.rs | 2 + .../connector_configs/toml/development.toml | 41 ++ crates/connector_configs/toml/production.toml | 40 + crates/connector_configs/toml/sandbox.toml | 40 + .../src/connectors/jpmorgan.rs | 442 ++++++++--- .../src/connectors/jpmorgan/transformers.rs | 686 +++++++++++++++--- .../hyperswitch_connectors/src/constants.rs | 1 + crates/router/src/core/admin.rs | 4 + crates/router/src/types/api.rs | 4 +- crates/router/src/types/transformers.rs | 2 +- .../router/tests/connectors/sample_auth.toml | 3 +- crates/test_utils/src/connector_auth.rs | 2 +- .../cypress/e2e/PaymentUtils/Jpmorgan.js | 225 ++++++ .../cypress/e2e/PaymentUtils/Utils.js | 2 + loadtest/config/development.toml | 1 + 25 files changed, 1341 insertions(+), 187 deletions(-) create mode 100644 cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 14e624233c36..20ec4157f566 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6557,6 +6557,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", @@ -19423,6 +19424,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index c7527405a5f2..2aa19e4e223c 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -9276,6 +9276,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", @@ -23842,6 +23843,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", diff --git a/config/config.example.toml b/config/config.example.toml index 4d9950226451..11e8bed7dacd 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -223,6 +223,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url= "https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 56dd0ef7451e..7fdc0c6d5e83 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -65,6 +65,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -298,6 +299,10 @@ debit.currency = "USD" ali_pay.currency = "GBP,CNY" we_chat_pay.currency = "GBP,CNY" +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 464f9bcba32e..9c91c12f3b43 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -69,6 +69,7 @@ iatapay.base_url = "https://iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://secure.api.itau/" jpmorgan.base_url = "https://api-ms.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.klarna.com/" mifinity.base_url = "https://secure.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -332,6 +333,10 @@ debit.currency = "USD" ali_pay.currency = "GBP,CNY" we_chat_pay.currency = "GBP,CNY" +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index fb210a28862c..152f4f04cd89 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -69,6 +69,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -333,6 +334,10 @@ debit.currency = "USD" ali_pay.currency = "GBP,CNY" we_chat_pay.currency = "GBP,CNY" +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } diff --git a/config/development.toml b/config/development.toml index 7008af1f28a2..133823d6cb37 100644 --- a/config/development.toml +++ b/config/development.toml @@ -241,6 +241,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url= "https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -491,6 +492,10 @@ paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,N credit = { currency = "USD" } debit = { currency = "USD" } +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,EUR,EUR,CAD,CZK,DKK,EUR,EUR,EUR,EUR,EUR,EUR,EUR,NZD,NOK,PLN,EUR,EUR,SEK,CHF,GBP,USD" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index b861e47e5945..1ad2ef91336c 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -154,6 +154,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 0989614c8a04..3a3fe5e8f429 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -88,7 +88,7 @@ pub enum Connector { // Inespay, Iatapay, Itaubank, - //Jpmorgan, + Jpmorgan, Klarna, Mifinity, Mollie, @@ -175,6 +175,7 @@ impl Connector { (Self::Airwallex, _) | (Self::Deutschebank, _) | (Self::Globalpay, _) + | (Self::Jpmorgan, _) | (Self::Paypal, _) | (Self::Payu, _) | (Self::Trustpay, PaymentMethod::BankRedirect) @@ -234,7 +235,7 @@ impl Connector { | Self::Iatapay // | Self::Inespay | Self::Itaubank - //| Self::Jpmorgan + | Self::Jpmorgan | Self::Klarna | Self::Mifinity | Self::Mollie diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 1776ade36661..127d5a9901c6 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -83,7 +83,7 @@ pub enum RoutableConnectors { Iatapay, // Inespay, Itaubank, - //Jpmorgan, + Jpmorgan, Klarna, Mifinity, Mollie, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 1af99f110208..985a6d7b547f 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -198,6 +198,7 @@ pub struct ConnectorConfig { pub gpayments: Option, pub helcim: Option, // pub inespay: Option, + pub jpmorgan: Option, pub klarna: Option, pub mifinity: Option, pub mollie: Option, @@ -370,6 +371,7 @@ impl ConnectorConfig { Connector::Gpayments => Ok(connector_data.gpayments), Connector::Helcim => Ok(connector_data.helcim), // Connector::Inespay => Ok(connector_data.inespay), + Connector::Jpmorgan => Ok(connector_data.jpmorgan), Connector::Klarna => Ok(connector_data.klarna), Connector::Mifinity => Ok(connector_data.mifinity), Connector::Mollie => Ok(connector_data.mollie), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index fcf0dca100cb..f49f6b311f6e 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -1731,6 +1731,47 @@ api_key="Client Secret" api_secret="Certificates" key2="Certificate Key" +[jpmorgan] +[[jpmorgan.credit]] + payment_method_type = "American Express" +[[jpmorgan.credit]] + payment_method_type = "ChaseNet" +[[jpmorgan.credit]] + payment_method_type = "Diners Club" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "JCB" +[[jpmorgan.credit]] + payment_method_type = "Mastercard" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "UnionPay" +[[jpmorgan.credit]] + payment_method_type = "Visa" + [[jpmorgan.debit]] + payment_method_type = "American Express" +[[jpmorgan.debit]] + payment_method_type = "ChaseNet" +[[jpmorgan.debit]] + payment_method_type = "Diners Club" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "JCB" +[[jpmorgan.debit]] + payment_method_type = "Mastercard" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "UnionPay" +[[jpmorgan.debit]] + payment_method_type = "Visa" +[jpmorgan.connector_auth.BodyKey] +api_key="Client ID" +key1="Client Secret" + [klarna] [[klarna.pay_later]] payment_method_type = "klarna" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 443d3fd72ac7..7216d4f7890b 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1443,6 +1443,46 @@ api_key="Client Secret" api_secret="Certificates" key2="Certificate Key" +[jpmorgan] +[[jpmorgan.credit]] + payment_method_type = "American Express" +[[jpmorgan.credit]] + payment_method_type = "ChaseNet" +[[jpmorgan.credit]] + payment_method_type = "Diners Club" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "JCB" +[[jpmorgan.credit]] + payment_method_type = "Mastercard" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "UnionPay" +[[jpmorgan.credit]] + payment_method_type = "Visa" + [[jpmorgan.debit]] + payment_method_type = "American Express" +[[jpmorgan.debit]] + payment_method_type = "ChaseNet" +[[jpmorgan.debit]] + payment_method_type = "Diners Club" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "JCB" +[[jpmorgan.debit]] + payment_method_type = "Mastercard" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "UnionPay" +[[jpmorgan.debit]] + payment_method_type = "Visa" +[jpmorgan.connector_auth.BodyKey] +api_key="Access Token" + [klarna] [[klarna.pay_later]] payment_method_type = "klarna" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 4d3f6ede1403..4c6ee7566f31 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -1681,6 +1681,46 @@ api_key="Client Secret" api_secret="Certificates" key2="Certificate Key" +[jpmorgan] +[[jpmorgan.credit]] + payment_method_type = "American Express" +[[jpmorgan.credit]] + payment_method_type = "ChaseNet" +[[jpmorgan.credit]] + payment_method_type = "Diners Club" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "JCB" +[[jpmorgan.credit]] + payment_method_type = "Mastercard" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "UnionPay" +[[jpmorgan.credit]] + payment_method_type = "Visa" + [[jpmorgan.debit]] + payment_method_type = "American Express" +[[jpmorgan.debit]] + payment_method_type = "ChaseNet" +[[jpmorgan.debit]] + payment_method_type = "Diners Club" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "JCB" +[[jpmorgan.debit]] + payment_method_type = "Mastercard" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "UnionPay" +[[jpmorgan.debit]] + payment_method_type = "Visa" +[jpmorgan.connector_auth.BodyKey] +api_key="Access Token" + [klarna] [[klarna.pay_later]] payment_method_type = "klarna" diff --git a/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs b/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs index 6095a53ad00b..9e5a24faab5f 100644 --- a/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs +++ b/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs @@ -1,14 +1,15 @@ pub mod transformers; - +use base64::Engine; +use common_enums::enums; use common_utils::{ errors::CustomResult, ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, MinorUnit, MinorUnitForConnector}, }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ - router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_data::{AccessToken, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, @@ -21,33 +22,36 @@ use hyperswitch_domain_models::{ }, router_response_types::{PaymentsResponseData, RefundsResponseData}, types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, }, }; -// use hyperswitch_interfaces::{ api::{self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorValidation}, configs::Connectors, - errors, + consts, errors, events::connector_api_logs::ConnectorEvent, - types::{self, Response}, + types::{self, RefreshTokenType, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; -use transformers as jpmorgan; +use masking::{Mask, Maskable, PeekInterface}; +use transformers::{self as jpmorgan, JpmorganErrorResponse}; -use crate::{constants::headers, types::ResponseRouterData, utils}; +use crate::{ + constants::headers, + types::{RefreshTokenRouterData, ResponseRouterData}, + utils, +}; #[derive(Clone)] pub struct Jpmorgan { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl Jpmorgan { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &MinorUnitForConnector, } } } @@ -79,14 +83,38 @@ where &self, req: &RouterData, _connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( + ) -> CustomResult)>, errors::ConnectorError> { + let mut headers = vec![( headers::CONTENT_TYPE.to_string(), self.get_content_type().to_string().into(), )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + let auth_header = ( + headers::AUTHORIZATION.to_string(), + format!( + "Bearer {}", + req.access_token + .clone() + .ok_or(errors::ConnectorError::FailedToObtainAuthType)? + .token + .peek() + ) + .into_masked(), + ); + let request_id = ( + headers::REQUEST_ID.to_string(), + req.connector_request_reference_id + .clone() + .to_string() + .into_masked(), + ); + let merchant_id = ( + headers::MERCHANT_ID.to_string(), + req.merchant_id.get_string_repr().to_string().into_masked(), + ); + headers.push(auth_header); + headers.push(request_id); + headers.push(merchant_id); + Ok(headers) } } @@ -96,11 +124,7 @@ impl ConnectorCommon for Jpmorgan { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Base - //todo!() - // TODO! Check connector documentation, on which unit they are processing the currency. - // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, - // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base + api::CurrencyUnit::Minor } fn common_get_content_type(&self) -> &'static str { @@ -111,36 +135,29 @@ impl ConnectorCommon for Jpmorgan { connectors.jpmorgan.base_url.as_ref() } - fn get_auth_header( - &self, - auth_type: &ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = jpmorgan::JpmorganAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) - } - fn build_error_response( &self, res: Response, event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { - let response: jpmorgan::JpmorganErrorResponse = res + let response: JpmorganErrorResponse = res .response .parse_struct("JpmorganErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); + event_builder.map(|i| i.set_response_body(&response)); + + let response_message = response + .response_message + .as_ref() + .map_or_else(|| consts::NO_ERROR_MESSAGE.to_string(), ToString::to_string); Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: response.response_code, + message: response_message.clone(), + reason: Some(response_message), attempt_status: None, connector_transaction_id: None, }) @@ -148,14 +165,150 @@ impl ConnectorCommon for Jpmorgan { } impl ConnectorValidation for Jpmorgan { - //TODO: implement functions when support enabled + fn validate_capture_method( + &self, + capture_method: Option, + _pmt: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Automatic | enums::CaptureMethod::Manual => Ok(()), + enums::CaptureMethod::ManualMultiple + | enums::CaptureMethod::Scheduled + | enums::CaptureMethod::SequentialAutomatic => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Jpmorgan"), + ))? + } + } + } + + fn validate_psync_reference_id( + &self, + data: &PaymentsSyncData, + _is_three_ds: bool, + _status: enums::AttemptStatus, + _connector_meta_data: Option, + ) -> CustomResult<(), errors::ConnectorError> { + if data.encoded_data.is_some() + || data + .connector_transaction_id + .get_connector_transaction_id() + .is_ok() + { + return Ok(()); + } + Err(errors::ConnectorError::MissingConnectorTransactionID.into()) + } } impl ConnectorIntegration for Jpmorgan { //TODO: implement sessions flow } -impl ConnectorIntegration for Jpmorgan {} +impl ConnectorIntegration for Jpmorgan { + fn get_headers( + &self, + req: &RefreshTokenRouterData, + _connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let client_id = req.request.app_id.clone(); + + let client_secret = req.request.id.clone(); + + let creds = format!( + "{}:{}", + client_id.peek(), + client_secret.unwrap_or_default().peek() + ); + let encoded_creds = common_utils::consts::BASE64_ENGINE.encode(creds); + + let auth_string = format!("Basic {}", encoded_creds); + Ok(vec![ + ( + headers::CONTENT_TYPE.to_string(), + RefreshTokenType::get_content_type(self).to_string().into(), + ), + ( + headers::AUTHORIZATION.to_string(), + auth_string.into_masked(), + ), + ]) + } + + fn get_content_type(&self) -> &'static str { + "application/x-www-form-urlencoded" + } + + fn get_url( + &self, + _req: &RefreshTokenRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!( + "{}/am/oauth2/alpha/access_token", + connectors + .jpmorgan + .secondary_base_url + .as_ref() + .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)? + )) + } + + fn get_request_body( + &self, + req: &RefreshTokenRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = jpmorgan::JpmorganAuthUpdateRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &RefreshTokenRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .attach_default_headers() + .headers(RefreshTokenType::get_headers(self, req, connectors)?) + .url(&RefreshTokenType::get_url(self, req, connectors)?) + .set_body(RefreshTokenType::get_request_body(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &RefreshTokenRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: jpmorgan::JpmorganAuthUpdateResponse = res + .response + .parse_struct("jpmorgan JpmorganAuthUpdateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} impl ConnectorIntegration for Jpmorgan @@ -167,7 +320,7 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -178,9 +331,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/payments", self.base_url(connectors))) } fn get_request_body( @@ -188,7 +341,7 @@ impl ConnectorIntegration CustomResult { - let amount = utils::convert_amount( + let amount: MinorUnit = utils::convert_amount( self.amount_converter, req.request.minor_amount, req.request.currency, @@ -249,12 +402,102 @@ impl ConnectorIntegration for Jpmorgan { + fn get_headers( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult { + let tid = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}/payments/{}/captures", + self.base_url(connectors), + tid + )) + } + + fn get_request_body( + &self, + req: &PaymentsCaptureRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let amount: MinorUnit = utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, + req.request.currency, + )?; + + let connector_router_data = jpmorgan::JpmorganRouterData::from((amount, req)); + let connector_req = jpmorgan::JpmorganCaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsCaptureRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: jpmorgan::JpmorganPaymentsResponse = res + .response + .parse_struct("Jpmorgan PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + impl ConnectorIntegration for Jpmorgan { fn get_headers( &self, req: &PaymentsSyncRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -264,10 +507,15 @@ impl ConnectorIntegration for Jpm fn get_url( &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + req: &PaymentsSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let tid = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!("{}/payments/{}", self.base_url(connectors), tid)) } fn build_request( @@ -313,12 +561,12 @@ impl ConnectorIntegration for Jpm } } -impl ConnectorIntegration for Jpmorgan { +impl ConnectorIntegration for Jpmorgan { fn get_headers( &self, - req: &PaymentsCaptureRouterData, + req: &PaymentsCancelRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -328,34 +576,41 @@ impl ConnectorIntegration fo fn get_url( &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, + req: &PaymentsCancelRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let tid = req.request.connector_transaction_id.clone(); + Ok(format!("{}/payments/{}", self.base_url(connectors), tid)) } fn get_request_body( &self, - _req: &PaymentsCaptureRouterData, + req: &PaymentsCancelRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let amount: MinorUnit = utils::convert_amount( + self.amount_converter, + req.request.minor_amount.unwrap_or_default(), + req.request.currency.unwrap_or_default(), + )?; + + let connector_router_data = jpmorgan::JpmorganRouterData::from((amount, req)); + let connector_req = jpmorgan::JpmorganCancelRequest::try_from(connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, - req: &PaymentsCaptureRouterData, + req: &PaymentsCancelRouterData, connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() - .method(Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .method(Method::Patch) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCaptureType::get_request_body( + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(), @@ -364,14 +619,15 @@ impl ConnectorIntegration fo fn handle_response( &self, - data: &PaymentsCaptureRouterData, + data: &PaymentsCancelRouterData, event_builder: Option<&mut ConnectorEvent>, res: Response, - ) -> CustomResult { - let response: jpmorgan::JpmorganPaymentsResponse = res + ) -> CustomResult { + let response: jpmorgan::JpmorganCancelResponse = res .response - .parse_struct("Jpmorgan PaymentsCaptureResponse") + .parse_struct("JpmrorganPaymentsVoidResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -390,14 +646,12 @@ impl ConnectorIntegration fo } } -impl ConnectorIntegration for Jpmorgan {} - impl ConnectorIntegration for Jpmorgan { fn get_headers( &self, req: &RefundsRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -410,7 +664,7 @@ impl ConnectorIntegration for Jpmorga _req: &RefundsRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Err(errors::ConnectorError::NotImplemented("Refunds".to_string()).into()) } fn get_request_body( @@ -434,18 +688,19 @@ impl ConnectorIntegration for Jpmorga req: &RefundsRouterData, connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { - let request = RequestBuilder::new() - .method(Method::Post) - .url(&types::RefundExecuteType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundExecuteType::get_headers( - self, req, connectors, - )?) - .set_body(types::RefundExecuteType::get_request_body( - self, req, connectors, - )?) - .build(); - Ok(Some(request)) + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .set_body(types::RefundExecuteType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } fn handle_response( @@ -454,9 +709,9 @@ impl ConnectorIntegration for Jpmorga event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: jpmorgan::RefundResponse = res + let response: jpmorgan::JpmorganRefundResponse = res .response - .parse_struct("jpmorgan RefundResponse") + .parse_struct("JpmorganRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -481,22 +736,21 @@ impl ConnectorIntegration for Jpmorgan &self, req: &RefundSyncRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { self.common_get_content_type() } - fn get_url( &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, + req: &RefundSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let tid = req.request.connector_transaction_id.clone(); + Ok(format!("{}/refunds/{}", self.base_url(connectors), tid)) } - fn build_request( &self, req: &RefundSyncRouterData, @@ -521,7 +775,7 @@ impl ConnectorIntegration for Jpmorgan event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: jpmorgan::RefundResponse = res + let response: jpmorgan::JpmorganRefundSyncResponse = res .response .parse_struct("jpmorgan RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; diff --git a/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs b/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs index 90b058697051..78c1b5eefb5f 100644 --- a/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs @@ -1,12 +1,15 @@ -use common_enums::enums; -use common_utils::types::StringMinorUnit; +use common_enums::enums::CaptureMethod; +use common_utils::types::MinorUnit; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, + router_data::{AccessToken, ConnectorAuthType, RouterData}, router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, + router_request_types::{PaymentsCancelData, ResponseId}, router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + RefreshTokenRouterData, RefundsRouterData, + }, }; use hyperswitch_interfaces::errors; use masking::Secret; @@ -14,18 +17,17 @@ use serde::{Deserialize, Serialize}; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + utils::{ + get_unimplemented_payment_method_error_message, CardData, RouterData as OtherRouterData, + }, }; - -//TODO: Fill the struct with respective fields pub struct JpmorganRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: MinorUnit, pub router_data: T, } -impl From<(StringMinorUnit, T)> for JpmorganRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts +impl From<(MinorUnit, T)> for JpmorganRouterData { + fn from((amount, item): (MinorUnit, T)) -> Self { Self { amount, router_data: item, @@ -33,20 +35,102 @@ impl From<(StringMinorUnit, T)> for JpmorganRouterData { } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] +#[derive(Debug, Clone, Serialize)] +pub struct JpmorganAuthUpdateRequest { + pub grant_type: String, + pub scope: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JpmorganAuthUpdateResponse { + pub access_token: Secret, + pub scope: String, + pub token_type: String, + pub expires_in: i64, +} + +impl TryFrom<&RefreshTokenRouterData> for JpmorganAuthUpdateRequest { + type Error = error_stack::Report; + fn try_from(_item: &RefreshTokenRouterData) -> Result { + Ok(Self { + grant_type: String::from("client_credentials"), + scope: String::from("jpm:payments:sandbox"), + }) + } +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(AccessToken { + token: item.response.access_token, + expires: item.response.expires_in, + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JpmorganPaymentsRequest { - amount: StringMinorUnit, - card: JpmorganCard, + capture_method: CapMethod, + amount: MinorUnit, + currency: common_enums::Currency, + merchant: JpmorganMerchant, + payment_method_type: JpmorganPaymentMethodType, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JpmorganCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, + account_number: Secret, + expiry: Expiry, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganPaymentMethodType { + card: JpmorganCard, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Expiry { + month: Secret, + year: Secret, +} + +#[derive(Serialize, Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganMerchantSoftware { + company_name: Secret, + product_name: Secret, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganMerchant { + merchant_software: JpmorganMerchantSoftware, +} + +fn map_capture_method( + capture_method: CaptureMethod, +) -> Result> { + match capture_method { + CaptureMethod::Automatic => Ok(CapMethod::Now), + CaptureMethod::Manual => Ok(CapMethod::Manual), + CaptureMethod::Scheduled + | CaptureMethod::ManualMultiple + | CaptureMethod::SequentialAutomatic => { + Err(errors::ConnectorError::NotImplemented("Capture Method".to_string()).into()) + } + } } impl TryFrom<&JpmorganRouterData<&PaymentsAuthorizeRouterData>> for JpmorganPaymentsRequest { @@ -56,84 +140,226 @@ impl TryFrom<&JpmorganRouterData<&PaymentsAuthorizeRouterData>> for JpmorganPaym ) -> Result { match item.router_data.request.payment_method_data.clone() { PaymentMethodData::Card(req_card) => { + if item.router_data.is_three_ds() { + return Err(errors::ConnectorError::NotSupported { + message: "3DS payments".to_string(), + connector: "Jpmorgan", + } + .into()); + } + + let capture_method = + map_capture_method(item.router_data.request.capture_method.unwrap_or_default()); + + let merchant_software = JpmorganMerchantSoftware { + company_name: String::from("JPMC").into(), + product_name: String::from("Hyperswitch").into(), + }; + + let merchant = JpmorganMerchant { merchant_software }; + + let expiry: Expiry = Expiry { + month: req_card.card_exp_month.clone(), + year: req_card.get_expiry_year_4_digit(), + }; + + let account_number = Secret::new(req_card.card_number.to_string()); + let card = JpmorganCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, + account_number, + expiry, }; + + let payment_method_type = JpmorganPaymentMethodType { card }; + Ok(Self { - amount: item.amount.clone(), - card, + capture_method: capture_method?, + currency: item.router_data.request.currency, + amount: item.amount, + merchant, + payment_method_type, }) } - _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), + PaymentMethodData::CardDetailsForNetworkTransactionId(_) + | PaymentMethodData::CardRedirect(_) + | PaymentMethodData::Wallet(_) + | PaymentMethodData::PayLater(_) + | PaymentMethodData::BankRedirect(_) + | PaymentMethodData::BankDebit(_) + | PaymentMethodData::BankTransfer(_) + | PaymentMethodData::Crypto(_) + | PaymentMethodData::MandatePayment + | PaymentMethodData::Reward + | PaymentMethodData::RealTimePayment(_) + | PaymentMethodData::MobilePayment(_) + | PaymentMethodData::Upi(_) + | PaymentMethodData::Voucher(_) + | PaymentMethodData::GiftCard(_) + | PaymentMethodData::OpenBanking(_) + | PaymentMethodData::CardToken(_) + | PaymentMethodData::NetworkToken(_) => Err(errors::ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("jpmorgan"), + ) + .into()), } } } -//TODO: Fill the struct with respective fields -// Auth Struct +//JP Morgan uses access token only due to which we aren't reading the fields in this struct +#[derive(Debug)] pub struct JpmorganAuthType { - pub(super) api_key: Secret, + pub(super) _api_key: Secret, + pub(super) _key1: Secret, } impl TryFrom<&ConnectorAuthType> for JpmorganAuthType { type Error = error_stack::Report; fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), + ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + _api_key: api_key.to_owned(), + _key1: key1.to_owned(), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum JpmorganPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum JpmorganTransactionStatus { + Success, + Denied, + Error, } -impl From for common_enums::AttemptStatus { - fn from(item: JpmorganPaymentStatus) -> Self { - match item { - JpmorganPaymentStatus::Succeeded => Self::Charged, - JpmorganPaymentStatus::Failed => Self::Failure, - JpmorganPaymentStatus::Processing => Self::Authorizing, - } - } +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "UPPERCASE")] +pub enum JpmorganTransactionState { + Closed, + Authorized, + Voided, + #[default] + Pending, + Declined, + Error, } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct JpmorganPaymentsResponse { - status: JpmorganPaymentStatus, - id: String, + transaction_id: String, + request_id: String, + transaction_state: JpmorganTransactionState, + response_status: String, + response_code: String, + response_message: String, + payment_method_type: PaymentMethodType, + capture_method: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Merchant { + merchant_id: Option, + merchant_software: MerchantSoftware, + merchant_category_code: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantSoftware { + company_name: Secret, + product_name: Secret, + version: Option>, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentMethodType { + card: Option, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Card { + expiry: Option, + card_type: Option>, + card_type_name: Option>, + masked_account_number: Option>, + card_type_indicators: Option, + network_response: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkResponse { + address_verification_result: Option>, + address_verification_result_code: Option>, + card_verification_result_code: Option>, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExpiryResponse { + month: Option>, + year: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CardTypeIndicators { + issuance_country_code: Option>, + is_durbin_regulated: Option, + card_product_types: Secret>, +} + +pub fn attempt_status_from_transaction_state( + transaction_state: JpmorganTransactionState, +) -> common_enums::AttemptStatus { + match transaction_state { + JpmorganTransactionState::Authorized => common_enums::AttemptStatus::Authorized, + JpmorganTransactionState::Closed => common_enums::AttemptStatus::Charged, + JpmorganTransactionState::Declined | JpmorganTransactionState::Error => { + common_enums::AttemptStatus::Failure + } + JpmorganTransactionState::Pending => common_enums::AttemptStatus::Pending, + JpmorganTransactionState::Voided => common_enums::AttemptStatus::Voided, + } } impl TryFrom> for RouterData { type Error = error_stack::Report; + fn try_from( item: ResponseRouterData, ) -> Result { + let transaction_state = match item.response.transaction_state { + JpmorganTransactionState::Closed => match item.response.capture_method { + Some(CapMethod::Now) => JpmorganTransactionState::Closed, + _ => JpmorganTransactionState::Authorized, + }, + JpmorganTransactionState::Authorized => JpmorganTransactionState::Authorized, + JpmorganTransactionState::Voided => JpmorganTransactionState::Voided, + JpmorganTransactionState::Pending => JpmorganTransactionState::Pending, + JpmorganTransactionState::Declined => JpmorganTransactionState::Declined, + JpmorganTransactionState::Error => JpmorganTransactionState::Error, + }; + let status = attempt_status_from_transaction_state(transaction_state); + Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), + status, response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), redirection_data: Box::new(None), mandate_reference: Box::new(None), connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), incremental_authorization_allowed: None, charge_id: None, }), @@ -142,24 +368,179 @@ impl TryFrom, + amount: MinorUnit, + currency: Option, } -impl TryFrom<&JpmorganRouterData<&RefundsRouterData>> for JpmorganRefundRequest { +#[derive(Debug, Default, Copy, Serialize, Deserialize, Clone)] +#[serde(rename_all = "UPPERCASE")] +pub enum CapMethod { + #[default] + Now, + Delayed, + Manual, +} + +impl TryFrom<&JpmorganRouterData<&PaymentsCaptureRouterData>> for JpmorganCaptureRequest { + type Error = error_stack::Report; + fn try_from( + item: &JpmorganRouterData<&PaymentsCaptureRouterData>, + ) -> Result { + let capture_method = Some(map_capture_method( + item.router_data.request.capture_method.unwrap_or_default(), + )?); + Ok(Self { + capture_method, + amount: item.amount, + currency: Some(item.router_data.request.currency), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganCaptureResponse { + pub transaction_id: String, + pub request_id: String, + pub transaction_state: JpmorganTransactionState, + pub response_status: JpmorganTransactionStatus, + pub response_code: String, + pub response_message: String, + pub payment_method_type: PaymentMethodTypeCapRes, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentMethodTypeCapRes { + pub card: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardCapRes { + pub card_type: Option>, + pub card_type_name: Option>, + unmasked_account_number: Option>, +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let status = attempt_status_from_transaction_state(item.response.transaction_state); + Ok(Self { + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganPSyncResponse { + transaction_id: String, + transaction_state: JpmorganTransactionState, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum JpmorganResponseStatus { + Success, + Denied, + Error, +} + +impl + TryFrom> + for RouterData +{ type Error = error_stack::Report; - fn try_from(item: &JpmorganRouterData<&RefundsRouterData>) -> Result { + fn try_from( + item: ResponseRouterData, + ) -> Result { + let status = attempt_status_from_transaction_state(item.response.transaction_state); Ok(Self { - amount: item.amount.to_owned(), + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data }) } } -// Type definition for Refund Response +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TransactionData { + payment_type: Option>, + status_code: Secret, + txn_secret: Option>, + tid: Option>, + test_mode: Option>, + status: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganRefundRequest { + pub merchant: MerchantRefundReq, + pub amount: MinorUnit, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantRefundReq { + pub merchant_software: MerchantSoftware, +} + +impl TryFrom<&JpmorganRouterData<&RefundsRouterData>> for JpmorganRefundRequest { + type Error = error_stack::Report; + fn try_from(_item: &JpmorganRouterData<&RefundsRouterData>) -> Result { + Err(errors::ConnectorError::NotImplemented("Refunds".to_string()).into()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganRefundResponse { + pub transaction_id: Option, + pub request_id: String, + pub transaction_state: JpmorganTransactionState, + pub amount: MinorUnit, + pub currency: common_enums::Currency, + pub response_status: JpmorganResponseStatus, + pub response_code: String, + pub response_message: String, + pub transaction_reference_id: Option, + pub remaining_refundable_amount: Option, +} #[allow(dead_code)] #[derive(Debug, Serialize, Default, Deserialize, Clone)] @@ -170,59 +551,192 @@ pub enum RefundStatus { Processing, } -impl From for enums::RefundStatus { +impl From for common_enums::RefundStatus { fn from(item: RefundStatus) -> Self { match item { RefundStatus::Succeeded => Self::Success, RefundStatus::Failed => Self::Failure, RefundStatus::Processing => Self::Pending, - //TODO: Review mapping } } } -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct RefundResponse { id: String, status: RefundStatus, } -impl TryFrom> for RefundsRouterData { +pub fn refund_status_from_transaction_state( + transaction_state: JpmorganTransactionState, +) -> common_enums::RefundStatus { + match transaction_state { + JpmorganTransactionState::Voided | JpmorganTransactionState::Closed => { + common_enums::RefundStatus::Success + } + JpmorganTransactionState::Declined | JpmorganTransactionState::Error => { + common_enums::RefundStatus::Failure + } + JpmorganTransactionState::Pending | JpmorganTransactionState::Authorized => { + common_enums::RefundStatus::Pending + } + } +} + +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item + .response + .transaction_id + .clone() + .ok_or(errors::ConnectorError::ResponseHandlingFailed)?, + refund_status: refund_status_from_transaction_state( + item.response.transaction_state, + ), }), ..item.data }) } } -impl TryFrom> for RefundsRouterData { +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganRefundSyncResponse { + transaction_id: String, + request_id: String, + transaction_state: JpmorganTransactionState, + amount: MinorUnit, + currency: common_enums::Currency, + response_status: JpmorganResponseStatus, + response_code: String, +} + +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.transaction_id.clone(), + refund_status: refund_status_from_transaction_state( + item.response.transaction_state, + ), + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganCancelRequest { + pub amount: Option, + pub is_void: Option, + pub reversal_reason: Option, +} + +impl TryFrom> for JpmorganCancelRequest { + type Error = error_stack::Report; + fn try_from(item: JpmorganRouterData<&PaymentsCancelRouterData>) -> Result { + Ok(Self { + amount: item.router_data.request.amount, + is_void: Some(true), + reversal_reason: item.router_data.request.cancellation_reason.clone(), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganCancelResponse { + transaction_id: String, + request_id: String, + response_status: JpmorganResponseStatus, + response_code: String, + response_message: String, + payment_method_type: JpmorganPaymentMethodTypeCancelResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganPaymentMethodTypeCancelResponse { + pub card: CardCancelResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardCancelResponse { + pub card_type: Secret, + pub card_type_name: Secret, +} + +impl + TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + JpmorganCancelResponse, + PaymentsCancelData, + PaymentsResponseData, + >, + ) -> Result { + let status = match item.response.response_status { + JpmorganResponseStatus::Success => common_enums::AttemptStatus::Voided, + JpmorganResponseStatus::Denied | JpmorganResponseStatus::Error => { + common_enums::AttemptStatus::Failure + } + }; + Ok(Self { + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, }), ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganValidationErrors { + pub code: Option, + pub message: Option, + pub entity: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganErrorInformation { + pub code: Option, + pub message: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct JpmorganErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, + pub response_status: JpmorganTransactionStatus, + pub response_code: String, + pub response_message: Option, } diff --git a/crates/hyperswitch_connectors/src/constants.rs b/crates/hyperswitch_connectors/src/constants.rs index 9ca4ee9591fb..3d99f8d167ff 100644 --- a/crates/hyperswitch_connectors/src/constants.rs +++ b/crates/hyperswitch_connectors/src/constants.rs @@ -10,6 +10,7 @@ pub(crate) mod headers { pub(crate) const IDEMPOTENCY_KEY: &str = "Idempotency-Key"; pub(crate) const MESSAGE_SIGNATURE: &str = "Message-Signature"; pub(crate) const MERCHANT_ID: &str = "Merchant-ID"; + pub(crate) const REQUEST_ID: &str = "request-id"; pub(crate) const NONCE: &str = "nonce"; pub(crate) const TIMESTAMP: &str = "Timestamp"; pub(crate) const TOKEN: &str = "token"; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 8e6d74c83753..ccb563b06102 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1401,6 +1401,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { itaubank::transformers::ItaubankAuthType::try_from(self.auth_type)?; Ok(()) } + api_enums::Connector::Jpmorgan => { + jpmorgan::transformers::JpmorganAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Klarna => { klarna::transformers::KlarnaAuthType::try_from(self.auth_type)?; klarna::transformers::KlarnaConnectorMetadataObject::try_from( diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index c39ead5fbce8..df5d497ea8fa 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -446,9 +446,11 @@ impl ConnectorData { // Ok(ConnectorEnum::Old(Box::new(connector::Inespay::new()))) // } enums::Connector::Itaubank => { - //enums::Connector::Jpmorgan => Ok(ConnectorEnum::Old(Box::new(connector::Jpmorgan))), Ok(ConnectorEnum::Old(Box::new(connector::Itaubank::new()))) } + enums::Connector::Jpmorgan => { + Ok(ConnectorEnum::Old(Box::new(connector::Jpmorgan::new()))) + } enums::Connector::Klarna => { Ok(ConnectorEnum::Old(Box::new(connector::Klarna::new()))) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 1947a2a2dee1..1d9a86f0aff9 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -256,7 +256,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Iatapay => Self::Iatapay, // api_enums::Connector::Inespay => Self::Inespay, api_enums::Connector::Itaubank => Self::Itaubank, - //api_enums::Connector::Jpmorgan => Self::Jpmorgan, + api_enums::Connector::Jpmorgan => Self::Jpmorgan, api_enums::Connector::Klarna => Self::Klarna, api_enums::Connector::Mifinity => Self::Mifinity, api_enums::Connector::Mollie => Self::Mollie, diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index f2f849731d52..10db11bb3b0c 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -294,7 +294,8 @@ api_key="API Key" api_key="API Key" [jpmorgan] -api_key="API Key" +api_key="Client ID" +key1 ="Client Secret" [elavon] api_key="API Key" diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 37cc341c4540..7b6a1ba6b7a0 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -51,7 +51,7 @@ pub struct ConnectorAuthentication { pub iatapay: Option, pub inespay: Option, pub itaubank: Option, - pub jpmorgan: Option, + pub jpmorgan: Option, pub mifinity: Option, pub mollie: Option, pub multisafepay: Option, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js b/cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js new file mode 100644 index 000000000000..36e53ffd995f --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js @@ -0,0 +1,225 @@ +const successfulNo3DSCardDetails = { + card_number: "6011016011016011", + card_exp_month: "10", + card_exp_year: "2027", + card_holder_name: "John Doe", + card_cvc: "123", +}; + +export const connectorDetails = { + card_pm: { + PaymentIntent: { + Request: { + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + }, + }, + }, + "3DSManualCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "3DS payments is not supported by Jpmorgan", + code: "IR_00", + }, + }, + }, + }, + + "3DSAutoCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Three_ds payments is not supported by Jpmorgan", + code: "IR_00", + }, + }, + }, + }, + No3DSManualCapture: { + Request: { + currency: "USD", + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + No3DSAutoCapture: { + Request: { + currency: "USD", + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + Capture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + amount: 6500, + amount_capturable: 0, + amount_received: 6500, + }, + }, + }, + PartialCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "partially_captured", + amount: 6500, + amount_capturable: 0, + amount_received: 100, + }, + }, + }, + Refund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + manualPaymentRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + manualPaymentPartialRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + PartialRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + SyncRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Response: { + status: 404, + body: { + type: "invalid_request", + message: "Refund does not exist in our records.", + code: "HE_02", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index e432c37717bf..3b4c9e5decf0 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -15,6 +15,7 @@ import { connectorDetails as fiservemeaConnectorDetails } from "./Fiservemea.js" import { connectorDetails as fiuuConnectorDetails } from "./Fiuu.js"; import { connectorDetails as iatapayConnectorDetails } from "./Iatapay.js"; import { connectorDetails as itaubankConnectorDetails } from "./ItauBank.js"; +import { connectorDetails as jpmorganConnectorDetails } from "./Jpmorgan.js"; import { connectorDetails as nexixpayConnectorDetails } from "./Nexixpay.js"; import { connectorDetails as nmiConnectorDetails } from "./Nmi.js"; import { connectorDetails as noonConnectorDetails } from "./Noon.js"; @@ -36,6 +37,7 @@ const connectorDetails = { fiservemea: fiservemeaConnectorDetails, iatapay: iatapayConnectorDetails, itaubank: itaubankConnectorDetails, + jpmorgan: jpmorganConnectorDetails, nexixpay: nexixpayConnectorDetails, nmi: nmiConnectorDetails, novalnet: novalnetConnectorDetails, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index f33ab82aa0dd..3320a5516d07 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -120,6 +120,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" From 0e3a9b6aef189eea84d854ff99778e0682312872 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:21:34 +0000 Subject: [PATCH 11/11] chore(version): 2024.12.23.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf6a551700f9..29ed75461f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.12.23.0 + +### Features + +- **connector:** [JPMORGAN] add Payment flows for cards ([#6668](https://github.com/juspay/hyperswitch/pull/6668)) ([`adcddd6`](https://github.com/juspay/hyperswitch/commit/adcddd643c002a5fe3e7c50c0f78fa5a46f210e7)) +- **payment_methods_v2:** Added Ephemeral auth for v2 ([#6813](https://github.com/juspay/hyperswitch/pull/6813)) ([`24401bc`](https://github.com/juspay/hyperswitch/commit/24401bc16f9677ce0f5fa70d739e5e6885c7e907)) +- **payments_v2:** Implement payments capture v2 ([#6722](https://github.com/juspay/hyperswitch/pull/6722)) ([`977cb70`](https://github.com/juspay/hyperswitch/commit/977cb704e7dcf35d0fa6bc0e3c6d335ad0601521)) +- **router:** + - Add /relay endpoint ([#6870](https://github.com/juspay/hyperswitch/pull/6870)) ([`22de8ad`](https://github.com/juspay/hyperswitch/commit/22de8ad132811b636fdb2594649e40b90810f564)) + - Add db interface for `/relay` ([#6879](https://github.com/juspay/hyperswitch/pull/6879)) ([`0f8b0b3`](https://github.com/juspay/hyperswitch/commit/0f8b0b3bc854be62942a77d08340510312157c67)) + +### Bug Fixes + +- **connector:** + - Paypal BankRedirects (Ideal/EPS) ([#6864](https://github.com/juspay/hyperswitch/pull/6864)) ([`dcd51a7`](https://github.com/juspay/hyperswitch/commit/dcd51a7fb8df673cc74130ee732542b55783602f)) + - [STRIPE] fix stripe mandate ([#6899](https://github.com/juspay/hyperswitch/pull/6899)) ([`9f2ce05`](https://github.com/juspay/hyperswitch/commit/9f2ce05b2591da0d757b267800f42b69fc38e3ee)) + - Update mandate PMT configs for Mandate Supported Connectors ([#6903](https://github.com/juspay/hyperswitch/pull/6903)) ([`02f0824`](https://github.com/juspay/hyperswitch/commit/02f0824d303fb9a36ee54123f52176014613a992)) +- Cypress reports generation ([#6894](https://github.com/juspay/hyperswitch/pull/6894)) ([`81b324c`](https://github.com/juspay/hyperswitch/commit/81b324caf1bef4f621de30824bfb1f05ef984362)) + +### Refactors + +- **connector:** [Airwallex] add device_data in payment request ([#6881](https://github.com/juspay/hyperswitch/pull/6881)) ([`573974b`](https://github.com/juspay/hyperswitch/commit/573974b3a5d53b279bd3959e400ac682aaacd474)) +- **customers_v2:** Include minor fixes for customer v2 flows ([#6876](https://github.com/juspay/hyperswitch/pull/6876)) ([`5cdeaf8`](https://github.com/juspay/hyperswitch/commit/5cdeaf8e6002ad087dba2a562f86b51e97516d29)) +- **dynamic_routing:** Add col payment_method_type in dynamic_routing_stats ([#6853](https://github.com/juspay/hyperswitch/pull/6853)) ([`492fd87`](https://github.com/juspay/hyperswitch/commit/492fd871a14e60e02f17fe073544bc40e79a7220)) +- **grpc:** Send `x-tenant-id` and `x-request-id` in grpc headers ([#6904](https://github.com/juspay/hyperswitch/pull/6904)) ([`dc0a92d`](https://github.com/juspay/hyperswitch/commit/dc0a92dc108c91d6c5f998af417e382aa7a0d9f1)) + +### Miscellaneous Tasks + +- **cypress:** Payout - fix test cases for adyenplatform bank ([#6887](https://github.com/juspay/hyperswitch/pull/6887)) ([`7540b74`](https://github.com/juspay/hyperswitch/commit/7540b7434766ff9dfa1aa2a56013ac89429dd1e6)) + +**Full Changelog:** [`2024.12.19.1...2024.12.23.0`](https://github.com/juspay/hyperswitch/compare/2024.12.19.1...2024.12.23.0) + +- - - + ## 2024.12.19.1 ### Features