refactor(connector): implement amount converter framework for coinbase, dummyconnector and gocardless (#8915)

This commit is contained in:
Pa1NarK
2025-08-18 20:02:06 +05:30
committed by GitHub
parent 96f92531c7
commit 79dcec465e
10 changed files with 194 additions and 93 deletions

View File

@ -1,13 +1,12 @@
pub mod transformers;
use std::fmt::Debug;
use common_enums::enums;
use common_utils::{
crypto,
errors::CustomResult,
ext_traits::ByteSliceExt,
request::{Method, Request, RequestBuilder, RequestContent},
types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector},
};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
@ -47,10 +46,24 @@ use masking::Mask;
use transformers as coinbase;
use self::coinbase::CoinbaseWebhookDetails;
use crate::{constants::headers, types::ResponseRouterData, utils};
use crate::{
constants::headers,
types::ResponseRouterData,
utils::{self, convert_amount},
};
#[derive(Debug, Clone)]
pub struct Coinbase;
#[derive(Clone)]
pub struct Coinbase {
amount_convertor: &'static (dyn AmountConvertor<Output = StringMajorUnit> + Sync),
}
impl Coinbase {
pub fn new() -> &'static Self {
&Self {
amount_convertor: &StringMajorUnitForConnector,
}
}
}
impl api::Payment for Coinbase {}
impl api::PaymentToken for Coinbase {}
@ -197,7 +210,14 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
req: &PaymentsAuthorizeRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = coinbase::CoinbasePaymentsRequest::try_from(req)?;
let amount = convert_amount(
self.amount_convertor,
req.request.minor_amount,
req.request.currency,
)?;
let connector_router_data = coinbase::CoinbaseRouterData::from((amount, req));
let connector_req = coinbase::CoinbasePaymentsRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}

View File

