mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +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