diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index d55b47a90ca..2628e938e21 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -147,7 +147,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount; const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout); - const confirmPayment = (type?: PaymentMethodType | undefined) => { + const confirmPayment = (type?: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type || !chatReport) { return; } @@ -156,7 +156,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) { setIsHoldMenuVisible(true); } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) { - IOU.payInvoice(type, chatReport, moneyRequestReport); + IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, moneyRequestReport, true); } diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index c796a267fd0..3c27ec64d13 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -138,6 +138,7 @@ function ReportPreview({ const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); + const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); const isApproved = ReportUtils.isReportApproved(iouReport, action); @@ -177,7 +178,7 @@ function ReportPreview({ [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled], ); - const confirmPayment = (type: PaymentMethodType | undefined) => { + const confirmPayment = (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type) { return; } @@ -187,7 +188,7 @@ function ReportPreview({ setIsHoldMenuVisible(true); } else if (chatReport && iouReport) { if (ReportUtils.isInvoiceReport(iouReport)) { - IOU.payInvoice(type, chatReport, iouReport); + IOU.payInvoice(type, chatReport, iouReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, iouReport); } @@ -246,7 +247,16 @@ function ReportPreview({ if (isScanning) { return translate('common.receipt'); } - let payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + + let payerOrApproverName; + if (isPolicyExpenseChat) { + payerOrApproverName = ReportUtils.getPolicyName(chatReport); + } else if (isInvoiceRoom) { + payerOrApproverName = ReportUtils.getInvoicePayerName(chatReport); + } else { + payerOrApproverName = ReportUtils.getDisplayNameForParticipant(managerID, true); + } + if (isApproved) { return translate('iou.managerApproved', {manager: payerOrApproverName}); } diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index 7a7e4e58436..8375498ed4b 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -5,11 +5,13 @@ import {useOnyx, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import * as IOU from '@userActions/IOU'; +import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -41,7 +43,7 @@ type SettlementButtonOnyxProps = { type SettlementButtonProps = SettlementButtonOnyxProps & { /** Callback to execute when this button is pressed. Receives a single payment type argument. */ - onPress: (paymentType?: PaymentMethodType) => void; + onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean) => void; /** The route to redirect if user does not have a payment method setup */ enablePaymentsRoute: EnablePaymentsRoute; @@ -143,6 +145,9 @@ function SettlementButton({ }: SettlementButtonProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + + const primaryPolicy = useMemo(() => PolicyActions.getPrimaryPolicy(activePolicyID), [activePolicyID]); const session = useSession(); // The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here. @@ -199,20 +204,39 @@ function SettlementButton({ } if (isInvoiceReport) { - buttonOptions.push({ - text: translate('iou.settlePersonal', {formattedAmount}), - icon: Expensicons.User, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - backButtonText: translate('iou.individual'), - subMenuItems: [ - { - text: translate('iou.payElsewhere', {formattedAmount: ''}), - icon: Expensicons.Cash, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), - }, - ], - }); + if (ReportUtils.isIndividualInvoiceRoom(chatReport)) { + buttonOptions.push({ + text: translate('iou.settlePersonal', {formattedAmount}), + icon: Expensicons.User, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.individual'), + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), + }, + ], + }); + } + + if (PolicyUtils.isPolicyAdmin(primaryPolicy) && PolicyUtils.isPaidGroupPolicy(primaryPolicy)) { + buttonOptions.push({ + text: translate('iou.settleBusiness', {formattedAmount}), + icon: Expensicons.Building, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.business'), + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, true), + }, + ], + }); + } } if (shouldShowApproveButton) { @@ -226,7 +250,7 @@ function SettlementButton({ return buttonOptions; // We don't want to reorder the options when the preferred payment method changes while the button is still visible // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currency, formattedAmount, iouReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); + }, [currency, formattedAmount, iouReport, chatReport, policyID, translate, shouldHidePaymentOptions, primaryPolicy, shouldShowApproveButton, shouldDisableApproveButton]); const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => { if (policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) { @@ -259,7 +283,7 @@ function SettlementButton({ return ( onPress(paymentType)} enablePaymentsRoute={enablePaymentsRoute} addBankAccountRoute={addBankAccountRoute} addDebitCardRoute={addDebitCardRoute} diff --git a/src/languages/en.ts b/src/languages/en.ts index 93694100307..c7e006d754a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -699,6 +699,7 @@ export default { settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', individual: 'Individual', + business: 'Business', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as an individual` : `Pay as an individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pay ${formattedAmount}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 59aad3275c4..83dcaae008b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -693,6 +693,7 @@ export default { settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', individual: 'Individual', + business: 'Empresa', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pago ${formattedAmount} como individuo` : `Pago individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pagar ${formattedAmount}`, diff --git a/src/libs/API/parameters/PayInvoiceParams.ts b/src/libs/API/parameters/PayInvoiceParams.ts index 4c6633749ad..a6b9746d87b 100644 --- a/src/libs/API/parameters/PayInvoiceParams.ts +++ b/src/libs/API/parameters/PayInvoiceParams.ts @@ -4,6 +4,7 @@ type PayInvoiceParams = { reportID: string; reportActionID: string; paymentMethodType: PaymentMethodType; + payAsBusiness: boolean; }; export default PayInvoiceParams; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8dfc05e911e..8385c242a1a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -902,11 +902,20 @@ function isTripRoom(report: OnyxEntry): boolean { return isChatReport(report) && getChatType(report) === CONST.REPORT.CHAT_TYPE.TRIP_ROOM; } +function isIndividualInvoiceRoom(report: OnyxEntry): boolean { + return isInvoiceRoom(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; +} + function isCurrentUserInvoiceReceiver(report: OnyxEntry): boolean { if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { return currentUserAccountID === report.invoiceReceiver.accountID; } + if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS) { + const policy = PolicyUtils.getPolicy(report.invoiceReceiver.policyID); + return PolicyUtils.isPolicyAdmin(policy); + } + return false; } @@ -1887,7 +1896,6 @@ function getParticipantsAccountIDsForDisplay(report: OnyxEntry, shouldEx if (shouldExcludeDeleted && report?.pendingChatMembers?.findLast((member) => member.accountID === accountID)?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return false; } - return true; }); } @@ -2028,9 +2036,15 @@ function getIcons( if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { icons.push(...getIconsForParticipants([report?.invoiceReceiver.accountID], personalDetails)); } else { - const receiverPolicy = getPolicy(report?.invoiceReceiver?.policyID); + const receiverPolicyID = report?.invoiceReceiver?.policyID; + const receiverPolicy = getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(report, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicyID, + }); } } } @@ -2102,10 +2116,16 @@ function getIcons( return icons; } - const receiverPolicy = getPolicy(invoiceRoomReport?.invoiceReceiver?.policyID); + const receiverPolicyID = invoiceRoomReport?.invoiceReceiver?.policyID; + const receiverPolicy = getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(invoiceRoomReport, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicyID, + }); } return icons; @@ -2570,7 +2590,16 @@ function getMoneyRequestReportName(report: OnyxEntry, policy?: OnyxEntry const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency); - let payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; + let payerOrApproverName; + if (isExpenseReport(report)) { + payerOrApproverName = getPolicyName(report, false, policy); + } else if (isInvoiceReport(report)) { + const chatReport = getReportOrDraftReport(report?.chatReportID); + payerOrApproverName = getInvoicePayerName(chatReport); + } else { + payerOrApproverName = getDisplayNameForParticipant(report?.managerID) ?? ''; + } + const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerOrApproverName, amount: formattedAmount, @@ -5569,6 +5598,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec isChatThread(report) || isTaskReport(report) || isMoneyRequestReport(report) || + isInvoiceReport(report) || isChatRoom(report) || isPolicyExpenseChat(report) || (isGroupChat(report) && !shouldIncludeGroupChats) @@ -7351,6 +7381,7 @@ export { isChatUsedForOnboarding, getChatUsedForOnboarding, findPolicyExpenseChatByPolicyID, + isIndividualInvoiceRoom, }; export type { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 48c70021cac..42381d9008a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -291,6 +291,12 @@ Onyx.connect({ }, }); +let primaryPolicyID: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + callback: (value) => (primaryPolicyID = value), +}); + /** * Get the report or draft report given a reportID */ @@ -5938,13 +5944,22 @@ function getSendMoneyParams( } function getPayMoneyRequestParams( - chatReport: OnyxTypes.Report, + initialChatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, recipient: Participant, paymentMethodType: PaymentMethodType, full: boolean, + payAsBusiness?: boolean, ): PayMoneyRequestData { const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport); + let chatReport = initialChatReport; + + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { + const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', primaryPolicyID); + if (existingB2BInvoiceRoom) { + chatReport = existingB2BInvoiceRoom; + } + } let total = (iouReport.total ?? 0) - (iouReport.nonReimbursableTotal ?? 0); if (ReportUtils.hasHeldExpenses(iouReport.reportID) && !full && !!iouReport.unheldTotal) { @@ -5977,19 +5992,27 @@ function getPayMoneyRequestParams( optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithExpensify: paymentMethodType === CONST.IOU.PAYMENT_TYPE.VBBA}); } + const optimisticChatReport = { + ...chatReport, + lastReadTime: DateUtils.getDBTime(), + lastVisibleActionCreated: optimisticIOUReportAction.created, + hasOutstandingChildRequest: false, + iouReportID: null, + lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), + lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticIOUReportAction), + }; + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { + optimisticChatReport.invoiceReceiver = { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, + policyID: primaryPolicyID, + }; + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, - value: { - ...chatReport, - lastReadTime: DateUtils.getDBTime(), - lastVisibleActionCreated: optimisticIOUReportAction.created, - hasOutstandingChildRequest: false, - iouReportID: null, - lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), - lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticIOUReportAction), - }, + value: optimisticChatReport, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -6611,19 +6634,20 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R Navigation.dismissModalWithReport(chatReport); } -function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report) { +function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report, payAsBusiness = false) { const recipient = {accountID: invoiceReport.ownerAccountID}; const { optimisticData, successData, failureData, params: {reportActionID}, - } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true); + } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true, payAsBusiness); const params: PayInvoiceParams = { reportID: invoiceReport.reportID, reportActionID, paymentMethodType, + payAsBusiness, }; API.write(WRITE_COMMANDS.PAY_INVOICE, params, {optimisticData, successData, failureData}); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index cd1acb564e2..d075f8653d7 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -187,7 +187,7 @@ function getPolicy(policyID: string | undefined): OnyxEntry { */ function getPrimaryPolicy(activePolicyID?: OnyxEntry): Policy | undefined { const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); - const primaryPolicy: Policy | null | undefined = allPolicies?.[activePolicyID ?? '-1']; + const primaryPolicy: Policy | null | undefined = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]; return primaryPolicy ?? activeAdminWorkspaces[0]; } diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 7b0db3e0d84..53527e85b21 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -19,6 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; import {getReportActionMessage} from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -112,21 +113,30 @@ function ReportActionItemSingle({ let secondaryAvatar: Icon; const primaryDisplayName = displayName; if (displayAllActors) { - // The ownerAccountID and actorAccountID can be the same if a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice - const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? actorAccountID : ownerAccountID; - const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; - const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); + if (ReportUtils.isInvoiceRoom(report) && !ReportUtils.isIndividualInvoiceRoom(report)) { + const secondaryPolicyID = report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : '-1'; + const secondaryPolicy = PolicyUtils.getPolicy(secondaryPolicyID); + const secondaryPolicyAvatar = secondaryPolicy?.avatarURL ?? ReportUtils.getDefaultWorkspaceAvatar(secondaryPolicy?.name); - if (!isInvoiceReport) { - displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; - } + secondaryAvatar = { + source: secondaryPolicyAvatar, + type: CONST.ICON_TYPE_WORKSPACE, + name: secondaryPolicy?.name, + id: secondaryPolicyID, + }; + } else { + // The ownerAccountID and actorAccountID can be the same if a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice + const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? actorAccountID : ownerAccountID; + const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; + const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); - secondaryAvatar = { - source: secondaryUserAvatar, - type: CONST.ICON_TYPE_AVATAR, - name: secondaryDisplayName ?? '', - id: secondaryAccountId, - }; + secondaryAvatar = { + source: secondaryUserAvatar, + type: CONST.ICON_TYPE_AVATAR, + name: secondaryDisplayName ?? '', + id: secondaryAccountId, + }; + } } else if (!isWorkspaceActor) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const avatarIconIndex = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1;