Skip to content

Conversation

bsayak03
Copy link
Contributor

@bsayak03 bsayak03 commented May 16, 2025

Type of Change

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

Description

In this PR, we have added integrity checks for Authorize, Capture, Refund and RSync flows for Xendit Connector
Also, moved the integrity functions from crates/router/src/connector/utils.rs to crates/hyperswitch_connectors/src/utils.rs. Thereby also changing the imports in Stripe.rs.

What is an integrity check?
A scenario where there is a discrepancy between the amount sent in the request and the amount received from the connector, which is checked during response handling.

Additional Changes

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

Motivation and Context

This Pr holds the payment in a non-terminal state incase there is any data discrepancy in the above flows.

How did you test it?

Case 1: AUTOMATIC Capture

Do a Payments - Create
Request:

{
    "amount": 651200,
    "currency": "IDR",
    "confirm": true,
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "customer_id": "First_Customer",
    "name": "John Doe",
    "authentication_type": "three_ds",
    "return_url": "https://google.com",
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4000000000001091",
            "card_exp_month": "12",
            "card_exp_year": "27",
            "card_holder_name": "joseph Doe",
            "card_cvc": "124"
        }
    },
    "billing": {
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        },
        "email": "[email protected]"
    }
}

Now after completing the 3DS authentication in the browser, Do a PSync on the payment_id you received in the response

PSync cURL :

curl --location 'http://localhost:8080/payments/pay_ZiImJRPcy4lBxikZXPZW?force_sync=true&expand_captures=true&expand_attempts=true' \
--header 'Accept: application/json' \
--header 'api-key: dev_iaxB63qB728km9UmcoRYaRDwLvz0ZMN9sEvIEZ3shfdxdqK0qwESalvaliyfhzVL' \
--header 'Cookie: PHPSESSID=0b47db9d7de94c37b6b272087a9f2fa7'

Response:

{
    "error": {
        "type": "api",
        "message": "Integrity Check Failed! as data mismatched for amount expected 651200 but found 651300",
        "code": "IE_00",
        "connector_transaction_id": "pr-dbc3c6aa-6734-47c6-b198-c88ca69173a0"
    }
}

Why this behaviour?
I have hardcoded the amount in the code and the amount i hardcoded is more than the amount in the request causing the payment to fail.

Case 2: MANUAL Capture

Do a payments create

Request:

{
    "amount": 651200,
    "currency": "IDR",
    "confirm": true,
    "capture_method": "manual",
    "capture_on": "2022-09-10T10:11:12Z",
    "customer_id": "First_Customer",
    "name": "John Doe",
    "authentication_type": "three_ds",
    "return_url": "https://google.com",
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4000000000001091",
            "card_exp_month": "12",
            "card_exp_year": "27",
            "card_holder_name": "joseph Doe",
            "card_cvc": "124"
        }
    },
    "billing": {
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        },
        "email": "[email protected]"
    }
}

Now do a force PSync after the 3DS authentication is done in the web browser and you got the payment_id

PSync cURL :

curl --location 'http://localhost:8080/payments/pay_TsnuX8XgHwIdYQWYBEIO?force_sync=true&expand_captures=true&expand_attempts=true' \
--header 'Accept: application/json' \
--header 'api-key: dev_iaxB63qB728km9UmcoRYaRDwLvz0ZMN9sEvIEZ3shfdxdqK0qwESalvaliyfhzVL' \
--header 'Cookie: PHPSESSID=0b47db9d7de94c37b6b272087a9f2fa7'

Response :

{
    "error": {
        "type": "api",
        "message": "Integrity Check Failed! as data mismatched for amount expected 651200 but found 651300",
        "code": "IE_00",
        "connector_transaction_id": "pr-a452cba4-c24f-43a1-9e36-9e7bd066e448"
    }
}

Why this behaviour?
I have hardcoded the amount in code for it to fail

Case 3: Refunds

Do a payments create and this time donot hardcode anything in the code for Payments Create

curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_iaxB63qB728km9UmcoRYaRDwLvz0ZMN9sEvIEZ3shfdxdqK0qwESalvaliyfhzVL' \
--header 'Cookie: PHPSESSID=0b47db9d7de94c37b6b272087a9f2fa7' \
--data-raw '{
    "amount": 651200,
    "currency": "IDR",
    "confirm": true,
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "customer_id": "First_Customer",
    "name": "John Doe",
    "authentication_type": "three_ds",
    "return_url": "https://google.com",
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4000000000001091",
            "card_exp_month": "12",
            "card_exp_year": "27",
            "card_holder_name": "joseph Doe",
            "card_cvc": "124"
        }
    },
    "billing": {
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        },
        "email": "[email protected]"
    }
}'

Now do a force PSync after the 3DS authentication is completed in the web browser and you have the payment_id

PSync cURL :

curl --location 'http://localhost:8080/payments/pay_PUy3X0USglGXRhUZwa6y?force_sync=true&expand_captures=true&expand_attempts=true' \
--header 'Accept: application/json' \
--header 'api-key: dev_iaxB63qB728km9UmcoRYaRDwLvz0ZMN9sEvIEZ3shfdxdqK0qwESalvaliyfhzVL' \
--header 'Cookie: PHPSESSID=0b47db9d7de94c37b6b272087a9f2fa7'

Response:

{
    "payment_id": "pay_PUy3X0USglGXRhUZwa6y",
    "merchant_id": "merchant_1747372268",
    "status": "succeeded",
    "amount": 651200,
    "net_amount": 651200,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 651200,
    "connector": "xendit",
    "client_secret": "pay_PUy3X0USglGXRhUZwa6y_secret_nMwSH8e7SAzjh0Yq253O",
    "created": "2025-05-16T05:59:21.548Z",
    "currency": "IDR",
    "customer_id": "First_Customer",
    "customer": {
        "id": "First_Customer",
        "name": "John Doe",
        "email": null,
        "phone": null,
        "phone_country_code": null
    },
    "description": null,
    "refunds": null,
    "disputes": null,
    "attempts": [
        {
            "attempt_id": "pay_PUy3X0USglGXRhUZwa6y_1",
            "status": "pending",
            "amount": 651200,
            "order_tax_amount": null,
            "currency": "IDR",
            "connector": "xendit",
            "error_message": null,
            "payment_method": "card",
            "connector_transaction_id": "pr-6899118b-61ed-4eaa-85ed-c19ebafa0517",
            "capture_method": "automatic",
            "authentication_type": "three_ds",
            "created_at": "2025-05-16T05:59:21.548Z",
            "modified_at": "2025-05-16T05:59:36.701Z",
            "cancellation_reason": null,
            "mandate_id": null,
            "error_code": null,
            "payment_token": null,
            "connector_metadata": null,
            "payment_experience": null,
            "payment_method_type": "credit",
            "reference_id": "dd99b9db-2807-4393-a160-18ac5e2598b3",
            "unified_code": null,
            "unified_message": null,
            "client_source": null,
            "client_version": null
        }
    ],
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1091",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "400000",
            "card_extended_bin": null,
            "card_exp_month": "12",
            "card_exp_year": "27",
            "card_holder_name": "joseph Doe",
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": {
        "address": null,
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        },
        "email": "[email protected]"
    },
    "order_details": null,
    "email": null,
    "name": "John Doe",
    "phone": null,
    "return_url": "https://google.com/",
    "authentication_type": "three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": null,
    "manual_retry_allowed": false,
    "connector_transaction_id": "pr-6899118b-61ed-4eaa-85ed-c19ebafa0517",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "dd99b9db-2807-4393-a160-18ac5e2598b3",
    "payment_link": null,
    "profile_id": "pro_p9XcyZ68jQWhZVhjDfvA",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_Yrd0AB3I63wrPAxClDXT",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-05-16T06:14:21.548Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2025-05-16T05:59:45.207Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null,
    "card_discovery": "manual",
    "force_3ds_challenge": false,
    "force_3ds_challenge_trigger": false,
    "issuer_error_code": null,
    "issuer_error_message": null
}

Now do a Refunds Create and in postman send an amount which is lower than the Captured Amount

Refunds Create cURL :

curl --location 'http://localhost:8080/refunds' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_iaxB63qB728km9UmcoRYaRDwLvz0ZMN9sEvIEZ3shfdxdqK0qwESalvaliyfhzVL' \
--header 'Cookie: PHPSESSID=0b47db9d7de94c37b6b272087a9f2fa7' \
--data '{
    "payment_id": "pay_PUy3X0USglGXRhUZwa6y",
    
    "amount": 651100,
    "reason": "Customer returned product",
    "refund_type": "instant",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    }
    
    
    
    
    
}'

