Skip to content

feat(refunds_v2): Add refund create core flow#7619

Merged
likhinbopanna merged 22 commits intomainfrom
refunds-create-core-flow
Apr 21, 2025
Merged

feat(refunds_v2): Add refund create core flow#7619
likhinbopanna merged 22 commits intomainfrom
refunds-create-core-flow

Conversation

@AmeyWale
Copy link
Contributor

@AmeyWale AmeyWale commented Mar 25, 2025

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

This PR is a part of v2 implementation of refunds. This PR contains refunds create core flow, i.e we can be able to create refunds for payments in v2.

Important
We are moving refunds/transformers.rs and refunds/validator.rs to a separate location because of accessibility
to both v1 and v2 refund code.

Additional Changes

  • This PR modifies the API contract
  • This PR modifies the database schema
  • This PR modifies application configuration/environment variables

Motivation and Context

Merging this PR would add support for creating refunds for payments in v2 apis.

How did you test it?

  • Create Payment Intent api call
curl --location 'http://localhost:8080/v2/payments/create-intent' \
--header 'api-key: xyz' \
--header 'Content-Type: application/json' \
--header 'x-profile-id: pro_' \
--header 'Authorization: api-key=xyz' \
--data-raw '{
    "amount_details": {
        "order_amount": 100,
        "currency": "USD"
    },
    "capture_method":"automatic",
    "authentication_type": "no_three_ds",
    "billing": {
        "address": {
            "first_name": "John",
            "last_name": "Dough"
        },
        "email": "example@example.com"
    },
    "shipping": {
        "address": {
            "first_name": "John",
            "last_name": "Dough",
            "city": "Karwar",
            "zip": "581301",
            "state": "Karnataka"
        },
        "email": "example@example.com"
    }
}'
  • Response from the above call
{
  "id": "12345_pay_0195d13dd6497c12a3b187039fc4b71e",
  "status": "requires_payment_method",
  "amount_details": {
    "order_amount": 100,
    "currency": "USD",
    "shipping_cost": null,
    "order_tax_amount": null,
    "external_tax_calculation": "skip",
    "surcharge_calculation": "skip",
    "surcharge_amount": null,
    "tax_on_surcharge": null
  },
  "client_secret": "12345_pay_0195d13dd6497c12a3b187039fc4b71e_secret_0195d13dd6547a938bfe7b97b36203a5",
  "profile_id": "pro_TujgCYCsvWvO6YNsRAmC",
  "merchant_reference_id": null,
  "routing_algorithm_id": null,
  "capture_method": "automatic",
  "authentication_type": "no_three_ds",
  "billing": {
    "address": {
      "city": null,
      "country": null,
      "line1": null,
      "line2": null,
      "line3": null,
      "zip": null,
      "state": null,
      "first_name": "John",
      "last_name": "Dough"
    },
    "phone": null,
    "email": "example@example.com"
  },
  "shipping": {
    "address": {
      "city": "Karwar",
      "country": null,
      "line1": null,
      "line2": null,
      "line3": null,
      "zip": "581301",
      "state": "Karnataka",
      "first_name": "John",
      "last_name": "Dough"
    },
    "phone": null,
    "email": "example@example.com"
  },
  "customer_id": null,
  "customer_present": "present",
  "description": null,
  "return_url": null,
  "setup_future_usage": "on_session",
  "apply_mit_exemption": "Skip",
  "statement_descriptor": null,
  "order_details": null,
  "allowed_payment_method_types": null,
  "metadata": null,
  "connector_metadata": null,
  "feature_metadata": null,
  "payment_link_enabled": "Skip",
  "payment_link_config": null,
  "request_incremental_authorization": "default",
  "expires_on": "2025-03-26T07:12:25.588Z",
  "frm_metadata": null,
  "request_external_three_ds_authentication": "Skip"
}
  • Confirm Intent API call
