feat(connector): [Nuvei] add support for card mandates (#818)

Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
Jagan
2023-04-18 00:00:54 +05:30
committed by GitHub
parent f57289551c
commit 298a0a4956
10 changed files with 475 additions and 255 deletions

View File

@ -325,9 +325,7 @@ pub struct SingleUseMandate {
pub currency: api_enums::Currency,
}
#[derive(
Clone, Eq, PartialEq, Copy, Debug, Default, ToSchema, serde::Serialize, serde::Deserialize,
)]
#[derive(Clone, Eq, PartialEq, Debug, Default, ToSchema, serde::Serialize, serde::Deserialize)]
pub struct MandateAmountData {
/// The maximum amount to be debited for the mandate transaction
#[schema(example = 6540)]
@ -335,6 +333,19 @@ pub struct MandateAmountData {
/// The currency for the transaction
#[schema(value_type = Currency, example = "USD")]
pub currency: api_enums::Currency,
/// Specifying start date of the mandate
#[schema(example = "2022-09-10T00:00:00Z")]
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
pub start_date: Option<PrimitiveDateTime>,
/// Specifying end date of the mandate
#[schema(example = "2023-09-10T23:59:59Z")]
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
pub end_date: Option<PrimitiveDateTime>,
/// Additional details required by mandate
#[schema(value_type = Option<Object>, example = r#"{
"frequency": "DAILY"
}"#)]
pub metadata: Option<pii::SecretSerdeValue>,
}
#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]

View File

@ -15,18 +15,31 @@ pub mod validation;
/// Date-time utilities.
pub mod date_time {
use std::num::NonZeroU8;
use std::{marker::PhantomData, num::NonZeroU8};
use masking::{Deserialize, Serialize};
#[cfg(feature = "async_ext")]
use time::Instant;
use time::{
format_description::well_known::iso8601::{Config, EncodedConfig, Iso8601, TimePrecision},
format_description::{
well_known::iso8601::{Config, EncodedConfig, Iso8601, TimePrecision},
FormatItem,
},
OffsetDateTime, PrimitiveDateTime,
};
/// Struct to represent milliseconds in time sensitive data fields
#[derive(Debug)]
pub struct Milliseconds(i32);
/// Enum to represent date formats
#[derive(Debug)]
pub enum DateFormat {
/// Format the date in 20191105081132 format
YYYYMMDDHHmmss,
/// Format the date in 20191105 format
YYYYMMDD,
}
/// Create a new [`PrimitiveDateTime`] with the current date and time in UTC.
pub fn now() -> PrimitiveDateTime {
let utc_date_time = OffsetDateTime::now_utc();
@ -53,10 +66,13 @@ pub mod date_time {
(result, start.elapsed().as_seconds_f64() * 1000f64)
}
/// Return the current date and time in UTC with the format YYYYMMDDHHmmss Eg: 20191105081132
pub fn date_as_yyyymmddhhmmss() -> Result<String, time::error::Format> {
let format = time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero][hour padding:zero repr:24][minute padding:zero][second padding:zero]");
now().format(&format)
/// Return the given date and time in UTC with the given format Eg: format: YYYYMMDDHHmmss Eg: 20191105081132
pub fn format_date(
date: PrimitiveDateTime,
format: DateFormat,
) -> Result<String, time::error::Format> {
let format = <&[FormatItem<'_>]>::from(format);
date.format(&format)
}
/// Return the current date and time in UTC with the format [year]-[month]-[day]T[hour]:[minute]:[second].mmmZ Eg: 2023-02-15T13:33:18.898Z
@ -68,6 +84,98 @@ pub mod date_time {
.encode();
now().assume_utc().format(&Iso8601::<ISO_CONFIG>)
}
impl From<DateFormat> for &[FormatItem<'_>] {
fn from(format: DateFormat) -> Self {
match format {
DateFormat::YYYYMMDDHHmmss => time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero][hour padding:zero repr:24][minute padding:zero][second padding:zero]"),
DateFormat::YYYYMMDD => time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero]"),
}
}
}
/// Format the date in 05112019 format
#[derive(Debug, Clone)]
pub struct DDMMYYYY;
/// Format the date in 20191105 format
#[derive(Debug, Clone)]
pub struct YYYYMMDD;
/// Format the date in 20191105081132 format
#[derive(Debug, Clone)]
pub struct YYYYMMDDHHmmss;
/// To serialize the date in Dateformats like YYYYMMDDHHmmss, YYYYMMDD, DDMMYYYY
#[derive(Debug, Deserialize, Clone)]
pub struct DateTime<T: TimeStrategy> {
inner: PhantomData<T>,
value: PrimitiveDateTime,
}
impl<T: TimeStrategy> From<PrimitiveDateTime> for DateTime<T> {
fn from(value: PrimitiveDateTime) -> Self {
Self {
inner: PhantomData,
value,
}
}
}
/// Time strategy for the Date, Eg: YYYYMMDDHHmmss, YYYYMMDD, DDMMYYYY
pub trait TimeStrategy {
/// Stringify the date as per the Time strategy
fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}
impl<T: TimeStrategy> Serialize for DateTime<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
impl<T: TimeStrategy> std::fmt::Display for DateTime<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
T::fmt(&self.value, f)
}
}
impl TimeStrategy for DDMMYYYY {
fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let year = input.year();
#[allow(clippy::as_conversions)]
let month = input.month() as u8;
let day = input.day();
let output = format!("{day:02}{month:02}{year}");
f.write_str(&output)
}
}
impl TimeStrategy for YYYYMMDD {
fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let year = input.year();
#[allow(clippy::as_conversions)]
let month: u8 = input.month() as u8;
let day = input.day();
let output = format!("{year}{month:02}{day:02}");
f.write_str(&output)
}
}
impl TimeStrategy for YYYYMMDDHHmmss {
fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let year = input.year();
#[allow(clippy::as_conversions)]
let month = input.month() as u8;
let day = input.day();
let hour = input.hour();
let minute = input.minute();
let second = input.second();
let output = format!("{year}{month:02}{day:02}{hour:02}{minute:02}{second:02}");
f.write_str(&output)
}
}
}
/// Generate a nanoid with the given prefix and length

