feat(order_details): Adding order_details both inside and outside of metadata, in payments request, for backward compatibility (#1344)

Co-authored-by: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com>
This commit is contained in:
rishavkar
2023-06-14 22:12:33 +05:30
committed by GitHub
parent 92c822257e
commit 913b833117
19 changed files with 640 additions and 268 deletions

View File

@@ -186,6 +186,14 @@ pub struct PaymentsRequest {
/// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.
pub metadata: Option<Metadata>,
/// Information about the product , quantity and amount for connectors. (e.g. Klarna)
#[schema(value_type = Option<Vec<OrderDetailsWithAmount>>, example = r#"[{
"product_name": "gillete creme",
"quantity": 15,
"amount" : 900
}]"#)]
pub order_details: Option<Vec<OrderDetailsWithAmount>>,
/// It's a token used for client side verification.
#[schema(example = "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo")]
pub client_secret: Option<String>,
@@ -1275,6 +1283,14 @@ pub struct PaymentsResponse {
#[schema(value_type = Option<Object>)]
pub metadata: Option<pii::SecretSerdeValue>,
/// Information about the product , quantity and amount for connectors. (e.g. Klarna)
#[schema(value_type = Option<Vec<OrderDetailsWithAmount>>, example = r#"[{
"product_name": "gillete creme",
"quantity": 15,
"amount" : 900
}]"#)]
pub order_details: Option<Vec<pii::SecretSerdeValue>>,
/// description: The customer's email address
#[schema(max_length = 255, value_type = Option<String>, example = "johntest@test.com")]
pub email: crypto::OptionalEncryptableEmail,
@@ -1573,6 +1589,18 @@ pub struct PaymentsRetrieveRequest {
pub merchant_connector_details: Option<admin::MerchantConnectorDetailsWrap>,
}
#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
pub struct OrderDetailsWithAmount {
/// Name of the product that is being purchased
#[schema(max_length = 255, example = "shirt")]
pub product_name: String,
/// The quantity of the product to be purchased
#[schema(example = 1)]
pub quantity: u16,
/// the amount per quantity of product
pub amount: i64,
}
#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
pub struct OrderDetails {
/// Name of the product that is being purchased

View File

@@ -946,18 +946,32 @@ fn get_address_info(address: Option<&api_models::payments::Address>) -> Option<A
}
fn get_line_items(item: &types::PaymentsAuthorizeRouterData) -> Vec<LineItem> {
let order_details = item.request.order_details.as_ref();
let line_item = LineItem {
amount_including_tax: Some(item.request.amount),
amount_excluding_tax: Some(item.request.amount),
description: order_details.map(|details| details.product_name.clone()),
// We support only one product details in payment request as of now, therefore hard coded the id.
// If we begin to support multiple product details in future then this logic should be made to create ID dynamically
id: Some(String::from("Items #1")),
tax_amount: None,
quantity: Some(order_details.map_or(1, |details| details.quantity)),
};
vec![line_item]
let order_details = item.request.order_details.clone();
match order_details {
Some(od) => od
.iter()
.enumerate()
.map(|(i, data)| LineItem {
amount_including_tax: Some(data.amount),
amount_excluding_tax: Some(data.amount),
description: Some(data.product_name.clone()),
id: Some(format!("Items #{i}")),
tax_amount: None,
quantity: Some(data.quantity),
})
.collect(),
None => {
let line_item = LineItem {
amount_including_tax: Some(item.request.amount),
amount_excluding_tax: Some(item.request.amount),
description: None,
id: Some(String::from("Items #1")),
tax_amount: None,
quantity: Some(1),
};
vec![line_item]
}
}
}
fn get_telephone_number(item: &types::PaymentsAuthorizeRouterData) -> Option<Secret<String>> {

View File

@@ -48,12 +48,15 @@ impl TryFrom<&types::PaymentsSessionRouterData> for KlarnaSessionRequest {
purchase_currency: request.currency,
order_amount: request.amount,
locale: "en-US".to_string(),
order_lines: vec![OrderLines {
name: order_details.product_name,
quantity: order_details.quantity,
unit_price: request.amount,
total_amount: request.amount,
}],
order_lines: order_details
.iter()
.map(|data| OrderLines {
name: data.product_name.clone(),
quantity: data.quantity,
unit_price: data.amount,
total_amount: i64::from(data.quantity) * (data.amount),
})
.collect(),
}),
None => Err(report!(errors::ConnectorError::MissingRequiredField {
field_name: "product_name",
@@ -93,12 +96,15 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for KlarnaPaymentsRequest {
purchase_country: "US".to_string(),
purchase_currency: request.currency,
order_amount: request.amount,
order_lines: vec![OrderLines {
name: order_details.product_name,
quantity: order_details.quantity,
unit_price: request.amount,
total_amount: request.amount,
}],
order_lines: order_details
.iter()
.map(|data| OrderLines {
name: data.product_name.clone(),
quantity: data.quantity,
unit_price: data.amount,
total_amount: i64::from(data.quantity) * (data.amount),
})
.collect(),
}),
None => Err(report!(errors::ConnectorError::MissingRequiredField {
field_name: "product_name"

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use api_models::payments::{self, OrderDetails};
use api_models::payments::{self, OrderDetailsWithAmount};
use base64::Engine;
use common_utils::{
date_time,
@@ -179,7 +179,7 @@ pub trait PaymentsAuthorizeRequestData {
fn is_auto_capture(&self) -> Result<bool, Error>;
fn get_email(&self) -> Result<Email, Error>;
fn get_browser_info(&self) -> Result<types::BrowserInformation, Error>;
fn get_order_details(&self) -> Result<OrderDetails, Error>;
fn get_order_details(&self) -> Result<Vec<OrderDetailsWithAmount>, Error>;
fn get_card(&self) -> Result<api::Card, Error>;
fn get_return_url(&self) -> Result<String, Error>;
fn connector_mandate_id(&self) -> Option<String>;
@@ -206,7 +206,7 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
.clone()
.ok_or_else(missing_field_err("browser_info"))
}
fn get_order_details(&self) -> Result<OrderDetails, Error> {
fn get_order_details(&self) -> Result<Vec<OrderDetailsWithAmount>, Error> {
self.order_details
.clone()
.ok_or_else(missing_field_err("order_details"))

View File

@@ -333,15 +333,18 @@ fn get_customer(
fn get_item_object(
item: &types::PaymentsAuthorizeRouterData,
amount: String,
_amount: String,
) -> Result<Vec<ZenItemObject>, error_stack::Report<errors::ConnectorError>> {
let order_details = item.request.get_order_details()?;
Ok(vec![ZenItemObject {
name: order_details.product_name,
price: amount.clone(),
quantity: 1,
line_amount_total: amount,
}])
Ok(order_details
.iter()
.map(|data| ZenItemObject {
name: data.product_name.clone(),
quantity: data.quantity,
price: data.amount.to_string(),
line_amount_total: (i64::from(data.quantity) * data.amount).to_string(),
})
.collect())
}
fn get_browser_details(

View File

@@ -2,7 +2,7 @@ use std::borrow::Cow;
use base64::Engine;
use common_utils::{
ext_traits::{AsyncExt, ByteSliceExt, ValueExt},
ext_traits::{AsyncExt, ByteSliceExt, Encode, ValueExt},
fp_utils, generate_id, pii,
};
// TODO : Evaluate all the helper functions ()
@@ -1686,6 +1686,7 @@ mod tests {
active_attempt_id: "nopes".to_string(),
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
order_details: None,
};
let req_cs = Some("1".to_string());
let merchant_fulfillment_time = Some(900);
@@ -1725,6 +1726,7 @@ mod tests {
active_attempt_id: "nopes".to_string(),
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
order_details: None,
};
let req_cs = Some("1".to_string());
let merchant_fulfillment_time = Some(10);
@@ -1764,6 +1766,7 @@ mod tests {
active_attempt_id: "nopes".to_string(),
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
order_details: None,
};
let req_cs = Some("1".to_string());
let merchant_fulfillment_time = Some(10);
@@ -2144,3 +2147,105 @@ impl AttemptType {
}
}
}
pub fn validate_and_add_order_details_to_payment_intent(
payment_intent: &mut storage::payment_intent::PaymentIntent,
request: &api::PaymentsRequest,
) -> RouterResult<()> {
let parsed_metadata_db: Option<api_models::payments::Metadata> = payment_intent
.metadata
.as_ref()
.map(|metadata_value| {
metadata_value
.peek()
.clone()
.parse_value("metadata")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "metadata",
})
.attach_printable("unable to parse metadata")
})
.transpose()?;
let order_details_metadata_db = parsed_metadata_db
.as_ref()
.and_then(|meta| meta.order_details.to_owned());
let order_details_outside_metadata_db = payment_intent.order_details.as_ref();
let order_details_outside_metadata_req = request.order_details.as_ref();
let order_details_metadata_req = request
.metadata
.as_ref()
.and_then(|meta| meta.order_details.to_owned());
if order_details_metadata_db
.as_ref()
.zip(order_details_outside_metadata_db.as_ref())
.is_some()
{
Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payment intent in db".to_string() })?
}
let order_details_outside = match order_details_outside_metadata_req {
Some(order) => match order_details_metadata_db {
Some(_) => Err(errors::ApiErrorResponse::NotSupported {
message: "order_details previously present inside of metadata".to_string(),
})?,
None => Some(order),
},
None => match order_details_metadata_req {
Some(_order) => match order_details_outside_metadata_db {
Some(_) => Err(errors::ApiErrorResponse::NotSupported {
message: "order_details previously present outside of metadata".to_string(),
})?,
None => None,
},
None => None,
},
};
add_order_details_and_metadata_to_payment_intent(
payment_intent,
request,
parsed_metadata_db,
&order_details_outside.map(|data| data.to_owned()),
)
}
pub fn add_order_details_and_metadata_to_payment_intent(
mut payment_intent: &mut storage::payment_intent::PaymentIntent,
request: &api::PaymentsRequest,
parsed_metadata_db: Option<api_models::payments::Metadata>,
order_details_outside: &Option<Vec<api_models::payments::OrderDetailsWithAmount>>,
) -> RouterResult<()> {
let metadata_with_order_details = match request.metadata.as_ref() {
Some(meta) => {
let transformed_metadata = match parsed_metadata_db {
Some(meta_db) => api_models::payments::Metadata {
order_details: meta.order_details.to_owned(),
..meta_db
},
None => meta.to_owned(),
};
let transformed_metadata_value =
Encode::<api_models::payments::Metadata>::encode_to_value(&transformed_metadata)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Encoding Metadata to value failed")?;
Some(masking::Secret::new(transformed_metadata_value))
}
None => None,
};
if let Some(order_details_outside_struct) = order_details_outside {
let order_details_outside_value = order_details_outside_struct
.iter()
.map(|order| {
Encode::<api_models::payments::OrderDetailsWithAmount>::encode_to_value(order)
.change_context(errors::ApiErrorResponse::InternalServerError)
.map(masking::Secret::new)
})
.collect::<Result<Vec<_>, _>>()?;
payment_intent.order_details = Some(order_details_outside_value);
};
if metadata_with_order_details.is_some() {
payment_intent.metadata = metadata_with_order_details;
}
Ok(())
}

View File

@@ -70,6 +70,11 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
"confirm",
)?;
let _ = helpers::validate_and_add_order_details_to_payment_intent(
&mut payment_intent,
request,
)?;
payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
payment_intent.payment_id.as_str(),
@@ -405,6 +410,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
let setup_future_usage = payment_data.payment_intent.setup_future_usage;
let business_label = Some(payment_data.payment_intent.business_label.clone());
let business_country = Some(payment_data.payment_intent.business_country);
let order_details = payment_data.payment_intent.order_details.clone();
let metadata = payment_data.payment_intent.metadata.clone();
payment_data.payment_intent = db
.update_payment_intent(
@@ -420,6 +427,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
return_url,
business_country,
business_label,
order_details,
metadata,
},
storage_scheme,
)
@@ -451,6 +460,18 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentConfir
BoxedOperation<'b, F, api::PaymentsRequest>,
operations::ValidateResult<'a>,
)> {
let order_details_inside_metadata = request
.metadata
.as_ref()
.and_then(|meta| meta.order_details.to_owned());
if request
.order_details
.as_ref()
.zip(order_details_inside_metadata)
.is_some()
{
Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })?
}
let given_payment_id = match &request.payment_id {
Some(id_type) => Some(
id_type

View File

@@ -1,5 +1,6 @@
use std::marker::PhantomData;
use api_models::payments::OrderDetailsWithAmount;
use async_trait::async_trait;
use common_utils::ext_traits::{AsyncExt, Encode, ValueExt};
use error_stack::{self, ResultExt};
@@ -31,6 +32,7 @@ use crate::{
},
utils::OptionExt,
};
#[derive(Debug, Clone, Copy, PaymentOperation)]
#[operation(ops = "all", flow = "authorize")]
pub struct PaymentCreate;
@@ -409,6 +411,18 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentCreate
BoxedOperation<'b, F, api::PaymentsRequest>,
operations::ValidateResult<'a>,
)> {
let order_details_inside_metadata = request
.metadata
.as_ref()
.and_then(|meta| meta.order_details.to_owned());
if request
.order_details
.as_ref()
.zip(order_details_inside_metadata)
.is_some()
{
Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })?
}
let given_payment_id = match &request.payment_id {
Some(id_type) => Some(
id_type
@@ -548,6 +562,32 @@ impl PaymentCreate {
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Encoding Metadata to value failed")?;
let order_details_metadata_req = request
.metadata
.as_ref()
.and_then(|meta| meta.order_details.to_owned());
if request
.order_details
.as_ref()
.zip(order_details_metadata_req)
.is_some()
{
Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })?
}
let order_details_outside_value = match request.order_details.as_ref() {
Some(od_value) => {
let order_details_outside_value_secret = od_value
.iter()
.map(|order| {
Encode::<OrderDetailsWithAmount>::encode_to_value(order)
.change_context(errors::ApiErrorResponse::InternalServerError)
.map(masking::Secret::new)
})
.collect::<Result<Vec<_>, _>>()?;
Some(order_details_outside_value_secret)
}
None => None,
};
let (business_country, business_label) = helpers::get_business_details(
request.business_country,
@@ -577,6 +617,7 @@ impl PaymentCreate {
business_country,
business_label,
active_attempt_id,
order_details: order_details_outside_value,
..storage::PaymentIntentNew::default()
})
}

View File

@@ -23,6 +23,7 @@ use crate::{
},
utils::OptionExt,
};
#[derive(Debug, Clone, Copy, PaymentOperation)]
#[operation(ops = "all", flow = "authorize")]
pub struct PaymentUpdate;
@@ -86,6 +87,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
let _ = helpers::validate_and_add_order_details_to_payment_intent(
&mut payment_intent,
request,
)?;
payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
payment_intent.payment_id.as_str(),
@@ -276,7 +281,6 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
.clone()
.map(ForeignInto::foreign_into)),
});
Ok((
next_operation,
PaymentData {
@@ -466,6 +470,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
let setup_future_usage = payment_data.payment_intent.setup_future_usage;
let business_label = Some(payment_data.payment_intent.business_label.clone());
let business_country = Some(payment_data.payment_intent.business_country);
let order_details = payment_data.payment_intent.order_details.clone();
let metadata = payment_data.payment_intent.metadata.clone();
payment_data.payment_intent = db
.update_payment_intent(
@@ -481,6 +487,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
return_url,
business_country,
business_label,
order_details,
metadata,
},
storage_scheme,
)
@@ -506,6 +514,18 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentUpdate
BoxedOperation<'b, F, api::PaymentsRequest>,
operations::ValidateResult<'a>,
)> {
let order_details_inside_metadata = request
.metadata
.as_ref()
.and_then(|meta| meta.order_details.to_owned());
if request
.order_details
.as_ref()
.zip(order_details_inside_metadata)
.is_some()
{
Err(errors::ApiErrorResponse::NotSupported { message: "order_details cannot be present both inside and outside metadata in payments request".to_string() })?
}
let given_payment_id = match &request.payment_id {
Some(id_type) => Some(
id_type

View File

@@ -1,7 +1,9 @@
use std::{fmt::Debug, marker::PhantomData};
use api_models::payments::OrderDetailsWithAmount;
use common_utils::fp_utils;
use error_stack::ResultExt;
use masking::PeekInterface;
use router_env::{instrument, tracing};
use storage_models::ephemeral_key;
@@ -437,6 +439,7 @@ where
.map(ForeignInto::foreign_into),
)
.set_metadata(payment_intent.metadata)
.set_order_details(payment_intent.order_details)
.set_connector_label(connector_label)
.set_business_country(payment_intent.business_country)
.set_business_label(payment_intent.business_label)
@@ -488,6 +491,7 @@ where
cancellation_reason: payment_attempt.cancellation_reason,
payment_token: payment_attempt.payment_token,
metadata: payment_intent.metadata,
order_details: payment_intent.order_details,
..Default::default()
}),
});
@@ -521,6 +525,7 @@ impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::Pay
currency: pi.currency.map(|c| c.to_string()).unwrap_or_default(),
description: pi.description,
metadata: pi.metadata,
order_details: pi.order_details,
customer_id: pi.customer_id,
connector: pa.connector,
payment_method: pa.payment_method.map(ForeignInto::foreign_into),
@@ -564,6 +569,17 @@ pub fn bank_transfer_next_steps_check(
Ok(bank_transfer_next_step)
}
pub fn change_order_details_to_new_type(
order_amount: i64,
order_details: api_models::payments::OrderDetails,
) -> Option<Vec<OrderDetailsWithAmount>> {
Some(vec![OrderDetailsWithAmount {
product_name: order_details.product_name,
quantity: order_details.quantity,
amount: order_amount,
}])
}
#[derive(Clone)]
pub struct PaymentAdditionalData<'a, F>
where
@@ -578,7 +594,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
type Error = error_stack::Report<errors::ApiErrorResponse>;
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
let payment_data = additional_data.payment_data;
let payment_data = additional_data.payment_data.clone();
let router_base_url = &additional_data.router_base_url;
let connector_name = &additional_data.connector_name;
let attempt = &payment_data.payment_attempt;
@@ -594,8 +610,10 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
let parsed_metadata: Option<api_models::payments::Metadata> = payment_data
.payment_intent
.metadata
.as_ref()
.map(|metadata_value| {
metadata_value
.clone()
.parse_value("metadata")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "metadata",
@@ -604,11 +622,11 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
})
.transpose()
.unwrap_or_default();
let order_category = parsed_metadata
.as_ref()
.and_then(|data| data.order_category.clone());
let order_details = parsed_metadata.and_then(|data| data.order_details);
let order_details =
fetch_order_details(additional_data.clone(), parsed_metadata, &payment_data)?;
let complete_authorize_url = Some(helpers::create_complete_authorize_url(
router_base_url,
attempt,
@@ -765,12 +783,14 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionD
type Error = error_stack::Report<errors::ApiErrorResponse>;
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
let payment_data = additional_data.payment_data;
let payment_data = additional_data.payment_data.clone();
let parsed_metadata: Option<api_models::payments::Metadata> = payment_data
.payment_intent
.metadata
.as_ref()
.map(|metadata_value| {
metadata_value
.clone()
.parse_value("metadata")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "metadata",
@@ -780,7 +800,8 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionD
.transpose()
.unwrap_or_default();
let order_details = parsed_metadata.and_then(|data| data.order_details);
let order_details =
fetch_order_details(additional_data.clone(), parsed_metadata, &payment_data)?;
Ok(Self {
amount: payment_data.amount.into(),
@@ -878,3 +899,38 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsPreProce
})
}
}
pub fn fetch_order_details<F: Clone>(
additional_data: PaymentAdditionalData<'_, F>,
parsed_metadata: Option<api_models::payments::Metadata>,
payment_data: &PaymentData<F>,
) -> RouterResult<Option<Vec<OrderDetailsWithAmount>>> {
let order_details_metadata_parsed = parsed_metadata.and_then(|data| data.order_details);
let order_details_outside_metadata_parsed =
match payment_data.payment_intent.order_details.clone() {
Some(order_details_outside_metadata_value) => {
let parsed_value = order_details_outside_metadata_value
.iter()
.map(|data| {
data.peek()
.to_owned()
.parse_value("OrderDetailsWithAmount")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "OrderDetailsWithAmount",
})
.attach_printable("unable to parse OrderDetailsWithAmount")
})
.collect::<Result<Vec<_>, _>>()?;
Some(parsed_value)
}
None => None,
};
let order_details = match order_details_metadata_parsed {
Some(odm) => change_order_details_to_new_type(
additional_data.clone().payment_data.payment_intent.amount,
odm,
),
None => order_details_outside_metadata_parsed,
};
Ok(order_details)
}

