diff --git a/cspell.json b/cspell.json index ad423327603d..21ba56fc2bae 100644 --- a/cspell.json +++ b/cspell.json @@ -1052,7 +1052,8 @@ "modules/group-ib-fp", "web/snippets/gib.js", "tests/unit/hooks/useLetterAvatars.test.tsx", - "tests/unit/usePersonalDetailSearchSelectorTest.tsx" + "tests/unit/usePersonalDetailSearchSelectorTest.tsx", + "tests/unit/BankAccountUtilsTest.ts" ], "ignoreRegExpList": ["@assets/.*"], "useGitignore": true diff --git a/src/CONST/index.ts b/src/CONST/index.ts index c4b8f0f4677b..e933ff7b1732 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -666,6 +666,7 @@ const CONST = { ACH_CONTRACT: 'ACHContractStep', VALIDATION: 'ValidationStep', ENABLE: 'EnableStep', + KYB_DOCS: 'UploadKYBDocs', }, PAGE_NAMES: { COUNTRY: 'currency-and-country', @@ -677,6 +678,7 @@ const CONST = { ACH_CONTRACT: 'ach-contract', VALIDATION: 'validation', ENABLE: 'enable', + KYB_DOCS: 'upload-kyb-documents', }, STEP_NAMES: ['1', '2', '3', '4', '5', '6'], BANK_INFO_STEP: { @@ -777,6 +779,33 @@ const CONST = { BUSINESS: 'BUSINESS', PERSONAL: 'PERSONAL', }, + KYB_STATUS: { + PASS: 'pass', + }, + KYB_REQUESTOR_IDENTITY_ERROR: { + ADDRESS: [ + 'resultcode.address.does.not.match', + 'resultcode.street.name.does.not.match', + 'resultcode.street.number.does.not.match', + 'resultcode.zip.does.not.match', + 'resultcode.state.does.not.match', + 'resultcode.alternate.address.alert', + 'resultcode.input.address.is.po.box', + 'resultcode.located.address.is.po.box', + 'resultcode.warm.address.alert', + ], + DOB: [ + 'resultcode.coppa.alert', + 'resultcode.age.below.minimum', + 'resultcode.dob.does.not.match', + 'resultcode.yob.does.not.match', + 'resultcode.yob.within.one.year', + 'resultcode.mob.does.not.match', + 'resultcode.no.mob.available', + 'resultcode.no.dob.available', + 'resultcode.ssn.issued.prior.to.dob', + ], + }, }, CORPAY_DOCUMENT: { ALLOWED_FILE_TYPES: ['pdf', 'jpg', 'jpeg', 'png'], diff --git a/src/languages/de.ts b/src/languages/de.ts index 1cb69b0b548e..56cacfc70cc6 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4112,6 +4112,38 @@ ${amount} für ${merchant} – ${date}`, weTake: 'Wir nehmen Ihre Sicherheit ernst. Bitte richten Sie jetzt 2FA ein, um Ihrem Konto eine zusätzliche Schutzebene hinzuzufügen.', secure: 'Schütze dein Konto', }, + documentsStep: { + beforeYouGo: 'Bevor du fortfährst, benötigen wir einige Dokumente, um bestimmte Angaben zu überprüfen', + subheader: 'Verifizierung', + verificationFailed: 'Die Verifizierung ist fehlgeschlagen, daher benötigen wir zusätzliche Dokumente, um dich und dein Unternehmen zu überprüfen', + taxIDVerification: 'Steuer-ID-Verifizierung', + taxIDVerificationDescription: dedent(` + Bitte lade eine der folgenden Dateien hoch: + • IRS TIN/EIN-Zuweisungsschreiben + • IRS TIN/EIN-Antragsbestätigung (enthält normalerweise „Congratulations! The EIN has been successfully assigned“) + • IRS-Steuerbefreiungsschreiben mit Firmenname und EIN`), + nameChangeDocument: 'Dokument zur Namensänderung', + nameChangeDocumentDescription: + 'Wenn sich der Name deines Unternehmens seit der Beantragung der TIN/EIN geändert hat, benötigen wir dieses Dokument zur Verifizierung der angegebenen Steuer-ID', + companyAddressVerification: 'Verifizierung der Unternehmensadresse', + companyAddressVerificationDescription: dedent(` + Bitte lade eine der folgenden Dateien hoch: + • Aktuelle Strom-, Wasser- oder Gasrechnung mit Firmenname und Adresse + • Kontoauszug mit Firmenname und Adresse + • Aktueller Miet- oder Leasingvertrag inkl. Unterschriftsseite mit Firmenname und aktueller Adresse + • Versicherungsnachweis mit Firmenname und Adresse + • TIN-Zuweisungsdokument mit Firmenname und Adresse`), + userAddressVerification: 'Adressverifizierung', + userAddressVerificationDescription: dedent(` + Bitte lade eine der folgenden Dateien hoch: + • Wählerregistrierungskarte + • Führerschein + • Kontoauszug + • Versorgungsrechnung`), + userDOBVerification: 'Geburtsdatumsverifizierung', + userDOBVerificationDescription: 'Bitte lade einen in den USA ausgestellten Ausweis hoch', + finishViaChat: 'Über Chat abschließen', + }, reimbursementAccountLoadingAnimation: { oneMoment: 'Einen Moment', explanationLine: 'Wir überprüfen gerade Ihre Angaben. Sie können in Kürze mit den nächsten Schritten fortfahren.', diff --git a/src/languages/en.ts b/src/languages/en.ts index a02b7011c0bc..50ffc84ef93b 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4193,6 +4193,38 @@ const translations = { weTake: 'We take your security seriously. Please set up 2FA now to add an extra layer of protection to your account.', secure: 'Secure your account', }, + documentsStep: { + beforeYouGo: 'Before you go, we need some documents to verify some things', + subheader: 'Verification', + verificationFailed: "The verification failed, so we'll need some extra documents to verify you and your business.", + taxIDVerification: 'Tax ID Verification', + taxIDVerificationDescription: dedent(` + Please upload one of the following files: + • IRS TIN/EIN Assignment Letter + • IRS TIN/EIN Application confirmation (Normally states "Congratulations! The EIN has been successfully assigned") + • IRS tax exemption letter that lists your company name and EIN`), + nameChangeDocument: 'Name Change Document', + nameChangeDocumentDescription: 'If your company’s name has changed since filing for the TIN/EIN we need this document to verify the Tax ID number you provided', + companyAddressVerification: 'Company address verification', + companyAddressVerificationDescription: dedent(` + Please upload one of the following files: + • Recent utility bill showing company name and address + • Bank Statement showing company name and address + • Current Lease/Rental Agreement including the signature page showing your company name and current address + • Insurance Statement showing company name and address + • TIN assignment doc showing company name and address + • Business tax return (most current) showing company name and address`), + userAddressVerification: 'Address verification', + userAddressVerificationDescription: dedent(` + Please upload one of the following files: + • Voter Registration Card + • Driver's License + • Bank Statement + • Utility Bill`), + userDOBVerification: 'Date of birth verification', + userDOBVerificationDescription: 'Please upload a US issued ID', + finishViaChat: 'Finish via chat', + }, reimbursementAccountLoadingAnimation: { oneMoment: 'One moment', explanationLine: "We’re taking a look at your information. You'll be able to continue with next steps shortly.", diff --git a/src/languages/es.ts b/src/languages/es.ts index 5102c7c2e784..0808c445a634 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3992,6 +3992,37 @@ ${amount} para ${merchant} - ${date}`, weTake: 'Nos tomamos su seguridad en serio. Por favor, configure 2FA ahora para agregar una capa adicional de protección a su cuenta.', secure: 'Asegure su cuenta', }, + documentsStep: { + beforeYouGo: 'Antes de continuar, necesitamos algunos documentos para verificar cierta información', + subheader: 'Verificación', + verificationFailed: 'La verificación falló, por lo que necesitaremos documentos adicionales para verificarte a ti y a tu empresa', + taxIDVerification: 'Verificación del ID fiscal', + taxIDVerificationDescription: dedent(` + Por favor, sube uno de los siguientes archivos: + • Carta de asignación de TIN/EIN del IRS + • Confirmación de solicitud de TIN/EIN del IRS (normalmente indica "Congratulations! The EIN has been successfully assigned") + • Carta de exención fiscal del IRS que incluya el nombre de la empresa y el EIN`), + nameChangeDocument: 'Documento de cambio de nombre', + nameChangeDocumentDescription: 'Si el nombre de tu empresa cambió desde que solicitaste el TIN/EIN, necesitamos este documento para verificar el número de ID fiscal proporcionado', + companyAddressVerification: 'Verificación de la dirección de la empresa', + companyAddressVerificationDescription: dedent(` + Por favor, sube uno de los siguientes archivos: + • Factura reciente de servicios públicos con nombre y dirección de la empresa + • Estado de cuenta bancario con nombre y dirección de la empresa + • Contrato de arrendamiento vigente con página de firmas que muestre el nombre y la dirección actual de la empresa + • Estado de seguro con nombre y dirección de la empresa + • Documento de asignación de TIN con nombre y dirección de la empresa`), + userAddressVerification: 'Verificación de dirección', + userAddressVerificationDescription: dedent(` + Por favor, sube uno de los siguientes archivos: + • Tarjeta de registro de votante + • Licencia de conducir + • Estado de cuenta bancario + • Factura de servicios públicos`), + userDOBVerification: 'Verificación de fecha de nacimiento', + userDOBVerificationDescription: 'Por favor, sube una identificación emitida en EE. UU.', + finishViaChat: 'Finalizar por chat', + }, reimbursementAccountLoadingAnimation: { oneMoment: 'Un momento', explanationLine: 'Estamos verificando tu información y podrás continuar con los siguientes pasos en unos momentos.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 1c0e568b5d94..1e2ea4857caf 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4125,6 +4125,38 @@ ${amount} pour ${merchant} - ${date}`, weTake: 'Nous accordons une grande importance à votre sécurité. Veuillez configurer l’authentification à deux facteurs (2FA) maintenant pour ajouter une couche de protection supplémentaire à votre compte.', secure: 'Sécurisez votre compte', }, + documentsStep: { + beforeYouGo: 'Avant de continuer, nous avons besoin de certains documents pour vérifier certaines informations', + subheader: 'Vérification', + verificationFailed: 'La vérification a échoué, nous aurons donc besoin de documents supplémentaires pour te vérifier ainsi que ton entreprise', + taxIDVerification: 'Vérification de l’identifiant fiscal', + taxIDVerificationDescription: dedent(` + Veuillez téléverser l’un des fichiers suivants : + • Lettre d’attribution TIN/EIN de l’IRS + • Confirmation de demande TIN/EIN de l’IRS (indique généralement « Congratulations! The EIN has been successfully assigned ») + • Lettre d’exonération fiscale de l’IRS indiquant le nom de l’entreprise et l’EIN`), + nameChangeDocument: 'Document de changement de nom', + nameChangeDocumentDescription: + 'Si le nom de ton entreprise a changé depuis la demande du TIN/EIN, ce document est nécessaire pour vérifier le numéro d’identification fiscale fourni', + companyAddressVerification: 'Vérification de l’adresse de l’entreprise', + companyAddressVerificationDescription: dedent(` + Veuillez téléverser l’un des fichiers suivants : + • Facture récente de services publics indiquant le nom et l’adresse de l’entreprise + • Relevé bancaire indiquant le nom et l’adresse de l’entreprise + • Contrat de location en cours incluant la page de signature avec le nom et l’adresse actuelle de l’entreprise + • Attestation d’assurance indiquant le nom et l’adresse de l’entreprise + • Document d’attribution TIN indiquant le nom et l’adresse de l’entreprise`), + userAddressVerification: 'Vérification de l’adresse', + userAddressVerificationDescription: dedent(` + Veuillez téléverser l’un des fichiers suivants : + • Carte d’inscription électorale + • Permis de conduire + • Relevé bancaire + • Facture de services publics`), + userDOBVerification: 'Vérification de la date de naissance', + userDOBVerificationDescription: 'Veuillez téléverser une pièce d’identité délivrée aux États-Unis', + finishViaChat: 'Finaliser via le chat', + }, reimbursementAccountLoadingAnimation: { oneMoment: 'Un instant', explanationLine: 'Nous examinons vos informations. Vous pourrez poursuivre les prochaines étapes sous peu.', diff --git a/src/languages/it.ts b/src/languages/it.ts index ef4ebbd1e2fa..b97673439ba8 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4100,6 +4100,38 @@ ${amount} per ${merchant} - ${date}`, weTake: 'Prendiamo molto sul serio la tua sicurezza. Configura ora l’autenticazione a due fattori (2FA) per aggiungere un ulteriore livello di protezione al tuo account.', secure: 'Proteggi il tuo account', }, + documentsStep: { + beforeYouGo: 'Prima di continuare, abbiamo bisogno di alcuni documenti per verificare alcune informazioni', + subheader: 'Verifica', + verificationFailed: 'La verifica non è riuscita, quindi avremo bisogno di documenti aggiuntivi per verificare te e la tua azienda', + taxIDVerification: 'Verifica dell’ID fiscale', + taxIDVerificationDescription: dedent(` + Carica uno dei seguenti file: + • Lettera di assegnazione TIN/EIN dell’IRS + • Conferma della richiesta TIN/EIN dell’IRS (di solito indica "Congratulations! The EIN has been successfully assigned") + • Lettera di esenzione fiscale dell’IRS con nome dell’azienda ed EIN`), + nameChangeDocument: 'Documento di cambio nome', + nameChangeDocumentDescription: + 'Se il nome della tua azienda è cambiato dopo la richiesta del TIN/EIN, abbiamo bisogno di questo documento per verificare il numero di ID fiscale fornito', + companyAddressVerification: 'Verifica dell’indirizzo aziendale', + companyAddressVerificationDescription: dedent(` + Carica uno dei seguenti file: + • Bolletta recente con nome e indirizzo dell’azienda + • Estratto conto bancario con nome e indirizzo dell’azienda + • Contratto di locazione attuale con pagina firme che mostri nome e indirizzo attuale dell’azienda + • Documento assicurativo con nome e indirizzo dell’azienda + • Documento di assegnazione TIN con nome e indirizzo dell’azienda`), + userAddressVerification: 'Verifica dell’indirizzo', + userAddressVerificationDescription: dedent(` + Carica uno dei seguenti file: + • Tessera elettorale + • Patente di guida + • Estratto conto bancario + • Bolletta`), + userDOBVerification: 'Verifica della data di nascita', + userDOBVerificationDescription: 'Carica un documento di identità rilasciato negli Stati Uniti', + finishViaChat: 'Completa via chat', + }, reimbursementAccountLoadingAnimation: { oneMoment: 'Un momento', explanationLine: 'Stiamo esaminando le tue informazioni. Potrai procedere con i prossimi passaggi a breve.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index cfc0233f441d..416a78595043 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4069,6 +4069,37 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' weTake: 'お客様のセキュリティを重要視しています。アカウントをさらに強固に保護するため、今すぐ2要素認証(2FA)を設定してください。', secure: 'アカウントを保護する', }, + documentsStep: { + beforeYouGo: '続行する前に、いくつかの情報を確認するための書類が必要です', + subheader: '確認', + verificationFailed: '確認に失敗したため、追加の書類で本人および事業の確認が必要です', + taxIDVerification: '納税者番号の確認', + taxIDVerificationDescription: dedent(` + 以下のいずれかの書類をアップロードしてください: + • IRS TIN/EIN 割当通知書 + • IRS TIN/EIN 申請確認書(通常「Congratulations! The EIN has been successfully assigned」と記載) + • 会社名と EIN が記載された IRS の免税通知書`), + nameChangeDocument: '名称変更書類', + nameChangeDocumentDescription: 'TIN/EIN 申請後に会社名が変更された場合、提供された納税者番号を確認するためにこの書類が必要です', + companyAddressVerification: '会社住所の確認', + companyAddressVerificationDescription: dedent(` + 以下のいずれかの書類をアップロードしてください: + • 会社名と住所が記載された最近の公共料金請求書 + • 会社名と住所が記載された銀行取引明細書 + • 署名ページを含む現行の賃貸契約書(会社名と現住所が記載されたもの) + • 会社名と住所が記載された保険証書 + • 会社名と住所が記載された TIN 割当書類`), + userAddressVerification: '住所確認', + userAddressVerificationDescription: dedent(` + 以下のいずれかの書類をアップロードしてください: + • 有権者登録カード + • 運転免許証 + • 銀行取引明細書 + • 公共料金請求書`), + userDOBVerification: '生年月日の確認', + userDOBVerificationDescription: '米国発行の身分証明書をアップロードしてください', + finishViaChat: 'チャットで完了', + }, reimbursementAccountLoadingAnimation: { oneMoment: '少々お待ちください', explanationLine: '現在、お客様の情報を確認しています。まもなく次のステップに進めるようになります。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index cbdb4e72b52b..fb5d20f97a28 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4096,6 +4096,37 @@ ${amount} voor ${merchant} - ${date}`, weTake: 'We nemen je beveiliging serieus. Stel nu 2FA in om een extra beveiligingslaag aan je account toe te voegen.', secure: 'Beveilig je account', }, + documentsStep: { + beforeYouGo: 'Voordat je verdergaat, hebben we enkele documenten nodig om bepaalde gegevens te verifiëren', + subheader: 'Verificatie', + verificationFailed: 'De verificatie is mislukt, daarom hebben we extra documenten nodig om jou en je bedrijf te verifiëren', + taxIDVerification: 'Belastingnummerverificatie', + taxIDVerificationDescription: dedent(` + Upload een van de volgende bestanden: + • IRS TIN/EIN-toewijzingsbrief + • IRS TIN/EIN-aanvraagbevestiging (bevat meestal "Congratulations! The EIN has been successfully assigned") + • IRS-belastingvrijstellingsbrief met bedrijfsnaam en EIN`), + nameChangeDocument: 'Document naamswijziging', + nameChangeDocumentDescription: 'Als de naam van je bedrijf is gewijzigd sinds de TIN/EIN-aanvraag, hebben we dit document nodig om het opgegeven belastingnummer te verifiëren', + companyAddressVerification: 'Verificatie van bedrijfsadres', + companyAddressVerificationDescription: dedent(` + Upload een van de volgende bestanden: + • Recente energierekening met bedrijfsnaam en adres + • Bankafschrift met bedrijfsnaam en adres + • Huidige huur- of leaseovereenkomst inclusief ondertekeningspagina met bedrijfsnaam en huidig adres + • Verzekeringsverklaring met bedrijfsnaam en adres + • TIN-toewijzingsdocument met bedrijfsnaam en adres`), + userAddressVerification: 'Adresverificatie', + userAddressVerificationDescription: dedent(` + Upload een van de volgende bestanden: + • Kiezersregistratiekaart + • Rijbewijs + • Bankafschrift + • Energierekening`), + userDOBVerification: 'Verificatie van geboortedatum', + userDOBVerificationDescription: 'Upload een in de VS uitgegeven identiteitsbewijs', + finishViaChat: 'Afronden via chat', + }, reimbursementAccountLoadingAnimation: { oneMoment: 'Een ogenblik', explanationLine: 'We bekijken je gegevens. Je kunt binnenkort verdergaan met de volgende stappen.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d8eefa2a66e1..d8334e0a5c8a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4088,6 +4088,37 @@ ${amount} dla ${merchant} - ${date}`, weTake: 'Poważnie podchodzimy do kwestii bezpieczeństwa. Skonfiguruj teraz uwierzytelnianie dwuskładnikowe (2FA), aby dodać dodatkową warstwę ochrony swojego konta.', secure: 'Zabezpiecz swoje konto', }, + documentsStep: { + beforeYouGo: 'Zanim przejdziesz dalej, potrzebujemy kilku dokumentów do weryfikacji informacji', + subheader: 'Weryfikacja', + verificationFailed: 'Weryfikacja nie powiodła się, dlatego potrzebujemy dodatkowych dokumentów do potwierdzenia Twojej tożsamości i firmy', + taxIDVerification: 'Weryfikacja numeru podatkowego', + taxIDVerificationDescription: dedent(` + Prześlij jeden z poniższych plików: + • List przydziału TIN/EIN z IRS + • Potwierdzenie wniosku TIN/EIN z IRS (zwykle zawiera „Congratulations! The EIN has been successfully assigned”) + • Pismo o zwolnieniu podatkowym z IRS zawierające nazwę firmy i EIN`), + nameChangeDocument: 'Dokument zmiany nazwy', + nameChangeDocumentDescription: 'Jeśli nazwa firmy zmieniła się od momentu złożenia wniosku o TIN/EIN, dokument ten jest wymagany do weryfikacji podanego numeru podatkowego', + companyAddressVerification: 'Weryfikacja adresu firmy', + companyAddressVerificationDescription: dedent(` + Prześlij jeden z poniższych plików: + • Aktualny rachunek za media z nazwą i adresem firmy + • Wyciąg bankowy z nazwą i adresem firmy + • Aktualna umowa najmu z podpisaną stroną zawierającą nazwę i adres firmy + • Dokument ubezpieczeniowy z nazwą i adresem firmy + • Dokument przydziału TIN z nazwą i adresem firmy`), + userAddressVerification: 'Weryfikacja adresu', + userAddressVerificationDescription: dedent(` + Prześlij jeden z poniższych plików: + • Karta rejestracji wyborcy + • Prawo jazdy + • Wyciąg bankowy + • Rachunek za media`), + userDOBVerification: 'Weryfikacja daty urodzenia', + userDOBVerificationDescription: 'Prześlij dokument tożsamości wydany w USA', + finishViaChat: 'Zakończ przez czat', + }, reimbursementAccountLoadingAnimation: { oneMoment: 'Chwileczkę', explanationLine: 'Sprawdzamy Twoje informacje. Wkrótce będziesz mógł/mogła przejść do kolejnych kroków.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index c71ea2e3dd60..6be7d4ef69ba 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4088,6 +4088,37 @@ ${amount} para ${merchant} - ${date}`, weTake: 'Levamos sua segurança a sério. Ative a autenticação em duas etapas (2FA) agora para adicionar uma camada extra de proteção à sua conta.', secure: 'Proteja sua conta', }, + documentsStep: { + beforeYouGo: 'Antes de continuar, precisamos de alguns documentos para verificar algumas informações', + subheader: 'Verificação', + verificationFailed: 'A verificação falhou, então precisaremos de documentos adicionais para verificar você e sua empresa', + taxIDVerification: 'Verificação de ID fiscal', + taxIDVerificationDescription: dedent(` + Envie um dos seguintes arquivos: + • Carta de atribuição de TIN/EIN do IRS + • Confirmação de solicitação de TIN/EIN do IRS (normalmente contém "Congratulations! The EIN has been successfully assigned") + • Carta de isenção fiscal do IRS com o nome da empresa e o EIN`), + nameChangeDocument: 'Documento de alteração de nome', + nameChangeDocumentDescription: 'Se o nome da sua empresa mudou desde a solicitação do TIN/EIN, precisamos deste documento para verificar o número de identificação fiscal informado', + companyAddressVerification: 'Verificação de endereço da empresa', + companyAddressVerificationDescription: dedent(` + Envie um dos seguintes arquivos: + • Conta recente de serviços públicos com nome e endereço da empresa + • Extrato bancário com nome e endereço da empresa + • Contrato de locação atual incluindo a página de assinatura com nome e endereço atual da empresa + • Apólice ou declaração de seguro com nome e endereço da empresa + • Documento de atribuição de TIN com nome e endereço da empresa`), + userAddressVerification: 'Verificação de endereço', + userAddressVerificationDescription: dedent(` + Envie um dos seguintes arquivos: + • Título de eleitor + • Carteira de motorista + • Extrato bancário + • Conta de serviços públicos`), + userDOBVerification: 'Verificação de data de nascimento', + userDOBVerificationDescription: 'Envie um documento de identidade emitido nos EUA', + finishViaChat: 'Finalizar pelo chat', + }, reimbursementAccountLoadingAnimation: { oneMoment: 'Um momento', explanationLine: 'Estamos analisando suas informações. Você poderá continuar com as próximas etapas em breve.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 056a7cd8d4e3..b623031cf39f 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3997,6 +3997,37 @@ ${amount},商户:${merchant} - 日期:${date}`, weTake: '我们非常重视您的安全。请立即设置双重验证,为您的账户增加一层额外保护。', secure: '保护你的账户', }, + documentsStep: { + beforeYouGo: '在继续之前,我们需要一些文件来验证相关信息', + subheader: '验证', + verificationFailed: '验证失败,因此我们需要额外的文件来验证你及你的企业', + taxIDVerification: '税务识别号验证', + taxIDVerificationDescription: dedent(` + 请上传以下任一文件: + • IRS TIN/EIN 分配函 + • IRS TIN/EIN 申请确认函(通常包含“Congratulations! The EIN has been successfully assigned”) + • 显示公司名称和 EIN 的 IRS 免税函`), + nameChangeDocument: '名称变更文件', + nameChangeDocumentDescription: '如果你的公司名称在申请 TIN/EIN 后发生更改,我们需要此文件来验证你提供的税务识别号', + companyAddressVerification: '公司地址验证', + companyAddressVerificationDescription: dedent(` + 请上传以下任一文件: + • 显示公司名称和地址的近期水电账单 + • 显示公司名称和地址的银行对账单 + • 包含签字页的有效租赁协议,显示公司名称和当前地址 + • 显示公司名称和地址的保险声明 + • 显示公司名称和地址的 TIN 分配文件`), + userAddressVerification: '地址验证', + userAddressVerificationDescription: dedent(` + 请上传以下任一文件: + • 选民登记卡 + • 驾驶证 + • 银行对账单 + • 水电账单`), + userDOBVerification: '出生日期验证', + userDOBVerificationDescription: '请上传美国签发的身份证件', + finishViaChat: '通过聊天完成', + }, reimbursementAccountLoadingAnimation: { oneMoment: '请稍候', explanationLine: '我们正在查看您的信息。您很快就可以继续进行下一步了。', diff --git a/src/libs/API/parameters/AcceptACHContractForBankAccount.ts b/src/libs/API/parameters/AcceptACHContractForBankAccount.ts index 52e5d2cec62e..baafd70d0246 100644 --- a/src/libs/API/parameters/AcceptACHContractForBankAccount.ts +++ b/src/libs/API/parameters/AcceptACHContractForBankAccount.ts @@ -1,5 +1,5 @@ import type {ACHContractStepProps} from '@src/types/form/ReimbursementAccountForm'; -type AcceptACHContractForBankAccount = ACHContractStepProps & {bankAccountID: number; policyID: string | undefined}; +type AcceptACHContractForBankAccount = ACHContractStepProps & {bankAccountID: number; policyID: string | undefined; includeUploadKYBSetupStep: boolean}; export default AcceptACHContractForBankAccount; diff --git a/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts index 6b816186ed45..4fb379b71f1d 100644 --- a/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts +++ b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts @@ -10,6 +10,7 @@ type OpenReimbursementAccountPageParams = { policyID?: string; bankAccountID?: number; shouldPreserveDraft?: boolean; + includeUploadKYBSetupStep: boolean; }; export default OpenReimbursementAccountPageParams; diff --git a/src/libs/API/parameters/UploadUserKYBDocsParams.ts b/src/libs/API/parameters/UploadUserKYBDocsParams.ts new file mode 100644 index 000000000000..b89605cb8fea --- /dev/null +++ b/src/libs/API/parameters/UploadUserKYBDocsParams.ts @@ -0,0 +1,12 @@ +import type {FileObject} from '@src/types/utils/Attachment'; + +type UploadUserKYBDocsParams = { + bankAccountID: number; + companyTaxID?: FileObject; + nameChangeDocument?: FileObject; + companyAddressVerification?: FileObject; + userAddressVerification?: FileObject; + userDOBVerification?: FileObject; +}; + +export default UploadUserKYBDocsParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 9e00d5dd8b70..3b30e4ff2944 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -554,3 +554,4 @@ export type {default as DeleteAgentParams} from './DeleteAgentParams'; export type {default as SendExportFileFromConciergeParams} from './SendExportFileFromConciergeParams'; export type {default as ClearExportDownloadParams} from './ClearExportDownloadParams'; export type {default as UpgradeSubmitParams} from './UpgradeSubmitParams'; +export type {default as UploadUserKYBDocsParams} from './UploadUserKYBDocsParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index f307ab6f4b54..66b763ddc417 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -624,6 +624,7 @@ const WRITE_COMMANDS = { SEND_EXPORT_FILE_FROM_CONCIERGE: 'SendExportFileFromConcierge', CLEAR_EXPORT_DOWNLOAD: 'ClearExportDownload', UPGRADE_SUBMIT: 'UpgradeSubmit', + UPLOAD_USER_KYB_DOCS: 'UploadUserKYBDocs', } as const; type WriteCommand = ValueOf; @@ -1048,6 +1049,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ENABLE_GLOBAL_REIMBURSEMENTS_FOR_USD_BANK_ACCOUNT]: Parameters.EnableGlobalReimbursementsForUSDBankAccountParams; [WRITE_COMMANDS.REOPEN_REPORT]: Parameters.ReopenReportParams; [WRITE_COMMANDS.SEND_SCHEDULE_CALL_NUDGE]: Parameters.SendScheduleCallNudgeParams; + [WRITE_COMMANDS.UPLOAD_USER_KYB_DOCS]: Parameters.UploadUserKYBDocsParams; [WRITE_COMMANDS.REJECT_MONEY_REQUEST_IN_BULK]: Parameters.RejectMoneyRequestInBulkParams; [WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH]: Parameters.ApproveMoneyRequestOnSearchParams; diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index bd0ede3b430e..15c284aca46d 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -1,8 +1,13 @@ import {Str} from 'expensify-common'; import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type * as OnyxTypes from '@src/types/onyx'; import type AccountData from '@src/types/onyx/AccountData'; +import type {ACHData} from '@src/types/onyx/ReimbursementAccount'; + +/** Responses of the additional KYB verification checks, hinting at which documents the user still needs to upload */ +type KYBVerificationResponses = NonNullable['externalApiResponses']; function getDefaultCompanyWebsite(session: OnyxEntry, account: OnyxEntry, shouldShowPublicDomain = false): string { return account?.isFromPublicDomain && !shouldShowPublicDomain ? '' : `https://www.${Str.extractEmailDomain(session?.email ?? '')}`; @@ -98,14 +103,88 @@ function hasPersonalBankAccountMissingInfo(bankAccountList: OnyxEntry isPersonalBankAccountMissingInfo(bankAccount?.accountData)); } +/** Compares error keys and searches for overlap. Based on the result we decide whether to gather extra file + * @param status - status of the check + * @param qualifiers - errors returned after the check + * @returns boolean - whether to gather additional address verification file + */ +function isUserAddressVerificationRequired( + status: string | undefined, + qualifiers: + | Array<{ + key: string; + message: string; + }> + | undefined, +): boolean { + return ( + status !== CONST.BANK_ACCOUNT.KYB_STATUS.PASS && + !!CONST.BANK_ACCOUNT.KYB_REQUESTOR_IDENTITY_ERROR.ADDRESS.find((error) => qualifiers?.map((qualifier) => qualifier.key).includes(error)) + ); +} + +/** Compares error keys and searches for overlap. Based on the result we decide whether to gather extra file + * @param status - status of the check + * @param qualifiers - errors returned after the check + * @returns boolean - whether to gather additional DOB verification file + */ +function isUserDOBVerificationRequired( + status: string | undefined, + qualifiers: + | Array<{ + key: string; + message: string; + }> + | undefined, +): boolean { + return ( + status !== CONST.BANK_ACCOUNT.KYB_STATUS.PASS && !!CONST.BANK_ACCOUNT.KYB_REQUESTOR_IDENTITY_ERROR.DOB.find((error) => qualifiers?.map((qualifier) => qualifier.key).includes(error)) + ); +} + +/** Builds the list of KYB document inputIDs the user must upload, based on which verification checks did not pass. + * Returns an empty array when no documents are required (e.g. automated verification passed), in which case the + * KYB documents step should be skipped entirely. + * @param externalApiResponses - statuses of the external verification checks from the reimbursement account + * @returns inputIDs of the documents that still need to be uploaded + */ +function getRequiredKYBDocuments(externalApiResponses: KYBVerificationResponses): string[] { + const requiredDocuments: string[] = []; + + const companyTaxIDStatus = externalApiResponses?.companyTaxID?.status; + if (companyTaxIDStatus !== undefined && companyTaxIDStatus !== CONST.BANK_ACCOUNT.KYB_STATUS.PASS) { + requiredDocuments.push(INPUT_IDS.KYB_DOCUMENTS.COMPANY_TAX_ID); + } + + const lexisNexisStatus = externalApiResponses?.lexisNexisInstantIDResult?.status; + if (lexisNexisStatus !== undefined && lexisNexisStatus !== CONST.BANK_ACCOUNT.KYB_STATUS.PASS) { + requiredDocuments.push(INPUT_IDS.KYB_DOCUMENTS.NAME_CHANGE_DOCUMENT, INPUT_IDS.KYB_DOCUMENTS.COMPANY_ADDRESS_VERIFICATION); + } + + const requestorIdentityStatus = externalApiResponses?.requestorIdentityID?.status; + const requestorIdentityQualifiers = externalApiResponses?.requestorIdentityID?.apiResult?.qualifiers?.qualifier; + if (isUserAddressVerificationRequired(requestorIdentityStatus, requestorIdentityQualifiers)) { + requiredDocuments.push(INPUT_IDS.KYB_DOCUMENTS.USER_ADDRESS_VERIFICATION); + } + if (isUserDOBVerificationRequired(requestorIdentityStatus, requestorIdentityQualifiers)) { + requiredDocuments.push(INPUT_IDS.KYB_DOCUMENTS.USER_DOB_VERIFICATION); + } + + return requiredDocuments; +} + export { getDefaultCompanyWebsite, + getRequiredKYBDocuments, getLastFourDigits, hasPartiallySetupBankAccount, hasPersonalBankAccountMissingInfo, isBankAccountPartiallySetup, + isUserAddressVerificationRequired, + isUserDOBVerificationRequired, doesPolicyHavePartiallySetupBankAccount, isPersonalBankAccountMissingInfo, getCompletedStepsForBankAccount, PERSONAL_INFO_STEP, }; +export type {KYBVerificationResponses}; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 9d9c3ecd2e38..12fe309454f7 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -18,6 +18,7 @@ import type { ShareBankAccountParams, UnshareBankAccountParams, UpdatePersonalBankAccountInfoParams, + UploadUserKYBDocsParams, ValidateBankAccountWithTransactionsParams, VerifyIdentityForBankAccountParams, } from '@libs/API/parameters'; @@ -1273,6 +1274,7 @@ function openReimbursementAccountPage({stepToOpen = '', subStep = '', localCurre policyID, bankAccountID, shouldPreserveDraft, + includeUploadKYBSetupStep: true, }; return API.read(READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE, parameters, onyxData); @@ -1329,6 +1331,7 @@ function acceptACHContractForBankAccount(bankAccountID: number, params: ACHContr ...params, bankAccountID, policyID, + includeUploadKYBSetupStep: true, }, onyxData, ); @@ -1366,6 +1369,10 @@ function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoD API.write(WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx()); } +function uploadUserKYBDocs(parameters: UploadUserKYBDocsParams) { + API.write(WRITE_COMMANDS.UPLOAD_USER_KYB_DOCS, parameters, getVBBADataForOnyx()); +} + function openWorkspaceView(policyID: string | undefined) { API.read( READ_COMMANDS.OPEN_WORKSPACE_VIEW, @@ -1885,4 +1892,5 @@ export { updatePersonalBankAccountInfo, initiateBankAccountUnlock, pressLockedBankAccount, + uploadUserKYBDocs, }; diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index e70826be0f45..89081da5fd99 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -356,6 +356,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen [CONST.BANK_ACCOUNT.STEP.COMPANY]: CONST.BANK_ACCOUNT.PAGE_NAMES.COMPANY, [CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS]: CONST.BANK_ACCOUNT.PAGE_NAMES.BENEFICIAL_OWNERS, [CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT]: CONST.BANK_ACCOUNT.PAGE_NAMES.ACH_CONTRACT, + [CONST.BANK_ACCOUNT.STEP.KYB_DOCS]: CONST.BANK_ACCOUNT.PAGE_NAMES.KYB_DOCS, [CONST.BANK_ACCOUNT.STEP.VALIDATION]: CONST.BANK_ACCOUNT.PAGE_NAMES.VALIDATION, }; const page = stepToPageName[currentStep] ?? CONST.BANK_ACCOUNT.PAGE_NAMES.COUNTRY; diff --git a/src/pages/ReimbursementAccount/USD/KYBDocuments/index.tsx b/src/pages/ReimbursementAccount/USD/KYBDocuments/index.tsx new file mode 100644 index 000000000000..9e243ba8c4b4 --- /dev/null +++ b/src/pages/ReimbursementAccount/USD/KYBDocuments/index.tsx @@ -0,0 +1,223 @@ +import {hasSeenTourSelector} from '@selectors/Onboarding'; +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import Text from '@components/Text'; +import UploadFile from '@components/UploadFile'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useReimbursementAccountSubmitCallback from '@hooks/useReimbursementAccountSubmitCallback'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getRequiredKYBDocuments} from '@libs/BankAccountUtils'; +import {uploadUserKYBDocs} from '@userActions/BankAccounts'; +import {clearErrorFields, setDraftValues, setErrorFields} from '@userActions/FormActions'; +import {navigateToConciergeChat} from '@userActions/Report'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import type {FileObject} from '@src/types/utils/Attachment'; + +type KYBDocumentsProps = { + /** Goes to the previous step */ + onBackButtonPress: () => void; + + /** Handles submit button press (URL-based navigation) */ + onSubmit?: () => void; +}; + +/** + * Maps the KYB document form input IDs to the param names expected by the UploadUserKYBDocs command. + * The tax ID document uses a form key (`companyTaxId`) distinct from the API param (`companyTaxID`) so it + * does not collide with the business-info step's string EIN/SSN draft field. + */ +const KYB_DOCUMENT_API_PARAM: Record = { + [INPUT_IDS.KYB_DOCUMENTS.COMPANY_TAX_ID]: 'companyTaxID', + [INPUT_IDS.KYB_DOCUMENTS.NAME_CHANGE_DOCUMENT]: 'nameChangeDocument', + [INPUT_IDS.KYB_DOCUMENTS.COMPANY_ADDRESS_VERIFICATION]: 'companyAddressVerification', + [INPUT_IDS.KYB_DOCUMENTS.USER_ADDRESS_VERIFICATION]: 'userAddressVerification', + [INPUT_IDS.KYB_DOCUMENTS.USER_DOB_VERIFICATION]: 'userDOBVerification', +}; + +function KYBDocuments({onBackButtonPress, onSubmit}: KYBDocumentsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [betas] = useOnyx(ONYXKEYS.BETAS); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const reimbursementAccountVerificationData = reimbursementAccount?.achData?.verifications?.externalApiResponses; + const bankAccountID = reimbursementAccount?.achData?.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; + const isLoading = reimbursementAccount?.isLoading; + + const handleNavigateToConciergeChat = () => + navigateToConciergeChat( + conciergeReportID, + introSelected, + currentUserAccountID, + isSelfTourViewed, + betas, + true, + undefined, + undefined, + reimbursementAccount?.achData?.ACHRequestReportActionID, + ); + + const defaultValues = { + [INPUT_IDS.KYB_DOCUMENTS.COMPANY_TAX_ID]: reimbursementAccountDraft?.[INPUT_IDS.KYB_DOCUMENTS.COMPANY_TAX_ID] ?? [], + [INPUT_IDS.KYB_DOCUMENTS.NAME_CHANGE_DOCUMENT]: reimbursementAccountDraft?.[INPUT_IDS.KYB_DOCUMENTS.NAME_CHANGE_DOCUMENT] ?? [], + [INPUT_IDS.KYB_DOCUMENTS.COMPANY_ADDRESS_VERIFICATION]: reimbursementAccountDraft?.[INPUT_IDS.KYB_DOCUMENTS.COMPANY_ADDRESS_VERIFICATION] ?? [], + [INPUT_IDS.KYB_DOCUMENTS.USER_ADDRESS_VERIFICATION]: reimbursementAccountDraft?.[INPUT_IDS.KYB_DOCUMENTS.USER_ADDRESS_VERIFICATION] ?? [], + [INPUT_IDS.KYB_DOCUMENTS.USER_DOB_VERIFICATION]: reimbursementAccountDraft?.[INPUT_IDS.KYB_DOCUMENTS.USER_DOB_VERIFICATION] ?? [], + } as Record; + + const DOCUMENTS_CONFIG = [ + { + inputID: INPUT_IDS.KYB_DOCUMENTS.COMPANY_TAX_ID, + title: 'documentsStep.taxIDVerification', + description: 'documentsStep.taxIDVerificationDescription', + }, + { + inputID: INPUT_IDS.KYB_DOCUMENTS.NAME_CHANGE_DOCUMENT, + title: 'documentsStep.nameChangeDocument', + description: 'documentsStep.nameChangeDocumentDescription', + }, + { + inputID: INPUT_IDS.KYB_DOCUMENTS.COMPANY_ADDRESS_VERIFICATION, + title: 'documentsStep.companyAddressVerification', + description: 'documentsStep.companyAddressVerificationDescription', + }, + { + inputID: INPUT_IDS.KYB_DOCUMENTS.USER_ADDRESS_VERIFICATION, + title: 'documentsStep.userAddressVerification', + description: 'documentsStep.userAddressVerificationDescription', + }, + { + inputID: INPUT_IDS.KYB_DOCUMENTS.USER_DOB_VERIFICATION, + title: 'documentsStep.userDOBVerification', + description: 'documentsStep.userDOBVerificationDescription', + }, + ] as const; + const requiredDocumentInputIDs = getRequiredKYBDocuments(reimbursementAccountVerificationData); + const requiredDocuments = DOCUMENTS_CONFIG.filter((document) => requiredDocumentInputIDs.includes(document.inputID)); + + const [uploadedFiles, setUploadedFiles] = useState>(defaultValues); + + const validate = (values: FormOnyxValues): FormInputErrors => { + const errors: FormInputErrors = {}; + + for (const document of requiredDocuments) { + const files = values[document.inputID] as FileObject[] | undefined; + if (!files || files.length === 0) { + errors[document.inputID] = translate('common.error.fieldRequired'); + } + } + + return errors; + }; + + const handleSelectFile = (files: FileObject[], inputID: string) => { + const updatedFiles = [...uploadedFiles[inputID], ...files]; + setUploadedFiles((prev) => ({...prev, [inputID]: updatedFiles})); + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[inputID]: updatedFiles}); + }; + + const handleRemoveFile = (fileName: string, inputID: string) => { + const updatedFiles = uploadedFiles[inputID].filter((file) => file.name !== fileName); + setUploadedFiles((prev) => ({...prev, [inputID]: updatedFiles})); + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[inputID]: updatedFiles}); + }; + + const setUploadError = (error: string, inputID: string) => { + if (!error) { + clearErrorFields(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM); + return; + } + setErrorFields(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[inputID]: {onUpload: error}}); + }; + + const markSubmitting = useReimbursementAccountSubmitCallback(onSubmit); + + const submit = useCallback(() => { + const params: Record = {}; + for (const [key, files] of Object.entries(uploadedFiles)) { + const file = files.at(0); + const apiParamKey = KYB_DOCUMENT_API_PARAM[key]; + if (file && apiParamKey) { + params[apiParamKey] = file; + } + } + uploadUserKYBDocs({ + ...params, + bankAccountID, + }); + markSubmitting(); + }, [uploadedFiles, bankAccountID, markSubmitting]); + + const footer = ( +