Skip to content

Commit

Permalink
fix: validate refund amount with amount_captured instead of amount (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
hrithikesh026 authored Dec 13, 2023
1 parent d28c82c commit be13d15
Show file tree
Hide file tree
Showing 31 changed files with 538 additions and 24 deletions.
2 changes: 1 addition & 1 deletion crates/router/src/compatibility/stripe/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub enum StripeErrorCode {
expected_format: String,
},

#[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_06", message = "Refund amount exceeds the payment amount.")]
#[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_06", message = "The refund amount exceeds the amount captured.")]
RefundAmountExceedsPaymentAmount { param: String },

#[error(error_type = StripeErrorType::ApiError, code = "payment_intent_authentication_failure", message = "Payment failed while processing with connector. Retry payment.")]
Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/core/errors/api_error_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub enum ApiErrorResponse {
CustomerRedacted,
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_12", message = "Reached maximum refund attempts")]
MaximumRefundCount,
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_13", message = "Refund amount exceeds the payment amount")]
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_13", message = "The refund amount exceeds the amount captured")]
RefundAmountExceedsPaymentAmount,
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_14", message = "This Payment could not be {current_flow} because it has a {field_name} of {current_value}. The expected state is {states}")]
PaymentUnexpectedState {
Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/core/errors/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl ErrorSwitch<api_models::errors::types::ApiErrorResponse> for ApiErrorRespon
}
Self::MaximumRefundCount => AER::BadRequest(ApiError::new("IR", 12, "Reached maximum refund attempts", None)),
Self::RefundAmountExceedsPaymentAmount => {
AER::BadRequest(ApiError::new("IR", 13, "Refund amount exceeds the payment amount", None))
AER::BadRequest(ApiError::new("IR", 13, "The refund amount exceeds the amount captured", None))
}
Self::PaymentUnexpectedState {
current_flow,
Expand Down
6 changes: 5 additions & 1 deletion crates/router/src/core/refunds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,11 @@ pub async fn validate_and_create_refund(
),
})?;

validator::validate_refund_amount(payment_attempt.amount, &all_refunds, refund_amount)
let total_amount_captured = payment_intent
.amount_captured
.unwrap_or(payment_attempt.amount);

validator::validate_refund_amount(total_amount_captured, &all_refunds, refund_amount)
.change_context(errors::ApiErrorResponse::RefundAmountExceedsPaymentAmount)?;

validator::validate_maximum_refund_against_payment_attempt(
Expand Down
6 changes: 3 additions & 3 deletions crates/router/src/core/refunds/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub const DEFAULT_LIMIT: i64 = 10;
pub enum RefundValidationError {
#[error("The payment attempt was not successful")]
UnsuccessfulPaymentAttempt,
#[error("The refund amount exceeds the payment amount")]
#[error("The refund amount exceeds the amount captured")]
RefundAmountExceedsPaymentAmount,
#[error("The order has expired")]
OrderExpired,
Expand All @@ -40,7 +40,7 @@ pub fn validate_success_transaction(

#[instrument(skip_all)]
pub fn validate_refund_amount(
payment_attempt_amount: i64, // &storage::PaymentAttempt,
amount_captured: i64,
all_refunds: &[storage::Refund],
refund_amount: i64,
) -> CustomResult<(), RefundValidationError> {
Expand All @@ -58,7 +58,7 @@ pub fn validate_refund_amount(
.sum();

utils::when(
refund_amount > (payment_attempt_amount - total_refunded_amount),
refund_amount > (amount_captured - total_refunded_amount),
|| {
Err(report!(
RefundValidationError::RefundAmountExceedsPaymentAmount
Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/routes/dummy_connector/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub enum DummyConnectorErrors {
#[error(error_type = ErrorType::InvalidRequestError, code = "DC_02", message = "Missing required param: {field_name}")]
MissingRequiredField { field_name: &'static str },

#[error(error_type = ErrorType::InvalidRequestError, code = "DC_03", message = "Refund amount exceeds the payment amount")]
#[error(error_type = ErrorType::InvalidRequestError, code = "DC_03", message = "The refund amount exceeds the amount captured")]
RefundAmountExceedsPaymentAmount,

#[error(error_type = ErrorType::InvalidRequestError, code = "DC_04", message = "Card not supported. Please use test cards")]
Expand Down
2 changes: 1 addition & 1 deletion crates/router/tests/integration_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,6 @@ async fn exceed_refund() {
let message: serde_json::Value = user_client.create_refund(&server, &payment_id, 100).await;
assert_eq!(
message["error"]["message"],
"Refund amount exceeds the payment amount."
"The refund amount exceeds the amount captured."
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ if (jsonData?.error?.type) {
// Response body should have value "invalid_request" for "error type"
if (jsonData?.error?.message) {
pm.test(
"[POST]::/payments - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'",
"[POST]::/payments - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'",
function () {
pm.expect(jsonData.error.message).to.eql(
"Refund amount exceeds the payment amount",
"The refund amount exceeds the amount captured",
);
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ if (jsonData?.refund_id) {
// Response body should have value "succeeded" for "status"
if (jsonData?.error.message) {
pm.test(
"[POST]::/refunds - Content check if value for 'message' matches 'Refund amount exceeds the payment amount'",
"[POST]::/refunds - Content check if value for 'message' matches 'The refund amount exceeds the amount captured'",
function () {
pm.expect(jsonData.error.message).to.eql("Refund amount exceeds the payment amount");
pm.expect(jsonData.error.message).to.eql("The refund amount exceeds the amount captured");
},
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ if (jsonData?.error?.type) {
);
}

// Response body should have value "Refund amount exceeds the payment amount" for "message"
// Response body should have value "The refund amount exceeds the amount captured" for "message"
if (jsonData?.error?.message) {
pm.test(
"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'",
"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'",
function () {
pm.expect(jsonData.error.message).to.eql(
"Refund amount exceeds the payment amount",
"The refund amount exceeds the amount captured",
);
},
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"childrenOrder": [
"Payments - Create",
"Payments - Capture",
"Payments - Retrieve",
"Refunds - Create"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"eventOrder": ["event.test.js"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Validate status 2xx
pm.test("[POST]::/payments/:id/capture - Status code is 2xx", function () {
pm.response.to.be.success;
});

// Validate if response header has matching content-type
pm.test(
"[POST]::/payments/:id/capture - 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/:id/capture - 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 "succeeded" for "status"
if (jsonData?.status) {
pm.test(
"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'",
function () {
pm.expect(jsonData.status).to.eql("partially_captured");
},
);
}

// Response body should have value "6540" for "amount"
if (jsonData?.amount) {
pm.test(
"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'",
function () {
pm.expect(jsonData.amount).to.eql(6540);
},
);
}

// Response body should have value "6000" for "amount_received"
if (jsonData?.amount_received) {
pm.test(
"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'",
function () {
pm.expect(jsonData.amount_received).to.eql(6000);
},
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"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_to_capture": 6000,
"statement_descriptor_name": "Joseph",
"statement_descriptor_suffix": "JS"
}
},
"url": {
"raw": "{{baseUrl}}/payments/:id/capture",
"host": ["{{baseUrl}}"],
"path": ["payments", ":id", "capture"],
"variable": [
{
"key": "id",
"value": "{{payment_id}}",
"description": "(Required) unique payment id"
}
]
},
"description": "To capture the funds for an uncaptured payment"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"eventOrder": ["event.test.js"]
}
Original file line number Diff line number Diff line change
@@ -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_capture" for "status"
if (jsonData?.status) {
pm.test(
"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'",
function () {
pm.expect(jsonData.status).to.eql("requires_capture");
},
);
}
Loading

0 comments on commit be13d15

Please sign in to comment.