diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 01fdde6cc85e..033c7999de35 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -112,6 +112,7 @@ import {
getOriginalAmountForDisplay,
getOriginalTransactionWithSplitInfo,
getReimbursable,
+ getReservationNights,
getTagForDisplay,
getTaxName,
hasMissingSmartscanFields,
@@ -367,6 +368,12 @@ function MoneyRequestView({
const formattedTransactionAmount = shouldDisplayTransactionAmount ? convertToDisplayString(actualAmount, actualCurrency) : '';
const formattedPerAttendeeAmount =
shouldDisplayTransactionAmount && actualAmount !== undefined ? convertToDisplayString(actualAmount / (transactionAttendees?.length ?? 1), actualCurrency) : '';
+ // Explains the per-night category limit check, so it uses the same reservation nights. Single-night stays show nothing.
+ const reservationNights = getReservationNights(updatedTransaction ?? transaction);
+ const reservationNightsHintText =
+ reservationNights > 1 && actualAmount !== undefined
+ ? translate('common.reservationNightsWithNightlyRate', {count: reservationNights, nightlyRate: convertToDisplayString(actualAmount / reservationNights, actualCurrency)})
+ : '';
const transactionOriginalAmount = transaction && getOriginalAmountForDisplay(transaction, isExpenseReport(moneyRequestReport));
const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency);
@@ -1527,6 +1534,7 @@ function MoneyRequestView({
message={getErrorForField('category')}
/>
)}
+ {!!reservationNightsHintText && }
)}
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 5dd7a081fe84..d7c2a1615d2c 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -139,6 +139,10 @@ const translations: TranslationDeepObject = {
agentThinking: 'Es wird nachgedacht...',
addCardTermsOfService: 'Expensify-Nutzungsbedingungen',
perPerson: 'pro Person',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `1 Nacht • ${nightlyRate} pro Nacht`,
+ other: `${count} Nächte • ${nightlyRate} pro Nacht`,
+ }),
phone: 'Telefon',
phoneNumber: 'Telefonnummer',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 3a0a2f2c3a30..02768e9ad7b5 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -142,6 +142,10 @@ const translations: TranslationDeepObject = {
agentThinking: 'Γίνεται επεξεργασία…',
addCardTermsOfService: 'Όροι χρήσης Expensify',
perPerson: 'ανά άτομο',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `1 διανυκτέρευση • ${nightlyRate} ανά διανυκτέρευση`,
+ other: `${count} διανυκτερεύσεις • ${nightlyRate} ανά διανυκτέρευση`,
+ }),
phone: 'Τηλέφωνο',
phoneNumber: 'Αριθμός τηλεφώνου',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index bb6975f9f669..a56cd9696f53 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -195,6 +195,11 @@ const translations = {
agentThinking: 'Thinking...',
addCardTermsOfService: 'Expensify Terms of Service',
perPerson: 'per person',
+ // @context Hint under the Category field of a multi-night reservation expense, for example “3 nights • $232.42 per night”. nightlyRate is a formatted currency amount.
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `1 night • ${nightlyRate} per night`,
+ other: `${count} nights • ${nightlyRate} per night`,
+ }),
phone: 'Phone',
phoneNumber: 'Phone number',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 1255ad655989..60dec1ea1756 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -121,6 +121,10 @@ const translations: TranslationDeepObject = {
privacyPolicy: 'la Política de Privacidad de Expensify',
addCardTermsOfService: 'Términos de Servicio',
perPerson: 'por persona',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `1 noche • ${nightlyRate} por noche`,
+ other: `${count} noches • ${nightlyRate} por noche`,
+ }),
signIn: 'Conectarse',
signInWithGoogle: 'Iniciar sesión con Google',
signInWithApple: 'Iniciar sesión con Apple',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index c629e2bd4b0e..350acfc33b09 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -139,6 +139,10 @@ const translations: TranslationDeepObject = {
agentThinking: 'Réflexion en cours…',
addCardTermsOfService: 'Conditions d’utilisation d’Expensify',
perPerson: 'par personne',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `${count} nuit • ${nightlyRate} par nuit`,
+ other: `${count} nuits • ${nightlyRate} par nuit`,
+ }),
phone: 'Téléphone',
phoneNumber: 'Numéro de téléphone',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 9db5fb5ac92f..5837d6df194f 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -139,6 +139,10 @@ const translations: TranslationDeepObject = {
agentThinking: 'Sto pensando...',
addCardTermsOfService: 'Termini di servizio di Expensify',
perPerson: 'per persona',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `1 notte • ${nightlyRate} a notte`,
+ other: `${count} notti • ${nightlyRate} a notte`,
+ }),
phone: 'Telefono',
phoneNumber: 'Numero di telefono',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index d412979fa9bc..f59993e8b277 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -139,6 +139,10 @@ const translations: TranslationDeepObject = {
agentThinking: '考えています…',
addCardTermsOfService: 'Expensify 利用規約',
perPerson: '1人あたり',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `1 泊 • 1泊あたり ${nightlyRate}`,
+ other: `${count} 泊 • 1泊あたり ${nightlyRate}`,
+ }),
phone: '電話',
phoneNumber: '電話番号',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 60718020161a..9c13b5029155 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -139,6 +139,10 @@ const translations: TranslationDeepObject = {
agentThinking: 'Bezig met nadenken...',
addCardTermsOfService: 'Expensify-servicevoorwaarden',
perPerson: 'per persoon',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `1 nacht • ${nightlyRate} per nacht`,
+ other: `${count} nachten • ${nightlyRate} per nacht`,
+ }),
phone: 'Telefoon',
phoneNumber: 'Telefoonnummer',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 9792f3bfe001..f51a2345f1f7 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -143,6 +143,12 @@ const translations: TranslationDeepObject = {
agentThinking: 'Myślę…',
addCardTermsOfService: 'Regulamin Expensify',
perPerson: 'na osobę',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `1 noc • ${nightlyRate} za noc`,
+ few: `${count} noce • ${nightlyRate} za noc`,
+ many: `${count} nocy • ${nightlyRate} za noc`,
+ other: `${count} nocy • ${nightlyRate} za noc`,
+ }),
phone: 'Telefon',
phoneNumber: 'Numer telefonu',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index ca2cb4c94ba2..45469451ab30 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -139,6 +139,10 @@ const translations: TranslationDeepObject = {
agentThinking: 'Pensando...',
addCardTermsOfService: 'Termos de Serviço da Expensify',
perPerson: 'por pessoa',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `${count} noite • ${nightlyRate} por noite`,
+ other: `${count} noites • ${nightlyRate} por noite`,
+ }),
phone: 'Telefone',
phoneNumber: 'Número de telefone',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 44a092b96b81..38349442cd5f 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -139,6 +139,10 @@ const translations: TranslationDeepObject = {
agentThinking: '正在思考中…',
addCardTermsOfService: 'Expensify 服务条款',
perPerson: '每人',
+ reservationNightsWithNightlyRate: ({count, nightlyRate}: {count: number; nightlyRate: string}) => ({
+ one: `1 晚 • 每晚 ${nightlyRate}`,
+ other: `${count} 晚 • 每晚 ${nightlyRate}`,
+ }),
phone: '电话',
phoneNumber: '电话号码',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
diff --git a/src/libs/actions/IOU/Receipt.ts b/src/libs/actions/IOU/Receipt.ts
index 4325a7d28327..a6380f22df3a 100644
--- a/src/libs/actions/IOU/Receipt.ts
+++ b/src/libs/actions/IOU/Receipt.ts
@@ -262,6 +262,9 @@ function replaceReceipt({
receiptTraceId,
// Clear the old count while the replacement is pending.
pageCount: null,
+ // A new receipt must not keep the old one's reservation dates, or the per-night limit check would use them.
+ // A crop or rotate keeps the same receipt, so its dates stay.
+ ...(isSameReceipt ? {} : {hotelReservationStartDate: null, hotelReservationEndDate: null}),
};
const newTransaction = transaction && {...transaction, receipt: receiptOptimistic};
diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts
index 54923d5f3601..2a9efc970826 100644
--- a/src/types/onyx/Transaction.ts
+++ b/src/types/onyx/Transaction.ts
@@ -293,11 +293,11 @@ type Receipt = {
/** Correlation id created at capture, used to follow this receipt from capture to upload in the logs. */
receiptTraceId?: string;
- /** Check-in date of a SmartScanned multi-day reservation, in YYYY-MM-DD */
- hotelReservationStartDate?: string;
+ /** Check-in date of a SmartScanned multi-day reservation, in YYYY-MM-DD. Null while a replacement receipt is pending. */
+ hotelReservationStartDate?: string | null;
- /** Check-out date of a SmartScanned multi-day reservation, in YYYY-MM-DD */
- hotelReservationEndDate?: string;
+ /** Check-out date of a SmartScanned multi-day reservation, in YYYY-MM-DD. Null while a replacement receipt is pending. */
+ hotelReservationEndDate?: string | null;
};
/** Model of route */
diff --git a/tests/actions/IOUTest/ReceiptTest.ts b/tests/actions/IOUTest/ReceiptTest.ts
index be990b00294f..be5480757164 100644
--- a/tests/actions/IOUTest/ReceiptTest.ts
+++ b/tests/actions/IOUTest/ReceiptTest.ts
@@ -268,6 +268,61 @@ describe('actions/IOU/Receipt', () => {
});
});
+ it('should clear the previous reservation dates when a new receipt replaces the old one', async () => {
+ // Given an expense whose SmartScanned hotel receipt has reservation dates
+ const transaction = await setupTransactionWithSnapshot(transactionID, {
+ receipt: {...OLD_RECEIPT, hotelReservationStartDate: '2026-10-01', hotelReservationEndDate: '2026-10-04'},
+ });
+
+ // When the user replaces it with a different receipt
+ replaceReceipt({
+ transaction,
+ file: createFile(),
+ source,
+ transactionPolicy: undefined,
+ transactionPolicyTagList: undefined,
+ transactionReport: undefined,
+ isVendorMatchingBetaEnabled: false,
+ delegateAccountID: undefined,
+ currentUserPersonalDetails: {accountID: RORY_ACCOUNT_ID, email: RORY_EMAIL},
+ transactionThreadReport: undefined,
+ });
+ await waitForBatchedUpdates();
+
+ // Then the old dates are gone, so the per-night limit check and hint don't use them for the new receipt
+ const updatedTransaction = await getUpdatedTransaction(transactionID);
+ expect(updatedTransaction?.receipt?.hotelReservationStartDate).toBeUndefined();
+ expect(updatedTransaction?.receipt?.hotelReservationEndDate).toBeUndefined();
+ });
+
+ it('should keep the reservation dates when the same receipt is cropped or rotated', async () => {
+ // Given an expense whose SmartScanned hotel receipt has reservation dates
+ const transaction = await setupTransactionWithSnapshot(transactionID, {
+ receipt: {...OLD_RECEIPT, hotelReservationStartDate: '2026-10-01', hotelReservationEndDate: '2026-10-04'},
+ });
+
+ // When the user crops or rotates that receipt
+ replaceReceipt({
+ transaction,
+ file: createFile(),
+ source,
+ isSameReceipt: true,
+ transactionPolicy: undefined,
+ transactionPolicyTagList: undefined,
+ transactionReport: undefined,
+ isVendorMatchingBetaEnabled: false,
+ delegateAccountID: undefined,
+ currentUserPersonalDetails: {accountID: RORY_ACCOUNT_ID, email: RORY_EMAIL},
+ transactionThreadReport: undefined,
+ });
+ await waitForBatchedUpdates();
+
+ // Then the dates stay, because the receipt is still the same stay
+ const updatedTransaction = await getUpdatedTransaction(transactionID);
+ expect(updatedTransaction?.receipt?.hotelReservationStartDate).toBe('2026-10-01');
+ expect(updatedTransaction?.receipt?.hotelReservationEndDate).toBe('2026-10-04');
+ });
+
it('should optimistically set pending field for receipt', async () => {
// Given a transaction with an existing receipt
const writeSpy = mockApiWrite();
diff --git a/tests/ui/MoneyRequestViewTest.tsx b/tests/ui/MoneyRequestViewTest.tsx
index 14d62db5a01c..996006672fd3 100644
--- a/tests/ui/MoneyRequestViewTest.tsx
+++ b/tests/ui/MoneyRequestViewTest.tsx
@@ -1233,4 +1233,54 @@ describe('MoneyRequestView edit fields', () => {
expect(screen.queryByLabelText(fieldLabel(commuterDistanceDescription))).not.toBeOnTheScreen();
});
});
+
+ describe('reservation nights hint under the Category field', () => {
+ // `translate` is mocked to return the key, so the hint renders as its translation key
+ const reservationNightsHint = 'common.reservationNightsWithNightlyRate';
+
+ const renderWithReservationDates = async (hotelReservationStartDate: string, hotelReservationEndDate: string) => {
+ const threadReport = {
+ ...LHNTestUtils.getFakeReport(),
+ parentReportID: expenseReportID,
+ parentReportActionID,
+ };
+
+ await setupTestData();
+ await act(async () => {
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {
+ Travel: {name: 'Travel', enabled: true},
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
+ category: 'Travel',
+ receipt: {hotelReservationStartDate, hotelReservationEndDate},
+ });
+ });
+ await waitForBatchedUpdatesWithAct();
+
+ renderMoneyRequestView(threadReport, {areCategoriesEnabled: true});
+ await waitForBatchedUpdatesWithAct();
+
+ await waitFor(() => {
+ expect(screen.getByLabelText(fieldLabel('common.category'))).toBeOnTheScreen();
+ });
+ };
+
+ it('shows the nights and nightly rate for a multi-night reservation', async () => {
+ // Given an expense whose SmartScanned reservation spans three nights
+ // When the expense is rendered
+ await renderWithReservationDates('2026-10-01', '2026-10-04');
+
+ // Then the hint explains the per-night evaluation used by the category limit check
+ expect(screen.getByText(reservationNightsHint)).toBeOnTheScreen();
+ });
+
+ it('does not show the hint for a single-night reservation', async () => {
+ // Given an expense whose reservation is a single night, which the design says should show nothing
+ // When the expense is rendered
+ await renderWithReservationDates('2026-10-01', '2026-10-02');
+
+ // Then no nightly hint is shown
+ expect(screen.queryByText(reservationNightsHint)).not.toBeOnTheScreen();
+ });
+ });
});