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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,7 @@ const ONYXKEYS = {
CARD_FEED_ERRORS: 'cardFeedErrors',
TODOS: 'todos',
RAM_ONLY_SORTED_REPORT_ACTIONS: 'sortedReportActions',
OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID: 'openAndSubmittedReportsByPolicyID',
},

/** Stores HybridApp specific state required to interoperate with OldDot */
Expand Down Expand Up @@ -1528,6 +1529,7 @@ type OnyxDerivedValuesMapping = {
[ONYXKEYS.DERIVED.CARD_FEED_ERRORS]: OnyxTypes.CardFeedErrorsDerivedValue;
[ONYXKEYS.DERIVED.TODOS]: OnyxTypes.TodosDerivedValue;
[ONYXKEYS.DERIVED.RAM_ONLY_SORTED_REPORT_ACTIONS]: OnyxTypes.SortedReportActionsDerivedValue;
[ONYXKEYS.DERIVED.OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID]: OnyxTypes.OpenAndSubmittedReportsByPolicyIDDerivedValue;
};

type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;
Expand Down
146 changes: 73 additions & 73 deletions src/libs/ReportUtils.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {ValueOf} from 'type-fest';
import ONYXKEYS from '@src/ONYXKEYS';
import cardFeedErrorsConfig from './configs/cardFeedErrors';
import nonPersonalAndWorkspaceCardListConfig from './configs/nonPersonalAndWorkspaceCardList';
import openAndSubmittedReportsByPolicyIDConfig from './configs/openAndSubmittedReportsByPolicyID';
import outstandingReportsByPolicyIDConfig from './configs/outstandingReportsByPolicyID';
import personalAndWorkspaceCardListConfig from './configs/personalAndWorkspaceCardList';
import reportAttributesConfig from './configs/reportAttributes';
Expand All @@ -25,6 +26,7 @@ const ONYX_DERIVED_VALUES = {
[ONYXKEYS.DERIVED.CARD_FEED_ERRORS]: cardFeedErrorsConfig,
[ONYXKEYS.DERIVED.TODOS]: todosConfig,
[ONYXKEYS.DERIVED.RAM_ONLY_SORTED_REPORT_ACTIONS]: sortedReportActionsConfig,
[ONYXKEYS.DERIVED.OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID]: openAndSubmittedReportsByPolicyIDConfig,
} as const satisfies {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[Key in ValueOf<typeof ONYXKEYS.DERIVED>]: OnyxDerivedValueConfig<Key, any>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig';
import ONYXKEYS from '@src/ONYXKEYS';
import type {OpenAndSubmittedReportsByPolicyIDDerivedValue} from '@src/types/onyx';

export default createOnyxDerivedValueConfig({
key: ONYXKEYS.DERIVED.OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID,
dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.SESSION],
compute: ([reports, session]) => {
if (!reports) {
return {};
}

const currentUserAccountID = session?.accountID;

return Object.entries(reports).reduce<OpenAndSubmittedReportsByPolicyIDDerivedValue>((acc, [reportID, report]) => {
if (!report) {
return acc;
}

// Get all reports, which are the ones that are:
// - Owned by the current user
// - Are either open or submitted
// - Belong to a workspace
if (report.policyID && report.ownerAccountID === currentUserAccountID && (report.stateNum ?? 0) <= 1) {
Comment thread
truph01 marked this conversation as resolved.
if (!acc[report.policyID]) {
acc[report.policyID] = {};
}

acc[report.policyID] = {
...acc[report.policyID],
[reportID]: report,
};
}

return acc;
}, {});
},
});
10 changes: 10 additions & 0 deletions src/types/onyx/DerivedValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ type ReportTransactionsAndViolationsDerivedValue = Record<string, ReportTransact
*/
type OutstandingReportsByPolicyIDDerivedValue = Record<string, OnyxCollection<Report>>;

/**
* The derived value for reports grouped by policy ID.
* Groups reports by their policyID where:
* - The report has a policyID
* - The report is owned by the current user
* - The report state is open or submitted (stateNum <= 1)
Comment thread
truph01 marked this conversation as resolved.
*/
type OpenAndSubmittedReportsByPolicyIDDerivedValue = Record<string, OnyxCollection<Report>>;