View File

@@ -95,6 +95,7 @@ mod storage {
business_country: new.business_country,
business_label: new.business_label.clone(),
active_attempt_id: new.active_attempt_id.to_owned(),
order_details: new.order_details.clone(),
};
match self
@@ -353,6 +354,7 @@ impl PaymentIntentInterface for MockDb {
business_country: new.business_country,
business_label: new.business_label,
active_attempt_id: new.active_attempt_id.to_owned(),
order_details: new.order_details,
};
payment_intents.push(payment_intent.clone());
Ok(payment_intent)

View File

@@ -174,6 +174,7 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payments::BankRedirectBilling,
api_models::payments::BankRedirectBilling,
api_models::payments::OrderDetails,
api_models::payments::OrderDetailsWithAmount,
api_models::payments::NextActionType,
api_models::payments::Metadata,
api_models::payments::WalletData,

View File

@@ -225,7 +225,7 @@ pub struct PaymentsAuthorizeData {
pub off_session: Option<bool>,
pub setup_mandate_details: Option<payments::MandateData>,
pub browser_info: Option<BrowserInformation>,
pub order_details: Option<api_models::payments::OrderDetails>,
pub order_details: Option<Vec<api_models::payments::OrderDetailsWithAmount>>,
pub order_category: Option<String>,
pub session_token: Option<String>,
pub enrolled_for_3ds: bool,
@@ -322,7 +322,7 @@ pub struct PaymentsSessionData {
pub amount: i64,
pub currency: storage_enums::Currency,
pub country: Option<api::enums::CountryAlpha2>,
pub order_details: Option<api_models::payments::OrderDetails>,
pub order_details: Option<Vec<api_models::payments::OrderDetailsWithAmount>>,
}
#[derive(Debug, Clone)]

View File

@@ -1,6 +1,6 @@
use std::str::FromStr;
use api_models::payments::OrderDetails;
use api_models::payments::OrderDetailsWithAmount;
use cards::CardNumber;
use common_utils::pii::Email;
use masking::Secret;
@@ -306,10 +306,11 @@ async fn should_fail_payment_for_incorrect_card_number() {
card_number: CardNumber::from_str("1234567891011").unwrap(),
..utils::CCardType::default().0
}),
order_details: Some(OrderDetails {
order_details: Some(vec![OrderDetailsWithAmount {
product_name: "test".to_string(),
quantity: 1,
}),
amount: 1000,
}]),
email: Some(Email::from_str("test@gmail.com").unwrap()),
webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()),
..utils::PaymentAuthorizeType::default().0
@@ -340,10 +341,11 @@ async fn should_fail_payment_for_incorrect_cvc() {
card_cvc: Secret::new("12345".to_string()),
..utils::CCardType::default().0
}),
order_details: Some(OrderDetails {
order_details: Some(vec![OrderDetailsWithAmount {
product_name: "test".to_string(),
quantity: 1,
}),
amount: 1000,
}]),
email: Some(Email::from_str("test@gmail.com").unwrap()),
webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()),
..utils::PaymentAuthorizeType::default().0
@@ -374,10 +376,11 @@ async fn should_fail_payment_for_invalid_exp_month() {
card_exp_month: Secret::new("20".to_string()),
..utils::CCardType::default().0
}),
order_details: Some(OrderDetails {
order_details: Some(vec![OrderDetailsWithAmount {
product_name: "test".to_string(),
quantity: 1,
}),
amount: 1000,
}]),
email: Some(Email::from_str("test@gmail.com").unwrap()),
webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()),
..utils::PaymentAuthorizeType::default().0
@@ -408,10 +411,11 @@ async fn should_fail_payment_for_incorrect_expiry_year() {
card_exp_year: Secret::new("2000".to_string()),
..utils::CCardType::default().0
}),
order_details: Some(OrderDetails {
order_details: Some(vec![OrderDetailsWithAmount {
product_name: "test".to_string(),
quantity: 1,
}),
amount: 1000,
}]),
email: Some(Email::from_str("test@gmail.com").unwrap()),
webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()),
..utils::PaymentAuthorizeType::default().0

