refactor(payment_intent_v2): payment intent fields refactoring (#5880)

Co-authored-by: hrithikesh026 <hrithikesh.vm@juspay.in>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Narayan Bhat
2024-09-20 16:50:53 +05:30
committed by GitHub
parent 00e913c75c
commit 5335f2d21c
48 changed files with 2478 additions and 1620 deletions

View File

@ -147,5 +147,10 @@ pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only";
/// Role ID for Internal Admin
pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin";
/// Max length allowed for Description
pub const MAX_DESCRIPTION_LENGTH: u16 = 255;
/// Max length allowed for Statement Descriptor
pub const MAX_STATEMENT_DESCRIPTOR_LENGTH: u16 = 255;
/// Payout flow identifier used for performing GSM operations
pub const PAYOUT_FLOW_STR: &str = "payout_flow";

View File

@ -11,9 +11,8 @@ mod payment;
mod profile;
mod routing;
#[cfg(feature = "v2")]
mod global_id;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
mod payment_methods;
pub use customer::CustomerId;
use diesel::{
@ -23,12 +22,12 @@ use diesel::{
serialize::{Output, ToSql},
sql_types,
};
#[cfg(feature = "v2")]
pub use global_id::{payment::GlobalPaymentId, payment_methods::GlobalPaymentMethodId, CellId};
pub use merchant::MerchantId;
pub use merchant_connector_account::MerchantConnectorAccountId;
pub use organization::OrganizationId;
pub use payment::PaymentId;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
pub use payment_methods::GlobalPaymentMethodId;
pub use profile::ProfileId;
pub use routing::RoutingId;
use serde::{Deserialize, Serialize};
@ -155,6 +154,7 @@ impl<const MAX_LENGTH: u8, const MIN_LENGTH: u8> LengthId<MAX_LENGTH, MIN_LENGTH
Self(alphanumeric_id)
}
#[cfg(feature = "v2")]
/// Create a new LengthId from aplhanumeric id
pub(crate) fn from_alphanumeric_id(
alphanumeric_id: AlphaNumericId,

View File

@ -1,4 +1,6 @@
#![allow(unused)]
pub mod payment;
pub mod payment_methods;
use diesel::{backend::Backend, deserialize::FromSql, serialize::ToSql, sql_types};
use error_stack::ResultExt;
@ -34,8 +36,9 @@ impl GlobalEntity {
}
}
/// Cell identifier for an instance / deployment of application
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub(crate) struct CellId(LengthId<CELL_IDENTIFIER_LENGTH, CELL_IDENTIFIER_LENGTH>);
pub struct CellId(LengthId<CELL_IDENTIFIER_LENGTH, CELL_IDENTIFIER_LENGTH>);
#[derive(Debug, Error, PartialEq, Eq)]
pub enum CellIdError {
@ -107,7 +110,9 @@ impl GlobalId {
Self(LengthId::new_unchecked(alphanumeric_id))
}
pub fn from_string(input_string: String) -> Result<Self, GlobalIdError> {
pub(crate) fn from_string(
input_string: std::borrow::Cow<'static, str>,
) -> Result<Self, GlobalIdError> {
let length_id = LengthId::from(input_string.into())?;
let input_string = &length_id.0 .0;
let (cell_id, remaining) = input_string
@ -118,6 +123,10 @@ impl GlobalId {
Ok(Self(length_id))
}
pub(crate) fn get_string_repr(&self) -> &str {
&self.0 .0 .0
}
}
impl<DB> ToSql<sql_types::Text, DB> for GlobalId
@ -154,7 +163,7 @@ impl<'de> serde::Deserialize<'de> for GlobalId {
D: serde::Deserializer<'de>,
{
let deserialized_string = String::deserialize(deserializer)?;
Self::from_string(deserialized_string).map_err(serde::de::Error::custom)
Self::from_string(deserialized_string.into()).map_err(serde::de::Error::custom)
}
}
@ -187,7 +196,7 @@ mod global_id_tests {
#[test]
fn test_global_id_from_string() {
let input_string = "12345_cus_abcdefghijklmnopqrstuvwxyz1234567890";
let global_id = GlobalId::from_string(input_string.to_string()).unwrap();
let global_id = GlobalId::from_string(input_string.into()).unwrap();
assert_eq!(global_id.0 .0 .0, input_string);
}

View File

@ -0,0 +1,63 @@
use crate::errors;
/// A global id that can be used to identify a payment
#[derive(
Debug,
Clone,
Hash,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
diesel::expression::AsExpression,
)]
#[diesel(sql_type = diesel::sql_types::Text)]
pub struct GlobalPaymentId(super::GlobalId);
// Database related implementations so that this field can be used directly in the database tables
crate::impl_queryable_id_type!(GlobalPaymentId);
impl GlobalPaymentId {
/// Get string representation of the id
pub fn get_string_repr(&self) -> &str {
self.0.get_string_repr()
}
}
// TODO: refactor the macro to include this id use case as well
impl TryFrom<std::borrow::Cow<'static, str>> for GlobalPaymentId {
type Error = error_stack::Report<errors::ValidationError>;
fn try_from(value: std::borrow::Cow<'static, str>) -> Result<Self, Self::Error> {
use error_stack::ResultExt;
let merchant_ref_id = super::GlobalId::from_string(value).change_context(
errors::ValidationError::IncorrectValueProvided {
field_name: "payment_id",
},
)?;
Ok(Self(merchant_ref_id))
}
}
// TODO: refactor the macro to include this id use case as well
impl<DB> diesel::serialize::ToSql<diesel::sql_types::Text, DB> for GlobalPaymentId
where
DB: diesel::backend::Backend,
super::GlobalId: diesel::serialize::ToSql<diesel::sql_types::Text, DB>,
{
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, DB>,
) -> diesel::serialize::Result {
self.0.to_sql(out)
}
}
impl<DB> diesel::deserialize::FromSql<diesel::sql_types::Text, DB> for GlobalPaymentId
where
DB: diesel::backend::Backend,
super::GlobalId: diesel::deserialize::FromSql<diesel::sql_types::Text, DB>,
{
fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
super::GlobalId::from_sql(value).map(Self)
}
}

