diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index b33b9eeb..2f110e82 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -412,6 +412,9 @@ export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) => state: hasProvince ? province : state, }; + const idNumber = + country === 'US' ? { ssn_last_4: personalIdNumber } : { personal_id_number: personalIdNumber }; + // Params for Stripe SDK const params = { legal_entity: { @@ -420,7 +423,7 @@ export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) => address: omitBy(address, isUndefined), dob: birthDate, type: 'individual', - personal_id_number: personalIdNumber, + ...idNumber, }, tos_shown_and_accepted: true, }; diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsForm.js b/src/forms/PayoutDetailsForm/PayoutDetailsForm.js index eb163bcc..2be17464 100644 --- a/src/forms/PayoutDetailsForm/PayoutDetailsForm.js +++ b/src/forms/PayoutDetailsForm/PayoutDetailsForm.js @@ -116,25 +116,47 @@ const PayoutDetailsFormComponent = props => ( const bankAccountRequired = validators.required(' '); const showPersonalIdNumber = - country && stripeCountryConfigs(country).personalIdNumberRequired; + (country && stripeCountryConfigs(country).personalIdNumberRequired) || + (country && stripeCountryConfigs(country).ssnLast4Required); - const personalIdNumberLabel = showPersonalIdNumber - ? intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberLabel.${country}`, + const personalIdNumberRequired = validators.required( + intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberRequired`, + }) + ); + + let personalIdNumberLabel = null; + let personalIdNumberPlaceholder = null; + let personalIdNumberValid = personalIdNumberRequired; + + if (country === 'US') { + personalIdNumberLabel = intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberLabel.US`, + }); + personalIdNumberPlaceholder = intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberPlaceholder.US`, + }); + + const validSSN = validators.validSsnLast4( + intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberValid`, }) - : null; - const personalIdNumberPlaceholder = showPersonalIdNumber - ? intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberPlaceholder.${country}`, + ); + personalIdNumberValid = validators.composeValidators(personalIdNumberRequired, validSSN); + } else if (country === 'HK') { + personalIdNumberLabel = intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberLabel.HK`, + }); + personalIdNumberPlaceholder = intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberPlaceholder.HK`, + }); + const validHKID = validators.validHKID( + intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberValid`, }) - : null; - const personalIdNumberRequired = showPersonalIdNumber - ? validators.required( - intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberRequired`, - }) - ) - : null; + ); + personalIdNumberValid = validators.composeValidators(personalIdNumberRequired, validHKID); + } const classes = classNames(css.root, className, { [css.disabled]: disabled, @@ -261,7 +283,7 @@ const PayoutDetailsFormComponent = props => ( type="text" label={personalIdNumberLabel} placeholder={personalIdNumberPlaceholder} - validate={personalIdNumberRequired} + validate={personalIdNumberValid} /> ) : null} diff --git a/src/translations/en.json b/src/translations/en.json index 519e548f..4f239168 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -503,6 +503,7 @@ "PayoutDetailsForm.personalIdNumberLabel.US": "Last 4 digits of social security number (SSN)", "PayoutDetailsForm.personalIdNumberPlaceholder.HK": "XY123456", "PayoutDetailsForm.personalIdNumberRequired": "This field is required", + "PayoutDetailsForm.personalIdNumberValid": "Invalid value", "PayoutDetailsForm.postalCodeLabel": "Postal code", "PayoutDetailsForm.postalCodePlaceholder": "00100", "PayoutDetailsForm.postalCodeRequired": "This field is required", diff --git a/src/util/validators.js b/src/util/validators.js index 07fe9660..2c6ecffa 100644 --- a/src/util/validators.js +++ b/src/util/validators.js @@ -136,5 +136,69 @@ export const ageAtLeast = (message, minYears) => value => { return message; }; +export const validSsnLast4 = message => value => { + return value.length === 4 ? VALID : message; +}; + +export const validHKID = message => value => { + // Accept value 000000000 for testing Stripe + if (value.length === 9 && value.match(/([0]{9})/)) { + return VALID; + } + + // HKID format example: AB364912(5) + // ID can start with one or two letters and the check digit in the end can be in brackets or not + if (value.length < 8) { + return message; + } + + // Handle possible brackets in value + if (value.charAt(value.length - 3) === '(' && value.charAt(value.length - 1) === ')') { + value = value.substring(0, value.length - 3) + value.charAt(value.length - 2); + } + value = value.toUpperCase(); + + // Check that pattern is correct and split value to array + const hkidPattern = /^([A-Z]{1,2})([0-9]{6})([A0-9])$/; + const matchArray = value.match(hkidPattern); + + if (!matchArray) { + return message; + } + + const charPart = matchArray[1]; + const numPart = matchArray[2]; + const checkDigit = matchArray[3]; + + // Calculate the checksum for character part. + // Transfer letters to numbers so that A=10, B=11, C=12 etc. + // If there is only one letter in the ID use 36 as the first value + // Total calculation is weighted so that 1st digit is x9, 2nd digit x8, 3rd digit x7 etc. + + const strValidChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + let checkSum = 0; + + if (charPart.length === 2) { + checkSum += 9 * (10 + strValidChars.indexOf(charPart.charAt(0))); + checkSum += 8 * (10 + strValidChars.indexOf(charPart.charAt(1))); + } else { + checkSum += 9 * 36; + checkSum += 8 * (10 + strValidChars.indexOf(charPart)); + } + + // Calculate the checksum for numeric part + + for (let i = 0, j = 7; i < numPart.length; i++, j--) { + checkSum += j * numPart.charAt(i); + } + + // Verify the check digit + const remaining = checkSum % 11; + const verify = remaining === 0 ? 0 : 11 - remaining; + const isValid = verify === checkDigit || (verify === 10 && checkDigit === 'A'); + + return isValid ? VALID : message; +}; + export const composeValidators = (...validators) => value => validators.reduce((error, validator) => error || validator(value), VALID);