diff --git a/src/CONST.ts b/src/CONST.ts
index 86cbd4c28fc9..cc5c25627c01 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -278,6 +278,9 @@ const CONST = {
// Regex to get link in href prop inside of component
REGEX_LINK_IN_ANCHOR: /]*?\s+)?href="([^"]*)"/gi,
+ // Regex to read violation value from string given by backend
+ VIOLATION_LIMIT_REGEX: /[^0-9]+/g,
+
MERCHANT_NAME_MAX_LENGTH: 255,
MASKED_PAN_PREFIX: 'XXXXXXXXXXXX',
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index 1c8c08eee082..3a12cb5f1752 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -233,7 +233,7 @@ function MoneyRequestConfirmationListFooter({
const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]);
const isMultilevelTags = useMemo(() => PolicyUtils.isMultiLevelTags(policyTags), [policyTags]);
- const shouldShowAttendees = useMemo(() => !!policy?.id && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM), [policy?.id, policy?.type]);
+ const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]);
const senderWorkspace = useMemo(() => {
const senderWorkspaceParticipant = selectedParticipants.find((participant) => participant.isSender);
@@ -516,7 +516,7 @@ function MoneyRequestConfirmationListFooter({
item.displayName ?? item.login).join(', ')}
+ title={iouAttendees?.map((item) => item?.displayName ?? item?.login).join(', ')}
description={`${translate('iou.attendees')} ${
iouAttendees?.length && iouAttendees.length > 1 ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : ''
}`}
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index a411b591a3d3..ceaf65f354ea 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -126,6 +126,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const {
created: transactionDate,
amount: transactionAmount,
+ attendees: transactionAttendees,
taxAmount: transactionTaxAmount,
currency: transactionCurrency,
comment: transactionDescription,
@@ -139,6 +140,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction);
const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : '';
+ const formattedPerAttendeeAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount / (transactionAttendees?.length ?? 1), transactionCurrency) : '';
const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency);
const isCardTransaction = TransactionUtils.isCardTransaction(transaction);
const cardProgramName = TransactionUtils.getCardName(transaction);
@@ -191,6 +193,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
+ const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]);
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest);
const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport);
@@ -637,6 +640,25 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
}}
/>
)}
+ {shouldShowAttendees && (
+
+ item?.displayName ?? item?.login).join(', ')}
+ description={`${translate('iou.attendees')} ${
+ transactionAttendees?.length && transactionAttendees.length > 1 ? `${formattedPerAttendeeAmount} ${translate('common.perPerson')}` : ''
+ }`}
+ style={[styles.moneyRequestMenuItem]}
+ titleStyle={styles.flex1}
+ onPress={() =>
+ Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ }
+ interactive
+ shouldRenderAsHTML
+ />
+
+ )}
{shouldShowBillable && (
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 99f483cf6ef6..f0428ae6bf64 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -302,6 +302,19 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
);
}
+ const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'attendees' in reportActionOriginalMessage;
+ if (hasModifiedAttendees) {
+ buildMessageFragmentForValue(
+ reportActionOriginalMessage.oldAttendees ?? '',
+ reportActionOriginalMessage.attendees ?? '',
+ Localize.translateLocal('iou.attendees'),
+ false,
+ setFragments,
+ removalFragments,
+ changeFragments,
+ );
+ }
+
const message =
getMessageLine(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) +
getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) +
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 292a33c320c0..8a8b060ae105 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -42,7 +42,7 @@ import type {
TransactionViolation,
UserWallet,
} from '@src/types/onyx';
-import type {Participant} from '@src/types/onyx/IOU';
+import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft';
import type {OriginalMessageExportedToIntegration} from '@src/types/onyx/OldDotAction';
import type Onboarding from '@src/types/onyx/Onboarding';
@@ -362,6 +362,7 @@ type OptimisticTaskReport = Pick<
type TransactionDetails = {
created: string;
amount: number;
+ attendees: Attendee[];
taxAmount?: number;
taxCode?: string;
currency: string;
@@ -2911,6 +2912,7 @@ function getTransactionDetails(transaction: OnyxInputOrEntry, creat
return {
created: TransactionUtils.getFormattedCreated(transaction, createdDateFormat),
amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
+ attendees: TransactionUtils.getAttendees(transaction),
taxAmount: TransactionUtils.getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
taxCode: TransactionUtils.getTaxCode(transaction),
currency: TransactionUtils.getCurrency(transaction),
@@ -3469,7 +3471,7 @@ function getModifiedExpenseOriginalMessage(
originalMessage.merchant = transactionChanges?.merchant;
}
if ('attendees' in transactionChanges) {
- [originalMessage.oldAttendees, originalMessage.attendees] = TransactionUtils.getFormattedAttendees(oldTransaction?.modifiedAttendees, oldTransaction?.attendees);
+ [originalMessage.oldAttendees, originalMessage.attendees] = TransactionUtils.getFormattedAttendees(transactionChanges?.attendees, TransactionUtils.getAttendees(oldTransaction));
}
// The amount is always a combination of the currency and the number value so when one changes we need to store both
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index ba1dae4d2e6f..fda799adf71a 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -19,6 +19,7 @@ import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import type {IOURequestType} from '@userActions/IOU';
import CONST from '@src/CONST';
+import type {IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
import type {Attendee} from '@src/types/onyx/IOU';
@@ -126,6 +127,7 @@ function buildOptimisticTransaction(
currency: string,
reportID: string,
comment = '',
+ attendees: Attendee[] = [],
created = '',
source = '',
originalTransactionID = '',
@@ -171,6 +173,7 @@ function buildOptimisticTransaction(
taxAmount,
billable,
reimbursable,
+ attendees,
};
}
@@ -199,6 +202,10 @@ function isMerchantMissing(transaction: OnyxEntry) {
return isMerchantEmpty;
}
+function shouldShowAttendees(iouType: IOUType, policy: OnyxEntry): boolean {
+ return iouType === CONST.IOU.TYPE.SUBMIT && !!policy?.id && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM);
+}
+
/**
* Check if the merchant is partial i.e. `(none)`
*/
@@ -293,6 +300,10 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
updatedTransaction.tag = transactionChanges.tag;
}
+ if (Object.hasOwn(transactionChanges, 'attendees')) {
+ updatedTransaction.modifiedAttendees = transactionChanges?.attendees;
+ }
+
if (
shouldUpdateReceiptState &&
shouldStopSmartscan &&
@@ -316,6 +327,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'taxAmount') && {taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'taxCode') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
+ ...(Object.hasOwn(transactionChanges, 'attendees') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
};
return updatedTransaction;
@@ -418,7 +430,14 @@ function getMerchant(transaction: OnyxInputOrEntry): string {
}
/**
- * Return the merchant field from the transaction, return the modifiedMerchant if present.
+ * Return the list of modified attendees if present otherwise list of attendees
+ */
+function getAttendees(transaction: OnyxInputOrEntry): Attendee[] {
+ return transaction?.modifiedAttendees ? transaction.modifiedAttendees : transaction?.attendees ?? [];
+}
+
+/**
+ * Return the list of attendees as a string and modified list of attendees as a string if present.
*/
function getFormattedAttendees(modifiedAttendees?: Attendee[], attendees?: Attendee[]): [string, string] {
const oldAttendees = modifiedAttendees ?? [];
@@ -1096,6 +1115,7 @@ export {
isManualRequest,
isScanRequest,
getAmount,
+ getAttendees,
getTaxAmount,
getTaxCode,
getCurrency,
@@ -1160,6 +1180,7 @@ export {
removeSettledAndApprovedTransactions,
getCardName,
hasReceiptSource,
+ shouldShowAttendees,
};
export type {TransactionChanges};
diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts
index adbc05460220..a3d6bd9932d0 100644
--- a/src/libs/Violations/ViolationsUtils.ts
+++ b/src/libs/Violations/ViolationsUtils.ts
@@ -345,6 +345,11 @@ const ViolationsUtils = {
return violation.name as never;
}
},
+
+ // We have to use regex, because Violation limit is given in a inconvenient form: "$2,000.00"
+ getViolationAmountLimit(violation: TransactionViolation): number {
+ return Number(violation.data?.formattedLimit?.replace(CONST.VIOLATION_LIMIT_REGEX, ''));
+ },
};
export default ViolationsUtils;
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 1f4b05912c14..b4819051c2ea 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -1901,6 +1901,7 @@ function getSendInvoiceInformation(
currency,
optimisticInvoiceReport.reportID,
trimmedComment,
+ [],
created,
'',
'',
@@ -2022,6 +2023,7 @@ function getMoneyRequestInformation(
payeeEmail = currentUserEmail,
moneyRequestReportID = '',
linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction,
+ attendees?: Attendee[],
): MoneyRequestInformation {
const payerEmail = PhoneNumber.addSMSDomainIfPhoneNumber(participant.login ?? '');
const payerAccountID = Number(participant.accountID);
@@ -2081,6 +2083,7 @@ function getMoneyRequestInformation(
currency,
iouReport.reportID,
comment,
+ attendees,
created,
'',
'',
@@ -2318,6 +2321,7 @@ function getTrackExpenseInformation(
currency,
shouldUseMoneyReport && iouReport ? iouReport.reportID : '-1',
comment,
+ [],
created,
'',
'',
@@ -2478,6 +2482,7 @@ function getUpdateMoneyRequestParams(
policyTagList: OnyxTypes.OnyxInputOrEntry,
policyCategories: OnyxTypes.OnyxInputOrEntry,
onlyIncludeChangedFields: boolean,
+ violations?: OnyxEntry,
): UpdateMoneyRequestData {
const optimisticData: OnyxUpdate[] = [];
const successData: OnyxUpdate[] = [];
@@ -2721,7 +2726,19 @@ function getUpdateMoneyRequestParams(
}
}
- // Clear out the error fields and loading states on success
+ const overLimitViolation = violations?.find((violation) => violation.name === 'overLimit');
+ // Update violation limit, if we modify attendees. The given limit value is for a single attendee, if we have multiple attendees we should multpiply limit by attende count
+ if ('attendees' in transactionChanges && !!overLimitViolation) {
+ const limitForSingleAttendee = ViolationsUtils.getViolationAmountLimit(overLimitViolation);
+ if (limitForSingleAttendee * (transactionChanges?.attendees?.length ?? 1) > Math.abs(TransactionUtils.getAmount(transaction))) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
+ value: violations?.filter((violation) => violation.name !== 'overLimit') ?? [],
+ });
+ }
+ }
+
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
@@ -3045,11 +3062,12 @@ function updateMoneyRequestAttendees(
policy: OnyxEntry,
policyTagList: OnyxEntry,
policyCategories: OnyxEntry,
+ violations: OnyxEntry,
) {
const transactionChanges: TransactionChanges = {
attendees,
};
- const data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
+ const data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true, violations);
const {params, onyxData} = data;
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_ATTENDEES, params, onyxData);
}
@@ -3572,6 +3590,7 @@ function requestMoney(
payeeEmail,
moneyRequestReportID,
linkedTrackedExpenseReportAction,
+ attendees,
);
const activeReportID = isMoneyRequestReport ? report?.reportID : chatReport.reportID;
@@ -3980,6 +3999,7 @@ function createSplitsAndOnyxData(
currency,
CONST.REPORT.SPLIT_REPORTID,
comment,
+ [],
created,
'',
'',
@@ -4217,6 +4237,7 @@ function createSplitsAndOnyxData(
currency,
oneOnOneIOUReport.reportID,
comment,
+ [],
created,
CONST.IOU.TYPE.SPLIT,
splitTransaction.transactionID,
@@ -4564,6 +4585,7 @@ function startSplitBill({
currency,
CONST.REPORT.SPLIT_REPORTID,
comment,
+ [],
'',
'',
'',
@@ -4970,6 +4992,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
currency ?? '',
oneOnOneIOUReport?.reportID ?? '-1',
updatedTransaction?.comment?.comment,
+ [],
updatedTransaction?.modifiedCreated,
CONST.IOU.TYPE.SPLIT,
transactionID,
diff --git a/src/pages/iou/request/step/IOURequestStepAttendees.tsx b/src/pages/iou/request/step/IOURequestStepAttendees.tsx
index a0f72304b506..f1d253db0c18 100644
--- a/src/pages/iou/request/step/IOURequestStepAttendees.tsx
+++ b/src/pages/iou/request/step/IOURequestStepAttendees.tsx
@@ -5,6 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import Navigation from '@libs/Navigation/Navigation';
+import * as TransactionUtils from '@libs/TransactionUtils';
import MoneyRequestAttendeeSelector from '@pages/iou/request/MoneyRequestAttendeeSelector';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
@@ -30,7 +31,6 @@ type IOURequestStepAttendeesOnyxProps = {
type IOURequestStepAttendeesProps = IOURequestStepAttendeesOnyxProps & WithWritableReportOrNotFoundProps;
function IOURequestStepAttendees({
- route,
route: {
params: {transactionID, reportID, iouType, backTo, action},
},
@@ -38,11 +38,12 @@ function IOURequestStepAttendees({
policyTags,
policyCategories,
}: IOURequestStepAttendeesProps) {
- const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${route?.params.transactionID || -1}`);
- const [attendees, setAttendees] = useState(transaction?.attendees ?? []);
+ const isEditing = action === CONST.IOU.ACTION.EDIT;
+ const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || -1}`);
+ const [attendees, setAttendees] = useState(TransactionUtils.getAttendees(transaction));
const previousAttendees = usePrevious(attendees);
const {translate} = useLocalize();
- const isEditing = action === CONST.IOU.ACTION.EDIT;
+ const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
const saveAttendees = useCallback(() => {
if (attendees.length <= 0) {
@@ -51,12 +52,12 @@ function IOURequestStepAttendees({
if (!lodashIsEqual(previousAttendees, attendees)) {
IOU.setMoneyRequestAttendees(transactionID, attendees, !isEditing);
if (isEditing) {
- IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories);
+ IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations);
}
}
Navigation.goBack(backTo);
- }, [attendees, backTo, isEditing, policy, policyCategories, policyTags, previousAttendees, reportID, transactionID]);
+ }, [attendees, backTo, isEditing, policy, policyCategories, policyTags, previousAttendees, reportID, transactionID, violations]);
const navigateBack = () => {
Navigation.goBack(backTo);
diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts
index f23259eb568c..6ec8e11d991a 100644
--- a/tests/unit/TransactionUtilsTest.ts
+++ b/tests/unit/TransactionUtilsTest.ts
@@ -1,3 +1,4 @@
+import type {Attendee} from '@src/types/onyx/IOU';
import * as TransactionUtils from '../../src/libs/TransactionUtils';
import type {Transaction} from '../../src/types/onyx';
@@ -6,8 +7,9 @@ function generateTransaction(values: Partial = {}): Transaction {
const amount = 100;
const currency = 'USD';
const comment = '';
+ const attendees: Attendee[] = [];
const created = '2023-10-01';
- const baseValues = TransactionUtils.buildOptimisticTransaction(amount, currency, reportID, comment, created);
+ const baseValues = TransactionUtils.buildOptimisticTransaction(amount, currency, reportID, comment, attendees, created);
return {...baseValues, ...values};
}