Skip to content

Commit

Permalink
Merge pull request #52954 from callstack-internal/feat/step-3-logic
Browse files Browse the repository at this point in the history
[NO QA] feat: Step 3 logic
  • Loading branch information
madmax330 authored Jan 22, 2025
2 parents 73f2714 + 937c8ec commit a0836a9
Show file tree
Hide file tree
Showing 29 changed files with 680 additions and 545 deletions.
10 changes: 10 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ const CONST = {
ALLOWED_FILE_TYPES: ['pdf', 'jpg', 'jpeg', 'png'],
FILE_LIMIT: 10,
TOTAL_FILES_SIZE_LIMIT: 5242880,
PURPOSE_OF_TRANSACTION_ID: 'Intercompany_Payment',
STEP: {
COUNTRY: 'CountryStep',
BANK_INFO: 'BankInfoStep',
Expand All @@ -603,6 +604,15 @@ const CONST = {
AGREEMENTS: 'AgreementsStep',
FINISH: 'FinishStep',
},
BUSINESS_INFO_STEP: {
PICKLIST: {
ANNUAL_VOLUME_RANGE: 'AnnualVolumeRange',
APPLICANT_TYPE: 'ApplicantType',
NATURE_OF_BUSINESS: 'NatureOfBusiness',
PURPOSE_OF_TRANSACTION: 'PurposeOfTransaction',
TRADE_VOLUME_RANGE: 'TradeVolumeRange',
},
},
BENEFICIAL_OWNER_INFO_STEP: {
SUBSTEP: {
IS_USER_BENEFICIAL_OWNER: 1,
Expand Down
6 changes: 5 additions & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,12 +461,15 @@ const ONYXKEYS = {
/** The user's Concierge reportID */
CONCIERGE_REPORT_ID: 'conciergeReportID',

/* Corpay fieds to be used in the bank account creation setup */
/** Corpay fields to be used in the bank account creation setup */
CORPAY_FIELDS: 'corpayFields',

/** The user's session that will be preserved when using imported state */
PRESERVED_USER_SESSION: 'preservedUserSession',

/** Corpay onboarding fields used in steps 3-5 in the global reimbursements */
CORPAY_ONBOARDING_FIELDS: 'corpayOnboardingFields',

/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
Expand Down Expand Up @@ -1048,6 +1051,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.CORPAY_FIELDS]: OnyxTypes.CorpayFields;
[ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session;
[ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining;
[ONYXKEYS.CORPAY_ONBOARDING_FIELDS]: OnyxTypes.CorpayOnboardingFields;
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;

Expand Down
10 changes: 10 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2253,11 +2253,14 @@ const translations = {
whatsTheBusinessAddress: "What's the business address?",
whatsTheBusinessContactInformation: "What's the business contact information?",
whatsTheBusinessRegistrationNumber: "What's the business registration number?",
whatsTheBusinessTaxIDEIN: "What's the business tax ID/EIN/VAT/GST registration number?",
whatsThisNumber: "What's this number?",
whereWasTheBusinessIncorporated: 'Where was the business incorporated?',
whatTypeOfBusinessIsIt: 'What type of business is it?',
whatsTheBusinessAnnualPayment: "What's the business's annual payment volume?",
whatsYourExpectedAverageReimbursements: "What's your expected average reimbursement amount?",
registrationNumber: 'Registration number',
taxIDEIN: 'Tax ID/EIN number',
businessAddress: 'Business address',
businessType: 'Business type',
incorporation: 'Incorporation',
Expand All @@ -2266,15 +2269,22 @@ const translations = {
businessCategory: 'Business category',
annualPaymentVolume: 'Annual payment volume',
annualPaymentVolumeInCurrency: ({currencyCode}: CurrencyCodeParams) => `Annual payment volume in ${currencyCode}`,
averageReimbursementAmount: 'Average reimbursement amount',
averageReimbursementAmountInCurrency: ({currencyCode}: CurrencyCodeParams) => `Average reimbursement amount in ${currencyCode}`,
selectIncorporationType: 'Select incorporation type',
selectBusinessCategory: 'Select business category',
selectAnnualPaymentVolume: 'Select annual payment volume',
selectIncorporationCountry: 'Select incorporation country',
selectIncorporationState: 'Select incorporation state',
selectAverageReimbursement: 'Select average reimbursement amount',
findIncorporationType: 'Find incorporation type',
findBusinessCategory: 'Find business category',
findAnnualPaymentVolume: 'Find annual payment volume',
findIncorporationState: 'Find incorporation state',
findAverageReimbursement: 'Find average reimbursement amount',
error: {
registrationNumber: 'Please provide a valid registration number.',
},
},
beneficialOwnerInfoStep: {
doYouOwn25percent: 'Do you own 25% or more of',
Expand Down
10 changes: 10 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2277,11 +2277,14 @@ const translations = {
whatsTheBusinessAddress: '¿Cuál es la dirección de la empresa?',
whatsTheBusinessContactInformation: '¿Cuál es la información de contacto de la empresa?',
whatsTheBusinessRegistrationNumber: '¿Cuál es el número de registro de la empresa?',
whatsTheBusinessTaxIDEIN: '¿Cuál es el número de identificación fiscal ID/EIN/VAT/GST de la empresa?',
whatsThisNumber: '¿Qué es este número?',
whereWasTheBusinessIncorporated: '¿Dónde se constituyó la empresa?',
whatTypeOfBusinessIsIt: '¿Qué tipo de empresa es?',
whatsTheBusinessAnnualPayment: '¿Cuál es el volumen anual de pagos de la empresa?',
whatsYourExpectedAverageReimbursements: '¿Cuál es el monto promedio esperado de reembolso?',
registrationNumber: 'Número de registro',
taxIDEIN: 'Número de identificación fiscal/EIN',
businessAddress: 'Dirección de la empresa',
businessType: 'Tipo de empresa',
incorporation: 'Constitución',
Expand All @@ -2290,15 +2293,22 @@ const translations = {
businessCategory: 'Categoría de la empresa',
annualPaymentVolume: 'Volumen anual de pagos',
annualPaymentVolumeInCurrency: ({currencyCode}: CurrencyCodeParams) => `Volumen anual de pagos en ${currencyCode}`,
averageReimbursementAmount: 'Monto promedio de reembolso',
averageReimbursementAmountInCurrency: ({currencyCode}: CurrencyCodeParams) => `Monto promedio de reembolso en ${currencyCode}`,
selectIncorporationType: 'Seleccione tipo de constitución',
selectBusinessCategory: 'Seleccione categoría de la empresa',
selectAnnualPaymentVolume: 'Seleccione volumen anual de pagos',
selectIncorporationCountry: 'Seleccione país de constitución',
selectIncorporationState: 'Seleccione estado de constitución',
selectAverageReimbursement: 'Selecciona el monto promedio de reembolso',
findIncorporationType: 'Buscar tipo de constitución',
findBusinessCategory: 'Buscar categoría de la empresa',
findAnnualPaymentVolume: 'Buscar volumen anual de pagos',
findIncorporationState: 'Buscar estado de constitución',
findAverageReimbursement: 'Encuentra el monto promedio de reembolso',
error: {
registrationNumber: 'Por favor, proporciona un número de registro válido.',
},
},
beneficialOwnerInfoStep: {
doYouOwn25percent: '¿Posees el 25% o más de',
Expand Down
7 changes: 7 additions & 0 deletions src/libs/API/parameters/GetCorpayOnboardingFieldsParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type {Country} from '@src/CONST';

type GetCorpayOnboardingFieldsParams = {
countryISO: Country | '';
};

export default GetCorpayOnboardingFieldsParams;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type CONST from '@src/CONST';

type SaveCorpayOnboardingCompanyDetails = {
annualVolume: string;
applicantTypeId: string;
companyName: string;
companyStreetAddress: string;
companyCity: string;
companyState?: string;
companyPostalCode: string;
companyCountryCode: string;
currencyNeeded: string;
businessContactNumber: string;
businessConfirmationEmail: string;
businessRegistrationIncorporationNumber: string;
formationIncorporationCountryCode: string;
formationIncorporationState?: string;
fundDestinationCountries: string;
fundSourceCountries: string;
natureOfBusiness: string;
purposeOfTransactionId: typeof CONST.NON_USD_BANK_ACCOUNT.PURPOSE_OF_TRANSACTION_ID;
tradeVolume: string;
taxIDEINNumber: string;
};

type SaveCorpayOnboardingCompanyDetailsParams = {
inputs: string;
bankAccountID: number;
};

export type {SaveCorpayOnboardingCompanyDetails, SaveCorpayOnboardingCompanyDetailsParams};
2 changes: 2 additions & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,5 @@ export type {default as DismissProductTrainingParams} from './DismissProductTrai
export type {default as OpenWorkspacePlanPageParams} from './OpenWorkspacePlanPage';
export type {default as ResetSMSDeliveryFailureStatusParams} from './ResetSMSDeliveryFailureStatusParams';
export type {default as CreatePerDiemRequestParams} from './CreatePerDiemRequestParams';
export type {default as GetCorpayOnboardingFieldsParams} from './GetCorpayOnboardingFieldsParams';
export type {SaveCorpayOnboardingCompanyDetailsParams} from './SaveCorpayOnboardingCompanyDetailsParams';
4 changes: 4 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ const WRITE_COMMANDS = {
VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'ValidateUserAndGetAccessiblePolicies',
DISMISS_PRODUCT_TRAINING: 'DismissProductTraining',
RESET_SMS_DELIVERY_FAILURE_STATUS: 'ResetSMSDeliveryFailureStatus',
SAVE_CORPAY_ONBOARDING_COMPANY_DETAILS: 'SaveCorpayOnboardingCompanyDetails',
} as const;

type WriteCommand = ValueOf<typeof WRITE_COMMANDS>;
Expand Down Expand Up @@ -776,6 +777,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.REQUEST_TAX_EXEMPTION]: null;
[WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT]: Parameters.UpdateWorkspaceCustomUnitParams;
[WRITE_COMMANDS.RESET_SMS_DELIVERY_FAILURE_STATUS]: Parameters.ResetSMSDeliveryFailureStatusParams;
[WRITE_COMMANDS.SAVE_CORPAY_ONBOARDING_COMPANY_DETAILS]: Parameters.SaveCorpayOnboardingCompanyDetailsParams;

[WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams;
[WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams;
Expand Down Expand Up @@ -971,6 +973,7 @@ const READ_COMMANDS = {
OPEN_CARD_DETAILS_PAGE: 'OpenCardDetailsPage',
GET_ASSIGNED_SUPPORT_DATA: 'GetAssignedSupportData',
OPEN_WORKSPACE_PLAN_PAGE: 'OpenWorkspacePlanPage',
GET_CORPAY_ONBOARDING_FIELDS: 'GetCorpayOnboardingFields',
} as const;

type ReadCommand = ValueOf<typeof READ_COMMANDS>;
Expand Down Expand Up @@ -1037,6 +1040,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.OPEN_CARD_DETAILS_PAGE]: Parameters.OpenCardDetailsPageParams;
[READ_COMMANDS.GET_ASSIGNED_SUPPORT_DATA]: Parameters.GetAssignedSupportDataParams;
[READ_COMMANDS.OPEN_WORKSPACE_PLAN_PAGE]: Parameters.OpenWorkspacePlanPageParams;
[READ_COMMANDS.GET_CORPAY_ONBOARDING_FIELDS]: Parameters.GetCorpayOnboardingFieldsParams;
};

const SIDE_EFFECT_REQUEST_COMMANDS = {
Expand Down
111 changes: 99 additions & 12 deletions src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import isObject from 'lodash/isObject';
import type {OnyxCollection} from 'react-native-onyx';
import type {FormInputErrors, FormOnyxKeys, FormOnyxValues, FormValue} from '@components/Form/types';
import CONST from '@src/CONST';
import type {Country} from '@src/CONST';
import type {OnyxFormKey} from '@src/ONYXKEYS';
import type {Report, TaxRates} from '@src/types/onyx';
import * as CardUtils from './CardUtils';
import {getMonthFromExpirationDateString, getYearFromExpirationDateString} from './CardUtils';
import DateUtils from './DateUtils';
import * as Localize from './Localize';
import * as LoginUtils from './LoginUtils';
import {translateLocal} from './Localize';
import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from './LoginUtils';
import {parsePhoneNumber} from './PhoneNumber';
import StringUtils from './StringUtils';

Expand Down Expand Up @@ -118,7 +119,7 @@ function getFieldRequiredErrors<TFormID extends OnyxFormKey>(values: FormOnyxVal
return;
}

errors[fieldKey] = Localize.translateLocal('common.error.fieldRequired');
errors[fieldKey] = translateLocal('common.error.fieldRequired');
});

return errors;
Expand All @@ -137,7 +138,7 @@ function isValidExpirationDate(string: string): boolean {
}

// Use the last of the month to check if the expiration date is in the future or not
const expirationDate = `${CardUtils.getYearFromExpirationDateString(string)}-${CardUtils.getMonthFromExpirationDateString(string)}-01`;
const expirationDate = `${getYearFromExpirationDateString(string)}-${getMonthFromExpirationDateString(string)}-01`;
return isAfter(new Date(expirationDate), endOfMonth(new Date()));
}

Expand Down Expand Up @@ -202,7 +203,7 @@ function getAgeRequirementError(date: string, minimumAge: number, maximumAge: nu
const testDate = parse(date, CONST.DATE.FNS_FORMAT_STRING, currentDate);

if (!isValid(testDate)) {
return Localize.translateLocal('common.error.dateInvalid');
return translateLocal('common.error.dateInvalid');
}

const maximalDate = subYears(currentDate, minimumAge);
Expand All @@ -213,10 +214,10 @@ function getAgeRequirementError(date: string, minimumAge: number, maximumAge: nu
}

if (isSameDay(testDate, maximalDate) || isAfter(testDate, maximalDate)) {
return Localize.translateLocal('privatePersonalDetails.error.dateShouldBeBefore', {dateString: format(maximalDate, CONST.DATE.FNS_FORMAT_STRING)});
return translateLocal('privatePersonalDetails.error.dateShouldBeBefore', {dateString: format(maximalDate, CONST.DATE.FNS_FORMAT_STRING)});
}

return Localize.translateLocal('privatePersonalDetails.error.dateShouldBeAfter', {dateString: format(minimalDate, CONST.DATE.FNS_FORMAT_STRING)});
return translateLocal('privatePersonalDetails.error.dateShouldBeAfter', {dateString: format(minimalDate, CONST.DATE.FNS_FORMAT_STRING)});
}

/**
Expand All @@ -228,14 +229,14 @@ function getDatePassedError(inputDate: string): string {

// If input date is not valid, return an error
if (!isValid(parsedDate)) {
return Localize.translateLocal('common.error.dateInvalid');
return translateLocal('common.error.dateInvalid');
}

// Clear time for currentDate so comparison is based solely on the date
currentDate.setHours(0, 0, 0, 0);

if (parsedDate < currentDate) {
return Localize.translateLocal('common.error.dateInvalid');
return translateLocal('common.error.dateInvalid');
}

return '';
Expand Down Expand Up @@ -318,7 +319,7 @@ function isValidTwoFactorCode(code: string): boolean {
* Checks whether a value is a numeric string including `(`, `)`, `-` and optional leading `+`
*/
function isNumericWithSpecialChars(input: string): boolean {
return /^\+?[\d\\+]*$/.test(LoginUtils.getPhoneNumberWithoutSpecialChars(input));
return /^\+?[\d\\+]*$/.test(getPhoneNumberWithoutSpecialChars(input));
}

/**
Expand Down Expand Up @@ -515,7 +516,7 @@ function isValidEmail(email: string): boolean {
* @param phoneNumber
*/
function isValidPhoneInternational(phoneNumber: string): boolean {
const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumber);
const phoneNumberWithCountryCode = appendCountryCode(phoneNumber);
const parsedPhoneNumber = parsePhoneNumber(phoneNumberWithCountryCode);

return parsedPhoneNumber.possible && Str.isValidE164Phone(parsedPhoneNumber.number?.e164 ?? '');
Expand Down Expand Up @@ -554,6 +555,91 @@ function isValidOwnershipPercentage(value: string, totalOwnedPercentage: Record<
return isValidNumber && isTotalSumValid;
}

/**
* Validates the given value if it is correct ABN number - https://abr.business.gov.au/Help/AbnFormat
* @param registrationNumber - number to validate.
*/
function isValidABN(registrationNumber: string): boolean {
const cleanedAbn: string = registrationNumber.replaceAll(/[ _]/g, '');
if (cleanedAbn.length !== 11) {
return false;
}

const weights: number[] = [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
const checksum: number = [...cleanedAbn].reduce((total: number, char: string, index: number) => {
let digit = Number(char);
if (index === 0) {
digit--;
} // First digit special rule
return total + digit * (weights.at(index) ?? 0); // Using optional chaining for safety
}, 0);

return checksum % 89 === 0;
}

/**
* Validates the given value if it is correct ACN number - https://asic.gov.au/for-business/registering-a-company/steps-to-register-a-company/australian-company-numbers/australian-company-number-digit-check/
* @param registrationNumber - number to validate.
*/
function isValidACN(registrationNumber: string): boolean {
const cleanedAcn: string = registrationNumber.replaceAll(/\s|-/g, '');
if (cleanedAcn.length !== 9 || Number.isNaN(Number(cleanedAcn))) {
return false;
}

const weights: number[] = [8, 7, 6, 5, 4, 3, 2, 1];
const tally: number = weights.reduce((total: number, weight: number, index: number) => {
return total + Number(cleanedAcn[index]) * weight;
}, 0);

const checkDigit: number = 10 - (tally % 10);
return checkDigit === Number(cleanedAcn[8]) || (checkDigit === 10 && Number(cleanedAcn[8]) === 0);
}

/**
* Validates the given value if it is correct australian registration number.
* @param registrationNumber
*/
function isValidAURegistrationNumber(registrationNumber: string): boolean {
return isValidABN(registrationNumber) || isValidACN(registrationNumber);
}

/**
* Validates the given value if it is correct british registration number.
* @param registrationNumber
*/
function isValidGBRegistrationNumber(registrationNumber: string): boolean {
return /^(?:\d{8}|[A-Z]{2}\d{6})$/.test(registrationNumber);
}

/**
* Validates the given value if it is correct canadian registration number.
* @param registrationNumber
*/
function isValidCARegistrationNumber(registrationNumber: string): boolean {
return /^\d{9}(?:[A-Z]{2}\d{4})?$/.test(registrationNumber);
}

/**
* Validates the given value if it is correct registration number for the given country.
* @param registrationNumber
* @param country
*/
function isValidRegistrationNumber(registrationNumber: string, country: Country | '') {
switch (country) {
case CONST.COUNTRY.AU:
return isValidAURegistrationNumber(registrationNumber);
case CONST.COUNTRY.GB:
return isValidGBRegistrationNumber(registrationNumber);
case CONST.COUNTRY.CA:
return isValidCARegistrationNumber(registrationNumber);
case CONST.COUNTRY.US:
return isValidTaxID(registrationNumber);
default:
return true;
}
}

export {
meetsMinimumAgeRequirement,
meetsMaximumAgeRequirement,
Expand Down Expand Up @@ -602,4 +688,5 @@ export {
isValidPhoneInternational,
isValidZipCodeInternational,
isValidOwnershipPercentage,
isValidRegistrationNumber,
};
Loading

0 comments on commit a0836a9

Please sign in to comment.