Skip to content

Commit

Permalink
Merge pull request #43036 from VickyStash/feature/40438-pay-as-business
Browse files Browse the repository at this point in the history
Add Pay as Business option for invoices sent to an individual who is admin of their primary workspace
  • Loading branch information
cristipaval authored Jul 3, 2024
2 parents 9babbda + 4a17fdc commit 35ae731
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 54 deletions.
4 changes: 2 additions & 2 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
}
Expand Down
16 changes: 13 additions & 3 deletions src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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});
}
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;

/** The route to redirect if user does not have a payment method setup */
enablePaymentsRoute: EnablePaymentsRoute;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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)) {
Expand Down Expand Up @@ -259,7 +283,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 @@ -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}`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
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;
43 changes: 37 additions & 6 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,11 +902,20 @@ function isTripRoom(report: OnyxEntry<Report>): boolean {
return isChatReport(report) && getChatType(report) === CONST.REPORT.CHAT_TYPE.TRIP_ROOM;
}

function isIndividualInvoiceRoom(report: OnyxEntry<Report>): boolean {
return isInvoiceRoom(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL;
}

function isCurrentUserInvoiceReceiver(report: OnyxEntry<Report>): 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;
}

Expand Down Expand Up @@ -1887,7 +1896,6 @@ function getParticipantsAccountIDsForDisplay(report: OnyxEntry<Report>, shouldEx
if (shouldExcludeDeleted && report?.pendingChatMembers?.findLast((member) => member.accountID === accountID)?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
return false;
}

return true;
});
}
Expand Down Expand Up @@ -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,
});
}
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2570,7 +2590,16 @@ function getMoneyRequestReportName(report: OnyxEntry<Report>, 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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -7351,6 +7381,7 @@ export {
isChatUsedForOnboarding,
getChatUsedForOnboarding,
findPolicyExpenseChatByPolicyID,
isIndividualInvoiceRoom,
};

export type {
Expand Down
48 changes: 36 additions & 12 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,12 @@ Onyx.connect({
},
});

let primaryPolicyID: OnyxEntry<string>;
Onyx.connect({
key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
callback: (value) => (primaryPolicyID = value),
});

/**
* Get the report or draft report given a reportID
*/
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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});
Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ function getPolicy(policyID: string | undefined): OnyxEntry<Policy> {
*/
function getPrimaryPolicy(activePolicyID?: OnyxEntry<string>): 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];
}
Expand Down
Loading

0 comments on commit 35ae731

Please sign in to comment.