Skip to content

Commit c0483a8

Browse files
authored
Merge pull request Expensify#96506 from callstack-internal/pmr-fail-on-server-commit
fix: positively recognize "already paid" server rejection
2 parents ab1e5b3 + 2efb156 commit c0483a8

3 files changed

Lines changed: 52 additions & 1 deletion

File tree

src/CONST/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2515,6 +2515,7 @@ const CONST = {
25152515
SOCKET: 'Issue connecting to database',
25162516
DUPLICATE_RECORD: '400 Unique Constraints Violation',
25172517
ALREADY_CREATED_TRANSACTION: 'Transaction already created.',
2518+
ALREADY_PAID: 'The request has already been paid',
25182519
},
25192520
NETWORK: {
25202521
METHOD: {

src/libs/HttpUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const addSkewList = new Set<string>([WRITE_COMMANDS.OPEN_REPORT, SIDE_EFFECT_REQ
5757
* Per-command server response messages we recognize as the PHP-wrapped "AlreadyCreated" error.
5858
* Add new variants here as we discover them for other non-idempotent commands.
5959
*/
60-
const ALREADY_CREATED_MESSAGES = new Set<string>([CONST.ERROR_TITLE.ALREADY_CREATED_TRANSACTION]);
60+
const ALREADY_CREATED_MESSAGES = new Set<string>([CONST.ERROR_TITLE.ALREADY_CREATED_TRANSACTION, CONST.ERROR_TITLE.ALREADY_PAID]);
6161

6262
/**
6363
* Regex to get API command from the command

tests/unit/HttpUtilsTest.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {WRITE_COMMANDS} from '@libs/API/types';
2+
3+
import CONST from '@src/CONST';
4+
import ONYXKEYS from '@src/ONYXKEYS';
5+
6+
import Onyx from 'react-native-onyx';
7+
8+
import HttpUtils from '../../src/libs/HttpUtils';
9+
10+
function mockFetchResponse(message: string) {
11+
global.fetch = jest.fn().mockResolvedValue({
12+
ok: true,
13+
status: 200,
14+
headers: {get: () => null},
15+
json: () => Promise.resolve({jsonCode: CONST.JSON_CODE.EXP_ERROR, message}),
16+
});
17+
}
18+
19+
beforeAll(() => {
20+
Onyx.init({
21+
keys: ONYXKEYS,
22+
});
23+
});
24+
25+
afterEach(() => {
26+
jest.restoreAllMocks();
27+
});
28+
29+
describe('HttpUtils', () => {
30+
// The mapping is keyed on the server message alone, not the command. The messages are
31+
// pinned as literals so a change to the CONST values can't silently drift from what the
32+
// server really sends.
33+
it.each([
34+
['Transaction already created.', WRITE_COMMANDS.REQUEST_MONEY],
35+
['The request has already been paid', WRITE_COMMANDS.PAY_MONEY_REQUEST],
36+
])('maps the jsonCode-666 rejection "%s" to ALREADY_CREATED', async (message, command) => {
37+
mockFetchResponse(message);
38+
39+
await expect(HttpUtils.xhr(command, {})).rejects.toMatchObject({
40+
message: CONST.ERROR.ALREADY_CREATED,
41+
title: message,
42+
});
43+
});
44+
45+
it('leaves a jsonCode-666 response with an unrecognized message untouched', async () => {
46+
mockFetchResponse('Some other error');
47+
48+
await expect(HttpUtils.xhr(WRITE_COMMANDS.PAY_MONEY_REQUEST, {})).resolves.toMatchObject({jsonCode: CONST.JSON_CODE.EXP_ERROR, message: 'Some other error'});
49+
});
50+
});

0 commit comments

Comments
 (0)