View File

@ -27,9 +27,6 @@ pub enum GlobalPaymentMethodIdError {
}
impl GlobalPaymentMethodId {
fn get_global_id(&self) -> &GlobalId {
&self.0
}
/// Create a new GlobalPaymentMethodId from celll id information
pub fn generate(cell_id: &str) -> error_stack::Result<Self, errors::ValidationError> {
let cell_id = CellId::from_string(cell_id.to_string())?;
@ -37,18 +34,18 @@ impl GlobalPaymentMethodId {
Ok(Self(global_id))
}
pub fn get_string_repr(&self) -> String {
todo!()
pub fn get_string_repr(&self) -> &str {
self.0.get_string_repr()
}
pub fn generate_from_string(value: String) -> CustomResult<Self, GlobalPaymentMethodIdError> {
let id = GlobalId::from_string(value)
let id = GlobalId::from_string(value.into())
.change_context(GlobalPaymentMethodIdError::ConstructionError)?;
Ok(Self(id))
}
}
impl<DB> diesel::Queryable<diesel::sql_types::Text, DB> for GlobalPaymentMethodId
impl<DB> diesel::Queryable<sql_types::Text, DB> for GlobalPaymentMethodId
where
DB: diesel::backend::Backend,
Self: diesel::deserialize::FromSql<diesel::sql_types::Text, DB>,
@ -68,8 +65,7 @@ where
&'b self,
out: &mut diesel::serialize::Output<'b, '_, DB>,
) -> diesel::serialize::Result {
let id = self.get_global_id();
id.to_sql(out)
self.0.to_sql(out)
}
}

View File

