Skip to content

Commit

Permalink
refactor(connector): add amount conversion framework for noon (#4843)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com>
Co-authored-by: Narayan Bhat <narayan.bhat@juspay.in>
  • Loading branch information
4 people authored Jun 19, 2024
1 parent 7208ca4 commit 8c7e1a3
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 76 deletions.
3 changes: 3 additions & 0 deletions crates/hyperswitch_domain_models/src/router_request_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@ pub struct PaymentsCancelData {
pub browser_info: Option<BrowserInformation>,
pub metadata: Option<pii::SecretSerdeValue>,
// This metadata is used to store the metadata shared during the payment intent request.

// minor amount data for amount framework
pub minor_amount: Option<MinorUnit>,
}

#[derive(Debug, Default, Clone)]
Expand Down
61 changes: 53 additions & 8 deletions crates/router/src/connector/noon.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
pub mod transformers;

use std::fmt::Debug;

use base64::Engine;
use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent};
use common_utils::{
crypto,
ext_traits::ByteSliceExt,
request::RequestContent,
types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector},
};
use diesel_models::enums;
use error_stack::{Report, ResultExt};
use masking::PeekInterface;
Expand All @@ -16,6 +19,7 @@ use crate::{
consts,
core::{
errors::{self, CustomResult},
mandate::MandateBehaviour,
payments,
},
events::connector_api_logs::ConnectorEvent,
Expand All @@ -33,8 +37,18 @@ use crate::{
utils::{self, BytesExt},
};

#[derive(Debug, Clone)]
pub struct Noon;
#[derive(Clone)]
pub struct Noon {
amount_converter: &'static (dyn AmountConvertor<Output = StringMajorUnit> + Sync),
}

impl Noon {
pub const fn new() -> &'static Self {
&Self {
amount_converter: &StringMajorUnitForConnector,
}
}
}

impl api::Payment for Noon {}
impl api::PaymentSession for Noon {}
Expand Down Expand Up @@ -260,7 +274,26 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = noon::NoonPaymentsRequest::try_from(req)?;
let amount = connector_utils::convert_amount(
self.amount_converter,
req.request.minor_amount,
req.request.currency,
)?;

let mandate_details =
connector_utils::get_mandate_details(req.request.get_setup_mandate_details())?;
let mandate_amount = mandate_details
.map(|mandate| {
connector_utils::convert_amount(
self.amount_converter,
mandate.amount,
mandate.currency,
)
})
.transpose()?;

let connector_router_data = noon::NoonRouterData::from((amount, req, mandate_amount));
let connector_req = noon::NoonPaymentsRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}

Expand Down Expand Up @@ -416,7 +449,13 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = noon::NoonPaymentsActionRequest::try_from(req)?;
let amount = connector_utils::convert_amount(
self.amount_converter,
req.request.minor_amount_to_capture,
req.request.currency,
)?;
let connector_router_data = noon::NoonRouterData::from((amount, req, None));
let connector_req = noon::NoonPaymentsActionRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}

Expand Down Expand Up @@ -656,7 +695,13 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
req: &types::RefundsRouterData<api::Execute>,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = noon::NoonPaymentsActionRequest::try_from(req)?;
let refund_amount = connector_utils::convert_amount(
self.amount_converter,
req.request.minor_refund_amount,
req.request.currency,
)?;
let connector_router_data = noon::NoonRouterData::from((refund_amount, req, None));
let connector_req = noon::NoonPaymentsActionRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}

