Skip to content

Commit a0836a9

Browse files
authored
Merge pull request #52954 from callstack-internal/feat/step-3-logic
[NO QA] feat: Step 3 logic
2 parents 73f2714 + 937c8ec commit a0836a9

29 files changed

+680
-545
lines changed

src/CONST.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ const CONST = {
594594
ALLOWED_FILE_TYPES: ['pdf', 'jpg', 'jpeg', 'png'],
595595
FILE_LIMIT: 10,
596596
TOTAL_FILES_SIZE_LIMIT: 5242880,
597+
PURPOSE_OF_TRANSACTION_ID: 'Intercompany_Payment',
597598
STEP: {
598599
COUNTRY: 'CountryStep',
599600
BANK_INFO: 'BankInfoStep',
@@ -603,6 +604,15 @@ const CONST = {
603604
AGREEMENTS: 'AgreementsStep',
604605
FINISH: 'FinishStep',
605606
},
607+
BUSINESS_INFO_STEP: {
608+
PICKLIST: {
609+
ANNUAL_VOLUME_RANGE: 'AnnualVolumeRange',
610+
APPLICANT_TYPE: 'ApplicantType',
611+
NATURE_OF_BUSINESS: 'NatureOfBusiness',
612+
PURPOSE_OF_TRANSACTION: 'PurposeOfTransaction',
613+
TRADE_VOLUME_RANGE: 'TradeVolumeRange',
614+
},
615+
},
606616
BENEFICIAL_OWNER_INFO_STEP: {
607617
SUBSTEP: {
608618
IS_USER_BENEFICIAL_OWNER: 1,

src/ONYXKEYS.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,12 +461,15 @@ const ONYXKEYS = {
461461
/** The user's Concierge reportID */
462462
CONCIERGE_REPORT_ID: 'conciergeReportID',
463463

464-
/* Corpay fieds to be used in the bank account creation setup */
464+
/** Corpay fields to be used in the bank account creation setup */
465465
CORPAY_FIELDS: 'corpayFields',
466466

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

470+
/** Corpay onboarding fields used in steps 3-5 in the global reimbursements */
471+
CORPAY_ONBOARDING_FIELDS: 'corpayOnboardingFields',
472+
470473
/** Collection Keys */
471474
COLLECTION: {
472475
DOWNLOAD: 'download_',
@@ -1048,6 +1051,7 @@ type OnyxValuesMapping = {
10481051
[ONYXKEYS.CORPAY_FIELDS]: OnyxTypes.CorpayFields;
10491052
[ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session;
10501053
[ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining;
1054+
[ONYXKEYS.CORPAY_ONBOARDING_FIELDS]: OnyxTypes.CorpayOnboardingFields;
10511055
};
10521056
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
10531057

src/languages/en.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,11 +2253,14 @@ const translations = {
22532253
whatsTheBusinessAddress: "What's the business address?",
22542254
whatsTheBusinessContactInformation: "What's the business contact information?",
22552255
whatsTheBusinessRegistrationNumber: "What's the business registration number?",
2256+
whatsTheBusinessTaxIDEIN: "What's the business tax ID/EIN/VAT/GST registration number?",
22562257
whatsThisNumber: "What's this number?",
22572258
whereWasTheBusinessIncorporated: 'Where was the business incorporated?',
22582259
whatTypeOfBusinessIsIt: 'What type of business is it?',
22592260
whatsTheBusinessAnnualPayment: "What's the business's annual payment volume?",
2261+
whatsYourExpectedAverageReimbursements: "What's your expected average reimbursement amount?",
22602262
registrationNumber: 'Registration number',
2263+
taxIDEIN: 'Tax ID/EIN number',
22612264
businessAddress: 'Business address',
22622265
businessType: 'Business type',
22632266
incorporation: 'Incorporation',
@@ -2266,15 +2269,22 @@ const translations = {
22662269
businessCategory: 'Business category',
22672270
annualPaymentVolume: 'Annual payment volume',
22682271
annualPaymentVolumeInCurrency: ({currencyCode}: CurrencyCodeParams) => `Annual payment volume in ${currencyCode}`,
2272+
averageReimbursementAmount: 'Average reimbursement amount',
2273+
averageReimbursementAmountInCurrency: ({currencyCode}: CurrencyCodeParams) => `Average reimbursement amount in ${currencyCode}`,
22692274
selectIncorporationType: 'Select incorporation type',
22702275
selectBusinessCategory: 'Select business category',
22712276
selectAnnualPaymentVolume: 'Select annual payment volume',
22722277
selectIncorporationCountry: 'Select incorporation country',
22732278
selectIncorporationState: 'Select incorporation state',
2279+
selectAverageReimbursement: 'Select average reimbursement amount',
22742280
findIncorporationType: 'Find incorporation type',
22752281
findBusinessCategory: 'Find business category',
22762282
findAnnualPaymentVolume: 'Find annual payment volume',
22772283
findIncorporationState: 'Find incorporation state',
2284+
findAverageReimbursement: 'Find average reimbursement amount',
2285+
error: {
2286+
registrationNumber: 'Please provide a valid registration number.',
2287+
},
22782288
},
22792289
beneficialOwnerInfoStep: {
22802290
doYouOwn25percent: 'Do you own 25% or more of',

src/languages/es.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2277,11 +2277,14 @@ const translations = {
22772277
whatsTheBusinessAddress: '¿Cuál es la dirección de la empresa?',
22782278
whatsTheBusinessContactInformation: '¿Cuál es la información de contacto de la empresa?',
22792279
whatsTheBusinessRegistrationNumber: '¿Cuál es el número de registro de la empresa?',
2280+
whatsTheBusinessTaxIDEIN: '¿Cuál es el número de identificación fiscal ID/EIN/VAT/GST de la empresa?',
22802281
whatsThisNumber: '¿Qué es este número?',
22812282
whereWasTheBusinessIncorporated: '¿Dónde se constituyó la empresa?',
22822283
whatTypeOfBusinessIsIt: '¿Qué tipo de empresa es?',
22832284
whatsTheBusinessAnnualPayment: '¿Cuál es el volumen anual de pagos de la empresa?',
2285+
whatsYourExpectedAverageReimbursements: '¿Cuál es el monto promedio esperado de reembolso?',
22842286
registrationNumber: 'Número de registro',
2287+
taxIDEIN: 'Número de identificación fiscal/EIN',
22852288
businessAddress: 'Dirección de la empresa',
22862289
businessType: 'Tipo de empresa',
22872290
incorporation: 'Constitución',
@@ -2290,15 +2293,22 @@ const translations = {
22902293
businessCategory: 'Categoría de la empresa',
22912294
annualPaymentVolume: 'Volumen anual de pagos',
22922295
annualPaymentVolumeInCurrency: ({currencyCode}: CurrencyCodeParams) => `Volumen anual de pagos en ${currencyCode}`,
2296+
averageReimbursementAmount: 'Monto promedio de reembolso',
2297+
averageReimbursementAmountInCurrency: ({currencyCode}: CurrencyCodeParams) => `Monto promedio de reembolso en ${currencyCode}`,
22932298
selectIncorporationType: 'Seleccione tipo de constitución',
22942299
selectBusinessCategory: 'Seleccione categoría de la empresa',
22952300
selectAnnualPaymentVolume: 'Seleccione volumen anual de pagos',
22962301
selectIncorporationCountry: 'Seleccione país de constitución',
22972302
selectIncorporationState: 'Seleccione estado de constitución',
2303+
selectAverageReimbursement: 'Selecciona el monto promedio de reembolso',
22982304
findIncorporationType: 'Buscar tipo de constitución',
22992305
findBusinessCategory: 'Buscar categoría de la empresa',
23002306
findAnnualPaymentVolume: 'Buscar volumen anual de pagos',
23012307
findIncorporationState: 'Buscar estado de constitución',
2308+
findAverageReimbursement: 'Encuentra el monto promedio de reembolso',
2309+
error: {
2310+
registrationNumber: 'Por favor, proporciona un número de registro válido.',
2311+
},
23022312
},
23032313
beneficialOwnerInfoStep: {
23042314
doYouOwn25percent: '¿Posees el 25% o más de',
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type {Country} from '@src/CONST';
2+
3+
type GetCorpayOnboardingFieldsParams = {
4+
countryISO: Country | '';
5+
};
6+
7+
export default GetCorpayOnboardingFieldsParams;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type CONST from '@src/CONST';
2+
3+
type SaveCorpayOnboardingCompanyDetails = {
4+
annualVolume: string;
5+
applicantTypeId: string;
6+
companyName: string;
7+
companyStreetAddress: string;
8+
companyCity: string;
9+
companyState?: string;
10+
companyPostalCode: string;
11+
companyCountryCode: string;
12+
currencyNeeded: string;
13+
businessContactNumber: string;
14+
businessConfirmationEmail: string;
15+
businessRegistrationIncorporationNumber: string;
16+
formationIncorporationCountryCode: string;
17+
formationIncorporationState?: string;
18+
fundDestinationCountries: string;
19+
fundSourceCountries: string;
20+
natureOfBusiness: string;
21+
purposeOfTransactionId: typeof CONST.NON_USD_BANK_ACCOUNT.PURPOSE_OF_TRANSACTION_ID;
22+
tradeVolume: string;
23+
taxIDEINNumber: string;
24+
};
25+
26+
type SaveCorpayOnboardingCompanyDetailsParams = {
27+
inputs: string;
28+
bankAccountID: number;
29+
};
30+
31+
export type {SaveCorpayOnboardingCompanyDetails, SaveCorpayOnboardingCompanyDetailsParams};

src/libs/API/parameters/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,5 @@ export type {default as DismissProductTrainingParams} from './DismissProductTrai
363363
export type {default as OpenWorkspacePlanPageParams} from './OpenWorkspacePlanPage';
364364
export type {default as ResetSMSDeliveryFailureStatusParams} from './ResetSMSDeliveryFailureStatusParams';
365365
export type {default as CreatePerDiemRequestParams} from './CreatePerDiemRequestParams';
366+
export type {default as GetCorpayOnboardingFieldsParams} from './GetCorpayOnboardingFieldsParams';
367+
export type {SaveCorpayOnboardingCompanyDetailsParams} from './SaveCorpayOnboardingCompanyDetailsParams';

src/libs/API/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ const WRITE_COMMANDS = {
448448
VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'ValidateUserAndGetAccessiblePolicies',
449449
DISMISS_PRODUCT_TRAINING: 'DismissProductTraining',
450450
RESET_SMS_DELIVERY_FAILURE_STATUS: 'ResetSMSDeliveryFailureStatus',
451+
SAVE_CORPAY_ONBOARDING_COMPANY_DETAILS: 'SaveCorpayOnboardingCompanyDetails',
451452
} as const;
452453

453454
type WriteCommand = ValueOf<typeof WRITE_COMMANDS>;
@@ -776,6 +777,7 @@ type WriteCommandParameters = {
776777
[WRITE_COMMANDS.REQUEST_TAX_EXEMPTION]: null;
777778
[WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT]: Parameters.UpdateWorkspaceCustomUnitParams;
778779
[WRITE_COMMANDS.RESET_SMS_DELIVERY_FAILURE_STATUS]: Parameters.ResetSMSDeliveryFailureStatusParams;
780+
[WRITE_COMMANDS.SAVE_CORPAY_ONBOARDING_COMPANY_DETAILS]: Parameters.SaveCorpayOnboardingCompanyDetailsParams;
779781

780782
[WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams;
781783
[WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams;
@@ -971,6 +973,7 @@ const READ_COMMANDS = {
971973
OPEN_CARD_DETAILS_PAGE: 'OpenCardDetailsPage',
972974
GET_ASSIGNED_SUPPORT_DATA: 'GetAssignedSupportData',
973975
OPEN_WORKSPACE_PLAN_PAGE: 'OpenWorkspacePlanPage',
976+
GET_CORPAY_ONBOARDING_FIELDS: 'GetCorpayOnboardingFields',
974977
} as const;
975978

976979
type ReadCommand = ValueOf<typeof READ_COMMANDS>;
@@ -1037,6 +1040,7 @@ type ReadCommandParameters = {
10371040
[READ_COMMANDS.OPEN_CARD_DETAILS_PAGE]: Parameters.OpenCardDetailsPageParams;
10381041
[READ_COMMANDS.GET_ASSIGNED_SUPPORT_DATA]: Parameters.GetAssignedSupportDataParams;
10391042
[READ_COMMANDS.OPEN_WORKSPACE_PLAN_PAGE]: Parameters.OpenWorkspacePlanPageParams;
1043+
[READ_COMMANDS.GET_CORPAY_ONBOARDING_FIELDS]: Parameters.GetCorpayOnboardingFieldsParams;
10401044
};
10411045

10421046
const SIDE_EFFECT_REQUEST_COMMANDS = {

src/libs/ValidationUtils.ts

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import isObject from 'lodash/isObject';
55
import type {OnyxCollection} from 'react-native-onyx';
66
import type {FormInputErrors, FormOnyxKeys, FormOnyxValues, FormValue} from '@components/Form/types';
77
import CONST from '@src/CONST';
8+
import type {Country} from '@src/CONST';
89
import type {OnyxFormKey} from '@src/ONYXKEYS';
910
import type {Report, TaxRates} from '@src/types/onyx';
10-
import * as CardUtils from './CardUtils';
11+
import {getMonthFromExpirationDateString, getYearFromExpirationDateString} from './CardUtils';
1112
import DateUtils from './DateUtils';
12-
import * as Localize from './Localize';
13-
import * as LoginUtils from './LoginUtils';
13+
import {translateLocal} from './Localize';
14+
import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from './LoginUtils';
1415
import {parsePhoneNumber} from './PhoneNumber';
1516
import StringUtils from './StringUtils';
1617

@@ -118,7 +119,7 @@ function getFieldRequiredErrors<TFormID extends OnyxFormKey>(values: FormOnyxVal
118119
return;
119120
}
120121

121-
errors[fieldKey] = Localize.translateLocal('common.error.fieldRequired');
122+
errors[fieldKey] = translateLocal('common.error.fieldRequired');
122123
});
123124

124125
return errors;
@@ -137,7 +138,7 @@ function isValidExpirationDate(string: string): boolean {
137138
}
138139

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

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

204205
if (!isValid(testDate)) {
205-
return Localize.translateLocal('common.error.dateInvalid');
206+
return translateLocal('common.error.dateInvalid');
206207
}
207208

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

215216
if (isSameDay(testDate, maximalDate) || isAfter(testDate, maximalDate)) {
216-
return Localize.translateLocal('privatePersonalDetails.error.dateShouldBeBefore', {dateString: format(maximalDate, CONST.DATE.FNS_FORMAT_STRING)});
217+
return translateLocal('privatePersonalDetails.error.dateShouldBeBefore', {dateString: format(maximalDate, CONST.DATE.FNS_FORMAT_STRING)});
217218
}
218219

219-
return Localize.translateLocal('privatePersonalDetails.error.dateShouldBeAfter', {dateString: format(minimalDate, CONST.DATE.FNS_FORMAT_STRING)});
220+
return translateLocal('privatePersonalDetails.error.dateShouldBeAfter', {dateString: format(minimalDate, CONST.DATE.FNS_FORMAT_STRING)});
220221
}
221222

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

229230
// If input date is not valid, return an error
230231
if (!isValid(parsedDate)) {
231-
return Localize.translateLocal('common.error.dateInvalid');
232+
return translateLocal('common.error.dateInvalid');
232233
}
233234

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

237238
if (parsedDate < currentDate) {
238-
return Localize.translateLocal('common.error.dateInvalid');
239+
return translateLocal('common.error.dateInvalid');
239240
}
240241

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

324325
/**
@@ -515,7 +516,7 @@ function isValidEmail(email: string): boolean {
515516
* @param phoneNumber
516517
*/
517518
function isValidPhoneInternational(phoneNumber: string): boolean {
518-
const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumber);
519+
const phoneNumberWithCountryCode = appendCountryCode(phoneNumber);
519520
const parsedPhoneNumber = parsePhoneNumber(phoneNumberWithCountryCode);
520521

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

558+
/**
559+
* Validates the given value if it is correct ABN number - https://abr.business.gov.au/Help/AbnFormat
560+
* @param registrationNumber - number to validate.
561+
*/
562+
function isValidABN(registrationNumber: string): boolean {
563+
const cleanedAbn: string = registrationNumber.replaceAll(/[ _]/g, '');
564+
if (cleanedAbn.length !== 11) {
565+
return false;
566+
}
567+
568+
const weights: number[] = [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
569+
const checksum: number = [...cleanedAbn].reduce((total: number, char: string, index: number) => {
570+
let digit = Number(char);
571+
if (index === 0) {
572+
digit--;
573+
} // First digit special rule
574+
return total + digit * (weights.at(index) ?? 0); // Using optional chaining for safety
575+
}, 0);
576+
577+
return checksum % 89 === 0;
578+
}
579+
580+
/**
581+
* 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/
582+
* @param registrationNumber - number to validate.
583+
*/
584+
function isValidACN(registrationNumber: string): boolean {
585+
const cleanedAcn: string = registrationNumber.replaceAll(/\s|-/g, '');
586+
if (cleanedAcn.length !== 9 || Number.isNaN(Number(cleanedAcn))) {
587+
return false;
588+
}
589+
590+
const weights: number[] = [8, 7, 6, 5, 4, 3, 2, 1];
591+
const tally: number = weights.reduce((total: number, weight: number, index: number) => {
592+
return total + Number(cleanedAcn[index]) * weight;
593+
}, 0);
594+
595+
const checkDigit: number = 10 - (tally % 10);
596+
return checkDigit === Number(cleanedAcn[8]) || (checkDigit === 10 && Number(cleanedAcn[8]) === 0);
597+
}
598+
599+
/**
600+
* Validates the given value if it is correct australian registration number.
601+
* @param registrationNumber
602+
*/
603+
function isValidAURegistrationNumber(registrationNumber: string): boolean {
604+
return isValidABN(registrationNumber) || isValidACN(registrationNumber);
605+
}
606+
607+
/**
608+
* Validates the given value if it is correct british registration number.
609+
* @param registrationNumber
610+
*/
611+
function isValidGBRegistrationNumber(registrationNumber: string): boolean {
612+
return /^(?:\d{8}|[A-Z]{2}\d{6})$/.test(registrationNumber);
613+
}
614+
615+
/**
616+
* Validates the given value if it is correct canadian registration number.
617+
* @param registrationNumber
618+
*/
619+
function isValidCARegistrationNumber(registrationNumber: string): boolean {
620+
return /^\d{9}(?:[A-Z]{2}\d{4})?$/.test(registrationNumber);
621+
}
622+
623+
/**
624+
* Validates the given value if it is correct registration number for the given country.
625+
* @param registrationNumber
626+
* @param country
627+
*/
628+
function isValidRegistrationNumber(registrationNumber: string, country: Country | '') {
629+
switch (country) {
630+
case CONST.COUNTRY.AU:
631+
return isValidAURegistrationNumber(registrationNumber);
632+
case CONST.COUNTRY.GB:
633+
return isValidGBRegistrationNumber(registrationNumber);
634+
case CONST.COUNTRY.CA:
635+
return isValidCARegistrationNumber(registrationNumber);
636+
case CONST.COUNTRY.US:
637+
return isValidTaxID(registrationNumber);
638+
default:
639+
return true;
640+
}
641+
}
642+
557643
export {
558644
meetsMinimumAgeRequirement,
559645
meetsMaximumAgeRequirement,
@@ -602,4 +688,5 @@ export {
602688
isValidPhoneInternational,
603689
isValidZipCodeInternational,
604690
isValidOwnershipPercentage,
691+
isValidRegistrationNumber,
605692
};

0 commit comments

Comments
 (0)