Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,7 @@ function MoneyRequestConfirmationList({
currency={currency}
didConfirm={!!didConfirm}
distance={distance}
rawAmount={amountToBeUsed}
formattedAmount={formattedAmount}
formattedAmountPerAttendee={formattedAmountPerAttendee}
formError={formError}
Expand Down
15 changes: 13 additions & 2 deletions src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** The distance of the transaction */
distance: number;

/** The raw numeric amount of the transaction */
rawAmount: number;

/** The formatted amount of the transaction */
formattedAmount: string;

Expand Down Expand Up @@ -244,6 +247,7 @@ function MoneyRequestConfirmationListFooter({
onToggleBillable,
policy,
policyTags,
rawAmount,
policyTagLists,
rate,
receiptFilename,
Expand Down Expand Up @@ -335,9 +339,15 @@ function MoneyRequestConfirmationListFooter({
let reportName = getReportName(selectedReport, selectedPolicy);

if (!reportName) {
const optimisticReport = buildOptimisticExpenseReport(reportID, selectedPolicy?.id, selectedPolicy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID, Number(formattedAmount), currency);
const optimisticReport = buildOptimisticExpenseReport(
reportID,
selectedPolicy?.id,
selectedPolicy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID,
rawAmount ?? transaction?.amount ?? 0,
currency,
);
selectedReportID = !selectedReportID ? optimisticReport.reportID : selectedReportID;
reportName = populateOptimisticReportFormula(selectedPolicy?.fieldList?.text_title?.defaultValue ?? '', optimisticReport, selectedPolicy);
reportName = populateOptimisticReportFormula(selectedPolicy?.fieldList?.text_title?.defaultValue ?? '', optimisticReport, selectedPolicy, true);
}

// When creating an expense in an individual report, the report field becomes read-only
Expand Down Expand Up @@ -1016,6 +1026,7 @@ export default memo(
prevProps.currency === nextProps.currency &&
prevProps.didConfirm === nextProps.didConfirm &&
prevProps.distance === nextProps.distance &&
prevProps.rawAmount === nextProps.rawAmount &&
prevProps.formattedAmount === nextProps.formattedAmount &&
prevProps.formError === nextProps.formError &&
prevProps.hasRoute === nextProps.hasRoute &&
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,7 @@ const translations = {
emptyStateUnreportedExpenseTitle: 'Keine nicht gemeldeten Ausgaben',
emptyStateUnreportedExpenseSubtitle: 'Es sieht so aus, als hätten Sie keine nicht gemeldeten Ausgaben. Versuchen Sie, unten eine zu erstellen.',
addUnreportedExpenseConfirm: 'Zum Bericht hinzufügen',
newReport: 'Neuer Bericht',
explainHold: 'Erklären Sie, warum Sie diese Ausgabe zurückhalten.',
retracted: 'zurückgezogen',
retract: 'Zurückziehen',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,7 @@ const translations = {
emptyStateUnreportedExpenseTitle: 'No unreported expenses',
emptyStateUnreportedExpenseSubtitle: 'Looks like you don’t have any unreported expenses. Try creating one below.',
addUnreportedExpenseConfirm: 'Add to report',
newReport: 'New report',
explainHold: "Explain why you're holding this expense.",
retracted: 'retracted',
retract: 'Retract',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,7 @@ const translations = {
addUnreportedExpenseConfirm: 'Añadir al informe',
heldExpense: 'retuvo este gasto',
unheldExpense: 'desbloqueó este gasto',
newReport: 'Nuevo informe',
explainHold: 'Explica la razón para retener esta solicitud.',
retract: 'Retractar',
reopened: 'reabrir',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,7 @@ const translations = {
emptyStateUnreportedExpenseTitle: 'Aucune dépense non déclarée',
emptyStateUnreportedExpenseSubtitle: "Il semble que vous n'ayez aucune dépense non déclarée. Essayez d'en créer une ci-dessous.",
addUnreportedExpenseConfirm: 'Ajouter au rapport',
newReport: 'Nouveau rapport',
explainHold: 'Expliquez pourquoi vous retenez cette dépense.',
retracted: 'retraité',
retract: 'Retirer',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,7 @@ const translations = {
emptyStateUnreportedExpenseTitle: 'Nessuna spesa non segnalata',
emptyStateUnreportedExpenseSubtitle: 'Sembra che non hai spese non segnalate. Prova a crearne una qui sotto.',
addUnreportedExpenseConfirm: 'Aggiungi al report',
newReport: 'Nuovo rapporto',
explainHold: 'Spiega perché stai trattenendo questa spesa.',
retracted: 'retratato',
retract: 'Ritirare',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,7 @@ const translations = {
emptyStateUnreportedExpenseTitle: '未報告の経費はありません',
emptyStateUnreportedExpenseSubtitle: '未報告の経費はないようです。以下で新しく作成してみてください。',
addUnreportedExpenseConfirm: 'レポートに追加',
newReport: '新しいレポート',
explainHold: 'この経費を保留している理由を説明してください。',
retracted: '撤回されました',
retract: '取り消す',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,7 @@ const translations = {
emptyStateUnreportedExpenseTitle: 'Geen niet-gerapporteerde uitgaven',
emptyStateUnreportedExpenseSubtitle: 'Het lijkt erop dat je geen niet-gerapporteerde uitgaven hebt. Probeer er hieronder een aan te maken.',
addUnreportedExpenseConfirm: 'Toevoegen aan rapport',
newReport: 'Nieuw rapport',
explainHold: 'Leg uit waarom je deze uitgave vasthoudt.',
retracted: 'ingetrokken',
retract: 'Intrekken',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1307,6 +1307,7 @@ const translations = {
emptyStateUnreportedExpenseTitle: 'Brak niezgłoszonych wydatków',
emptyStateUnreportedExpenseSubtitle: 'Wygląda na to, że nie masz żadnych niezgłoszonych wydatków. Spróbuj utworzyć jeden poniżej.',
addUnreportedExpenseConfirm: 'Dodaj do raportu',
newReport: 'Nowy raport',
explainHold: 'Wyjaśnij, dlaczego wstrzymujesz ten wydatek.',
retracted: 'wycofany',
retract: 'Wycofać',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,7 @@ const translations = {
emptyStateUnreportedExpenseTitle: 'Nenhuma despesa não relatada',
emptyStateUnreportedExpenseSubtitle: 'Parece que você não tem nenhuma despesa não relatada. Tente criar uma abaixo.',
addUnreportedExpenseConfirm: 'Adicionar ao relatório',
newReport: 'Novo relatório',
explainHold: 'Explique por que você está retendo esta despesa.',
retracted: 'retraído',
retract: 'Retrair',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,7 @@ const translations = {
emptyStateUnreportedExpenseTitle: '没有未报告的费用',
emptyStateUnreportedExpenseSubtitle: '看起来您没有未报告的费用。请尝试在下面创建一个。',
addUnreportedExpenseConfirm: '添加到报告',
newReport: '新报告',
explainHold: '请解释您为何保留此费用。',
retracted: '撤回',
retract: '撤回',
Expand Down
12 changes: 10 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import type EnvironmentType from './Environment/getEnvironment/types';
import {getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from './ErrorUtils';
import getAttachmentDetails from './fileDownload/getAttachmentDetails';
import getBase62ReportID from './getBase62ReportID';
import {isReportMessageAttachment} from './isReportMessageAttachment';
import localeCompareLibs from './LocaleCompare';
import {formatPhoneNumber} from './LocalePhoneNumber';
Expand Down Expand Up @@ -929,7 +930,7 @@

let conciergeReportID: OnyxEntry<string>;
Onyx.connect({
key: ONYXKEYS.CONCIERGE_REPORT_ID,

Check warning on line 933 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
callback: (value) => {
conciergeReportID = value;
},
Expand All @@ -937,7 +938,7 @@

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({
key: ONYXKEYS.SESSION,

Check warning on line 941 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
callback: (value) => {
// When signed out, val is undefined
if (!value) {
Expand All @@ -955,7 +956,7 @@
let allPersonalDetailLogins: string[];
let currentUserPersonalDetails: OnyxEntry<PersonalDetails>;
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,

Check warning on line 959 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
callback: (value) => {
if (currentUserAccountID) {
currentUserPersonalDetails = value?.[currentUserAccountID] ?? undefined;
Expand All @@ -967,14 +968,14 @@

let allReportsDraft: OnyxCollection<Report>;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,

Check warning on line 971 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
waitForCollectionCallback: true,
callback: (value) => (allReportsDraft = value),
});

let allPolicies: OnyxCollection<Policy>;
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY,

Check warning on line 978 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
});
Expand All @@ -982,7 +983,7 @@
let allReports: OnyxCollection<Report>;
let reportsByPolicyID: ReportByPolicyMap;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,

Check warning on line 986 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
waitForCollectionCallback: true,
callback: (value) => {
allReports = value;
Expand Down Expand Up @@ -1023,14 +1024,14 @@

let allBetas: OnyxEntry<Beta[]>;
Onyx.connect({
key: ONYXKEYS.BETAS,

Check warning on line 1027 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
callback: (value) => (allBetas = value),
});

let allTransactions: OnyxCollection<Transaction> = {};
let reportsTransactions: Record<string, Transaction[]> = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION,

Check warning on line 1034 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
waitForCollectionCallback: true,
callback: (value) => {
if (!value) {
Expand All @@ -1056,7 +1057,7 @@

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,

Check warning on line 1060 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
waitForCollectionCallback: true,
callback: (actions) => {
if (!actions) {
Expand All @@ -1069,7 +1070,7 @@
let allReportMetadata: OnyxCollection<ReportMetadata>;
const allReportMetadataKeyValue: Record<string, ReportMetadata> = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_METADATA,

Check warning on line 1073 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
waitForCollectionCallback: true,
callback: (value) => {
if (!value) {
Expand Down Expand Up @@ -6122,13 +6123,20 @@
* If after all replacements the formula is empty, the original formula is returned.
* See {@link https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates}
*/
function populateOptimisticReportFormula(formula: string, report: OptimisticExpenseReport, policy: OnyxEntry<Policy>): string {
function populateOptimisticReportFormula(formula: string, report: OptimisticExpenseReport | OptimisticNewReport, policy: OnyxEntry<Policy>, isMoneyRequestConfirmation = false): string {
// If this is a newly created report and it is from money request confirmation, we should use 'New report' as the report title
if (!report.parentReportActionID && isMoneyRequestConfirmation) {
return translateLocal('iou.newReport');
}

const createdDate = report.lastVisibleActionCreated ? new Date(report.lastVisibleActionCreated) : undefined;

const result = formula
// We don't translate because the server response is always in English
.replaceAll(/\{report:type\}/gi, 'Expense Report')
.replaceAll(/\{report:startdate\}/gi, createdDate ? format(createdDate, CONST.DATE.FNS_FORMAT_STRING) : '')
.replaceAll(/\{report:total\}/gi, report.total !== undefined ? convertToDisplayString(Math.abs(report.total), report.currency).toString() : '')
.replaceAll(/\{report:id\}/gi, getBase62ReportID(Number(report.reportID)))
.replaceAll(/\{report:total\}/gi, report.total !== undefined && !Number.isNaN(report.total) ? convertToDisplayString(Math.abs(report.total), report.currency).toString() : '')
.replaceAll(/\{report:currency\}/gi, report.currency ?? '')
.replaceAll(/\{report:policyname\}/gi, policy?.name ?? '')
.replaceAll(/\{report:workspacename\}/gi, policy?.name ?? '')
Expand Down
107 changes: 107 additions & 0 deletions tests/unit/ReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived';
import {putOnHold} from '@libs/actions/IOU';
import type {OnboardingTaskLinks} from '@libs/actions/Welcome/OnboardingFlow';
import DateUtils from '@libs/DateUtils';
import getBase62ReportID from '@libs/getBase62ReportID';
import {translateLocal} from '@libs/Localize';
import {getOriginalMessage, isWhisperAction} from '@libs/ReportActionsUtils';
import {
Expand Down Expand Up @@ -66,6 +67,7 @@ import {
isReportOutstanding,
isRootGroupChat,
parseReportRouteParams,
populateOptimisticReportFormula,
prepareOnboardingOnyxData,
requiresAttentionFromCurrentUser,
requiresManualSubmission,
Expand Down Expand Up @@ -6605,6 +6607,111 @@ describe('ReportUtils', () => {
expect(reportPreviewAction.childManagerAccountID).toBe(iouReport.managerID);
});
});

describe('populateOptimisticReportFormula', () => {
const mockPolicy: Policy = {
id: 'test-policy-id',
name: 'Test Policy',
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.ADMIN,
owner: 'test@example.com',
outputCurrency: CONST.CURRENCY.USD,
isPolicyExpenseChatEnabled: true,
autoReporting: true,
autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY,
harvesting: {
enabled: true,
},
defaultBillable: false,
disabledFields: {},
fieldList: {},
customUnits: {},
areCategoriesEnabled: true,
areTagsEnabled: true,
areDistanceRatesEnabled: true,
areWorkflowsEnabled: true,
areReportFieldsEnabled: true,
areConnectionsEnabled: true,
pendingAction: undefined,
errors: {},
isLoading: false,
errorFields: {},
};

const mockReport = {
reportID: '123456789',
reportName: 'Test Report',
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: 1,
currency: CONST.CURRENCY.USD,
total: -5000,
lastVisibleActionCreated: '2024-01-15 10:30:00',
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
chatReportID: 'chat-123',
policyID: 'test-policy-id',
participants: {},
parentReportID: 'chat-123',
};

it('should handle NaN total gracefully', () => {
const reportWithNaNTotal = {
...mockReport,
total: NaN,
};

const result = populateOptimisticReportFormula('{report:total}', reportWithNaNTotal, mockPolicy);
expect(result).toBe('{report:total}');
});

it('should replace {report:total} with formatted amount', () => {
const result = populateOptimisticReportFormula('{report:total}', mockReport, mockPolicy);
expect(result).toBe('$50.00');
});

it('should replace {report:id} with base62 report ID', () => {
const result = populateOptimisticReportFormula('{report:id}', mockReport, mockPolicy);
expect(result).toBe(getBase62ReportID(Number(mockReport.reportID)));
});

it('should replace multiple placeholders correctly', () => {
const formula = 'Report {report:id} has total {report:total}';
const result = populateOptimisticReportFormula(formula, mockReport, mockPolicy);
const expectedId = getBase62ReportID(Number(mockReport.reportID));
expect(result).toBe(`Report ${expectedId} has total $50.00`);
});

it('should handle undefined total gracefully', () => {
const reportWithUndefinedTotal = {
...mockReport,
total: undefined,
};

const result = populateOptimisticReportFormula('{report:total}', reportWithUndefinedTotal, mockPolicy);
expect(result).toBe('{report:total}');
});

it('should handle complex formula with multiple placeholders and some invalid values', () => {
const formula = 'ID: {report:id}, Total: {report:total}, Type: {report:type}';
const reportWithNaNTotal = {
...mockReport,
total: NaN,
};
const expectedId = getBase62ReportID(Number(mockReport.reportID));
const result = populateOptimisticReportFormula(formula, reportWithNaNTotal, mockPolicy);
expect(result).toBe(`ID: ${expectedId}, Total: , Type: Expense Report`);
});

it('should handle missing total gracefully', () => {
const reportWithMissingTotal = {
...mockReport,
total: undefined,
};

const result = populateOptimisticReportFormula('{report:total}', reportWithMissingTotal, mockPolicy);
expect(result).toBe('{report:total}');
});
});
describe('canSeeDefaultRoom', () => {
it('should return true if report is archived room ', () => {
const betas = [CONST.BETAS.DEFAULT_ROOMS];
Expand Down
Loading