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" } }
|
||||
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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
#[derive(
|
||||
Clone,
|
||||
|
||||
@ -680,6 +680,15 @@ pub enum BankDebitData {
|
||||
|
||||
#[schema(value_type = String, example = "John Doe")]
|
||||
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 {
|
||||
/// Billing details for bank debit
|
||||
|
||||
@ -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<api::RSync, types::RefundsData, types::RefundsResponse
|
||||
|
||||
#[async_trait::async_trait]
|
||||
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(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> 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(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> 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(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> 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 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<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)]
|
||||
#[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<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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user