/**
* The derived value for visible report actions.
*/
Expand Down Expand Up @@ -283,6 +292,7 @@ export type {
ReportTransactionsAndViolationsDerivedValue,
ReportTransactionsAndViolations,
OutstandingReportsByPolicyIDDerivedValue,
OpenAndSubmittedReportsByPolicyIDDerivedValue,
VisibleReportActionsDerivedValue,
SortedReportActionsDerivedValue,
NonPersonalAndWorkspaceCardListDerivedValue,
Expand Down
2 changes: 2 additions & 0 deletions src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type CustomStatusDraft from './CustomStatusDraft';
import type {
CardFeedErrorsDerivedValue,
NonPersonalAndWorkspaceCardListDerivedValue,
OpenAndSubmittedReportsByPolicyIDDerivedValue,
OutstandingReportsByPolicyIDDerivedValue,
PersonalAndWorkspaceCardListDerivedValue,
ReportAttributesDerivedValue,
Expand Down Expand Up @@ -366,6 +367,7 @@ export type {
LastSearchParams,
ReportTransactionsAndViolationsDerivedValue,
OutstandingReportsByPolicyIDDerivedValue,
OpenAndSubmittedReportsByPolicyIDDerivedValue,
VisibleReportActionsDerivedValue,
SortedReportActionsDerivedValue,
NonPersonalAndWorkspaceCardListDerivedValue,
Expand Down
31 changes: 10 additions & 21 deletions tests/unit/OnyxDerived/computeForReportTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {computeForReport} from '@libs/actions/OnyxDerived/configs/sortedReportAc
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Report, ReportAction, ReportActions} from '@src/types/onyx';
import {createMockReport} from '../../utils/ReportTestUtils';

function createAction(id: string, created: string, overrides: Partial<ReportAction> = {}): ReportAction {
return {
Expand All @@ -21,18 +22,6 @@ function createAction(id: string, created: string, overrides: Partial<ReportActi
} as ReportAction;
}

function createReport(reportID: string, overrides: Partial<Report> = {}): Report {
return {
reportID,
reportName: `Report ${reportID}`,
type: CONST.REPORT.TYPE.CHAT,
chatType: undefined,
ownerAccountID: 1,
isPinned: false,
...overrides,
} as Report;
}

function toReportActions(...actions: ReportAction[]): ReportActions {
const result: ReportActions = {};
for (const action of actions) {
Expand All @@ -52,7 +41,7 @@ describe('computeForReport', () => {
const action3 = createAction('3', '2024-01-03 10:00:00.000');
const actions = toReportActions(action1, action2, action3);
const allReportActions: OnyxCollection<ReportActions> = {[reportActionsKey]: actions};
const allReports: OnyxCollection<Report> = {[reportKey]: createReport(reportID)};
const allReports: OnyxCollection<Report> = {[reportKey]: createMockReport({reportID})};

const result = computeForReport(reportID, actions, allReportActions, allReports);

Expand All @@ -65,7 +54,7 @@ describe('computeForReport', () => {
const action3 = createAction('3', '2024-01-02 10:00:00.000');
const actions = toReportActions(action1, action2, action3);
const allReportActions: OnyxCollection<ReportActions> = {[reportActionsKey]: actions};
const allReports: OnyxCollection<Report> = {[reportKey]: createReport(reportID)};
const allReports: OnyxCollection<Report> = {[reportKey]: createMockReport({reportID})};

const result = computeForReport(reportID, actions, allReportActions, allReports);

Expand All @@ -75,7 +64,7 @@ describe('computeForReport', () => {
it('returns undefined lastAction for an empty actions object', () => {
const actions: ReportActions = {};
const allReportActions: OnyxCollection<ReportActions> = {[reportActionsKey]: actions};
const allReports: OnyxCollection<Report> = {[reportKey]: createReport(reportID)};
const allReports: OnyxCollection<Report> = {[reportKey]: createMockReport({reportID})};

const result = computeForReport(reportID, actions, allReportActions, allReports);

Expand All @@ -87,7 +76,7 @@ describe('computeForReport', () => {
const action1 = createAction('1', '2024-01-01 10:00:00.000');
const actions = toReportActions(action1);
const allReportActions: OnyxCollection<ReportActions> = {[reportActionsKey]: actions};
const allReports: OnyxCollection<Report> = {[reportKey]: createReport(reportID, {type: CONST.REPORT.TYPE.CHAT})};
const allReports: OnyxCollection<Report> = {[reportKey]: createMockReport({reportID, type: CONST.REPORT.TYPE.CHAT})};

const result = computeForReport(reportID, actions, allReportActions, allReports);

Expand All @@ -114,8 +103,8 @@ describe('computeForReport', () => {
const threadAction200 = createAction('200', '2024-01-01 11:00:00.000');
const threadAction201 = createAction('201', '2024-01-01 12:00:00.000');
const threadActions = toReportActions(threadAction200, threadAction201);
const chatReport = createReport('3', {type: CONST.REPORT.TYPE.CHAT});
const expenseReport = createReport(reportID, {type: CONST.REPORT.TYPE.EXPENSE, chatReportID: '3'});
const chatReport = createMockReport({reportID: '3', type: CONST.REPORT.TYPE.CHAT});
const expenseReport = createMockReport({reportID, type: CONST.REPORT.TYPE.EXPENSE, chatReportID: '3'});

const allReportActions: OnyxCollection<ReportActions> = {
[reportActionsKey]: parentActions,
Expand All @@ -141,7 +130,7 @@ describe('computeForReport', () => {
const action1 = createAction('1', '2024-06-15 08:30:00.000');
const actions = toReportActions(action1);
const allReportActions: OnyxCollection<ReportActions> = {[reportActionsKey]: actions};
const allReports: OnyxCollection<Report> = {[reportKey]: createReport(reportID)};
const allReports: OnyxCollection<Report> = {[reportKey]: createMockReport({reportID})};

const result = computeForReport(reportID, actions, allReportActions, allReports);

Expand All @@ -153,8 +142,8 @@ describe('computeForReport', () => {
it('handles undefined allReportActions gracefully for transaction thread merging', () => {
const action1 = createAction('1', '2024-01-01 10:00:00.000');
const actions = toReportActions(action1);
const expenseReport = createReport(reportID, {type: CONST.REPORT.TYPE.EXPENSE, chatReportID: '3'});
const chatReport = createReport('3', {type: CONST.REPORT.TYPE.CHAT});
const expenseReport = createMockReport({reportID, type: CONST.REPORT.TYPE.EXPENSE, chatReportID: '3'});
const chatReport = createMockReport({reportID: '3', type: CONST.REPORT.TYPE.CHAT});
const allReports: OnyxCollection<Report> = {
[reportKey]: expenseReport,
[`${ONYXKEYS.COLLECTION.REPORT}3`]: chatReport,
Expand Down
156 changes: 156 additions & 0 deletions tests/unit/OnyxDerived/openAndSubmittedReportsByPolicyIDTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import type {OnyxCollection} from 'react-native-onyx';
import openAndSubmittedReportsByPolicyIDConfig from '@libs/actions/OnyxDerived/configs/openAndSubmittedReportsByPolicyID';
import type {DerivedValueContext} from '@libs/actions/OnyxDerived/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Report, Session} from '@src/types/onyx';
import {createMockReport} from '../../utils/ReportTestUtils';

const CURRENT_USER_ACCOUNT_ID = 1;
const OTHER_USER_ACCOUNT_ID = 2;
const POLICY_ID_1 = 'policy1';
const POLICY_ID_2 = 'policy2';

function createReport(reportID: string, overrides: Partial<Report> = {}): Report {
return createMockReport({
reportID,
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
policyID: POLICY_ID_1,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
...overrides,
});
}

function buildReports(...reports: Report[]): OnyxCollection<Report> {
const result: OnyxCollection<Report> = {};
for (const report of reports) {
result[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = report;
}
return result;
}

function createSession(accountID: number = CURRENT_USER_ACCOUNT_ID): Session {
return {accountID} as Session;
}

const {compute} = openAndSubmittedReportsByPolicyIDConfig;
const emptyContext = {} as DerivedValueContext<typeof ONYXKEYS.DERIVED.OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID, [typeof ONYXKEYS.COLLECTION.REPORT, typeof ONYXKEYS.SESSION]>;

describe('openAndSubmittedReportsByPolicyID derived value', () => {
it('returns empty object when reports is null/undefined', () => {
const result = compute([undefined, createSession()], emptyContext);
expect(result).toEqual({});
});

it('returns empty object when reports is empty', () => {
const result = compute([{}, createSession()], emptyContext);
expect(result).toEqual({});
});

it('groups open reports owned by current user by policyID', () => {
const report1 = createReport('1', {policyID: POLICY_ID_1});
const report2 = createReport('2', {policyID: POLICY_ID_2});
const reports = buildReports(report1, report2);

const result = compute([reports, createSession()], emptyContext);

expect(Object.keys(result)).toHaveLength(2);
expect(result[POLICY_ID_1]).toEqual({[`${ONYXKEYS.COLLECTION.REPORT}1`]: report1});
expect(result[POLICY_ID_2]).toEqual({[`${ONYXKEYS.COLLECTION.REPORT}2`]: report2});
});

it('groups multiple reports under the same policyID', () => {
const report1 = createReport('1', {policyID: POLICY_ID_1});
const report2 = createReport('2', {policyID: POLICY_ID_1});
const reports = buildReports(report1, report2);

const result = compute([reports, createSession()], emptyContext);

expect(Object.keys(result)).toHaveLength(1);
expect(result[POLICY_ID_1]).toEqual({
[`${ONYXKEYS.COLLECTION.REPORT}1`]: report1,
[`${ONYXKEYS.COLLECTION.REPORT}2`]: report2,
});
});

it('excludes reports not owned by current user', () => {
const ownReport = createReport('1', {ownerAccountID: CURRENT_USER_ACCOUNT_ID});
const otherReport = createReport('2', {ownerAccountID: OTHER_USER_ACCOUNT_ID});
const reports = buildReports(ownReport, otherReport);

const result = compute([reports, createSession()], emptyContext);

expect(Object.keys(result)).toHaveLength(1);
expect(result[POLICY_ID_1]).toEqual({[`${ONYXKEYS.COLLECTION.REPORT}1`]: ownReport});
});

it('excludes reports without a policyID', () => {
const reportWithPolicy = createReport('1', {policyID: POLICY_ID_1});
const reportWithoutPolicy = createReport('2', {policyID: undefined});
const reports = buildReports(reportWithPolicy, reportWithoutPolicy);

const result = compute([reports, createSession()], emptyContext);

expect(Object.keys(result)).toHaveLength(1);
expect(result[POLICY_ID_1]).toEqual({[`${ONYXKEYS.COLLECTION.REPORT}1`]: reportWithPolicy});
});

it('includes open reports (stateNum = 0)', () => {
const report = createReport('1', {stateNum: CONST.REPORT.STATE_NUM.OPEN});
const reports = buildReports(report);

const result = compute([reports, createSession()], emptyContext);

expect(result[POLICY_ID_1]).toEqual({[`${ONYXKEYS.COLLECTION.REPORT}1`]: report});
});

it('includes submitted reports (stateNum = 1)', () => {
const report = createReport('1', {stateNum: CONST.REPORT.STATE_NUM.SUBMITTED});
const reports = buildReports(report);

const result = compute([reports, createSession()], emptyContext);

expect(result[POLICY_ID_1]).toEqual({[`${ONYXKEYS.COLLECTION.REPORT}1`]: report});
});

it('excludes approved reports (stateNum = 2)', () => {
const report = createReport('1', {stateNum: CONST.REPORT.STATE_NUM.APPROVED});
const reports = buildReports(report);

const result = compute([reports, createSession()], emptyContext);

expect(result).toEqual({});
});

it('treats undefined stateNum as open (defaults to 0)', () => {
const report = createReport('1', {stateNum: undefined});
const reports = buildReports(report);

const result = compute([reports, createSession()], emptyContext);

expect(result[POLICY_ID_1]).toEqual({[`${ONYXKEYS.COLLECTION.REPORT}1`]: report});
});

it('skips null report entries in collection', () => {
const report = createReport('1');
const reports = buildReports(report);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
reports![`${ONYXKEYS.COLLECTION.REPORT}2`] = undefined;

const result = compute([reports, createSession()], emptyContext);

expect(Object.keys(result)).toHaveLength(1);
expect(result[POLICY_ID_1]).toEqual({[`${ONYXKEYS.COLLECTION.REPORT}1`]: report});
});

it('returns empty object when session has no accountID', () => {
const report = createReport('1');
const reports = buildReports(report);

const result = compute([reports, {} as Session], emptyContext);

expect(result).toEqual({});
});
});
2 changes: 1 addition & 1 deletion tests/unit/ReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12136,7 +12136,7 @@ describe('ReportUtils', () => {
iouReportID: expenseReportID,
};

// Closed/approved report — stateNum > 1, so it won't be in reportsByPolicyID
// Closed/approved report — stateNum > 1, so it won't be in openAndSubmittedReportsByPolicyID
// and won't pass isOpenOrProcessingReport
const expenseReport: Report = {
...createExpenseReport(806),
Expand Down
Loading