@ -1,7 +1,7 @@
use crate::{
errors::{CustomResult, ValidationError},
generate_id_with_default_len,
id_type::{global_id, AlphaNumericId, LengthId},
id_type::{AlphaNumericId, LengthId},
};
crate::id_type!(
@ -15,23 +15,10 @@ crate::impl_debug_id_type!(PaymentId);
crate::impl_default_id_type!(PaymentId, "pay");
crate::impl_try_from_cow_str_id_type!(PaymentId, "payment_id");
// Database related implementations so that this field can be used directly in the database tables
crate::impl_queryable_id_type!(PaymentId);
crate::impl_to_sql_from_sql_id_type!(PaymentId);
/// A global id that can be used to identify a payment
#[derive(
Debug,
Clone,
Hash,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
diesel::expression::AsExpression,
)]
#[diesel(sql_type = diesel::sql_types::Text)]
pub struct PaymentGlobalId(global_id::GlobalId);
impl PaymentId {
/// Get the hash key to be stored in redis
pub fn get_hash_key_for_kv_store(&self) -> String {

View File

@ -5,6 +5,7 @@ pub mod keymanager;
pub mod authentication;
use std::{
borrow::Cow,
fmt::Display,
ops::{Add, Sub},
primitive::i64,
@ -28,13 +29,16 @@ use rust_decimal::{
};
use semver::Version;
use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
use thiserror::Error;
use time::PrimitiveDateTime;
use utoipa::ToSchema;
use crate::{
consts,
consts::{self, MAX_DESCRIPTION_LENGTH, MAX_STATEMENT_DESCRIPTOR_LENGTH},
errors::{CustomResult, ParsingError, PercentageError, ValidationError},
fp_utils::when,
};
/// Represents Percentage Value between 0 and 100 both inclusive
#[derive(Clone, Default, Debug, PartialEq, Serialize)]
pub struct Percentage<const PRECISION: u8> {
@ -583,6 +587,251 @@ impl StringMajorUnit {
}
}
#[derive(
Debug,
serde::Deserialize,
AsExpression,
serde::Serialize,
Clone,
PartialEq,
Eq,
Hash,
ToSchema,
PartialOrd,
)]
#[diesel(sql_type = sql_types::Text)]
/// This domain type can be used for any url
pub struct Url(url::Url);
impl<DB> ToSql<sql_types::Text, DB> for Url
where
DB: Backend,
str: ToSql<sql_types::Text, DB>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
let url_string = self.0.as_str();
url_string.to_sql(out)
}
}
impl<DB> FromSql<sql_types::Text, DB> for Url
where
DB: Backend,
String: FromSql<sql_types::Text, DB>,
{
fn from_sql(value: DB::RawValue<'_>) -> deserialize::Result<Self> {
let val = String::from_sql(value)?;
let url = url::Url::parse(&val)?;
Ok(Self(url))
}
}
#[cfg(feature = "v2")]
pub use client_secret_type::ClientSecret;
#[cfg(feature = "v2")]
mod client_secret_type {
use std::fmt;
use masking::PeekInterface;
use super::*;
use crate::id_type;
/// A domain type that can be used to represent a client secret
/// Client secret is generated for a payment and is used to authenticate the client side api calls
#[derive(Debug, PartialEq, Clone, AsExpression)]
#[diesel(sql_type = sql_types::Text)]
pub struct ClientSecret {
/// The payment id of the payment
pub payment_id: id_type::GlobalPaymentId,
/// The secret string
pub secret: masking::Secret<String>,
}
impl ClientSecret {
pub(crate) fn get_string_repr(&self) -> String {
format!(
"{}_secret_{}",
self.payment_id.get_string_repr(),
self.secret.peek()
)
}
}
impl<'de> Deserialize<'de> for ClientSecret {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ClientSecretVisitor;
impl<'de> Visitor<'de> for ClientSecretVisitor {
type Value = ClientSecret;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a string in the format '{payment_id}_secret_{secret}'")
}
fn visit_str<E>(self, value: &str) -> Result<ClientSecret, E>
where
E: serde::de::Error,
{
let (payment_id, secret) = value.rsplit_once("_secret_").ok_or_else(|| {
E::invalid_value(
serde::de::Unexpected::Str(value),
&"a string with '_secret_'",
)
})?;
let payment_id =
id_type::GlobalPaymentId::try_from(Cow::Owned(payment_id.to_owned()))
.map_err(serde::de::Error::custom)?;
Ok(ClientSecret {
payment_id,
secret: masking::Secret::new(secret.to_owned()),
})
}
}
deserializer.deserialize_str(ClientSecretVisitor)
}
}
impl Serialize for ClientSecret {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(self.get_string_repr().as_str())
}
}
impl ToSql<sql_types::Text, diesel::pg::Pg> for ClientSecret
where
String: ToSql<sql_types::Text, diesel::pg::Pg>,
{
fn to_sql<'b>(
&'b self,
out: &mut Output<'b, '_, diesel::pg::Pg>,
) -> diesel::serialize::Result {
let string_repr = self.get_string_repr();
<String as ToSql<sql_types::Text, diesel::pg::Pg>>::to_sql(
&string_repr,
&mut out.reborrow(),
)
}
}
impl<DB> FromSql<sql_types::Text, DB> for ClientSecret
where
DB: Backend,
String: FromSql<sql_types::Text, DB>,
{
fn from_sql(value: DB::RawValue<'_>) -> deserialize::Result<Self> {
let string_repr = String::from_sql(value)?;
Ok(serde_json::from_str(&string_repr)?)
}
}
impl<DB> Queryable<sql_types::Text, DB> for ClientSecret
where
DB: Backend,
Self: FromSql<sql_types::Text, DB>,
{
type Row = Self;
fn build(row: Self::Row) -> deserialize::Result<Self> {
Ok(row)
}
}
#[cfg(test)]
mod client_secret_tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use serde_json;
use super::*;
use crate::id_type::GlobalPaymentId;
#[test]
fn test_serialize_client_secret() {
let global_payment_id = "12345_pay_1a961ed9093c48b09781bf8ab17ba6bd";
let secret = "fc34taHLw1ekPgNh92qr".to_string();
let expected_client_secret_string = format!("\"{global_payment_id}_secret_{secret}\"");
let client_secret1 = ClientSecret {
payment_id: GlobalPaymentId::try_from(Cow::Borrowed(global_payment_id)).unwrap(),
secret: masking::Secret::new(secret),
};
let parsed_client_secret =
serde_json::to_string(&client_secret1).expect("Failed to serialize client_secret1");
assert_eq!(expected_client_secret_string, parsed_client_secret);
}
#[test]
fn test_deserialize_client_secret() {
// This is a valid global id
let global_payment_id_str = "12345_pay_1a961ed9093c48b09781bf8ab17ba6bd";
let secret = "fc34taHLw1ekPgNh92qr".to_string();
let valid_payment_global_id =
GlobalPaymentId::try_from(Cow::Borrowed(global_payment_id_str))
.expect("Failed to create valid global payment id");
// This is an invalid global id because of the cell id being in invalid length
let invalid_global_payment_id = "123_pay_1a961ed9093c48b09781bf8ab17ba6bd";
// Create a client secret string which is valid
let valid_client_secret = format!(r#""{global_payment_id_str}_secret_{secret}""#);
dbg!(&valid_client_secret);
// Create a client secret string which is invalid
let invalid_client_secret_because_of_invalid_payment_id =
format!(r#""{invalid_global_payment_id}_secret_{secret}""#);
// Create a client secret string which is invalid because of invalid secret
let invalid_client_secret_because_of_invalid_secret =
format!(r#""{invalid_global_payment_id}""#);
let valid_client_secret = serde_json::from_str::<ClientSecret>(&valid_client_secret)
.expect("Failed to deserialize client_secret_str1");
let invalid_deser1 = serde_json::from_str::<ClientSecret>(
&invalid_client_secret_because_of_invalid_payment_id,
);
dbg!(&invalid_deser1);
let invalid_deser2 = serde_json::from_str::<ClientSecret>(
&invalid_client_secret_because_of_invalid_secret,
);
dbg!(&invalid_deser2);
assert_eq!(valid_client_secret.payment_id, valid_payment_global_id);
assert_eq!(valid_client_secret.secret.peek(), &secret);
assert_eq!(
invalid_deser1.err().unwrap().to_string(),
"Incorrect value provided for field: payment_id at line 1 column 70"
);
assert_eq!(
invalid_deser2.err().unwrap().to_string(),
"invalid value: string \"123_pay_1a961ed9093c48b09781bf8ab17ba6bd\", expected a string with '_secret_' at line 1 column 42"
);
}
}
}
/// A type representing a range of time for filtering, including a mandatory start time and an optional end time.
#[derive(
Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, ToSchema,
@ -725,31 +974,107 @@ pub struct ChargeRefunds {
crate::impl_to_sql_from_sql_json!(ChargeRefunds);
/// Domain type for description
#[derive(
Debug, Clone, PartialEq, Eq, Queryable, serde::Deserialize, serde::Serialize, AsExpression,
)]
/// A common type of domain type that can be used for fields that contain a string with restriction of length
#[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq, AsExpression)]
#[diesel(sql_type = sql_types::Text)]
pub struct Description(String);
pub(crate) struct LengthString<const MAX_LENGTH: u16, const MIN_LENGTH: u8>(String);
/// Error generated from violation of constraints for MerchantReferenceId
#[derive(Debug, Error, PartialEq, Eq)]
pub(crate) enum LengthStringError {
#[error("the maximum allowed length for this field is {0}")]
/// Maximum length of string violated
MaxLengthViolated(u16),
#[error("the minimum required length for this field is {0}")]
/// Minimum length of string violated
MinLengthViolated(u8),
}
impl<const MAX_LENGTH: u16, const MIN_LENGTH: u8> LengthString<MAX_LENGTH, MIN_LENGTH> {
/// Generates new [MerchantReferenceId] from the given input string
pub fn from(input_string: Cow<'static, str>) -> Result<Self, LengthStringError> {
let trimmed_input_string = input_string.trim().to_string();
let length_of_input_string = u16::try_from(trimmed_input_string.len())
.map_err(|_| LengthStringError::MaxLengthViolated(MAX_LENGTH))?;
when(length_of_input_string > MAX_LENGTH, || {
Err(LengthStringError::MaxLengthViolated(MAX_LENGTH))
})?;
when(length_of_input_string < u16::from(MIN_LENGTH), || {
Err(LengthStringError::MinLengthViolated(MIN_LENGTH))
})?;
Ok(Self(trimmed_input_string))
}
pub(crate) fn new_unchecked(input_string: String) -> Self {
Self(input_string)
}
}
impl<'de, const MAX_LENGTH: u16, const MIN_LENGTH: u8> Deserialize<'de>
for LengthString<MAX_LENGTH, MIN_LENGTH>
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let deserialized_string = String::deserialize(deserializer)?;
Self::from(deserialized_string.into()).map_err(serde::de::Error::custom)
}
}
impl<DB> FromSql<sql_types::Text, DB> for LengthString<MAX_DESCRIPTION_LENGTH, 1>
where
DB: Backend,
String: FromSql<sql_types::Text, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
let val = String::from_sql(bytes)?;
Ok(Self(val))
}
}
impl<DB, const MAX_LENGTH: u16, const MIN_LENGTH: u8> ToSql<sql_types::Text, DB>
for LengthString<MAX_LENGTH, MIN_LENGTH>
where
DB: Backend,
String: ToSql<sql_types::Text, DB>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
self.0.to_sql(out)
}
}
impl<DB> Queryable<sql_types::Text, DB> for LengthString<MAX_DESCRIPTION_LENGTH, 1>
where
DB: Backend,
Self: FromSql<sql_types::Text, DB>,
{
type Row = Self;
fn build(row: Self::Row) -> deserialize::Result<Self> {
Ok(row)
}
}
/// Domain type for description
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, AsExpression)]
#[diesel(sql_type = sql_types::Text)]
pub struct Description(LengthString<MAX_DESCRIPTION_LENGTH, 1>);
impl Description {
/// Create a new Description Domain type
pub fn new(value: String) -> Self {
Self(value)
/// Create a new Description Domain type without any length check from a static str
pub fn from_str_unchecked(input_str: &'static str) -> Self {
Self(LengthString::new_unchecked(input_str.to_owned()))
}
}
impl From<Description> for String {
fn from(description: Description) -> Self {
description.0
}
}
impl From<String> for Description {
fn from(description: String) -> Self {
Self(description)
}
}
/// Domain type for Statement Descriptor
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, AsExpression)]
#[diesel(sql_type = sql_types::Text)]
pub struct StatementDescriptor(LengthString<MAX_STATEMENT_DESCRIPTOR_LENGTH, 1>);
impl<DB> Queryable<sql_types::Text, DB> for Description
where
@ -766,18 +1091,51 @@ where
impl<DB> FromSql<sql_types::Text, DB> for Description
where
DB: Backend,
String: FromSql<sql_types::Text, DB>,
LengthString<MAX_DESCRIPTION_LENGTH, 1>: FromSql<sql_types::Text, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
let val = String::from_sql(bytes)?;
Ok(Self::from(val))
let val = LengthString::<MAX_DESCRIPTION_LENGTH, 1>::from_sql(bytes)?;
Ok(Self(val))
}
}
impl<DB> ToSql<sql_types::Text, DB> for Description
where
DB: Backend,
String: ToSql<sql_types::Text, DB>,
LengthString<MAX_DESCRIPTION_LENGTH, 1>: ToSql<sql_types::Text, DB>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
self.0.to_sql(out)
}
}
impl<DB> Queryable<sql_types::Text, DB> for StatementDescriptor
where
DB: Backend,
Self: FromSql<sql_types::Text, DB>,
{
type Row = Self;
fn build(row: Self::Row) -> deserialize::Result<Self> {
Ok(row)
}
}
impl<DB> FromSql<sql_types::Text, DB> for StatementDescriptor
where
DB: Backend,
LengthString<MAX_DESCRIPTION_LENGTH, 1>: FromSql<sql_types::Text, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
let val = LengthString::<MAX_DESCRIPTION_LENGTH, 1>::from_sql(bytes)?;
Ok(Self(val))
}
}
impl<DB> ToSql<sql_types::Text, DB> for StatementDescriptor
where
DB: Backend,
LengthString<MAX_DESCRIPTION_LENGTH, 1>: ToSql<sql_types::Text, DB>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
self.0.to_sql(out)