From 23d7175148c9ea16e897a375b4eb4afb16ed595c Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Thu, 11 Dec 2025 10:46:16 +0700
Subject: [PATCH 01/14] fix: remove updateOptimisticReportNamesFromUpdates

---
 src/libs/API/index.ts                         |  25 +-
 src/libs/OptimisticReportNames.ts             | 200 +------------
 .../OptimisticReportNames.perf-test.ts        | 192 +------------
 tests/unit/OptimisticReportNamesTest.ts       | 266 +-----------------
 4 files changed, 7 insertions(+), 676 deletions(-)

diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index 44aee2458702..35e9d1d6cdc3 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -8,8 +8,7 @@ import {handleDeletedAccount, HandleUnusedOptimisticID, Logging, Pagination, Rea
 import FraudMonitoring from '@libs/Middleware/FraudMonitoring';
 import {isOffline} from '@libs/Network/NetworkStore';
 import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue';
-import * as OptimisticReportNames from '@libs/OptimisticReportNames';
-import {getUpdateContext, initialize as initializeOptimisticReportNamesContext} from '@libs/OptimisticReportNamesConnectionManager';
+import {initialize as initializeOptimisticReportNamesContext} from '@libs/OptimisticReportNamesConnectionManager';
 import Pusher from '@libs/Pusher';
 import {addMiddleware, processWithMiddleware} from '@libs/Request';
 import {getAll, getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests';
@@ -18,7 +17,7 @@ import type OnyxRequest from '@src/types/onyx/Request';
 import type {PaginatedRequest, PaginationConfig, RequestConflictResolver} from '@src/types/onyx/Request';
 import type Response from '@src/types/onyx/Response';
 import type {ApiCommand, ApiRequestCommandParameters, ApiRequestType, CommandOfType, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types';
-import {READ_COMMANDS, WRITE_COMMANDS} from './types';
+import {READ_COMMANDS} from './types';
 
 // Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next).
 // Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next.
@@ -92,25 +91,7 @@ function prepareRequest<TCommand extends ApiCommand>(
 
     if (optimisticData && shouldApplyOptimisticData) {
         Log.info('[API] Applying optimistic data', false, {command, type});
-
-        // Process optimistic data through report name middleware
-        // Skip for OpenReport command to avoid unnecessary processing
-        if (command === WRITE_COMMANDS.OPEN_REPORT) {
-            Onyx.update(optimisticData);
-        } else {
-            try {
-                const context = getUpdateContext();
-                const processedData = OptimisticReportNames.updateOptimisticReportNamesFromUpdates(optimisticData, context, successData, failureData);
-
-                Onyx.update(processedData.optimisticData);
-                processedSuccessData = processedData.successData;
-                processedFailureData = processedData.failureData;
-            } catch (error) {
-                Log.hmmm('[API] Failed to process optimistic report names', {error});
-                // Fallback to original optimistic data if processing fails
-                Onyx.update(optimisticData);
-            }
-        }
+        Onyx.update(optimisticData);
     }
 
     const isWriteRequest = type === CONST.API_REQUEST_TYPE.WRITE;
diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts
index 1c72f58ed40b..d10d3b71d219 100644
--- a/src/libs/OptimisticReportNames.ts
+++ b/src/libs/OptimisticReportNames.ts
@@ -1,7 +1,5 @@
 import type {OnyxUpdate} from 'react-native-onyx';
-import Onyx from 'react-native-onyx';
 import CONST from '@src/CONST';
-import type {OnyxKey} from '@src/ONYXKEYS';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {Transaction} from '@src/types/onyx';
 import type Policy from '@src/types/onyx/Policy';
@@ -10,35 +8,13 @@ import type {FormulaContext} from './Formula';
 import {compute, FORMULA_PART_TYPES, parse, requiresBackendComputation} from './Formula';
 import Log from './Log';
 import type {UpdateContext} from './OptimisticReportNamesConnectionManager';
-import Permissions from './Permissions';
-import {getTitleReportField, isArchivedReport} from './ReportUtils';
+import {getTitleReportField} from './ReportUtils';
 
 type ReportNameUpdate = {
     name: string | undefined;
     isPending?: boolean;
 };
 
-type ProcessedUpdates = {
-    optimisticData: OnyxUpdate[];
-    successData?: OnyxUpdate[];
-    failureData?: OnyxUpdate[];
-};
-
-type AdditionalUpdates = {
-    optimisticData: OnyxUpdate[];
-    successData: OnyxUpdate[];
-    failureData: OnyxUpdate[];
-};
-
-/**
- * Get the title field from report name value pairs
- */
-// eslint-disable-next-line @typescript-eslint/no-unused-vars -- this will be used in near future
-function getTitleFieldFromRNVP(reportID: string, context: UpdateContext) {
-    const reportNameValuePairs = context.allReportNameValuePairs[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`];
-    return reportNameValuePairs?.expensify_text_title;
-}
-
 /**
  * Temporary function to get the title field from a policy. Eventually we want to move this to report name value pairs.
  * @param policyId
@@ -87,14 +63,6 @@ function getPolicyIDFromKey(key: string): string {
     return key.replace(ONYXKEYS.COLLECTION.POLICY, '');
 }
 
-/**
- * Extract transaction ID from an Onyx key
- */
-// eslint-disable-next-line @typescript-eslint/no-unused-vars -- this will be used in near future
-function getTransactionIDFromKey(key: string): string {
-    return key.replace(ONYXKEYS.COLLECTION.TRANSACTION, '');
-}
-
 /**
  * Get report by ID from the reports collection
  */
@@ -119,39 +87,6 @@ function getTransactionByID(transactionID: string, allTransactions: Record<strin
     return allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
 }
 
-/**
- * Get all reports associated with a policy ID
- */
-function getReportsForNameComputation(policyID: string, allReports: Record<string, Report>, context: UpdateContext): Report[] {
-    if (policyID === CONST.POLICY.ID_FAKE) {
-        return [];
-    }
-    return Object.values(allReports).filter((report) => {
-        if (report?.policyID !== policyID) {
-            return false;
-        }
-
-        // Filter by type - only reports that support custom names
-        if (!isValidReportType(report.type)) {
-            return false;
-        }
-
-        // Filter by state - exclude reports in high states (like approved or higher)
-        const stateThreshold = CONST.REPORT.STATE_NUM.APPROVED;
-        if (report.stateNum && report.stateNum > stateThreshold) {
-            return false;
-        }
-
-        // Filter by isArchived - exclude archived reports
-        const reportNameValuePairs = context.allReportNameValuePairs[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`];
-        if (isArchivedReport(reportNameValuePairs)) {
-            return false;
-        }
-
-        return true;
-    });
-}
-
 /**
  * Get the report associated with a transaction ID
  */
@@ -170,13 +105,6 @@ function getReportByTransactionID(transactionID: string, context: UpdateContext)
     return getReportByID(transaction.reportID, context.allReports);
 }
 
-/**
- * Generate the Onyx key for a report
- */
-function getReportKey(reportID: string): OnyxKey {
-    return `${ONYXKEYS.COLLECTION.REPORT}${reportID}` as OnyxKey;
-}
-
 /**
  * Check if a report should have its name automatically computed
  */
@@ -307,129 +235,5 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O
     return null;
 }
 
-/**
- * Add computed report name updates to the provided update buckets.
- * Appends optimistic data and clears pending fields on success/failure if needed.
- */
-function addAdditionalUpdates(reportID: string, reportNameUpdate: ReportNameUpdate, additionalUpdates: AdditionalUpdates) {
-    // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-    additionalUpdates.optimisticData.push({
-        key: getReportKey(reportID),
-        onyxMethod: Onyx.METHOD.MERGE,
-        value: {
-            reportName: reportNameUpdate.name,
-            ...(reportNameUpdate.isPending && {
-                pendingFields: {reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
-            }),
-        },
-    });
-
-    // If there's a pending field, add updates to clear it in success/failure data
-    if (reportNameUpdate.isPending) {
-        // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-        const clearPendingUpdate: OnyxUpdate = {
-            key: getReportKey(reportID),
-            onyxMethod: Onyx.METHOD.MERGE,
-            value: {pendingFields: {reportName: null}},
-        };
-
-        additionalUpdates.successData.push(clearPendingUpdate);
-        additionalUpdates.failureData.push(clearPendingUpdate);
-    }
-}
-
-/**
- * Update optimistic report names based on incoming updates
- * This is the main middleware function that processes optimistic data
- */
-function updateOptimisticReportNamesFromUpdates(optimisticData: OnyxUpdate[], context: UpdateContext, successData?: OnyxUpdate[], failureData?: OnyxUpdate[]): ProcessedUpdates {
-    const {betas, allReports, betaConfiguration} = context;
-
-    // Check if the feature is enabled
-    if (!Permissions.isBetaEnabled(CONST.BETAS.CUSTOM_REPORT_NAMES, betas, betaConfiguration)) {
-        return {
-            optimisticData,
-            successData,
-            failureData,
-        };
-    }
-
-    Log.info('[OptimisticReportNames] Processing optimistic updates for report names', false, {
-        updatesCount: optimisticData.length,
-    });
-
-    const additionalUpdates: AdditionalUpdates = {
-        optimisticData: [],
-        successData: [],
-        failureData: [],
-    };
-
-    for (const update of optimisticData) {
-        const objectType = determineObjectTypeByKey(update.key);
-
-        switch (objectType) {
-            case 'report': {
-                const reportID = getReportIDFromKey(update.key);
-                const report = getReportByID(reportID, allReports);
-
-                // Handle both existing and new reports with the same function
-                const reportNameUpdate = computeReportNameIfNeeded(report, update, context);
-
-                if (reportNameUpdate) {
-                    addAdditionalUpdates(reportID, reportNameUpdate, additionalUpdates);
-                }
-                break;
-            }
-
-            case 'policy': {
-                const policyID = getPolicyIDFromKey(update.key);
-                const affectedReports = getReportsForNameComputation(policyID, allReports, context);
-                for (const report of affectedReports) {
-                    const reportNameUpdate = computeReportNameIfNeeded(report, update, context);
-
-                    if (reportNameUpdate) {
-                        addAdditionalUpdates(report.reportID, reportNameUpdate, additionalUpdates);
-                    }
-                }
-                break;
-            }
-
-            case 'transaction': {
-                let report: Report | undefined;
-                const transactionUpdate = update.value as Partial<Transaction>;
-                if (transactionUpdate.reportID) {
-                    report = getReportByID(transactionUpdate.reportID, allReports);
-                } else {
-                    report = getReportByTransactionID(getTransactionIDFromKey(update.key), context);
-                }
-
-                if (report) {
-                    const reportNameUpdate = computeReportNameIfNeeded(report, update, context);
-
-                    if (reportNameUpdate) {
-                        addAdditionalUpdates(report.reportID, reportNameUpdate, additionalUpdates);
-                    }
-                }
-                break;
-            }
-
-            default:
-                continue;
-        }
-    }
-
-    Log.info('[OptimisticReportNames] Processing completed', false, {
-        additionalOptimisticUpdatesCount: additionalUpdates.optimisticData.length,
-        additionalSuccessUpdatesCount: additionalUpdates.successData.length,
-        additionalFailureUpdatesCount: additionalUpdates.failureData.length,
-    });
-
-    return {
-        optimisticData: optimisticData.concat(additionalUpdates.optimisticData),
-        successData: successData ? successData.concat(additionalUpdates.successData) : additionalUpdates.successData,
-        failureData: failureData ? failureData.concat(additionalUpdates.failureData) : additionalUpdates.failureData,
-    };
-}
-
-export {computeReportNameIfNeeded, getReportByTransactionID, shouldComputeReportName, updateOptimisticReportNamesFromUpdates};
+export {computeReportNameIfNeeded, getReportByTransactionID, shouldComputeReportName};
 export type {UpdateContext};
diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts
index e40cca17aabb..1b50f220f4b7 100644
--- a/tests/perf-test/OptimisticReportNames.perf-test.ts
+++ b/tests/perf-test/OptimisticReportNames.perf-test.ts
@@ -1,7 +1,7 @@
 import Onyx from 'react-native-onyx';
 import {measureFunction} from 'reassure';
 import type {UpdateContext} from '@libs/OptimisticReportNames';
-import {computeReportNameIfNeeded, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames';
+import {computeReportNameIfNeeded} from '@libs/OptimisticReportNames';
 // eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock
 import * as ReportUtils from '@libs/ReportUtils';
 import CONST from '@src/CONST';
@@ -98,194 +98,4 @@ describe('[OptimisticReportNames] Performance Tests', () => {
             await measureFunction(() => computeReportNameIfNeeded(report, update, mockContext));
         });
     });
-
-    describe('Batch Processing Performance', () => {
-        test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 10 new reports', async () => {
-            const updates = Array.from({length: 10}, (_, i) => ({
-                key: `report_new${i}` as OnyxKey,
-                onyxMethod: Onyx.METHOD.SET,
-                value: {
-                    reportID: `new${i}`,
-                    policyID: `policy${i % 10}`,
-                    total: -(Math.random() * 50000),
-                    currency: 'USD',
-                    lastVisibleActionCreated: new Date().toISOString(),
-                },
-            }));
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext));
-        });
-
-        test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 50 existing report updates', async () => {
-            const reportKeys = Object.keys(mockReports).slice(0, 50) as OnyxKey[];
-            const updates = reportKeys.map((key) => ({
-                key,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {total: -(Math.random() * 100000)},
-            }));
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext));
-        });
-
-        test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 100 mixed updates', async () => {
-            const newReportUpdates = Array.from({length: 50}, (_, i) => ({
-                key: `report_batch${i}` as OnyxKey,
-                onyxMethod: Onyx.METHOD.SET,
-                value: {
-                    reportID: `batch${i}`,
-                    policyID: `policy${i % 20}`,
-                    total: -(Math.random() * 75000),
-                    currency: 'USD',
-                    lastVisibleActionCreated: new Date().toISOString(),
-                },
-            }));
-
-            const existingReportUpdates = Object.keys(mockReports)
-                .slice(0, 50)
-                .map((key) => ({
-                    key: key as OnyxKey,
-                    onyxMethod: Onyx.METHOD.MERGE,
-                    value: {total: -(Math.random() * 125000)},
-                }));
-
-            const allUpdates = [...newReportUpdates, ...existingReportUpdates];
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => updateOptimisticReportNamesFromUpdates(allUpdates, mockContext));
-        });
-    });
-
-    describe('Policy Update Impact Performance', () => {
-        test('[OptimisticReportNames] policy update affecting multiple reports', async () => {
-            const policyUpdate = {
-                key: 'policy_policy1' as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {name: 'Updated Policy Name'},
-            };
-
-            // This should trigger name computation for all reports using policy1
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => updateOptimisticReportNamesFromUpdates([policyUpdate], mockContext));
-        });
-
-        test('[OptimisticReportNames] multiple policy updates', async () => {
-            const policyUpdates = Array.from({length: 10}, (_, i) => ({
-                key: `policy_policy${i}` as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {name: `Bulk Updated Policy ${i}`},
-            }));
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => updateOptimisticReportNamesFromUpdates(policyUpdates, mockContext));
-        });
-    });
-
-    describe('Large Dataset Performance', () => {
-        test('[OptimisticReportNames] processing with large context (1000 reports)', async () => {
-            const updates = Array.from({length: 1000}, (_, i) => ({
-                key: `report_large${i}` as OnyxKey,
-                onyxMethod: Onyx.METHOD.SET,
-                value: {
-                    reportID: `large${i}`,
-                    policyID: `policy${i % 50}`,
-                    total: -(Math.random() * 200000),
-                    currency: 'USD',
-                    lastVisibleActionCreated: new Date().toISOString(),
-                },
-            }));
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext));
-        });
-
-        test('[OptimisticReportNames] worst case: many irrelevant updates', async () => {
-            // Create updates that won't trigger name computation to test filtering performance
-            const irrelevantUpdates = Array.from({length: 100}, (_, i) => ({
-                key: `transaction_${i}` as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {description: `Updated transaction ${i}`},
-            }));
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => updateOptimisticReportNamesFromUpdates(irrelevantUpdates, mockContext));
-        });
-    });
-
-    describe('Edge Cases Performance', () => {
-        test('[OptimisticReportNames] reports without formulas', async () => {
-            // Mock reports with policies that don't have formulas
-            const contextWithoutFormulas: UpdateContext = {
-                ...mockContext,
-                allPolicies: createCollection(
-                    (item) => `policy_${item.id}`,
-                    (index) =>
-                        ({
-                            id: `policy${index}`,
-                            name: `Policy ${index}`,
-                            fieldList: {
-                                // eslint-disable-next-line @typescript-eslint/naming-convention
-                                text_title: {
-                                    name: 'Title',
-                                    defaultValue: 'Static Title',
-                                    fieldID: 'text_title',
-                                    orderWeight: 0,
-                                    type: 'text' as const,
-                                    deletable: true,
-                                    values: [],
-                                    keys: [],
-                                    externalIDs: [],
-                                    disabledOptions: [],
-                                    isTax: false,
-                                },
-                            },
-                        }) as unknown as Policy,
-                    50,
-                ),
-                allReportNameValuePairs: {},
-                isOffline: false,
-            };
-
-            const updates = Array.from({length: 20}, (_, i) => ({
-                key: `report_static${i}` as OnyxKey,
-                onyxMethod: Onyx.METHOD.SET,
-                value: {
-                    reportID: `static${i}`,
-                    policyID: `policy${i % 10}`,
-                    total: -10000,
-                    currency: 'USD',
-                },
-            }));
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, contextWithoutFormulas));
-        });
-
-        test('[OptimisticReportNames] missing policies and reports', async () => {
-            const contextWithMissingData: UpdateContext = {
-                betas: [CONST.BETAS.CUSTOM_REPORT_NAMES],
-                betaConfiguration: {},
-                allReports: {},
-                allPolicies: {},
-                allReportNameValuePairs: {},
-                allTransactions: {},
-                isOffline: false,
-            };
-
-            const updates = Array.from({length: 10}, (_, i) => ({
-                key: `report_missing${i}` as OnyxKey,
-                onyxMethod: Onyx.METHOD.SET,
-                value: {
-                    reportID: `missing${i}`,
-                    policyID: 'nonexistent',
-                    total: -10000,
-                    currency: 'USD',
-                },
-            }));
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, contextWithMissingData));
-        });
-    });
 });
diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts
index bd005f1003e2..36121446bf31 100644
--- a/tests/unit/OptimisticReportNamesTest.ts
+++ b/tests/unit/OptimisticReportNamesTest.ts
@@ -2,7 +2,7 @@ import Onyx from 'react-native-onyx';
 // eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock
 import type * as CurrencyUtils from '@libs/CurrencyUtils';
 import type {UpdateContext} from '@libs/OptimisticReportNames';
-import {computeReportNameIfNeeded, getReportByTransactionID, shouldComputeReportName, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames';
+import {computeReportNameIfNeeded, getReportByTransactionID, shouldComputeReportName} from '@libs/OptimisticReportNames';
 // eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock
 import * as ReportUtils from '@libs/ReportUtils';
 import CONST from '@src/CONST';
@@ -143,135 +143,6 @@ describe('OptimisticReportNames', () => {
         });
     });
 