@ -1,14 +1,14 @@
use std::collections::HashMap;
use common_enums::enums;
use common_utils::{pii, request::Method};
use common_utils::{pii, request::Method, types::StringMajorUnit};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
router_data::{ConnectorAuthType, RouterData},
router_flow_types::refunds::{Execute, RSync},
router_request_types::ResponseId,
router_response_types::{PaymentsResponseData, RedirectForm},
types,
types::{self, PaymentsAuthorizeRouterData},
};
use hyperswitch_interfaces::errors;
use masking::Secret;
@ -21,9 +21,24 @@ use crate::{
},
};
#[derive(Debug, Serialize)]
pub struct CoinbaseRouterData<T> {
amount: StringMajorUnit,
router_data: T,
}
impl<T> From<(StringMajorUnit, T)> for CoinbaseRouterData<T> {
fn from((amount, item): (StringMajorUnit, T)) -> Self {
Self {
amount,
router_data: item,
}
}
}
#[derive(Debug, Default, Eq, PartialEq, Serialize)]
pub struct LocalPrice {
pub amount: String,
pub amount: StringMajorUnit,
pub currency: String,
}
@ -43,9 +58,11 @@ pub struct CoinbasePaymentsRequest {
pub cancel_url: String,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for CoinbasePaymentsRequest {
impl TryFrom<&CoinbaseRouterData<&PaymentsAuthorizeRouterData>> for CoinbasePaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
fn try_from(
item: &CoinbaseRouterData<&PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
get_crypto_specific_payment_data(item)
}
}
@ -263,23 +280,24 @@ impl TryFrom<&Option<pii::SecretSerdeValue>> for CoinbaseConnectorMeta {
}
fn get_crypto_specific_payment_data(
item: &types::PaymentsAuthorizeRouterData,
item: &CoinbaseRouterData<&PaymentsAuthorizeRouterData>,
) -> Result<CoinbasePaymentsRequest, error_stack::Report<errors::ConnectorError>> {
let billing_address = item
.router_data
.get_billing()
.ok()
.and_then(|billing_address| billing_address.address.as_ref());
let name =
billing_address.and_then(|add| add.get_first_name().ok().map(|name| name.to_owned()));
let description = item.get_description().ok();
let connector_meta = CoinbaseConnectorMeta::try_from(&item.connector_meta_data)
let description = item.router_data.get_description().ok();
let connector_meta = CoinbaseConnectorMeta::try_from(&item.router_data.connector_meta_data)
.change_context(errors::ConnectorError::InvalidConnectorConfig {
config: "Merchant connector account metadata",
})?;
let pricing_type = connector_meta.pricing_type;
let local_price = get_local_price(item);
let redirect_url = item.request.get_router_return_url()?;
let cancel_url = item.request.get_router_return_url()?;
let redirect_url = item.router_data.request.get_router_return_url()?;
let cancel_url = item.router_data.request.get_router_return_url()?;
Ok(CoinbasePaymentsRequest {
name,
@ -291,10 +309,10 @@ fn get_crypto_specific_payment_data(
})
}
fn get_local_price(item: &types::PaymentsAuthorizeRouterData) -> LocalPrice {
fn get_local_price(item: &CoinbaseRouterData<&PaymentsAuthorizeRouterData>) -> LocalPrice {
LocalPrice {
amount: format!("{:?}", item.request.amount),
currency: item.request.currency.to_string(),
amount: item.amount.clone(),
currency: item.router_data.request.currency.to_string(),
}
}
@ -333,36 +351,42 @@ pub enum WebhookEventType {
Unknown,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Redirects {
cancel_url: Option<String>,
success_url: Option<String>,
will_redirect_after_success: bool,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct CoinbasePaymentResponseData {
pub id: String,
pub code: String,
pub name: Option<Secret<String>>,
pub utxo: bool,
pub utxo: Option<bool>,
pub pricing: HashMap<String, OverpaymentAbsoluteThreshold>,
pub fee_rate: f64,
pub logo_url: String,
pub fee_rate: Option<f64>,
pub logo_url: Option<String>,
pub metadata: Option<Metadata>,
pub payments: Vec<PaymentElement>,
pub resource: String,
pub resource: Option<String>,
pub timeline: Vec<Timeline>,
pub pwcb_only: bool,
pub cancel_url: String,
pub created_at: String,
pub expires_at: String,
pub hosted_url: String,
pub brand_color: String,
pub description: Option<String>,
pub confirmed_at: Option<String>,
pub fees_settled: bool,
pub fees_settled: Option<bool>,
pub pricing_type: String,
pub redirect_url: String,
pub redirects: Redirects,
pub support_email: pii::Email,
pub brand_logo_url: String,
pub offchain_eligible: bool,
pub offchain_eligible: Option<bool>,
pub organization_name: String,
pub payment_threshold: PaymentThreshold,
pub coinbase_managed_merchant: bool,
pub payment_threshold: Option<PaymentThreshold>,
pub coinbase_managed_merchant: Option<bool>,
}
#[derive(Debug, Serialize, Default, Deserialize)]
@ -375,7 +399,7 @@ pub struct PaymentThreshold {
#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq, Eq)]
pub struct OverpaymentAbsoluteThreshold {
pub amount: String,
pub amount: StringMajorUnit,
pub currency: String,
}

View File

@ -1,7 +1,5 @@
pub mod transformers;
use std::fmt::Debug;
use api_models::webhooks::{IncomingWebhookEvent, ObjectReferenceId};
use common_enums::{CaptureMethod, PaymentMethod, PaymentMethodType};
use common_utils::{
@ -9,6 +7,7 @@ use common_utils::{
errors::CustomResult,
ext_traits::BytesExt,
request::{Method, Request, RequestBuilder, RequestContent},
types::{AmountConvertor, MinorUnit, MinorUnitForConnector},
};
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::{
@ -45,15 +44,26 @@ use hyperswitch_interfaces::{
webhooks::{IncomingWebhook, IncomingWebhookRequestDetails},
};
use masking::{Mask as _, Maskable};
use transformers as dummyconnector;
use crate::{
constants::headers,
types::ResponseRouterData,
utils::{construct_not_supported_error_report, RefundsRequestData as _},
utils::{construct_not_supported_error_report, convert_amount, RefundsRequestData as _},
};
#[derive(Debug, Clone)]
pub struct DummyConnector<const T: u8>;
#[derive(Clone)]
pub struct DummyConnector<const T: u8> {
amount_converter: &'static (dyn AmountConvertor<Output = MinorUnit> + Sync),
}
impl<const T: u8> DummyConnector<T> {
pub fn new() -> &'static Self {
&Self {
amount_converter: &MinorUnitForConnector,
}
}
}
impl<const T: u8> Payment for DummyConnector<T> {}
impl<const T: u8> PaymentSession for DummyConnector<T> {}
@ -237,7 +247,15 @@ impl<const T: u8> ConnectorIntegration<Authorize, PaymentsAuthorizeData, Payment
req: &PaymentsAuthorizeRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, ConnectorError> {
let connector_req = transformers::DummyConnectorPaymentsRequest::<T>::try_from(req)?;
let amount = convert_amount(
self.amount_converter,
req.request.minor_amount,
req.request.currency,
)?;
let connector_router_data = dummyconnector::DummyConnectorRouterData::from((amount, req));
let connector_req =
transformers::DummyConnectorPaymentsRequest::<T>::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
@ -480,7 +498,15 @@ impl<const T: u8> ConnectorIntegration<Execute, RefundsData, RefundsResponseData
req: &RefundsRouterData<Execute>,
_connectors: &Connectors,
) -> CustomResult<RequestContent, ConnectorError> {
let connector_req = transformers::DummyConnectorRefundRequest::try_from(req)?;
let amount = convert_amount(
self.amount_converter,
req.request.minor_refund_amount,
req.request.currency,
)?;
let connector_router_data = dummyconnector::DummyConnectorRouterData::from((amount, req));
let connector_req =
transformers::DummyConnectorRefundRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}

View File

@ -1,5 +1,5 @@
use common_enums::{AttemptStatus, Currency, RefundStatus};
use common_utils::{pii, request::Method};
use common_utils::{pii, request::Method, types::MinorUnit};
use hyperswitch_domain_models::{
payment_method_data::{
Card, PayLaterData, PaymentMethodData, UpiCollectData, UpiData, WalletData,
@ -20,6 +20,21 @@ use crate::{
utils::RouterData as _,
};
#[derive(Debug, Serialize)]
pub struct DummyConnectorRouterData<T> {
pub amount: MinorUnit,
pub router_data: T,
}
impl<T> From<(MinorUnit, T)> for DummyConnectorRouterData<T> {
fn from((amount, router_data): (MinorUnit, T)) -> Self {
Self {
amount,
router_data,
}
}
}
#[derive(Debug, Serialize, strum::Display, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
@ -70,7 +85,7 @@ impl From<u8> for DummyConnectors {
#[derive(Debug, Serialize, Eq, PartialEq)]
pub struct DummyConnectorPaymentsRequest<const T: u8> {
amount: i64,
amount: MinorUnit,
currency: Currency,
payment_method_data: DummyPaymentMethodData,
return_url: Option<String>,
@ -176,13 +191,17 @@ impl TryFrom<PayLaterData> for DummyConnectorPayLater {
}
}
impl<const T: u8> TryFrom<&PaymentsAuthorizeRouterData> for DummyConnectorPaymentsRequest<T> {
impl<const T: u8> TryFrom<&DummyConnectorRouterData<&PaymentsAuthorizeRouterData>>
for DummyConnectorPaymentsRequest<T>
{
type Error = error_stack::Report<ConnectorError>;
fn try_from(item: &PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
fn try_from(
item: &DummyConnectorRouterData<&PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
let payment_method_data: Result<DummyPaymentMethodData, Self::Error> =
match item.request.payment_method_data {
match item.router_data.request.payment_method_data {
PaymentMethodData::Card(ref req_card) => {
let card_holder_name = item.get_optional_billing_full_name();
let card_holder_name = item.router_data.get_optional_billing_full_name();
Ok(DummyPaymentMethodData::Card(DummyConnectorCard::try_from(
(req_card.clone(), card_holder_name),
)?))
@ -204,10 +223,10 @@ impl<const T: u8> TryFrom<&PaymentsAuthorizeRouterData> for DummyConnectorPaymen
_ => Err(ConnectorError::NotImplemented("Payment methods".to_string()).into()),
};
Ok(Self {
amount: item.request.amount,
currency: item.request.currency,
amount: item.router_data.request.minor_amount,
currency: item.router_data.request.currency,
payment_method_data: payment_method_data?,
return_url: item.request.router_return_url.clone(),
return_url: item.router_data.request.router_return_url.clone(),
connector: Into::<DummyConnectors>::into(T),
})
}
@ -254,7 +273,7 @@ impl From<DummyConnectorPaymentStatus> for AttemptStatus {
pub struct PaymentsResponse {
status: DummyConnectorPaymentStatus,
id: String,
amount: i64,
amount: MinorUnit,
currency: Currency,
created: String,
payment_method_type: PaymentMethodType,
@ -322,14 +341,16 @@ impl DummyConnectorNextAction {
// Type definition for RefundRequest
#[derive(Default, Debug, Serialize)]
pub struct DummyConnectorRefundRequest {
pub amount: i64,
pub amount: MinorUnit,
}
impl<F> TryFrom<&RefundsRouterData<F>> for DummyConnectorRefundRequest {
impl<F> TryFrom<&DummyConnectorRouterData<&RefundsRouterData<F>>> for DummyConnectorRefundRequest {
type Error = error_stack::Report<ConnectorError>;
fn try_from(item: &RefundsRouterData<F>) -> Result<Self, Self::Error> {
fn try_from(
item: &DummyConnectorRouterData<&RefundsRouterData<F>>,
) -> Result<Self, Self::Error> {
Ok(Self {
amount: item.request.refund_amount,
amount: item.router_data.request.minor_refund_amount,
})
}
}
@ -363,8 +384,8 @@ pub struct RefundResponse {
status: DummyRefundStatus,
currency: Currency,
created: String,
payment_amount: i64,
refund_amount: i64,
payment_amount: MinorUnit,
refund_amount: MinorUnit,
}
impl TryFrom<RefundsResponseRouterData<Execute, RefundResponse>> for RefundsRouterData<Execute> {

View File

@ -1,7 +1,5 @@
pub mod transformers;
use std::fmt::Debug;
use api_models::webhooks::{IncomingWebhookEvent, ObjectReferenceId};
use common_enums::enums;
use common_utils::{
@ -9,6 +7,7 @@ use common_utils::{
errors::CustomResult,
ext_traits::{ByteSliceExt, BytesExt},
request::{Method, Request, RequestBuilder, RequestContent},
types::{AmountConvertor, MinorUnit, MinorUnitForConnector},
};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
@ -52,11 +51,21 @@ use transformers as gocardless;
use crate::{
constants::headers,
types::ResponseRouterData,
utils::{is_mandate_supported, PaymentMethodDataType},
utils::{self, is_mandate_supported, PaymentMethodDataType},
};
#[derive(Debug, Clone)]
pub struct Gocardless;
#[derive(Clone)]
pub struct Gocardless {
amount_converter: &'static (dyn AmountConvertor<Output = MinorUnit> + Sync),
}
impl Gocardless {
pub fn new() -> &'static Self {
&Self {
amount_converter: &MinorUnitForConnector,
}
}
}
impl api::Payment for Gocardless {}
impl api::PaymentSession for Gocardless {}
@ -471,12 +480,13 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
req: &PaymentsAuthorizeRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_router_data = gocardless::GocardlessRouterData::try_from((
&self.get_currency_unit(),
let amount = utils::convert_amount(
self.amount_converter,
req.request.minor_amount,
req.request.currency,
req.request.amount,
req,
))?;
)?;
let connector_router_data = gocardless::GocardlessRouterData::from((amount, req));
let connector_req =
gocardless::GocardlessPaymentsRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
@ -637,12 +647,14 @@ impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Gocardl
req: &RefundsRouterData<Execute>,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_router_data = gocardless::GocardlessRouterData::try_from((
&self.get_currency_unit(),
let refund_amount = utils::convert_amount(
self.amount_converter,
req.request.minor_refund_amount,
req.request.currency,
req.request.refund_amount,
req,
))?;
)?;
let connector_router_data = gocardless::GocardlessRouterData::from((refund_amount, req));
let connector_req = gocardless::GocardlessRefundRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}

View File

@ -2,6 +2,7 @@ use common_enums::{enums, CountryAlpha2, UsStatesAbbreviation};
use common_utils::{
id_type,
pii::{self, IpAddress},
types::MinorUnit,
};
use hyperswitch_domain_models::{
address::AddressDetails,
@ -15,7 +16,7 @@ use hyperswitch_domain_models::{
router_response_types::{MandateReference, PaymentsResponseData, RefundsResponseData},
types,
};
use hyperswitch_interfaces::{api, errors};
use hyperswitch_interfaces::errors;
use masking::{ExposeInterface, Secret};
use serde::{Deserialize, Serialize};
@ -28,19 +29,16 @@ use crate::{
};
pub struct GocardlessRouterData<T> {
pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc.
pub amount: MinorUnit,
pub router_data: T,
}
impl<T> TryFrom<(&api::CurrencyUnit, enums::Currency, i64, T)> for GocardlessRouterData<T> {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
(_currency_unit, _currency, amount, item): (&api::CurrencyUnit, enums::Currency, i64, T),
) -> Result<Self, Self::Error> {
Ok(Self {
impl<T> From<(MinorUnit, T)> for GocardlessRouterData<T> {
fn from((amount, item): (MinorUnit, T)) -> Self {
Self {
amount,
router_data: item,
})
}
}
}
@ -547,7 +545,7 @@ pub struct GocardlessPaymentsRequest {
#[derive(Debug, Serialize)]
pub struct GocardlessPayment {
amount: i64,
amount: MinorUnit,
currency: enums::Currency,
description: Option<String>,
metadata: PaymentMetaData,
@ -583,7 +581,7 @@ impl TryFrom<&GocardlessRouterData<&types::PaymentsAuthorizeRouterData>>
.into())
}?;
let payments = GocardlessPayment {
amount: item.router_data.request.amount,
amount: item.router_data.request.minor_amount,
currency: item.router_data.request.currency,
description: item.router_data.description.clone(),
metadata: PaymentMetaData {
@ -733,7 +731,7 @@ pub struct GocardlessRefundRequest {
#[derive(Default, Debug, Serialize)]
pub struct GocardlessRefund {
amount: i64,
amount: MinorUnit,
metadata: RefundMetaData,
links: RefundLink,
}

View File

@ -173,7 +173,7 @@ impl ConnectorData {
Ok(ConnectorEnum::Old(Box::new(connector::Checkout::new())))
}
enums::Connector::Coinbase => {
Ok(ConnectorEnum::Old(Box::new(&connector::Coinbase)))
Ok(ConnectorEnum::Old(Box::new(connector::Coinbase::new())))
}
enums::Connector::Coingate => {
Ok(ConnectorEnum::Old(Box::new(connector::Coingate::new())))
@ -207,35 +207,35 @@ impl ConnectorData {
}
#[cfg(feature = "dummy_connector")]
enums::Connector::DummyConnector1 => Ok(ConnectorEnum::Old(Box::new(
&connector::DummyConnector::<1>,
connector::DummyConnector::<1>::new(),
))),
#[cfg(feature = "dummy_connector")]
enums::Connector::DummyConnector2 => Ok(ConnectorEnum::Old(Box::new(
&connector::DummyConnector::<2>,
connector::DummyConnector::<2>::new(),
))),
#[cfg(feature = "dummy_connector")]
enums::Connector::DummyConnector3 => Ok(ConnectorEnum::Old(Box::new(
&connector::DummyConnector::<3>,
connector::DummyConnector::<3>::new(),
))),
#[cfg(feature = "dummy_connector")]
enums::Connector::DummyConnector4 => Ok(ConnectorEnum::Old(Box::new(
&connector::DummyConnector::<4>,
connector::DummyConnector::<4>::new(),
))),
#[cfg(feature = "dummy_connector")]
enums::Connector::DummyConnector5 => Ok(ConnectorEnum::Old(Box::new(
&connector::DummyConnector::<5>,
connector::DummyConnector::<5>::new(),
))),
#[cfg(feature = "dummy_connector")]
enums::Connector::DummyConnector6 => Ok(ConnectorEnum::Old(Box::new(
&connector::DummyConnector::<6>,
connector::DummyConnector::<6>::new(),
))),
#[cfg(feature = "dummy_connector")]
enums::Connector::DummyConnector7 => Ok(ConnectorEnum::Old(Box::new(
&connector::DummyConnector::<7>,
connector::DummyConnector::<7>::new(),
))),
#[cfg(feature = "dummy_connector")]
enums::Connector::DummyBillingConnector => Ok(ConnectorEnum::Old(Box::new(
&connector::DummyConnector::<8>,
connector::DummyConnector::<8>::new(),
))),
// enums::Connector::Dwolla => {
// Ok(ConnectorEnum::Old(Box::new(connector::Dwolla::new())))
@ -272,7 +272,7 @@ impl ConnectorData {
Ok(ConnectorEnum::Old(Box::new(connector::Globepay::new())))
}
enums::Connector::Gocardless => {
Ok(ConnectorEnum::Old(Box::new(&connector::Gocardless)))
Ok(ConnectorEnum::Old(Box::new(connector::Gocardless::new())))
}
enums::Connector::Hipay => {
Ok(ConnectorEnum::Old(Box::new(connector::Hipay::new())))

View File

@ -15,7 +15,7 @@ impl utils::Connector for CoinbaseTest {
fn get_data(&self) -> api::ConnectorData {
use router::connector::Coinbase;
utils::construct_connector_data_old(
Box::new(&Coinbase),
Box::new(Coinbase::new()),
types::Connector::Coinbase,
api::GetToken::Connector,
None,

View File

@ -14,7 +14,7 @@ impl utils::Connector for DummyConnectorTest {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::DummyConnector;
utils::construct_connector_data_old(
Box::new(&DummyConnector::<1>),
Box::new(DummyConnector::<1>::new()),
types::Connector::DummyConnector1,
types::api::GetToken::Connector,
None,

View File

@ -11,7 +11,7 @@ impl utils::Connector for GocardlessTest {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Gocardless;
utils::construct_connector_data_old(
Box::new(&Gocardless),
Box::new(Gocardless::new()),
types::Connector::Gocardless,
types::api::GetToken::Connector,
None,