Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e2cd622
fix: Start & End date does not matches Title format with Trip-based A…
TaduJR Nov 15, 2025
f674272
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 16, 2025
c9760f6
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 18, 2025
adb76cd
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 18, 2025
3df3cc2
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 18, 2025
7724474
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 19, 2025
242ca42
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 20, 2025
4921d18
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 22, 2025
ca25d91
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 24, 2025
4ecc69b
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 24, 2025
3b6bea7
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 25, 2025
90af9d4
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 27, 2025
a6befc9
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Nov 29, 2025
cdd53ba
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Dec 2, 2025
8e4d98e
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Dec 2, 2025
5e28585
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Dec 3, 2025
95c13fe
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Dec 3, 2025
1763ffc
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Dec 4, 2025
7942b56
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Dec 6, 2025
f9a3b40
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Dec 10, 2025
f16e461
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Dec 12, 2025
6e6520a
Merge branch 'main' of https://github.com/TaduJR/App into feat-ORTF-A…
TaduJR Jan 7, 2026
275b6e1
refactor: Remove unnecessary empty string type and move transaction f…
TaduJR Jan 7, 2026
16770ef
test: Add unit test for TRIP auto-reporting fallback when no transact…
TaduJR Jan 7, 2026
da5a744
Merge branch 'Expensify:main' into feat-ORTF-Auto-reporting-frequency
TaduJR Jan 12, 2026
95deb4a
revert: Unwanted Changes
TaduJR Jan 12, 2026
6bd5194
revert: Unwanted Changes
TaduJR Jan 12, 2026
2df13fb
Merge branch 'main' of https://github.com/TaduJR/App into feat-ORTF-A…
TaduJR Jan 14, 2026
bd46029
revert: Unwanted Changes
TaduJR Jan 14, 2026
e46478b
Merge branch 'main' of https://github.com/TaduJR/App into feat-ORTF-A…
TaduJR Mar 27, 2026
2aabfc0
fix: pass FormulaContext to getAutoReportingDates for trip frequency …
TaduJR Mar 27, 2026
c591511
test: verify trip autoreporting uses context.transaction for optimist…
TaduJR Mar 27, 2026
eb5436b
fix: use full Formula engine for trip report name recalculation on ex…
TaduJR Mar 27, 2026
d0981ea
test: cover allTransactions merge with Onyx + optimistic transaction …
TaduJR Mar 27, 2026
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
26 changes: 21 additions & 5 deletions src/libs/Formula.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ function computeAutoReportingInfo(part: FormulaPart, context: FormulaContext, su
return part.definition;
}

const {startDate, endDate} = getAutoReportingDates(policy, report);
const {startDate, endDate} = getAutoReportingDates(policy, report, new Date(), context);