curl --location 'http://localhost:8080/v2/payments/12345_pay_0195d13dd6497c12a3b187039fc4b71e/confirm-intent' \
--header 'x-client-secret: 12345_pay_0195d13dd6497c12a3b187039fc4b71e_secret_0195d13dd6547a938bfe7b97b36203a5' \
--header 'x-profile-id: pro_TujgCYCsvWvO6YNsRAmC' \
--header 'Content-Type: application/json' \
--header 'Authorization: publishable-key=pk_dev_58559cdf90434adcb0b92eeb6a8b633f,client-secret=12345_pay_0195d13dd6497c12a3b187039fc4b71e_secret_0195d13dd6547a938bfe7b97b36203a5' \
--data '{
    "payment_method_data": {
        "card": {
            "card_number": "4242424242424242",
            "card_exp_month": "09",
            "card_exp_year": "25",
            "card_holder_name": "John Doe",
            "card_cvc": "100"
        }
    },
    "payment_method_type": "card",
    "payment_method_subtype": "credit"
}'
  • Response from the above call
{
  "id": "12345_pay_0195d13dd6497c12a3b187039fc4b71e",
  "status": "succeeded",
  "amount": {
    "order_amount": 100,
    "currency": "USD",
    "shipping_cost": null,
    "order_tax_amount": null,
    "external_tax_calculation": "skip",
    "surcharge_calculation": "skip",
    "surcharge_amount": null,
    "tax_on_surcharge": null,
    "net_amount": 100,
    "amount_to_capture": null,
    "amount_capturable": 0,
    "amount_captured": 100
  },
  "customer_id": null,
  "connector": "stripe",
  "client_secret": "12345_pay_0195d13dd6497c12a3b187039fc4b71e_secret_0195d13dd6547a938bfe7b97b36203a5",
  "created": "2025-03-26T06:57:25.588Z",
  "payment_method_data": {
    "billing": null
  },
  "payment_method_type": "card",
  "payment_method_subtype": "credit",
  "connector_transaction_id": "pi_3R6ntzD5R7gDAGff0MYUxZFn",
  "connector_reference_id": null,
  "merchant_connector_id": "mca_FhEKUEAUdgv7mVCs3A9U",
  "browser_info": null,
  "error": null,
  "shipping": null,
  "billing": null,
  "attempts": null,
  "connector_token_details": {
    "token": "pm_1R6ntzD5R7gDAGff07DogD0c",
    "connector_token_request_reference_id": "oMf8ppmFul0oTqI6Zl"
  },
  "payment_method_id": null,
  "next_action": null,
  "return_url": "https://google.com/success",
  "authentication_type": "no_three_ds",
  "authentication_type_applied": "no_three_ds"
}
  • Create refund for a successful payment
curl --location 'http://localhost:8080/v2/refunds' \
--header 'X-Profile-Id: pro_TujgCYCsvWvO6YNsRAmC' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_kksGlKY14xNypCzsrJIXFCzL3t2j5kUs7ChmloPryJsdRy2RWYljonzPCxJSDmuZ' \
--data '{
    "payment_id":"12345_pay_0195d13dd6497c12a3b187039fc4b71e",
    "merchant_reference_id":"1742972385",
    "reason":"Paid by mistake",
    "metadata":{
        "foo":"bar"
    }
}'
  • Response from the above call
{
  "id": "12345_ref_0195d13dfedb7790bc1a84be825e64d5",
  "payment_id": "12345_pay_0195d13dd6497c12a3b187039fc4b71e",
  "merchant_reference_id": "1742972256",
  "amount": 100,
  "currency": "USD",
  "status": "succeeded",
  "reason": "Paid by mistake",
  "metadata": {
    "foo": "bar"
  },
  "error_details": {
    "code": "",
    "message": ""
  },
  "created_at": "2025-03-26T06:57:35.993Z",
  "updated_at": "2025-03-26T06:57:36.981Z",
  "connector": "stripe",
  "profile_id": "pro_TujgCYCsvWvO6YNsRAmC",
  "merchant_connector_id": "mca_FhEKUEAUdgv7mVCs3A9U",
  "connector_refund_reference_id": "12345_ref_0195d13dfedb7790bc1a84be825e64d5"
}
  • Trying the same refund again will give us this response
{"error":{"type":"invalid_request","message":"The refund amount exceeds the amount captured","code":"IR_13"}}
  • What if the payment was failed and we try to refund it. (Invalid card number in confirm intent call)
