From 7afc44e8357b09c900a1e9aa384619f93f3bc81d Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:26:20 +0530 Subject: [PATCH] feat: store payment check codes and authentication data from processors (#3958) Co-authored-by: AkshayaFoiger Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 28 ++ .../src/payments/payment_attempt.rs | 2 + crates/diesel_models/src/payment_attempt.rs | 6 + crates/router/src/connector/stripe.rs | 5 +- .../src/connector/stripe/transformers.rs | 134 ++++++- .../src/core/authentication/transformers.rs | 1 + .../core/fraud_check/flows/checkout_flow.rs | 1 + .../fraud_check/flows/fulfillment_flow.rs | 1 + .../core/fraud_check/flows/record_return.rs | 1 + .../src/core/fraud_check/flows/sale_flow.rs | 1 + .../fraud_check/flows/transaction_flow.rs | 1 + crates/router/src/core/mandate/utils.rs | 1 + crates/router/src/core/payments.rs | 4 +- crates/router/src/core/payments/helpers.rs | 72 +++- .../payments/operations/payment_create.rs | 25 +- .../payments/operations/payment_response.rs | 24 +- crates/router/src/core/payments/retry.rs | 11 + .../router/src/core/payments/transformers.rs | 1 + crates/router/src/core/utils.rs | 7 + crates/router/src/core/webhooks/utils.rs | 1 + crates/router/src/types.rs | 30 ++ .../router/src/types/api/verify_connector.rs | 1 + crates/router/src/workflows/payment_sync.rs | 1 + crates/router/tests/connectors/aci.rs | 4 +- crates/router/tests/connectors/utils.rs | 1 + .../src/payments/payment_attempt.rs | 8 + .../.event.meta.json | 6 + .../Payments - cvv check fails/event.test.js | 26 ++ .../Payments - cvv check fails/request.json | 56 +++ .../.event.meta.json | 6 + .../event.test.js | 40 +++ .../request.json | 42 +++ .../.event.meta.json | 6 + .../event.test.js | 37 ++ .../request.json | 56 +++ .../.event.meta.json | 6 + .../event.test.js | 40 +++ .../request.json | 56 +++ .../stripe.postman_collection.json | 332 ++++++++++++++++++ 39 files changed, 1065 insertions(+), 16 deletions(-) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/request.json diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index b5a47a57a2..40d11543c0 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1240,6 +1240,7 @@ impl PaymentMethodData { self.to_owned() } } + pub fn get_payment_method(&self) -> Option { match self { Self::Card(_) => Some(api_enums::PaymentMethod::Card), @@ -1443,17 +1444,40 @@ pub struct GiftCardDetails { #[derive(Default, Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub struct AdditionalCardInfo { + /// The name of issuer of the card pub card_issuer: Option, + + /// Card network of the card pub card_network: Option, + + /// Card type, can be either `credit` or `debit` pub card_type: Option, + pub card_issuing_country: Option, pub bank_code: Option, + + /// Last 4 digits of the card number pub last4: Option, + + /// The ISIN of the card pub card_isin: Option, + + /// Extended bin of card, contains the first 8 digits of card number pub card_extended_bin: Option, + pub card_exp_month: Option>, + pub card_exp_year: Option>, + pub card_holder_name: Option>, + + /// Additional payment checks done on the cvv and billing address by the processors. + /// This is a free form field and the structure varies from processor to processor + pub payment_checks: Option, + + /// Details about the threeds environment. + /// This is a free form field and the structure varies from processor to processor + pub authentication_data: Option, } #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] @@ -2001,6 +2025,8 @@ pub struct CardResponse { pub card_exp_month: Option>, pub card_exp_year: Option>, pub card_holder_name: Option>, + pub payment_checks: Option, + pub authentication_data: Option, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -3003,6 +3029,8 @@ impl From for CardResponse { card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, card_holder_name: card.card_holder_name, + payment_checks: card.payment_checks, + authentication_data: card.authentication_data, } } } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index b54418e4ee..8984c1caee 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -361,6 +361,7 @@ pub enum PaymentAttemptUpdate { encoded_data: Option, unified_code: Option>, unified_message: Option>, + payment_method_data: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -388,6 +389,7 @@ pub enum PaymentAttemptUpdate { unified_code: Option>, unified_message: Option>, connector_transaction_id: Option, + payment_method_data: Option, }, CaptureUpdate { amount_to_capture: Option, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 9433e650fc..77fd21165d 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -268,6 +268,7 @@ pub enum PaymentAttemptUpdate { encoded_data: Option, unified_code: Option>, unified_message: Option>, + payment_method_data: Option, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -295,6 +296,7 @@ pub enum PaymentAttemptUpdate { unified_code: Option>, unified_message: Option>, connector_transaction_id: Option, + payment_method_data: Option, }, CaptureUpdate { amount_to_capture: Option, @@ -661,6 +663,7 @@ impl From for PaymentAttemptUpdateInternal { encoded_data, unified_code, unified_message, + payment_method_data, } => Self { status: Some(status), connector: connector.map(Some), @@ -681,6 +684,7 @@ impl From for PaymentAttemptUpdateInternal { encoded_data, unified_code, unified_message, + payment_method_data, ..Default::default() }, PaymentAttemptUpdate::ErrorUpdate { @@ -694,6 +698,7 @@ impl From for PaymentAttemptUpdateInternal { unified_code, unified_message, connector_transaction_id, + payment_method_data, } => Self { connector: connector.map(Some), status: Some(status), @@ -706,6 +711,7 @@ impl From for PaymentAttemptUpdateInternal { unified_code, unified_message, connector_transaction_id, + payment_method_data, ..Default::default() }, PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 958867465f..bf942ea1b3 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -673,10 +673,10 @@ impl match id.get_connector_transaction_id() { Ok(x) if x.starts_with("set") => Ok(format!( - "{}{}/{}", + "{}{}/{}?expand[0]=latest_attempt", // expand latest attempt to extract payment checks and three_d_secure data self.base_url(connectors), "v1/setup_intents", - x + x, )), Ok(x) => Ok(format!( "{}{}/{}{}", @@ -711,7 +711,6 @@ impl res: types::Response, ) -> CustomResult where - types::PaymentsAuthorizeData: Clone, types::PaymentsResponseData: Clone, { let id = data.request.connector_transaction_id.clone(); diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 4bbf7ee0d2..67b7603538 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -105,6 +105,14 @@ pub struct StripeMandateRequest { mandate_type: StripeMandateType, } +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ExpandableObjects { + LatestCharge, + Customer, + LatestAttempt, +} + #[derive(Debug, Eq, PartialEq, Serialize)] pub struct PaymentIntentRequest { pub amount: i64, //amount in cents, hence passed as integer @@ -133,6 +141,8 @@ pub struct PaymentIntentRequest { pub off_session: Option, #[serde(rename = "payment_method_types[0]")] pub payment_method_types: Option, + #[serde(rename = "expand[0]")] + pub expand: Option, } // Field rename is required only in case of serialization as it is passed in the request to the connector. @@ -162,6 +172,10 @@ pub struct SetupIntentRequest { pub payment_method_options: Option, // For mandate txns using network_txns_id, needs to be validated #[serde(flatten)] pub meta_data: Option>, + #[serde(rename = "payment_method_types[0]")] + pub payment_method_types: Option, + #[serde(rename = "expand[0]")] + pub expand: Option, } #[derive(Debug, Eq, PartialEq, Serialize)] @@ -1956,6 +1970,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest { off_session: item.request.off_session, setup_future_usage: item.request.setup_future_usage, payment_method_types, + expand: Some(ExpandableObjects::LatestCharge), }) } } @@ -2018,6 +2033,8 @@ impl TryFrom<&types::SetupMandateRouterData> for SetupIntentRequest { payment_method_options: None, customer: item.connector_customer.to_owned().map(Secret::new), meta_data, + payment_method_types: Some(pm_type), + expand: Some(ExpandableObjects::LatestAttempt), }) } } @@ -2110,6 +2127,7 @@ pub struct PaymentIntentResponse { pub payment_method_options: Option, pub last_payment_error: Option, pub latest_attempt: Option, //need a merchant to test this + pub latest_charge: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] @@ -2186,20 +2204,20 @@ pub struct PaymentIntentSyncResponse { pub latest_charge: Option, } -#[derive(Deserialize, Debug, Clone, Serialize)] +#[derive(Debug, Eq, PartialEq, Deserialize, Clone, Serialize)] #[serde(untagged)] pub enum StripeChargeEnum { ChargeId(String), ChargeObject(StripeCharge), } -#[derive(Deserialize, Clone, Debug, Serialize)] +#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] pub struct StripeCharge { pub id: String, pub payment_method_details: Option, } -#[derive(Deserialize, Clone, Debug, Serialize)] +#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] pub struct StripeBankRedirectDetails { #[serde(rename = "generated_sepa_debit")] attached_payment_method: Option>, @@ -2213,7 +2231,13 @@ impl Deref for PaymentIntentSyncResponse { } } -#[derive(Deserialize, Clone, Debug, Serialize)] +#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] +pub struct StripeAdditionalCardDetails { + checks: Option, + three_d_secure: Option, +} + +#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case", tag = "type")] pub enum StripePaymentMethodDetailsResponse { //only ideal, sofort and bancontact is supported by stripe for recurring payment in bank redirect @@ -2234,7 +2258,9 @@ pub enum StripePaymentMethodDetailsResponse { Giropay, #[serde(rename = "p24")] Przelewy24, - Card, + Card { + card: StripeAdditionalCardDetails, + }, Klarna, Affirm, AfterpayClearpay, @@ -2253,6 +2279,50 @@ pub enum StripePaymentMethodDetailsResponse { CustomerBalance, } +pub struct AdditionalPaymentMethodDetails { + pub payment_checks: Option, + pub authentication_details: Option, +} + +impl From for types::AdditionalPaymentMethodConnectorResponse { + fn from(item: AdditionalPaymentMethodDetails) -> Self { + Self::Card { + authentication_data: item.authentication_details, + payment_checks: item.payment_checks, + } + } +} + +impl StripePaymentMethodDetailsResponse { + pub fn get_additional_payment_method_data(&self) -> Option { + match self { + Self::Card { card } => Some(AdditionalPaymentMethodDetails { + payment_checks: card.checks.clone(), + authentication_details: card.three_d_secure.clone(), + }), + Self::Ideal { .. } + | Self::Sofort { .. } + | Self::Bancontact { .. } + | Self::Blik + | Self::Eps + | Self::Fpx + | Self::Giropay + | Self::Przelewy24 + | Self::Klarna + | Self::Affirm + | Self::AfterpayClearpay + | Self::ApplePay + | Self::Ach + | Self::Sepa + | Self::Becs + | Self::Bacs + | Self::Wechatpay + | Self::Alipay + | Self::CustomerBalance => None, + } + } +} + #[derive(Deserialize)] pub struct SetupIntentSyncResponse { #[serde(flatten)] @@ -2313,6 +2383,36 @@ pub struct SetupIntentResponse { pub last_setup_error: Option, } +fn extract_payment_method_connector_response_from_latest_charge( + stripe_charge_enum: &StripeChargeEnum, +) -> Option { + if let StripeChargeEnum::ChargeObject(charge_object) = stripe_charge_enum { + charge_object + .payment_method_details + .as_ref() + .and_then(StripePaymentMethodDetailsResponse::get_additional_payment_method_data) + } else { + None + } + .map(types::AdditionalPaymentMethodConnectorResponse::from) + .map(types::ConnectorResponseData::with_additional_payment_method_data) +} + +fn extract_payment_method_connector_response_from_latest_attempt( + stripe_latest_attempt: &LatestAttempt, +) -> Option { + if let LatestAttempt::PaymentIntentAttempt(intent_attempt) = stripe_latest_attempt { + intent_attempt + .payment_method_details + .as_ref() + .and_then(StripePaymentMethodDetailsResponse::get_additional_payment_method_data) + } else { + None + } + .map(types::AdditionalPaymentMethodConnectorResponse::from) + .map(types::ConnectorResponseData::with_additional_payment_method_data) +} + impl ForeignFrom<(Option, String)> for types::MandateReference { fn foreign_from( (payment_method_options, payment_method_id): (Option, String), @@ -2396,6 +2496,12 @@ impl }) }; + let connector_response_data = item + .response + .latest_charge + .as_ref() + .and_then(extract_payment_method_connector_response_from_latest_charge); + Ok(Self { status, // client_secret: Some(item.response.client_secret.clone().as_str()), @@ -2404,6 +2510,7 @@ impl // three_ds_form, response, amount_captured: item.response.amount_received, + connector_response: connector_response_data, ..item.data }) } @@ -2503,7 +2610,7 @@ impl | Some(StripePaymentMethodDetailsResponse::Fpx) | Some(StripePaymentMethodDetailsResponse::Giropay) | Some(StripePaymentMethodDetailsResponse::Przelewy24) - | Some(StripePaymentMethodDetailsResponse::Card) + | Some(StripePaymentMethodDetailsResponse::Card { .. }) | Some(StripePaymentMethodDetailsResponse::Klarna) | Some(StripePaymentMethodDetailsResponse::Affirm) | Some(StripePaymentMethodDetailsResponse::AfterpayClearpay) @@ -2528,6 +2635,12 @@ impl let status = enums::AttemptStatus::from(item.response.status.to_owned()); + let connector_response_data = item + .response + .latest_charge + .as_ref() + .and_then(extract_payment_method_connector_response_from_latest_charge); + let response = if connector_util::is_payment_failure(status) { types::PaymentsResponseData::try_from(( &item.response.payment_intent_fields.last_payment_error, @@ -2550,6 +2663,7 @@ impl status: enums::AttemptStatus::from(item.response.status.to_owned()), response, amount_captured: item.response.amount_received, + connector_response: connector_response_data, ..item.data }) } @@ -2574,6 +2688,12 @@ impl types::MandateReference::foreign_from((item.response.payment_method_options, pm)) }); let status = enums::AttemptStatus::from(item.response.status); + let connector_response_data = item + .response + .latest_attempt + .as_ref() + .and_then(extract_payment_method_connector_response_from_latest_attempt); + let response = if connector_util::is_payment_failure(status) { types::PaymentsResponseData::try_from(( &item.response.last_setup_error, @@ -2595,6 +2715,7 @@ impl Ok(Self { status, response, + connector_response: connector_response_data, ..item.data }) } @@ -3033,6 +3154,7 @@ pub enum LatestAttempt { #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct LatestPaymentAttempt { pub payment_method_options: Option, + pub payment_method_details: Option, } // #[derive(Deserialize, Debug, Clone, Eq, PartialEq)] // pub struct Card diff --git a/crates/router/src/core/authentication/transformers.rs b/crates/router/src/core/authentication/transformers.rs index f098c85326..6ade1b37e9 100644 --- a/crates/router/src/core/authentication/transformers.rs +++ b/crates/router/src/core/authentication/transformers.rs @@ -181,6 +181,7 @@ pub fn construct_router_data( dispute_id: None, refund_id: None, payment_method_status: None, + connector_response: None, }) } diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index 67684ab4b1..f17a173008 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -122,6 +122,7 @@ impl ConstructFlowSpecificData( frm_metadata: None, refund_id: None, dispute_id: None, + connector_response: None, }; Ok(router_data) } diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs index 1af1e5fc9f..cba19769d5 100644 --- a/crates/router/src/core/fraud_check/flows/record_return.rs +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -100,6 +100,7 @@ impl ConstructFlowSpecificData( frm_metadata: router_data.frm_metadata, refund_id: router_data.refund_id, dispute_id: router_data.dispute_id, + connector_response: router_data.connector_response, } } @@ -3450,6 +3452,9 @@ pub async fn get_additional_payment_data( last4: last4.clone(), card_isin: card_isin.clone(), card_extended_bin: card_extended_bin.clone(), + // These are filled after calling the processor / connector + payment_checks: None, + authentication_data: None, }, )) } else { @@ -3477,6 +3482,9 @@ pub async fn get_additional_payment_data( card_exp_month: Some(card_data.card_exp_month.clone()), card_exp_year: Some(card_data.card_exp_year.clone()), card_holder_name: card_data.card_holder_name.clone(), + // These are filled after calling the processor / connector + payment_checks: None, + authentication_data: None, }, )) }); @@ -3494,6 +3502,9 @@ pub async fn get_additional_payment_data( card_exp_month: Some(card_data.card_exp_month.clone()), card_exp_year: Some(card_data.card_exp_year.clone()), card_holder_name: card_data.card_holder_name.clone(), + // These are filled after calling the processor / connector + payment_checks: None, + authentication_data: None, }, )) }) @@ -3926,6 +3937,65 @@ pub fn validate_session_expiry(session_expiry: u32) -> Result<(), errors::ApiErr } } +pub fn add_connector_response_to_additional_payment_data( + additional_payment_data: api_models::payments::AdditionalPaymentData, + connector_response_payment_method_data: core_types::AdditionalPaymentMethodConnectorResponse, +) -> api_models::payments::AdditionalPaymentData { + match ( + &additional_payment_data, + connector_response_payment_method_data, + ) { + ( + api_models::payments::AdditionalPaymentData::Card(additional_card_data), + core_types::AdditionalPaymentMethodConnectorResponse::Card { + authentication_data, + payment_checks, + }, + ) => api_models::payments::AdditionalPaymentData::Card(Box::new( + api_models::payments::AdditionalCardInfo { + payment_checks, + authentication_data, + ..*additional_card_data.clone() + }, + )), + _ => additional_payment_data, + } +} + +pub fn update_additional_payment_data_with_connector_response_pm_data( + additional_payment_data: Option, + connector_response_pm_data: Option, +) -> RouterResult> { + let parsed_additional_payment_method_data = additional_payment_data + .as_ref() + .map(|payment_method_data| { + payment_method_data + .clone() + .parse_value::( + "additional_payment_method_data", + ) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to parse value into additional_payment_method_data")?; + + let additional_payment_method_data = parsed_additional_payment_method_data + .zip(connector_response_pm_data) + .map(|(additional_pm_data, connector_response_pm_data)| { + add_connector_response_to_additional_payment_data( + additional_pm_data, + connector_response_pm_data, + ) + }); + + additional_payment_method_data + .as_ref() + .map(Encode::encode_to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode additional pm data") +} + pub async fn get_payment_method_details_from_payment_token( state: &AppState, payment_attempt: &PaymentAttempt, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 7c85624265..58675bf62c 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -820,7 +820,30 @@ impl PaymentCreate { external_three_ds_authentication_attempted: None, mandate_data, payment_method_billing_address_id, - ..storage::PaymentAttemptNew::default() + net_amount: i64::default(), + save_to_locker: None, + connector: None, + error_message: None, + offer_amount: None, + payment_method_id: None, + cancellation_reason: None, + error_code: None, + connector_metadata: None, + straight_through_algorithm: None, + preprocessing_step_id: None, + error_reason: None, + connector_response_reference_id: None, + multiple_capture_count: None, + amount_capturable: i64::default(), + updated_by: String::default(), + authentication_data: None, + encoded_data: None, + merchant_connector_id: None, + unified_code: None, + unified_message: None, + fingerprint_id: None, + authentication_connector: None, + authentication_id: None, }, additional_pm_data, )) diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index b8456993eb..ae350d6de2 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -18,7 +18,14 @@ use crate::{ errors::{self, RouterResult, StorageErrorExt}, mandate, payment_methods::PaymentMethodRetrieve, - payments::{helpers as payments_helpers, types::MultipleCaptureData, PaymentData}, + payments::{ + helpers::{ + self as payments_helpers, + update_additional_payment_data_with_connector_response_pm_data, + }, + types::MultipleCaptureData, + PaymentData, + }, utils as core_utils, }, routes::{metrics, AppState}, @@ -441,6 +448,18 @@ async fn payment_response_update_tracker( router_data: types::RouterData, storage_scheme: enums::MerchantStorageScheme, ) -> RouterResult> { + // Update additional payment data with the payment method response that we received from connector + let additional_payment_method_data = + update_additional_payment_data_with_connector_response_pm_data( + payment_data.payment_attempt.payment_method_data.clone(), + router_data + .connector_response + .as_ref() + .and_then(|connector_response| { + connector_response.additional_payment_method_data.clone() + }), + )?; + router_data.payment_method_status.and_then(|status| { payment_data .payment_method_info @@ -477,6 +496,7 @@ async fn payment_response_update_tracker( flow_name.clone(), ) .await; + let status = match err.attempt_status { // Use the status sent by connector in error_response if it's present Some(status) => status, @@ -519,6 +539,7 @@ async fn payment_response_update_tracker( unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), connector_transaction_id: err.connector_transaction_id, + payment_method_data: additional_payment_method_data, }), ) } @@ -672,6 +693,7 @@ async fn payment_response_update_tracker( updated_by: storage_scheme.to_string(), authentication_data, encoded_data, + payment_method_data: additional_payment_method_data, }), ), }; diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 2af73f2990..2c0183922b 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -341,6 +341,14 @@ where ); let db = &*state.store; + let additional_payment_method_data = + payments::helpers::update_additional_payment_data_with_connector_response_pm_data( + payment_data.payment_attempt.payment_method_data.clone(), + router_data + .connector_response + .clone() + .and_then(|connector_response| connector_response.additional_payment_method_data), + )?; match router_data.response { Ok(types::PaymentsResponseData::TransactionResponse { @@ -393,6 +401,7 @@ where encoded_data, unified_code: None, unified_message: None, + payment_method_data: additional_payment_method_data, }, storage_scheme, ) @@ -405,6 +414,7 @@ where } Err(ref error_response) => { let option_gsm = get_gsm(state, &router_data).await?; + db.update_payment_attempt_with_attempt_id( payment_data.payment_attempt.clone(), storage::PaymentAttemptUpdate::ErrorUpdate { @@ -418,6 +428,7 @@ where unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), connector_transaction_id: error_response.connector_transaction_id.clone(), + payment_method_data: additional_payment_method_data, }, storage_scheme, ) diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 0597adedd2..bc440e1bc3 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -171,6 +171,7 @@ where frm_metadata: None, refund_id: None, dispute_id: None, + connector_response: None, }; Ok(router_data) diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 93e8b51a40..a64a846930 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -186,6 +186,7 @@ pub async fn construct_payout_router_data<'a, F>( frm_metadata: None, refund_id: None, dispute_id: None, + connector_response: None, }; Ok(router_data) @@ -341,6 +342,7 @@ pub async fn construct_refund_router_data<'a, F>( frm_metadata: None, refund_id: Some(refund.refund_id.clone()), dispute_id: None, + connector_response: None, }; Ok(router_data) @@ -574,6 +576,7 @@ pub async fn construct_accept_dispute_router_data<'a>( frm_metadata: None, dispute_id: Some(dispute.dispute_id.clone()), refund_id: None, + connector_response: None, }; Ok(router_data) } @@ -665,6 +668,7 @@ pub async fn construct_submit_evidence_router_data<'a>( frm_metadata: None, refund_id: None, dispute_id: Some(dispute.dispute_id.clone()), + connector_response: None, }; Ok(router_data) } @@ -762,6 +766,7 @@ pub async fn construct_upload_file_router_data<'a>( frm_metadata: None, refund_id: None, dispute_id: None, + connector_response: None, }; Ok(router_data) } @@ -856,6 +861,7 @@ pub async fn construct_defend_dispute_router_data<'a>( frm_metadata: None, refund_id: None, dispute_id: Some(dispute.dispute_id.clone()), + connector_response: None, }; Ok(router_data) } @@ -943,6 +949,7 @@ pub async fn construct_retrieve_file_router_data<'a>( frm_metadata: None, refund_id: None, dispute_id: None, + connector_response: None, }; Ok(router_data) } diff --git a/crates/router/src/core/webhooks/utils.rs b/crates/router/src/core/webhooks/utils.rs index 7bc34a25ac..8afaf8c8fe 100644 --- a/crates/router/src/core/webhooks/utils.rs +++ b/crates/router/src/core/webhooks/utils.rs @@ -117,6 +117,7 @@ pub async fn construct_webhook_router_data<'a>( frm_metadata: None, refund_id: None, dispute_id: None, + connector_response: None, }; Ok(router_data) } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index bcf907618f..67ad246ba7 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -321,9 +321,37 @@ pub struct RouterData { pub dispute_id: Option, pub refund_id: Option, + + /// This field is used to store various data regarding the response from connector + pub connector_response: Option, pub payment_method_status: Option, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum AdditionalPaymentMethodConnectorResponse { + Card { + /// Details regarding the authentication details of the connector, if this is a 3ds payment. + authentication_data: Option, + /// Various payment checks that are done for a payment + payment_checks: Option, + }, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ConnectorResponseData { + pub additional_payment_method_data: Option, +} + +impl ConnectorResponseData { + pub fn with_additional_payment_method_data( + additional_payment_method_data: AdditionalPaymentMethodConnectorResponse, + ) -> Self { + Self { + additional_payment_method_data: Some(additional_payment_method_data), + } + } +} + #[derive(Debug, Clone, serde::Deserialize)] pub enum PaymentMethodToken { Token(String), @@ -1479,6 +1507,7 @@ impl From<(&RouterData, T2)> frm_metadata: data.frm_metadata.clone(), dispute_id: data.dispute_id.clone(), refund_id: data.refund_id.clone(), + connector_response: data.connector_response.clone(), } } } @@ -1538,6 +1567,7 @@ impl frm_metadata: None, refund_id: None, dispute_id: None, + connector_response: data.connector_response.clone(), } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 62cdb7c7e1..d05a2b8a8e 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -105,6 +105,7 @@ impl VerifyConnectorData { frm_metadata: None, refund_id: None, dispute_id: None, + connector_response: None, } } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index dd364f23b7..051157f504 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -136,6 +136,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { unified_code: None, unified_message: None, connector_transaction_id: None, + payment_method_data: None, }; payment_data.payment_attempt = db diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index ca80db5437..a2c05a1a67 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -87,7 +87,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { payment_method_token: None, connector_customer: None, recurring_mandate_payment_data: None, - + connector_response: None, preprocessing_id: None, connector_request_reference_id: uuid::Uuid::new_v4().to_string(), #[cfg(feature = "payouts")] @@ -149,7 +149,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { payment_method_token: None, connector_customer: None, recurring_mandate_payment_data: None, - + connector_response: None, preprocessing_id: None, connector_request_reference_id: uuid::Uuid::new_v4().to_string(), #[cfg(feature = "payouts")] diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 839a3437ab..8effc2479c 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -528,6 +528,7 @@ pub trait ConnectorActions: Connector { frm_metadata: None, refund_id: None, dispute_id: None, + connector_response: None, } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index dfec8d244b..04e755fa43 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1488,6 +1488,7 @@ impl DataModelExt for PaymentAttemptUpdate { encoded_data, unified_code, unified_message, + payment_method_data, } => DieselPaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -1507,6 +1508,7 @@ impl DataModelExt for PaymentAttemptUpdate { encoded_data, unified_code, unified_message, + payment_method_data, }, Self::UnresolvedResponseUpdate { status, @@ -1543,6 +1545,7 @@ impl DataModelExt for PaymentAttemptUpdate { unified_code, unified_message, connector_transaction_id, + payment_method_data, } => DieselPaymentAttemptUpdate::ErrorUpdate { connector, status, @@ -1554,6 +1557,7 @@ impl DataModelExt for PaymentAttemptUpdate { unified_code, unified_message, connector_transaction_id, + payment_method_data, }, Self::CaptureUpdate { multiple_capture_count, @@ -1794,6 +1798,7 @@ impl DataModelExt for PaymentAttemptUpdate { encoded_data, unified_code, unified_message, + payment_method_data, } => Self::ResponseUpdate { status, connector, @@ -1813,6 +1818,7 @@ impl DataModelExt for PaymentAttemptUpdate { encoded_data, unified_code, unified_message, + payment_method_data, }, DieselPaymentAttemptUpdate::UnresolvedResponseUpdate { status, @@ -1849,6 +1855,7 @@ impl DataModelExt for PaymentAttemptUpdate { unified_code, unified_message, connector_transaction_id, + payment_method_data, } => Self::ErrorUpdate { connector, status, @@ -1860,6 +1867,7 @@ impl DataModelExt for PaymentAttemptUpdate { unified_code, unified_message, connector_transaction_id, + payment_method_data, }, DieselPaymentAttemptUpdate::CaptureUpdate { amount_to_capture, diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/.event.meta.json new file mode 100644 index 0000000000..4ac527d834 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/event.test.js new file mode 100644 index 0000000000..70d8493b73 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/event.test.js @@ -0,0 +1,26 @@ +// Validate status 400 +pm.test("[POST]::/payments - Status code is 200", function () { + pm.response.to.be.success +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +pm.test("[POST]::/payments - Response has cvc check as fail", function () { + pm.expect(jsonData.payment_method_data.card.payment_checks.cvc_check).to.eql("fail"); +}) diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/request.json new file mode 100644 index 0000000000..9f5c6a8fb3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - cvv check fails/request.json @@ -0,0 +1,56 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4000000000000101", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrisoff Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "Narayan", + "last_name": "Doe" + }, + "email": "example@juspay.in" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "Create a Payment to ensure api contract is intact" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/.event.meta.json new file mode 100644 index 0000000000..4ac527d834 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/event.test.js new file mode 100644 index 0000000000..c474b40e23 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/event.test.js @@ -0,0 +1,40 @@ +// Validate status 400 +pm.test("[POST]::/payments - Status code is 200", function () { + pm.response.to.be.success +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +pm.test("[POST]::/payments - Response has payment checks", function () { + pm.expect(jsonData.payment_method_data.card.payment_checks).to.eql({ + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }); +}) diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/request.json new file mode 100644 index 0000000000..88fccca9a0 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - payment check populated for successful payment without billing/request.json @@ -0,0 +1,42 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "Create a Payment to ensure api contract is intact" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/.event.meta.json new file mode 100644 index 0000000000..4ac527d834 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/event.test.js new file mode 100644 index 0000000000..1e4a4c9946 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/event.test.js @@ -0,0 +1,37 @@ +// Validate status 400 +pm.test("[POST]::/payments - Status code is 200", function () { + pm.response.to.be.success +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +pm.test("[POST]::/payments - Response has postal code check as fail", function () { + pm.expect(jsonData.payment_method_data.card.payment_checks.address_postal_code_check).to.eql("fail"); +}) + diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/request.json new file mode 100644 index 0000000000..bdc8b6bedc --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - postal code check fail/request.json @@ -0,0 +1,56 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4000000000000036", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrisoff Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "Narayan", + "last_name": "Doe" + }, + "email": "example@juspay.in" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "Create a Payment to ensure api contract is intact" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/.event.meta.json new file mode 100644 index 0000000000..4ac527d834 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/event.test.js new file mode 100644 index 0000000000..7cec52764b --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/event.test.js @@ -0,0 +1,40 @@ +// Validate status 400 +pm.test("[POST]::/payments - Status code is 200", function () { + pm.response.to.be.success +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +pm.test("[POST]::/payments - Response has payment checks", function () { + pm.expect(jsonData.payment_method_data.card.payment_checks).to.eql({ + "address_line1_check": "pass", + "address_postal_code_check": "pass", + "cvc_check": "pass" + }); +}) diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/request.json new file mode 100644 index 0000000000..28e4576672 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario33-Check is payment checks are populated/Payments - successful payment with billing/request.json @@ -0,0 +1,56 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrisoff Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "Narayan", + "last_name": "Doe" + }, + "email": "example@juspay.in" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "Create a Payment to ensure api contract is intact" +} diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 46829047bd..c7437e8b1f 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -6722,6 +6722,338 @@ } ] }, + { + "name": "Scenario33-Check is payment checks are populated", + "item": [ + { + "name": "Payments - cvv check fails", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 400", + "pm.test(\"[POST]::/payments - Status code is 200\", function () {", + " pm.response.to.be.success", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "pm.test(\"[POST]::/payments - Response has cvc check as fail\", function () {", + " pm.expect(jsonData.payment_method_data.card.payment_checks.cvc_check).to.eql(\"fail\");", + "})", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000000101\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrisoff Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Narayan\",\"last_name\":\"Doe\"},\"email\":\"example@juspay.in\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "Create a Payment to ensure api contract is intact" + } + }, + { + "name": "Payments - payment check populated for successful payment without billing", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 400", + "pm.test(\"[POST]::/payments - Status code is 200\", function () {", + " pm.response.to.be.success", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "pm.test(\"[POST]::/payments - Response has payment checks\", function () {", + " pm.expect(jsonData.payment_method_data.card.payment_checks).to.eql({", + " \"address_line1_check\": null,", + " \"address_postal_code_check\": null,", + " \"cvc_check\": \"pass\"", + " });", + "})", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "Create a Payment to ensure api contract is intact" + } + }, + { + "name": "Payments - postal code check fail", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 400", + "pm.test(\"[POST]::/payments - Status code is 200\", function () {", + " pm.response.to.be.success", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "pm.test(\"[POST]::/payments - Response has postal code check as fail\", function () {", + " pm.expect(jsonData.payment_method_data.card.payment_checks.address_postal_code_check).to.eql(\"fail\");", + "})", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000000036\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrisoff Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Narayan\",\"last_name\":\"Doe\"},\"email\":\"example@juspay.in\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "Create a Payment to ensure api contract is intact" + } + }, + { + "name": "Payments - successful payment with billing", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 400", + "pm.test(\"[POST]::/payments - Status code is 200\", function () {", + " pm.response.to.be.success", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "pm.test(\"[POST]::/payments - Response has payment checks\", function () {", + " pm.expect(jsonData.payment_method_data.card.payment_checks).to.eql({", + " \"address_line1_check\": \"pass\",", + " \"address_postal_code_check\": \"pass\",", + " \"cvc_check\": \"pass\"", + " });", + "})", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrisoff Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Narayan\",\"last_name\":\"Doe\"},\"email\":\"example@juspay.in\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "Create a Payment to ensure api contract is intact" + } + } + ] + }, { "name": "Scenario1-Create payment with confirm true", "item": [