feat(checkbook_io): connector integrate ACH (#8730)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Nithin N
2025-08-08 18:18:14 +05:30
committed by GitHub
parent 3547eac397
commit bee4aed40c
25 changed files with 584 additions and 615 deletions

View File

@ -80,7 +80,7 @@ pub enum RoutableConnectors {
Celero,
Chargebee,
Custombilling,
// Checkbook,
Checkbook,
Checkout,
Coinbase,
Coingate,
@ -246,7 +246,7 @@ pub enum Connector {
Cashtocode,
Celero,
Chargebee,
// Checkbook,
Checkbook,
Checkout,
Coinbase,
Coingate,
@ -440,7 +440,7 @@ impl Connector {
| Self::Cashtocode
| Self::Celero
| Self::Chargebee
// | Self::Checkbook
| Self::Checkbook
| Self::Coinbase
| Self::Coingate
| Self::Cryptopay
@ -613,7 +613,7 @@ impl From<RoutableConnectors> for Connector {
RoutableConnectors::Celero => Self::Celero,
RoutableConnectors::Chargebee => Self::Chargebee,
RoutableConnectors::Custombilling => Self::Custombilling,
// RoutableConnectors::Checkbook => Self::Checkbook,
RoutableConnectors::Checkbook => Self::Checkbook,
RoutableConnectors::Checkout => Self::Checkout,
RoutableConnectors::Coinbase => Self::Coinbase,
RoutableConnectors::Cryptopay => Self::Cryptopay,
@ -742,7 +742,7 @@ impl TryFrom<Connector> for RoutableConnectors {
Connector::Cashtocode => Ok(Self::Cashtocode),
Connector::Celero => Ok(Self::Celero),
Connector::Chargebee => Ok(Self::Chargebee),
// Connector::Checkbook => Ok(Self::Checkbook),
Connector::Checkbook => Ok(Self::Checkbook),
Connector::Checkout => Ok(Self::Checkout),
Connector::Coinbase => Ok(Self::Coinbase),
Connector::Coingate => Ok(Self::Coingate),

View File

@ -413,6 +413,7 @@ impl ConnectorConfig {
Connector::Cashtocode => Ok(connector_data.cashtocode),
Connector::Celero => Ok(connector_data.celero),
Connector::Chargebee => Ok(connector_data.chargebee),
Connector::Checkbook => Ok(connector_data.checkbook),
Connector::Checkout => Ok(connector_data.checkout),
Connector::Coinbase => Ok(connector_data.coinbase),
Connector::Coingate => Ok(connector_data.coingate),

View File

@ -1446,6 +1446,17 @@ merchant_id_evoucher="MerchantId Evoucher"
[cashtocode.connector_webhook_details]
merchant_secret="Source verification key"
[checkbook]
[[checkbook.bank_transfer]]
payment_method_type = "ach"
[checkbook.connector_auth.BodyKey]
key1 = "Checkbook Publishable key"
api_key = "Checkbook API Secret key"
[checkbook.connector_webhook_details]
merchant_secret="Source verification key"
[checkout]
[[checkout.credit]]
payment_method_type = "Mastercard"

View File

@ -1212,6 +1212,15 @@ key1 = "Secret Key"
[cryptopay.connector_webhook_details]
merchant_secret = "Source verification key"
[checkbook]
[[checkbook.bank_transfer]]
payment_method_type = "ach"
[checkbook.connector_auth.BodyKey]
key1 = "Checkbook Publishable key"
api_key = "Checkbook API Secret key"
[checkbook.connector_webhook_details]
merchant_secret="Source verification key"
[checkout]
[[checkout.credit]]
payment_method_type = "Mastercard"

View File

@ -1445,6 +1445,15 @@ merchant_id_evoucher = "MerchantId Evoucher"
[cashtocode.connector_webhook_details]
merchant_secret = "Source verification key"
[checkbook]
[[checkbook.bank_transfer]]
payment_method_type = "ach"
[checkbook.connector_auth.BodyKey]
key1 = "Checkbook Publishable key"
api_key = "Checkbook API Secret key"
[checkbook.connector_webhook_details]
merchant_secret="Source verification key"
[checkout]
[[checkout.credit]]
payment_method_type = "Mastercard"

View File

@ -1,12 +1,16 @@
pub mod transformers;
use std::sync::LazyLock;
use api_models::{enums, payments::PaymentIdType};
use common_utils::{
crypto,
errors::CustomResult,
ext_traits::BytesExt,
ext_traits::{ByteSliceExt, BytesExt},
request::{Method, Request, RequestBuilder, RequestContent},
types::{AmountConvertor, FloatMajorUnit, FloatMajorUnitForConnector},
};
use error_stack::{report, ResultExt};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData},
router_flow_types::{
@ -19,9 +23,12 @@ use hyperswitch_domain_models::{
PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData,
RefundsData, SetupMandateRequestData,
},
router_response_types::{PaymentsResponseData, RefundsResponseData},
router_response_types::{
ConnectorInfo, PaymentMethodDetails, PaymentsResponseData, RefundsResponseData,
SupportedPaymentMethods, SupportedPaymentMethodsExt,
},
types::{
PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData,
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsSyncRouterData,
RefundSyncRouterData, RefundsRouterData,
},
};
@ -39,7 +46,7 @@ use hyperswitch_interfaces::{
use masking::{ExposeInterface, Mask};
use transformers as checkbook;
use crate::{constants::headers, types::ResponseRouterData, utils};
use crate::{constants::headers, types::ResponseRouterData};
#[derive(Clone)]
pub struct Checkbook {
@ -115,9 +122,14 @@ impl ConnectorCommon for Checkbook {
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
let auth = checkbook::CheckbookAuthType::try_from(auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
let auth_key = format!(
"{}:{}",
auth.publishable_key.expose(),
auth.secret_key.expose()
);
Ok(vec![(
headers::AUTHORIZATION.to_string(),
auth.api_key.expose().into_masked(),
auth_key.into_masked(),
)])
}
@ -148,9 +160,7 @@ impl ConnectorCommon for Checkbook {
}
}
impl ConnectorValidation for Checkbook {
//TODO: implement functions when support enabled
}
impl ConnectorValidation for Checkbook {}
impl ConnectorIntegration<Session, PaymentsSessionData, PaymentsResponseData> for Checkbook {
//TODO: implement sessions flow
@ -179,9 +189,9 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
fn get_url(
&self,
_req: &PaymentsAuthorizeRouterData,
_connectors: &Connectors,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
Ok(format!("{}/v3/invoice", self.base_url(connectors)))
}
fn get_request_body(
@ -189,14 +199,11 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
req: &PaymentsAuthorizeRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let amount = utils::convert_amount(
self.amount_converter,
req.request.minor_amount,
req.request.currency,
)?;
let connector_router_data = checkbook::CheckbookRouterData::from((amount, req));
let connector_req = checkbook::CheckbookPaymentsRequest::try_from(&connector_router_data)?;
let amount = self
.amount_converter
.convert(req.request.minor_amount, req.request.currency)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let connector_req = checkbook::CheckbookPaymentsRequest::try_from((amount, req))?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
@ -265,10 +272,19 @@ impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Che
fn get_url(
&self,
_req: &PaymentsSyncRouterData,
_connectors: &Connectors,
req: &PaymentsSyncRouterData,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
let connector_txn_id = req
.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
Ok(format!(
"{}/v3/invoice/{}",
self.base_url(connectors),
connector_txn_id
))
}
fn build_request(
@ -314,64 +330,53 @@ impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Che
}
}
impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> for Checkbook {
impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> for Checkbook {}
impl ConnectorIntegration<Void, PaymentsCancelData, PaymentsResponseData> for Checkbook {
fn get_headers(
&self,
req: &PaymentsCaptureRouterData,
req: &PaymentsCancelRouterData,
connectors: &Connectors,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &PaymentsCaptureRouterData,
_connectors: &Connectors,
req: &PaymentsCancelRouterData,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
}
fn get_request_body(
&self,
_req: &PaymentsCaptureRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into())
Ok(format!(
"{}v3/invoice/{}",
self.base_url(connectors),
req.request.connector_transaction_id
))
}
fn build_request(
&self,
req: &PaymentsCaptureRouterData,
req: &PaymentsCancelRouterData,
connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Ok(Some(
RequestBuilder::new()
.method(Method::Post)
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
.method(Method::Delete)
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.set_body(types::PaymentsCaptureType::get_request_body(
self, req, connectors,
)?)
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.build(),
))
}
fn handle_response(
&self,
data: &PaymentsCaptureRouterData,
data: &PaymentsCancelRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsCaptureRouterData, errors::ConnectorError> {
) -> CustomResult<PaymentsCancelRouterData, errors::ConnectorError> {
let response: checkbook::CheckbookPaymentsResponse = res
.response
.parse_struct("Checkbook PaymentsCaptureResponse")
.parse_struct("Checkbook PaymentsCancelResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
@ -391,8 +396,6 @@ impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> fo
}
}
impl ConnectorIntegration<Void, PaymentsCancelData, PaymentsResponseData> for Checkbook {}
impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Checkbook {
fn get_headers(
&self,
@ -411,23 +414,23 @@ impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Checkbo
_req: &RefundsRouterData<Execute>,
_connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
Err(errors::ConnectorError::NotSupported {
message: "Refunds are not supported".to_string(),
connector: "checkbook",
}
.into())
}
fn get_request_body(
&self,
req: &RefundsRouterData<Execute>,
_req: &RefundsRouterData<Execute>,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let refund_amount = utils::convert_amount(
self.amount_converter,
req.request.minor_refund_amount,
req.request.currency,
)?;
let connector_router_data = checkbook::CheckbookRouterData::from((refund_amount, req));
let connector_req = checkbook::CheckbookRefundRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
Err(errors::ConnectorError::NotSupported {
message: "Refunds are not supported".to_string(),
connector: "checkbook",
}
.into())
}
fn build_request(
@ -451,21 +454,11 @@ impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Checkbo
fn handle_response(
&self,
data: &RefundsRouterData<Execute>,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
_data: &RefundsRouterData<Execute>,
_event_builder: Option<&mut ConnectorEvent>,
_res: Response,
) -> CustomResult<RefundsRouterData<Execute>, errors::ConnectorError> {
let response: checkbook::RefundResponse = res
.response
.parse_struct("checkbook RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
Err(errors::ConnectorError::NotImplemented("Refunds are not supported".to_string()).into())
}
fn get_error_response(
@ -518,21 +511,11 @@ impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Checkbook
fn handle_response(
&self,
data: &RefundSyncRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
_data: &RefundSyncRouterData,
_event_builder: Option<&mut ConnectorEvent>,
_res: Response,
) -> CustomResult<RefundSyncRouterData, errors::ConnectorError> {
let response: checkbook::RefundResponse = res
.response
.parse_struct("checkbook RefundSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
Err(errors::ConnectorError::NotImplemented("Refunds are not supported".to_string()).into())
}
fn get_error_response(
@ -548,24 +531,128 @@ impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Checkbook
impl webhooks::IncomingWebhook for Checkbook {
fn get_webhook_object_reference_id(
&self,
_request: &webhooks::IncomingWebhookRequestDetails<'_>,
request: &webhooks::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
Err(report!(errors::ConnectorError::WebhooksNotImplemented))
let details: checkbook::CheckbookPaymentsResponse = request
.body
.parse_struct("CheckbookWebhookResponse")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
PaymentIdType::ConnectorTransactionId(details.id),
))
}
fn get_webhook_event_type(
&self,
_request: &webhooks::IncomingWebhookRequestDetails<'_>,
request: &webhooks::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::IncomingWebhookEvent, errors::ConnectorError> {
Err(report!(errors::ConnectorError::WebhooksNotImplemented))
let details: checkbook::CheckbookPaymentsResponse = request
.body
.parse_struct("CheckbookWebhookResponse")
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
Ok(api_models::webhooks::IncomingWebhookEvent::from(
details.status,
))
}
fn get_webhook_resource_object(
&self,
_request: &webhooks::IncomingWebhookRequestDetails<'_>,
request: &webhooks::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
Err(report!(errors::ConnectorError::WebhooksNotImplemented))
let details: checkbook::CheckbookPaymentsResponse = request
.body
.parse_struct("CheckbookWebhookResponse")
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
Ok(Box::new(details))
}
fn get_webhook_source_verification_algorithm(
&self,
_request: &webhooks::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
Ok(Box::new(crypto::HmacSha256))
}
fn get_webhook_source_verification_signature(
&self,
request: &webhooks::IncomingWebhookRequestDetails<'_>,
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let header_value = request
.headers
.get("signature")
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)
.attach_printable("Failed to get signature for checkbook")?
.to_str()
.map_err(|_| errors::ConnectorError::WebhookSignatureNotFound)
.attach_printable("Failed to get signature for checkbook")?;
let signature = header_value
.split(',')
.find_map(|s| s.strip_prefix("signature="))
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)?;
hex::decode(signature)
.change_context(errors::ConnectorError::WebhookSignatureNotFound)
.attach_printable("Failed to decrypt checkbook webhook payload for verification")
}
fn get_webhook_source_verification_message(
&self,
request: &webhooks::IncomingWebhookRequestDetails<'_>,
_merchant_id: &common_utils::id_type::MerchantId,
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let header_value = request
.headers
.get("signature")
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)?
.to_str()
.map_err(|_| errors::ConnectorError::WebhookSignatureNotFound)?;
let nonce = header_value
.split(',')
.find_map(|s| s.strip_prefix("nonce="))
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)?;
let message = format!("{}{}", String::from_utf8_lossy(request.body), nonce);
Ok(message.into_bytes())
}
}
impl ConnectorSpecifications for Checkbook {}
static CHECKBOOK_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> =
LazyLock::new(|| {
let supported_capture_methods = vec![enums::CaptureMethod::Automatic];
let mut checkbook_supported_payment_methods = SupportedPaymentMethods::new();
checkbook_supported_payment_methods.add(
enums::PaymentMethod::BankTransfer,
enums::PaymentMethodType::Ach,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
supported_capture_methods,
specific_features: None,
},
);
checkbook_supported_payment_methods
});
static CHECKBOOK_CONNECTOR_INFO: ConnectorInfo = ConnectorInfo {
display_name: "Checkbook",
description:
"Checkbook is a payment platform that allows users to send and receive digital checks.",
connector_type: enums::PaymentConnectorCategory::PaymentGateway,
};
static CHECKBOOK_SUPPORTED_WEBHOOK_FLOWS: [enums::EventClass; 1] = [enums::EventClass::Payments];
impl ConnectorSpecifications for Checkbook {
fn get_connector_about(&self) -> Option<&'static ConnectorInfo> {
Some(&CHECKBOOK_CONNECTOR_INFO)
}
fn get_supported_payment_methods(&self) -> Option<&'static SupportedPaymentMethods> {
Some(&*CHECKBOOK_SUPPORTED_PAYMENT_METHODS)
}
fn get_supported_webhook_flows(&self) -> Option<&'static [enums::EventClass]> {
Some(&CHECKBOOK_SUPPORTED_WEBHOOK_FLOWS)
}
}

View File

@ -1,102 +1,84 @@
use common_enums::enums;
use common_utils::types::FloatMajorUnit;
use api_models::webhooks::IncomingWebhookEvent;
use common_utils::{pii, types::FloatMajorUnit};
use hyperswitch_domain_models::{
payment_method_data::PaymentMethodData,
payment_method_data::{BankTransferData, PaymentMethodData},
router_data::{ConnectorAuthType, RouterData},
router_flow_types::refunds::{Execute, RSync},
router_request_types::ResponseId,
router_response_types::{PaymentsResponseData, RefundsResponseData},
types::{PaymentsAuthorizeRouterData, RefundsRouterData},
router_response_types::PaymentsResponseData,
types::PaymentsAuthorizeRouterData,
};
use hyperswitch_interfaces::errors;
use hyperswitch_interfaces::errors::ConnectorError;
use masking::Secret;
use serde::{Deserialize, Serialize};
use crate::{
types::{RefundsResponseRouterData, ResponseRouterData},
utils::PaymentsAuthorizeRequestData,
types::ResponseRouterData,
utils::{get_unimplemented_payment_method_error_message, RouterData as _},
};
//TODO: Fill the struct with respective fields
pub struct CheckbookRouterData<T> {
pub amount: FloatMajorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc.
pub router_data: T,
}
impl<T> From<(FloatMajorUnit, T)> for CheckbookRouterData<T> {
fn from((amount, item): (FloatMajorUnit, T)) -> Self {
//Todo : use utils to convert the amount to the type of amount that a connector accepts
Self {
amount,
router_data: item,
}
}
}
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Serialize, PartialEq)]
#[derive(Debug, Serialize)]
pub struct CheckbookPaymentsRequest {
name: Secret<String>,
recipient: pii::Email,
amount: FloatMajorUnit,
card: CheckbookCard,
description: String,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct CheckbookCard {
number: cards::CardNumber,
expiry_month: Secret<String>,
expiry_year: Secret<String>,
cvc: Secret<String>,
complete: bool,
}
impl TryFrom<&CheckbookRouterData<&PaymentsAuthorizeRouterData>> for CheckbookPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
impl TryFrom<(FloatMajorUnit, &PaymentsAuthorizeRouterData)> for CheckbookPaymentsRequest {
type Error = error_stack::Report<ConnectorError>;
fn try_from(
item: &CheckbookRouterData<&PaymentsAuthorizeRouterData>,
(amount, item): (FloatMajorUnit, &PaymentsAuthorizeRouterData),
) -> Result<Self, Self::Error> {
match item.router_data.request.payment_method_data.clone() {
PaymentMethodData::Card(req_card) => {
let card = CheckbookCard {
number: req_card.card_number,
expiry_month: req_card.card_exp_month,
expiry_year: req_card.card_exp_year,
cvc: req_card.card_cvc,
complete: item.router_data.request.is_auto_capture()?,
};
Ok(Self {
amount: item.amount,
card,
})
}
_ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()),
match item.request.payment_method_data.clone() {
PaymentMethodData::BankTransfer(bank_transfer_data) => match *bank_transfer_data {
BankTransferData::AchBankTransfer {} => Ok(Self {
name: item.get_billing_full_name()?,
recipient: item.get_billing_email()?,
amount,
description: item.get_description()?,
}),
_ => Err(ConnectorError::NotImplemented(
get_unimplemented_payment_method_error_message("Checkbook"),
)
.into()),
},
_ => Err(ConnectorError::NotImplemented(
get_unimplemented_payment_method_error_message("Checkbook"),
)
.into()),
}
}
}
//TODO: Fill the struct with respective fields
// Auth Struct
pub struct CheckbookAuthType {
pub(super) api_key: Secret<String>,
pub(super) publishable_key: Secret<String>,
pub(super) secret_key: Secret<String>,
}
impl TryFrom<&ConnectorAuthType> for CheckbookAuthType {
type Error = error_stack::Report<errors::ConnectorError>;
type Error = error_stack::Report<ConnectorError>;
fn try_from(auth_type: &ConnectorAuthType) -> Result<Self, Self::Error> {
match auth_type {
ConnectorAuthType::HeaderKey { api_key } => Ok(Self {
api_key: api_key.to_owned(),
ConnectorAuthType::BodyKey { key1, api_key } => Ok(Self {
publishable_key: key1.to_owned(),
secret_key: api_key.to_owned(),
}),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
_ => Err(ConnectorError::FailedToObtainAuthType.into()),
}
}
}
// PaymentsResponse
//TODO: Append the remaining status flags
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CheckbookPaymentStatus {
Succeeded,
Unpaid,
InProcess,
Paid,
Mailed,
Printed,
Failed,
Expired,
Void,
#[default]
Processing,
}
@ -104,24 +86,48 @@ pub enum CheckbookPaymentStatus {
impl From<CheckbookPaymentStatus> for common_enums::AttemptStatus {
fn from(item: CheckbookPaymentStatus) -> Self {
match item {
CheckbookPaymentStatus::Succeeded => Self::Charged,
CheckbookPaymentStatus::Failed => Self::Failure,
CheckbookPaymentStatus::Processing => Self::Authorizing,
CheckbookPaymentStatus::Paid
| CheckbookPaymentStatus::Mailed
| CheckbookPaymentStatus::Printed => Self::Charged,
CheckbookPaymentStatus::Failed | CheckbookPaymentStatus::Expired => Self::Failure,
CheckbookPaymentStatus::Unpaid => Self::AuthenticationPending,
CheckbookPaymentStatus::InProcess | CheckbookPaymentStatus::Processing => Self::Pending,
CheckbookPaymentStatus::Void => Self::Voided,
}
}
}
impl From<CheckbookPaymentStatus> for IncomingWebhookEvent {
fn from(status: CheckbookPaymentStatus) -> Self {
match status {
CheckbookPaymentStatus::Mailed
| CheckbookPaymentStatus::Printed
| CheckbookPaymentStatus::Paid => Self::PaymentIntentSuccess,
CheckbookPaymentStatus::Failed | CheckbookPaymentStatus::Expired => {
Self::PaymentIntentFailure
}
CheckbookPaymentStatus::Unpaid
| CheckbookPaymentStatus::InProcess
| CheckbookPaymentStatus::Processing => Self::PaymentIntentProcessing,
CheckbookPaymentStatus::Void => Self::PaymentIntentCancelled,
}
}
}
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CheckbookPaymentsResponse {
status: CheckbookPaymentStatus,
id: String,
pub status: CheckbookPaymentStatus,
pub id: String,
pub amount: Option<FloatMajorUnit>,
pub description: Option<String>,
pub name: Option<String>,
pub recipient: Option<String>,
}
impl<F, T> TryFrom<ResponseRouterData<F, CheckbookPaymentsResponse, T, PaymentsResponseData>>
for RouterData<F, T, PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
type Error = error_stack::Report<ConnectorError>;
fn try_from(
item: ResponseRouterData<F, CheckbookPaymentsResponse, T, PaymentsResponseData>,
) -> Result<Self, Self::Error> {
@ -142,83 +148,6 @@ impl<F, T> TryFrom<ResponseRouterData<F, CheckbookPaymentsResponse, T, PaymentsR
}
}
//TODO: Fill the struct with respective fields
// REFUND :
// Type definition for RefundRequest
#[derive(Default, Debug, Serialize)]
pub struct CheckbookRefundRequest {
pub amount: FloatMajorUnit,
}
impl<F> TryFrom<&CheckbookRouterData<&RefundsRouterData<F>>> for CheckbookRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &CheckbookRouterData<&RefundsRouterData<F>>) -> Result<Self, Self::Error> {
Ok(Self {
amount: item.amount.to_owned(),
})
}
}
// Type definition for Refund Response
#[allow(dead_code)]
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
pub enum RefundStatus {
Succeeded,
Failed,
#[default]
Processing,
}
impl From<RefundStatus> for enums::RefundStatus {
fn from(item: RefundStatus) -> Self {
match item {
RefundStatus::Succeeded => Self::Success,
RefundStatus::Failed => Self::Failure,
RefundStatus::Processing => Self::Pending,
//TODO: Review mapping
}
}
}
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct RefundResponse {
id: String,
status: RefundStatus,
}
impl TryFrom<RefundsResponseRouterData<Execute, RefundResponse>> for RefundsRouterData<Execute> {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: RefundsResponseRouterData<Execute, RefundResponse>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(RefundsResponseData {
connector_refund_id: item.response.id.to_string(),
refund_status: enums::RefundStatus::from(item.response.status),
}),
..item.data
})
}
}
impl TryFrom<RefundsResponseRouterData<RSync, RefundResponse>> for RefundsRouterData<RSync> {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: RefundsResponseRouterData<RSync, RefundResponse>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(RefundsResponseData {
connector_refund_id: item.response.id.to_string(),
refund_status: enums::RefundStatus::from(item.response.status),
}),
..item.data
})
}
}
//TODO: Fill the struct with respective fields
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct CheckbookErrorResponse {
pub status_code: u16,

View File

@ -198,6 +198,7 @@ enum RequiredField {
DcbMsisdn,
DcbClientUid,
OrderDetailsProductName,
Description,
}
impl RequiredField {
@ -856,6 +857,15 @@ impl RequiredField {
value: None,
},
),
Self::Description => (
"description".to_string(),
RequiredFieldInfo {
required_field: "description".to_string(),
display_name: "description".to_string(),
field_type: FieldType::Text,
value: None,
},
),
}
}
}
@ -3325,8 +3335,17 @@ fn get_bank_transfer_required_fields() -> HashMap<enums::PaymentMethodType, Conn
(
enums::PaymentMethodType::Ach,
connectors(vec![(
Connector::Stripe,
fields(vec![], vec![], vec![RequiredField::BillingEmail]),
Connector::Checkbook,
fields(
vec![],
vec![],
vec![
RequiredField::BillingUserFirstName,
RequiredField::BillingUserLastName,
RequiredField::BillingEmail,
RequiredField::Description,
],
),
)]),
),
(

View File

@ -155,10 +155,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> {
celero::transformers::CeleroAuthType::try_from(self.auth_type)?;
Ok(())
}
// api_enums::Connector::Checkbook => {
// checkbook::transformers::CheckbookAuthType::try_from(self.auth_type)?;
// Ok(())
// },
api_enums::Connector::Checkbook => {
checkbook::transformers::CheckbookAuthType::try_from(self.auth_type)?;
Ok(())
}
api_enums::Connector::Checkout => {
checkout::transformers::CheckoutAuthType::try_from(self.auth_type)?;
Ok(())

View File

@ -164,9 +164,9 @@ impl ConnectorData {
enums::Connector::Chargebee => {
Ok(ConnectorEnum::Old(Box::new(connector::Chargebee::new())))
}
// enums::Connector::Checkbook => {
// Ok(ConnectorEnum::Old(Box::new(connector::Checkbook)))
// }
enums::Connector::Checkbook => {
Ok(ConnectorEnum::Old(Box::new(connector::Checkbook::new())))
}
enums::Connector::Checkout => {
Ok(ConnectorEnum::Old(Box::new(connector::Checkout::new())))
}

View File

@ -29,7 +29,7 @@ impl ForeignTryFrom<api_enums::Connector> for common_enums::RoutableConnectors {
api_enums::Connector::Cashtocode => Self::Cashtocode,
api_enums::Connector::Celero => Self::Celero,
api_enums::Connector::Chargebee => Self::Chargebee,
// api_enums::Connector::Checkbook => Self::Checkbook,
api_enums::Connector::Checkbook => Self::Checkbook,
api_enums::Connector::Checkout => Self::Checkout,
api_enums::Connector::Coinbase => Self::Coinbase,
api_enums::Connector::Coingate => Self::Coingate,

View File

@ -1,6 +1,8 @@
use std::str::FromStr;
use hyperswitch_domain_models::address::{Address, AddressDetails};
use masking::Secret;
use router::types::{self, api, domain, storage::enums};
use test_utils::connector_auth;
use router::types::{self, api, storage::enums, Email};
use crate::utils::{self, ConnectorActions};
@ -12,19 +14,17 @@ impl utils::Connector for CheckbookTest {
use router::connector::Checkbook;
utils::construct_connector_data_old(
Box::new(Checkbook::new()),
types::Connector::DummyConnector1,
types::Connector::Checkbook,
api::GetToken::Connector,
None,
)
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
utils::to_connector_auth_type(
connector_auth::ConnectorAuthentication::new()
.checkbook
.expect("Missing connector authentication configuration")
.into(),
)
types::ConnectorAuthType::BodyKey {
key1: Secret::new("dummy_publishable_key".to_string()),
api_key: Secret::new("dummy_secret_key".to_string()),
}
}
fn get_name(&self) -> String {
@ -35,52 +35,40 @@ impl utils::Connector for CheckbookTest {
static CONNECTOR: CheckbookTest = CheckbookTest {};
fn get_default_payment_info() -> Option<utils::PaymentInfo> {
None
Some(utils::PaymentInfo {
address: Some(types::PaymentAddress::new(
None,
None,
Some(Address {
address: Some(AddressDetails {
first_name: Some(Secret::new("John".to_string())),
last_name: Some(Secret::new("Doe".to_string())),
..Default::default()
}),
phone: None,
email: Some(Email::from_str("abc@gmail.com").unwrap()),
}),
None,
)),
..Default::default()
})
}
fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
None
}
// Cards Positive Tests
// Creates a payment using the manual capture flow (Non 3DS).
// Creates a payment.
#[actix_web::test]
async fn should_only_authorize_payment() {
let response = CONNECTOR
.authorize_payment(payment_method_details(), get_default_payment_info())
.await
.expect("Authorize payment response");
assert_eq!(response.status, enums::AttemptStatus::Authorized);
assert_eq!(response.status, enums::AttemptStatus::AuthenticationPending);
}
// Captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info())
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
// Partially captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(
payment_method_details(),
Some(types::PaymentsCaptureData {
amount_to_capture: 50,
..utils::PaymentCaptureType::default().0
}),
get_default_payment_info(),
)
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
// Synchronizes a payment using the manual capture flow (Non 3DS).
// Synchronizes a payment.
#[actix_web::test]
async fn should_sync_authorized_payment() {
let authorize_response = CONNECTOR
@ -90,7 +78,7 @@ async fn should_sync_authorized_payment() {
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Authorized,
enums::AttemptStatus::AuthenticationPending,
Some(types::PaymentsSyncData {
connector_transaction_id: types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
@ -101,320 +89,20 @@ async fn should_sync_authorized_payment() {
)
.await
.expect("PSync response");
assert_eq!(response.status, enums::AttemptStatus::Authorized,);
assert_eq!(response.status, enums::AttemptStatus::AuthenticationPending);
}
// Voids a payment using the manual capture flow (Non 3DS).
// Voids a payment.
#[actix_web::test]
async fn should_void_authorized_payment() {
let authorize_response = CONNECTOR
.authorize_payment(payment_method_details(), get_default_payment_info())
.await
.expect("Authorize payment response");
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
let response = CONNECTOR
.authorize_and_void_payment(
payment_method_details(),
Some(types::PaymentsCancelData {
connector_transaction_id: String::from(""),
cancellation_reason: Some("requested_by_customer".to_string()),
..Default::default()
}),
get_default_payment_info(),
)
.void_payment(txn_id.unwrap(), None, get_default_payment_info())
.await
.expect("Void payment response");
assert_eq!(response.status, enums::AttemptStatus::Voided);
}
// Refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_manually_captured_payment() {
let response = CONNECTOR
.capture_payment_and_refund(
payment_method_details(),
None,
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Partially refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_manually_captured_payment() {
let response = CONNECTOR
.capture_payment_and_refund(
payment_method_details(),
None,
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Synchronizes a refund using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_manually_captured_refund() {
let refund_response = CONNECTOR
.capture_payment_and_refund(
payment_method_details(),
None,
None,
get_default_payment_info(),
)
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Creates a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_make_payment() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
}
// Synchronizes a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_auto_captured_payment() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Charged,
Some(types::PaymentsSyncData {
connector_transaction_id: types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
capture_method: Some(enums::CaptureMethod::Automatic),
..Default::default()
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Charged,);
}
// Refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_auto_captured_payment() {
let response = CONNECTOR
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Partially refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_succeeded_payment() {
let refund_response = CONNECTOR
.make_payment_and_refund(
payment_method_details(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
refund_response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_succeeded_payment_multiple_times() {
CONNECTOR
.make_payment_and_multiple_refund(
payment_method_details(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await;
}
// Synchronizes a refund using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_refund() {
let refund_response = CONNECTOR
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
.await
.unwrap();
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Cards Negative scenarios
// Creates a payment with incorrect CVC.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_cvc() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: domain::PaymentMethodData::Card(domain::Card {
card_cvc: Secret::new("12345".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Your card's security code is invalid.".to_string(),
);
}
// Creates a payment with incorrect expiry month.
#[actix_web::test]
async fn should_fail_payment_for_invalid_exp_month() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: domain::PaymentMethodData::Card(domain::Card {
card_exp_month: Secret::new("20".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Your card's expiration month is invalid.".to_string(),
);
}
// Creates a payment with incorrect expiry year.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_expiry_year() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: domain::PaymentMethodData::Card(domain::Card {
card_exp_year: Secret::new("2000".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Your card's expiration year is invalid.".to_string(),
);
}
// Voids a payment using automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_fail_void_payment_for_auto_capture() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
let void_response = CONNECTOR
.void_payment(txn_id.unwrap(), None, get_default_payment_info())
.await
.unwrap();
assert_eq!(
void_response.response.unwrap_err().message,
"You cannot cancel this PaymentIntent because it has a status of succeeded."
);
}
// Captures a payment using invalid connector payment id.
#[actix_web::test]
async fn should_fail_capture_for_invalid_payment() {
let capture_response = CONNECTOR
.capture_payment("123456789".to_string(), None, get_default_payment_info())
.await
.unwrap();
assert_eq!(
capture_response.response.unwrap_err().message,
String::from("No such payment_intent: '123456789'")
);
}
// Refunds a payment with refund amount higher than payment amount.
#[actix_web::test]
async fn should_fail_for_refund_amount_higher_than_payment_amount() {
let response = CONNECTOR
.make_payment_and_refund(
payment_method_details(),
Some(types::RefundsData {
refund_amount: 150,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Refund amount (₹1.50) is greater than charge amount (₹1.00)",
);
}
// Connector dependent test cases goes here
// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests