diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index a44d7d11af87..ad738e44ab44 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -54,6 +54,11 @@ platforms: icon: /assets/images/hand-card.svg description: Explore the perks and benefits of the Expensify Card. + - href: travel + title: Travel + icon: /assets/images/plane.svg + description: Manage all your corporate travel needs with Expensify Travel. + - href: copilots-and-delegates title: Copilots & Delegates icon: /assets/images/envelope-receipt.svg @@ -114,16 +119,16 @@ platforms: icon: /assets/images/money-into-wallet.svg description: Learn more about expense tracking and submission. - - href: bank-accounts-and-payments - title: Bank Accounts & Payments - icon: /assets/images/bank-card.svg - description: Send direct reimbursements, pay invoices, and receive payment. - - href: expensify-card title: Expensify Card icon: /assets/images/hand-card.svg description: Explore the perks and benefits of the Expensify Card. + - href: travel + title: Travel + icon: /assets/images/plane.svg + description: Manage all your corporate travel needs with Expensify Travel. + - href: connections title: Connections icon: /assets/images/workflow.svg diff --git a/docs/articles/expensify-classic/travel/Coming-Soon.md b/docs/articles/expensify-classic/travel/Coming-Soon.md new file mode 100644 index 000000000000..4d32487a14b5 --- /dev/null +++ b/docs/articles/expensify-classic/travel/Coming-Soon.md @@ -0,0 +1,6 @@ +--- +title: Coming soon +description: Coming soon +--- + +# Coming soon \ No newline at end of file diff --git a/docs/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.md b/docs/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.md deleted file mode 100644 index bc0676231544..000000000000 --- a/docs/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Connect a Business Bank Account - US -description: How to connect a business bank account to Expensify (US) ---- -# Overview -Adding a verified business bank account unlocks a myriad of features and automation in Expensify. -Once you connect your business bank account, you can: -- Reimburse expenses via direct bank transfer -- Pay bills -- Collect invoice payments -- Issue the Expensify Card - -# How to add a verified business bank account -To connect a business bank account to Expensify, follow the below steps: -1. Go to **Settings > Workspaces > _Workspace Name_ > Bank account > Connect bank account** -2. Click **Connect online with Plaid** -3. Click **Continue** -4. When you reach the **Plaid** screen, you'll be shown a list of compatible banks that offer direct online login access -5. Login to the business bank account: -- If the bank is not listed, click the X to go back to the connection type -- Here you’ll see the option to **Connect Manually** -- Enter your account and routing numbers -6. Enter your bank login credentials: -- If your bank requires additional security measures, you will be directed to obtain and enter a security code -- If you have more than one account available to choose from, you will be directed to choose the desired account - -Next, to verify the bank account, you’ll enter some details about the business as well as some personal information. - -## Enter company information -This is where you’ll add the legal business name as well as several other company details. - -- **Company address**: The company address must be located in the US and a physical location (If you input a maildrop address, PO box, or UPS Store, the address will be flagged for review, and adding the bank account to Expensify will be delayed) -- **Tax Identification Number**: This is the identification number that was assigned to the business by the IRS -- **Company website**: A company website is required to use most of Expensify’s payment features. When adding the website of the business, format it as, https://www.domain.com -- **Industry Classification Code**: You can locate a list of Industry Classification Codes [here]([url](https://www.census.gov/naics/?input=software&year=2022)) - -## Enter personal information -Whoever is connecting the bank account to Expensify, must enter their details under the Requestor Information section: -- The address must be a physical address -- The address must be located in the US -- The SSN must be US-issued - -This does not need to be a signor on the bank account. If someone other than the Expensify account holder enters their personal information in this section, the details will be flagged for review, and adding the bank account to Expensify will be delayed. - -## Upload ID -After entering your personal details, you’ll be prompted to click a link or scan a QR code so that you can do the following: -1. Upload a photo of the front and back of your ID (this cannot be a photo of an existing image) -2. Use your device to take a selfie and record a short video of yourself - -**Your ID must be:** -- Issued in the US -- Current (ie: the expiration date must be in the future) - -## Additional Information -Check the appropriate box under **Additional Information**, accept the agreement terms, and verify that all of the information is true and accurate: -- A Beneficial Owner refers to an **individual** who owns 25% or more of the business. -- If you or another **individual** owns 25% or more of the business, please check the appropriate box -- If someone else owns 25% or more of the business, you will be prompted to provide their personal information - -If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section. - -# How to validate the bank account - -The account you set up can be found under **Settings > Workspaces > _Workspace Name_ > Bank account** in either the **Verifying** or **Pending** state. - -If it is **Verifying**, then this means we sent you a message and need more information from you. Please review the automated message sent by Concierge. This should include a message with specific details about what's required to move forward. - -If it is **Pending**, then in 1-2 business days Expensify will administer 3 test transactions to your bank account. If after two business days you do not see these test transactions, reach out to Concierge for assistance. - -After these transactions (2 withdrawals and 1 deposit) have been processed to your account, head to the **Bank accounts** section of your workspace settings. Here you'll see a prompt to input the transaction amounts. - -Once you've finished these steps, your business bank account is ready to use in Expensify! - -# How to delete a verified bank account -If you need to delete a bank account from Expensify, run through the following steps: -1. Go to **Settings > Workspaces > _Workspace Name_ > Bank account** -2. Click the red **Delete** button under the corresponding bank account - -# Deep Dive - -## Verified bank account requirements - -To add a business bank account to issue reimbursements via ACH (US), to pay invoices (US), or to issue Expensify Cards: -- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We **cannot** accept a PO Box or MailDrop location. -- If you are adding the bank account to Expensify, you must add it from **your** Expensify account settings. -- If you are adding a bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US-issued photo ID. For using features related to US ACH, your ID must be issued by the United States. You and any Beneficial Owner (if one exists), must also have a US address -- You must have a valid website for your business to utilize the Expensify Card, or to pay invoices with Expensify. - -## Locked bank account -When you reimburse a report, you authorize Expensify to withdraw the funds from your account. If your bank rejects Expensify’s withdrawal request, your verified bank account is locked until the issue is resolved. - -Withdrawal requests can be rejected due to insufficient funds, or if the bank account has not been enabled for direct debit. -If you need to enable direct debits from your verified bank account, your bank will require the following details: -- The ACH CompanyIDs (1270239450, 4270239450 and 2270239450) -- The ACH Originator Name (Expensify) - -If using Expensify to process Bill payments, you'll also need to whitelist the ACH IDs from our partner [Stripe](https://support.stripe.com/questions/ach-direct-debit-company-ids-for-stripe?): -- The ACH CompanyIDs (1800948598 and 4270465600) -- The ACH Originator Name (expensify.com) - -If using Expensify to process international reimbursements from your USD bank account, you'll also need to whitelist the ACH IDs from our partner CorPay: -- The ACH CompanyIDs (1522304924 and 2522304924) -- The ACH Originator Name (Cambridge Global Payments) - -To request to unlock the bank account, go to **Settings > Workspaces > _Workspace Name_ > Bank account** and click **Fix.** This sends a request to our support team to review why the bank account was locked, who will send you a message to confirm that. - -Unlocking a bank account can take 4-5 business days to process, to allow for ACH processing time and clawback periods. - -## Error adding an ID to Onfido - -Expensify is required by both our sponsor bank and federal law to verify the identity of the individual who is initiating the movement of money. We use Onfido to confirm that the person adding a payment method is genuine and not impersonating someone else. - -If you get a generic error message that indicates, "Something's gone wrong", please go through the following steps: - -1. Ensure you are using either Safari (on iPhone) or Chrome (on Android) as your web browser. -2. Check your browser's permissions to make sure that the camera and microphone settings are set to "Allow" -3. Clear your web cache for Safari (on iPhone) or Chrome (on Android). -4. If using a corporate Wi-Fi network, confirm that your corporate firewall isn't blocking the website. -5. Make sure no other apps are overlapping your screen, such as the Facebook Messenger bubble, while recording the video. -6. On iPhone, if using iOS version 15 or later, disable the Hide IP address feature in Safari. -7. If possible, try these steps on another device -8. If you have another phone available, try to follow these steps on that device -If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. - -{% include faq-begin.md %} -## What is a Beneficial Owner? - -A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. - -## What do I do if the Beneficial Owner section only asks for personal details, but my organization is owned by another company? - -Please only indicate you have a Beneficial Owner, if it is an individual that owns 25% or more of the business. - -## Why can’t I input my address or upload my ID? - -Are you entering a US address? When adding a verified business bank account in Expensify, the individual adding the account, and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account, and then share access with you once verified. - -## Why am I asked for documents when adding my bank account? - -When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. -If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. - -If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist. - -## I don’t see all three microtransactions I need to validate my bank account. What should I do? - -It's a good idea to wait till the end of that second business day. If you still don’t see them, please reach out to your bank and ask them to whitelist our ACH IDs **1270239450**, **4270239450**, and **2270239450**. Expensify’s ACH Originator Name is "Expensify". - -Make sure to reach out to your Account Manager or Concierge once that's all set, and our team will be able to re-trigger those three test transactions! - -{% include faq-end.md %} diff --git a/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md new file mode 100644 index 000000000000..8cf0a18ba529 --- /dev/null +++ b/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md @@ -0,0 +1,98 @@ +--- +title: Connect a Business Bank Account +description: How to connect a business bank account to New Expensify +--- +
+ +Adding a business bank account unlocks a myriad of features and automation in Expensify, such as: +- Reimburse expenses via direct bank transfer +- Pay bills +- Collect invoice payments +- Issue the Expensify Card + +# To connect a bank account +1. Go to **Settings > Workspaces > _Workspace Name_ > Bank account > Connect bank account** +2. Click **Connect online with Plaid** +3. Click **Continue** +4. When you reach the **Plaid** screen, you'll be shown a list of compatible banks that offer direct online login access +5. Login to the business bank account: +- If the bank is not listed, click the X to go back to the connection type +- Here you’ll see the option to **Connect Manually** +- Enter your account and routing numbers +6. Enter your bank login credentials: +- If your bank requires additional security measures, you will be directed to obtain and enter a security code +- If you have more than one account available to choose from, you will be directed to choose the desired account + +## Enter company information +This is where you’ll add the legal business name as well as several other company details. + +- **Company address**: The company address must be located in the US and a physical location (If you input a maildrop address, PO box, or UPS Store, the address will be flagged for review, and adding the bank account to Expensify will be delayed) +- **Tax Identification Number**: This is the identification number that was assigned to the business by the IRS +- **Company website**: A company website is required to use most of Expensify’s payment features. When adding the website of the business, format it as, https://www.domain.com +- **Industry Classification Code**: You can locate a list of Industry Classification Codes [here]([url](https://www.census.gov/naics/?input=software&year=2022)) + +## Enter personal information +Whoever is connecting the bank account to Expensify, must enter their details under the Requestor Information section: +- The address must be a physical address +- The address must be located in the US +- The SSN must be US-issued + +This does not need to be a signor on the bank account. If someone other than the Expensify account owner enters their personal information in this section, the details will be flagged for review, and adding the bank account to Expensify will be delayed. + +## Upload ID +After entering your personal details, you’ll be prompted to click a link or scan a QR code so that you can do the following: +1. Upload a photo of the front and back of your ID (this cannot be a photo of an existing image) +2. Use your device to take a selfie and record a short video of yourself + +**Your ID must be:** +- Issued in the US +- Current (ie: the expiration date must be in the future) + +## Additional Information +Check the appropriate box under **Additional Information**, accept the agreement terms, and verify that all of the information is true and accurate: +- A Beneficial Owner refers to an **individual** who owns 25% or more of the business. +- If you or another **individual** owns 25% or more of the business, please check the appropriate box +- If someone else owns 25% or more of the business, you will be prompted to provide their personal information + +If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section. + +The details you submitted may require additional review. If that's the case, you'll receive a message from the Concierge outlining the next steps. Otherwise, your bank account will be connected automatically. + +{% include faq-begin.md %} + +## What are the general requirements for adding a business bank account? + +To add a business bank account to issue reimbursements via ACH (US), to pay invoices (US), or to issue Expensify Cards: +- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We **cannot** accept a PO Box or MailDrop location. +If you are adding the bank account to Expensify, you must do so from your Expensify account settings. +- If you are adding a bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US-issued photo ID. For using features related to US ACH, your ID must be issued by the United States. You and any Beneficial Owner (if one exists), must also have a US address +- You must have a valid website for your business to utilize the Expensify Card, or to pay invoices with Expensify. + +## What is a Beneficial Owner? + +A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. + +## What do I do if the Beneficial Owner section only asks for personal details, but my organization is owned by another company? + +Please indicate you have a Beneficial Owner only if it is an individual who owns 25% or more of the business. + +## Why can’t I input my address or upload my ID? + +Are you entering a US address? When adding a verified business bank account in Expensify, the individual adding the account and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account and then share access with you once it is verified. + +## Why am I asked for documents when adding my bank account? + +When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. +If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. + +If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist. + +## I don’t see all three microtransactions I need to validate my bank account. What should I do? + +It's a good idea to wait until the end of that second business day. If you still don’t see them, please contact your bank and ask them to whitelist our ACH IDs **1270239450**, **4270239450**, and **2270239450**. Expensify’s ACH Originator Name is "Expensify." + +Once that's all set, make sure to contact your account manager or concierge, and our team will be able to re-trigger those three test transactions! + +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/settings/Enable-Two-Factor-Authentication.md b/docs/articles/new-expensify/settings/Enable-Two-Factor-Authentication.md new file mode 100644 index 000000000000..5f0a33cc8754 --- /dev/null +++ b/docs/articles/new-expensify/settings/Enable-Two-Factor-Authentication.md @@ -0,0 +1,51 @@ +--- +title: Enable Two-Factor Authentication (2FA) +description: Add an extra layer of security for your Expensify login +--- +
+ +Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication (2FA). This will require you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) when you log in. + +To enable 2FA, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Security** in the left menu. +3. Under Security Options, click **Two Factor Authentication**. +4. Save a copy of your backup codes. This step is critical! You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes. + - Click **Download** to save a copy of your backup codes to your computer. + - Click **Copy** to paste the codes into a document or other secure location. +5. Click **Next**. +6. Download or open your authenticator app and connect it to Expensify by either: + - Scanning the QR code + - Entering the code into your authenticator app +7. Enter the 6-digit code from your authenticator app into Expensify and click **Verify**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon at the bottom of the screen. +2. Tap **Security**. +3. Under Security Options, tap **Two Factor Authentication**. +4. Save a copy of your backup codes. This step is critical! You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes. + - Tap **Download** to save a copy of your backup codes to your device. + - Tap **Copy** to paste the codes into a document or other secure location. +5. Tap **Next**. +6. Download or open your authenticator app and connect it to Expensify by either: + - Scanning the QR code + - Entering the code into your authenticator app +7. Enter the 6-digit code from your authenticator app into Expensify and tap **Verify**. +{% include end-option.html %} + +{% include end-selector.html %} + +When you log in to Expensify in the future, you’ll be emailed a magic code that you’ll use to log in with. Then you’ll be prompted to open your authenticator app to get the 6-digit code and enter it into Expensify. A new code regenerates every few seconds, so the code is always different. If the code time runs out, you can generate a new code as needed. + +{% include faq-begin.md %} +**How do I use my recovery codes if I lose access to my authenticator app?** + +Your recovery codes work the same way as your authenticator codes. Just enter a recovery code as you would the authenticator code. +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md b/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md new file mode 100644 index 000000000000..a431d34fbc0f --- /dev/null +++ b/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md @@ -0,0 +1,23 @@ +--- +title: Switch account language to Spanish +description: Change your account language +--- +
+ +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Preferences** in the left menu. +3. Click the Language option and select **Spanish**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Preferences**. +3. Tap the Language option and select **Spanish**. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/settings/Update-Notification-Preferences.md b/docs/articles/new-expensify/settings/Update-Notification-Preferences.md new file mode 100644 index 000000000000..e4111b3d02d3 --- /dev/null +++ b/docs/articles/new-expensify/settings/Update-Notification-Preferences.md @@ -0,0 +1,29 @@ +--- +title: Update notification preferences +description: Determine how you want to receive Expensify notifications +--- +
+ +To customize the email and in-app notifications you receive from Expensify, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Preferences** in the left menu. +3. Enable or disable the toggles under Notifications: + - **Receive relevant feature updates and Expensify news**: If enabled, you will receive emails and in-app notifications from Expensify about new product and company updates. + - **Mute all sounds from Expensify**: If enabled, all in-app notification sounds will be silenced. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Preferences**. +3. Enable or disable the toggles under Notifications: + - **Receive relevant feature updates and Expensify news**: If enabled, you will receive emails and in-app notifications from Expensify about new product and company updates. + - **Mute all sounds from Expensify**: If enabled, all in-app notification sounds will be silenced. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/travel/Coming-Soon.md b/docs/articles/new-expensify/travel/Coming-Soon.md new file mode 100644 index 000000000000..4d32487a14b5 --- /dev/null +++ b/docs/articles/new-expensify/travel/Coming-Soon.md @@ -0,0 +1,6 @@ +--- +title: Coming soon +description: Coming soon +--- + +# Coming soon \ No newline at end of file diff --git a/docs/assets/images/plane.svg b/docs/assets/images/plane.svg new file mode 100644 index 000000000000..0295aa3c66c0 --- /dev/null +++ b/docs/assets/images/plane.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/expensify-classic/hubs/travel/index.html b/docs/expensify-classic/hubs/travel/index.html new file mode 100644 index 000000000000..7c8c3d363d5e --- /dev/null +++ b/docs/expensify-classic/hubs/travel/index.html @@ -0,0 +1,6 @@ +--- +layout: default +title: Travel +--- + +{% include hub.html %} diff --git a/docs/new-expensify/hubs/bank-accounts-and-payments/index.html b/docs/new-expensify/hubs/bank-accounts-and-payments/index.html deleted file mode 100644 index 94db3c798710..000000000000 --- a/docs/new-expensify/hubs/bank-accounts-and-payments/index.html +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: default -title: Bank Accounts & Payments ---- - -{% include hub.html %} \ No newline at end of file diff --git a/docs/new-expensify/hubs/travel/index.html b/docs/new-expensify/hubs/travel/index.html new file mode 100644 index 000000000000..7c8c3d363d5e --- /dev/null +++ b/docs/new-expensify/hubs/travel/index.html @@ -0,0 +1,6 @@ +--- +layout: default +title: Travel +--- + +{% include hub.html %} diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bd7acc13d8d3..818a65910057 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -788,6 +788,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/import/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/taxes` as const, }, + POLICY_ACCOUNTING_XERO_EXPORT: { + route: 'settings/workspaces/:policyID/accounting/xero/export', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export` as const, + }, POLICY_ACCOUNTING_XERO_ADVANCED: { route: 'settings/workspaces/:policyID/accounting/xero/advanced', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5a0b29508e3a..29532bb9b3b6 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -244,6 +244,7 @@ const SCREENS = { XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers', XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer', XERO_TAXES: 'Policy_Accounting_Xero_Taxes', + XERO_EXPORT: 'Policy_Accounting_Xero_Export', XERO_ADVANCED: 'Policy_Accounting_Xero_Advanced', XERO_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Invoice_Account_Selector', XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector', diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 9484cff26005..40ebebdd0488 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -38,7 +38,6 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC return [ { name: selectedCategory, - enabled: true, accountID: undefined, isSelected: true, }, diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index bf35d65340fc..42de7e2fb7f4 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -141,6 +141,12 @@ type MenuItemBaseProps = { /** A description text to show under the title */ description?: string; + /** Text to show below menu item. This text is not interactive */ + helperText?: string; + + /** Any additional styles to pass to helper text. */ + helperTextStyle?: StyleProp; + /** Should the description be shown above the title (instead of the other way around) */ shouldShowDescriptionOnTop?: boolean; @@ -296,6 +302,8 @@ function MenuItem( furtherDetailsIcon, furtherDetails, description, + helperText, + helperTextStyle, error, errorText, success = false, @@ -679,6 +687,7 @@ function MenuItem( )} + {!!helperText && {helperText}} ); } diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index b97578210ad9..cad1c6c1bcf4 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -302,10 +302,12 @@ function MoneyRequestConfirmationList({ const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.canSendInvoice(allPolicies) && !!transaction?.isFromGlobalCreate, [allPolicies, transaction?.isFromGlobalCreate]); // A flag for showing the tags field - const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); + // TODO: remove the !isTypeInvoice from this condition after BE supports tags for invoices: https://github.com/Expensify/App/issues/41281 + const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists) && !isTypeInvoice, [isPolicyExpenseChat, policyTagLists, isTypeInvoice]); // A flag for showing tax rate - const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy); + // TODO: remove the !isTypeInvoice from this condition after BE supports tax for invoices: https://github.com/Expensify/App/issues/41281 + const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy) && !isTypeInvoice; // A flag for showing the billable field const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index ccd1cb883246..5876e476afa2 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -72,7 +72,7 @@ function BaseListItem({ } onSelectRow(item); }} - disabled={isDisabled} + disabled={isDisabled && !item.isSelected} accessibilityLabel={item.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 97cd9aa5c691..cbd9418a83e9 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -15,6 +15,7 @@ import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/ type SelectedTagOption = { name: string; enabled: boolean; + isSelected?: boolean; accountID: number | undefined; }; diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index 0aed28681d5c..9553fe4451ad 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -53,14 +53,14 @@ function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerPr return [ { - name: selectedTaxRate, - enabled: true, + modifiedName: selectedTaxRate, + isDisabled: false, accountID: null, }, ]; }, [selectedTaxRate]); - const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Category[], searchValue), [taxRates, searchValue, selectedOptions]); + const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Tax[], searchValue), [taxRates, searchValue, selectedOptions]); const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(sections[0].data.length > 0, searchValue); diff --git a/src/languages/en.ts b/src/languages/en.ts index 84f5c6cd2949..3b9e2217b682 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2026,11 +2026,26 @@ export default { customersDescription: 'Import customer contacts. Billable expenses need tags for export. Expenses will carry the customer information to Xero for sales invoices.', taxesDescription: 'Choose whether to import tax rates and tax defaults from your accounting integration.', notImported: 'Not imported', + export: 'Export', + exportDescription: 'Configure how data in Expensify gets exported to Xero.', + exportCompanyCard: 'Export company card expenses as', + purchaseBill: 'Purchase bill', + exportDeepDiveCompanyCard: + 'Each exported expense posts as a bank transaction to the Xero bank account you select below, and transaction dates will match the dates on your bank statement.', + bankTransactions: 'Bank transactions', + xeroBankAccount: 'Xero bank account', + preferredExporter: 'Preferred exporter', + exportExpenses: 'Export out-of-pocket expenses as', + exportExpensesDescription: 'Reports will export as a purchase bill, using the date and status you select below.', + purchaseBillDate: 'Purchase bill date', + exportInvoices: 'Export invoices as', + salesInvoice: 'Sales invoice', + exportInvoicesDescription: 'Sales invoices always display the date on which the invoice was sent.', advancedConfig: { advanced: 'Advanced', autoSync: 'Auto-Sync', autoSyncDescription: 'Sync Xero and Expensify automatically every day.', - purchaseBillStatusTitle: 'Set purchase bill status (optional)', + purchaseBillStatusTitle: 'Purchase bill status', reimbursedReports: 'Sync reimbursed reports', reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Xero account below.', xeroBillPaymentAccount: 'Xero Bill Payment Account', diff --git a/src/languages/es.ts b/src/languages/es.ts index 701fe7a91802..ac110ec6c59a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2059,11 +2059,26 @@ export default { 'Importar contactos de clientes. Los gastos facturables necesitan etiquetas para la exportación. Los gastos llevarán la información del cliente a Xero para las facturas de ventas.', taxesDescription: 'Elige si quires importar las tasas de impuestos y los impuestos por defecto de tu integración de contaduría.', notImported: 'No importado', + export: 'Exportar', + exportDescription: 'Configura cómo se exportan los datos de Expensify a Xero.', + exportCompanyCard: 'Exportar gastos de la tarjeta de empresa como', + purchaseBill: 'Factura de compra', + exportDeepDiveCompanyCard: + 'Cada gasto exportado se contabiliza como una transacción bancaria en la cuenta bancaria de Xero que selecciones a continuación. Las fechas de las transacciones coincidirán con las fechas de el extracto bancario.', + bankTransactions: 'Transacciones bancarias', + xeroBankAccount: 'Cuenta bancaria de Xero', + preferredExporter: 'Exportador preferido', + exportExpenses: 'Exportar gastos por cuenta propia como', + exportExpensesDescription: 'Los informes se exportarán como una factura de compra utilizando la fecha y el estado que seleccione a continuación', + purchaseBillDate: 'Fecha de la factura de compra', + exportInvoices: 'Exportar facturas como', + salesInvoice: 'Factura de venta', + exportInvoicesDescription: 'Las facturas de venta siempre muestran la fecha en la que se envió la factura.', advancedConfig: { advanced: 'Avanzado', autoSync: 'Autosincronización', autoSyncDescription: 'Sincroniza Xero y Expensify automáticamente todos los días.', - purchaseBillStatusTitle: 'Set purchase bill status (optional)', + purchaseBillStatusTitle: 'Estado de la factura de compra', reimbursedReports: 'Sincronizar informes reembolsados', reimbursedReportsDescription: 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Xero indicadas a continuación.', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 5b852b89adfa..c2b216214290 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -305,6 +305,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/xero/XeroOrganizationConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: () => require('../../../../pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: () => require('../../../../pages/workspace/accounting/xero/XeroTaxesConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT]: () => require('../../../../pages/workspace/accounting/xero/export/XeroExportConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: () => require('../../../../pages/workspace/accounting/xero/advanced/XeroAdvancedPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_INVOICE_ACCOUNT_SELECTOR]: () => require('../../../../pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 70fa6453a94c..1aeb48e71133 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -44,6 +44,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION, SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER, SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES, + SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT, SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED, SCREENS.WORKSPACE.ACCOUNTING.XERO_INVOICE_ACCOUNT_SELECTOR, SCREENS.WORKSPACE.ACCOUNTING.XERO_BILL_PAYMENT_ACCOUNT_SELECTOR, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 7f9836210ad6..d6fe4c4e8d90 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -331,6 +331,7 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: {path: ROUTES.POLICY_ACCOUNTING_XERO_CUSTOMER.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TAXES.route}, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT]: {path: ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ADVANCED.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_INVOICE_ACCOUNT_SELECTOR]: {path: ROUTES.POLICY_ACCOUNTING_XERO_INVOICE_SELECTOR.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_BILL_PAYMENT_ACCOUNT_SELECTOR]: {path: ROUTES.POLICY_ACCOUNTING_XERO_BILL_PAYMENT_ACCOUNT_SELECTOR.route}, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index d503c3ca5195..ed0e135d7031 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -324,6 +324,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT]: { + policyID: string; + }; [SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: { policyID: string; }; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ce02aec14fb4..12209cfcb361 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -126,6 +126,12 @@ type Category = { isSelected?: boolean; }; +type Tax = { + modifiedName: string; + isSelected?: boolean; + isDisabled?: boolean; +}; + type Hierarchy = Record; type GetOptionsConfig = { @@ -1010,12 +1016,21 @@ function getCategoryListSections( ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - + const enabledCategoriesNames = enabledCategories.map((category) => category.name); + const selectedOptionsWithDisabledState: Category[] = []; const categorySections: CategoryTreeSection[] = []; const numberOfEnabledCategories = enabledCategories.length; + selectedOptions.forEach((option) => { + if (enabledCategoriesNames.includes(option.name)) { + selectedOptionsWithDisabledState.push({...option, isSelected: true, enabled: true}); + return; + } + selectedOptionsWithDisabledState.push({...option, isSelected: true, enabled: false}); + }); + if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptions, true); + const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); categorySections.push({ // "Selected" section title: '', @@ -1028,9 +1043,10 @@ function getCategoryListSections( } if (searchInputValue) { + const categoriesForSearch = [...selectedOptionsWithDisabledState, ...enabledCategories]; const searchCategories: Category[] = []; - enabledCategories.forEach((category) => { + categoriesForSearch.forEach((category) => { if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) { return; } @@ -1053,7 +1069,7 @@ function getCategoryListSections( } if (selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptions, true); + const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); categorySections.push({ // "Selected" section title: '', @@ -1144,34 +1160,42 @@ function getTagListSections( const tagSections = []; const sortedTags = sortTags(tags) as PolicyTag[]; const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledTags = [...selectedOptions, ...sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name))]; + const enabledTags = sortedTags.filter((tag) => tag.enabled); + const enabledTagsNames = enabledTags.map((tag) => tag.name); + const enabledTagsWithoutSelectedOptions = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); + const selectedTagsWithDisabledState: SelectedTagOption[] = []; const numberOfTags = enabledTags.length; + selectedOptions.forEach((tag) => { + if (enabledTagsNames.includes(tag.name)) { + selectedTagsWithDisabledState.push({...tag, enabled: true}); + return; + } + selectedTagsWithDisabledState.push({...tag, enabled: false}); + }); + // If all tags are disabled but there's a previously selected tag, show only the selected tag if (numberOfTags === 0 && selectedOptions.length > 0) { - const selectedTagOptions = selectedOptions.map((option) => ({ - name: option.name, - // Should be marked as enabled to be able to be de-selected - enabled: true, - })); tagSections.push({ // "Selected" section title: '', shouldShow: false, - data: getTagsOptions(selectedTagOptions, selectedOptions), + data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); return tagSections; } if (searchInputValue) { - const searchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const enabledSearchTags = enabledTagsWithoutSelectedOptions.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const selectedSearchTags = selectedTagsWithDisabledState.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const tagsForSearch = [...selectedSearchTags, ...enabledSearchTags]; tagSections.push({ // "Search" section title: '', shouldShow: true, - data: getTagsOptions(searchTags, selectedOptions), + data: getTagsOptions(tagsForSearch, selectedOptions), }); return tagSections; @@ -1182,7 +1206,7 @@ function getTagListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTagsOptions(enabledTags, selectedOptions), + data: getTagsOptions([...selectedTagsWithDisabledState, ...enabledTagsWithoutSelectedOptions], selectedOptions), }); return tagSections; @@ -1194,20 +1218,13 @@ function getTagListSections( return !!tagObject?.enabled && !selectedOptionNames.includes(recentlyUsedTag); }) .map((tag) => ({name: tag, enabled: true})); - const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); if (selectedOptions.length) { - const selectedTagOptions = selectedOptions.map((option) => ({ - name: option.name, - // Should be marked as enabled to be able to unselect even though the selected category is disabled - enabled: true, - })); - tagSections.push({ // "Selected" section title: '', shouldShow: true, - data: getTagsOptions(selectedTagOptions, selectedOptions), + data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); } @@ -1226,7 +1243,7 @@ function getTagListSections( // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getTagsOptions(filteredTags, selectedOptions), + data: getTagsOptions(enabledTagsWithoutSelectedOptions, selectedOptions), }); return tagSections; @@ -1349,46 +1366,56 @@ function getTaxRatesOptions(taxRates: Array>): TaxRatesOption[] tooltipText: taxRate.modifiedName, isDisabled: taxRate.isDisabled, data: taxRate, + isSelected: taxRate.isSelected, })); } /** * Builds the section list for tax rates */ -function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): TaxSection[] { +function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Tax[], searchInputValue: string): TaxSection[] { const policyRatesSections = []; const taxes = transformedTaxRates(taxRates); const sortedTaxRates = sortTaxRates(taxes); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName); const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); + const enabledTaxRatesNames = enabledTaxRates.map((tax) => tax.modifiedName); + const enabledTaxRatesWithoutSelectedOptions = enabledTaxRates.filter((tax) => tax.modifiedName && !selectedOptionNames.includes(tax.modifiedName)); + const selectedTaxRateWithDisabledState: Tax[] = []; const numberOfTaxRates = enabledTaxRates.length; + selectedOptions.forEach((tax) => { + if (enabledTaxRatesNames.includes(tax.modifiedName)) { + selectedTaxRateWithDisabledState.push({...tax, isDisabled: false, isSelected: true}); + return; + } + selectedTaxRateWithDisabledState.push({...tax, isDisabled: true, isSelected: true}); + }); + // If all tax are disabled but there's a previously selected tag, show only the selected tag if (numberOfTaxRates === 0 && selectedOptions.length > 0) { - const selectedTaxRateOptions = selectedOptions.map((option) => ({ - modifiedName: option.name, - // Should be marked as enabled to be able to be de-selected - isDisabled: false, - })); policyRatesSections.push({ // "Selected" sectiong title: '', shouldShow: false, - data: getTaxRatesOptions(selectedTaxRateOptions), + data: getTaxRatesOptions(selectedTaxRateWithDisabledState), }); return policyRatesSections; } if (searchInputValue) { - const searchTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); + const enabledSearchTaxRates = enabledTaxRatesWithoutSelectedOptions.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); + const selectedSearchTags = selectedTaxRateWithDisabledState.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); + const taxesForSearch = [...selectedSearchTags, ...enabledSearchTaxRates]; policyRatesSections.push({ // "Search" section title: '', shouldShow: true, - data: getTaxRatesOptions(searchTaxRates), + data: getTaxRatesOptions(taxesForSearch), }); return policyRatesSections; @@ -1399,30 +1426,18 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTaxRatesOptions(enabledTaxRates), + data: getTaxRatesOptions([...selectedTaxRateWithDisabledState, ...enabledTaxRatesWithoutSelectedOptions]), }); return policyRatesSections; } - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const filteredTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName && !selectedOptionNames.includes(taxRate.modifiedName)); - if (selectedOptions.length > 0) { - const selectedTaxRatesOptions = selectedOptions.map((option) => { - const taxRateObject = Object.values(taxes).find((taxRate) => taxRate.modifiedName === option.name); - - return { - modifiedName: option.name, - isDisabled: !!taxRateObject?.isDisabled, - }; - }); - policyRatesSections.push({ // "Selected" section title: '', shouldShow: true, - data: getTaxRatesOptions(selectedTaxRatesOptions), + data: getTaxRatesOptions(selectedTaxRateWithDisabledState), }); } @@ -1430,7 +1445,7 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "All" section when number of items are more than the threshold title: '', shouldShow: true, - data: getTaxRatesOptions(filteredTaxRates), + data: getTaxRatesOptions(enabledTaxRatesWithoutSelectedOptions), }); return policyRatesSections; @@ -1684,7 +1699,7 @@ function getOptions( } if (includeTaxRates) { - const taxRatesOptions = getTaxRatesSection(taxRates, selectedOptions as Category[], searchInputValue); + const taxRatesOptions = getTaxRatesSection(taxRates, selectedOptions as Tax[], searchInputValue); return { recentReports: [], @@ -2424,4 +2439,4 @@ export { getFirstKeyForList, }; -export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree}; +export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, Tax, TaxRatesOption, Option, OptionTree}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 641d3ddaa268..fb3371e94491 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6686,6 +6686,7 @@ export { isExpensifyOnlyParticipantInReport, isGroupChat, isGroupChatAdmin, + isGroupPolicy, isReportInGroupPolicy, isHoldCreator, isIOUOwnedByCurrentUser, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d71664a959ed..3154ae218d72 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1220,8 +1220,8 @@ function togglePinnedState(reportID: string, isPinnedChat: boolean) { * tab, refresh etc without worrying about loosing what they typed out. * When empty string or null is passed, it will delete the draft comment from Onyx store. */ -function saveReportDraftComment(reportID: string, comment: string | null) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, prepareDraftComment(comment)); +function saveReportDraftComment(reportID: string, comment: string | null, callback: () => void = () => {}) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, prepareDraftComment(comment)).then(callback); } /** Broadcasts whether or not a user is typing on a report over the report's private pusher channel. */ @@ -1252,13 +1252,17 @@ function handleReportChanged(report: OnyxEntry) { // In this case, the API will let us know by returning a preexistingReportID. // We should clear out the optimistically created report and re-route the user to the preexisting report. if (report?.reportID && report.preexistingReportID) { - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null); - + let callback = () => {}; // Only re-route them if they are still looking at the optimistically created report if (Navigation.getActiveRoute().includes(`/r/${report.reportID}`)) { - // Pass 'FORCED_UP' type to replace new report on second login with proper one in the Navigation - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID), CONST.NAVIGATION.TYPE.FORCED_UP); + callback = () => { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID ?? ''), CONST.NAVIGATION.TYPE.FORCED_UP); + }; } + DeviceEventEmitter.emit(`switchToPreExistingReport_${report.reportID}`, { + preexistingReportID: report.preexistingReportID, + callback, + }); return; } diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 013bc484fc63..105eadffd436 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -396,9 +396,10 @@ const ContextMenuActions: ContextMenuAction[] = [ return type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction); }, onPress: (closePopover, {reportAction, reportID}) => { + const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); Environment.getEnvironmentURL().then((environmentURL) => { const reportActionID = reportAction?.reportActionID; - Clipboard.setString(`${environmentURL}/r/${reportID}/${reportActionID}`); + Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); }); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 469a7300a84f..3120bbe9bed2 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -12,7 +12,7 @@ import type { TextInputKeyPressEventData, TextInputSelectionChangeEventData, } from 'react-native'; -import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; +import {DeviceEventEmitter, findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {useAnimatedRef} from 'react-native-reanimated'; @@ -344,6 +344,20 @@ function ComposerWithSuggestions( [], ); + useEffect(() => { + const switchToCurrentReport = DeviceEventEmitter.addListener(`switchToPreExistingReport_${reportID}`, ({preexistingReportID, callback}) => { + if (!commentRef.current) { + callback(); + return; + } + Report.saveReportDraftComment(preexistingReportID, commentRef.current, callback); + }); + + return () => { + switchToCurrentReport.remove(); + }; + }, [reportID]); + /** * Find the newly added characters between the previous text and the new text based on the selection. * diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 0bd546318186..d4919a4172aa 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -83,9 +83,11 @@ function IOURequestStepCategory({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const reportAction = reportActions?.[report?.parentReportActionID || reportActionID] ?? null; - // The transactionCategory can be an empty string, so to maintain the logic we'd like to keep it in this shape until utils refactor - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowCategory = ReportUtils.isReportInGroupPolicy(report, policy) && (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const shouldShowCategory = + (ReportUtils.isReportInGroupPolicy(report) || ReportUtils.isGroupPolicy(policy?.type ?? '')) && + // The transactionCategory can be an empty string, so to maintain the logic we'd like to keep it in this shape until utils refactor + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); @@ -153,7 +155,7 @@ function IOURequestStepCategory({ {translate('iou.categorySelection')} @@ -170,19 +172,19 @@ const IOURequestStepCategoryWithOnyx = withOnyx `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, report)}`, }, policyDraft: { - key: ({reportDraft}) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${reportDraft ? reportDraft.policyID : '0'}`, + key: ({reportDraft, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`, }, policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, report)}`, }, policyCategoriesDraft: { - key: ({reportDraft}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${reportDraft ? reportDraft.policyID : '0'}`, + key: ({reportDraft, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`, }, policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${IOU.getIOURequestPolicyID(transaction, report)}`, }, reportActions: { key: ({ diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 831d58c43434..e458e57ae3bb 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -146,7 +146,7 @@ function IOURequestStepConfirmation({ }) ?? [], [transaction?.participants, personalDetails, iouType], ); - const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); + const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)) || ReportUtils.isGroupPolicy(policy?.type ?? ''), [report, policy]); const formHasBeenSubmitted = useRef(false); useEffect(() => { @@ -574,7 +574,7 @@ function IOURequestStepConfirmation({ iouType={iouType} reportID={reportID} isPolicyExpenseChat={isPolicyExpenseChat} - policyID={report?.policyID} + policyID={report?.policyID ?? policy?.id} bankAccountRoute={ReportUtils.getBankAccountRoute(report)} iouMerchant={transaction?.merchant} iouCreated={transaction?.created} diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 0b40cd46fd3a..fbd5b669fa62 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -92,7 +92,7 @@ function accountingIntegrationData( /> ), onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.getRoute(policyID)), - onExportPagePress: () => {}, + onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID)), onAdvancedPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ADVANCED.getRoute(policyID)), }; default: diff --git a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx new file mode 100644 index 000000000000..934c41dab614 --- /dev/null +++ b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import ConnectionLayout from '@components/ConnectionLayout'; +import type {MenuItemProps} from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; + +type MenuItem = MenuItemProps & {pendingAction?: OfflineWithFeedbackProps['pendingAction']}; + +function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? ''; + const policyOwner = policy?.owner ?? ''; + + const {export: exportConfiguration, errorFields, pendingFields} = policy?.connections?.xero?.config ?? {}; + const menuItems: MenuItem[] = [ + { + description: translate('workspace.xero.preferredExporter'), + onPress: () => {}, + brickRoadIndicator: errorFields?.exporter ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: exportConfiguration?.exporter ?? policyOwner, + pendingAction: pendingFields?.export, + error: errorFields?.exporter ? translate('common.genericErrorMessage') : undefined, + }, + { + description: translate('workspace.xero.exportExpenses'), + title: translate('workspace.xero.purchaseBill'), + interactive: false, + shouldShowRightIcon: false, + helperText: translate('workspace.xero.exportExpensesDescription'), + }, + { + description: translate('workspace.xero.purchaseBillDate'), + brickRoadIndicator: errorFields?.billDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: exportConfiguration?.billDate, + pendingAction: pendingFields?.export, + error: errorFields?.billDate ? translate('common.genericErrorMessage') : undefined, + }, + { + description: translate('workspace.xero.advancedConfig.purchaseBillStatusTitle'), + onPress: () => {}, + title: exportConfiguration?.billStatus?.purchase, + pendingAction: pendingFields?.export, + error: errorFields?.purchase ? translate('common.genericErrorMessage') : undefined, + }, + { + description: translate('workspace.xero.exportInvoices'), + title: translate('workspace.xero.salesInvoice'), + interactive: false, + shouldShowRightIcon: false, + helperText: translate('workspace.xero.exportInvoicesDescription'), + }, + { + description: translate('workspace.xero.exportCompanyCard'), + title: translate('workspace.xero.bankTransactions'), + shouldShowRightIcon: false, + interactive: false, + helperText: translate('workspace.xero.exportDeepDiveCompanyCard'), + }, + { + description: translate('workspace.xero.xeroBankAccount'), + onPress: () => {}, + brickRoadIndicator: errorFields?.nonReimbursableAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: undefined, + pendingAction: pendingFields?.export, + error: undefined, + }, + ]; + + return ( + + {menuItems.map((menuItem) => ( + + + + ))} + + ); +} + +XeroExportConfigurationPage.displayName = 'XeroExportConfigurationPage'; + +export default withPolicyConnections(XeroExportConfigurationPage); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 3a322405c6e1..04a95267215c 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -61,6 +61,9 @@ type TaxRate = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Indicates if the tax rate is disabled. */ isDisabled?: boolean; + /** Indicates if the tax rate is selected. */ + isSelected?: boolean; + /** An error message to display to the user */ errors?: OnyxCommon.Errors; diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts new file mode 100644 index 000000000000..a17179d8f7af --- /dev/null +++ b/tests/actions/PolicyTaxTest.ts @@ -0,0 +1,884 @@ +import Onyx from 'react-native-onyx'; +import {createPolicyTax, deletePolicyTaxes, renamePolicyTax, setPolicyTaxesEnabled, updatePolicyTaxValue} from '@libs/actions/TaxRate'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as Policy from '@src/libs/actions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy as PolicyType, TaxRate} from '@src/types/onyx'; +import createRandomPolicy from '../utils/collections/policies'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +OnyxUpdateManager(); +describe('actions/PolicyTax', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); + return Onyx.clear() + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy)) + .then(waitForBatchedUpdates); + }); + + describe('SetPolicyCustomTaxName', () => { + it('Set policy`s custom tax name', () => { + const customTaxName = 'Custom tag name'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.name).toBe(customTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.pendingFields?.name).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + it('Reset policy`s custom tax name when API returns an error', () => { + const customTaxName = 'Custom tag name'; + const originalCustomTaxName = fakePolicy?.taxRates?.name; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.name).toBe(customTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume() as Promise; + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.name).toBe(originalCustomTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBeFalsy(); + expect(policy?.taxRates?.errorFields?.name).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); + }); + + describe('SetPolicyCurrencyDefaultTax', () => { + it('Set policy`s currency default tax', () => { + const taxCode = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.defaultExternalID).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + it('Reset policy`s currency default tax when API returns an error', () => { + const taxCode = 'id_TAX_RATE_1'; + const originalDefaultExternalID = fakePolicy?.taxRates?.defaultExternalID; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.defaultExternalID).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume() as Promise; + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.defaultExternalID).toBe(originalDefaultExternalID); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBeFalsy(); + expect(policy?.taxRates?.errorFields?.defaultExternalID).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); + }); + describe('SetPolicyForeignCurrencyDefaultTax', () => { + it('Set policy`s foreign currency default', () => { + const taxCode = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.foreignTaxDefault).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + // Check if the policy pendingFields was cleared + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + it('Reset policy`s foreign currency default when API returns an error', () => { + const taxCode = 'id_TAX_RATE_1'; + const originalDefaultForeignCurrencyID = fakePolicy?.taxRates?.foreignTaxDefault; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.foreignTaxDefault).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume() as Promise; + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + // Check if the policy pendingFields was cleared + expect(policy?.taxRates?.foreignTaxDefault).toBe(originalDefaultForeignCurrencyID); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(policy?.taxRates?.errorFields?.foreignTaxDefault).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); + }); + describe('CreatePolicyTax', () => { + it('Create a new tax', () => { + const newTaxRate: TaxRate = { + name: 'Tax rate 2', + value: '2%', + code: 'id_TAX_RATE_2', + }; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + createPolicyTax(fakePolicy.id, newTaxRate); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.code).toBe(newTaxRate.code); + expect(createdTax?.name).toBe(newTaxRate.name); + expect(createdTax?.value).toBe(newTaxRate.value); + expect(createdTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.errors).toBeFalsy(); + expect(createdTax?.pendingFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('Remove the optimistic tax if the API returns an error', () => { + const newTaxRate: TaxRate = { + name: 'Tax rate 2', + value: '2%', + code: 'id_TAX_RATE_2', + }; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + createPolicyTax(fakePolicy.id, newTaxRate); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.code).toBe(newTaxRate.code); + expect(createdTax?.name).toBe(newTaxRate.name); + expect(createdTax?.value).toBe(newTaxRate.value); + expect(createdTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume() as Promise; + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.errors).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); + }); + describe('SetPolicyTaxesEnabled', () => { + it('Disable policy`s taxes', () => { + const disableTaxID = 'id_TAX_RATE_1'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + expect(disabledTax?.pendingFields?.isDisabled).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('Disable policy`s taxes but API returns an error, then enable policy`s taxes again', () => { + const disableTaxID = 'id_TAX_RATE_1'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); + const originalTaxes = {...fakePolicy?.taxRates?.taxes}; + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume() as Promise; + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBe(!!originalTaxes[disableTaxID].isDisabled); + expect(disabledTax?.errorFields?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBeFalsy(); + resolve(); + }, + }); + }), + ); + }); + }); + + describe('RenamePolicyTax', () => { + it('Rename tax', () => { + const taxID = 'id_TAX_RATE_1'; + const newTaxName = 'Tax rate 1 updated'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + renamePolicyTax(fakePolicy.id, taxID, newTaxName); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(newTaxName); + expect(updatedTax?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.name).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.errorFields?.name).toBeFalsy(); + expect(updatedTax?.pendingFields?.name).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('Rename tax but API returns an error, then recover the original tax`s name', () => { + const taxID = 'id_TAX_RATE_1'; + const newTaxName = 'Tax rate 1 updated'; + const originalTaxRate = {...fakePolicy?.taxRates?.taxes[taxID]}; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + renamePolicyTax(fakePolicy.id, taxID, newTaxName); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(newTaxName); + expect(updatedTax?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.name).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume() as Promise; + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(originalTaxRate.name); + expect(updatedTax?.errorFields?.name).toBeTruthy(); + expect(updatedTax?.pendingFields?.name).toBeFalsy(); + resolve(); + }, + }); + }), + ); + }); + }); + describe('UpdatePolicyTaxValue', () => { + it('Update tax`s value', () => { + const taxID = 'id_TAX_RATE_1'; + const newTaxValue = 10; + const stringTaxValue = `${newTaxValue}%`; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(stringTaxValue); + expect(updatedTax?.pendingFields?.value).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.value).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.errorFields?.value).toBeFalsy(); + expect(updatedTax?.pendingFields?.value).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('Update tax`s value but API returns an error, then recover the original tax`s value', () => { + const taxID = 'id_TAX_RATE_1'; + const newTaxValue = 10; + const originalTaxRate = {...fakePolicy?.taxRates?.taxes[taxID]}; + const stringTaxValue = `${newTaxValue}%`; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(stringTaxValue); + expect(updatedTax?.pendingFields?.value).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.value).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume() as Promise; + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(originalTaxRate.value); + expect(updatedTax?.errorFields?.value).toBeTruthy(); + expect(updatedTax?.pendingFields?.value).toBeFalsy(); + resolve(); + }, + }); + }), + ); + }); + }); + describe('DeletePolicyTaxes', () => { + it('Delete tax that is not foreignTaxDefault', () => { + const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; + const taxID = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + deletePolicyTaxes(fakePolicy.id, [taxID]); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(taxRates?.foreignTaxDefault).toBe(foreignTaxDefault); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('Delete tax that is foreignTaxDefault', () => { + const taxID = 'id_TAX_RATE_1'; + const firstTaxID = 'id_TAX_EXEMPT'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, {taxRates: {foreignTaxDefault: 'id_TAX_RATE_1'}}) + .then(() => { + deletePolicyTaxes(fakePolicy.id, [taxID]); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(taxRates?.foreignTaxDefault).toBe(firstTaxID); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('Delete tax that is not foreignTaxDefault but API return an error, then recover the delated tax', () => { + const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; + const taxID = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + deletePolicyTaxes(fakePolicy.id, [taxID]); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(taxRates?.foreignTaxDefault).toBe(foreignTaxDefault); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume() as Promise; + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax?.pendingAction).toBeFalsy(); + expect(deletedTax?.errors).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); + }); +}); diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 76b4324f697b..0df4e2fe124b 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1067,8 +1067,8 @@ describe('OptionsListUtils', () => { keyForList: 'Medical', searchText: 'Medical', tooltipText: 'Medical', - isDisabled: false, - isSelected: false, + isDisabled: true, + isSelected: true, }, ], }, @@ -1236,8 +1236,8 @@ describe('OptionsListUtils', () => { keyForList: 'Medical', searchText: 'Medical', tooltipText: 'Medical', - isDisabled: false, - isSelected: false, + isDisabled: true, + isSelected: true, }, ], }, @@ -2587,6 +2587,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax exempt 1 (0%) • Default', tooltipText: 'Tax exempt 1 (0%) • Default', isDisabled: undefined, + isSelected: undefined, // creates a data option. data: { name: 'Tax exempt 1', @@ -2601,6 +2602,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax option 3 (5%)', tooltipText: 'Tax option 3 (5%)', isDisabled: undefined, + isSelected: undefined, data: { name: 'Tax option 3', code: 'CODE3', @@ -2614,6 +2616,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', isDisabled: undefined, + isSelected: undefined, data: { name: 'Tax rate 2', code: 'CODE2', @@ -2637,6 +2640,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', isDisabled: undefined, + isSelected: undefined, data: { name: 'Tax rate 2', code: 'CODE2',