fix(deserialization): deserialize reward payment method data (#4011)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Narayan Bhat
2024-03-08 01:52:11 +05:30
committed by GitHub
parent 4902c40345
commit f6b44f3860
9 changed files with 468 additions and 4 deletions

View File

@ -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<PaymentMethodDataRequest>,
/// 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<Option<PaymentMethodDataRequest>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum __Inner {
RewardString(String),
OptionalPaymentMethod(Box<PaymentMethodDataRequest>),
}
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<S>(
payment_method_data_request: &Option<PaymentMethodDataRequest>,
serializer: S,
) -> Result<S::Ok, S::Error>
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<JCSVoucherData>),
}
/// Use custom serializer to provide backwards compatible response for `reward` payment_method_data
pub fn serialize_payment_method_data_response<S>(
payment_method_data_response: &Option<PaymentMethodDataResponseWithBilling>,
serializer: S,
) -> Result<S::Ok, S::Error>
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<PaymentMethod>, example = "bank_transfer")]
#[auth_based]
#[serde(serialize_with = "serialize_payment_method_data_response")]
pub payment_method_data: Option<PaymentMethodDataResponseWithBilling>,
/// Provide a reference to a stored payment method

View File

@ -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"
]
}

View File

@ -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();
});

View File

@ -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"
}

View File

@ -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");
},
);
}

View File

@ -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"
}

View File

@ -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"
}
}
]
}
]
},