Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(router): add support for relay refund incoming webhooks #6974

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

ShankarSinghC
Copy link
Contributor

@ShankarSinghC ShankarSinghC commented Jan 2, 2025

Type of Change

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

Description

Some connectors, like Adyen, do not support a refund sync API, so we need to rely on refund webhooks for the status. To address this, support for relay refund webhooks is being added.

To support this use case, a new webhook endpoint needs to be introduced for relay webhooks:
/webhooks/relay/{merchant_id}/{connector_id_or_name}

Once the webhook is identified as a relay webhook, it should be handled based on the flow type, such as refunds, payments, etc. For now, only support for relay refund webhooks has been added.

Additional Changes

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

Motivation and Context

How did you test it?

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

@ShankarSinghC ShankarSinghC added A-core Area: Core flows C-refactor Category: Refactor A-refunds Area: Refund flows labels Jan 2, 2025
@ShankarSinghC ShankarSinghC self-assigned this Jan 2, 2025
@ShankarSinghC ShankarSinghC requested review from a team as code owners January 2, 2025 13:49
Copy link

semanticdiff-com bot commented Jan 2, 2025

@ShankarSinghC ShankarSinghC changed the title add support for relay refund incoming webhooks feat(router): add support for relay refund incoming webhooks Jan 2, 2025
@ShankarSinghC ShankarSinghC linked an issue Jan 2, 2025 that may be closed by this pull request
crates/router/src/core/webhooks/incoming.rs Outdated Show resolved Hide resolved
crates/router/src/core/webhooks/incoming.rs Outdated Show resolved Hide resolved
) -> impl Responder {
let flow = Flow::IncomingWebhookReceive;
let (merchant_id, connector_id_or_name) = path.into_inner();
let is_relay_webhook = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this to a const

Copy link
Member

Choose a reason for hiding this comment

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

Can we consider using an enum instead of a bool maybe?

crates/router/src/routes/app.rs Show resolved Hide resolved
crates/router/src/routes/app.rs Outdated Show resolved Hide resolved
crates/router/src/core/webhooks/incoming.rs Outdated Show resolved Hide resolved
crates/router/src/core/webhooks/incoming.rs Outdated Show resolved Hide resolved
crates/router/src/core/webhooks/incoming.rs Outdated Show resolved Hide resolved
) -> StorageResult<Self> {
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
conn,
dsl::id.eq(connector_reference_id.to_owned()),
Copy link
Contributor

Choose a reason for hiding this comment

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

We should use dsl::connector_reference_id right?


pub async fn find_by_connector_reference_id(
conn: &PgPooledConn,
connector_reference_id: &str,
Copy link
Contributor

Choose a reason for hiding this comment

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

As, it's not globally unique we may have to query this along with profile_id. You can add a index in DB too


// Using early return ensures unsupported webhooks are acknowledged to the connector
if let Some(error) = relay_webhook_response.as_ref().err() {
if let errors::ApiErrorResponse::NotSupported { .. } = error.current_context() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can avoid this nested if, by getting the current context in the outer if statement

))
.await
.attach_printable("Incoming webhook flow for disputes failed"),
metrics::WEBHOOK_EVENT_TYPE_IDENTIFICATION_FAILURE_COUNT.add(
Copy link
Contributor

Choose a reason for hiding this comment

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

This metric is not relevant in current case, this metric indicates event type identification failure where as our case is event type being identified correctly but not supported

.get_webhook_api_response(&request_details, None)
.switch()
.attach_printable(
"Failed while early return in case of event type parsing",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"Failed while early return in case of event type parsing",
"Failed while early return in case of not supported event types in relay webhooks",

))
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update relay")?;
Copy link
Contributor

Choose a reason for hiding this comment

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

message's incorrect here

response
} else {
Err(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to force sync relay")?
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.attach_printable("Failed to force sync relay")?
.attach_printable("Unexpcted response from force sync relay")?

impl RelayWebhooks {
pub fn server(state: AppState) -> Scope {
use api_models::webhooks as webhook_type;
web::scope("/webhooks")
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
web::scope("/webhooks")
web::scope("/webhooks/relay")

Copy link
Contributor

Choose a reason for hiding this comment

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

scope for RelayWebhooks would always be /webhooks/relay

@hyperswitch-bot hyperswitch-bot bot added the M-database-changes Metadata: This PR involves database schema changes label Jan 7, 2025
Comment on lines +50 to +54
pub async fn find_by_connector_reference_id(
conn: &PgPooledConn,
connector_reference_id: &str,
profile_id: &common_utils::id_type::ProfileId,
) -> StorageResult<Self> {
Copy link
Member

Choose a reason for hiding this comment

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

Can you rename this accordingly:

Either:

Suggested change
pub async fn find_by_connector_reference_id(
conn: &PgPooledConn,
connector_reference_id: &str,
profile_id: &common_utils::id_type::ProfileId,
) -> StorageResult<Self> {
pub async fn find_by_profile_id_connector_reference_id(
conn: &PgPooledConn,
profile_id: &common_utils::id_type::ProfileId,
connector_reference_id: &str,
) -> StorageResult<Self> {

Or:

Suggested change
pub async fn find_by_connector_reference_id(
conn: &PgPooledConn,
connector_reference_id: &str,
profile_id: &common_utils::id_type::ProfileId,
) -> StorageResult<Self> {
pub async fn find_by_connector_reference_id_profile_id(
conn: &PgPooledConn,
connector_reference_id: &str,
profile_id: &common_utils::id_type::ProfileId,
) -> StorageResult<Self> {

I'd prefer the former. Then fix the param order in the usages as well.

Comment on lines +39 to +45
async fn find_relay_by_connector_reference_id(
&self,
key_manager_state: &KeyManagerState,
merchant_key_store: &domain::MerchantKeyStore,
connector_reference_id: &str,
profile_id: &common_utils::id_type::ProfileId,
) -> CustomResult<hyperswitch_domain_models::relay::Relay, errors::StorageError>;
Copy link
Member

Choose a reason for hiding this comment

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

Same here:

Suggested change
async fn find_relay_by_connector_reference_id(
&self,
key_manager_state: &KeyManagerState,
merchant_key_store: &domain::MerchantKeyStore,
connector_reference_id: &str,
profile_id: &common_utils::id_type::ProfileId,
) -> CustomResult<hyperswitch_domain_models::relay::Relay, errors::StorageError>;
async fn find_relay_by_profile_id_connector_reference_id(
&self,
key_manager_state: &KeyManagerState,
merchant_key_store: &domain::MerchantKeyStore,
profile_id: &common_utils::id_type::ProfileId,
connector_reference_id: &str,
) -> CustomResult<hyperswitch_domain_models::relay::Relay, errors::StorageError>;

And fix it in trait implementations and usages.

impl RelayWebhooks {
pub fn server(state: AppState) -> Scope {
use api_models::webhooks as webhook_type;
web::scope("/webhooks/relay")
Copy link
Member

Choose a reason for hiding this comment

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

Have we confirmed that this doesn't affect the existing incoming webhooks flows, due to the possibly conflicting routes?

) -> impl Responder {
let flow = Flow::IncomingWebhookReceive;
let (merchant_id, connector_id_or_name) = path.into_inner();
let is_relay_webhook = true;
Copy link
Member

Choose a reason for hiding this comment

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

Can we consider using an enum instead of a bool maybe?

path: web::Path<(common_utils::id_type::MerchantId, String)>,
) -> impl Responder {
let flow = Flow::IncomingWebhookReceive;
let (merchant_id, connector_id_or_name) = path.into_inner();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
let (merchant_id, connector_id_or_name) = path.into_inner();
let (merchant_id, connector_id) = path.into_inner();

state: web::Data<AppState>,
req: HttpRequest,
body: web::Bytes,
path: web::Path<(common_utils::id_type::MerchantId, String)>,
Copy link
Member

Choose a reason for hiding this comment

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

Can we use MerchantConnectorAccountId instead of a String, if we only expect a MCA ID in the path?

Suggested change
path: web::Path<(common_utils::id_type::MerchantId, String)>,
path: web::Path<(common_utils::id_type::MerchantId, common_utils::id_type::MerchantConnectorAccountId)>,

@@ -0,0 +1,2 @@
-- Your SQL goes here
CREATE UNIQUE INDEX relay_profile_id_connector_reference_id ON relay (profile_id, connector_reference_id);
Copy link
Member

Choose a reason for hiding this comment

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

Nit:

Suggested change
CREATE UNIQUE INDEX relay_profile_id_connector_reference_id ON relay (profile_id, connector_reference_id);
CREATE UNIQUE INDEX relay_profile_id_connector_reference_id_index ON relay (profile_id, connector_reference_id);

Update the down.sql file as well if you're taking up this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-core Area: Core flows A-refunds Area: Refund flows C-refactor Category: Refactor M-database-changes Metadata: This PR involves database schema changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

add support for relay refund incoming webhooks
3 participants