From f6b44f3860147a2ddc7b37123bfe064e50b7182a Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Fri, 8 Mar 2024 01:52:11 +0530 Subject: [PATCH] fix(deserialization): deserialize reward payment method data (#4011) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 129 ++++++++++++- .../Flow Testcases/Happy Cases/.meta.json | 3 +- .../.event.meta.json | 6 + .../event.test.js | 9 + .../request.json | 41 ++++ .../.event.meta.json | 6 + .../event.test.js | 71 +++++++ .../request.json | 31 +++ .../stripe.postman_collection.json | 176 ++++++++++++++++++ 9 files changed, 468 insertions(+), 4 deletions(-) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/request.json diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index b58ab47b31..e5882fcb49 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -10,7 +10,8 @@ use masking::Secret; use router_derive::Setter; use serde::{ de::{self, Unexpected, Visitor}, - Deserialize, Deserializer, Serialize, Serializer, + ser::Serializer, + Deserialize, Deserializer, Serialize, }; use time::PrimitiveDateTime; use url::Url; @@ -309,6 +310,7 @@ pub struct PaymentsRequest { /// The payment method information provided for making a payment #[schema(example = "bank_transfer")] + #[serde(with = "payment_method_data_serde", default)] pub payment_method_data: Option, /// The payment method that is to be used @@ -1056,6 +1058,93 @@ pub enum BankDebitData { }, } +/// Custom serializer and deserializer for PaymentMethodData +mod payment_method_data_serde { + + use super::*; + + /// Deserialize `reward` payment_method as string for backwards compatibility + /// The api contract would be + /// ```json + /// { + /// "payment_method": "reward", + /// "payment_method_type": "evoucher", + /// "payment_method_data": "reward", + /// } + /// ``` + /// + /// For other payment methods, use the provided deserializer + /// ```json + /// "payment_method_data": { + /// "card": { + /// "card_number": "4242424242424242", + /// "card_exp_month": "10", + /// "card_exp_year": "25", + /// "card_holder_name": "joseph Doe", + /// "card_cvc": "123" + /// } + /// } + /// ``` + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result, D::Error> + where + D: Deserializer<'de>, + { + #[derive(serde::Deserialize, Debug)] + #[serde(untagged)] + enum __Inner { + RewardString(String), + OptionalPaymentMethod(Box), + } + + let deserialize_to_inner = __Inner::deserialize(deserializer)?; + match deserialize_to_inner { + __Inner::OptionalPaymentMethod(value) => Ok(Some(*value)), + __Inner::RewardString(inner_string) => { + let payment_method_data = match inner_string.as_str() { + "reward" => PaymentMethodData::Reward, + _ => Err(serde::de::Error::custom("Invalid Variant"))?, + }; + + Ok(Some(PaymentMethodDataRequest { + payment_method_data, + billing: None, + })) + } + } + } + + pub fn serialize( + payment_method_data_request: &Option, + serializer: S, + ) -> Result + where + S: Serializer, + { + if let Some(payment_method_data_request) = payment_method_data_request { + match payment_method_data_request.payment_method_data { + PaymentMethodData::Reward => serializer.serialize_str("reward"), + PaymentMethodData::CardRedirect(_) + | PaymentMethodData::BankDebit(_) + | PaymentMethodData::BankRedirect(_) + | PaymentMethodData::BankTransfer(_) + | PaymentMethodData::CardToken(_) + | PaymentMethodData::Crypto(_) + | PaymentMethodData::GiftCard(_) + | PaymentMethodData::PayLater(_) + | PaymentMethodData::Upi(_) + | PaymentMethodData::Voucher(_) + | PaymentMethodData::Card(_) + | PaymentMethodData::MandatePayment + | PaymentMethodData::Wallet(_) => payment_method_data_request.serialize(serializer), + } + } else { + serializer.serialize_none() + } + } +} + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema, Eq, PartialEq)] pub struct PaymentMethodDataRequest { #[serde(flatten)] @@ -1121,7 +1210,7 @@ impl PaymentMethodData { | Self::BankTransfer(_) | Self::Crypto(_) | Self::MandatePayment - | Self::Reward + | Self::Reward {} | Self::Upi(_) | Self::Voucher(_) | Self::GiftCard(_) @@ -1939,6 +2028,39 @@ pub enum VoucherData { PayEasy(Box), } +/// Use custom serializer to provide backwards compatible response for `reward` payment_method_data +pub fn serialize_payment_method_data_response( + payment_method_data_response: &Option, + serializer: S, +) -> Result +where + S: Serializer, +{ + if let Some(payment_method_data_response) = payment_method_data_response { + match payment_method_data_response.payment_method_data { + PaymentMethodDataResponse::Reward {} => serializer.serialize_str("reward"), + PaymentMethodDataResponse::BankDebit {} + | PaymentMethodDataResponse::BankRedirect {} + | PaymentMethodDataResponse::Card(_) + | PaymentMethodDataResponse::CardRedirect {} + | PaymentMethodDataResponse::CardToken {} + | PaymentMethodDataResponse::Crypto {} + | PaymentMethodDataResponse::MandatePayment {} + | PaymentMethodDataResponse::GiftCard {} + | PaymentMethodDataResponse::PayLater {} + | PaymentMethodDataResponse::Paypal {} + | PaymentMethodDataResponse::Upi {} + | PaymentMethodDataResponse::Wallet(_) + | PaymentMethodDataResponse::BankTransfer {} + | PaymentMethodDataResponse::Voucher {} => { + payment_method_data_response.serialize(serializer) + } + } + } else { + serializer.serialize_none() + } +} + #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum PaymentMethodDataResponse { @@ -1960,7 +2082,7 @@ pub enum PaymentMethodDataResponse { CardToken {}, } -#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] +#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, ToSchema, serde::Serialize)] pub struct PaymentMethodDataResponseWithBilling { // The struct is flattened in order to provide backwards compatibility #[serde(flatten)] @@ -2426,6 +2548,7 @@ pub struct PaymentsResponse { /// The payment method information provided for making a payment #[schema(value_type = Option, example = "bank_transfer")] #[auth_based] + #[serde(serialize_with = "serialize_payment_method_data_response")] pub payment_method_data: Option, /// Provide a reference to a stored payment method diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json index 016af982e9..3a414a0e13 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json @@ -34,6 +34,7 @@ "Scenario28-Confirm a payment with requires_customer_action status", "Scenario29-Create payment with payment method billing", "Scenario30-Update payment with payment method billing", - "Scenario31-Pass payment method billing in Confirm" + "Scenario31-Pass payment method billing in Confirm", + "Scenario32-Ensure API Contract for Payment Method Data" ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/.event.meta.json new file mode 100644 index 0000000000..4ac527d834 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/.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/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/event.test.js new file mode 100644 index 0000000000..d462b381cc --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/event.test.js @@ -0,0 +1,9 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); \ No newline at end of file diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/request.json new file mode 100644 index 0000000000..05bc9dbc32 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Card Payment Method/request.json @@ -0,0 +1,41 @@ +{ + "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", + "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/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/.event.meta.json new file mode 100644 index 0000000000..4ac527d834 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/.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/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/event.test.js new file mode 100644 index 0000000000..0444324000 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", 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.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/request.json new file mode 100644 index 0000000000..e5797a68e1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario32-Ensure API Contract for Payment Method Data/Payments - Reward Payment Method/request.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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 605d94dd00..497d1c3669 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -21795,6 +21795,182 @@ "response": [] } ] + }, + { + "name": "Scenario32-Ensure API Contract for Payment Method Data", + "item": [ + { + "name": "Payments - Card Payment Method", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});" + ], + "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\",\"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 - Reward Payment Method", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", 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.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "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\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "Create a Payment to ensure api contract is intact" + } + } + ] } ] },