switch (subField.toLowerCase()) {
case 'start':
Expand Down Expand Up @@ -658,6 +658,21 @@ function getAllReportTransactionsWithContext(reportID: string, context?: Formula
const transactions = [...getReportTransactions(reportID)];
const contextTransaction = context?.transaction;

// Merge optimistic transactions not yet in Onyx, passed via FormulaContext.allTransactions.
if (context?.allTransactions) {
for (const ctxTransaction of Object.values(context.allTransactions)) {
if (!ctxTransaction?.transactionID || ctxTransaction.reportID !== reportID) {
continue;
}
const existingIndex = transactions.findIndex((t) => t?.transactionID === ctxTransaction.transactionID);
if (existingIndex >= 0) {
transactions[existingIndex] = ctxTransaction;
} else {
transactions.push(ctxTransaction);
}
}
}

if (contextTransaction?.transactionID && contextTransaction.reportID === reportID) {
const transactionIndex = transactions.findIndex((transaction) => transaction?.transactionID === contextTransaction.transactionID);
if (transactionIndex >= 0) {
Expand Down Expand Up @@ -769,7 +784,7 @@ function getMonthlyLastBusinessDayPeriod(currentDate: Date): {startDate: Date; e
/**
* Calculate the start and end dates for auto-reporting based on the frequency and current date
*/
function getAutoReportingDates(policy: OnyxEntry<Policy>, report: Report, currentDate = new Date()): {startDate: Date | undefined; endDate: Date | undefined} {
function getAutoReportingDates(policy: OnyxEntry<Policy>, report: Report, currentDate = new Date(), context?: FormulaContext): {startDate: Date | undefined; endDate: Date | undefined} {
const frequency = policy?.autoReportingFrequency;
const offset = policy?.autoReportingOffset;

Expand Down Expand Up @@ -822,10 +837,11 @@ function getAutoReportingDates(policy: OnyxEntry<Policy>, report: Report, curren
}

case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP: {
// For trip-based, use oldest transaction as start
const oldestTransactionDateString = getOldestTransactionDate(report.reportID);
// For trip-based, use oldest transaction as start and newest transaction as end
const oldestTransactionDateString = getOldestTransactionDate(report.reportID, context);
const newestTransactionDateString = getNewestTransactionDate(report.reportID, context);
startDate = oldestTransactionDateString ? new Date(oldestTransactionDateString) : currentDate;
endDate = currentDate;
endDate = newestTransactionDateString ? new Date(newestTransactionDateString) : currentDate;
break;
}

Expand Down
39 changes: 31 additions & 8 deletions src/libs/actions/IOU/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
canBeAutoReimbursed,
canSubmitAndIsAwaitingForCurrentUser,
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
computeOptimisticReportName,
findSelfDMReportID,
generateReportID,
getAllHeldTransactions as getAllHeldTransactionsReportUtils,
Expand Down Expand Up @@ -181,7 +182,6 @@
isSettled,
isTestTransactionReport,
isTrackExpenseReport,
populateOptimisticReportFormula,
prepareOnboardingOnyxData,
shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils,
shouldEnableNegative,
Expand Down Expand Up @@ -262,7 +262,7 @@
import type {Comment, Receipt, ReceiptSource, Routes, SplitShares, TransactionChanges, TransactionCustomUnit, WaypointCollection} from '@src/types/onyx/Transaction';
import type {FileObject} from '@src/types/utils/Attachment';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {resolveDetachReceiptConflicts} from '../RequestConflictUtils';

Check warning on line 265 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Unexpected parent import '../RequestConflictUtils'. Use '@userActions/RequestConflictUtils' instead

type IOURequestType = ValueOf<typeof CONST.IOU.REQUEST_TYPE>;

Expand Down Expand Up @@ -782,7 +782,7 @@
};

let allPersonalDetails: OnyxTypes.PersonalDetailsList = {};
Onyx.connect({

Check warning on line 785 in src/libs/actions/IOU/index.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
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand Down Expand Up @@ -915,7 +915,7 @@
};

let allTransactions: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 918 in src/libs/actions/IOU/index.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
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -929,7 +929,7 @@
});

let allTransactionDrafts: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 932 in src/libs/actions/IOU/index.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
key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -938,7 +938,7 @@
});

let allTransactionViolations: NonNullable<OnyxCollection<OnyxTypes.TransactionViolations>> = {};
Onyx.connect({

Check warning on line 941 in src/libs/actions/IOU/index.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
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -952,7 +952,7 @@
});

let allPolicyTags: OnyxCollection<OnyxTypes.PolicyTagLists> = {};
Onyx.connect({

Check warning on line 955 in src/libs/actions/IOU/index.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
key: ONYXKEYS.COLLECTION.POLICY_TAGS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -965,7 +965,7 @@
});

let allReports: OnyxCollection<OnyxTypes.Report>;
Onyx.connect({

Check warning on line 968 in src/libs/actions/IOU/index.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
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -974,7 +974,7 @@
});

let allReportNameValuePairs: OnyxCollection<OnyxTypes.ReportNameValuePairs>;
Onyx.connect({

Check warning on line 977 in src/libs/actions/IOU/index.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
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -984,7 +984,7 @@

let userAccountID = -1;
let currentUserEmail = '';
Onyx.connect({

Check warning on line 987 in src/libs/actions/IOU/index.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
key: ONYXKEYS.SESSION,
callback: (value) => {
currentUserEmail = value?.email ?? '';
Expand All @@ -993,7 +993,7 @@
});

let deprecatedCurrentUserPersonalDetails: OnyxEntry<OnyxTypes.PersonalDetails>;
Onyx.connect({

Check warning on line 996 in src/libs/actions/IOU/index.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
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
deprecatedCurrentUserPersonalDetails = value?.[userAccountID] ?? undefined;
Expand Down Expand Up @@ -3184,25 +3184,45 @@
* This is needed when report totals change (e.g., adding expenses or changing reimbursable status)
* to ensure the report title reflects the updated values like {report:reimbursable}.
*/
function recalculateOptimisticReportName(iouReport: OnyxTypes.Report, policy: OnyxEntry<OnyxTypes.Policy>): string | undefined {
function recalculateOptimisticReportName(iouReport: OnyxTypes.Report, policy: OnyxEntry<OnyxTypes.Policy>, newTransaction?: OnyxTypes.Transaction): string | undefined {
if (!policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]) {
return undefined;
}
const titleFormula = policy.fieldList[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]?.defaultValue ?? '';
if (!titleFormula) {
return undefined;
}
return populateOptimisticReportFormula(titleFormula, iouReport as Parameters<typeof populateOptimisticReportFormula>[1], policy);

// Gather existing transactions + the optimistic one not yet in Onyx.
const existingTransactions = getReportTransactions(iouReport.reportID);
const transactionsRecord: Record<string, OnyxTypes.Transaction> = {};
for (const transaction of existingTransactions) {
if (transaction?.transactionID) {
transactionsRecord[transaction.transactionID] = transaction;
}
}
if (newTransaction?.transactionID) {
transactionsRecord[newTransaction.transactionID] = newTransaction;
}

const computedName = computeOptimisticReportName(iouReport, policy, iouReport.policyID, transactionsRecord);
return computedName ?? undefined;
}

function maybeUpdateReportNameForFormulaTitle(iouReport: OnyxTypes.Report, policy: OnyxEntry<OnyxTypes.Policy>): OnyxTypes.Report {
function maybeUpdateReportNameForFormulaTitle(iouReport: OnyxTypes.Report, policy: OnyxEntry<OnyxTypes.Policy>, newTransaction?: OnyxTypes.Transaction): OnyxTypes.Report {
const reportNameValuePairs = allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${iouReport.reportID}`];
const titleField = reportNameValuePairs?.expensify_text_title;
if (titleField?.type !== CONST.REPORT_FIELD_TYPES.FORMULA) {

// Fall back to policy.fieldList when reportNameValuePairs doesn't exist yet (optimistic reports).
const isFormulaTitle = reportNameValuePairs
? titleField?.type === CONST.REPORT_FIELD_TYPES.FORMULA
: policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]?.type === CONST.REPORT_FIELD_TYPES.FORMULA;

if (!isFormulaTitle) {
return iouReport;
}

const updatedReportName = recalculateOptimisticReportName(iouReport, policy);
const updatedReportName = recalculateOptimisticReportName(iouReport, policy, newTransaction);
if (!updatedReportName) {
return iouReport;
}
Expand Down Expand Up @@ -3386,8 +3406,6 @@
iouReport.nonReimbursableTotal = (iouReport.nonReimbursableTotal ?? 0) - amount;
}
}

iouReport = maybeUpdateReportNameForFormulaTitle(iouReport, policy);
}
if (typeof iouReport.unheldTotal === 'number') {
// Use newReportTotal in scenarios where the total is based on more than just the current transaction amount, and we need to override it manually
Expand Down Expand Up @@ -3489,6 +3507,11 @@
}
}

// Recalculate report name after STEP 3 so the optimistic transaction is included in formula computation.
if (!shouldCreateNewMoneyRequestReport && isPolicyExpenseChat) {
iouReport = maybeUpdateReportNameForFormulaTitle(iouReport, policy, optimisticTransaction);
}

// STEP 4: Build optimistic reportActions. We need:
// 1. CREATED action for the chatReport
// 2. CREATED action for the iouReport
Expand Down
65 changes: 65 additions & 0 deletions tests/unit/FormulaTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,71 @@ describe('CustomFormula', () => {
const context = createMockContext(policy);

expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-08');
expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-14');
});

test('should use context.transaction for trip end date when adding a new expense to existing report', () => {
// First transaction already in Onyx (oldest expense, dated Jan 8)
mockReportUtils.getReportTransactions.mockReturnValue([
{transactionID: 'existing1', reportID: '123', created: '2025-01-08T12:00:00Z', merchant: 'Hotel', amount: 5000} as Transaction,
]);

const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP} as Policy;
// Second transaction passed via context (newest expense, dated Jan 14 — not in Onyx yet)
const context: FormulaContext = {
report: mockReport,
policy,
transaction: {transactionID: 'optimistic1', reportID: '123', created: '2025-01-14T16:00:00Z', merchant: 'Restaurant', amount: 3000} as Transaction,
};

// Start should be oldest (Jan 8 from Onyx), end should be newest (Jan 14 from context)
expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-08');
expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-14');
});

test('should use allTransactions for trip dates when Onyx is empty (new report optimistic flow)', () => {
mockReportUtils.getReportTransactions.mockReturnValue([]);

const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP} as Policy;
const context: FormulaContext = {
report: mockReport,
policy,
allTransactions: {
trans1: {transactionID: 'trans1', reportID: '123', created: '2025-01-08T12:00:00Z', merchant: 'Hotel', amount: 5000} as Transaction,
},
};

expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-08');
expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-08');
});

test('should use allTransactions to merge Onyx + optimistic transaction for trip date range', () => {
mockReportUtils.getReportTransactions.mockReturnValue([
{transactionID: 'existing1', reportID: '123', created: '2025-01-08T12:00:00Z', merchant: 'Hotel', amount: 5000} as Transaction,
]);

const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP} as Policy;
const context: FormulaContext = {
report: mockReport,
policy,
allTransactions: {
existing1: {transactionID: 'existing1', reportID: '123', created: '2025-01-08T12:00:00Z', merchant: 'Hotel', amount: 5000} as Transaction,
optimistic1: {transactionID: 'optimistic1', reportID: '123', created: '2025-01-14T16:00:00Z', merchant: 'Restaurant', amount: 3000} as Transaction,
},
};

expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-08');
expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-14');
});

test('should fallback to current date for trip frequency when no transactions', () => {
mockReportUtils.getReportTransactions.mockReturnValue([]);

const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP} as Policy;
const context = createMockContext(policy);

// Should fall back to current date (2025-01-19 from jest.setSystemTime)
expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-19');
expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-19');
});

Expand Down
Loading