Skip to content

fix: prevent IEEE 754 precision loss for large acquirer BIN values#4939

Open
itsaryanchauhan wants to merge 1 commit into
juspay:mainfrom
itsaryanchauhan:fix/acquirer-bin-precision-loss
Open

fix: prevent IEEE 754 precision loss for large acquirer BIN values#4939
itsaryanchauhan wants to merge 1 commit into
juspay:mainfrom
itsaryanchauhan:fix/acquirer-bin-precision-loss

Conversation

@itsaryanchauhan

Copy link
Copy Markdown
Contributor
Untitled.mov

Type of Change

  • Bugfix

Description

The Acquirer BIN field was silently truncating values with more than 15-16 significant digits before sending them to the API.

Example:

12345678901234567890 was being sent as 12345678901234567000.

This happened because:

  1. binField used numericTextInput, which stores input as a JavaScript float. IEEE 754 double precision can only safely represent around 15-16 significant digits.
  2. normalizeNumericStringFields used getFloat to read the BIN value, which also parsed string values as floats and caused truncation even after the input was changed to store strings.

The fix:

  • Replaced numericTextInput with a custom text input for binField that stores BIN as a string.
  • Added digits-only filtering with a max length of 20 characters.
  • Removed AcquirerBin from normalizeNumericStringFields so the value passes through unchanged.
  • Updated validateForm to validate BIN length as a string instead of parsing it as a float.

Motivation and Context

Fixes #4118

How did you test it?

Manually entered a 20-digit BIN value via the Add Network modal:

12345678901234567890

Confirmed that the full value is preserved exactly in the API request payload without precision loss.

Where to test it?

  • INTEG
  • SANDBOX
  • PROD

Backend Dependency

  • Yes
  • No

Feature Flag

  • New feature flag added
  • Existing feature flag updated
  • No feature flag changes

Checklist

  • I ran npm run re:build
  • I reviewed submitted code
  • I added unit tests for my changes where possible

@itsaryanchauhan itsaryanchauhan requested a review from a team as a code owner June 8, 2026 20:34
@semanticdiff-com

Copy link
Copy Markdown

Review changes with  SemanticDiff

@XyneSpaces XyneSpaces left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Summary

Classification: Bugfix — IEEE 754 Precision Loss Fix
Risk Level: Low (localized form input fix)
Files Changed: 1 file (MerchantAcquirerDetailsUtils.res)


Tier 1 — High Signal Issues

No critical issues found.


Tier 2 — Medium Signal Observations

⚠️ Simplified validation logic may lose edge-case coverage

The validateForm function was simplified to use valuesDict->getString(key, "")->isNonEmptyString for all required fields including AcquirerBin. Previously, AcquirerBin had special handling using getOptionFloat to validate presence.

Fix: Verify that the empty-string check is sufficient for AcquirerBin validation. If the form initializes with undefined or null values that render as empty strings, the validation still works. If there's any edge case where the field might be missing entirely from the dict, consider keeping explicit key existence checking.


Tier 3 — Minor Nits

🔍 Regex replacement on every keystroke

let filteredValue = value->String.replaceRegExp(%re("/[^0-9]/g"), "")

The regex runs on every character input. While negligible for this use case, consider memoizing the regex or using a simple character-filter function for consistency with the codebase pattern.

Fix (optional):

let filteredValue = value->String.split("")->Array.filter(c => c >= "0" && c <= "9")->Array.join("")

Positive Notes

  • Correct identification of IEEE 754 precision loss as the root cause
  • Proper use of string-based storage for arbitrary-length numeric identifiers
  • Appropriate max length validation (4-20 digits)
  • Clean removal of AcquirerBin from normalizeNumericStringFields
  • The fix aligns with the pattern used for other text fields in the same form

Pre-existing Issues (not introduced by this PR)

🔍 AcquirerIca still uses numericTextInput but may face same precision issue

The icaField still uses numericTextInput which converts input to float. If ICA values can exceed 15-16 digits, they will suffer the same precision loss. Consider applying the same string-input fix to icaField if the backend expects exact digit preservation.


Verdict:Approve — Clean fix for a subtle but impactful bug.

@XyneSpaces XyneSpaces left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Simplified validation logic should verify edge cases

The validation change from getOptionFloat to getString for AcquirerBin assumes the value will always be a string in the form. Verify that:

  1. Form initialization doesn't leave the field absent from the dict
  2. Backend submission still receives the value as a string, not a number

This change is likely safe given the text input, but confirm the form state initialization sets an empty string default for acquirer_bin.

@@ -137,23 +146,18 @@ let validateForm = (~requiredKeys: array<acquirerField>, values: JSON.t): JSON.t

