-
Notifications
You must be signed in to change notification settings - Fork 93
fix: prevent IEEE 754 precision loss for large acquirer BIN values #4939
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,16 @@ let binField = makeFieldInfo( | |
| ~label="Acquirer BIN", | ||
| ~name="acquirer_bin", | ||
| ~placeholder="e.g. 56688", | ||
| ~customInput=InputFields.numericTextInput(~removeLeadingZeroes=true, ~maxLength=20, ~precision=0), | ||
| ~customInput=(~input: ReactFinalForm.fieldRenderPropsInput, ~placeholder) => { | ||
| let handleChange = event => { | ||
| let value = ReactEvent.Form.target(event)["value"] | ||
| let filteredValue = value->String.replaceRegExp(%re("/[^0-9]/g"), "") | ||
| input.onChange(filteredValue->Identity.stringToFormReactEvent) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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:
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 |
||
| } | ||
| <TextInputAdapter | ||
| input={...input, onChange: handleChange} placeholder autoComplete="off" maxLength=20 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Identity conversion obscures intent The If the type system strictly requires an event-shaped object, add a brief inline comment explaining why this conversion is necessary. |
||
| /> | ||
| }, | ||
| ~isRequired=true, | ||
| ) | ||
|
|
||
|
|
@@ -120,7 +129,7 @@ let stampProfileId = (body: Dict.t<JSON.t>, ~profileId: string) => { | |
| } | ||
|
|
||
| let normalizeNumericStringFields = (body: Dict.t<JSON.t>) => { | ||
| [AcquirerBin, AcquirerIca]->Array.forEach(field => { | ||
| [AcquirerIca]->Array.forEach(field => { | ||
| let key = (field :> string) | ||
| let value = body->getFloat(key, 0.0) | ||
| if value > 0.0 { | ||
|
|
@@ -137,23 +146,18 @@ let validateForm = (~requiredKeys: array<acquirerField>, values: JSON.t): JSON.t | |
|
|
||
| requiredKeys->Array.forEach(field => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The validation change from
This change is likely safe given the text input, but confirm the form state initialization sets an empty string default for |
||
| let key = (field :> string) | ||
| let present = switch field { | ||
| | AcquirerBin => valuesDict->getOptionFloat(key)->Option.isSome | ||
| | _ => valuesDict->getString(key, "")->isNonEmptyString | ||
| } | ||
| let present = valuesDict->getString(key, "")->isNonEmptyString | ||
| if !present { | ||
| setErr(key, "This field is required") | ||
| } | ||
| }) | ||
|
|
||
| valuesDict | ||
| ->getOptionFloat((AcquirerBin :> string)) | ||
| ->mapOptionOrDefault((), binFloat => { | ||
| let binStr = binFloat->Float.toString | ||
| let binStr = valuesDict->getString((AcquirerBin :> string), "") | ||
| if binStr->isNonEmptyString { | ||
| if binStr->String.length < 4 || binStr->String.length > 20 { | ||
| setErr((AcquirerBin :> string), "Acquirer BIN must be between 4 and 20 digits") | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| valuesDict | ||
| ->getOptionFloat((AcquirerFraudRate :> string)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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")
| _ => ()
} |
||
|
|
||
There was a problem hiding this comment.
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:Or if regex is preferred, lift it to module scope to avoid reallocation.