curl --location 'http://localhost:8080/v2/payments/12345_pay_0195d142100474c3bcb32e40935e290d/confirm-intent' \
--header 'x-client-secret: 12345_pay_0195d142100474c3bcb32e40935e290d_secret_0195d14210127d30b524ec0e063b3c0a' \
--header 'x-profile-id: pro_TujgCYCsvWvO6YNsRAmC' \
--header 'Content-Type: application/json' \
--header 'Authorization: publishable-key=pk_dev_58559cdf90434adcb0b92eeb6a8b633f,client-secret=12345_pay_0195d142100474c3bcb32e40935e290d_secret_0195d14210127d30b524ec0e063b3c0a' \
--data '{
    "payment_method_data": {
        "card": {
            "card_number": "4242424242424242",
            "card_exp_month": "01",
            "card_exp_year": "25",
            "card_holder_name": "John Doe",
            "card_cvc": "100"
        }
    },
    "payment_method_type": "card",
    "payment_method_subtype": "credit"
}'
  • Response from the above call
{
  "id": "12345_pay_0195d142100474c3bcb32e40935e290d",
  "status": "failed",
  "amount": {
    "order_amount": 100,
    "currency": "USD",
    "shipping_cost": null,
    "order_tax_amount": null,
    "external_tax_calculation": "skip",
    "surcharge_calculation": "skip",
    "surcharge_amount": null,
    "tax_on_surcharge": null,
    "net_amount": 100,
    "amount_to_capture": null,
    "amount_capturable": 0,
    "amount_captured": null
  },
  "customer_id": null,
  "connector": "stripe",
  "client_secret": "12345_pay_0195d142100474c3bcb32e40935e290d_secret_0195d14210127d30b524ec0e063b3c0a",
  "created": "2025-03-26T07:02:02.514Z",
  "payment_method_data": {
    "billing": null
  },
  "payment_method_type": "card",
  "payment_method_subtype": "credit",
  "connector_transaction_id": null,
  "connector_reference_id": null,
  "merchant_connector_id": "mca_FhEKUEAUdgv7mVCs3A9U",
  "browser_info": null,
  "error": {
    "code": "invalid_expiry_month",
    "message": "invalid_expiry_month",
    "unified_code": null,
    "unified_message": null
  },
  "shipping": null,
  "billing": null,
  "attempts": null,
  "connector_token_details": null,
  "payment_method_id": null,
  "next_action": null,
  "return_url": "https://google.com/success",
  "authentication_type": "no_three_ds",
  "authentication_type_applied": "no_three_ds"
}
  • Trying to refund failed payment
curl --location 'http://localhost:8080/v2/refunds' \
--header 'X-Profile-Id: pro_TujgCYCsvWvO6YNsRAmC' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_kksGlKY14xNypCzsrJIXFCzL3t2j5kUs7ChmloPryJsdRy2RWYljonzPCxJSDmuZ' \
--data '{
    "payment_id":"12345_pay_0195d142100474c3bcb32e40935e290d",
    "merchant_reference_id":"1742972638",
    "reason":"Paid by mistake",
    "metadata":{
        "foo":"bar"
    }
}'
  • Response from the above call
{
  "error": {
    "type": "invalid_request",
    "message": "This Payment could not be refund because it has a status of failed. The expected state is succeeded, partially_captured",
    "code": "IR_14"
  }
}

Error case responses

  • Connector NotImplemented Error (501)
{
    "error": {
        "type": "invalid_request",
        "message": "get_url method is not implemented",
        "code": "IR_00"
    }
}
  • Connector NotSupported Error (400)
{
    "error": {
        "type": "invalid_request",
        "message": "Payment method type not supported",
        "code": "IR_19",
        "reason": "Refund is not supported by Stripe"
    }
}

Checklist

  • I formatted the code cargo +nightly fmt --all
  • I addressed lints thrown by cargo clippy
  • I reviewed the submitted code
  • I added unit tests for my changes where possible

@AmeyWale AmeyWale requested review from a team as code owners March 25, 2025 09:38
@semanticdiff-com
Copy link

semanticdiff-com bot commented Mar 25, 2025

Review changes with  SemanticDiff

