feat(storage): make amount as an enum (#98)

This commit is contained in:
Nishant Joshi
2022-12-10 15:05:17 +05:30
committed by GitHub
parent 091d5af42a
commit 031c073e79
18 changed files with 190 additions and 95 deletions

View File

@ -136,7 +136,7 @@ pub(crate) struct StripePaymentIntentRequest {
impl From<StripePaymentIntentRequest> for PaymentsRequest {
fn from(item: StripePaymentIntentRequest) -> Self {
PaymentsRequest {
amount: item.amount,
amount: item.amount.map(|amount| amount.into()),
currency: item.currency.as_ref().map(|c| c.to_uppercase()),
capture_method: item.capture_method,
amount_to_capture: item.amount_capturable,

View File

@ -431,7 +431,7 @@ where
pub payment_intent: storage::PaymentIntent,
pub payment_attempt: storage::PaymentAttempt,
pub connector_response: storage::ConnectorResponse,
pub amount: i32,
pub amount: api::Amount,
pub currency: enums::Currency,
pub mandate_id: Option<String>,
pub setup_mandate: Option<api::MandateData>,

View File

@ -210,28 +210,38 @@ pub fn validate_merchant_id(
#[instrument(skip_all)]
pub fn validate_request_amount_and_amount_to_capture(
op_amount: Option<i32>,
op_amount: Option<api::Amount>,
op_amount_to_capture: Option<i32>,
) -> CustomResult<(), errors::ApiErrorResponse> {
match (op_amount, op_amount_to_capture) {
(None, _) => Ok(()),
(Some(_amount), None) => Ok(()),
(Some(amount), Some(amount_to_capture)) => {
match amount {
api::Amount::Value(amount_inner) => {
// If both amount and amount to capture is present
// then amount to be capture should be less than or equal to request amount
let is_capture_amount_valid = op_amount
.and_then(|amount| {
op_amount_to_capture.map(|amount_to_capture| amount_to_capture.le(&amount))
})
.unwrap_or(true);
utils::when(
!is_capture_amount_valid,
!amount_to_capture.le(&amount_inner),
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
message: format!(
"amount_to_capture is greater than amount capture_amount: {:?} request_amount: {:?}",
op_amount_to_capture, op_amount
amount_to_capture, amount
)
})),
)
}
api::Amount::Zero => {
// If the amount is Null but still amount_to_capture is passed this is invalid and
Err(report!(errors::ApiErrorResponse::PreconditionFailed {
message: "amount_to_capture should not exist for when amount = 0"
.to_string()
}))
}
}
}
}
}
pub fn validate_mandate(
req: impl Into<api::MandateValidationFields>,

View File

@ -77,7 +77,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsCancelRequest>
error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
})?;
let currency = payment_attempt.currency.get_required_value("currency")?;
let amount = payment_attempt.amount;
let amount = payment_attempt.amount.into();
payment_attempt.cancellation_reason = request.cancellation_reason.clone();

View File

@ -85,7 +85,7 @@ impl<F: Send + Clone> GetTracker<F, payments::PaymentData<F>, api::PaymentsCaptu
currency = payment_attempt.currency.get_required_value("currency")?;
amount = payment_attempt.amount;
amount = payment_attempt.amount.into();
let connector_response = db
.find_connector_response_by_payment_id_merchant_id_txn_id(

View File

@ -93,7 +93,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
payment_attempt.payment_method = payment_method_type.or(payment_attempt.payment_method);
payment_attempt.browser_info = browser_info;
currency = payment_attempt.currency.get_required_value("currency")?;
amount = payment_attempt.amount;
amount = payment_attempt.amount.into();
connector_response = db
.find_connector_response_by_payment_id_merchant_id_txn_id(

View File

@ -284,10 +284,8 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentCreate
helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id)
.change_context(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let amount = request.amount.get_required_value("amount")?;
helpers::validate_request_amount_and_amount_to_capture(
Some(amount),
request.amount,
request.amount_to_capture,
)
.change_context(errors::ApiErrorResponse::InvalidDataFormat {
@ -295,9 +293,10 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentCreate
expected_format: "amount_to_capture lesser than amount".to_string(),
})?;
let mandate_type = helpers::validate_mandate(request)?;
let payment_id = core_utils::get_or_generate_id("payment_id", &given_payment_id, "pay")?;
let mandate_type = helpers::validate_mandate(request)?;
Ok((
Box::new(self),
operations::ValidateResult {
@ -316,7 +315,7 @@ impl PaymentCreate {
payment_id: &str,
merchant_id: &str,
connector: types::Connector,
money: (i32, enums::Currency),
money: (api::Amount, enums::Currency),
payment_method: Option<enums::PaymentMethodType>,
request: &api::PaymentsRequest,
browser_info: Option<serde_json::Value>,
@ -330,7 +329,7 @@ impl PaymentCreate {
merchant_id: merchant_id.to_string(),
txn_id: Uuid::new_v4().to_string(),
status,
amount,
amount: amount.into(),
currency,
connector: connector.to_string(),
payment_method,
@ -351,7 +350,7 @@ impl PaymentCreate {
payment_id: &str,
merchant_id: &str,
connector_id: &str,
money: (i32, enums::Currency),
money: (api::Amount, enums::Currency),
request: &api::PaymentsRequest,
shipping_address_id: Option<String>,
billing_address_id: Option<String>,
@ -366,7 +365,7 @@ impl PaymentCreate {
payment_id: payment_id.to_string(),
merchant_id: merchant_id.to_string(),
status,
amount,
amount: amount.into(),
currency,
connector_id: Some(connector_id.to_string()),
description: request.description.clone(),
@ -406,7 +405,7 @@ impl PaymentCreate {
#[instrument(skip_all)]
pub fn payments_create_request_validation(
req: &api::PaymentsRequest,
) -> RouterResult<(i32, enums::Currency)> {
) -> RouterResult<(api::Amount, enums::Currency)> {
let currency: enums::Currency = req
.currency
.as_ref()

View File

@ -135,7 +135,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::VerifyRequest> for Paym
payment_attempt,
/// currency and amount are irrelevant in this scenario
currency: storage_enums::Currency::default(),
amount: 0,
amount: api::Amount::Zero,
mandate_id: None,
setup_mandate: request.mandate_data.clone(),
token: request.payment_token.clone(),

View File

@ -72,7 +72,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>
payment_attempt.payment_method = Some(enums::PaymentMethodType::Wallet);
let amount = payment_intent.amount;
let amount = payment_intent.amount.into();
if let Some(ref payment_intent_client_secret) = payment_intent.client_secret {
if request.client_secret.ne(payment_intent_client_secret) {

View File

@ -68,7 +68,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsStartRequest> f
})?;
currency = payment_attempt.currency.get_required_value("currency")?;
amount = payment_attempt.amount;
amount = payment_attempt.amount.into();
let shipping_address = helpers::get_address_for_payment_request(
db,

View File

@ -167,7 +167,7 @@ async fn get_tracker_for_sync<
connector_response.encoded_data = request.param.clone();
currency = payment_attempt.currency.get_required_value("currency")?;
amount = payment_attempt.amount;
amount = payment_attempt.amount.into();
let shipping_address =
helpers::get_address_by_id(db, payment_intent.shipping_address_id.clone()).await?;

View File

@ -42,12 +42,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
PaymentData<F>,
Option<CustomerDetails>,
)> {
let (mut payment_intent, mut payment_attempt, currency, amount): (
_,
_,
enums::Currency,
_,
);
let (mut payment_intent, mut payment_attempt, currency): (_, _, enums::Currency);
let payment_id = payment_id
.get_payment_intent_id()
@ -80,7 +75,9 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
payment_attempt.payment_method = payment_method_type.or(payment_attempt.payment_method);
amount = request.amount.unwrap_or(payment_attempt.amount);
let amount = request
.amount
.unwrap_or_else(|| payment_attempt.amount.into());
payment_intent = db
.find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme)
@ -202,7 +199,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
.update_payment_attempt(
payment_data.payment_attempt,
storage::PaymentAttemptUpdate::Update {
amount: payment_data.amount,
amount: payment_data.amount.into(),
currency: payment_data.currency,
status: get_attempt_status(),
authentication_type: None,
@ -237,7 +234,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
.update_payment_intent(
payment_data.payment_intent.clone(),
storage::PaymentIntentUpdate::Update {
amount: payment_data.amount,
amount: payment_data.amount.into(),
currency: payment_data.currency,
status: get_status(),
customer_id,
@ -285,6 +282,7 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRequest> for PaymentUpdate
field_name: "merchant_id".to_string(),
expected_format: "merchant_id from merchant account".to_string(),
})?;
helpers::validate_request_amount_and_amount_to_capture(
request.amount,
request.amount_to_capture,

View File

@ -361,7 +361,7 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsAuthorizeData {
confirm: payment_data.payment_attempt.confirm,
statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix,
capture_method: payment_data.payment_attempt.capture_method,
amount: payment_data.amount,
amount: payment_data.amount.into(),
currency: payment_data.currency,
browser_info,
})

View File

@ -16,13 +16,13 @@ use crate::{
services::api,
types::api::{
enums as api_enums,
self as api_types, enums as api_enums,
payments::{
PaymentIdType, PaymentListConstraints, PaymentsCancelRequest, PaymentsCaptureRequest,
PaymentsRequest, PaymentsRetrieveRequest,
},
Authorize, Capture, PSync, PaymentRetrieveBody, PaymentsResponse, PaymentsStartRequest,
Verify, VerifyResponse, Void,
Verify, Void,
}, // FIXME imports
};
@ -38,32 +38,16 @@ pub async fn payments_create(
if let Some(api_enums::CaptureMethod::Scheduled) = payload.capture_method {
return http_not_implemented();
};
match payload.amount {
Some(0) | None => {
api::server_wrap(
&state,
&req,
payload.into(),
|state, merchant_account, req| {
payments::payments_core::<Verify, VerifyResponse, _, _, _>(
state,
merchant_account,
payments::PaymentMethodValidate,
req,
api::AuthFlow::Merchant,
payments::CallConnectorAction::Trigger,
)
},
api::MerchantAuthentication::ApiKey,
)
.await
}
_ => {
api::server_wrap(
&state,
&req,
payload,
|state, merchant_account, req| {
// TODO: Change for making it possible for the flow to be inferred internally or through validation layer
async {
match req.amount.as_ref() {
Some(api_types::Amount::Value(_)) | None => {
payments::payments_core::<Authorize, PaymentsResponse, _, _, _>(
state,
merchant_account,
@ -72,13 +56,26 @@ pub async fn payments_create(
api::AuthFlow::Merchant,
payments::CallConnectorAction::Trigger,
)
},
api::MerchantAuthentication::ApiKey,
.await
}
Some(api_types::Amount::Zero) => {
payments::payments_core::<Verify, PaymentsResponse, _, _, _>(
state,
merchant_account,
payments::PaymentCreate,
req,
api::AuthFlow::Merchant,
payments::CallConnectorAction::Trigger,
)
.await
}
}
}
},
api::MerchantAuthentication::ApiKey,
)
.await
}
#[instrument(skip(state), fields(flow = ?Flow::PaymentsStart))]
pub async fn payments_start(

View File

@ -27,7 +27,8 @@ pub struct PaymentsRequest {
)]
pub payment_id: Option<PaymentIdType>,
pub merchant_id: Option<String>,
pub amount: Option<i32>,
#[serde(default, deserialize_with = "custom_serde::amount::deserialize_option")]
pub amount: Option<Amount>,
pub currency: Option<String>,
pub capture_method: Option<api_enums::CaptureMethod>,
pub amount_to_capture: Option<i32>,
@ -68,6 +69,30 @@ impl PaymentsRequest {
}
}
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq, Eq)]
pub enum Amount {
Value(i32),
#[default]
Zero,
}
impl From<Amount> for i32 {
fn from(amount: Amount) -> Self {
match amount {
Amount::Value(v) => v,
Amount::Zero => 0,
}
}
}
impl From<i32> for Amount {
fn from(val: i32) -> Self {
match val {
0 => Amount::Zero,
amount => Amount::Value(amount),
}
}
}
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct PaymentsRedirectRequest {
@ -788,7 +813,7 @@ mod payments_test {
#[allow(dead_code)]
fn payments_request() -> PaymentsRequest {
PaymentsRequest {
amount: Some(200),
amount: Some(Amount::Value(200)),
payment_method_data: Some(PaymentMethod::Card(card())),
..PaymentsRequest::default()
}

View File

@ -72,3 +72,69 @@ pub(crate) mod payment_id_type {
deserializer.deserialize_option(OptionalPaymentIdVisitor)
}
}
pub(crate) mod amount {
use serde::de;
use crate::types::api;
struct AmountVisitor;
struct OptionalAmountVisitor;
// This is defined to provide guarded deserialization of amount
// which itself handles zero and non-zero values internally
impl<'de> de::Visitor<'de> for AmountVisitor {
type Value = api::Amount;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "amount as i32")
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(match v {
0 => api::Amount::Zero,
amount => api::Amount::Value(amount),
})
}
}
impl<'de> de::Visitor<'de> for OptionalAmountVisitor {
type Value = Option<api::Amount>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "option of amount (as i32)")
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(AmountVisitor).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
}
#[allow(dead_code)]
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<api::Amount, D::Error>
where
D: de::Deserializer<'de>,
{
deserializer.deserialize_any(AmountVisitor)
}
pub(crate) fn deserialize_option<'de, D>(
deserializer: D,
) -> Result<Option<api::Amount>, D::Error>
where
D: de::Deserializer<'de>,
{
deserializer.deserialize_option(OptionalAmountVisitor)
}
}

View File

@ -288,7 +288,7 @@ async fn payments_create_core() {
"pay_mbabizu24mvu3mela5njyhpit10".to_string(),
)),
merchant_id: Some("jarnura".to_string()),
amount: Some(6540),
amount: Some(6540.into()),
currency: Some("USD".to_string()),
capture_method: Some(api_enums::CaptureMethod::Automatic),
amount_to_capture: Some(6540),
@ -444,7 +444,7 @@ async fn payments_create_core_adyen_no_redirect() {
let req = api::PaymentsRequest {
payment_id: Some(api::PaymentIdType::PaymentIntentId(payment_id.clone())),
merchant_id: Some(merchant_id.clone()),
amount: Some(6540),
amount: Some(6540.into()),
currency: Some("USD".to_string()),
capture_method: Some(api_enums::CaptureMethod::Automatic),
amount_to_capture: Some(6540),

View File

@ -49,7 +49,7 @@ async fn payments_create_core() {
"pay_mbabizu24mvu3mela5njyhpit10".to_string(),
)),
merchant_id: Some("jarnura".to_string()),
amount: Some(6540),
amount: Some(6540.into()),
currency: Some("USD".to_string()),
capture_method: Some(api_enums::CaptureMethod::Automatic),
amount_to_capture: Some(6540),
@ -200,7 +200,7 @@ async fn payments_create_core_adyen_no_redirect() {
let req = api::PaymentsRequest {
payment_id: Some(api::PaymentIdType::PaymentIntentId(payment_id.clone())),
merchant_id: Some(merchant_id.clone()),
amount: Some(6540),
amount: Some(6540.into()),
currency: Some("USD".to_string()),
capture_method: Some(api_enums::CaptureMethod::Automatic),
amount_to_capture: Some(6540),