Skip to content

Commit

Permalink
feat(connector): [Stax] Implement Bank Debits and Webhooks for Connec…
Browse files Browse the repository at this point in the history
…tor Stax (#1832)

Co-authored-by: Arjun Karthik <[email protected]>
  • Loading branch information
deepanshu-iiitu and ArjunKarthik authored Aug 8, 2023
1 parent 58a0cb7 commit 0f2bb6c
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 12 deletions.
2 changes: 1 addition & 1 deletion config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions crates/api_models/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 72 additions & 7 deletions crates/router/src/connector/stax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,7 +23,7 @@ use crate::{
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
ErrorResponse, Response,
domain, ErrorResponse, Response,
},
utils::{self, BytesExt},
};
Expand Down Expand Up @@ -751,24 +754,86 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse

#[async_trait::async_trait]
impl api::IncomingWebhook for Stax {
fn get_webhook_object_reference_id(
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<'_>,
) -> 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)
}
}
68 changes: 67 additions & 1 deletion crates/router/src/connector/stax/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -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()),
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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(_)
Expand Down Expand Up @@ -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,
}
17 changes: 16 additions & 1 deletion openapi/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
}
}
}
Expand Down

0 comments on commit 0f2bb6c

Please sign in to comment.