Changed Files
File Status
  crates/router/src/lib.rs  82% smaller
  crates/router/src/core/refunds.rs  45% smaller
  crates/router/src/types/api/refunds.rs  41% smaller
  crates/openapi/src/openapi_v2.rs  26% smaller
  api-reference-v2/openapi_spec.json  14% smaller
  crates/api_models/src/refunds.rs  12% smaller
  crates/router/src/core/utils.rs  10% smaller
  crates/api_models/src/events/refund.rs  8% smaller
  crates/api_models/Cargo.toml Unsupported file format
  crates/common_utils/src/types.rs  0% smaller
  crates/diesel_models/src/query/payment_attempt.rs  0% smaller
  crates/diesel_models/src/query/refund.rs  0% smaller
  crates/diesel_models/src/refund.rs  0% smaller
  crates/hyperswitch_domain_models/src/payments.rs  0% smaller
  crates/hyperswitch_domain_models/src/payments/payment_attempt.rs  0% smaller
  crates/openapi/Cargo.toml Unsupported file format
  crates/router/Cargo.toml Unsupported file format
  crates/router/src/core.rs  0% smaller
  crates/router/src/core/refunds_v2.rs  0% smaller
  crates/router/src/core/utils/refunds_transformers.rs  0% smaller
  crates/router/src/core/utils/refunds_validator.rs  0% smaller
  crates/router/src/db/kafka_store.rs  0% smaller
  crates/router/src/db/refund.rs  0% smaller
  crates/router/src/routes.rs  0% smaller
  crates/router/src/routes/app.rs  0% smaller
  crates/router/src/routes/refunds.rs  0% smaller
  crates/storage_impl/src/mock_db/payment_attempt.rs  0% smaller
  crates/storage_impl/src/payments/payment_attempt.rs  0% smaller

@AmeyWale AmeyWale self-assigned this Mar 25, 2025
@AmeyWale AmeyWale linked an issue Mar 25, 2025 that may be closed by this pull request
@hyperswitch-bot hyperswitch-bot bot added the M-api-contract-changes Metadata: This PR involves API contract changes label Mar 25, 2025
hrithikesh026
hrithikesh026 previously approved these changes Apr 2, 2025
hrithikesh026
hrithikesh026 previously approved these changes Apr 2, 2025
hrithikesh026
hrithikesh026 previously approved these changes Apr 11, 2025
Copy link
Contributor

@hrithikesh026 hrithikesh026 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

hrithikesh026
hrithikesh026 previously approved these changes Apr 11, 2025
jarnura
jarnura previously approved these changes Apr 13, 2025
#[cfg(feature = "v2")]
impl ApiEventMetric for RefundsCreateRequest {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we returning None here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are going to take this up later in separate implementation.

tsdk02
tsdk02 previously approved these changes Apr 15, 2025
@likhinbopanna likhinbopanna added this pull request to the merge queue Apr 17, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Apr 17, 2025
@AmeyWale AmeyWale dismissed stale reviews from tsdk02, jarnura, and hrithikesh026 via 07095f6 April 17, 2025 09:51
@likhinbopanna likhinbopanna added this pull request to the merge queue Apr 21, 2025
Merged via the queue into main with commit eabef32 Apr 21, 2025
16 of 20 checks passed
@likhinbopanna likhinbopanna deleted the refunds-create-core-flow branch April 21, 2025 09:19
pixincreate added a commit that referenced this pull request Apr 22, 2025
…acilitapay-pix-pmt

* 'main' of github.com:juspay/hyperswitch:
  fix(connector): revert noon-paypal (#7864)
  refactor(cypress): do not update `card_expiry` while updating card info (#7834)
  feat(vsaas): add processor_merchant_id and created_by column in payment_intents and payments_attempts for v1 (#7768)
  chore(dynamic-fields): remove billing details as required fields for Worldpay connector (#7853)
  feat(dynamic_routing): integration of elimination routing for core flows (#6816)
  chore(version): 2025.04.22.0
  revert: fix(connector): [noon] address `next_action_url` being `null` for cards in 3ds payment (#7859)
  feat(dynamic_routing): add open router integration for success based routing (#7795)
  feat(refunds_v2): Add refund create core flow (#7619)
  fix(core): [CARD TESTING GUARD] Added Card Testing Guard Config response in case of NULL (#7478)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-v2 M-api-contract-changes Metadata: This PR involves API contract changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Add refunds create core flow for v2 apis.

6 participants