Skip to content

Conversation

bsayak03
Copy link
Contributor

@bsayak03 bsayak03 commented May 24, 2025

Type of Change

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

Description

The amount field in DisputePayload in crates/hyperswitch_interfaces/src/disputes.rs was String. Changed it to StringMinorUnit.

Made changes in the following connectors : Adyen, Airwallex, Bluesnap, Braintree, Checkout, Novalnet, Payme, Paypal, Rapyd, Stripe, Trustpay.

In the Dispute table, changed the amount field from String to StringMinorUnit and dispute_amount from i64 to MinorUnit.

Dispute Webhooks need to be tested for all the above mentioned connectors.

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?

  1. Stripe Payments Create - cURL :

run these on stripe cli

brew install stripe/stripe-cli/stripe
stripe login --api-key <stripe_api_key>
stripe listen --events charge.dispute.created \ --forward-to localhost:8080/webhook
stripe listen --forward-to http://localhost:8080/webhooks/<merchant_id>/<merchant_connector_id>

curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_k7bBo7Jh3VaqAt5yqY9tG8IjjVbi4VlCLNHxzifBoz4ckrJQGUZePElGu0NMl9N6' \
--header 'Cookie: PHPSESSID=0b47db9d7de94c37b6b272087a9f2fa7' \
--data-raw '{
    "amount": 6540,
    "currency": "USD",
    "confirm": true,
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "amount_to_capture": 6540,
    "customer_id": "StripeCustomer",
    "email": "[email protected]",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+65",
    "description": "Its my first payment request",
    "authentication_type": "no_three_ds",
    "return_url": "https://duck.com",
    "setup_future_usage": "on_session",
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4000000000000259",
            
            "card_exp_month": "10",
            "card_exp_year": "25",
            "card_holder_name": "joseph Doe",
            "card_cvc": "123"
        }
    },
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "joseph",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "joseph",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    },
    "routing": {
        "type": "single",
        "data": "stripe"
    }
}'

This will initiate a dispute and you will either accept it or counter it and hence will receive incoming webhooks

Screenshot 2025-05-28 at 5 32 41 PM
  1. Checkout Payments Create

cURL :

curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_EAjwqV3dxVM3Lkxhw6O2epmoAoNaP5PXnCqJbeilH5OEBRNMzNWS5leo6KuJN4Pw' \
--header 'Cookie: PHPSESSID=0b47db9d7de94c37b6b272087a9f2fa7' \
--data-raw '{
    "amount": 1050,
    "currency": "GBP",
    "confirm": true,
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "amount_to_capture": 1050,
    "customer_id": "StripeCustomer",
    "email": "[email protected]",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+1",
    "description": "Its my first payment request",
    "authentication_type": "no_three_ds",
    "return_url": "https://duck.com",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "card_number": "4005519200000004",
            "card_exp_month": "01",
            "card_exp_year": "2099",
            "card_holder_name": "joseph Doe",
            "card_cvc": "123"
        }
    },
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "GB",
            "first_name": "PiX"
        }
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "GB",
            "first_name": "PiX"
        }
    },
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    }
}'

After this head to Checkout Dashboard and you'll see this being marked as Dispute Resolved and you'll receive Dispute Webhooks too.

Screenshot 2025-05-28 at 7 19 00 PM
  1. Trustpay

Manually sent incoming webhooks to Hyperswitch

curl --location 'https://1ebd-103-170-182-246.ngrok-free.app/webhooks/merchant_1748457198/mca_rBig1cjLFYB1NRvjE7EU' \
--header 'Content-Type: application/json' \
--data '{
    "PaymentInformation": {
        "CreditDebitIndicator": "DBIT",
    "References": {
        "MerchantReference": "pay_ViKeFnsTWv7YPs8HcLMM_1",        
        "PaymentId": "6964857984"                              
        
    },
    "Status": "Chargebacked",
    "Amount": {
        "Amount": 65.41,
        "Currency": "EUR"
    },
    "StatusReasonInformation": null
    },
    "Signature": "abcd"
}'
Screenshot 2025-05-29 at 12 11 27 AM
  1. Novalnet