Expand Down
126 changes: 61 additions & 65 deletions crates/router/src/connector/noon/transformers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use common_utils::{ext_traits::Encode, pii};
use common_utils::{ext_traits::Encode, pii, types::StringMajorUnit};
use error_stack::ResultExt;
use masking::{ExposeInterface, PeekInterface, Secret};
use serde::{Deserialize, Serialize};
Expand All @@ -8,7 +8,7 @@ use crate::{
self as conn_utils, is_refund_failure, CardData, PaymentsAuthorizeRequestData,
RevokeMandateRequestData, RouterData, WalletData,
},
core::{errors, mandate::MandateBehaviour},
core::errors,
services,
types::{self, api, domain, storage::enums, transformers::ForeignFrom, ErrorResponse},
};
Expand All @@ -17,6 +17,25 @@ use crate::{
const GOOGLEPAY_API_VERSION_MINOR: u8 = 0;
const GOOGLEPAY_API_VERSION: u8 = 2;

#[derive(Debug, Serialize)]
pub struct NoonRouterData<T> {
pub amount: StringMajorUnit,
pub router_data: T,
pub mandate_amount: Option<StringMajorUnit>,
}

impl<T> From<(StringMajorUnit, T, Option<StringMajorUnit>)> for NoonRouterData<T> {
fn from(
(amount, router_data, mandate_amount): (StringMajorUnit, T, Option<StringMajorUnit>),
) -> Self {
Self {
amount,
router_data,
mandate_amount,
}
}
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum NoonChannels {
Expand All @@ -36,7 +55,7 @@ pub struct NoonSubscriptionData {
subscription_type: NoonSubscriptionType,
//Short description about the subscription.
name: String,
max_amount: String,
max_amount: StringMajorUnit,
}

#[derive(Debug, Serialize)]
Expand All @@ -59,7 +78,7 @@ pub struct NoonBilling {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonOrder {
amount: String,
amount: StringMajorUnit,
currency: Option<diesel_models::enums::Currency>,
channel: NoonChannels,
category: Option<String>,
Expand Down Expand Up @@ -226,9 +245,15 @@ pub struct NoonPaymentsRequest {
billing: Option<NoonBilling>,
}

impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest {
impl TryFrom<&NoonRouterData<&types::PaymentsAuthorizeRouterData>> for NoonPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
fn try_from(
data: &NoonRouterData<&types::PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
let item = data.router_data;
let amount = &data.amount;
let mandate_amount = &data.mandate_amount;

let (payment_data, currency, category) = match item.request.connector_mandate_id() {
Some(mandate_id) => (
NoonPaymentData::Subscription(NoonSubscription {
Expand Down Expand Up @@ -342,15 +367,6 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest {
),
};

// The description should not have leading or trailing whitespaces, also it should not have double whitespaces and a max 50 chars according to Noon's Docs
let name: String = item
.get_description()?
.trim()
.replace(" ", " ")
.chars()
.take(50)
.collect();

let ip_address = item.request.get_ip_address_as_optional();

let channel = NoonChannels::Web;
Expand All @@ -370,47 +386,27 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest {
},
});

let subscription: Option<NoonSubscriptionData> = item
.request
.get_setup_mandate_details()
.map(|mandate_data| {
let max_amount = match &mandate_data.mandate_type {
Some(hyperswitch_domain_models::mandates::MandateDataType::SingleUse(
mandate,
))
| Some(hyperswitch_domain_models::mandates::MandateDataType::MultiUse(Some(
mandate,
))) => conn_utils::to_currency_base_unit(
mandate.amount.get_amount_as_i64(),
mandate.currency,
),
Some(hyperswitch_domain_models::mandates::MandateDataType::MultiUse(None)) => {
Err(errors::ConnectorError::MissingRequiredField {
field_name:
"setup_future_usage.mandate_data.mandate_type.multi_use.amount",
}
.into())
}
None => Err(errors::ConnectorError::MissingRequiredField {
field_name: "setup_future_usage.mandate_data.mandate_type",
}
.into()),
}?;

Ok::<NoonSubscriptionData, error_stack::Report<errors::ConnectorError>>(
NoonSubscriptionData {
subscription_type: NoonSubscriptionType::Unscheduled,
name: name.clone(),
max_amount,
},
)
})
.transpose()?;
// The description should not have leading or trailing whitespaces, also it should not have double whitespaces and a max 50 chars according to Noon's Docs
let name: String = item
.get_description()?
.trim()
.replace(" ", " ")
.chars()
.take(50)
.collect();

let subscription = mandate_amount
.as_ref()
.map(|mandate_max_amount| NoonSubscriptionData {
subscription_type: NoonSubscriptionType::Unscheduled,
name: name.clone(),
max_amount: mandate_max_amount.to_owned(),
});

let tokenize_c_c = subscription.is_some().then_some(true);

let order = NoonOrder {
amount: conn_utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
amount: amount.to_owned(),
currency,
channel,
category,
Expand Down Expand Up @@ -613,7 +609,7 @@ impl<F, T>
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonActionTransaction {
amount: String,
amount: StringMajorUnit,
currency: diesel_models::enums::Currency,
transaction_reference: Option<String>,
}
Expand All @@ -632,17 +628,18 @@ pub struct NoonPaymentsActionRequest {
transaction: NoonActionTransaction,
}

impl TryFrom<&types::PaymentsCaptureRouterData> for NoonPaymentsActionRequest {
impl TryFrom<&NoonRouterData<&types::PaymentsCaptureRouterData>> for NoonPaymentsActionRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
fn try_from(
data: &NoonRouterData<&types::PaymentsCaptureRouterData>,
) -> Result<Self, Self::Error> {
let item = data.router_data;
let amount = &data.amount;
let order = NoonActionOrder {
id: item.request.connector_transaction_id.clone(),
};
let transaction = NoonActionTransaction {
amount: conn_utils::to_currency_base_unit(
item.request.amount_to_capture,
item.request.currency,
)?,
amount: amount.to_owned(),
currency: item.request.currency,
transaction_reference: None,
};
Expand Down Expand Up @@ -693,17 +690,16 @@ impl TryFrom<&types::MandateRevokeRouterData> for NoonRevokeMandateRequest {
}
}

impl<F> TryFrom<&types::RefundsRouterData<F>> for NoonPaymentsActionRequest {
impl<F> TryFrom<&NoonRouterData<&types::RefundsRouterData<F>>> for NoonPaymentsActionRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
fn try_from(data: &NoonRouterData<&types::RefundsRouterData<F>>) -> Result<Self, Self::Error> {
let item = data.router_data;
let refund_amount = &data.amount;
let order = NoonActionOrder {
id: item.request.connector_transaction_id.clone(),
};
let transaction = NoonActionTransaction {
amount: conn_utils::to_currency_base_unit(
item.request.refund_amount,
item.request.currency,
)?,
amount: refund_amount.to_owned(),
currency: item.request.currency,
transaction_reference: Some(item.request.refund_id.clone()),
};
Expand Down
23 changes: 22 additions & 1 deletion crates/router/src/connector/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use common_utils::{
};
use diesel_models::enums;
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt;
use hyperswitch_domain_models::{mandates, payments::payment_attempt::PaymentAttempt};
use masking::{ExposeInterface, Secret};
use once_cell::sync::Lazy;
use regex::Regex;
Expand Down Expand Up @@ -2832,6 +2832,27 @@ impl From<domain::payments::PaymentMethodData> for PaymentMethodDataType {
}
}

pub fn get_mandate_details(
setup_mandate_details: Option<&mandates::MandateData>,
) -> Result<Option<&mandates::MandateAmountData>, error_stack::Report<errors::ConnectorError>> {
setup_mandate_details
.map(|mandate_data| match &mandate_data.mandate_type {
Some(mandates::MandateDataType::SingleUse(mandate))
| Some(mandates::MandateDataType::MultiUse(Some(mandate))) => Ok(mandate),
Some(mandates::MandateDataType::MultiUse(None)) => {
Err(errors::ConnectorError::MissingRequiredField {
field_name: "setup_future_usage.mandate_data.mandate_type.multi_use.amount",
}
.into())
}
None => Err(errors::ConnectorError::MissingRequiredField {
field_name: "setup_future_usage.mandate_data.mandate_type",
}
.into()),
})
.transpose()
}

pub fn convert_amount<T>(
amount_convertor: &dyn AmountConvertor<Output = T>,
amount: MinorUnit,
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/core/payments/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCancelDa
let amount = MinorUnit::from(payment_data.amount);
Ok(Self {
amount: Some(amount.get_amount_as_i64()), // This should be removed once we start moving to connector module
minor_amount: Some(amount),
currency: Some(payment_data.currency),
connector_transaction_id: connector
.connector
Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/types/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ impl ConnectorData {
enums::Connector::Mifinity => Ok(Box::new(&connector::Mifinity)),
enums::Connector::Mollie => Ok(Box::new(&connector::Mollie)),
enums::Connector::Nmi => Ok(Box::new(connector::Nmi::new())),
enums::Connector::Noon => Ok(Box::new(&connector::Noon)),
enums::Connector::Noon => Ok(Box::new(connector::Noon::new())),
enums::Connector::Nuvei => Ok(Box::new(&connector::Nuvei)),
enums::Connector::Opennode => Ok(Box::new(&connector::Opennode)),
// "payeezy" => Ok(Box::new(&connector::Payeezy)), As psync and rsync are not supported by this connector, it is added as template code for future usage
Expand Down
2 changes: 1 addition & 1 deletion crates/router/tests/connectors/noon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ impl utils::Connector for NoonTest {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Noon;
types::api::ConnectorData {
connector: Box::new(&Noon),
connector: Box::new(Noon::new()),
connector_name: types::Connector::Noon,
get_token: types::api::GetToken::Connector,
merchant_connector_id: None,
Expand Down

0 comments on commit 8c7e1a3

Please sign in to comment.