Response:

{
    "refund_id": "ref_UL8G03huxgVfg85dBBpj",
    "payment_id": "pay_PUy3X0USglGXRhUZwa6y",
    "amount": 651100,
    "currency": "IDR",
    "status": "review",
    "reason": "Customer returned product",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    },
    "error_message": "Integrity Check Failed! as data mismatched for fields refund_amount expected 651100 but found 651200",
    "error_code": "IE",
    "unified_code": null,
    "unified_message": null,
    "created_at": "2025-05-16T05:59:52.764Z",
    "updated_at": "2025-05-16T05:59:54.555Z",
    "connector": "xendit",
    "profile_id": "pro_p9XcyZ68jQWhZVhjDfvA",
    "merchant_connector_id": "mca_Yrd0AB3I63wrPAxClDXT",
    "split_refunds": null,
    "issuer_error_code": null,
    "issuer_error_message": null
}

Why this behaviour?
I have hardcoded the amount to be refunded in the code as the same as the captured amount but in postman the amount i have sent is less than that, causing the payments to fail (intentionally)

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 a review from a team as a code owner May 16, 2025 06:35
Copy link

semanticdiff-com bot commented May 16, 2025

Review changes with  SemanticDiff

Changed Files
File Status
  crates/hyperswitch_connectors/src/utils.rs  93% smaller
  crates/hyperswitch_connectors/src/connectors/xendit/transformers.rs  34% smaller
  crates/hyperswitch_connectors/src/connectors/xendit.rs  30% smaller
  crates/router/src/connector/utils.rs  0% smaller

@bsayak03 bsayak03 self-assigned this May 16, 2025
@hyperswitch-bot hyperswitch-bot bot requested a review from a team as a code owner May 16, 2025 07:15
Comment on lines +795 to 797
pub amount: FloatMajorUnit,
pub currency: String,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

are we sure about this as required fields @deepanshu-iiitu can you please check and confirm

Copy link
Contributor Author

Choose a reason for hiding this comment

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

currency should be an enum of Currency, correcting it

Copy link
Contributor

Choose a reason for hiding this comment

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

can you please test end to end!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have tested the flows, added them in the PR description as well

Copy link
Contributor

Choose a reason for hiding this comment

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

okay sure! but you are sure about the required fields from connector end right. We don't want a deserialization error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines +6299 to +6319
pub fn get_sync_integrity_object<T>(
amount_convertor: &dyn AmountConvertor<Output = T>,
amount: T,
currency: String,
) -> Result<SyncIntegrityObject, error_stack::Report<errors::ConnectorError>> {
let currency_enum = enums::Currency::from_str(currency.to_uppercase().as_str())
.change_context(errors::ConnectorError::ParsingFailed)?;
let amount_in_minor_unit =
convert_back_amount_to_minor_units(amount_convertor, amount, currency_enum)?;

Ok(SyncIntegrityObject {
amount: Some(amount_in_minor_unit),
currency: Some(currency_enum),
})
}

pub fn get_capture_integrity_object<T>(
amount_convertor: &dyn AmountConvertor<Output = T>,
capture_amount: Option<T>,
currency: String,
) -> Result<CaptureIntegrityObject, error_stack::Report<errors::ConnectorError>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

what about Authorise Flow why are not doing integrity for authorisation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

missed it, adding that

Copy link
Contributor

Choose a reason for hiding this comment

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

also please change the pr title mentioning authorise flow

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure

Comment on lines 1628 to 1631
if (
setupFutureUsage === "off_session" &&
response.body.status === "succeeded"
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

can we remove all the unnecessary changes in the cypress files

Copy link
Contributor Author

Choose a reason for hiding this comment

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

these are formatting changes that took place due to this command cargo +nightly fmt --all

@bsayak03 bsayak03 changed the title feat(XENDIT): added integrity check support for capture and refund flows feat(XENDIT): Added Integrity Check for Authorize, Capture, Refund & RSync flows May 17, 2025
@bsayak03 bsayak03 changed the title feat(XENDIT): Added Integrity Check for Authorize, Capture, Refund & RSync flows feat(connector): [XENDIT] Added Integrity Check for Authorize, Capture, Refund & RSync flows May 20, 2025
This was referenced Sep 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants