mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 01:57:45 +08:00 
			
		
		
		
	feat(connector): [Stax] Implement Bank Debits and Webhooks for Connector Stax (#1832)
Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com>
This commit is contained in:
		| @ -300,7 +300,7 @@ base_url = "" # Base url used when adding links that should redirect to self | |||||||
| stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } | stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } | ||||||
| checkout = { long_lived_token = false, payment_method = "wallet" } | checkout = { long_lived_token = false, payment_method = "wallet" } | ||||||
| mollie = {long_lived_token = false, payment_method = "card"} | mollie = {long_lived_token = false, payment_method = "card"} | ||||||
| stax = { long_lived_token = true, payment_method = "card" } | stax = { long_lived_token = true, payment_method = "card,bank_debit" } | ||||||
|  |  | ||||||
| [dummy_connector] | [dummy_connector] | ||||||
| payment_ttl = 172800             # Time to live for dummy connector payment in redis | payment_ttl = 172800             # Time to live for dummy connector payment in redis | ||||||
|  | |||||||
| @ -350,7 +350,7 @@ debit = { currency = "USD" } | |||||||
| [tokenization] | [tokenization] | ||||||
| stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } | stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } | ||||||
| checkout = { long_lived_token = false, payment_method = "wallet" } | checkout = { long_lived_token = false, payment_method = "wallet" } | ||||||
| stax = { long_lived_token = true, payment_method = "card" } | stax = { long_lived_token = true, payment_method = "card,bank_debit" } | ||||||
| mollie = {long_lived_token = false, payment_method = "card"} | mollie = {long_lived_token = false, payment_method = "card"} | ||||||
|  |  | ||||||
| [connector_customer] | [connector_customer] | ||||||
|  | |||||||
| @ -193,7 +193,7 @@ consumer_group = "SCHEDULER_GROUP" | |||||||
| stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } | stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } | ||||||
| checkout = { long_lived_token = false, payment_method = "wallet" } | checkout = { long_lived_token = false, payment_method = "wallet" } | ||||||
| mollie = {long_lived_token = false, payment_method = "card"} | mollie = {long_lived_token = false, payment_method = "card"} | ||||||
| stax = { long_lived_token = true, payment_method = "card" } | stax = { long_lived_token = true, payment_method = "card,bank_debit" } | ||||||
|  |  | ||||||
| [dummy_connector] | [dummy_connector] | ||||||
| payment_ttl = 172800 | payment_ttl = 172800 | ||||||
|  | |||||||
| @ -264,6 +264,46 @@ impl From<PayoutConnectors> for RoutableConnectors { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive( | ||||||
|  |     Clone, | ||||||
|  |     Copy, | ||||||
|  |     Debug, | ||||||
|  |     Eq, | ||||||
|  |     Hash, | ||||||
|  |     PartialEq, | ||||||
|  |     serde::Deserialize, | ||||||
|  |     serde::Serialize, | ||||||
|  |     strum::Display, | ||||||
|  |     strum::EnumString, | ||||||
|  |     ToSchema, | ||||||
|  | )] | ||||||
|  | #[strum(serialize_all = "snake_case")] | ||||||
|  | #[serde(rename_all = "snake_case")] | ||||||
|  | pub enum BankType { | ||||||
|  |     Checking, | ||||||
|  |     Savings, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive( | ||||||
|  |     Clone, | ||||||
|  |     Copy, | ||||||
|  |     Debug, | ||||||
|  |     Eq, | ||||||
|  |     Hash, | ||||||
|  |     PartialEq, | ||||||
|  |     serde::Deserialize, | ||||||
|  |     serde::Serialize, | ||||||
|  |     strum::Display, | ||||||
|  |     strum::EnumString, | ||||||
|  |     ToSchema, | ||||||
|  | )] | ||||||
|  | #[strum(serialize_all = "snake_case")] | ||||||
|  | #[serde(rename_all = "snake_case")] | ||||||
|  | pub enum BankHolderType { | ||||||
|  |     Personal, | ||||||
|  |     Business, | ||||||
|  | } | ||||||
|  |  | ||||||
| /// Name of banks supported by Hyperswitch | /// Name of banks supported by Hyperswitch | ||||||
| #[derive( | #[derive( | ||||||
|     Clone, |     Clone, | ||||||
|  | |||||||
| @ -680,6 +680,15 @@ pub enum BankDebitData { | |||||||
|  |  | ||||||
|         #[schema(value_type = String, example = "John Doe")] |         #[schema(value_type = String, example = "John Doe")] | ||||||
|         bank_account_holder_name: Option<Secret<String>>, |         bank_account_holder_name: Option<Secret<String>>, | ||||||
|  |  | ||||||
|  |         #[schema(value_type = String, example = "ACH")] | ||||||
|  |         bank_name: Option<enums::BankNames>, | ||||||
|  |  | ||||||
|  |         #[schema(value_type = String, example = "Checking")] | ||||||
|  |         bank_type: Option<enums::BankType>, | ||||||
|  |  | ||||||
|  |         #[schema(value_type = String, example = "Personal")] | ||||||
|  |         bank_holder_type: Option<enums::BankHolderType>, | ||||||
|     }, |     }, | ||||||
|     SepaBankDebit { |     SepaBankDebit { | ||||||
|         /// Billing details for bank debit |         /// Billing details for bank debit | ||||||
|  | |||||||
| @ -2,15 +2,18 @@ pub mod transformers; | |||||||
|  |  | ||||||
| use std::fmt::Debug; | use std::fmt::Debug; | ||||||
|  |  | ||||||
|  | use common_utils::ext_traits::ByteSliceExt; | ||||||
| use error_stack::{IntoReport, ResultExt}; | use error_stack::{IntoReport, ResultExt}; | ||||||
| use masking::PeekInterface; | use masking::PeekInterface; | ||||||
| use transformers as stax; | use transformers as stax; | ||||||
|  |  | ||||||
|  | use self::stax::StaxWebhookEventType; | ||||||
| use super::utils::{to_connector_meta, RefundsRequestData}; | use super::utils::{to_connector_meta, RefundsRequestData}; | ||||||
| use crate::{ | use crate::{ | ||||||
|     configs::settings, |     configs::settings, | ||||||
|     consts, |     consts, | ||||||
|     core::errors::{self, CustomResult}, |     core::errors::{self, CustomResult}, | ||||||
|  |     db::StorageInterface, | ||||||
|     headers, |     headers, | ||||||
|     services::{ |     services::{ | ||||||
|         self, |         self, | ||||||
| @ -20,7 +23,7 @@ use crate::{ | |||||||
|     types::{ |     types::{ | ||||||
|         self, |         self, | ||||||
|         api::{self, ConnectorCommon, ConnectorCommonExt}, |         api::{self, ConnectorCommon, ConnectorCommonExt}, | ||||||
|         ErrorResponse, Response, |         domain, ErrorResponse, Response, | ||||||
|     }, |     }, | ||||||
|     utils::{self, BytesExt}, |     utils::{self, BytesExt}, | ||||||
| }; | }; | ||||||
| @ -751,24 +754,86 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse | |||||||
|  |  | ||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| impl api::IncomingWebhook for Stax { | impl api::IncomingWebhook for Stax { | ||||||
|  |     async fn verify_webhook_source( | ||||||
|  |         &self, | ||||||
|  |         _db: &dyn StorageInterface, | ||||||
|  |         _request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|  |         _merchant_id: &str, | ||||||
|  |         _connector_label: &str, | ||||||
|  |         _key_store: &domain::MerchantKeyStore, | ||||||
|  |     ) -> CustomResult<bool, errors::ConnectorError> { | ||||||
|  |         Ok(false) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     fn get_webhook_object_reference_id( |     fn get_webhook_object_reference_id( | ||||||
|         &self, |         &self, | ||||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|     ) -> CustomResult<api::webhooks::ObjectReferenceId, errors::ConnectorError> { |     ) -> CustomResult<api::webhooks::ObjectReferenceId, errors::ConnectorError> { | ||||||
|         Err(errors::ConnectorError::WebhooksNotImplemented).into_report() |         let webhook_body: stax::StaxWebhookBody = request | ||||||
|  |             .body | ||||||
|  |             .parse_struct("StaxWebhookBody") | ||||||
|  |             .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; | ||||||
|  |  | ||||||
|  |         match webhook_body.transaction_type { | ||||||
|  |             stax::StaxWebhookEventType::Refund => { | ||||||
|  |                 Ok(api_models::webhooks::ObjectReferenceId::RefundId( | ||||||
|  |                     api_models::webhooks::RefundIdType::ConnectorRefundId(webhook_body.id), | ||||||
|  |                 )) | ||||||
|  |             } | ||||||
|  |             stax::StaxWebhookEventType::Unknown => { | ||||||
|  |                 Err(errors::ConnectorError::WebhookEventTypeNotFound.into()) | ||||||
|  |             } | ||||||
|  |             stax::StaxWebhookEventType::PreAuth | ||||||
|  |             | stax::StaxWebhookEventType::Capture | ||||||
|  |             | stax::StaxWebhookEventType::Charge | ||||||
|  |             | stax::StaxWebhookEventType::Void => { | ||||||
|  |                 Ok(api_models::webhooks::ObjectReferenceId::PaymentId( | ||||||
|  |                     api_models::payments::PaymentIdType::ConnectorTransactionId(match webhook_body | ||||||
|  |                         .transaction_type | ||||||
|  |                     { | ||||||
|  |                         stax::StaxWebhookEventType::Capture => webhook_body | ||||||
|  |                             .auth_id | ||||||
|  |                             .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?, | ||||||
|  |                         _ => webhook_body.id, | ||||||
|  |                     }), | ||||||
|  |                 )) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn get_webhook_event_type( |     fn get_webhook_event_type( | ||||||
|         &self, |         &self, | ||||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|     ) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> { |     ) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> { | ||||||
|         Err(errors::ConnectorError::WebhooksNotImplemented).into_report() |         let details: stax::StaxWebhookBody = request | ||||||
|  |             .body | ||||||
|  |             .parse_struct("StaxWebhookEventType") | ||||||
|  |             .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; | ||||||
|  |  | ||||||
|  |         Ok(match &details.transaction_type { | ||||||
|  |             StaxWebhookEventType::Refund => match &details.success { | ||||||
|  |                 true => api::IncomingWebhookEvent::RefundSuccess, | ||||||
|  |                 false => api::IncomingWebhookEvent::RefundFailure, | ||||||
|  |             }, | ||||||
|  |             StaxWebhookEventType::Capture | StaxWebhookEventType::Charge => { | ||||||
|  |                 match &details.success { | ||||||
|  |                     true => api::IncomingWebhookEvent::PaymentIntentSuccess, | ||||||
|  |                     false => api::IncomingWebhookEvent::PaymentIntentFailure, | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             StaxWebhookEventType::PreAuth | ||||||
|  |             | StaxWebhookEventType::Void | ||||||
|  |             | StaxWebhookEventType::Unknown => api::IncomingWebhookEvent::EventNotSupported, | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn get_webhook_resource_object( |     fn get_webhook_resource_object( | ||||||
|         &self, |         &self, | ||||||
|         _request: &api::IncomingWebhookRequestDetails<'_>, |         request: &api::IncomingWebhookRequestDetails<'_>, | ||||||
|     ) -> CustomResult<serde_json::Value, errors::ConnectorError> { |     ) -> CustomResult<serde_json::Value, errors::ConnectorError> { | ||||||
|         Err(errors::ConnectorError::WebhooksNotImplemented).into_report() |         let reference_object: serde_json::Value = serde_json::from_slice(request.body) | ||||||
|  |             .into_report() | ||||||
|  |             .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; | ||||||
|  |         Ok(reference_object) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ use masking::{ExposeInterface, Secret}; | |||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     connector::utils::{CardData, PaymentsAuthorizeRequestData, RouterData}, |     connector::utils::{missing_field_err, CardData, PaymentsAuthorizeRequestData, RouterData}, | ||||||
|     core::errors, |     core::errors, | ||||||
|     types::{self, api, storage::enums}, |     types::{self, api, storage::enums}, | ||||||
| }; | }; | ||||||
| @ -37,6 +37,16 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for StaxPaymentsRequest { | |||||||
|                     payment_method_id: Secret::new(item.get_payment_method_token()?), |                     payment_method_id: Secret::new(item.get_payment_method_token()?), | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|  |             api::PaymentMethodData::BankDebit(_) => { | ||||||
|  |                 let pre_auth = !item.request.is_auto_capture()?; | ||||||
|  |                 Ok(Self { | ||||||
|  |                     meta: StaxPaymentsRequestMetaData { tax: 0 }, | ||||||
|  |                     total: item.request.amount, | ||||||
|  |                     is_refundable: true, | ||||||
|  |                     pre_auth, | ||||||
|  |                     payment_method_id: Secret::new(item.get_payment_method_token()?), | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|             _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), |             _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -115,11 +125,23 @@ pub struct StaxTokenizeData { | |||||||
|     customer_id: Secret<String>, |     customer_id: Secret<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize)] | ||||||
|  | pub struct StaxBankTokenizeData { | ||||||
|  |     person_name: Secret<String>, | ||||||
|  |     bank_account: Secret<String>, | ||||||
|  |     bank_routing: Secret<String>, | ||||||
|  |     bank_name: api_models::enums::BankNames, | ||||||
|  |     bank_type: api_models::enums::BankType, | ||||||
|  |     bank_holder_type: api_models::enums::BankHolderType, | ||||||
|  |     customer_id: Secret<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize)] | #[derive(Debug, Serialize)] | ||||||
| #[serde(tag = "method")] | #[serde(tag = "method")] | ||||||
| #[serde(rename_all = "lowercase")] | #[serde(rename_all = "lowercase")] | ||||||
| pub enum StaxTokenRequest { | pub enum StaxTokenRequest { | ||||||
|     Card(StaxTokenizeData), |     Card(StaxTokenizeData), | ||||||
|  |     Bank(StaxBankTokenizeData), | ||||||
| } | } | ||||||
|  |  | ||||||
| impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { | impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { | ||||||
| @ -138,6 +160,29 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { | |||||||
|                 }; |                 }; | ||||||
|                 Ok(Self::Card(stax_card_data)) |                 Ok(Self::Card(stax_card_data)) | ||||||
|             } |             } | ||||||
|  |             api_models::payments::PaymentMethodData::BankDebit( | ||||||
|  |                 api_models::payments::BankDebitData::AchBankDebit { | ||||||
|  |                     billing_details, | ||||||
|  |                     account_number, | ||||||
|  |                     routing_number, | ||||||
|  |                     bank_name, | ||||||
|  |                     bank_type, | ||||||
|  |                     bank_holder_type, | ||||||
|  |                     .. | ||||||
|  |                 }, | ||||||
|  |             ) => { | ||||||
|  |                 let stax_bank_data = StaxBankTokenizeData { | ||||||
|  |                     person_name: billing_details.name, | ||||||
|  |                     bank_account: account_number, | ||||||
|  |                     bank_routing: routing_number, | ||||||
|  |                     bank_name: bank_name.ok_or_else(missing_field_err("bank_name"))?, | ||||||
|  |                     bank_type: bank_type.ok_or_else(missing_field_err("bank_type"))?, | ||||||
|  |                     bank_holder_type: bank_holder_type | ||||||
|  |                         .ok_or_else(missing_field_err("bank_holder_type"))?, | ||||||
|  |                     customer_id: Secret::new(customer_id), | ||||||
|  |                 }; | ||||||
|  |                 Ok(Self::Bank(stax_bank_data)) | ||||||
|  |             } | ||||||
|             api::PaymentMethodData::BankDebit(_) |             api::PaymentMethodData::BankDebit(_) | ||||||
|             | api::PaymentMethodData::Wallet(_) |             | api::PaymentMethodData::Wallet(_) | ||||||
|             | api::PaymentMethodData::PayLater(_) |             | api::PaymentMethodData::PayLater(_) | ||||||
| @ -355,3 +400,24 @@ impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>> | |||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Deserialize)] | ||||||
|  | #[serde(rename_all = "snake_case")] | ||||||
|  | pub enum StaxWebhookEventType { | ||||||
|  |     PreAuth, | ||||||
|  |     Capture, | ||||||
|  |     Charge, | ||||||
|  |     Void, | ||||||
|  |     Refund, | ||||||
|  |     #[serde(other)] | ||||||
|  |     Unknown, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Deserialize)] | ||||||
|  | pub struct StaxWebhookBody { | ||||||
|  |     #[serde(rename = "type")] | ||||||
|  |     pub transaction_type: StaxWebhookEventType, | ||||||
|  |     pub id: String, | ||||||
|  |     pub auth_id: Option<String>, | ||||||
|  |     pub success: bool, | ||||||
|  | } | ||||||
|  | |||||||
| @ -2485,7 +2485,10 @@ | |||||||
|                   "account_number", |                   "account_number", | ||||||
|                   "routing_number", |                   "routing_number", | ||||||
|                   "card_holder_name", |                   "card_holder_name", | ||||||
|                   "bank_account_holder_name" |                   "bank_account_holder_name", | ||||||
|  |                   "bank_name", | ||||||
|  |                   "bank_type", | ||||||
|  |                   "bank_holder_type" | ||||||
|                 ], |                 ], | ||||||
|                 "properties": { |                 "properties": { | ||||||
|                   "billing_details": { |                   "billing_details": { | ||||||
| @ -2508,6 +2511,18 @@ | |||||||
|                   "bank_account_holder_name": { |                   "bank_account_holder_name": { | ||||||
|                     "type": "string", |                     "type": "string", | ||||||
|                     "example": "John Doe" |                     "example": "John Doe" | ||||||
|  |                   }, | ||||||
|  |                   "bank_name": { | ||||||
|  |                     "type": "string", | ||||||
|  |                     "example": "ACH" | ||||||
|  |                   }, | ||||||
|  |                   "bank_type": { | ||||||
|  |                     "type": "string", | ||||||
|  |                     "example": "Checking" | ||||||
|  |                   }, | ||||||
|  |                   "bank_holder_type": { | ||||||
|  |                     "type": "string", | ||||||
|  |                     "example": "Personal" | ||||||
|                   } |                   } | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 DEEPANSHU BANSAL
					DEEPANSHU BANSAL