View File

@ -1,6 +1,6 @@
use common_utils::{
crypto::{self, GenerateDigest},
date_time,
date_time, fp_utils,
pii::Email,
};
use error_stack::{IntoReport, ResultExt};
@ -9,7 +9,9 @@ use reqwest::Url;
use serde::{Deserialize, Serialize};
use crate::{
connector::utils::{self, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, RouterData},
connector::utils::{
self, MandateData, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, RouterData,
},
consts,
core::errors,
services,
@ -22,12 +24,17 @@ pub struct NuveiMeta {
}
#[derive(Debug, Serialize, Default, Deserialize)]
pub struct NuveiMandateMeta {
pub frequency: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NuveiSessionRequest {
pub merchant_id: String,
pub merchant_site_id: String,
pub client_request_id: String,
pub time_stamp: String,
pub time_stamp: date_time::DateTime<date_time::YYYYMMDDHHmmss>,
pub checksum: String,
}
@ -45,6 +52,7 @@ pub struct NuveiSessionResponse {
pub client_request_id: String,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NuveiPaymentsRequest {
@ -55,9 +63,11 @@ pub struct NuveiPaymentsRequest {
pub client_request_id: String,
pub amount: String,
pub currency: String,
pub user_token_id: String,
/// This ID uniquely identifies your consumer/user in your system.
pub user_token_id: Option<Secret<String, Email>>,
pub client_unique_id: String,
pub transaction_type: TransactionType,
pub is_rebilling: Option<String>,
pub payment_option: PaymentOption,
pub checksum: String,
pub billing_address: Option<BillingAddress>,
@ -76,6 +86,7 @@ pub struct NuveiInitPaymentRequest {
pub checksum: String,
}
/// Handles payment request for capture, void and refund flows
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NuveiPaymentFlowRequest {
@ -102,6 +113,7 @@ pub enum TransactionType {
Sale,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentOption {
@ -132,6 +144,7 @@ pub struct BillingAddress {
pub country: api_models::enums::CountryCode,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Card {
@ -173,6 +186,7 @@ pub enum ExternalTokenProvider {
ApplePay,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreeD {
@ -229,7 +243,11 @@ pub struct BrowserDetails {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct V2AdditionalParams {
pub challenge_window_size: String,
pub challenge_window_size: Option<String>,
/// Recurring Expiry in format YYYYMMDD. REQUIRED if isRebilling = 0, We recommend setting rebillExpiry to a value of no more than 5 years from the date of the initial transaction processing date.
pub rebill_expiry: Option<String>,
/// Recurring Frequency in days
pub rebill_frequency: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -309,9 +327,7 @@ impl TryFrom<&types::PaymentsAuthorizeSessionTokenRouterData> for NuveiSessionRe
let merchant_id = connector_meta.merchant_id;
let merchant_site_id = connector_meta.merchant_site_id;
let client_request_id = item.attempt_id.clone();
let time_stamp = date_time::date_as_yyyymmddhhmmss()
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let time_stamp = date_time::DateTime::<date_time::YYYYMMDDHHmmss>::from(date_time::now());
let merchant_secret = connector_meta.merchant_secret;
Ok(Self {
merchant_id: merchant_id.clone(),
@ -322,7 +338,7 @@ impl TryFrom<&types::PaymentsAuthorizeSessionTokenRouterData> for NuveiSessionRe
merchant_id,
merchant_site_id,
client_request_id,
time_stamp,
time_stamp.to_string(),
merchant_secret,
])?,
})
@ -348,6 +364,46 @@ impl<F, T>
}
}
#[derive(Debug, Default)]
pub struct NuveiCardDetails {
card: api_models::payments::Card,
three_d: Option<ThreeD>,
}
impl From<api_models::payments::GooglePayWalletData> for NuveiPaymentsRequest {
fn from(gpay_data: api_models::payments::GooglePayWalletData) -> Self {
Self {
payment_option: PaymentOption {
card: Some(Card {
external_token: Some(ExternalToken {
external_token_provider: ExternalTokenProvider::GooglePay,
mobile_token: gpay_data.tokenization_data.token,
}),
..Default::default()
}),
..Default::default()
},
..Default::default()
}
}
}
impl From<api_models::payments::ApplePayWalletData> for NuveiPaymentsRequest {
fn from(apple_pay_data: api_models::payments::ApplePayWalletData) -> Self {
Self {
payment_option: PaymentOption {
card: Some(Card {
external_token: Some(ExternalToken {
external_token_provider: ExternalTokenProvider::ApplePay,
mobile_token: apple_pay_data.payment_data,
}),
..Default::default()
}),
..Default::default()
},
..Default::default()
}
}
}
impl<F>
TryFrom<(
&types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
@ -362,50 +418,13 @@ impl<F>
),
) -> Result<Self, Self::Error> {
let item = data.0;
let session_token = data.1;
if session_token.is_empty() {
return Err(errors::ConnectorError::MissingRequiredField {
field_name: "session_token",
}
.into());
}
let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?;
let merchant_id = connector_meta.merchant_id;
let merchant_site_id = connector_meta.merchant_site_id;
let client_request_id = item.attempt_id.clone();
let time_stamp = date_time::date_as_yyyymmddhhmmss()
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let merchant_secret = connector_meta.merchant_secret;
let request_data = match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(card) => get_card_info(item, &card),
api::PaymentMethodData::Wallet(wallet) => match wallet {
api_models::payments::WalletData::GooglePay(gpay_data) => Ok(Self {
payment_option: PaymentOption {
card: Some(Card {
external_token: Some(ExternalToken {
external_token_provider: ExternalTokenProvider::GooglePay,
mobile_token: gpay_data.tokenization_data.token,
}),
..Default::default()
}),
..Default::default()
},
..Default::default()
}),
api_models::payments::WalletData::ApplePay(apple_data) => Ok(Self {
payment_option: PaymentOption {
card: Some(Card {
external_token: Some(ExternalToken {
external_token_provider: ExternalTokenProvider::ApplePay,
mobile_token: apple_data.payment_data,
}),
..Default::default()
}),
..Default::default()
},
..Default::default()
}),
api_models::payments::WalletData::GooglePay(gpay_data) => Ok(Self::from(gpay_data)),
api_models::payments::WalletData::ApplePay(apple_pay_data) => {
Ok(Self::from(apple_pay_data))
}
api_models::payments::WalletData::PaypalRedirect(_) => Ok(Self {
payment_option: PaymentOption {
alternative_payment_method: Some(AlternativePaymentMethod {
@ -428,49 +447,85 @@ impl<F>
},
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
}?;
Ok(Self {
merchant_id: merchant_id.clone(),
merchant_site_id: merchant_site_id.clone(),
client_request_id: client_request_id.clone(),
let request = Self::try_from(NuveiPaymentRequestData {
amount: item.request.amount.clone().to_string(),
currency: item.request.currency.clone().to_string(),
transaction_type: item
.request
.capture_method
.map(TransactionType::from)
.unwrap_or_default(),
time_stamp: time_stamp.clone(),
session_token,
checksum: encode_payload(vec![
merchant_id,
merchant_site_id,
client_request_id,
item.request.amount.to_string(),
item.request.currency.to_string(),
time_stamp,
merchant_secret,
])?,
..request_data
connector_auth_type: item.connector_auth_type.clone(),
client_request_id: item.attempt_id.clone(),
session_token: data.1,
capture_method: item.request.capture_method,
..Default::default()
})?;
Ok(Self {
is_rebilling: request_data.is_rebilling,
user_token_id: request_data.user_token_id,
related_transaction_id: request_data.related_transaction_id,
payment_option: request_data.payment_option,
..request
})
}
}
fn get_card_info<F>(
item: &types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>,
card_details: &api_models::payments::Card,
) -> Result<NuveiPaymentsRequest, error_stack::Report<errors::ConnectorError>> {
let browser_info = item.request.get_browser_info()?;
let related_transaction_id = if item.request.enrolled_for_3ds {
let related_transaction_id = if item.is_three_ds() {
item.request.related_transaction_id.clone()
} else {
None
};
let three_d = if item.request.enrolled_for_3ds {
let connector_mandate_id = &item.request.connector_mandate_id();
if connector_mandate_id.is_some() {
Ok(NuveiPaymentsRequest {
related_transaction_id,
is_rebilling: Some("1".to_string()), // In case of second installment, rebilling should be 1
user_token_id: Some(item.request.get_email()?),
payment_option: PaymentOption {
user_payment_option_id: connector_mandate_id.clone(),
..Default::default()
},
..Default::default()
})
} else {
let (is_rebilling, additional_params, user_token_id) =
match item.request.setup_mandate_details.clone() {
Some(mandate_data) => {
let details = match mandate_data.mandate_type {
api_models::payments::MandateType::SingleUse(details) => details,
api_models::payments::MandateType::MultiUse(details) => {
details.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "mandate_data.mandate_type.multi_use",
})?
}
};
let mandate_meta: NuveiMandateMeta =
utils::to_connector_meta_from_secret(Some(details.get_metadata()?))?;
(
Some("0".to_string()), // In case of first installment, rebilling should be 0
Some(V2AdditionalParams {
rebill_expiry: Some(
details.get_end_date(date_time::DateFormat::YYYYMMDD)?,
),
rebill_frequency: Some(mandate_meta.frequency),
challenge_window_size: None,
}),
Some(item.request.get_email()?),
)
}
_ => (None, None, None),
};
let three_d = if item.is_three_ds() {
Some(ThreeD {
browser_details: Some(BrowserDetails {
accept_header: browser_info.accept_header,
ip: browser_info.ip_address,
java_enabled: browser_info.java_enabled.to_string().to_uppercase(),
java_script_enabled: browser_info.java_script_enabled.to_string().to_uppercase(),
java_script_enabled: browser_info
.java_script_enabled
.to_string()
.to_uppercase(),
language: browser_info.language,
color_depth: browser_info.color_depth,
screen_height: browser_info.screen_height,
@ -478,6 +533,7 @@ fn get_card_info<F>(
time_zone: browser_info.time_zone,
user_agent: browser_info.user_agent,
}),
v2_additional_params: additional_params,
notification_url: item.request.complete_authorize_url.clone(),
merchant_url: item.return_url.clone(),
platform_type: Some(PlatformType::Browser),
@ -487,163 +543,183 @@ fn get_card_info<F>(
} else {
None
};
let card = card_details.clone();
Ok(NuveiPaymentsRequest {
related_transaction_id,
payment_option: PaymentOption {
card: Some(Card {
card_number: Some(card.card_number),
card_holder_name: Some(card.card_holder_name),
expiration_month: Some(card.card_exp_month),
expiration_year: Some(card.card_exp_year),
is_rebilling,
user_token_id,
payment_option: PaymentOption::from(NuveiCardDetails {
card: card_details.clone(),
three_d,
cvv: Some(card.card_cvc),
..Default::default()
}),
..Default::default()
},
..Default::default()
})
}
}
impl<F>
TryFrom<(
&types::RouterData<F, types::CompleteAuthorizeData, types::PaymentsResponseData>,
String,
)> for NuveiPaymentsRequest
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
data: (
&types::RouterData<F, types::CompleteAuthorizeData, types::PaymentsResponseData>,
String,
),
) -> Result<Self, Self::Error> {
let item = data.0;
let session_token = data.1;
if session_token.is_empty() {
return Err(errors::ConnectorError::MissingRequiredField {
field_name: "session_token",
}
.into());
}
let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?;
let merchant_id = connector_meta.merchant_id;
let merchant_site_id = connector_meta.merchant_site_id;
let client_request_id = item.attempt_id.clone();
let time_stamp = date_time::date_as_yyyymmddhhmmss()
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let merchant_secret = connector_meta.merchant_secret;
let request_data = match item.request.payment_method_data.clone() {
Some(api::PaymentMethodData::Card(card)) => Ok(Self {
related_transaction_id: item.request.connector_transaction_id.clone(),
payment_option: PaymentOption {
impl From<NuveiCardDetails> for PaymentOption {
fn from(card_details: NuveiCardDetails) -> Self {
let card = card_details.card;
Self {
card: Some(Card {
card_number: Some(card.card_number),
card_holder_name: Some(card.card_holder_name),
expiration_month: Some(card.card_exp_month),
expiration_year: Some(card.card_exp_year),
three_d: card_details.three_d,
cvv: Some(card.card_cvc),
..Default::default()
}),
..Default::default()
},
}
}
}
impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, String)> for NuveiPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
data: (&types::PaymentsCompleteAuthorizeRouterData, String),
) -> Result<Self, Self::Error> {
let item = data.0;
let request_data = match item.request.payment_method_data.clone() {
Some(api::PaymentMethodData::Card(card)) => Ok(Self {
payment_option: PaymentOption::from(NuveiCardDetails {
card,
three_d: None,
}),
..Default::default()
}),
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
)),
}?;
let request = Self::try_from(NuveiPaymentRequestData {
amount: item.request.amount.clone().to_string(),
currency: item.request.currency.clone().to_string(),
connector_auth_type: item.connector_auth_type.clone(),
client_request_id: item.attempt_id.clone(),
session_token: data.1,
capture_method: item.request.capture_method,
..Default::default()
})?;
Ok(Self {
related_transaction_id: request_data.related_transaction_id,
payment_option: request_data.payment_option,
..request
})
}
}
impl TryFrom<NuveiPaymentRequestData> for NuveiPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(request: NuveiPaymentRequestData) -> Result<Self, Self::Error> {
let session_token = request.session_token;
fp_utils::when(session_token.is_empty(), || {
Err(errors::ConnectorError::FailedToObtainAuthType)
})?;
let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&request.connector_auth_type)?;
let merchant_id = connector_meta.merchant_id;
let merchant_site_id = connector_meta.merchant_site_id;
let client_request_id = request.client_request_id;
let time_stamp =
date_time::format_date(date_time::now(), date_time::DateFormat::YYYYMMDDHHmmss)
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let merchant_secret = connector_meta.merchant_secret;
Ok(Self {
merchant_id: merchant_id.clone(),
merchant_site_id: merchant_site_id.clone(),
client_request_id: client_request_id.clone(),
amount: item.request.amount.clone().to_string(),
currency: item.request.currency.clone().to_string(),
transaction_type: item
.request
time_stamp: time_stamp.clone(),
session_token,
transaction_type: request
.capture_method
.map(TransactionType::from)
.unwrap_or_default(),
time_stamp: time_stamp.clone(),
session_token,
checksum: encode_payload(vec![
merchant_id,
merchant_site_id,
client_request_id,
item.request.amount.to_string(),
item.request.currency.to_string(),
request.amount.clone(),
request.currency.clone(),
time_stamp,
merchant_secret,
])?,
..request_data
amount: request.amount,
currency: request.currency,
..Default::default()
})
}
}
impl TryFrom<NuveiPaymentRequestData> for NuveiPaymentFlowRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(request: NuveiPaymentRequestData) -> Result<Self, Self::Error> {
let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&request.connector_auth_type)?;
let merchant_id = connector_meta.merchant_id;
let merchant_site_id = connector_meta.merchant_site_id;
let client_request_id = request.client_request_id;
let time_stamp =
date_time::format_date(date_time::now(), date_time::DateFormat::YYYYMMDDHHmmss)
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let merchant_secret = connector_meta.merchant_secret;
Ok(Self {
merchant_id: merchant_id.clone(),
merchant_site_id: merchant_site_id.clone(),
client_request_id: client_request_id.clone(),
time_stamp: time_stamp.clone(),
checksum: encode_payload(vec![
merchant_id,
merchant_site_id,
client_request_id,
request.amount.clone(),
request.currency.clone(),
request.related_transaction_id.clone().unwrap_or_default(),
time_stamp,
merchant_secret,
])?,
amount: request.amount,
currency: request.currency,
related_transaction_id: request.related_transaction_id,
})
}
}
/// Common request handler for all the flows that has below fields in common
#[derive(Debug, Clone, Default)]
pub struct NuveiPaymentRequestData {
pub amount: String,
pub currency: String,
pub related_transaction_id: Option<String>,
pub client_request_id: String,
pub connector_auth_type: types::ConnectorAuthType,
pub session_token: String,
pub capture_method: Option<storage_models::enums::CaptureMethod>,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for NuveiPaymentFlowRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?;
let merchant_id = connector_meta.merchant_id;
let merchant_site_id = connector_meta.merchant_site_id;
let client_request_id = item.attempt_id.clone();
let time_stamp = date_time::date_as_yyyymmddhhmmss()
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let merchant_secret = connector_meta.merchant_secret;
Ok(Self {
merchant_id: merchant_id.clone(),
merchant_site_id: merchant_site_id.clone(),
client_request_id: client_request_id.clone(),
amount: item.request.amount_to_capture.clone().to_string(),
currency: item.request.currency.clone().to_string(),
Self::try_from(NuveiPaymentRequestData {
client_request_id: item.attempt_id.clone(),
connector_auth_type: item.connector_auth_type.clone(),
amount: item.request.amount_to_capture.to_string(),
currency: item.request.currency.to_string(),
related_transaction_id: Some(item.request.connector_transaction_id.clone()),
time_stamp: time_stamp.clone(),
checksum: encode_payload(vec![
merchant_id,
merchant_site_id,
client_request_id,
item.request.amount_to_capture.to_string(),
item.request.currency.to_string(),
item.request.connector_transaction_id.clone(),
time_stamp,
merchant_secret,
])?,
..Default::default()
})
}
}
impl TryFrom<&types::RefundExecuteRouterData> for NuveiPaymentFlowRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundExecuteRouterData) -> Result<Self, Self::Error> {
let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?;
let merchant_id = connector_meta.merchant_id;
let merchant_site_id = connector_meta.merchant_site_id;
let client_request_id = item.attempt_id.clone();
let time_stamp = date_time::date_as_yyyymmddhhmmss()
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let merchant_secret = connector_meta.merchant_secret;
Ok(Self {
merchant_id: merchant_id.clone(),
merchant_site_id: merchant_site_id.clone(),
client_request_id: client_request_id.clone(),
amount: item.request.amount.clone().to_string(),
currency: item.request.currency.clone().to_string(),
Self::try_from(NuveiPaymentRequestData {
client_request_id: item.attempt_id.clone(),
connector_auth_type: item.connector_auth_type.clone(),
amount: item.request.amount.to_string(),
currency: item.request.currency.to_string(),
related_transaction_id: Some(item.request.connector_transaction_id.clone()),
time_stamp: time_stamp.clone(),
checksum: encode_payload(vec![
merchant_id,
merchant_site_id,
client_request_id,
item.request.amount.to_string(),
item.request.currency.to_string(),
item.request.connector_transaction_id.clone(),
time_stamp,
merchant_secret,
])?,
..Default::default()
})
}
}
@ -661,34 +737,13 @@ impl TryFrom<&types::PaymentsSyncRouterData> for NuveiPaymentSyncRequest {
impl TryFrom<&types::PaymentsCancelRouterData> for NuveiPaymentFlowRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> {
let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?;
let merchant_id = connector_meta.merchant_id;
let merchant_site_id = connector_meta.merchant_site_id;
let client_request_id = item.attempt_id.clone();
let time_stamp = date_time::date_as_yyyymmddhhmmss()
.into_report()
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let merchant_secret = connector_meta.merchant_secret;
let amount = item.request.get_amount()?.to_string();
let currency = item.request.get_currency()?.to_string();
Ok(Self {
merchant_id: merchant_id.clone(),
merchant_site_id: merchant_site_id.clone(),
client_request_id: client_request_id.clone(),
amount: amount.clone(),
currency: currency.clone(),
Self::try_from(NuveiPaymentRequestData {
client_request_id: item.attempt_id.clone(),
connector_auth_type: item.connector_auth_type.clone(),
amount: item.request.get_amount()?.to_string(),
currency: item.request.get_currency()?.to_string(),
related_transaction_id: Some(item.request.connector_transaction_id.clone()),
time_stamp: time_stamp.clone(),
checksum: encode_payload(vec![
merchant_id,
merchant_site_id,
client_request_id,
amount,
currency,
item.request.connector_transaction_id.clone(),
time_stamp,
merchant_secret,
])?,
..Default::default()
})
}
}
@ -882,7 +937,9 @@ impl<F, T>
.map(types::ResponseId::ConnectorTransactionId)
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?,
redirection_data,
mandate_reference: None,
mandate_reference: response
.payment_option
.and_then(|po| po.user_payment_option_id),
// we don't need to save session token for capture, void flow so ignoring if it is not present
connector_metadata: if let Some(token) = response.session_token {
Some(
@ -903,26 +960,6 @@ impl<F, T>
}
}
impl<F, T> TryFrom<types::ResponseRouterData<F, NuveiACSResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, NuveiACSResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: enums::AttemptStatus::AuthenticationFailed,
response: Err(types::ErrorResponse {
code: consts::NO_ERROR_CODE.to_string(),
message: "Authentication Failed".to_string(),
reason: None,
status_code: item.http_code,
}),
..item.data
})
}
}
impl From<NuveiTransactionStatus> for enums::RefundStatus {
fn from(item: NuveiTransactionStatus) -> Self {
match item {

View File

@ -1,7 +1,9 @@
use std::collections::HashMap;
use api_models::payments;
use base64::Engine;
use common_utils::{
date_time,
errors::ReportSwitchExt,
pii::{self, Email},
};
@ -145,6 +147,7 @@ pub trait PaymentsAuthorizeRequestData {
fn get_browser_info(&self) -> Result<types::BrowserInformation, Error>;
fn get_card(&self) -> Result<api::Card, Error>;
fn get_return_url(&self) -> Result<String, Error>;
fn connector_mandate_id(&self) -> Option<String>;
fn is_mandate_payment(&self) -> bool;
fn get_webhook_url(&self) -> Result<String, Error>;
}
@ -172,6 +175,11 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
.clone()
.ok_or_else(missing_field_err("return_url"))
}
fn connector_mandate_id(&self) -> Option<String> {
self.mandate_id
.as_ref()
.and_then(|mandate_ids| mandate_ids.connector_mandate_id.clone())
}
fn is_mandate_payment(&self) -> bool {
self.setup_mandate_details.is_some()
|| self
@ -435,6 +443,27 @@ impl AddressDetailsData for api::AddressDetails {
}
}
pub trait MandateData {
fn get_end_date(&self, format: date_time::DateFormat) -> Result<String, Error>;
fn get_metadata(&self) -> Result<pii::SecretSerdeValue, Error>;
}
impl MandateData for payments::MandateAmountData {
fn get_end_date(&self, format: date_time::DateFormat) -> Result<String, Error> {
let date = self.end_date.ok_or_else(missing_field_err(
"mandate_data.mandate_type.{multi_use|single_use}.end_date",
))?;
date_time::format_date(date, format)
.into_report()
.change_context(errors::ConnectorError::DateFormattingFailed)
}
fn get_metadata(&self) -> Result<pii::SecretSerdeValue, Error> {
self.metadata.clone().ok_or_else(missing_field_err(
"mandate_data.mandate_type.{multi_use|single_use}.metadata",
))
}
}
pub fn get_header_key_value<'a>(
key: &str,
headers: &'a actix_web::http::header::HeaderMap,

View File

@ -283,6 +283,8 @@ pub enum ConnectorError {
WebhookResponseEncodingFailed,
#[error("Invalid Date/time format")]
InvalidDateFormat,
#[error("Date Formatting Failed")]
DateFormattingFailed,
#[error("Invalid Data format")]
InvalidDataFormat { field_name: &'static str },
#[error("Payment Method data / Payment Method Type / Payment Experience Mismatch ")]

View File

@ -306,6 +306,19 @@ fn validate_new_mandate_request(req: api::MandateValidationFields) -> RouterResu
}))?
}
let mandate_details = match mandate_data.mandate_type {
api_models::payments::MandateType::SingleUse(details) => Some(details),
api_models::payments::MandateType::MultiUse(details) => details,
};
mandate_details.and_then(|md| md.start_date.zip(md.end_date)).map(|(start_date, end_date)|
utils::when (start_date >= end_date, || {
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
message: "`mandate_data.mandate_type.{multi_use|single_use}.start_date` should be greater than \
`mandate_data.mandate_type.{multi_use|single_use}.end_date`"
.into()
}))
})).transpose()?;
Ok(())
}
@ -1161,7 +1174,10 @@ pub fn generate_mandate(
api::MandateType::MultiUse(op_data) => match op_data {
Some(data) => new_mandate
.set_mandate_amount(Some(data.amount))
.set_mandate_currency(Some(data.currency.foreign_into())),
.set_mandate_currency(Some(data.currency.foreign_into()))
.set_start_date(data.start_date)
.set_end_date(data.end_date)
.set_metadata(data.metadata),
None => &mut new_mandate,
}
.set_mandate_type(storage_enums::MandateType::MultiUse)

View File

@ -26,6 +26,9 @@ pub struct Mandate {
pub amount_captured: Option<i64>,
pub connector: String,
pub connector_mandate_id: Option<String>,
pub start_date: Option<PrimitiveDateTime>,
pub end_date: Option<PrimitiveDateTime>,
pub metadata: Option<pii::SecretSerdeValue>,
}
#[derive(
@ -50,6 +53,9 @@ pub struct MandateNew {
pub amount_captured: Option<i64>,
pub connector: String,
pub connector_mandate_id: Option<String>,
pub start_date: Option<PrimitiveDateTime>,
pub end_date: Option<PrimitiveDateTime>,
pub metadata: Option<pii::SecretSerdeValue>,
}
#[derive(Debug)]

View File

@ -199,6 +199,9 @@ diesel::table! {
amount_captured -> Nullable<Int8>,
connector -> Varchar,
connector_mandate_id -> Nullable<Varchar>,
start_date -> Nullable<Timestamp>,
end_date -> Nullable<Timestamp>,
metadata -> Nullable<Jsonb>,
}
}

View File

@ -0,0 +1,4 @@
ALTER TABLE mandate
DROP COLUMN IF EXISTS start_date,
DROP COLUMN IF EXISTS end_date,
DROP COLUMN IF EXISTS metadata;

View File

@ -0,0 +1,4 @@
ALTER TABLE mandate
ADD IF NOT EXISTS start_date TIMESTAMP NULL,
ADD IF NOT EXISTS end_date TIMESTAMP NULL,
ADD COLUMN metadata JSONB DEFAULT NULL;