mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 20:23:43 +08:00
feat(connector): [Nuvei] add support for card mandates (#818)
Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
@ -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)]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 ")]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
ALTER TABLE mandate
|
||||
DROP COLUMN IF EXISTS start_date,
|
||||
DROP COLUMN IF EXISTS end_date,
|
||||
DROP COLUMN IF EXISTS metadata;
|
||||
@ -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;
|
||||
Reference in New Issue
Block a user