View File

@@ -36,6 +36,8 @@ pub struct PaymentIntent {
pub active_attempt_id: String,
pub business_country: storage_enums::CountryAlpha2,
pub business_label: String,
#[diesel(deserialize_as = super::OptionalDieselArray<pii::SecretSerdeValue>)]
pub order_details: Option<Vec<pii::SecretSerdeValue>>,
}
#[derive(
@@ -78,6 +80,8 @@ pub struct PaymentIntentNew {
pub active_attempt_id: String,
pub business_country: storage_enums::CountryAlpha2,
pub business_label: String,
#[diesel(deserialize_as = super::OptionalDieselArray<pii::SecretSerdeValue>)]
pub order_details: Option<Vec<pii::SecretSerdeValue>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -116,6 +120,8 @@ pub enum PaymentIntentUpdate {
return_url: Option<String>,
business_country: Option<storage_enums::CountryAlpha2>,
business_label: Option<String>,
order_details: Option<Vec<pii::SecretSerdeValue>>,
metadata: Option<pii::SecretSerdeValue>,
},
PaymentAttemptUpdate {
active_attempt_id: String,
@@ -146,6 +152,8 @@ pub struct PaymentIntentUpdateInternal {
pub active_attempt_id: Option<String>,
pub business_country: Option<storage_enums::CountryAlpha2>,
pub business_label: Option<String>,
#[diesel(deserialize_as = super::OptionalDieselArray<pii::SecretSerdeValue>)]
pub order_details: Option<Vec<pii::SecretSerdeValue>>,
}
impl PaymentIntentUpdate {
@@ -173,6 +181,7 @@ impl PaymentIntentUpdate {
.shipping_address_id
.or(source.shipping_address_id),
modified_at: common_utils::date_time::now(),
order_details: internal_update.order_details.or(source.order_details),
..source
}
}
@@ -192,6 +201,8 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
return_url,
business_country,
business_label,
order_details,
metadata,
} => Self {
amount: Some(amount),
currency: Some(currency),
@@ -205,6 +216,8 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
return_url,
business_country,
business_label,
order_details,
metadata,
..Default::default()
},
PaymentIntentUpdate::MetadataUpdate { metadata } => Self {

View File

@@ -475,6 +475,7 @@ diesel::table! {
business_country -> CountryAlpha2,
#[max_length = 64]
business_label -> Varchar,
order_details -> Nullable<Array<Nullable<Jsonb>>>,
}
}

View File

@@ -0,0 +1 @@
ALTER TABLE payment_intent DROP COLUMN order_details;

View File

@@ -0,0 +1 @@
ALTER TABLE payment_intent ADD COLUMN order_details jsonb[];

View File

@@ -5425,6 +5425,34 @@
}
}
},
"OrderDetailsWithAmount": {
"type": "object",
"required": [
"product_name",
"quantity",
"amount"
],
"properties": {
"product_name": {
"type": "string",
"description": "Name of the product that is being purchased",
"example": "shirt",
"maxLength": 255
},
"quantity": {
"type": "integer",
"format": "int32",
"description": "The quantity of the product to be purchased",
"example": 1,
"minimum": 0.0
},
"amount": {
"type": "integer",
"format": "int64",
"description": "the amount per quantity of product"
}
}
},
"PayLaterData": {
"oneOf": [
{
@@ -6187,26 +6215,30 @@
"PaymentsCreateRequest": {
"type": "object",
"required": [
"amount",
"manual_retry",
"currency",
"amount"
"currency"
],
"properties": {
"payment_method_data": {
"allOf": [
{
"$ref": "#/components/schemas/PaymentMethodData"
}
],
"phone": {
"type": "string",
"description": "The customer's phone number",
"example": "3141592653",
"nullable": true,
"maxLength": 255
},
"card_cvc": {
"type": "string",
"description": "This is used when payment is to be confirmed and the card is not saved",
"nullable": true
},
"capture_method": {
"allOf": [
{
"$ref": "#/components/schemas/CaptureMethod"
}
],
"nullable": true
"amount": {
"type": "integer",
"format": "int64",
"description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,",
"example": 6540,
"nullable": true,
"minimum": 0.0
},
"customer_id": {
"type": "string",
@@ -6215,87 +6247,28 @@
"nullable": true,
"maxLength": 255
},
"payment_method": {
"allOf": [
{
"$ref": "#/components/schemas/PaymentMethod"
}
],
"order_details": {
"type": "array",
"items": {
"$ref": "#/components/schemas/OrderDetailsWithAmount"
},
"description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)",
"example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]",
"nullable": true
},
"confirm": {
"type": "boolean",
"description": "Whether to confirm the payment (if applicable)",
"default": false,
"example": true,
"amount_to_capture": {
"type": "integer",
"format": "int64",
"description": "The Amount to be captured/ debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,\nIf not provided, the default amount_to_capture will be the payment amount.",
"example": 6540,
"nullable": true
},
"shipping": {
"allOf": [
{
"$ref": "#/components/schemas/Address"
}
],
"nullable": true
},
"payment_id": {
"business_label": {
"type": "string",
"description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.",
"example": "pay_mbabizu24mvu3mela5njyhpit4",
"nullable": true,
"maxLength": 30,
"minLength": 30
},
"payment_method_type": {
"allOf": [
{
"$ref": "#/components/schemas/PaymentMethodType"
}
],
"description": "Business label of the merchant for this payment",
"example": "food",
"nullable": true
},
"payment_experience": {
"allOf": [
{
"$ref": "#/components/schemas/PaymentExperience"
}
],
"nullable": true
},
"payment_token": {
"type": "string",
"description": "Provide a reference to a stored payment method",
"example": "187282ab-40ef-47a9-9206-5099ba31e432",
"nullable": true
},
"authentication_type": {
"allOf": [
{
"$ref": "#/components/schemas/AuthenticationType"
}
],
"nullable": true
},
"routing": {
"allOf": [
{
"$ref": "#/components/schemas/RoutingAlgorithm"
}
],
"nullable": true
},
"business_country": {
"allOf": [
{
"$ref": "#/components/schemas/CountryAlpha2"
}
],
"nullable": true
},
"manual_retry": {
"type": "boolean",
"description": "If enabled payment can be retried from the client side until the payment is successful or payment expires or the attempts(configured by the merchant) for payment are exhausted."
},
"capture_on": {
"type": "string",
"format": "date-time",
@@ -6303,37 +6276,29 @@
"example": "2022-09-10T10:11:12Z",
"nullable": true
},
"statement_descriptor_suffix": {
"phone_country_code": {
"type": "string",
"description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor thats set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.",
"example": "Payment for shoes purchase",
"description": "The country code for the customer phone number",
"example": "+1",
"nullable": true,
"maxLength": 255
},
"merchant_connector_details": {
"allOf": [
{
"$ref": "#/components/schemas/MerchantConnectorDetailsWrap"
}
],
"payment_token": {
"type": "string",
"description": "Provide a reference to a stored payment method",
"example": "187282ab-40ef-47a9-9206-5099ba31e432",
"nullable": true
},
"business_sub_label": {
"client_secret": {
"type": "string",
"description": "Business sub label for the payment",
"description": "It's a token used for client side verification.",
"example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo",
"nullable": true
},
"email": {
"type": "string",
"description": "description: The customer's email address",
"example": "johntest@test.com",
"nullable": true,
"maxLength": 255
},
"setup_future_usage": {
"payment_experience": {
"allOf": [
{
"$ref": "#/components/schemas/FutureUsage"
"$ref": "#/components/schemas/PaymentExperience"
}
],
"nullable": true
@@ -6346,10 +6311,55 @@
"description": "Allowed Payment Method Types for a given PaymentIntent",
"nullable": true
},
"currency": {
"email": {
"type": "string",
"description": "description: The customer's email address",
"example": "johntest@test.com",
"nullable": true,
"maxLength": 255
},
"authentication_type": {
"allOf": [
{
"$ref": "#/components/schemas/Currency"
"$ref": "#/components/schemas/AuthenticationType"
}
],
"nullable": true
},
"business_country": {
"allOf": [
{
"$ref": "#/components/schemas/CountryAlpha2"
}
],
"nullable": true
},
"return_url": {
"type": "string",
"description": "The URL to redirect after the completion of the operation",
"example": "https://hyperswitch.io",
"nullable": true
},
"payment_method_data": {
"allOf": [
{
"$ref": "#/components/schemas/PaymentMethodData"
}
],
"nullable": true
},
"payment_id": {
"type": "string",
"description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.",
"example": "pay_mbabizu24mvu3mela5njyhpit4",
"nullable": true,
"maxLength": 30,
"minLength": 30
},
"shipping": {
"allOf": [
{
"$ref": "#/components/schemas/Address"
}
],
"nullable": true
@@ -6362,48 +6372,6 @@
],
"nullable": true
},
"phone_country_code": {
"type": "string",
"description": "The country code for the customer phone number",
"example": "+1",
"nullable": true,
"maxLength": 255
},
"return_url": {
"type": "string",
"description": "The URL to redirect after the completion of the operation",
"example": "https://hyperswitch.io",
"nullable": true
},
"statement_descriptor_name": {
"type": "string",
"description": "For non-card charges, you can use this value as the complete description that appears on your customers statements. Must contain at least one letter, maximum 22 characters.",
"example": "Hyperswitch Router",
"nullable": true,
"maxLength": 255
},
"name": {
"type": "string",
"description": "description: The customer's name",
"example": "John Test",
"nullable": true,
"maxLength": 255
},
"amount": {
"type": "integer",
"format": "int64",
"description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,",
"example": 6540,
"nullable": true,
"minimum": 0.0
},
"amount_to_capture": {
"type": "integer",
"format": "int64",
"description": "The Amount to be captured/ debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,\nIf not provided, the default amount_to_capture will be the payment amount.",
"example": 6540,
"nullable": true
},
"metadata": {
"allOf": [
{
@@ -6412,69 +6380,6 @@
],
"nullable": true
},
"merchant_id": {
"type": "string",
"description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request",
"example": "merchant_1668273825",
"nullable": true,
"maxLength": 255
},
"phone": {
"type": "string",
"description": "The customer's phone number",
"example": "3141592653",
"nullable": true,
"maxLength": 255
},
"client_secret": {
"type": "string",
"description": "It's a token used for client side verification.",
"example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo",
"nullable": true
},
"mandate_data": {
"allOf": [
{
"$ref": "#/components/schemas/MandateData"
}
],
"nullable": true
},
"mandate_id": {
"type": "string",
"description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data",
"example": "mandate_iwer89rnjef349dni3",
"nullable": true,
"maxLength": 255
},
"browser_info": {
"type": "object",
"description": "Additional details required by 3DS 2.0",
"nullable": true
},
"business_label": {
"type": "string",
"description": "Business label of the merchant for this payment",
"example": "food",
"nullable": true
},
"description": {
"type": "string",
"description": "A description of the payment",
"example": "It's my first payment request",
"nullable": true
},
"card_cvc": {
"type": "string",
"description": "This is used when payment is to be confirmed and the card is not saved",
"nullable": true
},
"off_session": {
"type": "boolean",
"description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with `confirm: true`.",
"example": true,
"nullable": true
},
"connector": {
"type": "array",
"items": {
@@ -6486,6 +6391,138 @@
"adyen"
],
"nullable": true
},
"capture_method": {
"allOf": [
{
"$ref": "#/components/schemas/CaptureMethod"
}
],
"nullable": true
},
"manual_retry": {
"type": "boolean",
"description": "If enabled payment can be retried from the client side until the payment is successful or payment expires or the attempts(configured by the merchant) for payment are exhausted."
},
"statement_descriptor_suffix": {
"type": "string",
"description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor thats set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.",
"example": "Payment for shoes purchase",
"nullable": true,
"maxLength": 255
},
"payment_method_type": {
"allOf": [
{
"$ref": "#/components/schemas/PaymentMethodType"
}
],
"nullable": true
},
"mandate_data": {
"allOf": [
{
"$ref": "#/components/schemas/MandateData"
}
],
"nullable": true
},
"confirm": {
"type": "boolean",
"description": "Whether to confirm the payment (if applicable)",
"default": false,
"example": true,
"nullable": true
},
"description": {
"type": "string",
"description": "A description of the payment",
"example": "It's my first payment request",
"nullable": true
},
"off_session": {
"type": "boolean",
"description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with `confirm: true`.",
"example": true,
"nullable": true
},
"merchant_connector_details": {
"allOf": [
{
"$ref": "#/components/schemas/MerchantConnectorDetailsWrap"
}
],
"nullable": true
},
"payment_method": {
"allOf": [
{
"$ref": "#/components/schemas/PaymentMethod"
}
],
"nullable": true
},
"statement_descriptor_name": {
"type": "string",
"description": "For non-card charges, you can use this value as the complete description that appears on your customers statements. Must contain at least one letter, maximum 22 characters.",
"example": "Hyperswitch Router",
"nullable": true,
"maxLength": 255
},
"merchant_id": {
"type": "string",
"description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request",
"example": "merchant_1668273825",
"nullable": true,
"maxLength": 255
},
"routing": {
"allOf": [
{
"$ref": "#/components/schemas/RoutingAlgorithm"
}
],
"nullable": true
},
"name": {
"type": "string",
"description": "description: The customer's name",
"example": "John Test",
"nullable": true,
"maxLength": 255
},
"setup_future_usage": {
"allOf": [
{
"$ref": "#/components/schemas/FutureUsage"
}
],
"nullable": true
},
"mandate_id": {
"type": "string",
"description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data",
"example": "mandate_iwer89rnjef349dni3",
"nullable": true,
"maxLength": 255
},
"currency": {
"allOf": [
{
"$ref": "#/components/schemas/Currency"
}
],
"nullable": true
},
"browser_info": {
"type": "object",
"description": "Additional details required by 3DS 2.0",
"nullable": true
},
"business_sub_label": {
"type": "string",
"description": "Business sub label for the payment",
"nullable": true
}
}
},
@@ -6706,6 +6743,15 @@
],
"nullable": true
},
"order_details": {
"type": "array",
"items": {
"$ref": "#/components/schemas/OrderDetailsWithAmount"
},
"description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)",
"example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]",
"nullable": true
},
"client_secret": {
"type": "string",
"description": "It's a token used for client side verification.",
@@ -6973,6 +7019,15 @@
"description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.",
"nullable": true
},
"order_details": {
"type": "array",
"items": {
"$ref": "#/components/schemas/OrderDetailsWithAmount"
},
"description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)",
"example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]",
"nullable": true
},
"email": {
"type": "string",
"description": "description: The customer's email address",