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
26 changes: 5 additions & 21 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, new Date(), context);
const {startDate, endDate} = getAutoReportingDates(policy, report);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass formula context into auto-reporting date calculation

computeAutoReportingInfo() stopped forwarding FormulaContext into getAutoReportingDates(), so trip auto-reporting dates can no longer see optimistic/context transactions during expense creation and edit flows. In practice, {report:autoreporting:start|end} may be computed from stale Onyx data (or current time fallbacks) until sync completes, which reintroduces incorrect optimistic titles/date ranges for trip-based reports.

Useful? React with 👍 / 👎.


switch (subField.toLowerCase()) {
case 'start':
Expand Down Expand Up @@ -658,21 +658,6 @@ 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 @@ -784,7 +769,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(), context?: FormulaContext): {startDate: Date | undefined; endDate: Date | undefined} {
function getAutoReportingDates(policy: OnyxEntry<Policy>, report: Report, currentDate = new Date()): {startDate: Date | undefined; endDate: Date | undefined} {
const frequency = policy?.autoReportingFrequency;
const offset = policy?.autoReportingOffset;

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

case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP: {
// 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);
// For trip-based, use oldest transaction as start
const oldestTransactionDateString = getOldestTransactionDate(report.reportID);
startDate = oldestTransactionDateString ? new Date(oldestTransactionDateString) : currentDate;
endDate = newestTransactionDateString ? new Date(newestTransactionDateString) : currentDate;
endDate = currentDate;
break;
}

Expand Down
39 changes: 8 additions & 31 deletions src/libs/actions/IOU/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@
canEditFieldOfMoneyRequest,
canSubmitAndIsAwaitingForCurrentUser,
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
computeOptimisticReportName,
findSelfDMReportID,
generateReportID,
getAllHeldTransactions as getAllHeldTransactionsReportUtils,
Expand Down Expand Up @@ -189,6 +188,7 @@
isSettled,
isTestTransactionReport,
isTrackExpenseReport,
populateOptimisticReportFormula,
prepareOnboardingOnyxData,
shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils,
shouldEnableNegative,
Expand Down Expand Up @@ -792,7 +792,7 @@
};

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

Check warning on line 795 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 @@ -925,7 +925,7 @@
};

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

Check warning on line 928 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 @@ -939,7 +939,7 @@
});

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

Check warning on line 942 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 @@ -948,7 +948,7 @@
});

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

Check warning on line 951 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 @@ -962,7 +962,7 @@
});

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

Check warning on line 965 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 @@ -975,7 +975,7 @@
});

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

Check warning on line 978 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 @@ -984,7 +984,7 @@
});

let allReportNameValuePairs: OnyxCollection<OnyxTypes.ReportNameValuePairs>;
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.COLLECTION.REPORT_NAME_VALUE_PAIRS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -994,7 +994,7 @@

let deprecatedUserAccountID = -1;
let deprecatedCurrentUserEmail = '';
Onyx.connect({

Check warning on line 997 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) => {
deprecatedCurrentUserEmail = value?.email ?? '';
Expand All @@ -1003,7 +1003,7 @@
});

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

Check warning on line 1006 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?.[deprecatedUserAccountID] ?? undefined;
Expand All @@ -1011,7 +1011,7 @@
});

let allReportActions: OnyxCollection<OnyxTypes.ReportActions>;
Onyx.connect({

Check warning on line 1014 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_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand Down Expand Up @@ -3192,45 +3192,25 @@
* 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>, newTransaction?: OnyxTypes.Transaction): string | undefined {
function recalculateOptimisticReportName(iouReport: OnyxTypes.Report, policy: OnyxEntry<OnyxTypes.Policy>): 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;
}

// 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;
return populateOptimisticReportFormula(titleFormula, iouReport as Parameters<typeof populateOptimisticReportFormula>[1], policy);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore full formula evaluation for report-name recompute

recalculateOptimisticReportName() now calls populateOptimisticReportFormula(), which only replaces a limited hardcoded token set and drops other {report:*} expressions, so title formulas that previously worked via computeOptimisticReportName() (for example auto-reporting/date-range formulas) will be flattened or partially emptied when an expense is added/edited. This makes optimistic report names diverge from the policy formula behavior and from the initial name computed at report creation time.

Useful? React with 👍 / 👎.

}

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

// 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) {
if (titleField?.type !== CONST.REPORT_FIELD_TYPES.FORMULA) {
return iouReport;
}

const updatedReportName = recalculateOptimisticReportName(iouReport, policy, newTransaction);
const updatedReportName = recalculateOptimisticReportName(iouReport, policy);
if (!updatedReportName) {
return iouReport;
}
Expand Down Expand Up @@ -3414,6 +3394,8 @@
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 @@ -3515,11 +3497,6 @@
}
}

// 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: 0 additions & 65 deletions tests/unit/FormulaTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,71 +675,6 @@ 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