-    describe('updateOptimisticReportNamesFromUpdates()', () => {
-        test('should detect new report creation and add name update', () => {
-            const updates = [
-                {
-                    key: 'report_456' as OnyxKey,
-                    onyxMethod: Onyx.METHOD.SET,
-                    value: {
-                        reportID: '456',
-                        policyID: 'policy1',
-                        total: -15000,
-                        currency: 'USD',
-                        type: 'expense',
-                    },
-                },
-            ];
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            const result = updateOptimisticReportNamesFromUpdates(updates, mockContext);
-            expect(result.optimisticData).toHaveLength(2); // Original + name update
-            expect(result.optimisticData.at(1)).toEqual({
-                key: 'report_456',
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {reportName: 'Expense Report - $150.00'},
-            });
-        });
-
-        test('should handle existing report updates', () => {
-            const updates = [
-                {
-                    key: 'report_123' as OnyxKey,
-                    onyxMethod: Onyx.METHOD.MERGE,
-                    value: {total: -25000},
-                },
-            ];
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            const result = updateOptimisticReportNamesFromUpdates(updates, mockContext);
-            expect(result.optimisticData).toHaveLength(2); // Original + name update
-            expect(result.optimisticData.at(1)?.value).toEqual({reportName: 'Expense Report - $250.00'});
-        });
-
-        test('should handle policy updates affecting multiple reports', () => {
-            const contextWithMultipleReports = {
-                ...mockContext,
-                allReports: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    report_123: {...mockReport, reportID: '123'},
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    report_456: {...mockReport, reportID: '456'},
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    report_789: {...mockReport, reportID: '789'},
-                },
-                allPolicies: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    policy_policy1: {
-                        id: 'policy1',
-                        fieldList: {
-                            [CONST.REPORT_FIELD_TITLE_FIELD_ID]: {
-                                fieldID: CONST.REPORT_FIELD_TITLE_FIELD_ID,
-                                defaultValue: 'Policy: {report:policyname}',
-                            },
-                        },
-                    } as unknown as Policy,
-                },
-                allReportNameValuePairs: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    reportNameValuePairs_123: {private_isArchived: '', expensify_text_title: {defaultValue: 'Policy: {report:policyname}'}} as unknown as ReportNameValuePairs,
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    reportNameValuePairs_456: {private_isArchived: '', expensify_text_title: {defaultValue: 'Policy: {report:policyname}'}} as unknown as ReportNameValuePairs,
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    reportNameValuePairs_789: {private_isArchived: '', expensify_text_title: {defaultValue: 'Policy: {report:policyname}'}} as unknown as ReportNameValuePairs,
-                },
-            };
-
-            const updates = [
-                {
-                    key: 'policy_policy1' as OnyxKey,
-                    onyxMethod: Onyx.METHOD.MERGE,
-                    value: {name: 'Updated Policy Name'},
-                },
-            ];
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            const result = updateOptimisticReportNamesFromUpdates(updates, contextWithMultipleReports);
-
-            expect(result.optimisticData).toHaveLength(4);
-
-            // Assert the original policy update
-            expect(result.optimisticData.at(0)).toEqual({
-                key: 'policy_policy1',
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {name: 'Updated Policy Name'},
-            });
-
-            // Assert individual report name updates
-            expect(result.optimisticData.at(1)).toEqual({
-                key: 'report_123',
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {reportName: 'Policy: Updated Policy Name'},
-            });
-
-            expect(result.optimisticData.at(2)).toEqual({
-                key: 'report_456',
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {reportName: 'Policy: Updated Policy Name'},
-            });
-
-            expect(result.optimisticData.at(3)).toEqual({
-                key: 'report_789',
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {reportName: 'Policy: Updated Policy Name'},
-            });
-        });
-
-        test('should handle unknown object types gracefully', () => {
-            const updates = [
-                {
-                    key: 'unknown_123' as OnyxKey,
-                    onyxMethod: Onyx.METHOD.MERGE,
-                    value: {someData: 'value'},
-                },
-            ];
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            const result = updateOptimisticReportNamesFromUpdates(updates, mockContext);
-            expect(result.optimisticData).toEqual(updates); // Unchanged
-        });
-    });
-
     describe('Edge Cases', () => {
         test('should handle missing report gracefully', () => {
             const update = {
@@ -287,40 +158,6 @@ describe('OptimisticReportNames', () => {
     });
 
     describe('Transaction Updates', () => {
-        test('should process transaction updates and trigger report name updates', () => {
-            const contextWithTransaction = {
-                ...mockContext,
-                allTransactions: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    transactions_txn123: {
-                        transactionID: 'txn123',
-                        reportID: '123',
-                        created: '2024-01-01',
-                        amount: -5000,
-                        currency: 'USD',
-                        merchant: 'Original Merchant',
-                    },
-                },
-            };
-
-            const update = {
-                key: 'transactions_txn123' as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {
-                    created: '2024-02-15', // Updated date
-                    reportID: '123',
-                },
-            };
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            const result = updateOptimisticReportNamesFromUpdates([update], contextWithTransaction);
-
-            // Should include original update + new report name update
-            expect(result.optimisticData).toHaveLength(2);
-            expect(result.optimisticData.at(0)).toEqual(update); // Original transaction update
-            expect(result.optimisticData.at(1)?.key).toBe('report_123'); // New report update
-        });
-
         test('getReportByTransactionID should find report from transaction', () => {
             const contextWithTransaction = {
                 ...mockContext,
@@ -366,106 +203,5 @@ describe('OptimisticReportNames', () => {
             const result = getReportByTransactionID('incomplete', contextWithIncompleteTransaction);
             expect(result).toBeUndefined();
         });
-
-        test('should handle transaction updates that rely on context lookup', () => {
-            const contextWithTransaction = {
-                ...mockContext,
-                allTransactions: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    transactions_xyz789: {
-                        transactionID: 'xyz789',
-                        reportID: '123',
-                        created: '2024-01-01',
-                        amount: -3000,
-                        currency: 'EUR',
-                        merchant: 'Context Store',
-                    },
-                },
-            };
-
-            // Transaction update without reportID in the value
-            const update = {
-                key: 'transactions_xyz789' as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {
-                    amount: -4000, // Updated amount
-                    // No reportID provided in update
-                },
-            };
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            const result = updateOptimisticReportNamesFromUpdates([update], contextWithTransaction);
-
-            // Should still find the report through context lookup and generate update
-            expect(result.optimisticData).toHaveLength(2);
-            expect(result.optimisticData.at(1)?.key).toBe('report_123');
-        });
-
-        test('should use optimistic transaction data in formula computation', () => {
-            const contextWithTransaction = {
-                ...mockContext,
-                allReportNameValuePairs: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    reportNameValuePairs_123: {
-                        // eslint-disable-next-line @typescript-eslint/naming-convention
-                        expensify_text_title: {
-                            defaultValue: 'Report from {report:startdate}',
-                        },
-                    } as unknown as ReportNameValuePairs,
-                },
-                allPolicies: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    policy_policy1: {
-                        id: 'policy1',
-                        fieldList: {
-                            [CONST.REPORT_FIELD_TITLE_FIELD_ID]: {
-                                fieldID: CONST.REPORT_FIELD_TITLE_FIELD_ID,
-                                defaultValue: 'Report from {report:startdate}',
-                            },
-                        },
-                    } as unknown as Policy,
-                },
-                allTransactions: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    transactions_formula123: {
-                        transactionID: 'formula123',
-                        reportID: '123',
-                        created: '2024-01-01', // Original date
-                        amount: -5000,
-                        currency: 'USD',
-                        merchant: 'Original Store',
-                    },
-                },
-            };
-
-            // Mock getReportTransactions to return the original transaction
-            // eslint-disable-next-line @typescript-eslint/dot-notation
-            mockReportUtils.getReportTransactions.mockReturnValue([contextWithTransaction.allTransactions['transactions_formula123']]);
-
-            const update = {
-                key: 'transactions_formula123' as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {
-                    transactionID: 'formula123',
-                    created: '2024-03-15', // Updated date that should be used in formula
-                    modifiedCreated: '2024-03-15',
-                },
-            };
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            const result = updateOptimisticReportNamesFromUpdates([update], contextWithTransaction);
-
-            expect(result.optimisticData).toHaveLength(2);
-
-            // The key test: verify exact report name with optimistic date
-            const reportUpdate = result.optimisticData.at(1);
-            expect(reportUpdate).toEqual({
-                key: 'report_123',
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {
-                    reportName: 'Report from 2024-03-15', // Exact expected result with updated date
-                },
-            });
-        });
     });
 });

From c1ade0b3e446d635ff0d660f24a3cf0632a5fafa Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Thu, 11 Dec 2025 11:04:52 +0700
Subject: [PATCH 02/14] fix: lint

---
 src/libs/API/index.ts | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index 35e9d1d6cdc3..26a7da670a30 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -86,9 +86,6 @@ function prepareRequest<TCommand extends ApiCommand>(
 
     const {optimisticData, successData, failureData, ...onyxDataWithoutOptimisticData} = onyxData;
 
-    let processedSuccessData = successData;
-    let processedFailureData = failureData;
-
     if (optimisticData && shouldApplyOptimisticData) {
         Log.info('[API] Applying optimistic data', false, {command, type});
         Onyx.update(optimisticData);
@@ -118,8 +115,8 @@ function prepareRequest<TCommand extends ApiCommand>(
         initiatedOffline: isOffline(),
         requestID: requestIndex++,
         ...onyxDataWithoutOptimisticData,
-        successData: processedSuccessData,
-        failureData: processedFailureData,
+        successData,
+        failureData,
         ...conflictResolver,
     };
 

From 9d8eeaefb0503d361105e64872c1e5aa9882cc04 Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Mon, 15 Dec 2025 09:11:09 +0700
Subject: [PATCH 03/14] fix: remove all related files

---
 src/libs/OptimisticReportNames.ts             | 239 ------------------
 .../OptimisticReportNames.perf-test.ts        | 101 --------
 tests/unit/OptimisticReportNamesTest.ts       | 207 ---------------
 3 files changed, 547 deletions(-)
 delete mode 100644 src/libs/OptimisticReportNames.ts
 delete mode 100644 tests/perf-test/OptimisticReportNames.perf-test.ts
 delete mode 100644 tests/unit/OptimisticReportNamesTest.ts

diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts
deleted file mode 100644
index d10d3b71d219..000000000000
--- a/src/libs/OptimisticReportNames.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-import type {OnyxUpdate} from 'react-native-onyx';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Transaction} from '@src/types/onyx';
-import type Policy from '@src/types/onyx/Policy';
-import type Report from '@src/types/onyx/Report';
-import type {FormulaContext} from './Formula';
-import {compute, FORMULA_PART_TYPES, parse, requiresBackendComputation} from './Formula';
-import Log from './Log';
-import type {UpdateContext} from './OptimisticReportNamesConnectionManager';
-import {getTitleReportField} from './ReportUtils';
-
-type ReportNameUpdate = {
-    name: string | undefined;
-    isPending?: boolean;
-};
-
-/**
- * Temporary function to get the title field from a policy. Eventually we want to move this to report name value pairs.
- * @param policyId
- * @param context
- */
-function getTitleFieldFromPolicy(policyId: string | undefined, context: UpdateContext) {
-    if (!policyId) {
-        return;
-    }
-
-    const policy = context.allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyId}`];
-    if (!policy || !policy.fieldList) {
-        return;
-    }
-
-    return getTitleReportField(policy.fieldList);
-}
-
-/**
- * Get the object type from an Onyx key
- */
-function determineObjectTypeByKey(key: string): 'report' | 'policy' | 'transaction' | 'unknown' {
-    if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) {
-        return 'report';
-    }
-    if (key.startsWith(ONYXKEYS.COLLECTION.POLICY)) {
-        return 'policy';
-    }
-    if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
-        return 'transaction';
-    }
-    return 'unknown';
-}
-
-/**
- * Extract report ID from an Onyx key
- */
-function getReportIDFromKey(key: string): string {
-    return key.replace(ONYXKEYS.COLLECTION.REPORT, '');
-}
-
-/**
- * Extract policy ID from an Onyx key
- */
-function getPolicyIDFromKey(key: string): string {
-    return key.replace(ONYXKEYS.COLLECTION.POLICY, '');
-}
-
-/**
- * Get report by ID from the reports collection
- */
-function getReportByID(reportID: string, allReports: Record<string, Report>): Report | undefined {
-    return allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
-}
-
-/**
- * Get policy by ID from the policies collection
- */
-function getPolicyByID(policyID: string | undefined, allPolicies: Record<string, Policy>): Policy | undefined {
-    if (!policyID) {
-        return;
-    }
-    return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
-}
-
-/**
- * Get transaction by ID from the transactions collection
- */
-function getTransactionByID(transactionID: string, allTransactions: Record<string, Transaction>): Transaction | undefined {
-    return allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
-}
-
-/**
- * Get the report associated with a transaction ID
- */
-function getReportByTransactionID(transactionID: string, context: UpdateContext): Report | undefined {
-    if (!transactionID) {
-        return undefined;
-    }
-
-    const transaction = getTransactionByID(transactionID, context.allTransactions);
-
-    if (!transaction?.reportID) {
-        return undefined;
-    }
-
-    // Get the report using the transaction's reportID from context
-    return getReportByID(transaction.reportID, context.allReports);
-}
-
-/**
- * Check if a report should have its name automatically computed
- */
-function shouldComputeReportName(report: Report, context: UpdateContext): boolean {
-    if (!report) {
-        return false;
-    }
-
-    if (!isValidReportType(report.type)) {
-        return false;
-    }
-
-    // Only compute names for expense reports with policies that have title fields
-    // Check if the report has a title field with a formula in policy
-    const policyTitleField = getTitleFieldFromPolicy(report.policyID, context);
-    if (!policyTitleField?.defaultValue) {
-        return false;
-    }
-    return true;
-}
-
-function isValidReportType(reportType?: string): boolean {
-    if (!reportType) {
-        return false;
-    }
-    return (
-        reportType === CONST.REPORT.TYPE.EXPENSE ||
-        reportType === CONST.REPORT.TYPE.INVOICE ||
-        reportType === CONST.REPORT.UNSUPPORTED_TYPE.BILL ||
-        reportType === CONST.REPORT.UNSUPPORTED_TYPE.PAYCHECK ||
-        reportType === 'trip'
-    );
-}
-
-/**
- * Compute a new report name if needed based on an optimistic update
- */
-function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: OnyxUpdate, context: UpdateContext): ReportNameUpdate | null {
-    const {allPolicies, isOffline} = context;
-
-    // If no report is provided, extract it from the update (for new reports)
-    const targetReport = report ?? (incomingUpdate.value as Report);
-
-    if (!targetReport) {
-        return null;
-    }
-
-    const policy = getPolicyByID(targetReport.policyID, allPolicies);
-
-    if (!shouldComputeReportName(targetReport, context)) {
-        return null;
-    }
-
-    const titleField = getTitleFieldFromPolicy(targetReport.policyID, context);
-
-    // Quick check: see if the update might affect the report name
-    const updateType = determineObjectTypeByKey(incomingUpdate.key);
-    const formula = titleField?.defaultValue;
-    const formulaParts = parse(formula);
-
-    let transaction: Transaction | undefined;
-    if (updateType === 'transaction') {
-        transaction = getTransactionByID((incomingUpdate.value as Transaction).transactionID, context.allTransactions);
-    }
-
-    // Check if any formula part might be affected by this update
-    const isAffected = formulaParts.some((part) => {
-        if (part.type === FORMULA_PART_TYPES.REPORT) {
-            // Checking if the formula part is affected in this manner works, but it could certainly be more precise.
-            // For example, a policy update only affects the part if the formula in the policy changed, or if the report part references a field on the policy.
-            // However, if we run into performance problems, this would be a good place to optimize.
-            return updateType === 'report' || updateType === 'transaction' || updateType === 'policy';
-        }
-        if (part.type === FORMULA_PART_TYPES.FIELD) {
-            return updateType === 'report';
-        }
-        return false;
-    });
-
-    if (!isAffected) {
-        return null;
-    }
-
-    // Build context with the updated data
-    const updatedReport =
-        updateType === 'report' && targetReport.reportID === getReportIDFromKey(incomingUpdate.key) ? {...targetReport, ...(incomingUpdate.value as Partial<Report>)} : targetReport;
-
-    const updatedPolicy = updateType === 'policy' && targetReport.policyID === getPolicyIDFromKey(incomingUpdate.key) ? {...(policy ?? {}), ...(incomingUpdate.value as Policy)} : policy;
-
-    const updatedTransaction = updateType === 'transaction' ? {...(transaction ?? {}), ...(incomingUpdate.value as Transaction)} : undefined;
-
-    // Compute the new name
-    const formulaContext: FormulaContext = {
-        report: updatedReport,
-        policy: updatedPolicy,
-        transaction: updatedTransaction,
-        allTransactions: context.allTransactions,
-    };
-
-    // When we cannot properly compute the formula (e.g., currency conversion requires exchange rates),
-    // computing while online causes flickering between incorrect optimistic values and correct backend values.
-    // Return null to skip optimistic updates and let the backend provide the accurate result.
-    if (requiresBackendComputation(formulaParts, formulaContext)) {
-        if (!isOffline) {
-            return null;
-        }
-
-        return {
-            name: targetReport.reportName,
-            isPending: true,
-        };
-    }
-
-    const newName = compute(formula, formulaContext);
-
-    // Only return an update if the name actually changed
-    if (newName && newName !== targetReport.reportName) {
-        Log.info('[OptimisticReportNames] Report name computed', false, {
-            updateType,
-            isNewReport: !report,
-        });
-
-        return {
-            name: newName,
-        };
-    }
-
-    return null;
-}
-
-export {computeReportNameIfNeeded, getReportByTransactionID, shouldComputeReportName};
-export type {UpdateContext};
diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts
deleted file mode 100644
index 1b50f220f4b7..000000000000
--- a/tests/perf-test/OptimisticReportNames.perf-test.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import Onyx from 'react-native-onyx';
-import {measureFunction} from 'reassure';
-import type {UpdateContext} from '@libs/OptimisticReportNames';
-import {computeReportNameIfNeeded} from '@libs/OptimisticReportNames';
-// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock
-import * as ReportUtils from '@libs/ReportUtils';
-import CONST from '@src/CONST';
-import type {OnyxKey} from '@src/ONYXKEYS';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Policy, Report} from '@src/types/onyx';
-import createCollection from '../utils/collections/createCollection';
-import {createRandomReport} from '../utils/collections/reports';
-import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
-
-// Mock dependencies
-jest.mock('@libs/ReportUtils', () => ({
-    // jest.requireActual is necessary to include multi-layered module imports (eg. Report.ts has processReportIDDeeplink() which uses parseReportRouteParams() imported from getReportIDFromUrl.ts)
-    // Without jest.requireActual, parseReportRouteParams would be undefined, causing the test to fail.
-    ...jest.requireActual<typeof ReportUtils>('@libs/ReportUtils'),
-    // These methods are mocked below in the beforeAll function to return specific values
-    isExpenseReport: jest.fn(),
-    getTitleReportField: jest.fn(),
-}));
-jest.mock('@libs/Log', () => ({
-    info: jest.fn(),
-}));
-
-const mockReportUtils = ReportUtils as jest.Mocked<typeof ReportUtils>;
-
-describe('[OptimisticReportNames] Performance Tests', () => {
-    const REPORTS_COUNT = 1000;
-    const POLICIES_COUNT = 100;
-
-    const mockPolicy = {
-        id: 'policy1',
-        name: 'Test Policy',
-        fieldList: {
-            // eslint-disable-next-line @typescript-eslint/naming-convention
-            text_title: {
-                defaultValue: '{report:type} - {report:startdate} - {report:total} {report:currency}',
-            },
-        },
-    } as unknown as Policy;
-
-    const mockPolicies = createCollection<Policy>(
-        (item) => `policy_${item.id}`,
-        (index) => ({
-            ...mockPolicy,
-            id: `policy${index}`,
-            name: `Policy ${index}`,
-        }),
-        POLICIES_COUNT,
-    );
-
-    const mockReports = createCollection<Report>(
-        (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`,
-        (index) => ({
-            ...createRandomReport(index, undefined),
-            policyID: `policy${index % POLICIES_COUNT}`,
-            total: -(Math.random() * 100000), // Random negative amount
-            currency: 'USD',
-            lastVisibleActionCreated: new Date().toISOString(),
-        }),
-        REPORTS_COUNT,
-    );
-
-    const mockContext: UpdateContext = {
-        betas: [CONST.BETAS.CUSTOM_REPORT_NAMES],
-        betaConfiguration: {},
-        allReports: mockReports,
-        allPolicies: mockPolicies,
-        allReportNameValuePairs: {},
-        allTransactions: {},
-        isOffline: false,
-    };
-
-    beforeAll(async () => {
-        Onyx.init({keys: ONYXKEYS});
-        mockReportUtils.isExpenseReport.mockReturnValue(true);
-        mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList?.text_title);
-        await waitForBatchedUpdates();
-    });
-
-    afterAll(() => {
-        Onyx.clear();
-    });
-
-    describe('Single Report Name Computation', () => {
-        test('[OptimisticReportNames] computeReportNameIfNeeded() single report', async () => {
-            const report = Object.values(mockReports).at(0);
-            const update = {
-                key: `report_${report?.reportID}` as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {total: -20000},
-            };
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            await measureFunction(() => computeReportNameIfNeeded(report, update, mockContext));
-        });
-    });
-});
diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts
deleted file mode 100644
index 36121446bf31..000000000000
--- a/tests/unit/OptimisticReportNamesTest.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-import Onyx from 'react-native-onyx';
-// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock
-import type * as CurrencyUtils from '@libs/CurrencyUtils';
-import type {UpdateContext} from '@libs/OptimisticReportNames';
-import {computeReportNameIfNeeded, getReportByTransactionID, shouldComputeReportName} from '@libs/OptimisticReportNames';
-// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock
-import * as ReportUtils from '@libs/ReportUtils';
-import CONST from '@src/CONST';
-import type {OnyxKey} from '@src/ONYXKEYS';
-import type {Policy, Report, ReportNameValuePairs, Transaction} from '@src/types/onyx';
-
-// Mock dependencies
-jest.mock('@libs/ReportUtils', () => ({
-    ...jest.requireActual<typeof ReportUtils>('@libs/ReportUtils'),
-    isExpenseReport: jest.fn(),
-    getReportTransactions: jest.fn(),
-}));
-
-jest.mock('@libs/CurrencyUtils', () => ({
-    ...jest.requireActual<typeof CurrencyUtils>('@libs/CurrencyUtils'),
-    isValidCurrencyCode: jest.fn().mockImplementation((code: string) => ['USD'].includes(code)),
-}));
-
-const mockReportUtils = ReportUtils as jest.Mocked<typeof ReportUtils>;
-
-describe('OptimisticReportNames', () => {
-    const mockPolicy = {
-        id: 'policy1',
-        fieldList: {
-            [CONST.REPORT_FIELD_TITLE_FIELD_ID]: {
-                fieldID: CONST.REPORT_FIELD_TITLE_FIELD_ID,
-                defaultValue: '{report:type} - {report:total}',
-            },
-        },
-    } as unknown as Policy;
-
-    const mockReport = {
-        reportID: '123',
-        reportName: 'Old Name',
-        policyID: 'policy1',
-        total: -10000,
-        currency: 'USD',
-        lastVisibleActionCreated: '2025-01-15T10:30:00Z',
-        type: 'expense',
-    } as Report;
-
-    const mockContext: UpdateContext = {
-        betas: [CONST.BETAS.CUSTOM_REPORT_NAMES],
-        betaConfiguration: {},
-        allReports: {
-            // eslint-disable-next-line @typescript-eslint/naming-convention
-            report_123: mockReport,
-        },
-        allPolicies: {
-            // eslint-disable-next-line @typescript-eslint/naming-convention
-            policy_policy1: mockPolicy,
-        },
-        allReportNameValuePairs: {
-            // eslint-disable-next-line @typescript-eslint/naming-convention
-            reportNameValuePairs_123: {
-                private_isArchived: '',
-                // eslint-disable-next-line @typescript-eslint/naming-convention
-                expensify_text_title: {
-                    defaultValue: '{report:type} - {report:total}',
-                },
-            } as unknown as ReportNameValuePairs,
-        },
-        allTransactions: {},
-        isOffline: false,
-    };
-
-    beforeEach(() => {
-        jest.clearAllMocks();
-        mockReportUtils.isExpenseReport.mockReturnValue(true);
-    });
-
-    describe('shouldComputeReportName()', () => {
-        test('should return true for report with title field formula', () => {
-            const result = shouldComputeReportName(mockReport, mockContext);
-            expect(result).toBe(true);
-        });
-
-        test('should return false when no title field', () => {
-            const context = {
-                ...mockContext,
-                allReportNameValuePairs: {
-                    ...mockContext.allReportNameValuePairs,
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    reportNameValuePairs_123: {
-                        private_isArchived: '',
-                    },
-                },
-                allPolicies: {},
-            };
-            const result = shouldComputeReportName(mockReport, context);
-            expect(result).toBe(false);
-        });
-
-        test('should return false for reports with unsupported type', () => {
-            mockReportUtils.isExpenseReport.mockReturnValue(false);
-
-            const result = shouldComputeReportName(
-                {
-                    ...mockReport,
-                    type: 'iou',
-                } as Report,
-                mockContext,
-            );
-            expect(result).toBe(false);
-        });
-    });
-
-    describe('computeReportNameIfNeeded()', () => {
-        test('should compute name when report data changes', () => {
-            const update = {
-                key: 'report_123' as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {total: -20000},
-            };
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            const result = computeReportNameIfNeeded(mockReport, update, mockContext);
-            expect(result?.name).toEqual('Expense Report - $200.00');
-        });
-
-        test('should return null when name would not change', () => {
-            const update = {
-                key: 'report_456' as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {description: 'Updated description'},
-            };
-
-            const result = computeReportNameIfNeeded(
-                {
-                    ...mockReport,
-                    reportName: 'Expense Report - $100.00',
-                },
-                // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-                update,
-                mockContext,
-            );
-            expect(result).toBeNull();
-        });
-    });
-
-    describe('Edge Cases', () => {
-        test('should handle missing report gracefully', () => {
-            const update = {
-                key: 'report_999' as OnyxKey,
-                onyxMethod: Onyx.METHOD.MERGE,
-                value: {total: -10000},
-            };
-
-            // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
-            const result = computeReportNameIfNeeded(undefined, update, mockContext);
-            expect(result).toBeNull();
-        });
-    });
-
-    describe('Transaction Updates', () => {
-        test('getReportByTransactionID should find report from transaction', () => {
-            const contextWithTransaction = {
-                ...mockContext,
-                allTransactions: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    transactions_abc123: {
-                        transactionID: 'abc123',
-                        reportID: '123',
-                        amount: -7500,
-                        created: '2024-01-15',
-                        currency: 'USD',
-                        merchant: 'Test Store',
-                    },
-                },
-            };
-
-            const result = getReportByTransactionID('abc123', contextWithTransaction);
-
-            expect(result).toEqual(mockReport);
-            expect(result?.reportID).toBe('123');
-        });
-
-        test('getReportByTransactionID should return undefined for missing transaction', () => {
-            const result = getReportByTransactionID('nonexistent', mockContext);
-            expect(result).toBeUndefined();
-        });
-
-        test('getReportByTransactionID should return undefined for transaction without reportID', () => {
-            const contextWithIncompleteTransaction = {
-                ...mockContext,
-                allTransactions: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    transactions_incomplete: {
-                        transactionID: 'incomplete' as OnyxKey,
-                        amount: -1000,
-                        currency: 'USD',
-                        merchant: 'Store',
-                        // Missing reportID
-                    } as unknown as Transaction,
-                },
-            };
-
-            const result = getReportByTransactionID('incomplete', contextWithIncompleteTransaction);
-            expect(result).toBeUndefined();
-        });
-    });
-});

From 1f4182d04890d0ca401f7b4a739558cfe58f5a05 Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Tue, 23 Dec 2025 17:16:43 +0700
Subject: [PATCH 04/14] fix: remove OptimisticReportNamesConnectionManager

---
 src/libs/API/index.ts                         |   6 -
 .../OptimisticReportNamesConnectionManager.ts | 149 ------------------
 2 files changed, 155 deletions(-)
 delete mode 100644 src/libs/OptimisticReportNamesConnectionManager.ts

diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index d6ddb9798332..996fa7e9479f 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -8,7 +8,6 @@ import {handleDeletedAccount, HandleUnusedOptimisticID, Logging, Pagination, Rea
 import FraudMonitoring from '@libs/Middleware/FraudMonitoring';
 import {isOffline} from '@libs/Network/NetworkStore';
 import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue';
-import {initialize as initializeOptimisticReportNamesContext} from '@libs/OptimisticReportNamesConnectionManager';
 import Pusher from '@libs/Pusher';
 import {addMiddleware, processWithMiddleware} from '@libs/Request';
 import {getAll, getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests';
@@ -50,11 +49,6 @@ addMiddleware(SaveResponseInOnyx);
 // FraudMonitoring - Tags the request with the appropriate Fraud Protection event.
 addMiddleware(FraudMonitoring);
 
-// Initialize OptimisticReportNames context on module load
-initializeOptimisticReportNamesContext().catch(() => {
-    Log.warn('Failed to initialize OptimisticReportNames context');
-});
-
 let requestIndex = 0;
 
 type OnyxData = {
diff --git a/src/libs/OptimisticReportNamesConnectionManager.ts b/src/libs/OptimisticReportNamesConnectionManager.ts
deleted file mode 100644
index eb932e0f569c..000000000000
--- a/src/libs/OptimisticReportNamesConnectionManager.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import type {OnyxEntry} from 'react-native-onyx';
-import Onyx from 'react-native-onyx';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Beta, BetaConfiguration, Policy, Report, Transaction} from '@src/types/onyx';
-import type ReportNameValuePairs from '@src/types/onyx/ReportNameValuePairs';
-
-type UpdateContext = {
-    betas: OnyxEntry<Beta[]>;
-    betaConfiguration: OnyxEntry<BetaConfiguration>;
-    allReports: Record<string, Report>;
-    allPolicies: Record<string, Policy>;
-    allReportNameValuePairs: Record<string, ReportNameValuePairs>;
-    allTransactions: Record<string, Transaction>;
-    isOffline: boolean;
-};
-
-let betas: OnyxEntry<Beta[]>;
-let betaConfiguration: OnyxEntry<BetaConfiguration>;
-let allReports: Record<string, Report>;
-let allPolicies: Record<string, Policy>;
-let allReportNameValuePairs: Record<string, ReportNameValuePairs>;
-let allTransactions: Record<string, Transaction>;
-let isOffline: OnyxEntry<boolean>;
-let isInitialized = false;
-let connectionsInitializedCount = 0;
-const totalConnections = 7;
-let initializationPromise: Promise<void> | null = null;
-
-/**
- * Initialize persistent connections to Onyx data needed for OptimisticReportNames
- * This is called lazily when OptimisticReportNames functionality is first used
- * Returns a Promise that resolves when all connections have received their initial data
- *
- * We use Onyx.connectWithoutView because we do not use this in React components and this logic is not tied directly to the UI.
- * This is a centralized system that needs access to all objects of several types, so that when any updates affect
- * the computed report names, we can compute the new names according to the formula and add the necessary updates.
- * It wouldn't be possible to do this without connecting to all the data.
- *
- */
-function initialize(): Promise<void> {
-    if (isInitialized) {
-        return Promise.resolve();
-    }
-
-    if (initializationPromise) {
-        return initializationPromise;
-    }
-
-    initializationPromise = new Promise<void>((resolve) => {
-        const incrementInitialization = () => {
-            connectionsInitializedCount++;
-            if (connectionsInitializedCount === totalConnections) {
-                isInitialized = true;
-                resolve();
-            }
-        };
-
-        // Connect to BETAS
-        Onyx.connectWithoutView({
-            key: ONYXKEYS.BETAS,
-            callback: (val) => {
-                betas = val;
-                incrementInitialization();
-            },
-        });
-
-        // Connect to BETA_CONFIGURATION
-        Onyx.connectWithoutView({
-            key: ONYXKEYS.BETA_CONFIGURATION,
-            callback: (val) => {
-                betaConfiguration = val;
-                incrementInitialization();
-            },
-        });
-
-        // Connect to all REPORTS
-        Onyx.connectWithoutView({
-            key: ONYXKEYS.COLLECTION.REPORT,
-            waitForCollectionCallback: true,
-            callback: (val) => {
-                allReports = (val as Record<string, Report>) ?? {};
-                incrementInitialization();
-            },
-        });
-
-        // Connect to all POLICIES
-        Onyx.connectWithoutView({
-            key: ONYXKEYS.COLLECTION.POLICY,
-            waitForCollectionCallback: true,
-            callback: (val) => {
-                allPolicies = (val as Record<string, Policy>) ?? {};
-                incrementInitialization();
-            },
-        });
-
-        // Connect to all REPORT_NAME_VALUE_PAIRS
-        Onyx.connectWithoutView({
-            key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
-            waitForCollectionCallback: true,
-            callback: (val) => {
-                allReportNameValuePairs = (val as Record<string, ReportNameValuePairs>) ?? {};
-                incrementInitialization();
-            },
-        });
-
-        // Connect to all TRANSACTIONS
-        Onyx.connectWithoutView({
-            key: ONYXKEYS.COLLECTION.TRANSACTION,
-            waitForCollectionCallback: true,
-            callback: (val) => {
-                allTransactions = (val as Record<string, Transaction>) ?? {};
-                incrementInitialization();
-            },
-        });
-
-        // Connect to NETWORK to track offline status
-        Onyx.connectWithoutView({
-            key: ONYXKEYS.NETWORK,
-            callback: (val) => {
-                isOffline = val?.isOffline;
-                incrementInitialization();
-            },
-        });
-    });
-
-    return initializationPromise;
-}
-
-/**
- * Get the current update context synchronously
- * Must be called after initialize() has completed
- */
-function getUpdateContext(): UpdateContext {
-    if (!isInitialized) {
-        throw new Error('OptimisticReportNamesConnectionManager not initialized. Call initialize() first.');
-    }
-
-    return {
-        betas,
-        betaConfiguration,
-        allReports: allReports ?? {},
-        allPolicies: allPolicies ?? {},
-        allReportNameValuePairs: allReportNameValuePairs ?? {},
-        allTransactions: allTransactions ?? {},
-        isOffline: isOffline ?? false,
-    };
-}
-export {initialize, getUpdateContext};
-export type {UpdateContext};

From b6b6ec4a20cfc6c4e5103aa5c5c0dd35b3214e07 Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Tue, 23 Dec 2025 17:20:05 +0700
Subject: [PATCH 05/14] fix: remove unsed CONST

---
 src/CONST/index.ts | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 9780d4cb607a..7948f77f8123 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -1658,8 +1658,6 @@ const CONST = {
         APPLY_AIRSHIP_UPDATES: 'apply_airship_updates',
         APPLY_PUSHER_UPDATES: 'apply_pusher_updates',
         APPLY_HTTPS_UPDATES: 'apply_https_updates',
-        COMPUTE_REPORT_NAME: 'compute_report_name',
-        COMPUTE_REPORT_NAME_FOR_NEW_REPORT: 'compute_report_name_for_new_report',
         COLD: 'cold',
         WARM: 'warm',
         REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500,

From 246a08b51ff4db0a8ba93b94a4bd8e96ba16a4db Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Tue, 23 Dec 2025 17:45:03 +0700
Subject: [PATCH 06/14] remove more functions

---
 src/libs/Formula.ts       |   93 +--
 tests/unit/FormulaTest.ts | 1337 +------------------------------------
 2 files changed, 3 insertions(+), 1427 deletions(-)

diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts
index 1859abd2ded3..448b2fe04dcb 100644
--- a/src/libs/Formula.ts
+++ b/src/libs/Formula.ts
@@ -198,40 +198,6 @@ function parsePart(definition: string): FormulaPart {
     return part;
 }
 
-/**
- * Check if a formula requires backend computation (e.g., currency conversion with exchange rates)
- * This is used by OptimisticReportNames to skip optimistic updates when online and backend is needed
- */
-function requiresBackendComputation(parts: FormulaPart[], context?: FormulaContext): boolean {
-    if (!context) {
-        return false;
-    }
-
-    const {report} = context;
-
-    for (const part of parts) {
-        if (part.type === FORMULA_PART_TYPES.REPORT) {
-            const [field, ...additionalPath] = part.fieldPath;
-            // Reconstruct format string by joining additional path elements with ':'
-            // This handles format strings with colons like 'HH:mm:ss'
-            const format = additionalPath.length > 0 ? additionalPath.join(':') : undefined;
-            const fieldName = field?.toLowerCase();
-
-            if (fieldName === 'total' || fieldName === 'reimbursable') {
-                // Use formatAmount to check whether a currency conversion is needed.
-                // A null return means the backend must handle the conversion.
-                // We rely on report.total because zero values can be computed optimistically.
-                const result = formatAmount(report.total, report.currency, format);
-                if (result === null) {
-                    return true;
-                }
-            }
-        }
-    }
-
-    return false;
-}
-
 /**
  * Check if the report field formula value is containing circular references, e.g example:  A -> A,  A->B->A,  A->B->C->A, etc
  */
@@ -294,61 +260,6 @@ function hasCircularReferences(fieldValue: string, fieldName: string, fieldList?
     return hasCircularReferencesRecursive(fieldValue, fieldName);
 }
 
-/**
- * Check if a formula part is a submission info part (report:submit:*)
- */
-function isSubmissionInfoPart(part: FormulaPart): boolean {
-    return part.type === FORMULA_PART_TYPES.REPORT && part.fieldPath.at(0)?.toLowerCase() === 'submit';
-}
-
-/**
- * Compute the value of a formula given a context
- */
-function compute(formula?: string, context?: FormulaContext): string {
-    if (!formula || typeof formula !== 'string') {
-        return '';
-    }
-    if (!context) {
-        return '';
-    }
-
-    const parts = parse(formula);
-    let result = '';
-
-    for (const part of parts) {
-        let value = '';
-
-        switch (part.type) {
-            case FORMULA_PART_TYPES.REPORT:
-                value = computeReportPart(part, context);
-                // Apply fallback to formula definition for empty values, except for submission info
-                // Submission info explicitly returns empty strings when data is missing (matches backend)
-                if (value === '' && !isSubmissionInfoPart(part)) {
-                    value = part.definition;
-                }
-                break;
-            case FORMULA_PART_TYPES.FIELD:
-                value = computeFieldPart(part);
-                break;
-            case FORMULA_PART_TYPES.USER:
-                value = computeUserPart(part);
-                break;
-            case FORMULA_PART_TYPES.FREETEXT:
-                value = part.definition;
-                break;
-            default:
-                // If we don't recognize the part type, use the original definition
-                value = part.definition;
-        }
-
-        // Apply any functions to the computed value
-        value = applyFunctions(value, part.functions);
-        result += value;
-    }
-
-    return result;
-}
-
 /**
  * Compute auto-reporting info for a report formula part
  */
@@ -942,6 +853,6 @@ function computePersonalDetailsField(path: string[], personalDetails: PersonalDe
     }
 }
 
-export {FORMULA_PART_TYPES, compute, extract, getAutoReportingDates, parse, hasCircularReferences, requiresBackendComputation};
+export {FORMULA_PART_TYPES, parse, hasCircularReferences};
 
-export type {FormulaContext, FormulaPart, FieldList};
+export type {FieldList};
diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts
index d4dda90765b6..e2f270d819ec 100644
--- a/tests/unit/FormulaTest.ts
+++ b/tests/unit/FormulaTest.ts
@@ -1,13 +1,8 @@
 // eslint-disable-next-line no-restricted-syntax -- disabled because we need CurrencyUtils to mock
 import * as CurrencyUtils from '@libs/CurrencyUtils';
-import type {FormulaContext} from '@libs/Formula';
-import {compute, extract, hasCircularReferences, parse, requiresBackendComputation} from '@libs/Formula';
-// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportActionsUtils to mock
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+import {hasCircularReferences, parse} from '@libs/Formula';
 // eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock
 import * as ReportUtils from '@libs/ReportUtils';
-import CONST from '@src/CONST';
-import type {PersonalDetails, Policy, Report, ReportActions, Transaction} from '@src/types/onyx';
 
 jest.mock('@libs/ReportActionsUtils', () => ({
     getAllReportActions: jest.fn(),
@@ -23,33 +18,7 @@ jest.mock('@libs/CurrencyUtils', () => ({
     isValidCurrencyCode: jest.fn(),
 }));
 
-const mockReportActionsUtils = ReportActionsUtils as jest.Mocked<typeof ReportActionsUtils>;
-const mockReportUtils = ReportUtils as jest.Mocked<typeof ReportUtils>;
-const mockCurrencyUtils = CurrencyUtils as jest.Mocked<typeof CurrencyUtils>;
-
 describe('CustomFormula', () => {
-    describe('extract()', () => {
-        test('should extract formula parts with default braces', () => {
-            expect(extract('{report:type} - {report:total}')).toEqual(['{report:type}', '{report:total}']);
-        });
-
-        test('should handle nested braces', () => {
-            expect(extract('{report:{report:submit:from:firstName|substr:2}}')).toEqual(['{report:{report:submit:from:firstName|substr:2}}']);
-        });
-
-        test('should handle escaped braces', () => {
-            expect(extract('\\{not-formula} {report:type}')).toEqual(['{report:type}']);
-        });
-
-        test('should handle empty formula', () => {
-            expect(extract('')).toEqual([]);
-        });
-
-        test('should handle formula without braces', () => {
-            expect(extract('no braces here')).toEqual([]);
-        });
-    });
-
     describe('parse()', () => {
         test('should parse report formula parts', () => {
             const parts = parse('{report:type} {report:startdate}');
@@ -99,1315 +68,11 @@ describe('CustomFormula', () => {
         });
     });
 
-    describe('compute()', () => {
-        const mockContext: FormulaContext = {
-            report: {
-                reportID: '123',
-                reportName: '',
-                type: 'expense',
-                total: -10000, // -$100.00
-                currency: 'USD',
-                lastVisibleActionCreated: '2025-01-15T10:30:00Z',
-                policyID: 'policy1',
-            } as Report,
-            policy: {
-                name: 'Test Policy',
-            } as Policy,
-        };
-
-        beforeEach(() => {
-            jest.clearAllMocks();
-
-            mockCurrencyUtils.isValidCurrencyCode.mockImplementation((code: string) => ['USD', 'EUR', 'JPY', 'NPR'].includes(code));
-
-            const mockReportActions = {
-                // eslint-disable-next-line @typescript-eslint/naming-convention
-                '1': {
-                    reportActionID: '1',
-                    created: '2025-01-10T08:00:00Z', // Oldest action
-                    actionName: 'CREATED',
-                },
-                // eslint-disable-next-line @typescript-eslint/naming-convention
-                '2': {
-                    reportActionID: '2',
-                    created: '2025-01-15T10:30:00Z', // Later action
-                    actionName: 'IOU',
-                },
-                // eslint-disable-next-line @typescript-eslint/naming-convention
-                '3': {
-                    reportActionID: '3',
-                    created: '2025-01-12T14:20:00Z', // Middle action
-                    actionName: 'COMMENT',
-                },
-            } as unknown as ReportActions;
-
-            const mockTransactions = [
-                {
-                    transactionID: 'trans1',
-                    created: '2025-01-08T12:00:00Z', // Oldest transaction
-                    amount: 5000,
-                    merchant: 'ACME Ltd.',
-                },
-                {
-                    transactionID: 'trans2',
-                    created: '2025-01-14T16:45:00Z', // Later transaction
-                    amount: 3000,
-                    merchant: 'ACME Ltd.',
-                },
-                {
-                    transactionID: 'trans3',
-                    created: '2025-01-11T09:15:00Z', // Middle transaction
-                    amount: 2000,
-                    merchant: 'ACME Ltd.',
-                },
-            ] as Transaction[];
-
-            mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions);
-            mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
-        });
-
-        test('should compute basic report formula', () => {
-            const result = compute('{report:type} {report:total}', mockContext);
-            expect(result).toBe('Expense Report $100.00'); // No space between parts
-        });
-
-        test('should compute startdate formula using transactions', () => {
-            const result = compute('{report:startdate}', mockContext);
-            expect(result).toBe('2025-01-08'); // Should use oldest transaction date (2025-01-08)
-        });
-
-        test('should compute enddate formula using transactions', () => {
-            const result = compute('{report:enddate}', mockContext);
-            expect(result).toBe('2025-01-14'); // Should use newest transaction date (2025-01-14)
-        });
-
-        test('should compute created formula using report actions', () => {
-            const result = compute('{report:created}', mockContext);
-            expect(result).toBe('2025-01-10'); // Should use oldest report action date (2025-01-10)
-        });
-
-        test('should compute startdate with custom format', () => {
-            const result = compute('{report:startdate:MM/dd/yyyy}', mockContext);
-            expect(result).toBe('01/08/2025'); // Should use oldest transaction date with yyyy-MM-dd format
-        });
-
-        test('should compute enddate with custom format', () => {
-            const result = compute('{report:enddate:MM/dd/yyyy}', mockContext);
-            expect(result).toBe('01/14/2025'); // Should use newest transaction date with MM/dd/yyyy format
-        });
-
-        test('should compute created with custom format', () => {
-            const result = compute('{report:created:MMMM dd, yyyy}', mockContext);
-            expect(result).toBe('January 10, 2025'); // Should use oldest report action date with MMMM dd, yyyy format
-        });
-
-        test('should compute startdate with short month format', () => {
-            const result = compute('{report:startdate:dd MMM yyyy}', mockContext);
-            expect(result).toBe('08 Jan 2025'); // Should use oldest transaction date with dd MMM yyyy format
-        });
-
-        test('should compute enddate with short month format', () => {
-            const result = compute('{report:enddate:dd MMM yyyy}', mockContext);
-            expect(result).toBe('14 Jan 2025'); // Should use newest transaction date with dd MMM yyyy format
-        });
-
-        test('should compute policy name', () => {
-            const result = compute('{report:policyname}', mockContext);
-            expect(result).toBe('Test Policy');
-        });
-
-        test('should compute report ID in base62 format', () => {
-            const result = compute('{report:id}', mockContext);
-            expect(result).toBe('R0000000001z');
-        });
-
-        test('should compute report status', () => {
-            const contextWithStatus: FormulaContext = {
-                ...mockContext,
-                report: {
-                    ...mockContext.report,
-                    statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
-                },
-            };
-            const result = compute('{report:status}', contextWithStatus);
-            expect(result).toBe('Submitted');
-        });
-
-        test('should compute expenses count', () => {
-            const result = compute('{report:expensescount}', mockContext);
-            expect(result).toBe('0');
-        });
-
-        test('should compute expenses count using allTransactions from context', () => {
-            const allTransactions = {
-                // eslint-disable-next-line @typescript-eslint/naming-convention
-                transactions_trans1: {
-                    transactionID: 'trans1',
-                    reportID: '123',
-                    created: '2025-01-08T12:00:00Z',
-                    amount: 5000,
-                    merchant: 'ACME Ltd.',
-                } as Transaction,
-                // eslint-disable-next-line @typescript-eslint/naming-convention
-                transactions_trans2: {
-                    transactionID: 'trans2',
-                    reportID: '123',
-                    created: '2025-01-14T16:45:00Z',
-                    amount: 3000,
-                    merchant: 'ACME Ltd.',
-                } as Transaction,
-                // eslint-disable-next-line @typescript-eslint/naming-convention
-                transactions_trans3: {
-                    transactionID: 'trans3',
-                    reportID: '123',
-                    created: '2025-01-11T09:15:00Z',
-                    amount: 2000,
-                    merchant: 'ACME Ltd.',
-                } as Transaction,
-            };
-
-            const contextWithAllTransactions: FormulaContext = {
-                ...mockContext,
-                allTransactions,
-            };
-
-            const result = compute('{report:expensescount}', contextWithAllTransactions);
-            expect(result).toBe('3');
-            // Verify that getReportTransactions was NOT called when allTransactions is provided
-            expect(mockReportUtils.getReportTransactions).not.toHaveBeenCalled();
-        });
-
-        test('should handle empty formula', () => {
-            expect(compute('', mockContext)).toBe('');
-        });
-
-        test('should handle unknown formula parts', () => {
-            const result = compute('{report:unknown}', mockContext);
-            expect(result).toBe('{report:unknown}');
-        });
-
-        test('should handle missing report data gracefully', () => {
-            const contextWithMissingData: FormulaContext = {
-                report: {} as unknown as Report,
-                policy: null as unknown as Policy,
-            };
-            const result = compute('{report:total} {report:policyname}', contextWithMissingData);
-            expect(result).toBe('{report:total} {report:policyname}'); // Empty data is replaced with definition
-        });
-
-        test('should preserve free text', () => {
-            const result = compute('Expense Report - {report:total}', mockContext);
-            expect(result).toBe('Expense Report - $100.00');
-        });
-
-        test('should preserve exact spacing around formula parts', () => {
-            const result = compute('Report with type after 4 spaces   {report:type}-and no space after computed part', mockContext);
-            expect(result).toBe('Report with type after 4 spaces   Expense Report-and no space after computed part');
-        });
-
-        test('should compute complex formula with multiple new parts', () => {
-            const contextWithStatus: FormulaContext = {
-                ...mockContext,
-                report: {
-                    ...mockContext.report,
-                    transactionCount: 3,
-                    statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
-                },
-            };
-            const result = compute('Report {report:id} has {report:expensescount} expenses and is {report:status}', contextWithStatus);
-            expect(result).toBe('Report R0000000001z has 3 expenses and is Approved');
-        });
-
-        test('should handle combination of new and existing formula parts', () => {
-            const contextWithStatus: FormulaContext = {
-                ...mockContext,
-                report: {
-                    ...mockContext.report,
-                    statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
-                    transactionCount: 3,
-                },
-            };
-            const result = compute('{report:type} {report:id} - {report:status} - Total: {report:total} ({report:expensescount} expenses)', contextWithStatus);
-            expect(result).toBe('Expense Report R0000000001z - Submitted - Total: $100.00 (3 expenses)');
-        });
-
-        test('should handle different status numbers', () => {
-            const testCases = [
-                {statusNum: CONST.REPORT.STATUS_NUM.OPEN, expected: 'Open'},
-                {statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, expected: 'Submitted'},
-                {statusNum: CONST.REPORT.STATUS_NUM.CLOSED, expected: 'Closed'},
-                {statusNum: CONST.REPORT.STATUS_NUM.APPROVED, expected: 'Approved'},
-                {statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, expected: 'Reimbursed'},
-            ];
-
-            for (const {statusNum, expected} of testCases) {
-                const contextWithStatus: FormulaContext = {
-                    ...mockContext,
-                    report: {
-                        ...mockContext.report,
-                        statusNum,
-                    },
-                };
-                const result = compute('{report:status}', contextWithStatus);
-                expect(result).toBe(expected);
-            }
-        });
-
-        test('should handle undefined status number', () => {
-            const contextWithUndefinedStatus: FormulaContext = {
-                ...mockContext,
-                report: {
-                    ...mockContext.report,
-                    statusNum: undefined,
-                },
-            };
-            const result = compute('{report:status}', contextWithUndefinedStatus);
-            expect(result).toBe('{report:status}');
-        });
-
-        test('should return 0 for expensescount when no transactions exist', () => {
-            mockReportUtils.getReportTransactions.mockReturnValue([]);
-            const result = compute('{report:expensescount}', mockContext);
-            expect(result).toBe('0');
-        });
-
-        test('should return 0 for expensescount when reportID is empty', () => {
-            const contextWithEmptyReportID: FormulaContext = {
-                ...mockContext,
-                report: {
-                    ...mockContext.report,
-                    reportID: '',
-                },
-            };
-            const result = compute('{report:expensescount}', contextWithEmptyReportID);
-            expect(result).toBe('0');
-        });
-
-        test('should compute report ID with different reportID values', () => {
-            const contextWithDifferentID: FormulaContext = {
-                ...mockContext,
-                report: {
-                    ...mockContext.report,
-                    reportID: '456789',
-                },
-            };
-            const result = compute('{report:id}', contextWithDifferentID);
-            expect(result).toBe('R00000001upZ');
-        });
-    });
-
-    describe('Reimbursable Amount', () => {
-        const reimbursableContext: FormulaContext = {
-            report: {
-                reportID: '123',
-                reportName: '',
-                type: 'expense',
-                policyID: 'policy1',
-            },
-            policy: {
-                name: 'Test Policy',
-            } as Policy,
-        };
-
-        const calculateExpectedReimbursable = (total: number, nonReimbursableTotal: number) => {
-            const reimbursableAmount = total - nonReimbursableTotal;
-            return Math.abs(reimbursableAmount) / 100;
-        };
-
-        beforeEach(() => {
-            jest.clearAllMocks();
-        });
-
-        test('should compute reimbursable amount', () => {
-            reimbursableContext.report.currency = 'USD';
-            reimbursableContext.report.total = -10000; // -$100.00
-            reimbursableContext.report.nonReimbursableTotal = -2500; // -$25.00
-
-            const expectedReimbursable = calculateExpectedReimbursable(reimbursableContext.report.total, reimbursableContext.report.nonReimbursableTotal);
-            const result = compute('{report:reimbursable}', reimbursableContext);
-            expect(result).toBe(`$${expectedReimbursable.toFixed(2)}`);
-        });
-
-        test('should compute reimbursable amount with different currency', () => {
-            reimbursableContext.report.currency = 'EUR';
-            reimbursableContext.report.total = -8000; // -€80.00
-            reimbursableContext.report.nonReimbursableTotal = -3000; // -€30.00
-
-            const expectedReimbursable = calculateExpectedReimbursable(reimbursableContext.report.total, reimbursableContext.report.nonReimbursableTotal);
-            const result = compute('{report:reimbursable}', reimbursableContext);
-
-            expect(result).toBe(`€${expectedReimbursable.toFixed(2)}`);
-        });
-
-        test('should handle zero reimbursable amount', () => {
-            reimbursableContext.report.currency = 'USD';
-            reimbursableContext.report.total = -10000; // -$100.00
-            reimbursableContext.report.nonReimbursableTotal = -10000; // -$100.00 (all non-reimbursable)
-
-            const expectedReimbursable = calculateExpectedReimbursable(reimbursableContext.report.total, reimbursableContext.report.nonReimbursableTotal);
-            const result = compute('{report:reimbursable}', reimbursableContext);
-            expect(result).toBe(`$${expectedReimbursable.toFixed(2)}`);
-        });
-
-        test('should handle undefined reimbursable amount', () => {
-            reimbursableContext.report.currency = 'USD';
-            reimbursableContext.report.total = undefined;
-            reimbursableContext.report.nonReimbursableTotal = undefined;
-
-            const result = compute('{report:reimbursable}', reimbursableContext);
-            expect(result).toBe('$0.00');
-        });
-
-        test('should handle missing currency gracefully', () => {
-            reimbursableContext.report.currency = undefined;
-            reimbursableContext.report.total = -10000; // -100.00
-            reimbursableContext.report.nonReimbursableTotal = -2500; // -25.00
-
-            const expectedReimbursable = calculateExpectedReimbursable(reimbursableContext.report.total, reimbursableContext.report.nonReimbursableTotal);
-            const result = compute('{report:reimbursable}', reimbursableContext);
-            expect(result).toBe(`${expectedReimbursable.toFixed(2)}`);
-        });
-
-        describe('Currency Formatting & Conversion', () => {
-            const currencyContext: FormulaContext = {
-                report: {
-                    reportID: '123',
-                    total: -10000,
-                    currency: 'USD',
-                } as Report,
-                policy: {} as Policy,
-            };
-
-            beforeEach(() => {
-                jest.clearAllMocks();
-            });
-
-            describe('Format options', () => {
-                test('nosymbol - should format without currency symbol', () => {
-                    const result = compute('{report:total:nosymbol}', currencyContext);
-                    expect(result).toBe('100.00');
-
-                    // Should not require backend computation
-                    const parts = parse('{report:total:nosymbol}');
-                    expect(requiresBackendComputation(parts, currencyContext)).toBe(false);
-                });
-
-                test('same currency - should format normally (case insensitive)', () => {
-                    currencyContext.report.currency = 'EUR';
-                    expect(compute('{report:total:EUR}', currencyContext)).toBe('€100.00');
-                    expect(compute('{report:total:eur}', currencyContext)).toBe('€100.00');
-
-                    // Should not require backend computation when currencies match
-                    const parts = parse('{report:total:EUR}');
-                    expect(requiresBackendComputation(parts, currencyContext)).toBe(false);
-                });
-
-                test('default (no format) - should use report currency', () => {
-                    currencyContext.report.currency = 'NPR';
-                    const result = compute('{report:total}', currencyContext);
-                    expect(result).toBe('NPR\u00A0100.00');
-
-                    // Should not require backend computation
-                    const parts = parse('{report:total}');
-                    expect(requiresBackendComputation(parts, currencyContext)).toBe(false);
-                });
-            });
-
-            describe('Currency conversion (requires backend)', () => {
-                test('different valid currencies - should return placeholder', () => {
-                    currencyContext.report.currency = 'USD';
-
-                    // Various currencies requiring conversion
-                    expect(compute('{report:total:EUR}', currencyContext)).toBe('{report:total:EUR}');
-                    expect(compute('{report:total:JPY}', currencyContext)).toBe('{report:total:JPY}');
-
-                    // Should require backend computation
-                    const parts = parse('{report:total:EUR}');
-                    expect(requiresBackendComputation(parts, currencyContext)).toBe(true);
-                });
-
-                test('case and whitespace handling - should normalize and detect conversion', () => {
-                    currencyContext.report.currency = 'USD';
-
-                    // Mixed case and whitespace
-                    expect(compute('{report:total:EuR}', currencyContext)).toBe('{report:total:EuR}');
-                    expect(compute('{report:total: EUR }', currencyContext)).toBe('{report:total: EUR }');
-                    expect(compute('{report:total:eur }', currencyContext)).toBe('{report:total:eur }');
-                });
-
-                test('in complex formulas - should detect conversion need', () => {
-                    currencyContext.report.currency = 'USD';
-
-                    // Multiple parts where one needs conversion
-                    const parts = parse('Report: {report:type} Total: {report:total:EUR}');
-                    expect(requiresBackendComputation(parts, currencyContext)).toBe(true);
-
-                    // Non-total fields don't need backend
-                    const simpleParts = parse('{report:type} {report:policyname}');
-                    expect(requiresBackendComputation(simpleParts, currencyContext)).toBe(false);
-                });
-            });
-
-            describe('Edge cases', () => {
-                test('undefined currency - should format without symbol', () => {
-                    currencyContext.report.currency = undefined;
-                    const result = compute('{report:total}', currencyContext);
-                    expect(result).toBe('100.00');
-                });
-
-                test('invalid source currency - should return placeholder', () => {
-                    currencyContext.report.currency = 'UNKNOWN';
-                    const result = compute('{report:total}', currencyContext);
-                    expect(result).toBe('{report:total}');
-                });
-
-                test('invalid format currency - should return placeholder', () => {
-                    currencyContext.report.currency = 'EUR';
-                    const result = compute('{report:total:UNKNOWN}', currencyContext);
-                    expect(result).toBe('{report:total:UNKNOWN}');
-                });
-            });
-        });
-    });
-
-    describe('Function Modifiers', () => {
-        const mockContext: FormulaContext = {
-            report: {
-                reportID: 'report123456789',
-                reportName: '',
-                type: 'expense',
-                total: -10000, // -$100.00
-                currency: 'USD',
-                lastVisibleActionCreated: '2025-01-15T10:30:00Z',
-                policyID: 'policy1',
-            } as Report,
-            policy: {
-                name: 'Engineering Department Rules',
-            } as Policy,
-        };
-
-        beforeEach(() => {
-            jest.clearAllMocks();
-        });
-
-        describe('frontpart modifier', () => {
-            test('should extract first word from non-email text', () => {
-                const result = compute('{report:policyname|frontpart}', mockContext);
-                expect(result).toBe('Engineering'); // First word of "Engineering Department Rules"
-            });
-
-            test('should handle empty strings', () => {
-                const contextWithEmpty: FormulaContext = {
-                    report: {} as Report,
-                    policy: {name: ''} as Policy,
-                };
-                const result = compute('{report:policyname|frontpart}', contextWithEmpty);
-                expect(result).toBe('{report:policyname|frontpart}'); // Falls back to formula definition
-            });
-        });
-
-        describe('domain modifier', () => {
-            test('should extract domain from email', () => {
-                const result = compute('{report:submit:from:email|domain}', mockContext);
-                // Submit part extraction not implemented yet; for now, it returns the definition
-                // Once implemented, this should return 'domain' of the email
-                expect(result).toBe('');
-            });
-
-            test('should return empty for non-email text', () => {
-                const result = compute('{report:policyname|domain}', mockContext);
-                expect(result).toBe(''); // "Engineering Department Rules" has no @ symbol
-            });
-
-            test('should handle empty strings', () => {
-                const contextWithEmpty: FormulaContext = {
-                    report: {} as Report,
-                    policy: {name: ''} as Policy,
-                };
-                const result = compute('{report:policyname|domain}', contextWithEmpty);
-                expect(result).toBe(''); // Empty policy name
-            });
-        });
-
-        describe('substr modifier', () => {
-            test('should extract substring with start and length', () => {
-                const result = compute('{report:policyname|substr:0:11}', mockContext);
-                expect(result).toBe('Engineering'); // First 11 characters of "Engineering Department Rules"
-            });
-
-            test('should extract substring with only start position', () => {
-                const result = compute('{report:policyname|substr:12}', mockContext);
-                expect(result).toBe('Department Rules'); // From position 12 to end
-            });
-
-            test('should handle start position beyond string length', () => {
-                const result = compute('{report:policyname|substr:50:10}', mockContext);
-                expect(result).toBe(''); // Start position 50 is beyond string length
-            });
-
-            test('should handle length larger than remaining string', () => {
-                const result = compute('{report:policyname|substr:23:50}', mockContext);
-                expect(result).toBe('Rules'); // Only remaining characters
-            });
-
-            test('should handle invalid length parameter', () => {
-                const result = compute('{report:policyname|substr:0:abc}', mockContext);
-                expect(result).toBe(''); // Invalid length, returns empty
-            });
-        });
-    });
-
-    describe('Auto-reporting Frequency', () => {
-        const mockReport = {reportID: '123'} as Report;
-        const createMockContext = (policy: Policy): FormulaContext => ({report: mockReport, policy});
-
-        beforeEach(() => {
-            jest.clearAllMocks();
-            jest.useFakeTimers();
-            jest.setSystemTime(new Date('2025-01-19T14:23:45Z'));
-        });
-
-        afterEach(() => {
-            jest.useRealTimers();
-        });
-
-        test('should compute weekly frequency dates', () => {
-            const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY} as Policy;
-            const context = createMockContext(policy);
-
-            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-13');
-            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-19');
-        });
-
-        test('should compute semi-monthly frequency dates', () => {
-            jest.setSystemTime(new Date('2025-01-10T12:00:00Z'));
-            const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY} as Policy;
-            const context = createMockContext(policy);
-
-            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-01');
-            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-15');
-
-            jest.setSystemTime(new Date('2025-01-20T12:00:00Z'));
-            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-16');
-            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-31');
-        });
-
-        test('should compute monthly frequency with specific offset', () => {
-            const policy = {
-                autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY,
-                autoReportingOffset: 25,
-            } as Policy;
-            const context = createMockContext(policy);
-
-            expect(compute('{report:autoreporting:start}', context)).toBe('2024-12-26');
-            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-25');
-        });
-
-        test('should compute monthly frequency with last business day', () => {
-            const policy = {
-                autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY,
-                autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH,
-            } as Policy;
-            const context = createMockContext(policy);
-
-            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-01');
-            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-31');
-        });
-
-        test('should compute trip frequency dates', () => {
-            const mockTransactions = [
-                {transactionID: 'trans1', created: '2025-01-08T12:00:00Z', merchant: 'Hotel', amount: 5000} as Transaction,
-                {transactionID: 'trans2', created: '2025-01-14T16:45:00Z', merchant: 'Restaurant', amount: 3000} as Transaction,
-            ];
-
-            mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
-
-            const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP} as Policy;
-            const context = createMockContext(policy);
-
-            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-08');
-            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-19');
-        });
-
-        test('should apply custom date formats', () => {
-            const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY} as Policy;
-            const context = createMockContext(policy);
-
-            expect(compute('{report:autoreporting:start:MMMM dd, yyyy}', context)).toBe('January 13, 2025');
-            expect(compute('{report:autoreporting:end:MM/dd/yyyy}', context)).toBe('01/19/2025');
-        });
-
-        test('should return formula definition when policy or frequency is missing', () => {
-            expect(compute('{report:autoreporting:start}', {report: mockReport, policy: undefined})).toBe('{report:autoreporting:start}');
-            expect(compute('{report:autoreporting:end}', createMockContext({} as Policy))).toBe('{report:autoreporting:end}');
-        });
-    });
-
     describe('Edge Cases', () => {
         test('should handle malformed braces', () => {
             const parts = parse('{incomplete');
             expect(parts.at(0)?.type).toBe('freetext');
         });
-
-        test('should handle undefined amounts', () => {
-            const context: FormulaContext = {
-                report: {total: undefined} as Report,
-                policy: null as unknown as Policy,
-            };
-            const result = compute('{report:total}', context);
-            expect(result).toBe('{report:total}');
-        });
-
-        test('should handle missing report actions for created', () => {
-            mockReportActionsUtils.getAllReportActions.mockReturnValue({});
-            const context: FormulaContext = {
-                report: {reportID: '123'} as Report,
-                policy: null as unknown as Policy,
-            };
-
-            const result = compute('{report:created}', context);
-            expect(result).toBe('{report:created}');
-        });
-
-        test('should handle missing transactions for startdate', () => {
-            mockReportUtils.getReportTransactions.mockReturnValue([]);
-            const context: FormulaContext = {
-                report: {reportID: '123'} as Report,
-                policy: null as unknown as Policy,
-            };
-            const today = new Date();
-            const expected = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
-            const result = compute('{report:startdate}', context);
-            expect(result).toBe(expected);
-        });
-
-        test('should handle missing transactions for enddate', () => {
-            mockReportUtils.getReportTransactions.mockReturnValue([]);
-            const context: FormulaContext = {
-                report: {reportID: '123'} as Report,
-                policy: null as unknown as Policy,
-            };
-            const today = new Date();
-            const expected = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
-            const result = compute('{report:enddate}', context);
-            expect(result).toBe(expected);
-        });
-
-        test('should call getReportTransactions with correct reportID for startdate', () => {
-            const context: FormulaContext = {
-                report: {reportID: 'test-report-123'} as Report,
-                policy: null as unknown as Policy,
-            };
-
-            compute('{report:startdate}', context);
-            expect(mockReportUtils.getReportTransactions).toHaveBeenCalledWith('test-report-123');
-        });
-
-        test('should call getAllReportActions with correct reportID for created', () => {
-            const context: FormulaContext = {
-                report: {reportID: 'test-report-456'} as Report,
-                policy: null as unknown as Policy,
-            };
-
-            compute('{report:created}', context);
-            expect(mockReportActionsUtils.getAllReportActions).toHaveBeenCalledWith('test-report-456');
-        });
-
-        test('should skip partial transactions (empty merchant)', () => {
-            const mockTransactions = [
-                {
-                    transactionID: 'trans1',
-                    created: '2025-01-15T12:00:00Z',
-                    amount: 5000,
-                    merchant: 'ACME Ltd.',
-                },
-                {
-                    transactionID: 'trans2',
-                    created: '2025-01-08T16:45:00Z', // Older but partial
-                    amount: 3000,
-                    merchant: '', // Empty merchant = partial
-                },
-                {
-                    transactionID: 'trans3',
-                    created: '2025-01-12T09:15:00Z', // Should be oldest valid
-                    amount: 2000,
-                    merchant: 'Gamma Inc.',
-                },
-            ] as Transaction[];
-
-            mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
-            const context: FormulaContext = {
-                report: {reportID: 'test-report-123'} as Report,
-                policy: null as unknown as Policy,
-            };
-
-            const result = compute('{report:startdate}', context);
-            expect(result).toBe('2025-01-12');
-
-            const endResult = compute('{report:enddate}', context);
-            expect(endResult).toBe('2025-01-15');
-        });
-
-        test('should skip partial transactions (zero amount)', () => {
-            const mockTransactions = [
-                {
-                    transactionID: 'trans1',
-                    created: '2025-01-15T12:00:00Z',
-                    amount: 5000,
-                    merchant: 'ACME Ltd.',
-                },
-                {
-                    transactionID: 'trans2',
-                    created: '2025-01-08T16:45:00Z', // Older but partial
-                    amount: 0, // Zero amount = partial
-                    merchant: 'Beta Corp.',
-                    iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN,
-                },
-                {
-                    transactionID: 'trans3',
-                    created: '2025-01-12T09:15:00Z', // Should be oldest valid
-                    amount: 2000,
-                    merchant: 'Gamma Inc.',
-                },
-            ] as Transaction[];
-
-            mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
-            const context: FormulaContext = {
-                report: {reportID: 'test-report-123'} as Report,
-                policy: null as unknown as Policy,
-            };
-
-            const result = compute('{report:startdate}', context);
-            expect(result).toBe('2025-01-12');
-
-            const endResult = compute('{report:enddate}', context);
-            expect(endResult).toBe('2025-01-15');
-        });
-    });
-
-    describe('Date Format Tokens', () => {
-        // Test date: Wednesday, January 8, 2025, 3:30:45 PM (15:30:45) UTC
-        const testDate = '2025-01-08T15:30:45.123Z';
-        const morningDate = '2025-01-08T09:05:02.123Z'; // 9:05:02 AM for leading zero tests
-
-        const mockContextWithDate: FormulaContext = {
-            report: {reportID: '123'} as Report,
-            policy: null as unknown as Policy,
-        };
-
-        const setupMockDate = (date: string) => {
-            const mockTransaction = {
-                transactionID: 'trans1',
-                created: date,
-                amount: -5000,
-                merchant: 'Test Store',
-            } as Transaction;
-            mockReportUtils.getReportTransactions.mockReturnValue([mockTransaction]);
-
-            const mockReportAction = {
-                created: date,
-                actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
-            } as unknown as ReportActions[string];
-            mockReportActionsUtils.getAllReportActions.mockReturnValue({action1: mockReportAction});
-        };
-
-        beforeEach(() => setupMockDate(testDate));
-
-        test('year formats - yyyy/yy/Y/y/YYYY', () => {
-            expect(compute('{report:startdate:yyyy}', mockContextWithDate)).toBe('2025');
-            expect(compute('{report:startdate:YYYY}', mockContextWithDate)).toBe('2025');
-            expect(compute('{report:startdate:Y}', mockContextWithDate)).toBe('2025');
-            expect(compute('{report:startdate:yy}', mockContextWithDate)).toBe('25');
-            expect(compute('{report:startdate:y}', mockContextWithDate)).toBe('25');
-        });
-
-        test('month formats - names and numbers', () => {
-            expect(compute('{report:startdate:MMMM}', mockContextWithDate)).toBe('January');
-            expect(compute('{report:startdate:F}', mockContextWithDate)).toBe('January');
-            expect(compute('{report:startdate:MMM}', mockContextWithDate)).toBe('Jan');
-            expect(compute('{report:startdate:M}', mockContextWithDate)).toBe('Jan');
-            expect(compute('{report:startdate:MM}', mockContextWithDate)).toBe('01');
-            expect(compute('{report:startdate:n}', mockContextWithDate)).toBe('1');
-            expect(compute('{report:startdate:t}', mockContextWithDate)).toBe('31');
-        });
-
-        test('day formats - numbers and names', () => {
-            expect(compute('{report:startdate:dd}', mockContextWithDate)).toBe('08');
-            expect(compute('{report:startdate:d}', mockContextWithDate)).toBe('08');
-            expect(compute('{report:startdate:j}', mockContextWithDate)).toBe('8');
-            expect(compute('{report:startdate:S}', mockContextWithDate)).toBe('th');
-            expect(compute('{report:startdate:jS}', mockContextWithDate)).toBe('8th');
-            expect(compute('{report:startdate:dddd}', mockContextWithDate)).toBe('Wednesday');
-            expect(compute('{report:startdate:l}', mockContextWithDate)).toBe('Wednesday');
-            expect(compute('{report:startdate:ddd}', mockContextWithDate)).toBe('Wed');
-            expect(compute('{report:startdate:D}', mockContextWithDate)).toBe('Wed');
-            expect(compute('{report:startdate:w}', mockContextWithDate)).toBe('3');
-            expect(compute('{report:startdate:N}', mockContextWithDate)).toBe('3');
-            expect(compute('{report:startdate:z}', mockContextWithDate)).toBe('7');
-            expect(compute('{report:startdate:W}', mockContextWithDate)).toBe('02');
-        });
-
-        test('ISO week number - first Thursday rule', () => {
-            // January 1, 2021 is a Friday
-            // Week 1 of 2021 contains the first Thursday (Jan 7)
-            // So Jan 1-3 (Fri, Sat, Sun) belong to week 53 of 2020
-            setupMockDate('2021-01-01T12:00:00Z'); // Friday, Jan 1, 2021
-            expect(compute('{report:startdate:W}', mockContextWithDate)).toBe('53');
-
-            // January 4 is Monday, which is in week 1
-            setupMockDate('2021-01-04T12:00:00Z'); // Monday, Jan 4, 2021
-            expect(compute('{report:startdate:W}', mockContextWithDate)).toBe('01');
-
-            // December 31, 2020 is Thursday, should be week 53
-            setupMockDate('2020-12-31T12:00:00Z'); // Thursday, Dec 31, 2020
-            expect(compute('{report:startdate:W}', mockContextWithDate)).toBe('53');
-        });
-
-        test('complex date formats', () => {
-            expect(compute('{report:startdate:MMMM dd, yyyy}', mockContextWithDate)).toBe('January 08, 2025');
-            expect(compute('{report:startdate:dd MMM yyyy}', mockContextWithDate)).toBe('08 Jan 2025');
-            expect(compute('{report:startdate:yyyy-MM-dd}', mockContextWithDate)).toBe('2025-01-08');
-        });
-
-        test('time formats - hours', () => {
-            // 24-hour format: 15:30:45 (afternoon) and 09:05:02 (morning with leading zero)
-            expect(compute('{report:startdate:HH}', mockContextWithDate)).toBe('15');
-            expect(compute('{report:startdate:H}', mockContextWithDate)).toBe('15');
-            expect(compute('{report:startdate:G}', mockContextWithDate)).toBe('15');
-
-            setupMockDate(morningDate);
-            expect(compute('{report:startdate:HH}', mockContextWithDate)).toBe('09');
-            expect(compute('{report:startdate:H}', mockContextWithDate)).toBe('09'); // H has leading zeros per spec
-            expect(compute('{report:startdate:G}', mockContextWithDate)).toBe('9'); // G has NO leading zeros
-
-            // 12-hour format: 3:30 PM (afternoon) and 9:05 AM (morning)
-            setupMockDate(testDate);
-            expect(compute('{report:startdate:hh}', mockContextWithDate)).toBe('03');
-            expect(compute('{report:startdate:h}', mockContextWithDate)).toBe('03'); // h has leading zeros per spec
-            expect(compute('{report:startdate:g}', mockContextWithDate)).toBe('3'); // g has NO leading zeros
-
-            setupMockDate(morningDate);
-            expect(compute('{report:startdate:hh}', mockContextWithDate)).toBe('09');
-            expect(compute('{report:startdate:h}', mockContextWithDate)).toBe('09'); // h has leading zeros per spec
-            expect(compute('{report:startdate:g}', mockContextWithDate)).toBe('9'); // g has NO leading zeros
-        });
-
-        test('time formats - minutes and seconds', () => {
-            // Minutes: 30 (double digit) and 05 (single digit with leading zero)
-            setupMockDate(testDate);
-            expect(compute('{report:startdate:mm}', mockContextWithDate)).toBe('30');
-            expect(compute('{report:startdate:i}', mockContextWithDate)).toBe('30');
-
-            setupMockDate(morningDate);
-            expect(compute('{report:startdate:mm}', mockContextWithDate)).toBe('05');
-            expect(compute('{report:startdate:i}', mockContextWithDate)).toBe('05');
-
-            // Seconds: 45 (double digit) and 02 (single digit with leading zero)
-            setupMockDate(testDate);
-            expect(compute('{report:startdate:ss}', mockContextWithDate)).toBe('45');
-            expect(compute('{report:startdate:s}', mockContextWithDate)).toBe('45');
-
-            setupMockDate(morningDate);
-            expect(compute('{report:startdate:ss}', mockContextWithDate)).toBe('02');
-            expect(compute('{report:startdate:s}', mockContextWithDate)).toBe('02');
-        });
-
-        test('time formats - AM/PM', () => {
-            expect(compute('{report:startdate:tt}', mockContextWithDate)).toBe('PM');
-            expect(compute('{report:startdate:A}', mockContextWithDate)).toBe('PM');
-            expect(compute('{report:startdate:a}', mockContextWithDate)).toBe('pm');
-        });
-
-        test('full date/time formats - c, r, U', () => {
-            // ISO 8601 format (c token)
-            const cResult = compute('{report:startdate:c}', mockContextWithDate);
-            expect(cResult).toBe('2025-01-08T15:30:45.123Z');
-
-            // RFC 2822 format (r token)
-            const rResult = compute('{report:startdate:r}', mockContextWithDate);
-            expect(rResult).toMatch(/^Wed, 08 Jan 2025 \d{2}:\d{2}:\d{2} [+-]\d{4}$/);
-
-            // Unix timestamp (U token)
-            const uResult = compute('{report:startdate:U}', mockContextWithDate);
-            const expectedTimestamp = Math.floor(new Date(testDate).getTime() / 1000).toString();
-            expect(uResult).toBe(expectedTimestamp);
-        });
-
-        test('format strings with colons', () => {
-            expect(compute('{report:startdate:HH:mm}', mockContextWithDate)).toBe('15:30');
-            expect(compute('{report:startdate:HH:mm:ss}', mockContextWithDate)).toBe('15:30:45');
-            expect(compute('{report:startdate:hh:mm tt}', mockContextWithDate)).toBe('03:30 PM');
-            expect(compute('{report:startdate:yyyy-MM-dd HH:mm:ss}', mockContextWithDate)).toBe('2025-01-08 15:30:45');
-            expect(compute('{report:created:HH:mm:ss}', mockContextWithDate)).toBe('15:30:45');
-            expect(compute('{report:startdate:g:i a}', mockContextWithDate)).toBe('3:30 pm');
-        });
-    });
-
-    describe('Submission Info', () => {
-        const mockSubmitter: PersonalDetails = {
-            accountID: 12345,
-            firstName: 'John',
-            lastName: 'Doe',
-            displayName: 'John Doe',
-            login: 'john.doe@company.com',
-        };
-
-        const mockManager: PersonalDetails = {
-            accountID: 67890,
-            firstName: 'Jane',
-            lastName: 'Smith',
-            displayName: 'Jane Smith',
-            login: 'jane.smith@company.com',
-        };
-
-        const mockContextWithSubmissionInfo: FormulaContext = {
-            report: {
-                reportID: '123',
-                reportName: '',
-                type: 'expense',
-                ownerAccountID: 12345,
-                managerID: 67890,
-                stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
-                statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
-                created: '2025-01-15T10:30:00Z',
-            } as Report,
-            policy: {
-                name: 'Test Policy',
-                employeeList: {
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    'john.doe@company.com': {
-                        email: 'john.doe@company.com',
-                        employeeUserID: 'EMP001',
-                        employeePayrollID: 'PAY123',
-                    },
-                    // eslint-disable-next-line @typescript-eslint/naming-convention
-                    'jane.smith@company.com': {
-                        email: 'jane.smith@company.com',
-                        employeeUserID: 'EMP002',
-                        employeePayrollID: 'PAY456',
-                    },
-                },
-            } as unknown as Policy,
-            submitterPersonalDetails: mockSubmitter,
-            managerPersonalDetails: mockManager,
-        };
-
-        beforeEach(() => {
-            jest.clearAllMocks();
-
-            const mockReportActions = {
-                // eslint-disable-next-line @typescript-eslint/naming-convention
-                '1': {
-                    reportActionID: '1',
-                    created: '2025-01-10T08:00:00Z',
-                    actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
-                },
-                // eslint-disable-next-line @typescript-eslint/naming-convention
-                '2': {
-                    reportActionID: '2',
-                    created: '2025-01-15T10:30:00Z',
-                    actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED,
-                },
-            } as unknown as ReportActions;
-
-            mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions);
-        });
-
-        describe('Submitter information', () => {
-            test('firstname - basic submitter first name', () => {
-                expect(compute('{report:submit:from:firstname}', mockContextWithSubmissionInfo)).toBe('John');
-            });
-
-            test('lastname - basic submitter last name', () => {
-                expect(compute('{report:submit:from:lastname}', mockContextWithSubmissionInfo)).toBe('Doe');
-            });
-
-            test('fullname - basic submitter full name', () => {
-                expect(compute('{report:submit:from:fullname}', mockContextWithSubmissionInfo)).toBe('John Doe');
-            });
-
-            test('email - basic submitter email', () => {
-                expect(compute('{report:submit:from:email}', mockContextWithSubmissionInfo)).toBe('john.doe@company.com');
-            });
-
-            test('userid - submitter employee user ID (alias for customfield1)', () => {
-                expect(compute('{report:submit:from:userid}', mockContextWithSubmissionInfo)).toBe('EMP001');
-            });
-
-            test('customfield1 - submitter employee user ID from policy', () => {
-                expect(compute('{report:submit:from:customfield1}', mockContextWithSubmissionInfo)).toBe('EMP001');
-            });
-
-            test('customfield2 - submitter employee payroll ID from policy', () => {
-                expect(compute('{report:submit:from:customfield2}', mockContextWithSubmissionInfo)).toBe('PAY123');
-            });
-
-            test('payrollid - submitter employee payroll ID (alias for customfield2)', () => {
-                expect(compute('{report:submit:from:payrollid}', mockContextWithSubmissionInfo)).toBe('PAY123');
-            });
-
-            test('name fields fall back to email when name missing', () => {
-                const contextWithPartialDetails: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: null as unknown as Policy,
-                    submitterPersonalDetails: {
-                        accountID: 111,
-                        login: 'fallback@email.com',
-                    } as PersonalDetails,
-                };
-
-                expect(compute('{report:submit:from:firstname}', contextWithPartialDetails)).toBe('fallback@email.com');
-                expect(compute('{report:submit:from:lastname}', contextWithPartialDetails)).toBe('fallback@email.com');
-                expect(compute('{report:submit:from:fullname}', contextWithPartialDetails)).toBe('fallback@email.com');
-            });
-
-            test('customfield1 - return empty when employeeList missing', () => {
-                const contextWithoutEmployeeList: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: {
-                        name: 'Test Policy',
-                    } as Policy,
-                    submitterPersonalDetails: mockSubmitter,
-                };
-
-                expect(compute('{report:submit:from:customfield1}', contextWithoutEmployeeList)).toBe('');
-            });
-
-            test('customfield2 - return empty when employeeList missing', () => {
-                const contextWithoutEmployeeList: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: {
-                        name: 'Test Policy',
-                    } as Policy,
-                    submitterPersonalDetails: mockSubmitter,
-                };
-
-                expect(compute('{report:submit:from:customfield2}', contextWithoutEmployeeList)).toBe('');
-            });
-
-            test('customfield1 - return empty when user not in employeeList', () => {
-                const contextWithDifferentEmployee: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: {
-                        name: 'Test Policy',
-                        employeeList: {
-                            // eslint-disable-next-line @typescript-eslint/naming-convention
-                            'other.user@company.com': {
-                                email: 'other.user@company.com',
-                                employeeUserID: 'EMP999',
-                            },
-                        },
-                    } as unknown as Policy,
-                    submitterPersonalDetails: mockSubmitter,
-                };
-
-                expect(compute('{report:submit:from:customfield1}', contextWithDifferentEmployee)).toBe('');
-            });
-        });
-
-        describe('Manager information', () => {
-            test('firstname - basic manager first name', () => {
-                expect(compute('{report:submit:to:firstname}', mockContextWithSubmissionInfo)).toBe('Jane');
-            });
-
-            test('lastname - basic manager last name', () => {
-                expect(compute('{report:submit:to:lastname}', mockContextWithSubmissionInfo)).toBe('Smith');
-            });
-
-            test('fullname - basic manager full name', () => {
-                expect(compute('{report:submit:to:fullname}', mockContextWithSubmissionInfo)).toBe('Jane Smith');
-            });
-
-            test('email - basic manager email', () => {
-                expect(compute('{report:submit:to:email}', mockContextWithSubmissionInfo)).toBe('jane.smith@company.com');
-            });
-
-            test('userid - manager employee user ID (alias for customfield1)', () => {
-                expect(compute('{report:submit:to:userid}', mockContextWithSubmissionInfo)).toBe('EMP002');
-            });
-
-            test('customfield1 - manager employee user ID from policy', () => {
-                expect(compute('{report:submit:to:customfield1}', mockContextWithSubmissionInfo)).toBe('EMP002');
-            });
-
-            test('customfield2 - manager employee payroll ID from policy', () => {
-                expect(compute('{report:submit:to:customfield2}', mockContextWithSubmissionInfo)).toBe('PAY456');
-            });
-
-            test('payrollid - manager employee payroll ID (alias for customfield2)', () => {
-                expect(compute('{report:submit:to:payrollid}', mockContextWithSubmissionInfo)).toBe('PAY456');
-            });
-
-            test('firstname - fall back to email when manager name missing', () => {
-                const contextWithPartialManagerDetails: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: null as unknown as Policy,
-                    managerPersonalDetails: {
-                        accountID: 222,
-                        login: 'manager@email.com',
-                    } as PersonalDetails,
-                };
-
-                expect(compute('{report:submit:to:firstname}', contextWithPartialManagerDetails)).toBe('manager@email.com');
-            });
-
-            test('fullname - fall back to email when manager displayName missing', () => {
-                const contextWithPartialManagerDetails: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: null as unknown as Policy,
-                    managerPersonalDetails: {
-                        accountID: 222,
-                        login: 'manager@email.com',
-                    } as PersonalDetails,
-                };
-
-                expect(compute('{report:submit:to:fullname}', contextWithPartialManagerDetails)).toBe('manager@email.com');
-            });
-        });
-
-        describe('Submission date', () => {
-            test('default format - yyyy-MM-dd', () => {
-                expect(compute('{report:submit:date}', mockContextWithSubmissionInfo)).toBe('2025-01-15');
-            });
-
-            test('custom format - verifies date formatting works', () => {
-                expect(compute('{report:submit:date:MMMM dd, yyyy}', mockContextWithSubmissionInfo)).toBe('January 15, 2025');
-            });
-        });
-
-        describe('Function modifiers', () => {
-            test('frontpart - extract username from submitter email', () => {
-                expect(compute('{report:submit:from:email|frontpart}', mockContextWithSubmissionInfo)).toBe('john.doe');
-            });
-
-            test('domain - extract domain from submitter email', () => {
-                expect(compute('{report:submit:from:email|domain}', mockContextWithSubmissionInfo)).toBe('company.com');
-            });
-
-            test('frontpart - extract username from manager email', () => {
-                expect(compute('{report:submit:to:email|frontpart}', mockContextWithSubmissionInfo)).toBe('jane.smith');
-            });
-
-            test('substr - extract first 4 characters from fullname', () => {
-                expect(compute('{report:submit:from:fullname|substr:0:4}', mockContextWithSubmissionInfo)).toBe('John');
-            });
-
-            test('chained modifiers - frontpart then substr on email', () => {
-                expect(compute('{report:submit:from:email|frontpart|substr:0:4}', mockContextWithSubmissionInfo)).toBe('john');
-            });
-        });
-
-        describe('Combined formulas', () => {
-            test('submitter and manager names together', () => {
-                expect(compute('{report:submit:from:firstname} -> {report:submit:to:firstname}', mockContextWithSubmissionInfo)).toBe('John -> Jane');
-            });
-
-            test('transaction date range with submission date', () => {
-                const mockTransactions = [
-                    {
-                        transactionID: 'trans1',
-                        created: '2025-01-08T12:00:00Z',
-                        amount: 5000,
-                        merchant: 'Store',
-                    },
-                ] as Transaction[];
-
-                mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
-
-                expect(compute('{report:startdate} to {report:submit:date}', mockContextWithSubmissionInfo)).toBe('2025-01-08 to 2025-01-15');
-            });
-        });
-
-        describe('Edge cases', () => {
-            test('empty email - return empty when email empty', () => {
-                const contextWithEmptyEmail: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: null as unknown as Policy,
-                    submitterPersonalDetails: {
-                        accountID: 123,
-                        login: '',
-                    } as PersonalDetails,
-                };
-
-                expect(compute('{report:submit:from:email}', contextWithEmptyEmail)).toBe('');
-            });
-
-            test('empty email with name - return empty when name also empty', () => {
-                const contextWithEmptyEmail: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: null as unknown as Policy,
-                    submitterPersonalDetails: {
-                        accountID: 123,
-                        login: '',
-                    } as PersonalDetails,
-                };
-
-                expect(compute('{report:submit:from:firstname}', contextWithEmptyEmail)).toBe('');
-            });
-
-            test('empty firstname - fallback to email when firstname is empty string', () => {
-                const contextWithEmptyFirstName: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: null as unknown as Policy,
-                    submitterPersonalDetails: {
-                        accountID: 123,
-                        firstName: '',
-                        login: 'user@test.com',
-                    } as PersonalDetails,
-                };
-
-                expect(compute('{report:submit:from:firstname}', contextWithEmptyFirstName)).toBe('user@test.com');
-            });
-
-            test('empty lastname - fallback to email when lastname is empty string', () => {
-                const contextWithEmptyLastName: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: null as unknown as Policy,
-                    submitterPersonalDetails: {
-                        accountID: 123,
-                        lastName: '',
-                        login: 'user@test.com',
-                    } as PersonalDetails,
-                };
-
-                expect(compute('{report:submit:from:lastname}', contextWithEmptyLastName)).toBe('user@test.com');
-            });
-
-            test('empty displayName - fallback to email when displayName is empty string', () => {
-                const contextWithEmptyDisplayName: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: null as unknown as Policy,
-                    submitterPersonalDetails: {
-                        accountID: 123,
-                        displayName: '',
-                        login: 'user@test.com',
-                    } as PersonalDetails,
-                };
-
-                expect(compute('{report:submit:from:fullname}', contextWithEmptyDisplayName)).toBe('user@test.com');
-            });
-
-            test('empty email with frontpart - return empty for empty email modifier', () => {
-                const contextWithEmptyEmail: FormulaContext = {
-                    report: {reportID: '123'} as Report,
-                    policy: null as unknown as Policy,
-                    submitterPersonalDetails: {
-                        accountID: 123,
-                        login: '',
-                    } as PersonalDetails,
-                };
-
-                expect(compute('{report:submit:from:email|frontpart}', contextWithEmptyEmail)).toBe('');
-            });
-
-            test('unknown field - return empty for invalid field name', () => {
-                expect(compute('{report:submit:from:unknown}', mockContextWithSubmissionInfo)).toBe('');
-            });
-
-            test('invalid direction - return empty for invalid from/to', () => {
-                expect(compute('{report:submit:invalid:email}', mockContextWithSubmissionInfo)).toBe('');
-            });
-        });
     });
 
     describe('hasCircularReferences()', () => {

From ceb52b552e34f0f4da4d3f89de958e110362c6a1 Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Tue, 23 Dec 2025 18:11:33 +0700
Subject: [PATCH 07/14] fix: cleanup fomular file

---
 src/libs/Formula.ts | 613 --------------------------------------------
 1 file changed, 613 deletions(-)

diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts
index 448b2fe04dcb..b54dac751a82 100644
--- a/src/libs/Formula.ts
+++ b/src/libs/Formula.ts
@@ -1,16 +1,5 @@
-import {endOfDay, endOfMonth, endOfWeek, getDay, lastDayOfMonth, set, startOfMonth, startOfWeek, subDays} from 'date-fns';
-import type {OnyxEntry} from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
-import CONST from '@src/CONST';
-import type {PersonalDetails, Policy, Report, Transaction} from '@src/types/onyx';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import {convertToDisplayString, convertToDisplayStringWithoutCurrency, isValidCurrencyCode} from './CurrencyUtils';
-import {formatDate} from './FormulaDatetime';
-import getBase62ReportID from './getBase62ReportID';
-import Log from './Log';
-import {getAllReportActions} from './ReportActionsUtils';
-import {getHumanReadableStatus, getMoneyRequestSpendBreakdown, getReportTransactions} from './ReportUtils';
-import {getCreated, isPartialTransaction, isTransactionPendingDelete} from './TransactionUtils';
 
 type FormulaPart = {
     /** The original definition from the formula */
@@ -26,15 +15,6 @@ type FormulaPart = {
     functions: string[];
 };
 
-type FormulaContext = {
-    report: Report;
-    policy: OnyxEntry<Policy>;
-    transaction?: Transaction;
-    submitterPersonalDetails?: PersonalDetails;
-    managerPersonalDetails?: PersonalDetails;
-    allTransactions?: Record<string, Transaction>;
-};
-
 type FieldList = Record<string, {name: string; defaultValue: string}>;
 
 const FORMULA_PART_TYPES = {
@@ -260,599 +240,6 @@ function hasCircularReferences(fieldValue: string, fieldName: string, fieldList?
     return hasCircularReferencesRecursive(fieldValue, fieldName);
 }
 
-/**
- * Compute auto-reporting info for a report formula part
- */
-function computeAutoReportingInfo(part: FormulaPart, context: FormulaContext, subField: string | undefined, format: string | undefined): string {
-    const {report, policy} = context;
-
-    if (!subField) {
-        return part.definition;
-    }
-
-    const {startDate, endDate} = getAutoReportingDates(policy, report);
-
-    switch (subField.toLowerCase()) {
-        case 'start':
-            return formatDate(startDate?.toISOString(), format);
-        case 'end':
-            return formatDate(endDate?.toISOString(), format);
-        default:
-            return part.definition;
-    }
-}
-
-/**
- * Compute the value of a report formula part
- */
-function computeReportPart(part: FormulaPart, context: FormulaContext): string {
-    const {report, policy, allTransactions} = context;
-    const [field, ...additionalPath] = part.fieldPath;
-    // Reconstruct format string by joining additional path elements with ':'
-    // This handles format strings with colons like 'HH:mm:ss'
-    const format = additionalPath.length > 0 ? additionalPath.join(':') : undefined;
-
-    if (!field) {
-        return part.definition;
-    }
-
-    switch (field.toLowerCase()) {
-        case 'id':
-            return getBase62ReportID(Number(report.reportID));
-        case 'status':
-            return formatStatus(report.statusNum);
-        case 'expensescount':
-            return String(getExpensesCount(report, allTransactions));
-        case 'type':
-            return formatType(report.type);
-        case 'startdate':
-            return formatDate(getOldestTransactionDate(report.reportID, context), format);
-        case 'enddate':
-            return formatDate(getNewestTransactionDate(report.reportID, context), format);
-        case 'total': {
-            const formattedAmount = formatAmount(report.total, report.currency, format);
-            // Return empty string when conversion needed (formatAmount returns null for unavailable conversions)
-            return formattedAmount ?? '';
-        }
-        case 'reimbursable': {
-            const formattedAmount = formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, report.currency, format);
-            return formattedAmount ?? '';
-        }
-        case 'currency':
-            return report.currency ?? '';
-        case 'policyname':
-        case 'workspacename':
-            return policy?.name ?? '';
-        case 'created':
-            // Backend will always return at least one report action (of type created) and its date is equal to report's creation date
-            // We can make it slightly more efficient in the future by ensuring report.created is always present in backend's responses
-            return formatDate(getOldestReportActionDate(report.reportID), format);
-        case 'submit': {
-            return computeSubmitPart(additionalPath, context);
-        }
-        case 'autoreporting': {
-            const subField = additionalPath.at(0);
-            // For multi-part formulas, format is everything after the subfield
-            const autoReportingFormat = additionalPath.length > 1 ? additionalPath.slice(1).join(':') : undefined;
-            return computeAutoReportingInfo(part, context, subField, autoReportingFormat);
-        }
-        default:
-            return part.definition;
-    }
-}
-
-/**
- * Get the number of expenses in a report
- * @param report - The report to get expenses for
- * @param allTransactions - Optional map of all transactions. If provided, uses this instead of fetching from Onyx
- */
-function getExpensesCount(report: Report, allTransactions?: Record<string, Transaction>): number {
-    if (!report.reportID) {
-        return 0;
-    }
-
-    if (allTransactions) {
-        const transactions = Object.values(allTransactions).filter((transaction): transaction is Transaction => !!transaction && transaction.reportID === report.reportID);
-        return transactions?.filter((transaction) => !isTransactionPendingDelete(transaction))?.length ?? 0;
-    }
-
-    return report.transactionCount ?? 0;
-}
-
-/**
- * Format a report status number to human-readable string
- */
-function formatStatus(statusNum: number | undefined): string {
-    if (statusNum === undefined) {
-        return '';
-    }
-
-    return getHumanReadableStatus(statusNum);
-}
-
-/**
- * Compute the value of a field formula part
- */
-function computeFieldPart(part: FormulaPart): string {
-    // Field computation will be implemented later
-    return part.definition;
-}
-
-/**
- * Compute the value of a user formula part
- */
-function computeUserPart(part: FormulaPart): string {
-    // User computation will be implemented later
-    return part.definition;
-}
-
-/**
- * Apply functions to a computed value
- */
-function applyFunctions(value: string, functions: string[]): string {
-    let result = value;
-
-    for (const func of functions) {
-        const [functionName, ...args] = func.split(':');
-
-        switch (functionName.toLowerCase()) {
-            case 'frontpart':
-                result = getFrontPart(result);
-                break;
-            case 'substr':
-                result = getSubstring(result, args);
-                break;
-            case 'domain':
-                result = getDomainName(result);
-                break;
-            default:
-                // Unknown function, leave value as is
-                break;
-        }
-    }
-
-    return result;
-}
-
-/**
- * Get the front part of an email or first word of a string
- */
-function getFrontPart(value: string): string {
-    const trimmed = value.trim();
-
-    // If it's an email, return the part before @
-    if (trimmed.includes('@')) {
-        return trimmed.split('@').at(0) ?? '';
-    }
-
-    // Otherwise, return the first word
-    return trimmed.split(' ').at(0) ?? '';
-}
-
-/**
- * Get the domain name of an email or URL
- */
-function getDomainName(value: string): string {
-    const trimmed = value.trim();
-
-    // If it's an email, return the part after @
-    if (trimmed.includes('@')) {
-        return trimmed.split('@').at(1) ?? '';
-    }
-
-    return '';
-}
-
-/**
- * Get substring of a value
- */
-function getSubstring(value: string, args: string[]): string {
-    const start = parseInt(args.at(0) ?? '', 10) || 0;
-    const length = args.at(1) ? parseInt(args.at(1) ?? '', 10) : undefined;
-
-    if (length !== undefined) {
-        return value.substring(start, start + length);
-    }
-
-    return value.substring(start);
-}
-
-/**
- * Format an amount value
- * @returns The formatted amount string, or null if currency conversion is needed (unavailable on frontend)
- */
-function formatAmount(amount: number | undefined, currency: string | undefined, displayCurrency?: string): string | null {
-    if (amount === undefined) {
-        return '';
-    }
-
-    const absoluteAmount = Math.abs(amount);
-
-    try {
-        const trimmedDisplayCurrency = displayCurrency?.trim().toUpperCase();
-        if (trimmedDisplayCurrency) {
-            if (trimmedDisplayCurrency === 'NOSYMBOL') {
-                return convertToDisplayStringWithoutCurrency(absoluteAmount, currency);
-            }
-
-            // Check if format is a valid currency code (e.g., USD, EUR, eur)
-            if (!isValidCurrencyCode(trimmedDisplayCurrency)) {
-                return '';
-            }
-
-            // If a currency conversion is needed (displayCurrency differs from the source),
-            // return null so the backend can compute it.
-            // We can only compute the value optimistically when the amount is 0.
-            if (absoluteAmount !== 0 && currency !== trimmedDisplayCurrency) {
-                return null;
-            }
-
-            return convertToDisplayString(absoluteAmount, trimmedDisplayCurrency);
-        }
-
-        if (currency && isValidCurrencyCode(currency)) {
-            return convertToDisplayString(absoluteAmount, currency);
-        }
-
-        return convertToDisplayStringWithoutCurrency(absoluteAmount, currency);
-    } catch (error) {
-        Log.hmmm('[Formula] formatAmount failed', {error, amount, currency, displayCurrency});
-        return '';
-    }
-}
-
-/**
- * Get the date of the oldest report action for a given report
- */
-function getOldestReportActionDate(reportID: string): string | undefined {
-    if (!reportID) {
-        return undefined;
-    }
-
-    const reportActions = getAllReportActions(reportID);
-    if (!reportActions || Object.keys(reportActions).length === 0) {
-        return undefined;
-    }
-
-    let oldestDate: string | undefined;
-
-    for (const action of Object.values(reportActions)) {
-        if (!action?.created) {
-            continue;
-        }
-
-        if (oldestDate && action.created > oldestDate) {
-            continue;
-        }
-        oldestDate = action.created;
-    }
-
-    return oldestDate;
-}
-
-/**
- * Format a report type to its human-readable string
- */
-function formatType(type: string | undefined): string {
-    if (!type) {
-        return '';
-    }
-
-    const typeMapping: Record<string, string> = {
-        [CONST.REPORT.TYPE.EXPENSE]: 'Expense Report',
-        [CONST.REPORT.TYPE.INVOICE]: 'Invoice',
-        [CONST.REPORT.TYPE.CHAT]: 'Chat',
-        [CONST.REPORT.UNSUPPORTED_TYPE.BILL]: 'Bill',
-        [CONST.REPORT.UNSUPPORTED_TYPE.PAYCHECK]: 'Paycheck',
-        [CONST.REPORT.TYPE.IOU]: 'IOU',
-        [CONST.REPORT.TYPE.TASK]: 'Task',
-        trip: 'Trip',
-    };
-
-    return typeMapping[type.toLowerCase()] || type;
-}
-
-/**
- * Get all transactions for a report, including any context transaction.
- * Updates an existing transaction if it matches the context or adds it if new.
- */
-function getAllReportTransactionsWithContext(reportID: string, context?: FormulaContext): Transaction[] {
-    const transactions = [...getReportTransactions(reportID)];
-    const contextTransaction = context?.transaction;
-
-    if (contextTransaction?.transactionID && contextTransaction.reportID === reportID) {
-        const transactionIndex = transactions.findIndex((transaction) => transaction?.transactionID === contextTransaction.transactionID);
-        if (transactionIndex >= 0) {
-            transactions[transactionIndex] = contextTransaction;
-        } else {
-            transactions.push(contextTransaction);
-        }
-    }
-
-    return transactions;
-}
-
-/**
- * Get the date of the oldest transaction for a given report
- */
-function getOldestTransactionDate(reportID: string, context?: FormulaContext): string | undefined {
-    if (!reportID) {
-        return undefined;
-    }
-
-    const transactions = getAllReportTransactionsWithContext(reportID, context);
-    if (!transactions || transactions.length === 0) {
-        return new Date().toISOString();
-    }
-
-    let oldestDate: string | undefined;
-
-    for (const transaction of transactions) {
-        const created = getCreated(transaction);
-        if (!created) {
-            continue;
-        }
-        // Skip transactions with pending deletion (offline deletes) to calculate dates properly.
-        if (transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
-            continue;
-        }
-        if (oldestDate && created >= oldestDate) {
-            continue;
-        }
-        if (isPartialTransaction(transaction)) {
-            continue;
-        }
-        oldestDate = created;
-    }
-
-    return oldestDate;
-}
-
-/**
- * Calculate monthly reporting period for a specific day offset
- */
-function getMonthlyReportingPeriod(currentDate: Date, offsetDay: number): {startDate: Date; endDate: Date} {
-    const currentDay = currentDate.getDate();
-    const currentYear = currentDate.getFullYear();
-    const currentMonth = currentDate.getMonth();
-
-    if (currentDay <= offsetDay) {
-        // We haven't reached the reporting day yet - period is from last month's offset+1 to this month's offset
-        const prevMonth = currentMonth - 1;
-        const prevYear = prevMonth < 0 ? currentYear - 1 : currentYear;
-        const adjustedPrevMonth = prevMonth < 0 ? 11 : prevMonth;
-
-        const prevMonthDays = lastDayOfMonth(new Date(prevYear, adjustedPrevMonth, 1)).getDate();
-        const prevOffsetDay = Math.min(offsetDay, prevMonthDays);
-
-        const currentMonthDays = lastDayOfMonth(currentDate).getDate();
-        const currentOffsetDay = Math.min(offsetDay, currentMonthDays);
-
-        return {
-            startDate: new Date(prevYear, adjustedPrevMonth, prevOffsetDay + 1, 0, 0, 0, 0),
-            endDate: new Date(currentYear, currentMonth, currentOffsetDay, 23, 59, 59, 999),
-        };
-    }
-
-    // We've passed the reporting day - period is from this month's offset+1 to next month's offset
-    const nextMonth = currentMonth + 1;
-    const nextYear = nextMonth > 11 ? currentYear + 1 : currentYear;
-    const adjustedNextMonth = nextMonth > 11 ? 0 : nextMonth;
-
-    const currentMonthDays = lastDayOfMonth(currentDate).getDate();
-    const currentOffsetDay = Math.min(offsetDay, currentMonthDays);
-
-    const nextMonthDays = lastDayOfMonth(new Date(nextYear, adjustedNextMonth, 1)).getDate();
-    const nextOffsetDay = Math.min(offsetDay, nextMonthDays);
-
-    return {
-        startDate: new Date(currentYear, currentMonth, currentOffsetDay + 1, 0, 0, 0, 0),
-        endDate: new Date(nextYear, adjustedNextMonth, nextOffsetDay, 23, 59, 59, 999),
-    };
-}
-
-/**
- * Calculate monthly reporting period for last business day
- */
-function getMonthlyLastBusinessDayPeriod(currentDate: Date): {startDate: Date; endDate: Date} {
-    let endDate = endOfMonth(currentDate);
-
-    // Move backward to find last business day (Mon-Fri)
-    while (getDay(endDate) === 0 || getDay(endDate) === 6) {
-        endDate = subDays(endDate, 1);
-    }
-
-    return {
-        startDate: startOfMonth(currentDate),
-        endDate: endOfDay(endDate),
-    };
-}
-
-/**
- * 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} {
-    const frequency = policy?.autoReportingFrequency;
-    const offset = policy?.autoReportingOffset;
-
-    // Return undefined if no frequency is set
-    if (!frequency || !policy) {
-        return {startDate: undefined, endDate: undefined};
-    }
-
-    let startDate: Date;
-    let endDate: Date;
-
-    switch (frequency) {
-        case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY: {
-            // Weekly: use the app's configured week start convention (Monday)
-            const weekStartsOn = CONST.WEEK_STARTS_ON;
-            startDate = startOfWeek(currentDate, {weekStartsOn});
-            endDate = endOfWeek(currentDate, {weekStartsOn});
-            break;
-        }
-
-        case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY: {
-            // Semi-monthly: 1st-15th or 16th-end of month
-            const dayOfMonth = currentDate.getDate();
-            if (dayOfMonth <= 15) {
-                startDate = startOfMonth(currentDate);
-                endDate = set(currentDate, {date: 15, hours: 23, minutes: 59, seconds: 59, milliseconds: 999});
-            } else {
-                startDate = set(currentDate, {date: 16, hours: 0, minutes: 0, seconds: 0, milliseconds: 0});
-                endDate = endOfMonth(currentDate);
-            }
-            break;
-        }
-
-        case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY: {
-            // Monthly reporting with different offset configurations
-            if (offset === CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH) {
-                const period = getMonthlyLastBusinessDayPeriod(currentDate);
-                startDate = period.startDate;
-                endDate = period.endDate;
-            } else if (typeof offset === 'number') {
-                const period = getMonthlyReportingPeriod(currentDate, offset);
-                startDate = period.startDate;
-                endDate = period.endDate;
-            } else {
-                // Default to full month
-                startDate = startOfMonth(currentDate);
-                endDate = endOfMonth(currentDate);
-            }
-            break;
-        }
-
-        case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP: {
-            // For trip-based, use oldest transaction as start
-            const oldestTransactionDateString = getOldestTransactionDate(report.reportID);
-            startDate = oldestTransactionDateString ? new Date(oldestTransactionDateString) : currentDate;
-            endDate = currentDate;
-            break;
-        }
-
-        default:
-            // For any other frequency, use current date as both start and end
-            startDate = currentDate;
-            endDate = currentDate;
-            break;
-    }
-
-    return {startDate, endDate};
-}
-
-/**
- * Get the date of the newest transaction for a given report
- */
-function getNewestTransactionDate(reportID: string, context?: FormulaContext): string | undefined {
-    if (!reportID) {
-        return undefined;
-    }
-
-    const transactions = getAllReportTransactionsWithContext(reportID, context);
-    if (!transactions || transactions.length === 0) {
-        return new Date().toISOString();
-    }
-
-    let newestDate: string | undefined;
-
-    for (const transaction of transactions) {
-        const created = getCreated(transaction);
-        if (!created) {
-            continue;
-        }
-        // Skip transactions with pending deletion (offline deletes) to calculate dates properly.
-        if (transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
-            continue;
-        }
-        if (newestDate && created <= newestDate) {
-            continue;
-        }
-        if (isPartialTransaction(transaction)) {
-            continue;
-        }
-        newestDate = created;
-    }
-
-    return newestDate;
-}
-
-/**
- * Compute the value of a report:submit:* formula part
- * Handles nested paths like submit:from:firstname, submit:to:email, submit:date
- */
-function computeSubmitPart(path: string[], context: FormulaContext): string {
-    const [direction, ...subPath] = path;
-
-    if (!direction) {
-        return '';
-    }
-
-    switch (direction.toLowerCase()) {
-        case 'from':
-            return computePersonalDetailsField(subPath, context.submitterPersonalDetails, context.policy);
-        case 'to':
-            return computePersonalDetailsField(subPath, context.managerPersonalDetails, context.policy);
-        case 'date': {
-            // TODO: Use report.submitted once backend adds it (issue #568267)
-            // Using report.created as placeholder until then
-            const submittedDate = context.report.created;
-            const format = subPath.length > 0 ? subPath.join(':') : undefined;
-            return formatDate(submittedDate, format);
-        }
-        default:
-            return '';
-    }
-}
-
-/**
- * Compute personal details information for either submitter (from) or manager (to)
- */
-function computePersonalDetailsField(path: string[], personalDetails: PersonalDetails | undefined, policy: OnyxEntry<Policy>): string {
-    const [field] = path;
-
-    if (!personalDetails || !field) {
-        return '';
-    }
-
-    switch (field.toLowerCase()) {
-        case 'firstname':
-            // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-            return personalDetails.firstName || personalDetails.login || '';
-        case 'lastname':
-            // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-            return personalDetails.lastName || personalDetails.login || '';
-        case 'fullname':
-            // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-            return personalDetails.displayName || personalDetails.login || '';
-        case 'email':
-            return personalDetails.login ?? '';
-        // userid/customfield1 returns employeeUserID from policy.employeeList
-        // TODO: Check policy.glCodes once backend adds it (issue #568268)
-        case 'userid':
-        case 'customfield1': {
-            const email = personalDetails.login;
-            if (!email || !policy?.employeeList) {
-                return '';
-            }
-            // eslint-disable-next-line rulesdir/no-default-id-values
-            return policy.employeeList[email]?.employeeUserID ?? '';
-        }
-        // payrollid/customfield2 returns employeePayrollID from policy.employeeList
-        case 'payrollid':
-        case 'customfield2': {
-            const email = personalDetails.login;
-            if (!email || !policy?.employeeList) {
-                return '';
-            }
-            // eslint-disable-next-line rulesdir/no-default-id-values
-            return policy.employeeList[email]?.employeePayrollID ?? '';
-        }
-        default:
-            return '';
-    }
-}
-
 export {FORMULA_PART_TYPES, parse, hasCircularReferences};
 
 export type {FieldList};

From 936eca368943cb2231bb52b85677768c99f70002 Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Tue, 23 Dec 2025 18:16:26 +0700
Subject: [PATCH 08/14] fix: lint

---
 tests/unit/FormulaTest.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts
index e2f270d819ec..432009a72188 100644
--- a/tests/unit/FormulaTest.ts
+++ b/tests/unit/FormulaTest.ts
@@ -9,11 +9,13 @@ jest.mock('@libs/ReportActionsUtils', () => ({
 }));
 
 jest.mock('@libs/ReportUtils', () => ({
+    // eslint-disable-next-line @typescript-eslint/consistent-type-imports
     ...jest.requireActual<typeof ReportUtils>('@libs/ReportUtils'),
     getReportTransactions: jest.fn(),
 }));
 
 jest.mock('@libs/CurrencyUtils', () => ({
+    // eslint-disable-next-line @typescript-eslint/consistent-type-imports
     ...jest.requireActual<typeof CurrencyUtils>('@libs/CurrencyUtils'),
     isValidCurrencyCode: jest.fn(),
 }));

From 9a8264657628e63bd70ae28c6b903e4cf14de17b Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Tue, 23 Dec 2025 18:19:26 +0700
Subject: [PATCH 09/14] fix: lint

---
 tests/unit/FormulaTest.ts | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts
index 432009a72188..39bc3089d15f 100644
--- a/tests/unit/FormulaTest.ts
+++ b/tests/unit/FormulaTest.ts
@@ -1,7 +1,7 @@
-// eslint-disable-next-line no-restricted-syntax -- disabled because we need CurrencyUtils to mock
+// eslint-disable-next-line no-restricted-syntax @typescript-eslint/consistent-type-imports -- disabled because we need CurrencyUtils to mock
 import * as CurrencyUtils from '@libs/CurrencyUtils';
 import {hasCircularReferences, parse} from '@libs/Formula';
-// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock
+// eslint-disable-next-line no-restricted-syntax @typescript-eslint/consistent-type-imports -- disabled because we need ReportUtils to mock
 import * as ReportUtils from '@libs/ReportUtils';
 
 jest.mock('@libs/ReportActionsUtils', () => ({
@@ -9,13 +9,11 @@ jest.mock('@libs/ReportActionsUtils', () => ({
 }));
 
 jest.mock('@libs/ReportUtils', () => ({
-    // eslint-disable-next-line @typescript-eslint/consistent-type-imports
     ...jest.requireActual<typeof ReportUtils>('@libs/ReportUtils'),
     getReportTransactions: jest.fn(),
 }));
 
 jest.mock('@libs/CurrencyUtils', () => ({
-    // eslint-disable-next-line @typescript-eslint/consistent-type-imports
     ...jest.requireActual<typeof CurrencyUtils>('@libs/CurrencyUtils'),
     isValidCurrencyCode: jest.fn(),
 }));

From c9d9f023238f838580ed4e12b35f5e01cde33b9d Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Tue, 23 Dec 2025 18:24:05 +0700
Subject: [PATCH 10/14] fix: lint

---
 tests/unit/FormulaTest.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts
index 39bc3089d15f..fbe6b27c82f9 100644
--- a/tests/unit/FormulaTest.ts
+++ b/tests/unit/FormulaTest.ts
@@ -1,7 +1,7 @@
-// eslint-disable-next-line no-restricted-syntax @typescript-eslint/consistent-type-imports -- disabled because we need CurrencyUtils to mock
+// eslint-disable-next-line @typescript-eslint/consistent-type-imports
 import * as CurrencyUtils from '@libs/CurrencyUtils';
 import {hasCircularReferences, parse} from '@libs/Formula';
-// eslint-disable-next-line no-restricted-syntax @typescript-eslint/consistent-type-imports -- disabled because we need ReportUtils to mock
+// eslint-disable-next-line @typescript-eslint/consistent-type-imports
 import * as ReportUtils from '@libs/ReportUtils';
 
 jest.mock('@libs/ReportActionsUtils', () => ({

From d36acfddb776ea227416a3aa3d9c1446e1d77fdf Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Tue, 23 Dec 2025 18:32:35 +0700
Subject: [PATCH 11/14] fix: lint

---
 tests/unit/FormulaTest.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts
index fbe6b27c82f9..8d78a23cc635 100644
--- a/tests/unit/FormulaTest.ts
+++ b/tests/unit/FormulaTest.ts
@@ -1,8 +1,8 @@
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
-import * as CurrencyUtils from '@libs/CurrencyUtils';
+// eslint-disable-next-line no-restricted-syntax
+import type * as CurrencyUtils from '@libs/CurrencyUtils';
 import {hasCircularReferences, parse} from '@libs/Formula';
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
-import * as ReportUtils from '@libs/ReportUtils';
+// eslint-disable-next-line no-restricted-syntax
+import type * as ReportUtils from '@libs/ReportUtils';
 
 jest.mock('@libs/ReportActionsUtils', () => ({
     getAllReportActions: jest.fn(),

From 294c480e719b19c453b1bbae1c44ec844a1d0861 Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Thu, 25 Dec 2025 17:02:19 +0700
Subject: [PATCH 12/14] fix: Revert unnecessary changes

---
 src/libs/Formula.ts       |  673 +++++++++++++++++++-
 tests/unit/FormulaTest.ts | 1257 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 1923 insertions(+), 7 deletions(-)

diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts
index b54dac751a82..abf11d2c499e 100644
--- a/src/libs/Formula.ts
+++ b/src/libs/Formula.ts
@@ -1,5 +1,16 @@
+import {endOfDay, endOfMonth, endOfWeek, getDay, lastDayOfMonth, set, startOfMonth, startOfWeek, subDays} from 'date-fns';
+import type {OnyxEntry} from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
+import CONST from '@src/CONST';
+import type {PersonalDetails, Policy, Report, Transaction} from '@src/types/onyx';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import {convertToDisplayString, convertToDisplayStringWithoutCurrency, isValidCurrencyCode} from './CurrencyUtils';
+import {formatDate} from './FormulaDatetime';
+import getBase62ReportID from './getBase62ReportID';
+import Log from './Log';
+import {getAllReportActions} from './ReportActionsUtils';
+import {getHumanReadableStatus, getMoneyRequestSpendBreakdown, getReportTransactions} from './ReportUtils';
+import {getCreated, isPartialTransaction, isTransactionPendingDelete} from './TransactionUtils';
 
 type FormulaPart = {
     /** The original definition from the formula */
@@ -15,6 +26,15 @@ type FormulaPart = {
     functions: string[];
 };
 
+type FormulaContext = {
+    report: Report;
+    policy: OnyxEntry<Policy>;
+    transaction?: Transaction;
+    submitterPersonalDetails?: PersonalDetails;
+    managerPersonalDetails?: PersonalDetails;
+    allTransactions?: Record<string, Transaction>;
+};
+
 type FieldList = Record<string, {name: string; defaultValue: string}>;
 
 const FORMULA_PART_TYPES = {
@@ -178,6 +198,7 @@ function parsePart(definition: string): FormulaPart {
     return part;
 }
 
+
 /**
  * Check if the report field formula value is containing circular references, e.g example:  A -> A,  A->B->A,  A->B->C->A, etc
  */
@@ -240,6 +261,654 @@ function hasCircularReferences(fieldValue: string, fieldName: string, fieldList?
     return hasCircularReferencesRecursive(fieldValue, fieldName);
 }
 
-export {FORMULA_PART_TYPES, parse, hasCircularReferences};
+/**
+ * Check if a formula part is a submission info part (report:submit:*)
+ */
+function isSubmissionInfoPart(part: FormulaPart): boolean {
+    return part.type === FORMULA_PART_TYPES.REPORT && part.fieldPath.at(0)?.toLowerCase() === 'submit';
+}
+
+/**
+ * Compute the value of a formula given a context
+ */
+function compute(formula?: string, context?: FormulaContext): string {
+    if (!formula || typeof formula !== 'string') {
+        return '';
+    }
+    if (!context) {
+        return '';
+    }
+
+    const parts = parse(formula);
+    let result = '';
+
+    for (const part of parts) {
+        let value = '';
+
+        switch (part.type) {
+            case FORMULA_PART_TYPES.REPORT:
+                value = computeReportPart(part, context);
+                // Apply fallback to formula definition for empty values, except for submission info
+                // Submission info explicitly returns empty strings when data is missing (matches backend)
+                if (value === '' && !isSubmissionInfoPart(part)) {
+                    value = part.definition;
+                }
+                break;
+            case FORMULA_PART_TYPES.FIELD:
+                value = computeFieldPart(part);
+                break;
+            case FORMULA_PART_TYPES.USER:
+                value = computeUserPart(part);
+                break;
+            case FORMULA_PART_TYPES.FREETEXT:
+                value = part.definition;
+                break;
+            default:
+                // If we don't recognize the part type, use the original definition
+                value = part.definition;
+        }
+
+        // Apply any functions to the computed value
+        value = applyFunctions(value, part.functions);
+        result += value;
+    }
+
+    return result;
+}
+
+/**
+ * Compute auto-reporting info for a report formula part
+ */
+function computeAutoReportingInfo(part: FormulaPart, context: FormulaContext, subField: string | undefined, format: string | undefined): string {
+    const {report, policy} = context;
+
+    if (!subField) {
+        return part.definition;
+    }
+
+    const {startDate, endDate} = getAutoReportingDates(policy, report);
+
+    switch (subField.toLowerCase()) {
+        case 'start':
+            return formatDate(startDate?.toISOString(), format);
+        case 'end':
+            return formatDate(endDate?.toISOString(), format);
+        default:
+            return part.definition;
+    }
+}
+
+/**
+ * Compute the value of a report formula part
+ */
+function computeReportPart(part: FormulaPart, context: FormulaContext): string {
+    const {report, policy, allTransactions} = context;
+    const [field, ...additionalPath] = part.fieldPath;
+    // Reconstruct format string by joining additional path elements with ':'
+    // This handles format strings with colons like 'HH:mm:ss'
+    const format = additionalPath.length > 0 ? additionalPath.join(':') : undefined;
+
+    if (!field) {
+        return part.definition;
+    }
+
+    switch (field.toLowerCase()) {
+        case 'id':
+            return getBase62ReportID(Number(report.reportID));
+        case 'status':
+            return formatStatus(report.statusNum);
+        case 'expensescount':
+            return String(getExpensesCount(report, allTransactions));
+        case 'type':
+            return formatType(report.type);
+        case 'startdate':
+            return formatDate(getOldestTransactionDate(report.reportID, context), format);
+        case 'enddate':
+            return formatDate(getNewestTransactionDate(report.reportID, context), format);
+        case 'total': {
+            const formattedAmount = formatAmount(report.total, report.currency, format);
+            // Return empty string when conversion needed (formatAmount returns null for unavailable conversions)
+            return formattedAmount ?? '';
+        }
+        case 'reimbursable': {
+            const formattedAmount = formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, report.currency, format);
+            return formattedAmount ?? '';
+        }
+        case 'currency':
+            return report.currency ?? '';
+        case 'policyname':
+        case 'workspacename':
+            return policy?.name ?? '';
+        case 'created':
+            // Backend will always return at least one report action (of type created) and its date is equal to report's creation date
+            // We can make it slightly more efficient in the future by ensuring report.created is always present in backend's responses
+            return formatDate(getOldestReportActionDate(report.reportID), format);
+        case 'submit': {
+            return computeSubmitPart(additionalPath, context);
+        }
+        case 'autoreporting': {
+            const subField = additionalPath.at(0);
+            // For multi-part formulas, format is everything after the subfield
+            const autoReportingFormat = additionalPath.length > 1 ? additionalPath.slice(1).join(':') : undefined;
+            return computeAutoReportingInfo(part, context, subField, autoReportingFormat);
+        }
+        default:
+            return part.definition;
+    }
+}
+
+/**
+ * Get the number of expenses in a report
+ * @param report - The report to get expenses for
+ * @param allTransactions - Optional map of all transactions. If provided, uses this instead of fetching from Onyx
+ */
+function getExpensesCount(report: Report, allTransactions?: Record<string, Transaction>): number {
+    if (!report.reportID) {
+        return 0;
+    }
+
+    if (allTransactions) {
+        const transactions = Object.values(allTransactions).filter((transaction): transaction is Transaction => !!transaction && transaction.reportID === report.reportID);
+        return transactions?.filter((transaction) => !isTransactionPendingDelete(transaction))?.length ?? 0;
+    }
+
+    return report.transactionCount ?? 0;
+}
+
+/**
+ * Format a report status number to human-readable string
+ */
+function formatStatus(statusNum: number | undefined): string {
+    if (statusNum === undefined) {
+        return '';
+    }
+
+    return getHumanReadableStatus(statusNum);
+}
+
+/**
+ * Compute the value of a field formula part
+ */
+function computeFieldPart(part: FormulaPart): string {
+    // Field computation will be implemented later
+    return part.definition;
+}
+
+/**
+ * Compute the value of a user formula part
+ */
+function computeUserPart(part: FormulaPart): string {
+    // User computation will be implemented later
+    return part.definition;
+}
+
+/**
+ * Apply functions to a computed value
+ */
+function applyFunctions(value: string, functions: string[]): string {
+    let result = value;
+
+    for (const func of functions) {
+        const [functionName, ...args] = func.split(':');
+
+        switch (functionName.toLowerCase()) {
+            case 'frontpart':
+                result = getFrontPart(result);
+                break;
+            case 'substr':
+                result = getSubstring(result, args);
+                break;
+            case 'domain':
+                result = getDomainName(result);
+                break;
+            default:
+                // Unknown function, leave value as is
+                break;
+        }
+    }
+
+    return result;
+}
+
+/**
+ * Get the front part of an email or first word of a string
+ */
+function getFrontPart(value: string): string {
+    const trimmed = value.trim();
+
+    // If it's an email, return the part before @
+    if (trimmed.includes('@')) {
+        return trimmed.split('@').at(0) ?? '';
+    }
+
+    // Otherwise, return the first word
+    return trimmed.split(' ').at(0) ?? '';
+}
+
+/**
+ * Get the domain name of an email or URL
+ */
+function getDomainName(value: string): string {
+    const trimmed = value.trim();
+
+    // If it's an email, return the part after @
+    if (trimmed.includes('@')) {
+        return trimmed.split('@').at(1) ?? '';
+    }
+
+    return '';
+}
+
+/**
+ * Get substring of a value
+ */
+function getSubstring(value: string, args: string[]): string {
+    const start = parseInt(args.at(0) ?? '', 10) || 0;
+    const length = args.at(1) ? parseInt(args.at(1) ?? '', 10) : undefined;
+
+    if (length !== undefined) {
+        return value.substring(start, start + length);
+    }
+
+    return value.substring(start);
+}
+
+/**
+ * Format an amount value
+ * @returns The formatted amount string, or null if currency conversion is needed (unavailable on frontend)
+ */
+function formatAmount(amount: number | undefined, currency: string | undefined, displayCurrency?: string): string | null {
+    if (amount === undefined) {
+        return '';
+    }
+
+    const absoluteAmount = Math.abs(amount);
+
+    try {
+        const trimmedDisplayCurrency = displayCurrency?.trim().toUpperCase();
+        if (trimmedDisplayCurrency) {
+            if (trimmedDisplayCurrency === 'NOSYMBOL') {
+                return convertToDisplayStringWithoutCurrency(absoluteAmount, currency);
+            }
+
+            // Check if format is a valid currency code (e.g., USD, EUR, eur)
+            if (!isValidCurrencyCode(trimmedDisplayCurrency)) {
+                return '';
+            }
+
+            // If a currency conversion is needed (displayCurrency differs from the source),
+            // return null so the backend can compute it.
+            // We can only compute the value optimistically when the amount is 0.
+            if (absoluteAmount !== 0 && currency !== trimmedDisplayCurrency) {
+                return null;
+            }
+
+            return convertToDisplayString(absoluteAmount, trimmedDisplayCurrency);
+        }
+
+        if (currency && isValidCurrencyCode(currency)) {
+            return convertToDisplayString(absoluteAmount, currency);
+        }
+
+        return convertToDisplayStringWithoutCurrency(absoluteAmount, currency);
+    } catch (error) {
+        Log.hmmm('[Formula] formatAmount failed', {error, amount, currency, displayCurrency});
+        return '';
+    }
+}
+
+/**
+ * Get the date of the oldest report action for a given report
+ */
+function getOldestReportActionDate(reportID: string): string | undefined {
+    if (!reportID) {
+        return undefined;
+    }
+
+    const reportActions = getAllReportActions(reportID);
+    if (!reportActions || Object.keys(reportActions).length === 0) {
+        return undefined;
+    }
+
+    let oldestDate: string | undefined;
+
+    for (const action of Object.values(reportActions)) {
+        if (!action?.created) {
+            continue;
+        }
+
+        if (oldestDate && action.created > oldestDate) {
+            continue;
+        }
+        oldestDate = action.created;
+    }
+
+    return oldestDate;
+}
+
+/**
+ * Format a report type to its human-readable string
+ */
+function formatType(type: string | undefined): string {
+    if (!type) {
+        return '';
+    }
+
+    const typeMapping: Record<string, string> = {
+        [CONST.REPORT.TYPE.EXPENSE]: 'Expense Report',
+        [CONST.REPORT.TYPE.INVOICE]: 'Invoice',
+        [CONST.REPORT.TYPE.CHAT]: 'Chat',
+        [CONST.REPORT.UNSUPPORTED_TYPE.BILL]: 'Bill',
+        [CONST.REPORT.UNSUPPORTED_TYPE.PAYCHECK]: 'Paycheck',
+        [CONST.REPORT.TYPE.IOU]: 'IOU',
+        [CONST.REPORT.TYPE.TASK]: 'Task',
+        trip: 'Trip',
+    };
+
+    return typeMapping[type.toLowerCase()] || type;
+}
+
+/**
+ * Get all transactions for a report, including any context transaction.
+ * Updates an existing transaction if it matches the context or adds it if new.
+ */
+function getAllReportTransactionsWithContext(reportID: string, context?: FormulaContext): Transaction[] {
+    const transactions = [...getReportTransactions(reportID)];
+    const contextTransaction = context?.transaction;
+
+    if (contextTransaction?.transactionID && contextTransaction.reportID === reportID) {
+        const transactionIndex = transactions.findIndex((transaction) => transaction?.transactionID === contextTransaction.transactionID);
+        if (transactionIndex >= 0) {
+            transactions[transactionIndex] = contextTransaction;
+        } else {
+            transactions.push(contextTransaction);
+        }
+    }
+
+    return transactions;
+}
+
+/**
+ * Get the date of the oldest transaction for a given report
+ */
+function getOldestTransactionDate(reportID: string, context?: FormulaContext): string | undefined {
+    if (!reportID) {
+        return undefined;
+    }
+
+    const transactions = getAllReportTransactionsWithContext(reportID, context);
+    if (!transactions || transactions.length === 0) {
+        return new Date().toISOString();
+    }
+
+    let oldestDate: string | undefined;
+
+    for (const transaction of transactions) {
+        const created = getCreated(transaction);
+        if (!created) {
+            continue;
+        }
+        // Skip transactions with pending deletion (offline deletes) to calculate dates properly.
+        if (transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+            continue;
+        }
+        if (oldestDate && created >= oldestDate) {
+            continue;
+        }
+        if (isPartialTransaction(transaction)) {
+            continue;
+        }
+        oldestDate = created;
+    }
+
+    return oldestDate;
+}
+
+/**
+ * Calculate monthly reporting period for a specific day offset
+ */
+function getMonthlyReportingPeriod(currentDate: Date, offsetDay: number): {startDate: Date; endDate: Date} {
+    const currentDay = currentDate.getDate();
+    const currentYear = currentDate.getFullYear();
+    const currentMonth = currentDate.getMonth();
+
+    if (currentDay <= offsetDay) {
+        // We haven't reached the reporting day yet - period is from last month's offset+1 to this month's offset
+        const prevMonth = currentMonth - 1;
+        const prevYear = prevMonth < 0 ? currentYear - 1 : currentYear;
+        const adjustedPrevMonth = prevMonth < 0 ? 11 : prevMonth;
+
+        const prevMonthDays = lastDayOfMonth(new Date(prevYear, adjustedPrevMonth, 1)).getDate();
+        const prevOffsetDay = Math.min(offsetDay, prevMonthDays);
+
+        const currentMonthDays = lastDayOfMonth(currentDate).getDate();
+        const currentOffsetDay = Math.min(offsetDay, currentMonthDays);
+
+        return {
+            startDate: new Date(prevYear, adjustedPrevMonth, prevOffsetDay + 1, 0, 0, 0, 0),
+            endDate: new Date(currentYear, currentMonth, currentOffsetDay, 23, 59, 59, 999),
+        };
+    }
+
+    // We've passed the reporting day - period is from this month's offset+1 to next month's offset
+    const nextMonth = currentMonth + 1;
+    const nextYear = nextMonth > 11 ? currentYear + 1 : currentYear;
+    const adjustedNextMonth = nextMonth > 11 ? 0 : nextMonth;
+
+    const currentMonthDays = lastDayOfMonth(currentDate).getDate();
+    const currentOffsetDay = Math.min(offsetDay, currentMonthDays);
+
+    const nextMonthDays = lastDayOfMonth(new Date(nextYear, adjustedNextMonth, 1)).getDate();
+    const nextOffsetDay = Math.min(offsetDay, nextMonthDays);
+
+    return {
+        startDate: new Date(currentYear, currentMonth, currentOffsetDay + 1, 0, 0, 0, 0),
+        endDate: new Date(nextYear, adjustedNextMonth, nextOffsetDay, 23, 59, 59, 999),
+    };
+}
+
+/**
+ * Calculate monthly reporting period for last business day
+ */
+function getMonthlyLastBusinessDayPeriod(currentDate: Date): {startDate: Date; endDate: Date} {
+    let endDate = endOfMonth(currentDate);
+
+    // Move backward to find last business day (Mon-Fri)
+    while (getDay(endDate) === 0 || getDay(endDate) === 6) {
+        endDate = subDays(endDate, 1);
+    }
+
+    return {
+        startDate: startOfMonth(currentDate),
+        endDate: endOfDay(endDate),
+    };
+}
+
+/**
+ * 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} {
+    const frequency = policy?.autoReportingFrequency;
+    const offset = policy?.autoReportingOffset;
+
+    // Return undefined if no frequency is set
+    if (!frequency || !policy) {
+        return {startDate: undefined, endDate: undefined};
+    }
+
+    let startDate: Date;
+    let endDate: Date;
+
+    switch (frequency) {
+        case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY: {
+            // Weekly: use the app's configured week start convention (Monday)
+            const weekStartsOn = CONST.WEEK_STARTS_ON;
+            startDate = startOfWeek(currentDate, {weekStartsOn});
+            endDate = endOfWeek(currentDate, {weekStartsOn});
+            break;
+        }
+
+        case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY: {
+            // Semi-monthly: 1st-15th or 16th-end of month
+            const dayOfMonth = currentDate.getDate();
+            if (dayOfMonth <= 15) {
+                startDate = startOfMonth(currentDate);
+                endDate = set(currentDate, {date: 15, hours: 23, minutes: 59, seconds: 59, milliseconds: 999});
+            } else {
+                startDate = set(currentDate, {date: 16, hours: 0, minutes: 0, seconds: 0, milliseconds: 0});
+                endDate = endOfMonth(currentDate);
+            }
+            break;
+        }
+
+        case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY: {
+            // Monthly reporting with different offset configurations
+            if (offset === CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH) {
+                const period = getMonthlyLastBusinessDayPeriod(currentDate);
+                startDate = period.startDate;
+                endDate = period.endDate;
+            } else if (typeof offset === 'number') {
+                const period = getMonthlyReportingPeriod(currentDate, offset);
+                startDate = period.startDate;
+                endDate = period.endDate;
+            } else {
+                // Default to full month
+                startDate = startOfMonth(currentDate);
+                endDate = endOfMonth(currentDate);
+            }
+            break;
+        }
+
+        case CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP: {
+            // For trip-based, use oldest transaction as start
+            const oldestTransactionDateString = getOldestTransactionDate(report.reportID);
+            startDate = oldestTransactionDateString ? new Date(oldestTransactionDateString) : currentDate;
+            endDate = currentDate;
+            break;
+        }
+
+        default:
+            // For any other frequency, use current date as both start and end
+            startDate = currentDate;
+            endDate = currentDate;
+            break;
+    }
+
+    return {startDate, endDate};
+}
+
+/**
+ * Get the date of the newest transaction for a given report
+ */
+function getNewestTransactionDate(reportID: string, context?: FormulaContext): string | undefined {
+    if (!reportID) {
+        return undefined;
+    }
+
+    const transactions = getAllReportTransactionsWithContext(reportID, context);
+    if (!transactions || transactions.length === 0) {
+        return new Date().toISOString();
+    }
+
+    let newestDate: string | undefined;
+
+    for (const transaction of transactions) {
+        const created = getCreated(transaction);
+        if (!created) {
+            continue;
+        }
+        // Skip transactions with pending deletion (offline deletes) to calculate dates properly.
+        if (transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+            continue;
+        }
+        if (newestDate && created <= newestDate) {
+            continue;
+        }
+        if (isPartialTransaction(transaction)) {
+            continue;
+        }
+        newestDate = created;
+    }
+
+    return newestDate;
+}
+
+/**
+ * Compute the value of a report:submit:* formula part
+ * Handles nested paths like submit:from:firstname, submit:to:email, submit:date
+ */
+function computeSubmitPart(path: string[], context: FormulaContext): string {
+    const [direction, ...subPath] = path;
+
+    if (!direction) {
+        return '';
+    }
+
+    switch (direction.toLowerCase()) {
+        case 'from':
+            return computePersonalDetailsField(subPath, context.submitterPersonalDetails, context.policy);
+        case 'to':
+            return computePersonalDetailsField(subPath, context.managerPersonalDetails, context.policy);
+        case 'date': {
+            // TODO: Use report.submitted once backend adds it (issue #568267)
+            // Using report.created as placeholder until then
+            const submittedDate = context.report.created;
+            const format = subPath.length > 0 ? subPath.join(':') : undefined;
+            return formatDate(submittedDate, format);
+        }
+        default:
+            return '';
+    }
+}
+
+/**
+ * Compute personal details information for either submitter (from) or manager (to)
+ */
+function computePersonalDetailsField(path: string[], personalDetails: PersonalDetails | undefined, policy: OnyxEntry<Policy>): string {
+    const [field] = path;
+
+    if (!personalDetails || !field) {
+        return '';
+    }
+
+    switch (field.toLowerCase()) {
+        case 'firstname':
+            // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+            return personalDetails.firstName || personalDetails.login || '';
+        case 'lastname':
+            // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+            return personalDetails.lastName || personalDetails.login || '';
+        case 'fullname':
+            // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+            return personalDetails.displayName || personalDetails.login || '';
+        case 'email':
+            return personalDetails.login ?? '';
+        // userid/customfield1 returns employeeUserID from policy.employeeList
+        // TODO: Check policy.glCodes once backend adds it (issue #568268)
+        case 'userid':
+        case 'customfield1': {
+            const email = personalDetails.login;
+            if (!email || !policy?.employeeList) {
+                return '';
+            }
+            // eslint-disable-next-line rulesdir/no-default-id-values
+            return policy.employeeList[email]?.employeeUserID ?? '';
+        }
+        // payrollid/customfield2 returns employeePayrollID from policy.employeeList
+        case 'payrollid':
+        case 'customfield2': {
+            const email = personalDetails.login;
+            if (!email || !policy?.employeeList) {
+                return '';
+            }
+            // eslint-disable-next-line rulesdir/no-default-id-values
+            return policy.employeeList[email]?.employeePayrollID ?? '';
+        }
+        default:
+            return '';
+    }
+}
+
+export {FORMULA_PART_TYPES, compute, parse, hasCircularReferences};
 
-export type {FieldList};
+export type {FormulaContext, FieldList};
diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts
index 8d78a23cc635..375b49c63f11 100644
--- a/tests/unit/FormulaTest.ts
+++ b/tests/unit/FormulaTest.ts
@@ -1,8 +1,13 @@
-// eslint-disable-next-line no-restricted-syntax
-import type * as CurrencyUtils from '@libs/CurrencyUtils';
-import {hasCircularReferences, parse} from '@libs/Formula';
-// eslint-disable-next-line no-restricted-syntax
-import type * as ReportUtils from '@libs/ReportUtils';
+// eslint-disable-next-line no-restricted-syntax -- disabled because we need CurrencyUtils to mock
+import * as CurrencyUtils from '@libs/CurrencyUtils';
+import type {FormulaContext} from '@libs/Formula';
+import {compute, hasCircularReferences, parse} from '@libs/Formula';
+// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportActionsUtils to mock
+import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock
+import * as ReportUtils from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import type {PersonalDetails, Policy, Report, ReportActions, Transaction} from '@src/types/onyx';
 
 jest.mock('@libs/ReportActionsUtils', () => ({
     getAllReportActions: jest.fn(),
@@ -18,6 +23,10 @@ jest.mock('@libs/CurrencyUtils', () => ({
     isValidCurrencyCode: jest.fn(),
 }));
 
+const mockReportActionsUtils = ReportActionsUtils as jest.Mocked<typeof ReportActionsUtils>;
+const mockReportUtils = ReportUtils as jest.Mocked<typeof ReportUtils>;
+const mockCurrencyUtils = CurrencyUtils as jest.Mocked<typeof CurrencyUtils>;
+
 describe('CustomFormula', () => {
     describe('parse()', () => {
         test('should parse report formula parts', () => {
@@ -68,11 +77,1249 @@ describe('CustomFormula', () => {
         });
     });
 
+    describe('compute()', () => {
+        const mockContext: FormulaContext = {
+            report: {
+                reportID: '123',
+                reportName: '',
+                type: 'expense',
+                total: -10000, // -$100.00
+                currency: 'USD',
+                lastVisibleActionCreated: '2025-01-15T10:30:00Z',
+                policyID: 'policy1',
+            } as Report,
+            policy: {
+                name: 'Test Policy',
+            } as Policy,
+        };
+
+        beforeEach(() => {
+            jest.clearAllMocks();
+
+            mockCurrencyUtils.isValidCurrencyCode.mockImplementation((code: string) => ['USD', 'EUR', 'JPY', 'NPR'].includes(code));
+
+            const mockReportActions = {
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                '1': {
+                    reportActionID: '1',
+                    created: '2025-01-10T08:00:00Z', // Oldest action
+                    actionName: 'CREATED',
+                },
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                '2': {
+                    reportActionID: '2',
+                    created: '2025-01-15T10:30:00Z', // Later action
+                    actionName: 'IOU',
+                },
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                '3': {
+                    reportActionID: '3',
+                    created: '2025-01-12T14:20:00Z', // Middle action
+                    actionName: 'COMMENT',
+                },
+            } as unknown as ReportActions;
+
+            const mockTransactions = [
+                {
+                    transactionID: 'trans1',
+                    created: '2025-01-08T12:00:00Z', // Oldest transaction
+                    amount: 5000,
+                    merchant: 'ACME Ltd.',
+                },
+                {
+                    transactionID: 'trans2',
+                    created: '2025-01-14T16:45:00Z', // Later transaction
+                    amount: 3000,
+                    merchant: 'ACME Ltd.',
+                },
+                {
+                    transactionID: 'trans3',
+                    created: '2025-01-11T09:15:00Z', // Middle transaction
+                    amount: 2000,
+                    merchant: 'ACME Ltd.',
+                },
+            ] as Transaction[];
+
+            mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions);
+            mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
+        });
+
+        test('should compute basic report formula', () => {
+            const result = compute('{report:type} {report:total}', mockContext);
+            expect(result).toBe('Expense Report $100.00'); // No space between parts
+        });
+
+        test('should compute startdate formula using transactions', () => {
+            const result = compute('{report:startdate}', mockContext);
+            expect(result).toBe('2025-01-08'); // Should use oldest transaction date (2025-01-08)
+        });
+
+        test('should compute enddate formula using transactions', () => {
+            const result = compute('{report:enddate}', mockContext);
+            expect(result).toBe('2025-01-14'); // Should use newest transaction date (2025-01-14)
+        });
+
+        test('should compute created formula using report actions', () => {
+            const result = compute('{report:created}', mockContext);
+            expect(result).toBe('2025-01-10'); // Should use oldest report action date (2025-01-10)
+        });
+
+        test('should compute startdate with custom format', () => {
+            const result = compute('{report:startdate:MM/dd/yyyy}', mockContext);
+            expect(result).toBe('01/08/2025'); // Should use oldest transaction date with yyyy-MM-dd format
+        });
+
+        test('should compute enddate with custom format', () => {
+            const result = compute('{report:enddate:MM/dd/yyyy}', mockContext);
+            expect(result).toBe('01/14/2025'); // Should use newest transaction date with MM/dd/yyyy format
+        });
+
+        test('should compute created with custom format', () => {
+            const result = compute('{report:created:MMMM dd, yyyy}', mockContext);
+            expect(result).toBe('January 10, 2025'); // Should use oldest report action date with MMMM dd, yyyy format
+        });
+
+        test('should compute startdate with short month format', () => {
+            const result = compute('{report:startdate:dd MMM yyyy}', mockContext);
+            expect(result).toBe('08 Jan 2025'); // Should use oldest transaction date with dd MMM yyyy format
+        });
+
+        test('should compute enddate with short month format', () => {
+            const result = compute('{report:enddate:dd MMM yyyy}', mockContext);
+            expect(result).toBe('14 Jan 2025'); // Should use newest transaction date with dd MMM yyyy format
+        });
+
+        test('should compute policy name', () => {
+            const result = compute('{report:policyname}', mockContext);
+            expect(result).toBe('Test Policy');
+        });
+
+        test('should compute report ID in base62 format', () => {
+            const result = compute('{report:id}', mockContext);
+            expect(result).toBe('R0000000001z');
+        });
+
+        test('should compute report status', () => {
+            const contextWithStatus: FormulaContext = {
+                ...mockContext,
+                report: {
+                    ...mockContext.report,
+                    statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+                },
+            };
+            const result = compute('{report:status}', contextWithStatus);
+            expect(result).toBe('Submitted');
+        });
+
+        test('should compute expenses count', () => {
+            const result = compute('{report:expensescount}', mockContext);
+            expect(result).toBe('0');
+        });
+
+        test('should compute expenses count using allTransactions from context', () => {
+            const allTransactions = {
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                transactions_trans1: {
+                    transactionID: 'trans1',
+                    reportID: '123',
+                    created: '2025-01-08T12:00:00Z',
+                    amount: 5000,
+                    merchant: 'ACME Ltd.',
+                } as Transaction,
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                transactions_trans2: {
+                    transactionID: 'trans2',
+                    reportID: '123',
+                    created: '2025-01-14T16:45:00Z',
+                    amount: 3000,
+                    merchant: 'ACME Ltd.',
+                } as Transaction,
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                transactions_trans3: {
+                    transactionID: 'trans3',
+                    reportID: '123',
+                    created: '2025-01-11T09:15:00Z',
+                    amount: 2000,
+                    merchant: 'ACME Ltd.',
+                } as Transaction,
+            };
+
+            const contextWithAllTransactions: FormulaContext = {
+                ...mockContext,
+                allTransactions,
+            };
+
+            const result = compute('{report:expensescount}', contextWithAllTransactions);
+            expect(result).toBe('3');
+            // Verify that getReportTransactions was NOT called when allTransactions is provided
+            expect(mockReportUtils.getReportTransactions).not.toHaveBeenCalled();
+        });
+
+        test('should handle empty formula', () => {
+            expect(compute('', mockContext)).toBe('');
+        });
+
+        test('should handle unknown formula parts', () => {
+            const result = compute('{report:unknown}', mockContext);
+            expect(result).toBe('{report:unknown}');
+        });
+
+        test('should handle missing report data gracefully', () => {
+            const contextWithMissingData: FormulaContext = {
+                report: {} as unknown as Report,
+                policy: null as unknown as Policy,
+            };
+            const result = compute('{report:total} {report:policyname}', contextWithMissingData);
+            expect(result).toBe('{report:total} {report:policyname}'); // Empty data is replaced with definition
+        });
+
+        test('should preserve free text', () => {
+            const result = compute('Expense Report - {report:total}', mockContext);
+            expect(result).toBe('Expense Report - $100.00');
+        });
+
+        test('should preserve exact spacing around formula parts', () => {
+            const result = compute('Report with type after 4 spaces   {report:type}-and no space after computed part', mockContext);
+            expect(result).toBe('Report with type after 4 spaces   Expense Report-and no space after computed part');
+        });
+
+        test('should compute complex formula with multiple new parts', () => {
+            const contextWithStatus: FormulaContext = {
+                ...mockContext,
+                report: {
+                    ...mockContext.report,
+                    transactionCount: 3,
+                    statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
+                },
+            };
+            const result = compute('Report {report:id} has {report:expensescount} expenses and is {report:status}', contextWithStatus);
+            expect(result).toBe('Report R0000000001z has 3 expenses and is Approved');
+        });
+
+        test('should handle combination of new and existing formula parts', () => {
+            const contextWithStatus: FormulaContext = {
+                ...mockContext,
+                report: {
+                    ...mockContext.report,
+                    statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+                    transactionCount: 3,
+                },
+            };
+            const result = compute('{report:type} {report:id} - {report:status} - Total: {report:total} ({report:expensescount} expenses)', contextWithStatus);
+            expect(result).toBe('Expense Report R0000000001z - Submitted - Total: $100.00 (3 expenses)');
+        });
+
+        test('should handle different status numbers', () => {
+            const testCases = [
+                {statusNum: CONST.REPORT.STATUS_NUM.OPEN, expected: 'Open'},
+                {statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, expected: 'Submitted'},
+                {statusNum: CONST.REPORT.STATUS_NUM.CLOSED, expected: 'Closed'},
+                {statusNum: CONST.REPORT.STATUS_NUM.APPROVED, expected: 'Approved'},
+                {statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, expected: 'Reimbursed'},
+            ];
+
+            for (const {statusNum, expected} of testCases) {
+                const contextWithStatus: FormulaContext = {
+                    ...mockContext,
+                    report: {
+                        ...mockContext.report,
+                        statusNum,
+                    },
+                };
+                const result = compute('{report:status}', contextWithStatus);
+                expect(result).toBe(expected);
+            }
+        });
+
+        test('should handle undefined status number', () => {
+            const contextWithUndefinedStatus: FormulaContext = {
+                ...mockContext,
+                report: {
+                    ...mockContext.report,
+                    statusNum: undefined,
+                },
+            };
+            const result = compute('{report:status}', contextWithUndefinedStatus);
+            expect(result).toBe('{report:status}');
+        });
+
+        test('should return 0 for expensescount when no transactions exist', () => {
+            mockReportUtils.getReportTransactions.mockReturnValue([]);
+            const result = compute('{report:expensescount}', mockContext);
+            expect(result).toBe('0');
+        });
+
+        test('should return 0 for expensescount when reportID is empty', () => {
+            const contextWithEmptyReportID: FormulaContext = {
+                ...mockContext,
+                report: {
+                    ...mockContext.report,
+                    reportID: '',
+                },
+            };
+            const result = compute('{report:expensescount}', contextWithEmptyReportID);
+            expect(result).toBe('0');
+        });
+
+        test('should compute report ID with different reportID values', () => {
+            const contextWithDifferentID: FormulaContext = {
+                ...mockContext,
+                report: {
+                    ...mockContext.report,
+                    reportID: '456789',
+                },
+            };
+            const result = compute('{report:id}', contextWithDifferentID);
+            expect(result).toBe('R00000001upZ');
+        });
+    });
+
+    describe('Reimbursable Amount', () => {
+        const reimbursableContext: FormulaContext = {
+            report: {
+                reportID: '123',
+                reportName: '',
+                type: 'expense',
+                policyID: 'policy1',
+            },
+            policy: {
+                name: 'Test Policy',
+            } as Policy,
+        };
+
+        const calculateExpectedReimbursable = (total: number, nonReimbursableTotal: number) => {
+            const reimbursableAmount = total - nonReimbursableTotal;
+            return Math.abs(reimbursableAmount) / 100;
+        };
+
+        beforeEach(() => {
+            jest.clearAllMocks();
+        });
+
+        test('should compute reimbursable amount', () => {
+            reimbursableContext.report.currency = 'USD';
+            reimbursableContext.report.total = -10000; // -$100.00
+            reimbursableContext.report.nonReimbursableTotal = -2500; // -$25.00
+
+            const expectedReimbursable = calculateExpectedReimbursable(reimbursableContext.report.total, reimbursableContext.report.nonReimbursableTotal);
+            const result = compute('{report:reimbursable}', reimbursableContext);
+            expect(result).toBe(`$${expectedReimbursable.toFixed(2)}`);
+        });
+
+        test('should compute reimbursable amount with different currency', () => {
+            reimbursableContext.report.currency = 'EUR';
+            reimbursableContext.report.total = -8000; // -€80.00
+            reimbursableContext.report.nonReimbursableTotal = -3000; // -€30.00
+
+            const expectedReimbursable = calculateExpectedReimbursable(reimbursableContext.report.total, reimbursableContext.report.nonReimbursableTotal);
+            const result = compute('{report:reimbursable}', reimbursableContext);
+
+            expect(result).toBe(`€${expectedReimbursable.toFixed(2)}`);
+        });
+
+        test('should handle zero reimbursable amount', () => {
+            reimbursableContext.report.currency = 'USD';
+            reimbursableContext.report.total = -10000; // -$100.00
+            reimbursableContext.report.nonReimbursableTotal = -10000; // -$100.00 (all non-reimbursable)
+
+            const expectedReimbursable = calculateExpectedReimbursable(reimbursableContext.report.total, reimbursableContext.report.nonReimbursableTotal);
+            const result = compute('{report:reimbursable}', reimbursableContext);
+            expect(result).toBe(`$${expectedReimbursable.toFixed(2)}`);
+        });
+
+        test('should handle undefined reimbursable amount', () => {
+            reimbursableContext.report.currency = 'USD';
+            reimbursableContext.report.total = undefined;
+            reimbursableContext.report.nonReimbursableTotal = undefined;
+
+            const result = compute('{report:reimbursable}', reimbursableContext);
+            expect(result).toBe('$0.00');
+        });
+
+        test('should handle missing currency gracefully', () => {
+            reimbursableContext.report.currency = undefined;
+            reimbursableContext.report.total = -10000; // -100.00
+            reimbursableContext.report.nonReimbursableTotal = -2500; // -25.00
+
+            const expectedReimbursable = calculateExpectedReimbursable(reimbursableContext.report.total, reimbursableContext.report.nonReimbursableTotal);
+            const result = compute('{report:reimbursable}', reimbursableContext);
+            expect(result).toBe(`${expectedReimbursable.toFixed(2)}`);
+        });
+
+        describe('Currency Formatting & Conversion', () => {
+            const currencyContext: FormulaContext = {
+                report: {
+                    reportID: '123',
+                    total: -10000,
+                    currency: 'USD',
+                } as Report,
+                policy: {} as Policy,
+            };
+
+            beforeEach(() => {
+                jest.clearAllMocks();
+            });
+
+            describe('Edge cases', () => {
+                test('undefined currency - should format without symbol', () => {
+                    currencyContext.report.currency = undefined;
+                    const result = compute('{report:total}', currencyContext);
+                    expect(result).toBe('100.00');
+                });
+
+                test('invalid source currency - should return placeholder', () => {
+                    currencyContext.report.currency = 'UNKNOWN';
+                    const result = compute('{report:total}', currencyContext);
+                    expect(result).toBe('{report:total}');
+                });
+
+                test('invalid format currency - should return placeholder', () => {
+                    currencyContext.report.currency = 'EUR';
+                    const result = compute('{report:total:UNKNOWN}', currencyContext);
+                    expect(result).toBe('{report:total:UNKNOWN}');
+                });
+            });
+        });
+    });
+
+    describe('Function Modifiers', () => {
+        const mockContext: FormulaContext = {
+            report: {
+                reportID: 'report123456789',
+                reportName: '',
+                type: 'expense',
+                total: -10000, // -$100.00
+                currency: 'USD',
+                lastVisibleActionCreated: '2025-01-15T10:30:00Z',
+                policyID: 'policy1',
+            } as Report,
+            policy: {
+                name: 'Engineering Department Rules',
+            } as Policy,
+        };
+
+        beforeEach(() => {
+            jest.clearAllMocks();
+        });
+
+        describe('frontpart modifier', () => {
+            test('should extract first word from non-email text', () => {
+                const result = compute('{report:policyname|frontpart}', mockContext);
+                expect(result).toBe('Engineering'); // First word of "Engineering Department Rules"
+            });
+
+            test('should handle empty strings', () => {
+                const contextWithEmpty: FormulaContext = {
+                    report: {} as Report,
+                    policy: {name: ''} as Policy,
+                };
+                const result = compute('{report:policyname|frontpart}', contextWithEmpty);
+                expect(result).toBe('{report:policyname|frontpart}'); // Falls back to formula definition
+            });
+        });
+
+        describe('domain modifier', () => {
+            test('should extract domain from email', () => {
+                const result = compute('{report:submit:from:email|domain}', mockContext);
+                // Submit part extraction not implemented yet; for now, it returns the definition
+                // Once implemented, this should return 'domain' of the email
+                expect(result).toBe('');
+            });
+
+            test('should return empty for non-email text', () => {
+                const result = compute('{report:policyname|domain}', mockContext);
+                expect(result).toBe(''); // "Engineering Department Rules" has no @ symbol
+            });
+
+            test('should handle empty strings', () => {
+                const contextWithEmpty: FormulaContext = {
+                    report: {} as Report,
+                    policy: {name: ''} as Policy,
+                };
+                const result = compute('{report:policyname|domain}', contextWithEmpty);
+                expect(result).toBe(''); // Empty policy name
+            });
+        });
+
+        describe('substr modifier', () => {
+            test('should extract substring with start and length', () => {
+                const result = compute('{report:policyname|substr:0:11}', mockContext);
+                expect(result).toBe('Engineering'); // First 11 characters of "Engineering Department Rules"
+            });
+
+            test('should extract substring with only start position', () => {
+                const result = compute('{report:policyname|substr:12}', mockContext);
+                expect(result).toBe('Department Rules'); // From position 12 to end
+            });
+
+            test('should handle start position beyond string length', () => {
+                const result = compute('{report:policyname|substr:50:10}', mockContext);
+                expect(result).toBe(''); // Start position 50 is beyond string length
+            });
+
+            test('should handle length larger than remaining string', () => {
+                const result = compute('{report:policyname|substr:23:50}', mockContext);
+                expect(result).toBe('Rules'); // Only remaining characters
+            });
+
+            test('should handle invalid length parameter', () => {
+                const result = compute('{report:policyname|substr:0:abc}', mockContext);
+                expect(result).toBe(''); // Invalid length, returns empty
+            });
+        });
+    });
+
+    describe('Auto-reporting Frequency', () => {
+        const mockReport = {reportID: '123'} as Report;
+        const createMockContext = (policy: Policy): FormulaContext => ({report: mockReport, policy});
+
+        beforeEach(() => {
+            jest.clearAllMocks();
+            jest.useFakeTimers();
+            jest.setSystemTime(new Date('2025-01-19T14:23:45Z'));
+        });
+
+        afterEach(() => {
+            jest.useRealTimers();
+        });
+
+        test('should compute weekly frequency dates', () => {
+            const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY} as Policy;
+            const context = createMockContext(policy);
+
+            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-13');
+            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-19');
+        });
+
+        test('should compute semi-monthly frequency dates', () => {
+            jest.setSystemTime(new Date('2025-01-10T12:00:00Z'));
+            const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY} as Policy;
+            const context = createMockContext(policy);
+
+            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-01');
+            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-15');
+
+            jest.setSystemTime(new Date('2025-01-20T12:00:00Z'));
+            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-16');
+            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-31');
+        });
+
+        test('should compute monthly frequency with specific offset', () => {
+            const policy = {
+                autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY,
+                autoReportingOffset: 25,
+            } as Policy;
+            const context = createMockContext(policy);
+
+            expect(compute('{report:autoreporting:start}', context)).toBe('2024-12-26');
+            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-25');
+        });
+
+        test('should compute monthly frequency with last business day', () => {
+            const policy = {
+                autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY,
+                autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH,
+            } as Policy;
+            const context = createMockContext(policy);
+
+            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-01');
+            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-31');
+        });
+
+        test('should compute trip frequency dates', () => {
+            const mockTransactions = [
+                {transactionID: 'trans1', created: '2025-01-08T12:00:00Z', merchant: 'Hotel', amount: 5000} as Transaction,
+                {transactionID: 'trans2', created: '2025-01-14T16:45:00Z', merchant: 'Restaurant', amount: 3000} as Transaction,
+            ];
+
+            mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
+
+            const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP} as Policy;
+            const context = createMockContext(policy);
+
+            expect(compute('{report:autoreporting:start}', context)).toBe('2025-01-08');
+            expect(compute('{report:autoreporting:end}', context)).toBe('2025-01-19');
+        });
+
+        test('should apply custom date formats', () => {
+            const policy = {autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY} as Policy;
+            const context = createMockContext(policy);
+
+            expect(compute('{report:autoreporting:start:MMMM dd, yyyy}', context)).toBe('January 13, 2025');
+            expect(compute('{report:autoreporting:end:MM/dd/yyyy}', context)).toBe('01/19/2025');
+        });
+
+        test('should return formula definition when policy or frequency is missing', () => {
+            expect(compute('{report:autoreporting:start}', {report: mockReport, policy: undefined})).toBe('{report:autoreporting:start}');
+            expect(compute('{report:autoreporting:end}', createMockContext({} as Policy))).toBe('{report:autoreporting:end}');
+        });
+    });
+
     describe('Edge Cases', () => {
         test('should handle malformed braces', () => {
             const parts = parse('{incomplete');
             expect(parts.at(0)?.type).toBe('freetext');
         });
+
+        test('should handle undefined amounts', () => {
+            const context: FormulaContext = {
+                report: {total: undefined} as Report,
+                policy: null as unknown as Policy,
+            };
+            const result = compute('{report:total}', context);
+            expect(result).toBe('{report:total}');
+        });
+
+        test('should handle missing report actions for created', () => {
+            mockReportActionsUtils.getAllReportActions.mockReturnValue({});
+            const context: FormulaContext = {
+                report: {reportID: '123'} as Report,
+                policy: null as unknown as Policy,
+            };
+
+            const result = compute('{report:created}', context);
+            expect(result).toBe('{report:created}');
+        });
+
+        test('should handle missing transactions for startdate', () => {
+            mockReportUtils.getReportTransactions.mockReturnValue([]);
+            const context: FormulaContext = {
+                report: {reportID: '123'} as Report,
+                policy: null as unknown as Policy,
+            };
+            const today = new Date();
+            const expected = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
+            const result = compute('{report:startdate}', context);
+            expect(result).toBe(expected);
+        });
+
+        test('should handle missing transactions for enddate', () => {
+            mockReportUtils.getReportTransactions.mockReturnValue([]);
+            const context: FormulaContext = {
+                report: {reportID: '123'} as Report,
+                policy: null as unknown as Policy,
+            };
+            const today = new Date();
+            const expected = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
+            const result = compute('{report:enddate}', context);
+            expect(result).toBe(expected);
+        });
+
+        test('should call getReportTransactions with correct reportID for startdate', () => {
+            const context: FormulaContext = {
+                report: {reportID: 'test-report-123'} as Report,
+                policy: null as unknown as Policy,
+            };
+
+            compute('{report:startdate}', context);
+            expect(mockReportUtils.getReportTransactions).toHaveBeenCalledWith('test-report-123');
+        });
+
+        test('should call getAllReportActions with correct reportID for created', () => {
+            const context: FormulaContext = {
+                report: {reportID: 'test-report-456'} as Report,
+                policy: null as unknown as Policy,
+            };
+
+            compute('{report:created}', context);
+            expect(mockReportActionsUtils.getAllReportActions).toHaveBeenCalledWith('test-report-456');
+        });
+
+        test('should skip partial transactions (empty merchant)', () => {
+            const mockTransactions = [
+                {
+                    transactionID: 'trans1',
+                    created: '2025-01-15T12:00:00Z',
+                    amount: 5000,
+                    merchant: 'ACME Ltd.',
+                },
+                {
+                    transactionID: 'trans2',
+                    created: '2025-01-08T16:45:00Z', // Older but partial
+                    amount: 3000,
+                    merchant: '', // Empty merchant = partial
+                },
+                {
+                    transactionID: 'trans3',
+                    created: '2025-01-12T09:15:00Z', // Should be oldest valid
+                    amount: 2000,
+                    merchant: 'Gamma Inc.',
+                },
+            ] as Transaction[];
+
+            mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
+            const context: FormulaContext = {
+                report: {reportID: 'test-report-123'} as Report,
+                policy: null as unknown as Policy,
+            };
+
+            const result = compute('{report:startdate}', context);
+            expect(result).toBe('2025-01-12');
+
+            const endResult = compute('{report:enddate}', context);
+            expect(endResult).toBe('2025-01-15');
+        });
+
+        test('should skip partial transactions (zero amount)', () => {
+            const mockTransactions = [
+                {
+                    transactionID: 'trans1',
+                    created: '2025-01-15T12:00:00Z',
+                    amount: 5000,
+                    merchant: 'ACME Ltd.',
+                },
+                {
+                    transactionID: 'trans2',
+                    created: '2025-01-08T16:45:00Z', // Older but partial
+                    amount: 0, // Zero amount = partial
+                    merchant: 'Beta Corp.',
+                    iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN,
+                },
+                {
+                    transactionID: 'trans3',
+                    created: '2025-01-12T09:15:00Z', // Should be oldest valid
+                    amount: 2000,
+                    merchant: 'Gamma Inc.',
+                },
+            ] as Transaction[];
+
+            mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
+            const context: FormulaContext = {
+                report: {reportID: 'test-report-123'} as Report,
+                policy: null as unknown as Policy,
+            };
+
+            const result = compute('{report:startdate}', context);
+            expect(result).toBe('2025-01-12');
+
+            const endResult = compute('{report:enddate}', context);
+            expect(endResult).toBe('2025-01-15');
+        });
+    });
+
+    describe('Date Format Tokens', () => {
+        // Test date: Wednesday, January 8, 2025, 3:30:45 PM (15:30:45) UTC
+        const testDate = '2025-01-08T15:30:45.123Z';
+        const morningDate = '2025-01-08T09:05:02.123Z'; // 9:05:02 AM for leading zero tests
+
+        const mockContextWithDate: FormulaContext = {
+            report: {reportID: '123'} as Report,
+            policy: null as unknown as Policy,
+        };
+
+        const setupMockDate = (date: string) => {
+            const mockTransaction = {
+                transactionID: 'trans1',
+                created: date,
+                amount: -5000,
+                merchant: 'Test Store',
+            } as Transaction;
+            mockReportUtils.getReportTransactions.mockReturnValue([mockTransaction]);
+
+            const mockReportAction = {
+                created: date,
+                actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
+            } as unknown as ReportActions[string];
+            mockReportActionsUtils.getAllReportActions.mockReturnValue({action1: mockReportAction});
+        };
+
+        beforeEach(() => setupMockDate(testDate));
+
+        test('year formats - yyyy/yy/Y/y/YYYY', () => {
+            expect(compute('{report:startdate:yyyy}', mockContextWithDate)).toBe('2025');
+            expect(compute('{report:startdate:YYYY}', mockContextWithDate)).toBe('2025');
+            expect(compute('{report:startdate:Y}', mockContextWithDate)).toBe('2025');
+            expect(compute('{report:startdate:yy}', mockContextWithDate)).toBe('25');
+            expect(compute('{report:startdate:y}', mockContextWithDate)).toBe('25');
+        });
+
+        test('month formats - names and numbers', () => {
+            expect(compute('{report:startdate:MMMM}', mockContextWithDate)).toBe('January');
+            expect(compute('{report:startdate:F}', mockContextWithDate)).toBe('January');
+            expect(compute('{report:startdate:MMM}', mockContextWithDate)).toBe('Jan');
+            expect(compute('{report:startdate:M}', mockContextWithDate)).toBe('Jan');
+            expect(compute('{report:startdate:MM}', mockContextWithDate)).toBe('01');
+            expect(compute('{report:startdate:n}', mockContextWithDate)).toBe('1');
+            expect(compute('{report:startdate:t}', mockContextWithDate)).toBe('31');
+        });
+
+        test('day formats - numbers and names', () => {
+            expect(compute('{report:startdate:dd}', mockContextWithDate)).toBe('08');
+            expect(compute('{report:startdate:d}', mockContextWithDate)).toBe('08');
+            expect(compute('{report:startdate:j}', mockContextWithDate)).toBe('8');
+            expect(compute('{report:startdate:S}', mockContextWithDate)).toBe('th');
+            expect(compute('{report:startdate:jS}', mockContextWithDate)).toBe('8th');
+            expect(compute('{report:startdate:dddd}', mockContextWithDate)).toBe('Wednesday');
+            expect(compute('{report:startdate:l}', mockContextWithDate)).toBe('Wednesday');
+            expect(compute('{report:startdate:ddd}', mockContextWithDate)).toBe('Wed');
+            expect(compute('{report:startdate:D}', mockContextWithDate)).toBe('Wed');
+            expect(compute('{report:startdate:w}', mockContextWithDate)).toBe('3');
+            expect(compute('{report:startdate:N}', mockContextWithDate)).toBe('3');
+            expect(compute('{report:startdate:z}', mockContextWithDate)).toBe('7');
+            expect(compute('{report:startdate:W}', mockContextWithDate)).toBe('02');
+        });
+
+        test('ISO week number - first Thursday rule', () => {
+            // January 1, 2021 is a Friday
+            // Week 1 of 2021 contains the first Thursday (Jan 7)
+            // So Jan 1-3 (Fri, Sat, Sun) belong to week 53 of 2020
+            setupMockDate('2021-01-01T12:00:00Z'); // Friday, Jan 1, 2021
+            expect(compute('{report:startdate:W}', mockContextWithDate)).toBe('53');
+
+            // January 4 is Monday, which is in week 1
+            setupMockDate('2021-01-04T12:00:00Z'); // Monday, Jan 4, 2021
+            expect(compute('{report:startdate:W}', mockContextWithDate)).toBe('01');
+
+            // December 31, 2020 is Thursday, should be week 53
+            setupMockDate('2020-12-31T12:00:00Z'); // Thursday, Dec 31, 2020
+            expect(compute('{report:startdate:W}', mockContextWithDate)).toBe('53');
+        });
+
+        test('complex date formats', () => {
+            expect(compute('{report:startdate:MMMM dd, yyyy}', mockContextWithDate)).toBe('January 08, 2025');
+            expect(compute('{report:startdate:dd MMM yyyy}', mockContextWithDate)).toBe('08 Jan 2025');
+            expect(compute('{report:startdate:yyyy-MM-dd}', mockContextWithDate)).toBe('2025-01-08');
+        });
+
+        test('time formats - hours', () => {
+            // 24-hour format: 15:30:45 (afternoon) and 09:05:02 (morning with leading zero)
+            expect(compute('{report:startdate:HH}', mockContextWithDate)).toBe('15');
+            expect(compute('{report:startdate:H}', mockContextWithDate)).toBe('15');
+            expect(compute('{report:startdate:G}', mockContextWithDate)).toBe('15');
+
+            setupMockDate(morningDate);
+            expect(compute('{report:startdate:HH}', mockContextWithDate)).toBe('09');
+            expect(compute('{report:startdate:H}', mockContextWithDate)).toBe('09'); // H has leading zeros per spec
+            expect(compute('{report:startdate:G}', mockContextWithDate)).toBe('9'); // G has NO leading zeros
+
+            // 12-hour format: 3:30 PM (afternoon) and 9:05 AM (morning)
+            setupMockDate(testDate);
+            expect(compute('{report:startdate:hh}', mockContextWithDate)).toBe('03');
+            expect(compute('{report:startdate:h}', mockContextWithDate)).toBe('03'); // h has leading zeros per spec
+            expect(compute('{report:startdate:g}', mockContextWithDate)).toBe('3'); // g has NO leading zeros
+
+            setupMockDate(morningDate);
+            expect(compute('{report:startdate:hh}', mockContextWithDate)).toBe('09');
+            expect(compute('{report:startdate:h}', mockContextWithDate)).toBe('09'); // h has leading zeros per spec
+            expect(compute('{report:startdate:g}', mockContextWithDate)).toBe('9'); // g has NO leading zeros
+        });
+
+        test('time formats - minutes and seconds', () => {
+            // Minutes: 30 (double digit) and 05 (single digit with leading zero)
+            setupMockDate(testDate);
+            expect(compute('{report:startdate:mm}', mockContextWithDate)).toBe('30');
+            expect(compute('{report:startdate:i}', mockContextWithDate)).toBe('30');
+
+            setupMockDate(morningDate);
+            expect(compute('{report:startdate:mm}', mockContextWithDate)).toBe('05');
+            expect(compute('{report:startdate:i}', mockContextWithDate)).toBe('05');
+
+            // Seconds: 45 (double digit) and 02 (single digit with leading zero)
+            setupMockDate(testDate);
+            expect(compute('{report:startdate:ss}', mockContextWithDate)).toBe('45');
+            expect(compute('{report:startdate:s}', mockContextWithDate)).toBe('45');
+
+            setupMockDate(morningDate);
+            expect(compute('{report:startdate:ss}', mockContextWithDate)).toBe('02');
+            expect(compute('{report:startdate:s}', mockContextWithDate)).toBe('02');
+        });
+
+        test('time formats - AM/PM', () => {
+            expect(compute('{report:startdate:tt}', mockContextWithDate)).toBe('PM');
+            expect(compute('{report:startdate:A}', mockContextWithDate)).toBe('PM');
+            expect(compute('{report:startdate:a}', mockContextWithDate)).toBe('pm');
+        });
+
+        test('full date/time formats - c, r, U', () => {
+            // ISO 8601 format (c token)
+            const cResult = compute('{report:startdate:c}', mockContextWithDate);
+            expect(cResult).toBe('2025-01-08T15:30:45.123Z');
+
+            // RFC 2822 format (r token)
+            const rResult = compute('{report:startdate:r}', mockContextWithDate);
+            expect(rResult).toMatch(/^Wed, 08 Jan 2025 \d{2}:\d{2}:\d{2} [+-]\d{4}$/);
+
+            // Unix timestamp (U token)
+            const uResult = compute('{report:startdate:U}', mockContextWithDate);
+            const expectedTimestamp = Math.floor(new Date(testDate).getTime() / 1000).toString();
+            expect(uResult).toBe(expectedTimestamp);
+        });
+
+        test('format strings with colons', () => {
+            expect(compute('{report:startdate:HH:mm}', mockContextWithDate)).toBe('15:30');
+            expect(compute('{report:startdate:HH:mm:ss}', mockContextWithDate)).toBe('15:30:45');
+            expect(compute('{report:startdate:hh:mm tt}', mockContextWithDate)).toBe('03:30 PM');
+            expect(compute('{report:startdate:yyyy-MM-dd HH:mm:ss}', mockContextWithDate)).toBe('2025-01-08 15:30:45');
+            expect(compute('{report:created:HH:mm:ss}', mockContextWithDate)).toBe('15:30:45');
+            expect(compute('{report:startdate:g:i a}', mockContextWithDate)).toBe('3:30 pm');
+        });
+    });
+
+    describe('Submission Info', () => {
+        const mockSubmitter: PersonalDetails = {
+            accountID: 12345,
+            firstName: 'John',
+            lastName: 'Doe',
+            displayName: 'John Doe',
+            login: 'john.doe@company.com',
+        };
+
+        const mockManager: PersonalDetails = {
+            accountID: 67890,
+            firstName: 'Jane',
+            lastName: 'Smith',
+            displayName: 'Jane Smith',
+            login: 'jane.smith@company.com',
+        };
+
+        const mockContextWithSubmissionInfo: FormulaContext = {
+            report: {
+                reportID: '123',
+                reportName: '',
+                type: 'expense',
+                ownerAccountID: 12345,
+                managerID: 67890,
+                stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+                statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+                created: '2025-01-15T10:30:00Z',
+            } as Report,
+            policy: {
+                name: 'Test Policy',
+                employeeList: {
+                    // eslint-disable-next-line @typescript-eslint/naming-convention
+                    'john.doe@company.com': {
+                        email: 'john.doe@company.com',
+                        employeeUserID: 'EMP001',
+                        employeePayrollID: 'PAY123',
+                    },
+                    // eslint-disable-next-line @typescript-eslint/naming-convention
+                    'jane.smith@company.com': {
+                        email: 'jane.smith@company.com',
+                        employeeUserID: 'EMP002',
+                        employeePayrollID: 'PAY456',
+                    },
+                },
+            } as unknown as Policy,
+            submitterPersonalDetails: mockSubmitter,
+            managerPersonalDetails: mockManager,
+        };
+
+        beforeEach(() => {
+            jest.clearAllMocks();
+
+            const mockReportActions = {
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                '1': {
+                    reportActionID: '1',
+                    created: '2025-01-10T08:00:00Z',
+                    actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
+                },
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                '2': {
+                    reportActionID: '2',
+                    created: '2025-01-15T10:30:00Z',
+                    actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED,
+                },
+            } as unknown as ReportActions;
+
+            mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions);
+        });
+
+        describe('Submitter information', () => {
+            test('firstname - basic submitter first name', () => {
+                expect(compute('{report:submit:from:firstname}', mockContextWithSubmissionInfo)).toBe('John');
+            });
+
+            test('lastname - basic submitter last name', () => {
+                expect(compute('{report:submit:from:lastname}', mockContextWithSubmissionInfo)).toBe('Doe');
+            });
+
+            test('fullname - basic submitter full name', () => {
+                expect(compute('{report:submit:from:fullname}', mockContextWithSubmissionInfo)).toBe('John Doe');
+            });
+
+            test('email - basic submitter email', () => {
+                expect(compute('{report:submit:from:email}', mockContextWithSubmissionInfo)).toBe('john.doe@company.com');
+            });
+
+            test('userid - submitter employee user ID (alias for customfield1)', () => {
+                expect(compute('{report:submit:from:userid}', mockContextWithSubmissionInfo)).toBe('EMP001');
+            });
+
+            test('customfield1 - submitter employee user ID from policy', () => {
+                expect(compute('{report:submit:from:customfield1}', mockContextWithSubmissionInfo)).toBe('EMP001');
+            });
+
+            test('customfield2 - submitter employee payroll ID from policy', () => {
+                expect(compute('{report:submit:from:customfield2}', mockContextWithSubmissionInfo)).toBe('PAY123');
+            });
+
+            test('payrollid - submitter employee payroll ID (alias for customfield2)', () => {
+                expect(compute('{report:submit:from:payrollid}', mockContextWithSubmissionInfo)).toBe('PAY123');
+            });
+
+            test('name fields fall back to email when name missing', () => {
+                const contextWithPartialDetails: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: null as unknown as Policy,
+                    submitterPersonalDetails: {
+                        accountID: 111,
+                        login: 'fallback@email.com',
+                    } as PersonalDetails,
+                };
+
+                expect(compute('{report:submit:from:firstname}', contextWithPartialDetails)).toBe('fallback@email.com');
+                expect(compute('{report:submit:from:lastname}', contextWithPartialDetails)).toBe('fallback@email.com');
+                expect(compute('{report:submit:from:fullname}', contextWithPartialDetails)).toBe('fallback@email.com');
+            });
+
+            test('customfield1 - return empty when employeeList missing', () => {
+                const contextWithoutEmployeeList: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: {
+                        name: 'Test Policy',
+                    } as Policy,
+                    submitterPersonalDetails: mockSubmitter,
+                };
+
+                expect(compute('{report:submit:from:customfield1}', contextWithoutEmployeeList)).toBe('');
+            });
+
+            test('customfield2 - return empty when employeeList missing', () => {
+                const contextWithoutEmployeeList: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: {
+                        name: 'Test Policy',
+                    } as Policy,
+                    submitterPersonalDetails: mockSubmitter,
+                };
+
+                expect(compute('{report:submit:from:customfield2}', contextWithoutEmployeeList)).toBe('');
+            });
+
+            test('customfield1 - return empty when user not in employeeList', () => {
+                const contextWithDifferentEmployee: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: {
+                        name: 'Test Policy',
+                        employeeList: {
+                            // eslint-disable-next-line @typescript-eslint/naming-convention
+                            'other.user@company.com': {
+                                email: 'other.user@company.com',
+                                employeeUserID: 'EMP999',
+                            },
+                        },
+                    } as unknown as Policy,
+                    submitterPersonalDetails: mockSubmitter,
+                };
+
+                expect(compute('{report:submit:from:customfield1}', contextWithDifferentEmployee)).toBe('');
+            });
+        });
+
+        describe('Manager information', () => {
+            test('firstname - basic manager first name', () => {
+                expect(compute('{report:submit:to:firstname}', mockContextWithSubmissionInfo)).toBe('Jane');
+            });
+
+            test('lastname - basic manager last name', () => {
+                expect(compute('{report:submit:to:lastname}', mockContextWithSubmissionInfo)).toBe('Smith');
+            });
+
+            test('fullname - basic manager full name', () => {
+                expect(compute('{report:submit:to:fullname}', mockContextWithSubmissionInfo)).toBe('Jane Smith');
+            });
+
+            test('email - basic manager email', () => {
+                expect(compute('{report:submit:to:email}', mockContextWithSubmissionInfo)).toBe('jane.smith@company.com');
+            });
+
+            test('userid - manager employee user ID (alias for customfield1)', () => {
+                expect(compute('{report:submit:to:userid}', mockContextWithSubmissionInfo)).toBe('EMP002');
+            });
+
+            test('customfield1 - manager employee user ID from policy', () => {
+                expect(compute('{report:submit:to:customfield1}', mockContextWithSubmissionInfo)).toBe('EMP002');
+            });
+
+            test('customfield2 - manager employee payroll ID from policy', () => {
+                expect(compute('{report:submit:to:customfield2}', mockContextWithSubmissionInfo)).toBe('PAY456');
+            });
+
+            test('payrollid - manager employee payroll ID (alias for customfield2)', () => {
+                expect(compute('{report:submit:to:payrollid}', mockContextWithSubmissionInfo)).toBe('PAY456');
+            });
+
+            test('firstname - fall back to email when manager name missing', () => {
+                const contextWithPartialManagerDetails: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: null as unknown as Policy,
+                    managerPersonalDetails: {
+                        accountID: 222,
+                        login: 'manager@email.com',
+                    } as PersonalDetails,
+                };
+
+                expect(compute('{report:submit:to:firstname}', contextWithPartialManagerDetails)).toBe('manager@email.com');
+            });
+
+            test('fullname - fall back to email when manager displayName missing', () => {
+                const contextWithPartialManagerDetails: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: null as unknown as Policy,
+                    managerPersonalDetails: {
+                        accountID: 222,
+                        login: 'manager@email.com',
+                    } as PersonalDetails,
+                };
+
+                expect(compute('{report:submit:to:fullname}', contextWithPartialManagerDetails)).toBe('manager@email.com');
+            });
+        });
+
+        describe('Submission date', () => {
+            test('default format - yyyy-MM-dd', () => {
+                expect(compute('{report:submit:date}', mockContextWithSubmissionInfo)).toBe('2025-01-15');
+            });
+
+            test('custom format - verifies date formatting works', () => {
+                expect(compute('{report:submit:date:MMMM dd, yyyy}', mockContextWithSubmissionInfo)).toBe('January 15, 2025');
+            });
+        });
+
+        describe('Function modifiers', () => {
+            test('frontpart - extract username from submitter email', () => {
+                expect(compute('{report:submit:from:email|frontpart}', mockContextWithSubmissionInfo)).toBe('john.doe');
+            });
+
+            test('domain - extract domain from submitter email', () => {
+                expect(compute('{report:submit:from:email|domain}', mockContextWithSubmissionInfo)).toBe('company.com');
+            });
+
+            test('frontpart - extract username from manager email', () => {
+                expect(compute('{report:submit:to:email|frontpart}', mockContextWithSubmissionInfo)).toBe('jane.smith');
+            });
+
+            test('substr - extract first 4 characters from fullname', () => {
+                expect(compute('{report:submit:from:fullname|substr:0:4}', mockContextWithSubmissionInfo)).toBe('John');
+            });
+
+            test('chained modifiers - frontpart then substr on email', () => {
+                expect(compute('{report:submit:from:email|frontpart|substr:0:4}', mockContextWithSubmissionInfo)).toBe('john');
+            });
+        });
+
+        describe('Combined formulas', () => {
+            test('submitter and manager names together', () => {
+                expect(compute('{report:submit:from:firstname} -> {report:submit:to:firstname}', mockContextWithSubmissionInfo)).toBe('John -> Jane');
+            });
+
+            test('transaction date range with submission date', () => {
+                const mockTransactions = [
+                    {
+                        transactionID: 'trans1',
+                        created: '2025-01-08T12:00:00Z',
+                        amount: 5000,
+                        merchant: 'Store',
+                    },
+                ] as Transaction[];
+
+                mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions);
+
+                expect(compute('{report:startdate} to {report:submit:date}', mockContextWithSubmissionInfo)).toBe('2025-01-08 to 2025-01-15');
+            });
+        });
+
+        describe('Edge cases', () => {
+            test('empty email - return empty when email empty', () => {
+                const contextWithEmptyEmail: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: null as unknown as Policy,
+                    submitterPersonalDetails: {
+                        accountID: 123,
+                        login: '',
+                    } as PersonalDetails,
+                };
+
+                expect(compute('{report:submit:from:email}', contextWithEmptyEmail)).toBe('');
+            });
+
+            test('empty email with name - return empty when name also empty', () => {
+                const contextWithEmptyEmail: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: null as unknown as Policy,
+                    submitterPersonalDetails: {
+                        accountID: 123,
+                        login: '',
+                    } as PersonalDetails,
+                };
+
+                expect(compute('{report:submit:from:firstname}', contextWithEmptyEmail)).toBe('');
+            });
+
+            test('empty firstname - fallback to email when firstname is empty string', () => {
+                const contextWithEmptyFirstName: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: null as unknown as Policy,
+                    submitterPersonalDetails: {
+                        accountID: 123,
+                        firstName: '',
+                        login: 'user@test.com',
+                    } as PersonalDetails,
+                };
+
+                expect(compute('{report:submit:from:firstname}', contextWithEmptyFirstName)).toBe('user@test.com');
+            });
+
+            test('empty lastname - fallback to email when lastname is empty string', () => {
+                const contextWithEmptyLastName: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: null as unknown as Policy,
+                    submitterPersonalDetails: {
+                        accountID: 123,
+                        lastName: '',
+                        login: 'user@test.com',
+                    } as PersonalDetails,
+                };
+
+                expect(compute('{report:submit:from:lastname}', contextWithEmptyLastName)).toBe('user@test.com');
+            });
+
+            test('empty displayName - fallback to email when displayName is empty string', () => {
+                const contextWithEmptyDisplayName: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: null as unknown as Policy,
+                    submitterPersonalDetails: {
+                        accountID: 123,
+                        displayName: '',
+                        login: 'user@test.com',
+                    } as PersonalDetails,
+                };
+
+                expect(compute('{report:submit:from:fullname}', contextWithEmptyDisplayName)).toBe('user@test.com');
+            });
+
+            test('empty email with frontpart - return empty for empty email modifier', () => {
+                const contextWithEmptyEmail: FormulaContext = {
+                    report: {reportID: '123'} as Report,
+                    policy: null as unknown as Policy,
+                    submitterPersonalDetails: {
+                        accountID: 123,
+                        login: '',
+                    } as PersonalDetails,
+                };
+
+                expect(compute('{report:submit:from:email|frontpart}', contextWithEmptyEmail)).toBe('');
+            });
+
+            test('unknown field - return empty for invalid field name', () => {
+                expect(compute('{report:submit:from:unknown}', mockContextWithSubmissionInfo)).toBe('');
+            });
+
+            test('invalid direction - return empty for invalid from/to', () => {
+                expect(compute('{report:submit:invalid:email}', mockContextWithSubmissionInfo)).toBe('');
+            });
+        });
     });
 
     describe('hasCircularReferences()', () => {

From 9a136ac7cf687d7c984b7b8e1a37330eaa7eafc7 Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Thu, 25 Dec 2025 17:06:26 +0700
Subject: [PATCH 13/14] fix: prettier

---
 src/libs/Formula.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts
index abf11d2c499e..ad4e1c812d56 100644
--- a/src/libs/Formula.ts
+++ b/src/libs/Formula.ts
@@ -198,7 +198,6 @@ function parsePart(definition: string): FormulaPart {
     return part;
 }
 
-
 /**
  * Check if the report field formula value is containing circular references, e.g example:  A -> A,  A->B->A,  A->B->C->A, etc
  */

From 6859b1cdb2eac19ab93a4b75f4da9710f63bd15f Mon Sep 17 00:00:00 2001
From: truph01 <truphd01@gmail.com>
Date: Tue, 30 Dec 2025 15:50:47 +0700
Subject: [PATCH 14/14] fix: bring back format test

---
 tests/unit/FormulaTest.ts | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts
index 375b49c63f11..c782fbb26397 100644
--- a/tests/unit/FormulaTest.ts
+++ b/tests/unit/FormulaTest.ts
@@ -460,6 +460,43 @@ describe('CustomFormula', () => {
                 jest.clearAllMocks();
             });
 
+            describe('Format options', () => {
+                test('nosymbol - should format without currency symbol', () => {
+                    const result = compute('{report:total:nosymbol}', currencyContext);
+                    expect(result).toBe('100.00');
+                });
+                test('same currency - should format normally (case insensitive)', () => {
+                    currencyContext.report.currency = 'EUR';
+                    expect(compute('{report:total:EUR}', currencyContext)).toBe('€100.00');
+                    expect(compute('{report:total:eur}', currencyContext)).toBe('€100.00');
+                });
+
+                test('default (no format) - should use report currency', () => {
+                    currencyContext.report.currency = 'NPR';
+                    const result = compute('{report:total}', currencyContext);
+                    expect(result).toBe('NPR\u00A0100.00');
+                });
+            });
+
+            describe('Currency conversion (requires backend)', () => {
+                test('different valid currencies - should return placeholder', () => {
+                    currencyContext.report.currency = 'USD';
+
+                    // Various currencies requiring conversion
+                    expect(compute('{report:total:EUR}', currencyContext)).toBe('{report:total:EUR}');
+                    expect(compute('{report:total:JPY}', currencyContext)).toBe('{report:total:JPY}');
+                });
+
+                test('case and whitespace handling - should normalize and detect conversion', () => {
+                    currencyContext.report.currency = 'USD';
+
+                    // Mixed case and whitespace
+                    expect(compute('{report:total:EuR}', currencyContext)).toBe('{report:total:EuR}');
+                    expect(compute('{report:total: EUR }', currencyContext)).toBe('{report:total: EUR }');
+                    expect(compute('{report:total:eur }', currencyContext)).toBe('{report:total:eur }');
+                });
+            });
+
             describe('Edge cases', () => {
                 test('undefined currency - should format without symbol', () => {
                     currencyContext.report.currency = undefined;
