feat(payments): add connector_metadata, metadata and feature_metadata fields in payments, remove udf field (#1595)

This commit is contained in:
Abhishek Marrivagu
2023-07-05 17:51:05 +05:30
committed by GitHub
parent 8c90d0a78c
commit e713b62ae3
20 changed files with 479 additions and 441 deletions

View File

@@ -3,6 +3,7 @@ use std::num::NonZeroI64;
use cards::CardNumber;
use common_utils::{
crypto,
ext_traits::Encode,
pii::{self, Email},
};
use masking::{PeekInterface, Secret};
@@ -215,9 +216,6 @@ pub struct PaymentsRequest {
#[schema(max_length = 255, example = "Payment for shoes purchase")]
pub statement_descriptor_suffix: Option<String>,
/// 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",
@@ -282,9 +280,72 @@ pub struct PaymentsRequest {
#[schema(value_type = Option<RetryAction>)]
pub retry_action: Option<api_enums::RetryAction>,
/// Any user defined fields can be passed here.
/// 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.
#[schema(value_type = Option<Object>, example = r#"{ "udf1": "some-value", "udf2": "some-value" }"#)]
pub udf: Option<pii::SecretSerdeValue>,
pub metadata: Option<pii::SecretSerdeValue>,
/// additional data related to some connectors
pub connector_metadata: Option<ConnectorMetadata>,
/// additional data that might be required by hyperswitch
pub feature_metadata: Option<FeatureMetadata>,
}
impl PaymentsRequest {
pub fn get_feature_metadata_as_value(
&self,
) -> common_utils::errors::CustomResult<
Option<serde_json::Value>,
common_utils::errors::ParsingError,
> {
self.feature_metadata
.as_ref()
.map(Encode::<FeatureMetadata>::encode_to_value)
.transpose()
}
pub fn get_connector_metadata_as_value(
&self,
) -> common_utils::errors::CustomResult<
Option<serde_json::Value>,
common_utils::errors::ParsingError,
> {
self.connector_metadata
.as_ref()
.map(Encode::<ConnectorMetadata>::encode_to_value)
.transpose()
}
pub fn get_allowed_payment_method_types_as_value(
&self,
) -> common_utils::errors::CustomResult<
Option<serde_json::Value>,
common_utils::errors::ParsingError,
> {
self.allowed_payment_method_types
.as_ref()
.map(Encode::<Vec<api_enums::PaymentMethodType>>::encode_to_value)
.transpose()
}
pub fn get_order_details_as_value(
&self,
) -> common_utils::errors::CustomResult<
Option<Vec<pii::SecretSerdeValue>>,
common_utils::errors::ParsingError,
> {
self.order_details
.as_ref()
.map(|od| {
od.iter()
.map(|order| {
Encode::<OrderDetailsWithAmount>::encode_to_value(order)
.map(masking::Secret::new)
})
.collect::<Result<Vec<_>, _>>()
})
.transpose()
}
}
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq, Eq)]
@@ -1354,10 +1415,6 @@ pub struct PaymentsResponse {
/// The billing address for the payment
pub billing: Option<Address>,
/// 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.
#[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",
@@ -1432,7 +1489,7 @@ pub struct PaymentsResponse {
/// Allowed Payment Method Types for a given PaymentIntent
#[schema(value_type = Option<Vec<PaymentMethodType>>)]
pub allowed_payment_method_types: Option<Vec<api_enums::PaymentMethodType>>,
pub allowed_payment_method_types: Option<serde_json::Value>,
/// ephemeral_key for the customer_id mentioned
pub ephemeral_key: Option<EphemeralKeyCreateResponse>,
@@ -1440,13 +1497,21 @@ pub struct PaymentsResponse {
/// If true the payment can be retried with same or different payment method which means the confirm call can be made again.
pub manual_retry_allowed: Option<bool>,
/// Any user defined fields can be passed here.
#[schema(value_type = Option<Object>, example = r#"{ "udf1": "some-value", "udf2": "some-value" }"#)]
pub udf: Option<pii::SecretSerdeValue>,
/// A unique identifier for a payment provided by the connector
#[schema(value_type = Option<String>, example = "993672945374576J")]
pub connector_transaction_id: Option<String>,
/// 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.
#[schema(value_type = Option<Object>, example = r#"{ "udf1": "some-value", "udf2": "some-value" }"#)]
pub metadata: Option<pii::SecretSerdeValue>,
/// additional data related to some connectors
#[schema(value_type = Option<ConnectorMetadata>)]
pub connector_metadata: Option<serde_json::Value>, // This is Value because it is fetched from DB and before putting in DB the type is validated
/// additional data that might be required by hyperswitch
#[schema(value_type = Option<FeatureMetadata>)]
pub feature_metadata: Option<serde_json::Value>, // This is Value because it is fetched from DB and before putting in DB the type is validated
}
#[derive(Clone, Debug, serde::Deserialize, ToSchema)]
@@ -1704,23 +1769,6 @@ pub struct OrderDetails {
pub quantity: u16,
}
#[derive(Default, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
pub struct Metadata {
/// Information about the product and quantity for specific connectors. (e.g. Klarna)
pub order_details: Option<OrderDetails>,
/// Information about the order category that merchant wants to specify at connector level. (e.g. In Noon Payments it can take values like "pay", "food", or any other custom string set by the merchant in Noon's Dashboard)
pub order_category: Option<String>,
/// Redirection response coming in request as metadata field only for redirection scenarios
#[schema(value_type = Option<RedirectResponse>)]
pub redirect_response: Option<RedirectResponse>,
/// Allowed payment method types for a payment intent
#[schema(value_type = Option<Vec<PaymentMethodType>>)]
pub allowed_payment_method_types: Option<Vec<api_enums::PaymentMethodType>>,
}
#[derive(Default, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
pub struct RedirectResponse {
#[schema(value_type = Option<String>)]
@@ -1828,12 +1876,26 @@ pub struct ApplepaySessionRequest {
pub initiative_context: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct ConnectorMetadata {
pub apple_pay: Option<ApplepayConnectorMetadataRequest>,
pub airwallex: Option<AirwallexData>,
pub noon: Option<NoonData>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct AirwallexData {
/// payload required by airwallex
payload: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct NoonData {
/// Information about the order category that merchant wants to specify at connector level. (e.g. In Noon Payments it can take values like "pay", "food", or any other custom string set by the merchant in Noon's Dashboard)
pub order_category: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct ApplepayConnectorMetadataRequest {
pub session_token_data: Option<SessionTokenInfo>,
}
@@ -1857,7 +1919,7 @@ pub struct PaymentRequestMetadata {
pub label: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct SessionTokenInfo {
pub certificate: String,
pub certificate_keys: String,
@@ -2111,6 +2173,13 @@ pub struct PaymentsStartRequest {
pub attempt_id: String,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)]
pub struct FeatureMetadata {
/// Redirection response coming in request as metadata field only for redirection scenarios
#[schema(value_type = Option<RedirectResponse>)]
pub redirect_response: Option<RedirectResponse>,
}
mod payment_id_type {
use std::fmt;

View File

@@ -14,7 +14,6 @@ use crate::{
api::{admin, enums as api_enums},
transformers::{ForeignFrom, ForeignTryFrom},
},
utils::OptionExt,
};
#[derive(Default, Serialize, PartialEq, Eq, Deserialize, Clone)]
@@ -220,13 +219,7 @@ impl TryFrom<StripePaymentIntentRequest> for payments::PaymentsRequest {
field_name: "receipt_ipaddress".to_string(),
expected_format: "127.0.0.1".to_string(),
})?;
let metadata_object = item
.metadata
.clone()
.parse_value("metadata")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "metadata mapping failed",
})?;
let request = Ok(Self {
payment_id: item.id.map(payments::PaymentIdType::PaymentIntentId),
amount: item.amount.map(|amount| amount.into()),
@@ -264,8 +257,7 @@ impl TryFrom<StripePaymentIntentRequest> for payments::PaymentsRequest {
.and_then(|pmd| pmd.billing_details.map(payments::Address::from)),
statement_descriptor_name: item.statement_descriptor,
statement_descriptor_suffix: item.statement_descriptor_suffix,
metadata: metadata_object,
udf: item.metadata,
metadata: item.metadata,
client_secret: item.client_secret.map(|s| s.peek().clone()),
authentication_type,
mandate_data: mandate_options,
@@ -447,7 +439,7 @@ impl From<payments::PaymentsResponse> for StripePaymentIntentResponse {
statement_descriptor_suffix: resp.statement_descriptor_suffix,
next_action: into_stripe_next_action(resp.next_action, resp.return_url),
cancellation_reason: resp.cancellation_reason,
metadata: resp.udf,
metadata: resp.metadata,
charges: Charges::new(),
last_payment_error: resp.error_code.map(|code| LastPaymentError {
charge: None,

View File

@@ -237,7 +237,6 @@ impl TryFrom<StripeSetupIntentRequest> for payments::PaymentsRequest {
statement_descriptor_name: item.statement_descriptor,
statement_descriptor_suffix: item.statement_descriptor_suffix,
metadata: metadata_object,
udf: item.metadata,
client_secret: item.client_secret.map(|s| s.peek().clone()),
setup_future_usage: item.setup_future_usage,
merchant_connector_details: item.merchant_connector_details,
@@ -419,7 +418,7 @@ impl From<payments::PaymentsResponse> for StripeSetupIntentResponse {
charges: payment_intent::Charges::new(),
created: resp.created,
customer: resp.customer_id,
metadata: resp.udf,
metadata: resp.metadata,
id: resp.payment_id,
refunds: resp
.refunds

View File

@@ -1206,26 +1206,17 @@ pub async fn filter_payment_methods(
let parse_result = serde_json::from_value::<PaymentMethodsEnabled>(payment_method);
if let Ok(payment_methods_enabled) = parse_result {
let payment_method = payment_methods_enabled.payment_method;
let allowed_payment_method_types = payment_intent
.map(|payment_intent|
.and_then(|payment_intent| {
payment_intent
.metadata
.as_ref()
.and_then(|masked_metadata| {
let metadata = masked_metadata.peek().clone();
let parsed_metadata: Option<api_models::payments::Metadata> =
serde_json::from_value(metadata)
.map_err(|error| logger::error!(%error, "Failed to deserialize PaymentIntent metadata"))
.ok();
parsed_metadata.and_then(|pm| {
logger::info!(
"Only given PaymentMethodTypes will be allowed {:?}",
pm.allowed_payment_method_types
);
pm.allowed_payment_method_types
})
}))
.and_then(|a| a);
.allowed_payment_method_types
.clone()
.parse_value("Vec<PaymentMethodType>")
.map_err(|error| logger::error!(%error, "Failed to deserialize PaymentIntent allowed_payment_method_types"))
.ok()
});
for payment_method_type_info in payment_methods_enabled
.payment_method_types
.unwrap_or_default()

View File

@@ -8,7 +8,6 @@ pub mod transformers;
use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant};
use api_models::payments::Metadata;
use common_utils::pii;
use error_stack::{IntoReport, ResultExt};
use futures::future::join_all;
@@ -382,14 +381,11 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
let payment_confirm_req = api::PaymentsRequest {
payment_id: Some(req.resource_id.clone()),
merchant_id: req.merchant_id.clone(),
metadata: Some(Metadata {
order_details: None,
feature_metadata: Some(api_models::payments::FeatureMetadata {
redirect_response: Some(api_models::payments::RedirectResponse {
param: req.param.map(Secret::new),
json_payload: Some(req.json_payload.unwrap_or(serde_json::json!({})).into()),
}),
allowed_payment_method_types: None,
order_category: None,
}),
..Default::default()
};

View File

@@ -2,7 +2,7 @@ use std::borrow::Cow;
use base64::Engine;
use common_utils::{
ext_traits::{AsyncExt, ByteSliceExt, Encode, ValueExt},
ext_traits::{AsyncExt, ByteSliceExt, ValueExt},
fp_utils, generate_id, pii,
};
// TODO : Evaluate all the helper functions ()
@@ -1906,7 +1906,9 @@ mod tests {
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
order_details: None,
udf: None,
allowed_payment_method_types: None,
connector_metadata: None,
feature_metadata: None,
attempt_count: 1,
};
let req_cs = Some("1".to_string());
@@ -1948,7 +1950,9 @@ mod tests {
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
order_details: None,
udf: None,
allowed_payment_method_types: None,
connector_metadata: None,
feature_metadata: None,
attempt_count: 1,
};
let req_cs = Some("1".to_string());
@@ -1990,7 +1994,9 @@ mod tests {
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
order_details: None,
udf: None,
allowed_payment_method_types: None,
connector_metadata: None,
feature_metadata: None,
attempt_count: 1,
};
let req_cs = Some("1".to_string());
@@ -2434,108 +2440,6 @@ pub fn is_manual_retry_allowed(
}
}
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(())
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]

View File

@@ -159,14 +159,34 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
let redirect_response = request
.metadata
.feature_metadata
.as_ref()
.and_then(|secret_metadata| secret_metadata.redirect_response.to_owned());
.and_then(|fm| fm.redirect_response.clone());
payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id);
payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id);
payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string());
payment_intent.allowed_payment_method_types = request
.get_allowed_payment_method_types_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting allowed_payment_types to Value")?
.or(payment_intent.allowed_payment_method_types);
payment_intent.connector_metadata = request
.get_connector_metadata_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting connector_metadata to Value")?
.or(payment_intent.connector_metadata);
payment_intent.feature_metadata = request
.get_feature_metadata_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting feature_metadata to Value")?
.or(payment_intent.feature_metadata);
payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata);
// The operation merges mandate data from both request and payment_attempt
let setup_mandate = setup_mandate.map(|mandate_data| api_models::payments::MandateData {
customer_acceptance: mandate_data.customer_acceptance,

View File

@@ -72,11 +72,6 @@ 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(),
@@ -87,6 +82,12 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
payment_intent.order_details = request
.get_order_details_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to convert order details to value")?
.or(payment_intent.order_details);
let attempt_type =
helpers::get_attempt_type(&payment_intent, &payment_attempt, request, "confirm")?;
@@ -202,8 +203,25 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
.as_ref()
.map(|a| a.to_string())
.or(payment_intent.return_url);
payment_intent.udf = request.udf.clone().or(payment_intent.udf);
payment_intent.allowed_payment_method_types = request
.get_allowed_payment_method_types_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting allowed_payment_types to Value")?
.or(payment_intent.allowed_payment_method_types);
payment_intent.connector_metadata = request
.get_connector_metadata_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting connector_metadata to Value")?
.or(payment_intent.connector_metadata);
payment_intent.feature_metadata = request
.get_feature_metadata_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting feature_metadata to Value")?
.or(payment_intent.feature_metadata);
payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata);
payment_attempt.business_sub_label = request
.business_sub_label
.clone()
@@ -429,7 +447,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
.take();
let order_details = payment_data.payment_intent.order_details.clone();
let metadata = payment_data.payment_intent.metadata.clone();
let udf = payment_data.payment_intent.udf.clone();
payment_data.payment_intent = db
.update_payment_intent(
payment_data.payment_intent,
@@ -449,7 +467,6 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
statement_descriptor_suffix,
order_details,
metadata,
udf,
},
storage_scheme,
)
@@ -482,19 +499,6 @@ 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() })?
}
helpers::validate_customer_details_in_request(request)?;
let given_payment_id = match &request.payment_id {

View File

@@ -1,6 +1,5 @@
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};
@@ -414,19 +413,6 @@ 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() })?
}
helpers::validate_customer_details_in_request(request)?;
let given_payment_id = match &request.payment_id {
@@ -570,45 +556,11 @@ impl PaymentCreate {
let client_secret =
crate::utils::generate_id(consts::ID_LENGTH, format!("{payment_id}_secret").as_str());
let (amount, currency) = (money.0, Some(money.1));
let metadata = request
.metadata
.as_ref()
.map(|metadata| {
let transformed_metadata = api_models::payments::Metadata {
allowed_payment_method_types: request.allowed_payment_method_types.clone(),
..metadata.clone()
};
Encode::<api_models::payments::Metadata>::encode_to_value(&transformed_metadata)
})
.transpose()
let order_details = request
.get_order_details_as_value()
.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,
};
.attach_printable("Failed to convert order details to value")?;
let (business_country, business_label) = helpers::get_business_details(
request.business_country,
@@ -616,6 +568,21 @@ impl PaymentCreate {
merchant_account,
)?;
let allowed_payment_method_types = request
.get_allowed_payment_method_types_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting allowed_payment_types to Value")?;
let connector_metadata = request
.get_connector_metadata_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting connector_metadata to Value")?;
let feature_metadata = request
.get_feature_metadata_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting feature_metadata to Value")?;
Ok(storage::PaymentIntentNew {
payment_id: payment_id.to_string(),
merchant_id: merchant_account.merchant_id.to_string(),
@@ -634,14 +601,18 @@ impl PaymentCreate {
billing_address_id,
statement_descriptor_name: request.statement_descriptor_name.clone(),
statement_descriptor_suffix: request.statement_descriptor_suffix.clone(),
metadata: metadata.map(masking::Secret::new),
metadata: request.metadata.clone(),
business_country,
business_label,
active_attempt_id,
order_details: order_details_outside_value,
udf: request.udf.clone(),
order_details,
amount_captured: None,
customer_id: None,
connector_id: None,
allowed_payment_method_types,
connector_metadata,
feature_metadata,
attempt_count: 1,
..storage::PaymentIntentNew::default()
})
}

View File

@@ -90,10 +90,12 @@ 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_intent.order_details = request
.get_order_details_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to convert order details to value")?
.or(payment_intent.order_details);
payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
payment_intent.payment_id.as_str(),
@@ -158,6 +160,24 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
payment_intent.shipping_address_id = shipping_address.clone().map(|x| x.address_id);
payment_intent.billing_address_id = billing_address.clone().map(|x| x.address_id);
payment_intent.allowed_payment_method_types = request
.get_allowed_payment_method_types_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting allowed_payment_types to Value")?
.or(payment_intent.allowed_payment_method_types);
payment_intent.connector_metadata = request
.get_connector_metadata_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting connector_metadata to Value")?
.or(payment_intent.connector_metadata);
payment_intent.feature_metadata = request
.get_feature_metadata_as_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error converting feature_metadata to Value")?
.or(payment_intent.feature_metadata);
payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata);
Self::populate_payment_intent_with_request(&mut payment_intent, request);
let token = token.or_else(|| payment_attempt.payment_token.clone());
@@ -478,7 +498,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
.clone();
let order_details = payment_data.payment_intent.order_details.clone();
let metadata = payment_data.payment_intent.metadata.clone();
let udf = payment_data.payment_intent.udf.clone();
payment_data.payment_intent = db
.update_payment_intent(
payment_data.payment_intent,
@@ -498,7 +518,6 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
statement_descriptor_suffix,
order_details,
metadata,
udf,
},
storage_scheme,
)
@@ -524,19 +543,6 @@ 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() })?
}
helpers::validate_customer_details_in_request(request)?;
let given_payment_id = match &request.payment_id {
@@ -646,10 +652,5 @@ impl PaymentUpdate {
.client_secret
.clone()
.map(|i| payment_intent.client_secret.replace(i));
request
.udf
.clone()
.map(|udf| payment_intent.udf.replace(udf));
}
}

View File

@@ -3,7 +3,6 @@ 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;
@@ -361,19 +360,7 @@ where
connector_name,
)
});
let parsed_metadata: Option<api_models::payments::Metadata> = payment_intent
.metadata
.clone()
.map(|metadata_value| {
metadata_value
.parse_value("metadata")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "metadata",
})
.attach_printable("unable to parse metadata")
})
.transpose()
.unwrap_or_default();
let amount_captured = payment_intent.amount_captured.unwrap_or_default();
let amount_capturable = Some(payment_attempt.amount - amount_captured);
services::ApplicationResponse::Json(
@@ -460,16 +447,16 @@ where
.set_business_label(payment_intent.business_label)
.set_business_sub_label(payment_attempt.business_sub_label)
.set_allowed_payment_method_types(
parsed_metadata
.and_then(|metadata| metadata.allowed_payment_method_types),
payment_intent.allowed_payment_method_types,
)
.set_ephemeral_key(ephemeral_key_option.map(ForeignFrom::foreign_from))
.set_manual_retry_allowed(helpers::is_manual_retry_allowed(
&payment_intent.status,
&payment_attempt.status,
))
.set_udf(payment_intent.udf)
.set_connector_transaction_id(payment_attempt.connector_transaction_id)
.set_feature_metadata(payment_intent.feature_metadata)
.set_connector_metadata(payment_intent.connector_metadata)
.to_owned(),
)
}
@@ -517,8 +504,10 @@ where
&payment_attempt.status,
),
order_details: payment_intent.order_details,
udf: payment_intent.udf,
connector_transaction_id: payment_attempt.connector_transaction_id,
feature_metadata: payment_intent.feature_metadata,
connector_metadata: payment_intent.connector_metadata,
allowed_payment_method_types: payment_intent.allowed_payment_method_types,
..Default::default()
}),
});
@@ -662,31 +651,43 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
field_name: "browser_info",
})?;
let parsed_metadata: Option<api_models::payments::Metadata> = payment_data
let order_category = additional_data
.payment_data
.payment_intent
.metadata
.as_ref()
.map(|metadata_value| {
metadata_value
.clone()
.parse_value("metadata")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "metadata",
})
.attach_printable("unable to parse metadata")
.connector_metadata
.map(|cm| {
cm.parse_value::<api_models::payments::ConnectorMetadata>("ConnectorMetadata")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed parsing ConnectorMetadata")
})
.transpose()
.unwrap_or_default();
let order_category = parsed_metadata
.as_ref()
.and_then(|data| data.order_category.clone());
let order_details =
fetch_order_details(additional_data.clone(), parsed_metadata, &payment_data)?;
.transpose()?
.and_then(|cm| cm.noon.and_then(|noon| noon.order_category));
let order_details = additional_data
.payment_data
.payment_intent
.order_details
.map(|order_details| {
order_details
.iter()
.map(|data| {
data.to_owned()
.parse_value("OrderDetailsWithAmount")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "OrderDetailsWithAmount",
})
.attach_printable("Unable to parse OrderDetailsWithAmount")
})
.collect::<Result<Vec<_>, _>>()
})
.transpose()?;
let complete_authorize_url = Some(helpers::create_complete_authorize_url(
router_base_url,
attempt,
connector_name,
));
let webhook_url = Some(helpers::create_webhook_url(
router_base_url,
&attempt.merchant_id,
@@ -839,24 +840,25 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionD
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
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",
})
.attach_printable("unable to parse metadata")
})
.transpose()
.unwrap_or_default();
let order_details =
fetch_order_details(additional_data.clone(), parsed_metadata, &payment_data)?;
let order_details = additional_data
.payment_data
.payment_intent
.order_details
.map(|order_details| {
order_details
.iter()
.map(|data| {
data.to_owned()
.parse_value("OrderDetailsWithAmount")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "OrderDetailsWithAmount",
})
.attach_printable("Unable to parse OrderDetailsWithAmount")
})
.collect::<Result<Vec<_>, _>>()
})
.transpose()?;
Ok(Self {
amount: payment_data.amount.into(),
@@ -957,38 +959,3 @@ 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

@@ -96,7 +96,9 @@ mod storage {
business_label: new.business_label.clone(),
active_attempt_id: new.active_attempt_id.to_owned(),
order_details: new.order_details.clone(),
udf: new.udf.clone(),
allowed_payment_method_types: new.allowed_payment_method_types.clone(),
connector_metadata: new.connector_metadata.clone(),
feature_metadata: new.feature_metadata.clone(),
attempt_count: new.attempt_count,
};
@@ -357,7 +359,9 @@ impl PaymentIntentInterface for MockDb {
business_label: new.business_label,
active_attempt_id: new.active_attempt_id.to_owned(),
order_details: new.order_details,
udf: new.udf,
allowed_payment_method_types: new.allowed_payment_method_types,
connector_metadata: new.connector_metadata,
feature_metadata: new.feature_metadata,
attempt_count: new.attempt_count,
};
payment_intents.push(payment_intent.clone());

View File

@@ -178,10 +178,15 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payments::BankRedirectData,
api_models::payments::BankRedirectBilling,
api_models::payments::BankRedirectBilling,
api_models::payments::ConnectorMetadata,
api_models::payments::FeatureMetadata,
api_models::payments::ApplepayConnectorMetadataRequest,
api_models::payments::SessionTokenInfo,
api_models::payments::AirwallexData,
api_models::payments::NoonData,
api_models::payments::OrderDetails,
api_models::payments::OrderDetailsWithAmount,
api_models::payments::NextActionType,
api_models::payments::Metadata,
api_models::payments::WalletData,
api_models::payments::NextActionData,
api_models::payments::PayLaterData,

View File

@@ -38,7 +38,9 @@ pub struct PaymentIntent {
pub business_label: String,
#[diesel(deserialize_as = super::OptionalDieselArray<pii::SecretSerdeValue>)]
pub order_details: Option<Vec<pii::SecretSerdeValue>>,
pub udf: Option<pii::SecretSerdeValue>,
pub allowed_payment_method_types: Option<serde_json::Value>,
pub connector_metadata: Option<serde_json::Value>,
pub feature_metadata: Option<serde_json::Value>,
pub attempt_count: i16,
}
@@ -84,7 +86,9 @@ pub struct PaymentIntentNew {
pub business_label: String,
#[diesel(deserialize_as = super::OptionalDieselArray<pii::SecretSerdeValue>)]
pub order_details: Option<Vec<pii::SecretSerdeValue>>,
pub udf: Option<pii::SecretSerdeValue>,
pub allowed_payment_method_types: Option<serde_json::Value>,
pub connector_metadata: Option<serde_json::Value>,
pub feature_metadata: Option<serde_json::Value>,
pub attempt_count: i16,
}
@@ -129,7 +133,6 @@ pub enum PaymentIntentUpdate {
statement_descriptor_suffix: Option<String>,
order_details: Option<Vec<pii::SecretSerdeValue>>,
metadata: Option<pii::SecretSerdeValue>,
udf: Option<pii::SecretSerdeValue>,
},
PaymentAttemptUpdate {
active_attempt_id: String,
@@ -166,7 +169,6 @@ pub struct PaymentIntentUpdateInternal {
pub statement_descriptor_suffix: Option<String>,
#[diesel(deserialize_as = super::OptionalDieselArray<pii::SecretSerdeValue>)]
pub order_details: Option<Vec<pii::SecretSerdeValue>>,
pub udf: Option<pii::SecretSerdeValue>,
pub attempt_count: Option<i16>,
}
@@ -220,7 +222,6 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
statement_descriptor_suffix,
order_details,
metadata,
udf,
} => Self {
amount: Some(amount),
currency: Some(currency),
@@ -239,7 +240,6 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
statement_descriptor_suffix,
order_details,
metadata,
udf,
..Default::default()
},
PaymentIntentUpdate::MetadataUpdate { metadata } => Self {

View File

@@ -477,7 +477,9 @@ diesel::table! {
#[max_length = 64]
business_label -> Varchar,
order_details -> Nullable<Array<Nullable<Jsonb>>>,
udf -> Nullable<Jsonb>,
allowed_payment_method_types -> Nullable<Json>,
connector_metadata -> Nullable<Json>,
feature_metadata -> Nullable<Json>,
attempt_count -> Int2,
}
}

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE payment_intent ADD COLUMN udf JSONB;

View File

@@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE payment_intent DROP COLUMN udf;

View File

@@ -0,0 +1,5 @@
-- This file should undo anything in `up.sql`
ALTER TABLE payment_intent
DROP COLUMN allowed_payment_method_types,
DROP COLUMN connector_metadata,
DROP COLUMN feature_metadata;

View File

@@ -0,0 +1,5 @@
-- Your SQL goes here
ALTER TABLE payment_intent
ADD COLUMN allowed_payment_method_types JSON,
ADD COLUMN connector_metadata JSON,
ADD COLUMN feature_metadata JSON;

View File

@@ -1729,6 +1729,16 @@
}
}
},
"AirwallexData": {
"type": "object",
"properties": {
"payload": {
"type": "string",
"description": "payload required by airwallex",
"nullable": true
}
}
},
"AliPayHkRedirection": {
"type": "object"
},
@@ -1852,6 +1862,19 @@
}
}
},
"ApplepayConnectorMetadataRequest": {
"type": "object",
"properties": {
"session_token_data": {
"allOf": [
{
"$ref": "#/components/schemas/SessionTokenInfo"
}
],
"nullable": true
}
}
},
"ApplepayPaymentMethod": {
"type": "object",
"required": [
@@ -2886,6 +2909,35 @@
"zen"
]
},
"ConnectorMetadata": {
"type": "object",
"properties": {
"apple_pay": {
"allOf": [
{
"$ref": "#/components/schemas/ApplepayConnectorMetadataRequest"
}
],
"nullable": true
},
"airwallex": {
"allOf": [
{
"$ref": "#/components/schemas/AirwallexData"
}
],
"nullable": true
},
"noon": {
"allOf": [
{
"$ref": "#/components/schemas/NoonData"
}
],
"nullable": true
}
}
},
"ConnectorType": {
"type": "string",
"enum": [
@@ -3877,6 +3929,19 @@
}
}
},
"FeatureMetadata": {
"type": "object",
"properties": {
"redirect_response": {
"allOf": [
{
"$ref": "#/components/schemas/RedirectResponse"
}
],
"nullable": true
}
}
},
"FieldType": {
"oneOf": [
{
@@ -5335,40 +5400,6 @@
}
}
},
"Metadata": {
"type": "object",
"properties": {
"order_details": {
"allOf": [
{
"$ref": "#/components/schemas/OrderDetails"
}
],
"nullable": true
},
"order_category": {
"type": "string",
"description": "Information about the order category that merchant wants to specify at connector level. (e.g. In Noon Payments it can take values like \"pay\", \"food\", or any other custom string set by the merchant in Noon's Dashboard)",
"nullable": true
},
"redirect_response": {
"allOf": [
{
"$ref": "#/components/schemas/RedirectResponse"
}
],
"nullable": true
},
"allowed_payment_method_types": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PaymentMethodType"
},
"description": "Allowed payment method types for a payment intent",
"nullable": true
}
}
},
"MobilePayRedirection": {
"type": "object"
},
@@ -5525,6 +5556,16 @@
}
}
},
"NoonData": {
"type": "object",
"properties": {
"order_category": {
"type": "string",
"description": "Information about the order category that merchant wants to specify at connector level. (e.g. In Noon Payments it can take values like \"pay\", \"food\", or any other custom string set by the merchant in Noon's Dashboard)",
"nullable": true
}
}
},
"OnlineMandate": {
"type": "object",
"required": [
@@ -6606,14 +6647,6 @@
"nullable": true,
"maxLength": 255
},
"metadata": {
"allOf": [
{
"$ref": "#/components/schemas/Metadata"
}
],
"nullable": true
},
"order_details": {
"type": "array",
"items": {
@@ -6708,9 +6741,25 @@
],
"nullable": true
},
"udf": {
"metadata": {
"type": "object",
"description": "Any user defined fields can be passed here.",
"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
},
"connector_metadata": {
"allOf": [
{
"$ref": "#/components/schemas/ConnectorMetadata"
}
],
"nullable": true
},
"feature_metadata": {
"allOf": [
{
"$ref": "#/components/schemas/FeatureMetadata"
}
],
"nullable": true
}
}
@@ -6932,14 +6981,6 @@
"nullable": true,
"maxLength": 255
},
"metadata": {
"allOf": [
{
"$ref": "#/components/schemas/Metadata"
}
],
"nullable": true
},
"order_details": {
"type": "array",
"items": {
@@ -7034,9 +7075,25 @@
],
"nullable": true
},
"udf": {
"metadata": {
"type": "object",
"description": "Any user defined fields can be passed here.",
"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
},
"connector_metadata": {
"allOf": [
{
"$ref": "#/components/schemas/ConnectorMetadata"
}
],
"nullable": true
},
"feature_metadata": {
"allOf": [
{
"$ref": "#/components/schemas/FeatureMetadata"
}
],
"nullable": true
}
}
@@ -7220,11 +7277,6 @@
],
"nullable": true
},
"metadata": {
"type": "object",
"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": {
@@ -7363,16 +7415,32 @@
"description": "If true the payment can be retried with same or different payment method which means the confirm call can be made again.",
"nullable": true
},
"udf": {
"type": "object",
"description": "Any user defined fields can be passed here.",
"nullable": true
},
"connector_transaction_id": {
"type": "string",
"description": "A unique identifier for a payment provided by the connector",
"example": "993672945374576J",
"nullable": true
},
"metadata": {
"type": "object",
"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
},
"connector_metadata": {
"allOf": [
{
"$ref": "#/components/schemas/ConnectorMetadata"
}
],
"nullable": true
},
"feature_metadata": {
"allOf": [
{
"$ref": "#/components/schemas/FeatureMetadata"
}
],
"nullable": true
}
}
},
@@ -8147,6 +8215,37 @@
"propertyName": "wallet_name"
}
},
"SessionTokenInfo": {
"type": "object",
"required": [
"certificate",
"certificate_keys",
"merchant_identifier",
"display_name",
"initiative",
"initiative_context"
],
"properties": {
"certificate": {
"type": "string"
},
"certificate_keys": {
"type": "string"
},
"merchant_identifier": {
"type": "string"
},
"display_name": {
"type": "string"
},
"initiative": {
"type": "string"
},
"initiative_context": {
"type": "string"
}
}
},
"ThirdPartySdkSessionResponse": {
"type": "object",
"required": [