From 23bca66b810993895e4054cc4bf3fdcac6b2ed4c Mon Sep 17 00:00:00 2001 From: Sangamesh Kulkarni <59434228+Sangamesh26@users.noreply.github.com> Date: Fri, 19 May 2023 15:14:29 +0530 Subject: [PATCH] feat: ACH transfers (#905) --- crates/api_models/src/enums.rs | 1 + crates/api_models/src/payment_methods.rs | 10 + crates/api_models/src/payments.rs | 54 +++- crates/api_models/src/webhooks.rs | 7 + .../router/src/connector/aci/transformers.rs | 1 + .../connector/authorizedotnet/transformers.rs | 4 +- crates/router/src/connector/stripe.rs | 207 +++++++++++++-- .../src/connector/stripe/transformers.rs | 248 +++++++++++++++++- crates/router/src/core/errors.rs | 2 + .../router/src/core/payment_methods/vault.rs | 59 +++++ crates/router/src/core/payments.rs | 86 +++++- crates/router/src/core/payments/flows.rs | 76 ++++++ .../src/core/payments/flows/authorize_flow.rs | 81 +++++- .../payments/flows/complete_authorize_flow.rs | 4 +- .../src/core/payments/flows/verfiy_flow.rs | 1 + crates/router/src/core/payments/helpers.rs | 28 ++ .../payments/operations/payment_confirm.rs | 10 +- .../payments/operations/payment_response.rs | 12 + .../payments/operations/payment_status.rs | 17 ++ .../payments/operations/payment_update.rs | 2 +- .../router/src/core/payments/transformers.rs | 58 +++- crates/router/src/core/utils.rs | 6 + crates/router/src/core/webhooks.rs | 94 ++++++- crates/router/src/db/payment_attempt.rs | 122 +++++++++ crates/router/src/types.rs | 34 +++ crates/router/src/types/api/payments.rs | 28 +- crates/router/tests/connectors/aci.rs | 3 + crates/router/tests/connectors/adyen.rs | 1 + crates/router/tests/connectors/bitpay.rs | 1 + crates/router/tests/connectors/coinbase.rs | 1 + crates/router/tests/connectors/opennode.rs | 1 + crates/router/tests/connectors/utils.rs | 4 + crates/router/tests/connectors/worldline.rs | 1 + crates/storage_models/src/enums.rs | 1 + crates/storage_models/src/payment_attempt.rs | 26 ++ .../src/query/payment_attempt.rs | 15 ++ crates/storage_models/src/schema.rs | 1 + .../down.sql | 3 + .../up.sql | 3 + 39 files changed, 1247 insertions(+), 66 deletions(-) create mode 100644 migrations/2023-04-20-162755_add_preprocessing_step_id_payment_attempt/down.sql create mode 100644 migrations/2023-04-20-162755_add_preprocessing_step_id_payment_attempt/up.sql diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index eecaf5f44d..c672a8c9b0 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -468,6 +468,7 @@ pub enum PaymentMethod { PayLater, Wallet, BankRedirect, + BankTransfer, Crypto, BankDebit, } diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 4d53336eb0..64d792f2c1 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -608,3 +608,13 @@ pub struct TokenizedWalletValue1 { pub struct TokenizedWalletValue2 { pub customer_id: Option, } + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct TokenizedBankTransferValue1 { + pub data: payments::BankTransferData, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct TokenizedBankTransferValue2 { + pub customer_id: Option, +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 70a5316d9d..f935493ecc 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -547,6 +547,7 @@ pub enum PaymentMethodData { PayLater(PayLaterData), BankRedirect(BankRedirectData), BankDebit(BankDebitData), + BankTransfer(Box), Crypto(CryptoData), MandatePayment, } @@ -563,6 +564,7 @@ pub enum AdditionalPaymentData { }, Wallet {}, PayLater {}, + BankTransfer {}, Crypto {}, BankDebit {}, MandatePayment {}, @@ -589,6 +591,7 @@ impl From<&PaymentMethodData> for AdditionalPaymentData { }, PaymentMethodData::Wallet(_) => Self::Wallet {}, PaymentMethodData::PayLater(_) => Self::PayLater {}, + PaymentMethodData::BankTransfer(_) => Self::BankTransfer {}, PaymentMethodData::Crypto(_) => Self::Crypto {}, PaymentMethodData::BankDebit(_) => Self::BankDebit {}, PaymentMethodData::MandatePayment => Self::MandatePayment {}, @@ -695,6 +698,16 @@ pub enum BankRedirectData { }, } +#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct AchBankTransferData { + pub billing_details: AchBillingDetails, +} + +#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct AchBillingDetails { + pub email: Email, +} + #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub struct CryptoData {} @@ -716,6 +729,12 @@ pub struct BankRedirectBilling { pub email: Option, } +#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum BankTransferData { + AchBankTransfer(AchBankTransferData), +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone, ToSchema, Eq, PartialEq)] pub struct BankDebitBilling { /// The billing name for bank debits @@ -836,8 +855,7 @@ pub struct CardResponse { pub enum PaymentMethodDataResponse { #[serde(rename = "card")] Card(CardResponse), - #[serde(rename(deserialize = "bank_transfer"))] - BankTransfer, + BankTransfer(BankTransferData), Wallet(WalletData), PayLater(PayLaterData), Paypal, @@ -855,6 +873,8 @@ pub enum PaymentIdType { ConnectorTransactionId(String), /// The identifier for payment attempt PaymentAttemptId(String), + /// The identifier for preprocessing step + PreprocessingId(String), } impl std::fmt::Display for PaymentIdType { @@ -870,6 +890,9 @@ impl std::fmt::Display for PaymentIdType { Self::PaymentAttemptId(payment_attempt_id) => { write!(f, "payment_attempt_id = \"{payment_attempt_id}\"") } + Self::PreprocessingId(preprocessing_id) => { + write!(f, "preprocessing_id = \"{preprocessing_id}\"") + } } } } @@ -1004,15 +1027,39 @@ pub enum NextActionType { DisplayQrCode, InvokeSdkClient, TriggerApi, + DisplayBankTransferInformation, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, ToSchema)] pub struct NextAction { /// Specifying the action type to be performed next #[serde(rename = "type")] pub next_action_type: NextActionType, + //TODO: Make an enum having redirect_to_url and bank_transfer_steps_and_charges_details and use here /// Contains the url for redirection flow #[schema(example = "https://router.juspay.io/redirect/fakushdfjlksdfasklhdfj")] pub redirect_to_url: Option, + /// Informs the next steps for bank transfer and also contains the charges details (ex: amount received, amount charged etc) + pub bank_transfer_steps_and_charges_details: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NextStepsRequirements { + pub ach_credit_transfer: AchTransfer, + pub receiver: ReceiverDetails, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct AchTransfer { + pub account_number: Secret, + pub bank_name: String, + pub routing_number: Secret, + pub swift_code: Secret, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ReceiverDetails { + pub amount_received: i64, + pub amount_charged: i64, } #[derive(Setter, Clone, Default, Debug, Eq, PartialEq, serde::Serialize, ToSchema)] @@ -1391,6 +1438,9 @@ impl From for PaymentMethodDataResponse { PaymentMethodData::BankRedirect(bank_redirect_data) => { Self::BankRedirect(bank_redirect_data) } + PaymentMethodData::BankTransfer(bank_transfer_data) => { + Self::BankTransfer(*bank_transfer_data) + } PaymentMethodData::Crypto(crpto_data) => Self::Crypto(crpto_data), PaymentMethodData::BankDebit(bank_debit_data) => Self::BankDebit(bank_debit_data), PaymentMethodData::MandatePayment => Self::MandatePayment, diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index 0f02e7db3d..c966e12f33 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -12,6 +12,9 @@ pub enum IncomingWebhookEvent { PaymentIntentProcessing, PaymentActionRequired, EventNotSupported, + SourceChargeable, + SourceTransactionCreated, + ChargeSucceeded, RefundFailure, RefundSuccess, DisputeOpened, @@ -32,6 +35,7 @@ pub enum WebhookFlow { Dispute, Subscription, ReturnResponse, + BankTransfer, } impl From for WebhookFlow { @@ -52,6 +56,9 @@ impl From for WebhookFlow { IncomingWebhookEvent::DisputeWon => Self::Dispute, IncomingWebhookEvent::DisputeLost => Self::Dispute, IncomingWebhookEvent::EndpointVerification => Self::ReturnResponse, + IncomingWebhookEvent::SourceChargeable + | IncomingWebhookEvent::SourceTransactionCreated => Self::BankTransfer, + IncomingWebhookEvent::ChargeSucceeded => Self::Payment, } } } diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 0561dc1b23..cdda109caa 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -296,6 +296,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AciPaymentsRequest { api::PaymentMethodData::Crypto(_) | api::PaymentMethodData::BankDebit(_) + | api::PaymentMethodData::BankTransfer(_) | api::PaymentMethodData::MandatePayment => { Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.payment_method), diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 0ff92ca196..014d945b82 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -70,6 +70,7 @@ enum PaymentDetails { Paypal, #[serde(rename = "bankRedirect")] BankRedirect, + BankTransfer, } fn get_pm_and_subsequent_auth_detail( @@ -137,7 +138,8 @@ fn get_pm_and_subsequent_auth_detail( } api::PaymentMethodData::Crypto(_) | api::PaymentMethodData::BankDebit(_) - | api::PaymentMethodData::MandatePayment => { + | api::PaymentMethodData::MandatePayment + | api::PaymentMethodData::BankTransfer(_) => { Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "AuthorizeDotNet", diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 02cf834549..06a68c049a 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -1,6 +1,6 @@ mod transformers; -use std::{collections::HashMap, fmt::Debug}; +use std::{collections::HashMap, fmt::Debug, ops::Deref}; use error_stack::{IntoReport, ResultExt}; use router_env::{instrument, tracing}; @@ -21,7 +21,7 @@ use crate::{ self, api::{self, ConnectorCommon}, }, - utils::{self, crypto, ByteSliceExt, BytesExt}, + utils::{self, crypto, ByteSliceExt, BytesExt, OptionExt}, }; #[derive(Debug, Clone)] @@ -84,6 +84,116 @@ impl // Not Implemented (R) } +impl api::PaymentsPreProcessing for Stripe {} + +impl + services::ConnectorIntegration< + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > for Stripe +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + types::PaymentsPreProcessingType::get_content_type(self).to_string(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "v1/sources")) + } + + fn get_request_body( + &self, + req: &types::PaymentsPreProcessingRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req = stripe::StripeAchSourceRequest::try_from(req)?; + let pre_processing_request = + utils::Encode::::url_encode(&req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + Ok(Some(pre_processing_request)) + } + + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsPreProcessingType::get_request_body( + self, req, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: types::Response, + ) -> CustomResult { + let response: stripe::StripeSourceResponse = res + .response + .parse_struct("StripeSourceResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: stripe::ErrorResponse = res + .response + .parse_struct("ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(types::ErrorResponse { + status_code: res.status_code, + code: response + .error + .code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .error + .message + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + }) + } +} + impl api::ConnectorCustomer for Stripe {} impl @@ -542,6 +652,7 @@ impl } } +#[async_trait::async_trait] impl services::ConnectorIntegration< api::Authorize, @@ -569,24 +680,40 @@ impl fn get_url( &self, - _req: &types::PaymentsAuthorizeRouterData, + req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult { - Ok(format!( - "{}{}", - self.base_url(connectors), - "v1/payment_intents" - )) + match &req.request.payment_method_data { + api_models::payments::PaymentMethodData::BankTransfer(bank_transfer_data) => { + match bank_transfer_data.deref() { + api_models::payments::BankTransferData::AchBankTransfer(_) => { + Ok(format!("{}{}", self.base_url(connectors), "v1/charges")) + } + } + } + _ => Ok(format!( + "{}{}", + self.base_url(connectors), + "v1/payment_intents" + )), + } } fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, ) -> CustomResult, errors::ConnectorError> { - let req = stripe::PaymentIntentRequest::try_from(req)?; - let stripe_req = utils::Encode::::url_encode(&req) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + match &req.request.payment_method_data { + api_models::payments::PaymentMethodData::BankTransfer(bank_transfer_data) => { + stripe::get_bank_transfer_request_data(req, bank_transfer_data.deref()) + } + _ => { + let req = stripe::PaymentIntentRequest::try_from(req)?; + let request = utils::Encode::::url_encode(&req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(request)) + } + } } fn build_request( @@ -614,17 +741,24 @@ impl data: &types::PaymentsAuthorizeRouterData, res: types::Response, ) -> CustomResult { - let response: stripe::PaymentIntentResponse = res - .response - .parse_struct("PaymentIntentResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + match &data.request.payment_method_data { + api_models::payments::PaymentMethodData::BankTransfer(bank_transfer_data) => { + stripe::get_bank_transfer_authorize_response(data, res, bank_transfer_data.deref()) + } + _ => { + let response: stripe::PaymentIntentResponse = res + .response + .parse_struct("PaymentIntentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - .change_context(errors::ConnectorError::ResponseHandlingFailed) + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + } } fn get_error_response( @@ -1500,7 +1634,8 @@ impl api::IncomingWebhook for Stripe { .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; Ok(match details.event_data.event_object.object { - stripe::WebhookEventObjectType::PaymentIntent => { + stripe::WebhookEventObjectType::PaymentIntent + | stripe::WebhookEventObjectType::Charge => { api_models::webhooks::ObjectReferenceId::PaymentId( api_models::payments::PaymentIdType::ConnectorTransactionId( details.event_data.event_object.id, @@ -1518,7 +1653,13 @@ impl api::IncomingWebhook for Stripe { ), ) } - _ => Err(errors::ConnectorError::WebhookReferenceIdNotFound)?, + stripe::WebhookEventObjectType::Source => { + api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PreprocessingId( + details.event_data.event_object.id, + ), + ) + } }) } @@ -1530,6 +1671,7 @@ impl api::IncomingWebhook for Stripe { .body .parse_struct("WebhookEvent") .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + Ok(match details.event_type { stripe::WebhookEventType::PaymentIntentFailed => { api::IncomingWebhookEvent::PaymentIntentFailure @@ -1537,6 +1679,13 @@ impl api::IncomingWebhook for Stripe { stripe::WebhookEventType::PaymentIntentSucceed => { api::IncomingWebhookEvent::PaymentIntentSuccess } + stripe::WebhookEventType::SourceChargeable => { + api::IncomingWebhookEvent::SourceChargeable + } + stripe::WebhookEventType::SourceTransactionCreated => { + api::IncomingWebhookEvent::SourceTransactionCreated + } + stripe::WebhookEventType::ChargeSucceeded => api::IncomingWebhookEvent::ChargeSucceeded, stripe::WebhookEventType::DisputeCreated => api::IncomingWebhookEvent::DisputeOpened, stripe::WebhookEventType::DisputeClosed => api::IncomingWebhookEvent::DisputeCancelled, stripe::WebhookEventType::DisputeUpdated => api::IncomingWebhookEvent::try_from( @@ -1570,7 +1719,15 @@ impl api::IncomingWebhook for Stripe { .parse_struct("WebhookEvent") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; Ok(api::disputes::DisputePayload { - amount: details.event_data.event_object.amount.to_string(), + amount: details + .event_data + .event_object + .amount + .get_required_value("amount") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })? + .to_string(), currency: details.event_data.event_object.currency, dispute_stage: api_models::enums::DisputeStage::Dispute, connector_dispute_id: details.event_data.event_object.id, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index ea155fe612..8609a50053 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1,6 +1,12 @@ +use std::ops::Deref; + use api_models::{self, enums as api_enums, payments}; use base64::Engine; -use common_utils::{errors::CustomResult, ext_traits::ByteSliceExt, pii, pii::Email}; +use common_utils::{ + errors::CustomResult, + ext_traits::{ByteSliceExt, BytesExt}, + pii::{self, Email}, +}; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, ExposeOptionInterface, Secret}; use serde::{Deserialize, Serialize}; @@ -13,7 +19,7 @@ use crate::{ core::errors, services, types::{self, api, storage::enums, transformers::ForeignFrom}, - utils::OptionExt, + utils::{self, OptionExt}, }; pub struct StripeAuthType { @@ -172,6 +178,7 @@ pub struct CustomerRequest { pub email: Option, pub phone: Option>, pub name: Option, + pub source: Option, } #[derive(Debug, Eq, PartialEq, Deserialize)] @@ -183,6 +190,24 @@ pub struct StripeCustomerResponse { pub name: Option, } +#[derive(Debug, Eq, PartialEq, Serialize)] +pub struct ChargesRequest { + pub amount: String, + pub currency: String, + pub customer: String, + pub source: String, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] +pub struct ChargesResponse { + pub id: String, + pub amount: u64, + pub amount_captured: u64, + pub currency: String, + pub status: StripePaymentStatus, + pub source: StripeSourceResponse, +} + #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(untagged)] pub enum StripeBankName { @@ -302,6 +327,20 @@ pub struct StripeBankDebitData { pub bank_specific_data: BankDebitData, } +#[derive(Debug, Eq, PartialEq, Serialize)] +pub struct BankTransferData { + pub email: Email, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +pub struct StripeAchSourceRequest { + #[serde(rename = "type")] + pub transfer_type: StripePaymentMethodType, + #[serde(rename = "owner[email]")] + pub email: Email, + pub currency: String, +} + #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(untagged)] pub enum StripePaymentMethodData { @@ -310,6 +349,7 @@ pub enum StripePaymentMethodData { Wallet(StripeWallet), BankRedirect(StripeBankRedirectData), BankDebit(StripeBankDebitData), + AchBankTransfer(BankTransferData), } #[derive(Debug, Eq, PartialEq, Serialize)] @@ -389,6 +429,7 @@ pub enum StripePaymentMethodType { Giropay, Ideal, Sofort, + AchCreditTransfer, ApplePay, #[serde(rename = "us_bank_account")] Ach, @@ -968,6 +1009,17 @@ fn create_stripe_payment_method( Ok((pm_data, pm_type, billing_address)) } + payments::PaymentMethodData::BankTransfer(bank_transfer_data) => { + match bank_transfer_data.deref() { + payments::BankTransferData::AchBankTransfer(ach_bank_transfer_data) => Ok(( + StripePaymentMethodData::AchBankTransfer(BankTransferData { + email: ach_bank_transfer_data.billing_details.email.to_owned(), + }), + StripePaymentMethodType::AchCreditTransfer, + StripeBillingAddress::default(), + )), + } + } _ => Err(errors::ConnectorError::NotImplemented( "this payment method for stripe".to_string(), ) @@ -1189,6 +1241,7 @@ impl TryFrom<&types::ConnectorCustomerRouterData> for CustomerRequest { email: item.request.email.to_owned(), phone: item.request.phone.to_owned(), name: item.request.name.to_owned(), + source: item.request.preprocessing_id.to_owned(), }) } } @@ -1214,7 +1267,8 @@ pub enum StripePaymentStatus { RequiresConfirmation, Canceled, RequiresCapture, - // This is the case in Sofort Bank Redirects + Chargeable, + Consumed, Pending, } @@ -1230,6 +1284,8 @@ impl From for enums::AttemptStatus { StripePaymentStatus::RequiresConfirmation => Self::ConfirmationAwaited, StripePaymentStatus::Canceled => Self::Voided, StripePaymentStatus::RequiresCapture => Self::Authorized, + StripePaymentStatus::Chargeable => Self::Authorizing, + StripePaymentStatus::Consumed => Self::Authorizing, StripePaymentStatus::Pending => Self::Pending, } } @@ -1258,6 +1314,28 @@ pub struct PaymentIntentResponse { pub latest_attempt: Option, //need a merchant to test this } +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct StripeSourceResponse { + pub id: String, + pub ach_credit_transfer: AchCreditTransferResponse, + pub receiver: AchReceiverDetails, + pub status: StripePaymentStatus, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct AchCreditTransferResponse { + pub account_number: Secret, + pub bank_name: Secret, + pub routing_number: Secret, + pub swift_code: Secret, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct AchReceiverDetails { + pub amount_received: i64, + pub amount_charged: i64, +} + #[derive(Debug, Default, Eq, PartialEq, Deserialize)] pub struct PaymentSyncResponse { #[serde(flatten)] @@ -1265,7 +1343,7 @@ pub struct PaymentSyncResponse { pub last_payment_error: Option, } -impl std::ops::Deref for PaymentSyncResponse { +impl Deref for PaymentSyncResponse { type Target = PaymentIntentResponse; fn deref(&self) -> &Self::Target { @@ -1286,7 +1364,7 @@ pub struct PaymentIntentSyncResponse { pub last_payment_error: Option, } -impl std::ops::Deref for PaymentIntentSyncResponse { +impl Deref for PaymentIntentSyncResponse { type Target = PaymentIntentResponse; fn deref(&self) -> &Self::Target { @@ -1301,7 +1379,7 @@ pub struct SetupIntentSyncResponse { pub last_payment_error: Option, } -impl std::ops::Deref for SetupIntentSyncResponse { +impl Deref for SetupIntentSyncResponse { type Target = SetupIntentResponse; fn deref(&self) -> &Self::Target { @@ -1833,6 +1911,116 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for CaptureRequest { } } +impl TryFrom<&types::PaymentsPreProcessingRouterData> for StripeAchSourceRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { + Ok(Self { + transfer_type: StripePaymentMethodType::AchCreditTransfer, + email: item + .request + .email + .clone() + .get_required_value("email") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "email", + })?, + currency: item + .request + .currency + .get_required_value("currency") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })? + .to_string(), + }) + } +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + let connector_source_response = item.response.to_owned(); + let connector_metadata = + common_utils::ext_traits::Encode::::encode_to_value( + &connector_source_response, + ) + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + // We get pending as the status from stripe, but hyperswitch should give it as requires_customer_action as + // customer has to make payment to the virtual account number given in the source response + let status = match connector_source_response.status.clone().into() { + storage_models::enums::AttemptStatus::Pending => { + storage_models::enums::AttemptStatus::AuthenticationPending + } + _ => connector_source_response.status.into(), + }; + Ok(Self { + response: Ok(types::PaymentsResponseData::PreProcessingResponse { + pre_processing_id: item.response.id, + connector_metadata: Some(connector_metadata), + }), + status, + ..item.data + }) + } +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for ChargesRequest { + type Error = error_stack::Report; + + fn try_from(value: &types::PaymentsAuthorizeRouterData) -> Result { + Ok(Self { + amount: value.request.amount.to_string(), + currency: value.request.currency.to_string(), + customer: value + .connector_customer + .to_owned() + .get_required_value("customer_id") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "customer_id", + })?, + source: value + .preprocessing_id + .to_owned() + .get_required_value("source") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "source", + })?, + }) + } +} + +impl TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + let connector_source_response = item.response.to_owned(); + let connector_metadata = + common_utils::ext_traits::Encode::::encode_to_value( + &connector_source_response.source, + ) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(Self { + status: enums::AttemptStatus::from(item.response.status), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: Some(connector_metadata), + network_txn_id: None, + }), + ..item.data + }) + } +} + impl TryFrom> for types::RouterData @@ -1932,7 +2120,7 @@ pub struct WebhookEventData { pub struct WebhookEventObjectData { pub id: String, pub object: WebhookEventObjectType, - pub amount: i32, + pub amount: Option, pub currency: String, pub payment_intent: Option, pub reason: Option, @@ -1948,6 +2136,7 @@ pub enum WebhookEventObjectType { PaymentIntent, Dispute, Charge, + Source, } #[derive(Debug, Deserialize)] @@ -1992,6 +2181,10 @@ pub enum WebhookEventType { PaymentIntentRequiresAction, #[serde(rename = "amount_capturable_updated")] PaymentIntentAmountCapturableUpdated, + #[serde(rename = "source.chargeable")] + SourceChargeable, + #[serde(rename = "source.transaction.created")] + SourceTransactionCreated, } #[derive(Debug, Serialize, strum::Display, Deserialize, PartialEq)] @@ -2012,6 +2205,7 @@ pub enum WebhookEventStatus { Processing, RequiresCapture, Canceled, + Chargeable, } #[derive(Debug, Deserialize, PartialEq)] @@ -2107,6 +2301,15 @@ impl bank_specific_data: bank_data, })) } + api::PaymentMethodData::BankTransfer(bank_transfer_data) => { + match bank_transfer_data.deref() { + payments::BankTransferData::AchBankTransfer(ach_bank_transfer_data) => { + Ok(Self::AchBankTransfer(BankTransferData { + email: ach_bank_transfer_data.billing_details.email.to_owned(), + })) + } + } + } api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Crypto(_) => { Err(errors::ConnectorError::NotSupported { message: format!("{pm_type:?}"), @@ -2123,6 +2326,37 @@ impl pub struct StripeGpayToken { pub id: String, } +pub fn get_bank_transfer_request_data( + req: &types::PaymentsAuthorizeRouterData, + bank_transfer_data: &api_models::payments::BankTransferData, +) -> CustomResult, errors::ConnectorError> { + match bank_transfer_data { + api_models::payments::BankTransferData::AchBankTransfer(_) => { + let req = ChargesRequest::try_from(req)?; + let request = utils::Encode::::url_encode(&req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(request)) + } + } +} +pub fn get_bank_transfer_authorize_response( + data: &types::PaymentsAuthorizeRouterData, + res: types::Response, + _bank_transfer_data: &api_models::payments::BankTransferData, +) -> CustomResult { + let response: ChargesResponse = res + .response + .parse_struct("ChargesResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) +} + pub fn construct_file_upload_request( file_upload_router_data: types::UploadFileRouterData, ) -> CustomResult { diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index d1556cb26a..1bc43ac32f 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -462,6 +462,8 @@ pub enum WebhooksFlowError { DisputeWebhookValidationFailed, #[error("Outgoing webhook body encoding failed")] OutgoingWebhookEncodingFailed, + #[error("Missing required field: {field_name}")] + MissingRequiredField { field_name: &'static str }, } #[cfg(feature = "detailed_errors")] diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 96e3c5c2e9..db67d2e627 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -119,6 +119,50 @@ impl Vaultable for api::Card { } } +impl Vaultable for api_models::payments::BankTransferData { + fn get_value1(&self, _customer_id: Option) -> CustomResult { + let value1 = api_models::payment_methods::TokenizedBankTransferValue1 { + data: self.to_owned(), + }; + + utils::Encode::::encode_to_string_of_json(&value1) + .change_context(errors::VaultError::RequestEncodingFailed) + .attach_printable("Failed to encode bank transfer data") + } + + fn get_value2(&self, customer_id: Option) -> CustomResult { + let value2 = api_models::payment_methods::TokenizedBankTransferValue2 { customer_id }; + + utils::Encode::::encode_to_string_of_json(&value2) + .change_context(errors::VaultError::RequestEncodingFailed) + .attach_printable("Failed to encode bank transfer supplementary data") + } + + fn from_values( + value1: String, + value2: String, + ) -> CustomResult<(Self, SupplementaryVaultData), errors::VaultError> { + let value1: api_models::payment_methods::TokenizedBankTransferValue1 = value1 + .parse_struct("TokenizedBankTransferValue1") + .change_context(errors::VaultError::ResponseDeserializationFailed) + .attach_printable("Could not deserialize into bank transfer data")?; + + let value2: api_models::payment_methods::TokenizedBankTransferValue2 = value2 + .parse_struct("TokenizedBankTransferValue2") + .change_context(errors::VaultError::ResponseDeserializationFailed) + .attach_printable("Could not deserialize into supplementary bank transfer data")?; + + let bank_transfer_data = value1.data; + + let supp_data = SupplementaryVaultData { + customer_id: value2.customer_id, + payment_method_id: None, + }; + + Ok((bank_transfer_data, supp_data)) + } +} + impl Vaultable for api::WalletData { fn get_value1(&self, _customer_id: Option) -> CustomResult { let value1 = api::TokenizedWalletValue1 { @@ -168,6 +212,7 @@ impl Vaultable for api::WalletData { pub enum VaultPaymentMethod { Card(String), Wallet(String), + BankTransfer(String), } impl Vaultable for api::PaymentMethodData { @@ -175,6 +220,9 @@ impl Vaultable for api::PaymentMethodData { let value1 = match self { Self::Card(card) => VaultPaymentMethod::Card(card.get_value1(customer_id)?), Self::Wallet(wallet) => VaultPaymentMethod::Wallet(wallet.get_value1(customer_id)?), + Self::BankTransfer(bank_transfer) => { + VaultPaymentMethod::BankTransfer(bank_transfer.get_value1(customer_id)?) + } _ => Err(errors::VaultError::PaymentMethodNotSupported) .into_report() .attach_printable("Payment method not supported")?, @@ -189,6 +237,9 @@ impl Vaultable for api::PaymentMethodData { let value2 = match self { Self::Card(card) => VaultPaymentMethod::Card(card.get_value2(customer_id)?), Self::Wallet(wallet) => VaultPaymentMethod::Wallet(wallet.get_value2(customer_id)?), + Self::BankTransfer(bank_transfer) => { + VaultPaymentMethod::BankTransfer(bank_transfer.get_value2(customer_id)?) + } _ => Err(errors::VaultError::PaymentMethodNotSupported) .into_report() .attach_printable("Payment method not supported")?, @@ -222,6 +273,14 @@ impl Vaultable for api::PaymentMethodData { let (wallet, supp_data) = api::WalletData::from_values(mvalue1, mvalue2)?; Ok((Self::Wallet(wallet), supp_data)) } + ( + VaultPaymentMethod::BankTransfer(mvalue1), + VaultPaymentMethod::BankTransfer(mvalue2), + ) => { + let (bank_transfer, supp_data) = + api_models::payments::BankTransferData::from_values(mvalue1, mvalue2)?; + Ok((Self::BankTransfer(Box::new(bank_transfer)), supp_data)) + } _ => Err(errors::VaultError::PaymentMethodNotSupported) .into_report() .attach_printable("Payment method not supported"), diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 88978ff2ed..8b9e1c40e8 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -6,7 +6,7 @@ pub mod operations; pub mod tokenization; pub mod transformers; -use std::{fmt::Debug, marker::PhantomData, time::Instant}; +use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant}; use api_models::payments::Metadata; use common_utils::pii::Email; @@ -143,10 +143,12 @@ where .await?; if let Some(connector_details) = connector { - operation - .to_domain()? - .add_task_to_process_tracker(state, &payment_data.payment_attempt) - .await?; + if should_add_task_to_process_tracker(&payment_data)? { + operation + .to_domain()? + .add_task_to_process_tracker(state, &payment_data.payment_attempt) + .await?; + } payment_data = match connector_details { api::ConnectorCallType::Single(connector) => { @@ -188,8 +190,7 @@ where .await? } }; - if payment_data.payment_intent.status != storage_enums::IntentStatus::RequiresCustomerAction - { + if should_delete_pm_from_locker(payment_data.payment_intent.status) { vault::Vault::delete_locker_payment_method_by_lookup_key(state, &payment_data.token) .await } @@ -499,7 +500,7 @@ where .add_access_token(state, &connector, merchant_account) .await?; - let should_continue_payment = access_token::update_router_data_with_access_token_result( + let mut should_continue_payment = access_token::update_router_data_with_access_token_result( &add_access_token_result, &mut router_data, &call_connector_action, @@ -513,6 +514,15 @@ where router_data.payment_method_token = Some(payment_method_token); }; + (router_data, should_continue_payment) = complete_preprocessing_steps_if_required( + state, + &connector, + payment_data, + router_data, + should_continue_payment, + ) + .await?; + let router_data_res = if should_continue_payment { router_data .decide_flows( @@ -665,6 +675,39 @@ where } } +async fn complete_preprocessing_steps_if_required( + state: &AppState, + connector: &api::ConnectorData, + payment_data: &PaymentData, + router_data: types::RouterData, + should_continue_payment: bool, +) -> RouterResult<(types::RouterData, bool)> +where + F: Send + Clone + Sync, + Req: Send + Sync, + types::RouterData: Feature + Send, + dyn api::Connector: services::api::ConnectorIntegration, +{ + //TODO: For ACH transfers, if preprocessing_step is not required for connectors encountered in future, add the check + let router_data_and_should_continue_payment = match payment_data.payment_method_data.clone() { + Some(api_models::payments::PaymentMethodData::BankTransfer(data)) => match data.deref() { + api_models::payments::BankTransferData::AchBankTransfer(_) => { + if payment_data.payment_attempt.preprocessing_step_id.is_none() { + ( + router_data.preprocessing_steps(state, connector).await?, + false, + ) + } else { + (router_data, should_continue_payment) + } + } + }, + _ => (router_data, should_continue_payment), + }; + + Ok(router_data_and_should_continue_payment) +} + fn is_payment_method_tokenization_enabled_for_connector( state: &AppState, connector_name: &str, @@ -979,6 +1022,13 @@ pub fn should_call_connector( } } +pub fn should_delete_pm_from_locker(status: storage_enums::IntentStatus) -> bool { + !matches!( + status, + storage_models::enums::IntentStatus::RequiresCustomerAction + ) +} + pub fn is_operation_confirm(operation: &Op) -> bool { matches!(format!("{operation:?}").as_str(), "PaymentConfirm") } @@ -1259,3 +1309,23 @@ pub fn decide_connector( Ok(api::ConnectorCallType::Single(connector_data)) } + +pub fn should_add_task_to_process_tracker( + payment_data: &PaymentData, +) -> RouterResult { + let pm = payment_data + .payment_attempt + .payment_method + .get_required_value("payment_method")?; + let connector = payment_data + .payment_attempt + .connector + .clone() + .get_required_value("connector")?; + let add_task_check = if matches!(pm, storage_enums::PaymentMethod::BankTransfer) { + !connector.eq("stripe") + } else { + true + }; + Ok(add_task_check) +} diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 1ca81888e3..d619e72275 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -70,6 +70,19 @@ pub trait Feature { Ok(None) } + async fn preprocessing_steps<'a>( + self, + _state: &AppState, + _connector: &api::ConnectorData, + ) -> RouterResult + where + F: Clone, + Self: Sized, + dyn api::Connector: services::ConnectorIntegration, + { + Ok(self) + } + async fn create_connector_customer<'a>( &self, _state: &AppState, @@ -578,3 +591,66 @@ default_imp_for_defend_dispute!( connector::Worldpay, connector::Zen ); + +macro_rules! default_imp_for_pre_processing_steps{ + ($($path:ident::$connector:ident),*)=> { + $( + impl api::PaymentsPreProcessing for $path::$connector {} + impl + services::ConnectorIntegration< + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::PaymentsPreProcessing for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_pre_processing_steps!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bitpay, + connector::Bluesnap, + connector::Braintree, + connector::Checkout, + connector::Coinbase, + connector::Cybersource, + connector::Dlocal, + connector::Iatapay, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opennode, + connector::Payeezy, + connector::Paypal, + connector::Payu, + connector::Rapyd, + connector::Shift4, + connector::Trustpay, + connector::Worldline, + connector::Worldpay, + connector::Zen +); diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index b95fe32078..744c1c0d14 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -96,6 +96,14 @@ impl Feature for types::PaymentsAu .await } + async fn preprocessing_steps<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + ) -> RouterResult { + authorize_preprocessing_steps(state, &self, true, connector).await + } + async fn create_connector_customer<'a>( &self, state: &AppState, @@ -106,7 +114,7 @@ impl Feature for types::PaymentsAu state, connector, self, - types::ConnectorCustomerData::try_from(self.request.to_owned())?, + types::ConnectorCustomerData::try_from(self)?, connector_customer_map, ) .await @@ -213,15 +221,69 @@ impl mandate::MandateBehaviour for types::PaymentsAuthorizeData { } } -impl TryFrom for types::ConnectorCustomerData { +pub async fn authorize_preprocessing_steps( + state: &AppState, + router_data: &types::RouterData, + confirm: bool, + connector: &api::ConnectorData, +) -> RouterResult> { + if confirm { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let preprocessing_request_data = + types::PaymentsPreProcessingData::try_from(router_data.request.to_owned())?; + + let preprocessing_response_data: Result = + Err(types::ErrorResponse::default()); + + let preprocessing_router_data = + payments::helpers::router_data_type_conversion::<_, api::PreProcessing, _, _, _, _>( + router_data.clone(), + preprocessing_request_data, + preprocessing_response_data, + ); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &preprocessing_router_data, + payments::CallConnectorAction::Trigger, + ) + .await + .map_err(|error| error.to_payment_failed_response())?; + + let authorize_router_data = + payments::helpers::router_data_type_conversion::<_, F, _, _, _, _>( + resp.clone(), + router_data.request.to_owned(), + resp.response, + ); + + Ok(authorize_router_data) + } else { + Ok(router_data.clone()) + } +} + +impl TryFrom<&types::RouterData> + for types::ConnectorCustomerData +{ type Error = error_stack::Report; - fn try_from(data: types::PaymentsAuthorizeData) -> Result { + fn try_from( + data: &types::RouterData, + ) -> Result { Ok(Self { - email: data.email, + email: data.request.email.clone(), description: None, phone: None, name: None, + preprocessing_id: data.preprocessing_id.clone(), }) } } @@ -235,3 +297,14 @@ impl TryFrom for types::PaymentMethodTokenizationD }) } } + +impl TryFrom for types::PaymentsPreProcessingData { + type Error = error_stack::Report; + + fn try_from(data: types::PaymentsAuthorizeData) -> Result { + Ok(Self { + email: data.email, + currency: Some(data.currency), + }) + } +} diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index b8712d52f6..fe324dfb1e 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -55,7 +55,7 @@ impl Feature > { async fn decide_flows<'a>( - self, + mut self, state: &AppState, connector: &api::ConnectorData, customer: &Option, @@ -84,7 +84,7 @@ impl Feature impl types::PaymentsCompleteAuthorizeRouterData { pub async fn decide_flow<'a, 'b>( - &'b self, + &'b mut self, state: &'a AppState, connector: &api::ConnectorData, _maybe_customer: &Option, diff --git a/crates/router/src/core/payments/flows/verfiy_flow.rs b/crates/router/src/core/payments/flows/verfiy_flow.rs index 235a43b568..d09be85233 100644 --- a/crates/router/src/core/payments/flows/verfiy_flow.rs +++ b/crates/router/src/core/payments/flows/verfiy_flow.rs @@ -105,6 +105,7 @@ impl TryFrom for types::ConnectorCustomerData { description: None, phone: None, name: None, + preprocessing_id: None, }) } } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 06524d89cc..7605978e5d 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -803,6 +803,20 @@ pub async fn make_pm_data<'a, F: Clone, R>( } } + Some(api::PaymentMethodData::BankTransfer(bank_transfer)) => { + payment_data.payment_attempt.payment_method = + Some(storage_enums::PaymentMethod::BankTransfer); + let updated_pm = api::PaymentMethodData::BankTransfer(bank_transfer); + vault::Vault::store_payment_method_data_in_locker( + state, + Some(hyperswitch_token), + &updated_pm, + payment_data.payment_intent.customer_id.to_owned(), + enums::PaymentMethod::BankTransfer, + ) + .await?; + Some(updated_pm) + } Some(_) => Err(errors::ApiErrorResponse::InternalServerError) .into_report() .attach_printable( @@ -828,6 +842,18 @@ pub async fn make_pm_data<'a, F: Clone, R>( (pm @ Some(api::PaymentMethodData::BankRedirect(_)), _) => Ok(pm.to_owned()), (pm @ Some(api::PaymentMethodData::Crypto(_)), _) => Ok(pm.to_owned()), (pm @ Some(api::PaymentMethodData::BankDebit(_)), _) => Ok(pm.to_owned()), + (pm_opt @ Some(pm @ api::PaymentMethodData::BankTransfer(_)), _) => { + let token = vault::Vault::store_payment_method_data_in_locker( + state, + None, + pm, + payment_data.payment_intent.customer_id.to_owned(), + enums::PaymentMethod::BankTransfer, + ) + .await?; + payment_data.token = Some(token); + Ok(pm_opt.to_owned()) + } (pm_opt @ Some(pm @ api::PaymentMethodData::Wallet(_)), _) => { let token = vault::Vault::store_payment_method_data_in_locker( state, @@ -1683,6 +1709,7 @@ pub fn router_data_type_conversion( payment_method_token: router_data.payment_method_token, customer_id: router_data.customer_id, connector_customer: router_data.connector_customer, + preprocessing_id: router_data.preprocessing_id, } } @@ -1823,6 +1850,7 @@ impl AttemptType { // If the algorithm is entered in Create call from server side, it needs to be populated here, however it could be overridden from the request. straight_through_algorithm: old_payment_attempt.straight_through_algorithm, mandate_details: old_payment_attempt.mandate_details, + preprocessing_step_id: None, } } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 3ff0296f7a..c32a57a88b 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -108,6 +108,7 @@ impl GetTracker, api::PaymentsRequest> for Pa let browser_info = request .browser_info .clone() + .or(payment_attempt.browser_info) .map(|x| utils::Encode::::encode_to_value(&x)) .transpose() .change_context(errors::ApiErrorResponse::InvalidDataValue { @@ -133,7 +134,8 @@ impl GetTracker, api::PaymentsRequest> for Pa payment_attempt.payment_experience = request .payment_experience - .map(|experience| experience.foreign_into()); + .map(|experience| experience.foreign_into()) + .or(payment_attempt.payment_experience); payment_attempt.capture_method = request .capture_method @@ -176,7 +178,11 @@ impl GetTracker, api::PaymentsRequest> for Pa payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); - payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string()); + payment_intent.return_url = request + .return_url + .as_ref() + .map(|a| a.to_string()) + .or(payment_intent.return_url); payment_attempt.business_sub_label = request .business_sub_label diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index e7ed3384d9..d0b171267d 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -304,6 +304,18 @@ async fn payment_response_update_tracker( }), ), Ok(payments_response) => match payments_response { + types::PaymentsResponseData::PreProcessingResponse { + pre_processing_id, + connector_metadata, + } => { + let payment_attempt_update = storage::PaymentAttemptUpdate::PreprocessingUpdate { + status: router_data.status, + payment_method_id: Some(router_data.payment_method_id), + connector_metadata, + preprocessing_step_id: Some(pre_processing_id), + }; + (Some(payment_attempt_update), None) + } types::PaymentsResponseData::TransactionResponse { resource_id, redirection_data, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 8999fcc99c..8117a87f17 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -375,6 +375,23 @@ pub async fn get_payment_intent_payment_attempt( ) .await?; } + api_models::payments::PaymentIdType::PreprocessingId(ref id) => { + pa = db + .find_payment_attempt_by_preprocessing_id_merchant_id( + id, + merchant_id, + storage_scheme, + ) + .await?; + + pi = db + .find_payment_intent_by_payment_id_merchant_id( + pa.payment_id.as_str(), + merchant_id, + storage_scheme, + ) + .await?; + } } Ok((pi, pa)) })() diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 4c0b270a9b..6830cebb7e 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -472,7 +472,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen currency: payment_data.currency, setup_future_usage, status: intent_status, - customer_id, + customer_id: customer_id.clone(), shipping_address_id: shipping_address, billing_address_id: billing_address, return_url, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 8258b6bde5..9a81a6ef6b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -119,6 +119,7 @@ where reference_id: None, payment_method_token: payment_data.pm_token, connector_customer: payment_data.connector_customer_id, + preprocessing_id: payment_data.payment_attempt.preprocessing_step_id, }; Ok(router_data) @@ -301,16 +302,29 @@ where })) } else { let mut next_action_response = None; - if payment_intent.status == enums::IntentStatus::RequiresCustomerAction { + + let bank_transfer_next_steps = + bank_transfer_next_steps_check(payment_attempt.clone())?; + + if payment_intent.status == enums::IntentStatus::RequiresCustomerAction + || bank_transfer_next_steps.is_some() + { + let next_action_type = if bank_transfer_next_steps.is_some() { + api::NextActionType::DisplayBankTransferInformation + } else { + api::NextActionType::RedirectToUrl + }; next_action_response = Some(api::NextAction { - next_action_type: api::NextActionType::RedirectToUrl, + next_action_type, redirect_to_url: Some(helpers::create_startpay_url( server, &payment_attempt, &payment_intent, )), - }) - } + bank_transfer_steps_and_charges_details: bank_transfer_next_steps, + }); + }; + let mut response: api::PaymentsResponse = Default::default(); let routed_through = payment_attempt.connector.clone(); @@ -504,6 +518,29 @@ impl ForeignFrom for api::ephemeral_key::EphemeralK } } +pub fn bank_transfer_next_steps_check( + payment_attempt: storage::PaymentAttempt, +) -> RouterResult> { + let bank_transfer_next_step = if let Some(storage_models::enums::PaymentMethod::BankTransfer) = + payment_attempt.payment_method + { + let bank_transfer_next_steps: Option = + payment_attempt + .connector_metadata + .map(|metadata| { + metadata + .parse_value("NextStepsRequirements") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse the Value to NextRequirements struct") + }) + .transpose()?; + bank_transfer_next_steps + } else { + None + }; + Ok(bank_transfer_next_step) +} + #[derive(Clone)] pub struct PaymentAdditionalData<'a, F> where @@ -594,6 +631,7 @@ impl TryFrom> for types::PaymentsAuthoriz router_return_url, webhook_url, complete_authorize_url, + customer_id: None, }) } } @@ -801,3 +839,15 @@ impl TryFrom> for types::CompleteAuthoriz }) } } + +impl TryFrom> for types::PaymentsPreProcessingData { + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + let payment_data = additional_data.payment_data; + Ok(Self { + email: payment_data.email, + currency: Some(payment_data.currency), + }) + } +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index c409373496..a1df464adb 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -103,6 +103,7 @@ pub async fn construct_refund_router_data<'a, F>( reference_id: None, payment_method_token: None, connector_customer: None, + preprocessing_id: None, }; Ok(router_data) @@ -287,6 +288,7 @@ pub async fn construct_accept_dispute_router_data<'a>( payment_method_token: None, connector_customer: None, customer_id: None, + preprocessing_id: None, }; Ok(router_data) } @@ -345,6 +347,7 @@ pub async fn construct_submit_evidence_router_data<'a>( payment_method_token: None, connector_customer: None, customer_id: None, + preprocessing_id: None, }; Ok(router_data) } @@ -404,6 +407,7 @@ pub async fn construct_upload_file_router_data<'a>( payment_method_token: None, connector_customer: None, customer_id: None, + preprocessing_id: None, }; Ok(router_data) } @@ -465,6 +469,7 @@ pub async fn construct_defend_dispute_router_data<'a>( payment_method_token: None, customer_id: None, connector_customer: None, + preprocessing_id: None, }; Ok(router_data) } @@ -524,6 +529,7 @@ pub async fn construct_retrieve_file_router_data<'a>( session_token: None, reference_id: None, payment_method_token: None, + preprocessing_id: None, }; Ok(router_data) } diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 3c7b774d3e..ae697c7eb1 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -1,7 +1,7 @@ pub mod transformers; pub mod utils; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{report, IntoReport, ResultExt}; use masking::ExposeInterface; use router_env::{instrument, tracing}; @@ -197,7 +197,7 @@ pub async fn refunds_incoming_webhook_flow( } pub async fn get_payment_attempt_from_object_reference_id( - state: AppState, + state: &AppState, object_reference_id: api_models::webhooks::ObjectReferenceId, merchant_account: &storage::MerchantAccount, ) -> CustomResult { @@ -219,6 +219,14 @@ pub async fn get_payment_attempt_from_object_reference_id( ) .await .change_context(errors::WebhooksFlowError::ResourceNotFound), + api::ObjectReferenceId::PaymentId(api::PaymentIdType::PreprocessingId(ref id)) => db + .find_payment_attempt_by_preprocessing_id_merchant_id( + id, + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::WebhooksFlowError::ResourceNotFound), _ => Err(errors::WebhooksFlowError::ResourceNotFound).into_report(), } } @@ -311,7 +319,7 @@ pub async fn disputes_incoming_webhook_flow( .get_dispute_details(request_details) .change_context(errors::WebhooksFlowError::WebhookEventObjectCreationFailed)?; let payment_attempt = get_payment_attempt_from_object_reference_id( - state.clone(), + &state, webhook_details.object_reference_id, &merchant_account, ) @@ -359,6 +367,76 @@ pub async fn disputes_incoming_webhook_flow( } } +async fn bank_transfer_webhook_flow( + state: AppState, + merchant_account: storage::MerchantAccount, + webhook_details: api::IncomingWebhookDetails, + source_verified: bool, +) -> CustomResult<(), errors::WebhooksFlowError> { + let response = if source_verified { + let payment_attempt = get_payment_attempt_from_object_reference_id( + &state, + webhook_details.object_reference_id, + &merchant_account, + ) + .await?; + let payment_id = payment_attempt.payment_id; + let request = api::PaymentsRequest { + payment_id: Some(api_models::payments::PaymentIdType::PaymentIntentId( + payment_id, + )), + payment_token: payment_attempt.payment_token, + ..Default::default() + }; + payments::payments_core::( + &state, + merchant_account.to_owned(), + payments::PaymentConfirm, + request, + services::api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + ) + .await + .change_context(errors::WebhooksFlowError::PaymentsCoreFailed) + } else { + Err(report!( + errors::WebhooksFlowError::WebhookSourceVerificationFailed + )) + }; + + match response? { + services::ApplicationResponse::Json(payments_response) => { + let payment_id = payments_response + .payment_id + .clone() + .get_required_value("payment_id") + .change_context(errors::WebhooksFlowError::PaymentsCoreFailed)?; + + let event_type: enums::EventType = payments_response + .status + .foreign_try_into() + .into_report() + .change_context(errors::WebhooksFlowError::PaymentsCoreFailed)?; + + create_event_and_trigger_outgoing_webhook::( + state, + merchant_account, + event_type, + enums::EventClass::Payments, + None, + payment_id, + enums::EventObjectType::PaymentDetails, + api::OutgoingWebhookContent::PaymentDetails(payments_response), + ) + .await?; + } + + _ => Err(errors::WebhooksFlowError::PaymentsCoreFailed).into_report()?, + } + + Ok(()) +} + #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] pub async fn create_event_and_trigger_outgoing_webhook( @@ -601,6 +679,16 @@ pub async fn webhooks_core( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Incoming webhook flow for disputes failed")?, + api::WebhookFlow::BankTransfer => bank_transfer_webhook_flow::( + state.clone(), + merchant_account, + webhook_details, + source_verified, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Incoming bank-transfer webhook flow failed")?, + api::WebhookFlow::ReturnResponse => {} _ => Err(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/db/payment_attempt.rs b/crates/router/src/db/payment_attempt.rs index 331152afce..b576969023 100644 --- a/crates/router/src/db/payment_attempt.rs +++ b/crates/router/src/db/payment_attempt.rs @@ -55,6 +55,13 @@ pub trait PaymentAttemptInterface { merchant_id: &str, storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult; + + async fn find_payment_attempt_by_preprocessing_id_merchant_id( + &self, + preprocessing_id: &str, + merchant_id: &str, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult; } #[cfg(not(feature = "kv_store"))] @@ -169,6 +176,25 @@ mod storage { .map_err(Into::into) .into_report() } + + async fn find_payment_attempt_by_preprocessing_id_merchant_id( + &self, + preprocessing_id: &str, + merchant_id: &str, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + + PaymentAttempt::find_by_merchant_id_preprocessing_id( + &conn, + merchant_id, + preprocessing_id, + ) + .await + .map_err(Into::into) + .into_report() + } + async fn find_payment_attempt_by_attempt_id_merchant_id( &self, merchant_id: &str, @@ -208,6 +234,16 @@ impl PaymentAttemptInterface for MockDb { Err(errors::StorageError::MockDbError)? } + async fn find_payment_attempt_by_preprocessing_id_merchant_id( + &self, + _preprocessing_id: &str, + _merchant_id: &str, + _storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + // [#172]: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, _merchant_id: &str, @@ -266,6 +302,7 @@ impl PaymentAttemptInterface for MockDb { business_sub_label: payment_attempt.business_sub_label, straight_through_algorithm: payment_attempt.straight_through_algorithm, mandate_details: payment_attempt.mandate_details, + preprocessing_step_id: payment_attempt.preprocessing_step_id, }; payment_attempts.push(payment_attempt.clone()); Ok(payment_attempt) @@ -402,6 +439,7 @@ mod storage { .straight_through_algorithm .clone(), mandate_details: payment_attempt.mandate_details.clone(), + preprocessing_step_id: payment_attempt.preprocessing_step_id.clone(), }; let field = format!("pa_{}", created_attempt.attempt_id); @@ -473,6 +511,7 @@ mod storage { enums::MerchantStorageScheme::RedisKv => { let key = format!("{}_{}", this.merchant_id, this.payment_id); let old_connector_transaction_id = &this.connector_transaction_id; + let old_preprocessing_id = &this.preprocessing_step_id; let updated_attempt = payment_attempt.clone().apply_changeset(this.clone()); // Check for database presence as well Maybe use a read replica here ? let redis_value = serde_json::to_string(&updated_attempt) @@ -515,6 +554,32 @@ mod storage { (_, _) => {} } + match (old_preprocessing_id, &updated_attempt.preprocessing_step_id) { + (None, Some(preprocessing_id)) => { + add_preprocessing_id_to_reverse_lookup( + self, + key.as_str(), + this.merchant_id.as_str(), + updated_attempt.attempt_id.as_str(), + preprocessing_id.as_str(), + ) + .await?; + } + (Some(old_preprocessing_id), Some(preprocessing_id)) => { + if old_preprocessing_id.ne(preprocessing_id) { + add_preprocessing_id_to_reverse_lookup( + self, + key.as_str(), + this.merchant_id.as_str(), + updated_attempt.attempt_id.as_str(), + preprocessing_id.as_str(), + ) + .await?; + } + } + (_, _) => {} + } + let redis_entry = kv::TypedSql { op: kv::DBOperation::Update { updatable: kv::Updateable::PaymentAttemptUpdate( @@ -660,6 +725,41 @@ mod storage { } } + async fn find_payment_attempt_by_preprocessing_id_merchant_id( + &self, + preprocessing_id: &str, + merchant_id: &str, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult { + let database_call = || async { + let conn = connection::pg_connection_read(self).await?; + PaymentAttempt::find_by_merchant_id_preprocessing_id( + &conn, + merchant_id, + preprocessing_id, + ) + .await + .map_err(Into::into) + .into_report() + }; + match storage_scheme { + enums::MerchantStorageScheme::PostgresOnly => database_call().await, + enums::MerchantStorageScheme::RedisKv => { + let lookup_id = format!("{merchant_id}_{preprocessing_id}"); + let lookup = self.get_lookup_by_lookup_id(&lookup_id).await?; + let key = &lookup.pk_id; + + db_utils::try_redis_get_else_try_database_get( + self.redis_conn() + .map_err(Into::::into)? + .get_hash_field_and_deserialize(key, &lookup.sk_id, "PaymentAttempt"), + database_call, + ) + .await + } + } + } + async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id( &self, payment_id: &str, @@ -719,4 +819,26 @@ mod storage { .map_err(Into::::into) .into_report() } + + #[inline] + async fn add_preprocessing_id_to_reverse_lookup( + store: &Store, + key: &str, + merchant_id: &str, + updated_attempt_attempt_id: &str, + preprocessing_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(store).await?; + let field = format!("pa_{}", updated_attempt_attempt_id); + ReverseLookupNew { + lookup_id: format!("{}_{}", merchant_id, preprocessing_id), + pk_id: key.to_owned(), + sk_id: field.clone(), + source: "payment_attempt".to_string(), + } + .insert(&conn) + .await + .map_err(Into::::into) + .into_report() + } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 3b6e1f5019..dd0e0d0859 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -23,6 +23,8 @@ use crate::{core::errors, services}; pub type PaymentsAuthorizeRouterData = RouterData; +pub type PaymentsPreProcessingRouterData = + RouterData; pub type PaymentsAuthorizeSessionTokenRouterData = RouterData; pub type PaymentsCompleteAuthorizeRouterData = @@ -78,6 +80,11 @@ pub type PaymentsAuthorizeType = dyn services::ConnectorIntegration; pub type PaymentsVerifyType = dyn services::ConnectorIntegration; +pub type PaymentsPreProcessingType = dyn services::ConnectorIntegration< + api::PreProcessing, + PaymentsPreProcessingData, + PaymentsResponseData, +>; pub type PaymentsCompleteAuthorizeType = dyn services::ConnectorIntegration< api::CompleteAuthorize, CompleteAuthorizeData, @@ -186,6 +193,7 @@ pub struct RouterData { pub session_token: Option, pub reference_id: Option, pub payment_method_token: Option, + pub preprocessing_id: Option, /// Contains flow-specific data required to construct a request and send it to the connector. pub request: Request, @@ -222,6 +230,7 @@ pub struct PaymentsAuthorizeData { pub related_transaction_id: Option, pub payment_experience: Option, pub payment_method_type: Option, + pub customer_id: Option, } #[derive(Debug, Clone, Default)] @@ -247,6 +256,7 @@ pub struct ConnectorCustomerData { pub email: Option, pub phone: Option>, pub name: Option, + pub preprocessing_id: Option, } #[derive(Debug, Clone)] @@ -254,6 +264,12 @@ pub struct PaymentMethodTokenizationData { pub payment_method_data: payments::PaymentMethodData, } +#[derive(Debug, Clone)] +pub struct PaymentsPreProcessingData { + pub email: Option, + pub currency: Option, +} + #[derive(Debug, Clone)] pub struct CompleteAuthorizeData { pub payment_method_data: Option, @@ -372,6 +388,10 @@ pub enum PaymentsResponseData { enrolled_v2: bool, related_transaction_id: Option, }, + PreProcessingResponse { + pre_processing_id: String, + connector_metadata: Option, + }, } #[derive(Debug, Clone, Default)] @@ -670,6 +690,18 @@ impl From<&&mut PaymentsAuthorizeRouterData> for AuthorizeSessionTokenData { } } +impl From<&&mut PaymentsAuthorizeRouterData> for ConnectorCustomerData { + fn from(data: &&mut PaymentsAuthorizeRouterData) -> Self { + Self { + email: data.request.email.to_owned(), + preprocessing_id: data.preprocessing_id.to_owned(), + description: None, + phone: None, + name: None, + } + } +} + impl From<&VerifyRouterData> for PaymentsAuthorizeData { fn from(data: &VerifyRouterData) -> Self { Self { @@ -695,6 +727,7 @@ impl From<&VerifyRouterData> for PaymentsAuthorizeData { related_transaction_id: None, payment_experience: None, payment_method_type: None, + customer_id: None, } } } @@ -728,6 +761,7 @@ impl From<(&RouterData, T2)> reference_id: data.reference_id.clone(), customer_id: data.customer_id.clone(), payment_method_token: None, + preprocessing_id: None, connector_customer: data.connector_customer.clone(), } } diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 5ac327ed43..a1f6e1396b 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -67,6 +67,7 @@ pub struct AuthorizeSessionToken; #[derive(Debug, Clone)] pub struct CompleteAuthorize; + #[derive(Debug, Clone)] pub struct InitPayment; #[derive(Debug, Clone)] @@ -89,6 +90,9 @@ pub struct CreateConnectorCustomer; #[derive(Debug, Clone)] pub struct Verify; +#[derive(Debug, Clone)] +pub struct PreProcessing; + pub(crate) trait PaymentIdTypeExt { fn get_payment_intent_id(&self) -> errors::CustomResult; } @@ -97,13 +101,13 @@ impl PaymentIdTypeExt for PaymentIdType { fn get_payment_intent_id(&self) -> errors::CustomResult { match self { Self::PaymentIntentId(id) => Ok(id.clone()), - Self::ConnectorTransactionId(_) | Self::PaymentAttemptId(_) => { - Err(errors::ValidationError::IncorrectValueProvided { - field_name: "payment_id", - }) - .into_report() - .attach_printable("Expected payment intent ID but got connector transaction ID") - } + Self::ConnectorTransactionId(_) + | Self::PaymentAttemptId(_) + | Self::PreprocessingId(_) => Err(errors::ValidationError::IncorrectValueProvided { + field_name: "payment_id", + }) + .into_report() + .attach_printable("Expected payment intent ID but got connector transaction ID"), } } } @@ -181,6 +185,15 @@ pub trait ConnectorCustomer: { } +pub trait PaymentsPreProcessing: + api::ConnectorIntegration< + PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, +> +{ +} + pub trait Payment: api_types::ConnectorCommon + PaymentAuthorize @@ -191,6 +204,7 @@ pub trait Payment: + PreVerify + PaymentSession + PaymentToken + + PaymentsPreProcessing + ConnectorCustomer { } diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 74e9befb6a..2dd3eb35a0 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -62,6 +62,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { router_return_url: None, webhook_url: None, complete_authorize_url: None, + customer_id: None, }, response: Err(types::ErrorResponse::default()), payment_method_id: None, @@ -73,6 +74,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { reference_id: None, payment_method_token: None, connector_customer: None, + preprocessing_id: None, } } @@ -116,6 +118,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { reference_id: None, payment_method_token: None, connector_customer: None, + preprocessing_id: None, } } diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index ab37c8ff49..a9d3a03053 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -90,6 +90,7 @@ impl AdyenTest { router_return_url: Some(String::from("http://localhost:8080")), webhook_url: None, complete_authorize_url: None, + customer_id: None, }) } } diff --git a/crates/router/tests/connectors/bitpay.rs b/crates/router/tests/connectors/bitpay.rs index ebba241de1..e3e4f06efa 100644 --- a/crates/router/tests/connectors/bitpay.rs +++ b/crates/router/tests/connectors/bitpay.rs @@ -85,6 +85,7 @@ fn payment_method_details() -> Option { webhook_url: Some(String::from("https://google.com/")), complete_authorize_url: None, capture_method: None, + customer_id: None, }) } diff --git a/crates/router/tests/connectors/coinbase.rs b/crates/router/tests/connectors/coinbase.rs index 9ad2a4dd05..bbdb25f474 100644 --- a/crates/router/tests/connectors/coinbase.rs +++ b/crates/router/tests/connectors/coinbase.rs @@ -87,6 +87,7 @@ fn payment_method_details() -> Option { webhook_url: None, complete_authorize_url: None, capture_method: None, + customer_id: None, }) } diff --git a/crates/router/tests/connectors/opennode.rs b/crates/router/tests/connectors/opennode.rs index 06aeb5c4c2..c5be307172 100644 --- a/crates/router/tests/connectors/opennode.rs +++ b/crates/router/tests/connectors/opennode.rs @@ -86,6 +86,7 @@ fn payment_method_details() -> Option { webhook_url: Some(String::from("https://google.com/")), complete_authorize_url: None, capture_method: None, + customer_id: None, }) } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index a45038047b..8e87f19edd 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -402,6 +402,7 @@ pub trait ConnectorActions: Connector { reference_id: None, payment_method_token: None, connector_customer: None, + preprocessing_id: None, } } @@ -418,6 +419,7 @@ pub trait ConnectorActions: Connector { Ok(types::PaymentsResponseData::TokenizationResponse { .. }) => None, Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { .. }) => None, Ok(types::PaymentsResponseData::ConnectorCustomerResponse { .. }) => None, + Ok(types::PaymentsResponseData::PreProcessingResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Err(_) => None, } @@ -515,6 +517,7 @@ impl Default for PaymentAuthorizeType { router_return_url: None, complete_authorize_url: None, webhook_url: None, + customer_id: None, }; Self(data) } @@ -603,6 +606,7 @@ pub fn get_connector_transaction_id( Ok(types::PaymentsResponseData::SessionTokenResponse { .. }) => None, Ok(types::PaymentsResponseData::TokenizationResponse { .. }) => None, Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { .. }) => None, + Ok(types::PaymentsResponseData::PreProcessingResponse { .. }) => None, Ok(types::PaymentsResponseData::ConnectorCustomerResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Err(_) => None, diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 296d8e5eba..6b9414b6a2 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -93,6 +93,7 @@ impl WorldlineTest { router_return_url: None, webhook_url: None, complete_authorize_url: None, + customer_id: None, }) } } diff --git a/crates/storage_models/src/enums.rs b/crates/storage_models/src/enums.rs index 582afcaa82..55cd93ce79 100644 --- a/crates/storage_models/src/enums.rs +++ b/crates/storage_models/src/enums.rs @@ -459,6 +459,7 @@ pub enum PaymentMethod { PayLater, Wallet, BankRedirect, + BankTransfer, Crypto, BankDebit, } diff --git a/crates/storage_models/src/payment_attempt.rs b/crates/storage_models/src/payment_attempt.rs index e4154860ec..f7f265336a 100644 --- a/crates/storage_models/src/payment_attempt.rs +++ b/crates/storage_models/src/payment_attempt.rs @@ -46,6 +46,7 @@ pub struct PaymentAttempt { pub payment_method_data: Option, pub business_sub_label: Option, pub straight_through_algorithm: Option, + pub preprocessing_step_id: Option, // providing a location to store mandate details intermediately for transaction pub mandate_details: Option, } @@ -93,6 +94,7 @@ pub struct PaymentAttemptNew { pub payment_method_data: Option, pub business_sub_label: Option, pub straight_through_algorithm: Option, + pub preprocessing_step_id: Option, pub mandate_details: Option, } @@ -166,6 +168,12 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, }, + PreprocessingUpdate { + status: storage_enums::AttemptStatus, + payment_method_id: Option>, + connector_metadata: Option, + preprocessing_step_id: Option, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -192,6 +200,7 @@ pub struct PaymentAttemptUpdateInternal { payment_experience: Option, business_sub_label: Option, straight_through_algorithm: Option, + preprocessing_step_id: Option, } impl PaymentAttemptUpdate { @@ -214,6 +223,10 @@ impl PaymentAttemptUpdate { browser_info: pa_update.browser_info.or(source.browser_info), modified_at: common_utils::date_time::now(), payment_token: pa_update.payment_token.or(source.payment_token), + connector_metadata: pa_update.connector_metadata.or(source.connector_metadata), + preprocessing_step_id: pa_update + .preprocessing_step_id + .or(source.preprocessing_step_id), ..source } } @@ -364,6 +377,19 @@ impl From for PaymentAttemptUpdateInternal { error_message, ..Default::default() }, + PaymentAttemptUpdate::PreprocessingUpdate { + status, + payment_method_id, + connector_metadata, + preprocessing_step_id, + } => Self { + status: Some(status), + payment_method_id, + modified_at: Some(common_utils::date_time::now()), + connector_metadata, + preprocessing_step_id, + ..Default::default() + }, } } } diff --git a/crates/storage_models/src/query/payment_attempt.rs b/crates/storage_models/src/query/payment_attempt.rs index ec05c1b91d..7cb4078e66 100644 --- a/crates/storage_models/src/query/payment_attempt.rs +++ b/crates/storage_models/src/query/payment_attempt.rs @@ -142,6 +142,21 @@ impl PaymentAttempt { .await } + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_preprocessing_id( + conn: &PgPooledConn, + merchant_id: &str, + preprocessing_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::preprocessing_step_id.eq(preprocessing_id.to_owned())), + ) + .await + } + #[instrument(skip(conn))] pub async fn find_by_payment_id_merchant_id_attempt_id( conn: &PgPooledConn, diff --git a/crates/storage_models/src/schema.rs b/crates/storage_models/src/schema.rs index eaf872f1c9..01e7649952 100644 --- a/crates/storage_models/src/schema.rs +++ b/crates/storage_models/src/schema.rs @@ -321,6 +321,7 @@ diesel::table! { payment_method_data -> Nullable, business_sub_label -> Nullable, straight_through_algorithm -> Nullable, + preprocessing_step_id -> Nullable, mandate_details -> Nullable, } } diff --git a/migrations/2023-04-20-162755_add_preprocessing_step_id_payment_attempt/down.sql b/migrations/2023-04-20-162755_add_preprocessing_step_id_payment_attempt/down.sql new file mode 100644 index 0000000000..408777c8a1 --- /dev/null +++ b/migrations/2023-04-20-162755_add_preprocessing_step_id_payment_attempt/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP INDEX preprocessing_step_id_index; +ALTER TABLE payment_attempt DROP COLUMN preprocessing_step_id; diff --git a/migrations/2023-04-20-162755_add_preprocessing_step_id_payment_attempt/up.sql b/migrations/2023-04-20-162755_add_preprocessing_step_id_payment_attempt/up.sql new file mode 100644 index 0000000000..7a2edc5e3a --- /dev/null +++ b/migrations/2023-04-20-162755_add_preprocessing_step_id_payment_attempt/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE payment_attempt ADD COLUMN preprocessing_step_id VARCHAR DEFAULT NULL; +CREATE INDEX preprocessing_step_id_index ON payment_attempt (preprocessing_step_id);