requiredKeys->Array.forEach(field => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Simplified validation logic should verify edge cases

The validation change from getOptionFloat to getString for AcquirerBin assumes the value will always be a string in the form. Verify that:

  1. Form initialization doesn't leave the field absent from the dict
  2. Backend submission still receives the value as a string, not a number

This change is likely safe given the text input, but confirm the form state initialization sets an empty string default for acquirer_bin.

let handleChange = event => {
let value = ReactEvent.Form.target(event)["value"]
let filteredValue = value->String.replaceRegExp(%re("/[^0-9]/g"), "")
input.onChange(filteredValue->Identity.stringToFormReactEvent)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 Regex runs on every keystroke — consider optimization

While the %re("/[^0-9]/g") regex is compiled at build time, it still executes on every change event. For consistency with the codebase's functional style, consider:

let filteredValue = value->String.split("")->Array.filter(c => c >= "0" && c <= "9")->Array.join("")

Or if regex is preferred, lift it to module scope to avoid reallocation.

@XyneSpaces

Copy link
Copy Markdown
Collaborator

Detailed Review Findings

Overview

Clean fix for IEEE 754 precision loss in the Acquirer BIN field. The root cause analysis is accurate, and the solution properly addresses the issue.

Key Changes Reviewed

1. Input Component Replacement (Lines 36-45)

  • ✅ Correctly replaces numericTextInput with custom text input
  • ✅ Digits-only filtering prevents invalid characters
  • maxLength=20 enforces backend constraint
  • ⚠️ Consider optimizing regex usage (see inline comment)

2. Normalization Cleanup (Line 132)

  • ✅ Removes AcquirerBin from normalizeNumericStringFields
  • ✅ Prevents float parsing that caused truncation

3. Validation Updates (Lines 149-156)

  • ✅ Simplified to use getString for all fields
  • ✅ Direct string length validation (4-20 digits)
  • ⚠️ Verify form initialization handles empty strings correctly

Pre-existing Issue Noted

The AcquirerIca field still uses numericTextInput. If ICA values can exceed 15-16 digits, they will face the same precision loss issue. Consider applying the same fix if applicable.

Verdict

Approve — Well-executed bugfix with appropriate test coverage noted in PR description.

@XyneSpaces XyneSpaces left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Summary

Classification: Bugfix — IEEE 754 precision loss prevention for Acquirer BIN
Risk Level: Low-Medium
Files Changed: 1


Findings Overview

Severity Count Issue
⚠️ 1 Edge case allows empty BIN submission after filtering
💡 1 Identity conversion obscures intent
🔍 1 No user feedback on filtered characters

Verdict: One blocking issue (empty string edge case) needs addressing before merge.

let handleChange = event => {
let value = ReactEvent.Form.target(event)["value"]
let filteredValue = value->String.replaceRegExp(%re("/[^0-9]/g"), "")
input.onChange(filteredValue->Identity.stringToFormReactEvent)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Empty string edge case after filtering allows invalid submission

If the user types only non-digit characters (e.g., "abc"), the regex filter strips them all, resulting in an empty string. This passes both validation checks:

  • requiredKeys check passes because "" is technically a present value in the dict
  • BIN length check is skipped because isNonEmptyString returns false

The form would submit with an empty BIN despite the field being marked as required.

Fix: Either validate the filtered result is non-empty before setting, or ensure empty string after filtering fails the required check:

let filteredValue = value->String.replaceRegExp(%re("/[^0-9]/g"), "")
if filteredValue->isNonEmptyString || value->isEmptyString {
  // Only update if we have digits OR the original was already empty
  input.onChange(filteredValue->Identity.stringToFormReactEvent)
}

Or move the BIN length validation outside the empty check in validateForm.

input.onChange(filteredValue->Identity.stringToFormReactEvent)
}
<TextInputAdapter
input={...input, onChange: handleChange} placeholder autoComplete="off" maxLength=20

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💡 Identity conversion obscures intent

The ->Identity.stringToFormReactEvent conversion wraps a string in a fake event structure just to satisfy the type system. If input.onChange accepts the raw string in other contexts, consider passing filteredValue directly for clarity.

If the type system strictly requires an event-shaped object, add a brief inline comment explaining why this conversion is necessary.

}

valuesDict
->getOptionFloat((AcquirerFraudRate :> string))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 Validation only runs on non-empty strings

If the user types only non-digit characters, the filtered value becomes empty, and this length validation is skipped entirely. Combined with the requiredKeys check using getString (which returns "" for missing keys), this creates a gap where an all-non-digit input could slip through validation.

Consider consolidating the BIN presence and length validation into a single check:

let binStr = valuesDict->getString((AcquirerBin :> string), "")
switch binStr->String.length {
| 0 => setErr((AcquirerBin :> string), "This field is required")
| n if n < 4 || n > 20 => setErr((AcquirerBin :> string), "Acquirer BIN must be between 4 and 20 digits")
| _ => ()
}

@kanikabansal08

Copy link
Copy Markdown
Collaborator

I think the same sort of bug lives in sibling screens: AcquirerConfigSettings.res and AcquirerConfigSettingsRevamp can we check there as well?

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.

[BUG] Acquirer Bin field bug in payment settings

3 participants