https://developer.novalnet.com/asynchronousnotification/parameterspassed?payment=chargeback_creditcard_chargeback#parameter-list

Used Novalnet's Webhook simulator to send webhooks to Hyperswitch. Payment Access Key will be key1 of Hyperswitch and TID will be connector_transaction_id with which a successful payment was made previously.

{
    "event": {
        "checksum": "df3cabaabce5c30559eacf25e4413ac2e4f612ceae3b048ac17adc85f60a6389",
        "parent_tid": 15186900010502281,
        "tid": 12186900010502281,
        "type": "CHARGEBACK"
    },
    "result": {
        "status": "SUCCESS",
        "status_code": 100,
        "status_text": "Successful"
    },
    "transaction": {
        "amount": 10000,
        "currency": "EUR",
        "order_no": "pay_3jddDzdf0lWfwn05uI1q_1",
        "payment_type": "CREDITCARD_CHARGEBACK",
        "reason": "Fraud",
        "status": "CONFIRMED",
        "status_code": 100,
        "test_mode": 1,
        "tid": 12186900010502281
    },
    "merchant": {
        "project": 6120,
        "project_name": "Developer Portal",
        "project_url": "https://developer.novalnet.de",
        "vendor": 4
    },
    "customer": {
        "billing": {
            "city": "Musterhausen",
            "country_code": "DE",
            "house_no": "1467",
            "street": "CA",
            "zip": "12345"
        },
        "customer_ip": "103.170.182.246",
        "email": "[email protected]",
        "first_name": "Max",
        "gender": "u",
        "last_name": "Mustermann",
        "mobile": "8056594427"
    }
}
Screenshot 2025-05-29 at 12 03 36 PM

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

@bsayak03 bsayak03 requested review from a team as code owners May 24, 2025 14:33
Copy link

semanticdiff-com bot commented May 24, 2025

Review changes with  SemanticDiff

Changed Files
File Status
  crates/hyperswitch_connectors/src/connectors/braintree/transformers.rs  88% smaller
  crates/hyperswitch_connectors/src/connectors/rapyd/transformers.rs  87% smaller
  crates/router/src/types/api.rs  86% smaller
  crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs  77% smaller
  crates/router/tests/connectors/airwallex.rs  71% smaller
  crates/router/src/core/webhooks/incoming.rs  60% smaller
  crates/hyperswitch_connectors/src/connectors/braintree.rs  56% smaller
  crates/hyperswitch_connectors/src/connectors/novalnet.rs  51% smaller
  crates/hyperswitch_connectors/src/connectors/stripe.rs  49% smaller
  crates/router/src/services/kafka/dispute.rs  46% smaller
  crates/hyperswitch_connectors/src/connectors/rapyd.rs  43% smaller
  crates/router/src/services/kafka/dispute_event.rs  43% smaller
  crates/api_models/src/disputes.rs  42% smaller
  crates/router/src/utils/user/sample_data.rs  37% smaller
  crates/hyperswitch_connectors/src/connectors/paypal.rs  36% smaller
  crates/diesel_models/src/dispute.rs  32% smaller
  crates/hyperswitch_connectors/src/connectors/checkout.rs  29% smaller
  crates/hyperswitch_connectors/src/connectors/bluesnap.rs  26% smaller
  crates/hyperswitch_connectors/src/connectors/trustpay.rs  26% smaller
  crates/hyperswitch_connectors/src/connectors/adyen.rs  25% smaller
  crates/hyperswitch_connectors/src/connectors/payme.rs  21% smaller
  crates/hyperswitch_connectors/src/connectors/airwallex.rs  19% smaller
  crates/common_utils/src/types.rs  19% smaller
  crates/router/src/db/dispute.rs  17% smaller
  crates/openapi/src/openapi.rs  11% smaller
  crates/openapi/src/openapi_v2.rs  11% smaller
  crates/hyperswitch_connectors/src/connectors/bluesnap/transformers.rs  3% smaller
  crates/hyperswitch_connectors/src/connectors/trustpay/transformers.rs  3% smaller
  api-reference-v2/openapi_spec.json  0% smaller
  api-reference/openapi_spec.json  0% smaller
  crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs  0% smaller
  crates/hyperswitch_connectors/src/connectors/checkout/transformers.rs  0% smaller
  crates/hyperswitch_connectors/src/connectors/payme/transformers.rs  0% smaller
  crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs  0% smaller
  crates/hyperswitch_connectors/src/utils.rs  0% smaller
  crates/hyperswitch_interfaces/src/disputes.rs  0% smaller
  crates/router/src/compatibility/stripe/webhooks.rs  0% smaller

