Skip to content

Commit

Permalink
Merge pull request #44970 from VickyStash/feature/pay-invoice-as-busi…
Browse files Browse the repository at this point in the history
…ness

Add Pay as Business option for invoices sent to an individual who is admin of their primary workspace
  • Loading branch information
cristipaval authored Aug 7, 2024
2 parents c73b1f8 + a39aa22 commit 5f89592
Show file tree
Hide file tree
Showing 20 changed files with 239 additions and 99 deletions.
7 changes: 6 additions & 1 deletion src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,12 @@ const ROUTES = {
},
REPORT_AVATAR: {
route: 'r/:reportID/avatar',
getRoute: (reportID: string) => `r/${reportID}/avatar` as const,
getRoute: (reportID: string, policyID?: string) => {
if (policyID) {
return `r/${reportID}/avatar?policyID=${policyID}` as const;
}
return `r/${reportID}/avatar` as const;
},
},
EDIT_CURRENCY_REQUEST: {
route: 'r/:threadReportID/edit/currency',
Expand Down
10 changes: 7 additions & 3 deletions src/components/AvatarWithDisplayName.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {useCallback, useEffect, useRef} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import {useOnyx, withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
Expand Down Expand Up @@ -58,12 +58,16 @@ function AvatarWithDisplayName({
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const title = ReportUtils.getReportName(report);
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`);
const [invoiceReceiverPolicy] = useOnyx(
`${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : -1}`,
);
const title = ReportUtils.getReportName(report, undefined, undefined, undefined, invoiceReceiverPolicy);
const subtitle = ReportUtils.getChatRoomSubtitle(report);
const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report);
const isMoneyRequestOrReport =
ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report);
const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy);
const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy);
const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails);
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false);
const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report);
Expand Down
11 changes: 11 additions & 0 deletions src/components/LHNOptionsList/LHNOptionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,20 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const renderItem = useCallback(
({item: reportID}: RenderItemProps): ReactElement => {
const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const itemParentReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${itemFullReport?.parentReportID ?? '-1'}`];
const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`];
const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? '-1'];

let invoiceReceiverPolicyID = '-1';
if (itemFullReport?.invoiceReceiver && 'policyID' in itemFullReport.invoiceReceiver) {
invoiceReceiverPolicyID = itemFullReport.invoiceReceiver.policyID;
}
if (itemParentReport?.invoiceReceiver && 'policyID' in itemParentReport.invoiceReceiver) {
invoiceReceiverPolicyID = itemParentReport.invoiceReceiver.policyID;
}
const itemInvoiceReceiverPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`];

const iouReportIDOfLastAction = OptionsListUtils.getIOUReportIDOfLastAction(itemFullReport);
const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined;

Expand Down Expand Up @@ -146,6 +156,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
parentReportAction={itemParentReportAction}
iouReportReportActions={itemIouReportReportActions}
policy={itemPolicy}
invoiceReceiverPolicy={itemInvoiceReceiverPolicy}
personalDetails={personalDetails ?? {}}
transaction={itemTransaction}
lastReportActionTransaction={lastReportActionTransaction}
Expand Down
3 changes: 3 additions & 0 deletions src/components/LHNOptionsList/OptionRowLHNData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function OptionRowLHNData({
personalDetails = {},
preferredLocale = CONST.LOCALES.DEFAULT,
policy,
invoiceReceiverPolicy,
receiptTransactions,
parentReportAction,
iouReportReportActions,
Expand Down Expand Up @@ -49,6 +50,7 @@ function OptionRowLHNData({
parentReportAction,
hasViolations: !!shouldDisplayViolations || shouldDisplayReportViolations,
transactionViolations,
invoiceReceiverPolicy,
});
if (deepEqual(item, optionItemRef.current)) {
return optionItemRef.current;
Expand All @@ -72,6 +74,7 @@ function OptionRowLHNData({
transaction,
transactionViolations,
receiptTransactions,
invoiceReceiverPolicy,
shouldDisplayReportViolations,
]);

Expand Down
3 changes: 3 additions & 0 deletions src/components/LHNOptionsList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ type OptionRowLHNDataProps = {
/** The policy which the user has access to and which the report could be tied to */
policy?: OnyxEntry<Policy>;

/** Invoice receiver policy */
invoiceReceiverPolicy?: OnyxEntry<Policy>;

/** The action from the parent report */
parentReportAction?: OnyxEntry<ReportAction>;

Expand Down
4 changes: 2 additions & 2 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const displayedAmount = isAnyTransactionOnHold && 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;
}
Expand All @@ -144,7 +144,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
if (isAnyTransactionOnHold) {
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);
}
Expand Down
21 changes: 17 additions & 4 deletions src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, {useMemo, useState} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import {useOnyx, withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
Expand Down Expand Up @@ -139,6 +139,9 @@ function ReportPreview({
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy);
const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID ?? '');
const [paymentType, setPaymentType] = useState<PaymentMethodType>();
const [invoiceReceiverPolicy] = useOnyx(
`${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : -1}`,
);

const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? 0;
const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport);
Expand All @@ -147,6 +150,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);
Expand Down Expand Up @@ -187,7 +191,7 @@ function ReportPreview({
[chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled],
);

const confirmPayment = (type: PaymentMethodType | undefined) => {
const confirmPayment = (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
if (!type) {
return;
}
Expand All @@ -197,7 +201,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);
}
Expand Down Expand Up @@ -268,7 +272,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, invoiceReceiverPolicy);
} else {
payerOrApproverName = ReportUtils.getDisplayNameForParticipant(managerID, true);
}

if (isApproved) {
return translate('iou.managerApproved', {manager: payerOrApproverName});
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/RoomHeaderAvatars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ type RoomHeaderAvatarsProps = {

function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) {
const navigateToAvatarPage = (icon: Icon) => {
if (icon.type === CONST.ICON_TYPE_WORKSPACE) {
Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID));
if (icon.type === CONST.ICON_TYPE_WORKSPACE && icon.id) {
Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID, icon.id.toString()));
return;
}

Expand Down
58 changes: 41 additions & 17 deletions src/components/SettlementButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

/** Callback when the payment options popover is shown */
onPaymentOptionsShow?: () => void;
Expand Down Expand Up @@ -151,6 +153,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.
Expand Down Expand Up @@ -207,20 +212,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) {
Expand All @@ -234,7 +258,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-compiler/react-compiler, react-hooks/exhaustive-deps
}, [currency, formattedAmount, iouReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]);
}, [currency, formattedAmount, iouReport, chatReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]);

const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => {
if (policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) {
Expand Down Expand Up @@ -267,7 +291,7 @@ function SettlementButton({

return (
<KYCWall
onSuccessfulKYC={onPress}
onSuccessfulKYC={(paymentType) => onPress(paymentType)}
enablePaymentsRoute={enablePaymentsRoute}
addBankAccountRoute={addBankAccountRoute}
addDebitCardRoute={addDebitCardRoute}
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,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}`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,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}`,
Expand Down
1 change: 1 addition & 0 deletions src/libs/API/parameters/PayInvoiceParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type PayInvoiceParams = {
reportID: string;
reportActionID: string;
paymentMethodType: PaymentMethodType;
payAsBusiness: boolean;
};

export default PayInvoiceParams;
1 change: 1 addition & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,7 @@ type AuthScreensParamList = CentralPaneScreensParamList &
};
[SCREENS.REPORT_AVATAR]: {
reportID: string;
policyID?: string;
};
[SCREENS.NOT_FOUND]: undefined;
[NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams<LeftModalNavigatorParamList>;
Expand Down
Loading

0 comments on commit 5f89592

Please sign in to comment.