Skip to content

Commit e20b250

Browse files
authored
Merge pull request Expensify#85030 from nkdengineer/fix/83728
fix: Unexpected error when delete cropped receipt
2 parents c5195cc + 5f1af7f commit e20b250

4 files changed

Lines changed: 73 additions & 3 deletions

File tree

src/libs/TransactionUtils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ function isCreatedMissing(transaction: OnyxEntry<Transaction>) {
646646

647647
function areRequiredFieldsEmpty(transaction: OnyxEntry<Transaction>, transactionReport: OnyxEntry<Report>): boolean {
648648
const isFromExpenseReport = transactionReport?.type === CONST.REPORT.TYPE.EXPENSE;
649-
return (isFromExpenseReport && isMerchantMissing(transaction)) || isCreatedMissing(transaction);
649+
return (isFromExpenseReport && isMerchantMissing(transaction)) || isCreatedMissing(transaction) || (!isFromExpenseReport && getAmount(transaction) === 0);
650650
}
651651

652652
function getClearedPendingFields(transactionChanges: TransactionChanges) {

src/libs/actions/IOU/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ import type {OnyxData} from '@src/types/onyx/Request';
261261
import type {Comment, Receipt, ReceiptSource, Routes, SplitShares, TransactionChanges, TransactionCustomUnit, WaypointCollection} from '@src/types/onyx/Transaction';
262262
import type {FileObject} from '@src/types/utils/Attachment';
263263
import {isEmptyObject} from '@src/types/utils/EmptyObject';
264+
import {resolveDetachReceiptConflicts} from '../RequestConflictUtils';
264265

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

@@ -11548,7 +11549,14 @@ function detachReceipt(transactionID: string | undefined, transactionPolicy: Ony
1154811549

1154911550
const parameters: DetachReceiptParams = {transactionID, reportActionID: updatedReportAction.reportActionID};
1155011551

11551-
API.write(WRITE_COMMANDS.DETACH_RECEIPT, parameters, {optimisticData, successData, failureData});
11552+
API.write(
11553+
WRITE_COMMANDS.DETACH_RECEIPT,
11554+
parameters,
11555+
{optimisticData, successData, failureData},
11556+
{
11557+
checkAndFixConflictingRequest: (persistedRequests) => resolveDetachReceiptConflicts(persistedRequests, parameters),
11558+
},
11559+
);
1155211560
}
1155311561

1155411562
function replaceReceipt({transactionID, file, source, state, transactionPolicy, transactionPolicyCategories, isSameReceipt}: ReplaceReceipt) {

src/libs/actions/RequestConflictUtils.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {OnyxKey, OnyxUpdate} from 'react-native-onyx';
22
import Onyx from 'react-native-onyx';
33
import type {TupleToUnion} from 'type-fest';
4-
import type {OpenReportParams, UpdateCommentParams} from '@libs/API/parameters';
4+
import type {DetachReceiptParams, OpenReportParams, UpdateCommentParams} from '@libs/API/parameters';
55
import {WRITE_COMMANDS} from '@libs/API/types';
66
import type {ApiRequestCommandParameters} from '@libs/API/types';
77
import ONYXKEYS from '@src/ONYXKEYS';
@@ -227,6 +227,38 @@ function resolveEnableFeatureConflicts<TKey extends OnyxKey>(
227227
};
228228
}
229229

230+
function resolveDetachReceiptConflicts<TKey extends OnyxKey>(persistedRequests: Array<OnyxRequest<TKey>>, parameters: DetachReceiptParams): ConflictActionData {
231+
const indicesToDelete: number[] = [];
232+
for (const [index, request] of persistedRequests.entries()) {
233+
if (request.command !== WRITE_COMMANDS.REPLACE_RECEIPT || request.data?.transactionID !== parameters.transactionID) {
234+
continue;
235+
}
236+
indicesToDelete.push(index);
237+
}
238+
239+
// In the case the transaction doesn't have the receipt, remove all the replace receipt requests will make the detach receipt request invalid
240+
// So we should keep the last replace receipt request to ensure the detach receipt request is always valid
241+
if (indicesToDelete.length >= 1) {
242+
indicesToDelete.pop();
243+
}
244+
245+
if (indicesToDelete.length === 0) {
246+
return {
247+
conflictAction: {
248+
type: 'push',
249+
},
250+
};
251+
}
252+
253+
return {
254+
conflictAction: {
255+
type: 'delete',
256+
indices: indicesToDelete,
257+
pushNewRequest: true,
258+
},
259+
};
260+
}
261+
230262
export {
231263
resolveDuplicationConflictAction,
232264
resolveOpenReportDuplicationConflictAction,
@@ -235,6 +267,7 @@ export {
235267
createUpdateCommentMatcher,
236268
resolveEnableFeatureConflicts,
237269
enablePolicyFeatureCommand,
270+
resolveDetachReceiptConflicts,
238271
};
239272

240273
export type {EnablePolicyFeatureCommand, AnyRequestMatcher};

tests/unit/RequestConflictUtilsTest.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import Onyx from 'react-native-onyx';
22
import {
33
enablePolicyFeatureCommand,
44
resolveCommentDeletionConflicts,
5+
resolveDetachReceiptConflicts,
56
resolveDuplicationConflictAction,
67
resolveEditCommentWithNewAddCommentRequest,
78
resolveEnableFeatureConflicts,
89
} from '@libs/actions/RequestConflictUtils';
10+
import {WRITE_COMMANDS} from '@libs/API/types';
911
import type {WriteCommand} from '@libs/API/types';
1012

1113
describe('RequestConflictUtils', () => {
@@ -160,4 +162,31 @@ describe('RequestConflictUtils', () => {
160162
},
161163
});
162164
});
165+
166+
describe('resolveDetachReceiptConflicts', () => {
167+
it('returns push when no replace-receipt requests match transactionID', () => {
168+
const persistedRequests = [{command: 'OpenReport'}, {command: WRITE_COMMANDS.REPLACE_RECEIPT, data: {transactionID: '2'}}, {command: 'CloseAccount'}];
169+
const result = resolveDetachReceiptConflicts(persistedRequests, {transactionID: '1'} as never);
170+
expect(result).toEqual({conflictAction: {type: 'push'}});
171+
});
172+
173+
it('returns push when exactly one replace-receipt request matches transactionID', () => {
174+
const persistedRequests = [{command: WRITE_COMMANDS.REPLACE_RECEIPT, data: {transactionID: '1'}}];
175+
const result = resolveDetachReceiptConflicts(persistedRequests, {transactionID: '1'} as never);
176+
expect(result).toEqual({conflictAction: {type: 'push'}});
177+
});
178+
179+
it('deletes all but the last matching replace-receipt request and pushes new request', () => {
180+
const persistedRequests = [
181+
{command: WRITE_COMMANDS.REPLACE_RECEIPT, data: {transactionID: '1'}},
182+
{command: 'OpenReport'},
183+
{command: WRITE_COMMANDS.REPLACE_RECEIPT, data: {transactionID: '1'}},
184+
{command: WRITE_COMMANDS.REPLACE_RECEIPT, data: {transactionID: '2'}},
185+
{command: WRITE_COMMANDS.REPLACE_RECEIPT, data: {transactionID: '1'}},
186+
];
187+
188+
const result = resolveDetachReceiptConflicts(persistedRequests, {transactionID: '1'} as never);
189+
expect(result).toEqual({conflictAction: {type: 'delete', indices: [0, 2], pushNewRequest: true}});
190+
});
191+
});
163192
});

0 commit comments

Comments
 (0)