diff --git a/config/config.example.toml b/config/config.example.toml index 0daece8a0a9..c89e33b2e0d 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -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" } } checkout = { long_lived_token = false, payment_method = "wallet" } 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] payment_ttl = 172800 # Time to live for dummy connector payment in redis diff --git a/config/development.toml b/config/development.toml index 920ad46804b..8591e3dae22 100644 --- a/config/development.toml +++ b/config/development.toml @@ -350,7 +350,7 @@ debit = { currency = "USD" } [tokenization] 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" } -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"} [connector_customer] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index b5f23f5015e..c4c8052fc6b 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -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" } } checkout = { long_lived_token = false, payment_method = "wallet" } 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] payment_ttl = 172800 diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index ec1d77aa6bd..833eba4a04a 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -264,6 +264,46 @@ impl From 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 #[derive( Clone, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index ad2859d330f..32e40540385 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -680,6 +680,15 @@ pub enum BankDebitData { #[schema(value_type = String, example = "John Doe")] bank_account_holder_name: Option>, + + #[schema(value_type = String, example = "ACH")] + bank_name: Option, + + #[schema(value_type = String, example = "Checking")] + bank_type: Option, + + #[schema(value_type = String, example = "Personal")] + bank_holder_type: Option, }, SepaBankDebit { /// Billing details for bank debit diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 2795a0bf929..4f16c914132 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -2,15 +2,18 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::ext_traits::ByteSliceExt; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as stax; +use self::stax::StaxWebhookEventType; use super::utils::{to_connector_meta, RefundsRequestData}; use crate::{ configs::settings, consts, core::errors::{self, CustomResult}, + db::StorageInterface, headers, services::{ self, @@ -20,7 +23,7 @@ use crate::{ types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, - ErrorResponse, Response, + domain, ErrorResponse, Response, }, utils::{self, BytesExt}, }; @@ -751,24 +754,86 @@ impl ConnectorIntegration, + _merchant_id: &str, + _connector_label: &str, + _key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + Ok(false) + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - 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( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - 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( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - 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) } } diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index a04a1e43652..7cb3e8dda27 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -4,7 +4,7 @@ use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{CardData, PaymentsAuthorizeRequestData, RouterData}, + connector::utils::{missing_field_err, CardData, PaymentsAuthorizeRequestData, RouterData}, core::errors, 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()?), }) } + 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()), } } @@ -115,11 +125,23 @@ pub struct StaxTokenizeData { customer_id: Secret, } +#[derive(Debug, Serialize)] +pub struct StaxBankTokenizeData { + person_name: Secret, + bank_account: Secret, + bank_routing: Secret, + bank_name: api_models::enums::BankNames, + bank_type: api_models::enums::BankType, + bank_holder_type: api_models::enums::BankHolderType, + customer_id: Secret, +} + #[derive(Debug, Serialize)] #[serde(tag = "method")] #[serde(rename_all = "lowercase")] pub enum StaxTokenRequest { Card(StaxTokenizeData), + Bank(StaxBankTokenizeData), } impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { @@ -138,6 +160,29 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { }; 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::Wallet(_) | api::PaymentMethodData::PayLater(_) @@ -355,3 +400,24 @@ impl TryFrom> }) } } + +#[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, + pub success: bool, +} diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index a0d2ca9637c..948ed7518ad 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -2485,7 +2485,10 @@ "account_number", "routing_number", "card_holder_name", - "bank_account_holder_name" + "bank_account_holder_name", + "bank_name", + "bank_type", + "bank_holder_type" ], "properties": { "billing_details": { @@ -2508,6 +2511,18 @@ "bank_account_holder_name": { "type": "string", "example": "John Doe" + }, + "bank_name": { + "type": "string", + "example": "ACH" + }, + "bank_type": { + "type": "string", + "example": "Checking" + }, + "bank_holder_type": { + "type": "string", + "example": "Personal" } } }