@bsayak03 bsayak03 self-assigned this May 24, 2025
}

impl Adyen {
pub const fn new() -> &'static Self {
&Self {
amount_converter: &MinorUnitForConnector,
amount_converter_string_major_unit: &StringMinorUnitForConnector,
Copy link
Contributor

Choose a reason for hiding this comment

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

can u rename it to flow basis as having two amount converter creates confusion

Comment on lines 1398 to 1435
impl Display for StringMinorUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

impl<DB> FromSql<sql_types::Text, DB> for StringMinorUnit
where
DB: Backend,
String: FromSql<sql_types::Text, DB>,
{
fn from_sql(value: DB::RawValue<'_>) -> deserialize::Result<Self> {
let val = String::from_sql(value)?;
Ok(Self(val))
}
}

impl<DB> ToSql<sql_types::Text, DB> for StringMinorUnit
where
DB: Backend,
String: ToSql<sql_types::Text, DB>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
self.0.to_sql(out)
}
}

impl<DB> Queryable<sql_types::Text, DB> for StringMinorUnit
where
DB: Backend,
Self: FromSql<sql_types::Text, DB>,
{
type Row = Self;

fn build(row: Self::Row) -> deserialize::Result<Self> {
Ok(row)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

please move these impl below the struct for better visibility

@@ -1064,8 +1075,13 @@ impl IncomingWebhook for Airwallex {
.object
.parse_value("AirwallexDisputeObject")
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
let amount = convert_amount(
self.amount_converter,
MinorUnit::new(dispute_details.dispute_amount),
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of creating MinorUnit::new() we can change AirwallexDisputeObject itself to accept MinorUnit

Comment on lines 79 to 88
amount_converter_1: &'static (dyn AmountConvertor<Output = StringMinorUnit> + Sync),
amount_converter_webhooks: &'static (dyn AmountConvertor<Output = FloatMajorUnit> + Sync),
}

impl Bluesnap {
pub fn new() -> &'static Self {
&Self {
amount_converter: &StringMajorUnitForConnector,
amount_converter_1: &StringMinorUnitForConnector,
amount_converter_webhooks: &FloatMajorUnitForConnector,
Copy link
Contributor

Choose a reason for hiding this comment

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

please fix the naming convention amount_converter_1 doesn't make any sense

dispute_data.amount_disputed.to_string(),
amount: convert_amount(
self.amount_converter_webhooks,
MinorUnit::new(dispute_data.amount_disputed),
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 on accepting domain amount type in struct instead of creating new domain types on fly!

amount: webhook_object.price.to_string(),
amount: utils::convert_amount(
self.amount_converter_1,
MinorUnit::new(webhook_object.price),
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 struct handling

@@ -1187,7 +1192,18 @@ async fn get_or_update_dispute_object(
profile_id: Some(business_profile.get_id().to_owned()),
evidence: None,
merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
dispute_amount: dispute_details.amount.parse::<i64>().unwrap_or(0),
dispute_amount: MinorUnit::get_amount_as_i64(
Copy link
Contributor

Choose a reason for hiding this comment

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

change dispute_amount to MinorUnit

Comment on lines 53 to 63
dispute_id: &dispute.dispute_id,
dispute_amount: dispute.amount.parse::<i64>().unwrap_or_default(),
currency: dispute.dispute_currency.unwrap_or(
dispute
.currency
.to_uppercase()
.parse_enum("Currency")
.unwrap_or_default(),
),
dispute_amount: StringMinorUnitForConnector::convert_back(
&StringMinorUnitForConnector,
dispute.amount.clone(),
currency,
)
.map(MinorUnit::get_amount_as_i64)
.unwrap_or_else(|e| {
router_env::logger::error!("Failed to convert dispute amount: {e:?}");
0
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

change dispute.rs to MinorUnit

.to_uppercase()
.parse_enum("Currency")
.unwrap_or_default(),
);
Self {
dispute_id: &dispute.dispute_id,
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines 432 to 442
disputes_count += 1;
let currency = payment_intent
.currency
.unwrap_or(common_enums::Currency::USD);
Some(DisputeNew {
dispute_id: common_utils::generate_id_with_default_len("test"),
amount: (amount * 100).to_string(),
currency: payment_intent
.currency
.unwrap_or(common_enums::Currency::USD)
.to_string(),
amount: StringMinorUnitForConnector::convert(
&StringMinorUnitForConnector,
MinorUnit::new(amount * 100),
payment_intent.currency.unwrap_or(currency),
)
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 evaluating currency twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because in the amount field we want currency as an Enum type and in the currency field we want it as string type

Copy link
Contributor

Choose a reason for hiding this comment

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

you can reuse the existing currency evaluation

@@ -1064,8 +1075,13 @@ impl IncomingWebhook for Airwallex {
.object
.parse_value("AirwallexDisputeObject")
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
let amount = convert_amount(
Copy link
Contributor

Choose a reason for hiding this comment

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

do it for other flows aswell

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no other flow in Airwallex.rs is converting amount


let amount = utils::convert_amount(
self.amount_converter_1,
MinorUnit::new(i64::from(dispute_details.data.amount)),
Copy link
Contributor

Choose a reason for hiding this comment

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

please make the appropiate struct changes

@@ -86,12 +89,14 @@ use crate::{
#[derive(Clone)]
pub struct Paypal {
amount_converter: &'static (dyn AmountConvertor<Output = StringMajorUnit> + Sync),
amount_converter_1: &'static (dyn AmountConvertor<Output = StringMinorUnit> + Sync),
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

@@ -60,11 +63,13 @@ use crate::{
#[derive(Clone)]
pub struct Rapyd {
amount_converter: &'static (dyn AmountConvertor<Output = FloatMajorUnit> + Sync),
amount_converter_1: &'static (dyn AmountConvertor<Output = StringMinorUnit> + Sync),
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

amount: webhook_dispute_data.amount.to_string(),
amount: convert_amount(
self.amount_converter_1,
MinorUnit::new(webhook_dispute_data.amount),
Copy link
Contributor

Choose a reason for hiding this comment

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

appropiate struct change please

Comment on lines 432 to 442
disputes_count += 1;
let currency = payment_intent
.currency
.unwrap_or(common_enums::Currency::USD);
Some(DisputeNew {
dispute_id: common_utils::generate_id_with_default_len("test"),
amount: (amount * 100).to_string(),
currency: payment_intent
.currency
.unwrap_or(common_enums::Currency::USD)
.to_string(),
amount: StringMinorUnitForConnector::convert(
&StringMinorUnitForConnector,
MinorUnit::new(amount * 100),
payment_intent.currency.unwrap_or(currency),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

you can reuse the existing currency evaluation

@hyperswitch-bot hyperswitch-bot bot added the M-api-contract-changes Metadata: This PR involves API contract changes label May 26, 2025
@likhinbopanna likhinbopanna added this pull request to the merge queue May 30, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks May 30, 2025
@Gnanasundari24 Gnanasundari24 added this pull request to the merge queue May 30, 2025
Merged via the queue into main with commit 0476361 May 30, 2025
58 of 64 checks passed
@Gnanasundari24 Gnanasundari24 deleted the currency/fix branch May 30, 2025 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
M-api-contract-changes Metadata: This PR involves API contract changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants