diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 7cf78433f3..eefb27ee61 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -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, + /// 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, + /// Additional details required by mandate + #[schema(value_type = Option, example = r#"{ + "frequency": "DAILY" + }"#)] + pub metadata: Option, } #[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index b181cdcf4d..0bdd84848f 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -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 { - 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 { + 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::) } + + impl From 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 { + inner: PhantomData, + value: PrimitiveDateTime, + } + + impl From for DateTime { + 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 Serialize for DateTime { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(self) + } + } + + impl std::fmt::Display for DateTime { + 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 diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index 41572291a6..d554bf74be 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -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, 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>, pub client_unique_id: String, pub transaction_type: TransactionType, + pub is_rebilling: Option, pub payment_option: PaymentOption, pub checksum: String, pub billing_address: Option, @@ -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, + /// 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, + /// Recurring Frequency in days + pub rebill_frequency: Option, } #[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::::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 } } +#[derive(Debug, Default)] +pub struct NuveiCardDetails { + card: api_models::payments::Card, + three_d: Option, +} +impl From 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 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 TryFrom<( &types::RouterData, @@ -362,50 +418,13 @@ impl ), ) -> Result { 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,222 +447,279 @@ impl }, _ => 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( item: &types::RouterData, card_details: &api_models::payments::Card, ) -> Result> { 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 { - 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(), - language: browser_info.language, - color_depth: browser_info.color_depth, - screen_height: browser_info.screen_height, - screen_width: browser_info.screen_width, - time_zone: browser_info.time_zone, - user_agent: browser_info.user_agent, - }), - notification_url: item.request.complete_authorize_url.clone(), - merchant_url: item.return_url.clone(), - platform_type: Some(PlatformType::Browser), - method_completion_ind: Some(MethodCompletion::Unavailable), + 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 { - None - }; - let card = card_details.clone(); - Ok(NuveiPaymentsRequest { - related_transaction_id, - payment_option: PaymentOption { + 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(), + language: browser_info.language, + color_depth: browser_info.color_depth, + screen_height: browser_info.screen_height, + screen_width: browser_info.screen_width, + 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), + method_completion_ind: Some(MethodCompletion::Unavailable), + ..Default::default() + }) + } else { + None + }; + + Ok(NuveiPaymentsRequest { + related_transaction_id, + is_rebilling, + user_token_id, + payment_option: PaymentOption::from(NuveiCardDetails { + card: card_details.clone(), + three_d, + }), + ..Default::default() + }) + } +} +impl From 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, + three_d: card_details.three_d, cvv: Some(card.card_cvc), ..Default::default() }), ..Default::default() - }, - ..Default::default() - }) + } + } } -impl - TryFrom<( - &types::RouterData, - String, - )> for NuveiPaymentsRequest -{ +impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, String)> for NuveiPaymentsRequest { type Error = error_stack::Report; fn try_from( - data: ( - &types::RouterData, - String, - ), + data: (&types::PaymentsCompleteAuthorizeRouterData, String), ) -> Result { 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 { - 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), - cvv: Some(card.card_cvc), - ..Default::default() - }), - ..Default::default() - }, + 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 for NuveiPaymentsRequest { + type Error = error_stack::Report; + fn try_from(request: NuveiPaymentRequestData) -> Result { + 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 for NuveiPaymentFlowRequest { + type Error = error_stack::Report; + fn try_from(request: NuveiPaymentRequestData) -> Result { + 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, + pub client_request_id: String, + pub connector_auth_type: types::ConnectorAuthType, + pub session_token: String, + pub capture_method: Option, +} + impl TryFrom<&types::PaymentsCaptureRouterData> for NuveiPaymentFlowRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { - 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; fn try_from(item: &types::RefundExecuteRouterData) -> Result { - 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; fn try_from(item: &types::PaymentsCancelRouterData) -> Result { - 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 .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 } } -impl TryFrom> - for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData, - ) -> Result { - 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 for enums::RefundStatus { fn from(item: NuveiTransactionStatus) -> Self { match item { diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 8ac36fe140..ae0ad71938 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -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; fn get_card(&self) -> Result; fn get_return_url(&self) -> Result; + fn connector_mandate_id(&self) -> Option; fn is_mandate_payment(&self) -> bool; fn get_webhook_url(&self) -> Result; } @@ -172,6 +175,11 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { .clone() .ok_or_else(missing_field_err("return_url")) } + fn connector_mandate_id(&self) -> Option { + 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; + fn get_metadata(&self) -> Result; +} + +impl MandateData for payments::MandateAmountData { + fn get_end_date(&self, format: date_time::DateFormat) -> Result { + 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 { + 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, diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 48397952d8..4adbf39678 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -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 ")] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 91c2ef9a64..434adbe7de 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -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) diff --git a/crates/storage_models/src/mandate.rs b/crates/storage_models/src/mandate.rs index 3798f40840..3a1bbc6579 100644 --- a/crates/storage_models/src/mandate.rs +++ b/crates/storage_models/src/mandate.rs @@ -26,6 +26,9 @@ pub struct Mandate { pub amount_captured: Option, pub connector: String, pub connector_mandate_id: Option, + pub start_date: Option, + pub end_date: Option, + pub metadata: Option, } #[derive( @@ -50,6 +53,9 @@ pub struct MandateNew { pub amount_captured: Option, pub connector: String, pub connector_mandate_id: Option, + pub start_date: Option, + pub end_date: Option, + pub metadata: Option, } #[derive(Debug)] diff --git a/crates/storage_models/src/schema.rs b/crates/storage_models/src/schema.rs index 118464b87e..603f2945d6 100644 --- a/crates/storage_models/src/schema.rs +++ b/crates/storage_models/src/schema.rs @@ -199,6 +199,9 @@ diesel::table! { amount_captured -> Nullable, connector -> Varchar, connector_mandate_id -> Nullable, + start_date -> Nullable, + end_date -> Nullable, + metadata -> Nullable, } } diff --git a/migrations/2023-03-30-132338_add_start_end_date_for_mandates/down.sql b/migrations/2023-03-30-132338_add_start_end_date_for_mandates/down.sql new file mode 100644 index 0000000000..7dfcf9c2d6 --- /dev/null +++ b/migrations/2023-03-30-132338_add_start_end_date_for_mandates/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE mandate +DROP COLUMN IF EXISTS start_date, +DROP COLUMN IF EXISTS end_date, +DROP COLUMN IF EXISTS metadata; \ No newline at end of file diff --git a/migrations/2023-03-30-132338_add_start_end_date_for_mandates/up.sql b/migrations/2023-03-30-132338_add_start_end_date_for_mandates/up.sql new file mode 100644 index 0000000000..8e57e2b8a1 --- /dev/null +++ b/migrations/2023-03-30-132338_add_start_end_date_for_mandates/up.sql @@ -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; \ No newline at end of file