From 320b302b89b2385aeda3723890df36b12345e81d Mon Sep 17 00:00:00 2001
From: daledah <daleda261@outlook.com>
Date: Wed, 2 Jul 2025 00:20:51 +0700
Subject: [PATCH 1/4] fix: DRY resolving duplicate request

---
 src/libs/API/index.ts                    | 58 +++++++++++++++++++++++-
 src/libs/actions/App.ts                  |  9 +---
 src/libs/actions/Policy/Category.ts      |  5 +-
 src/libs/actions/Policy/DistanceRate.ts  |  5 +-
 src/libs/actions/Policy/Policy.ts        | 33 ++++----------
 src/libs/actions/Policy/Tag.ts           |  5 +-
 src/libs/actions/Report.ts               |  7 +--
 src/libs/actions/RequestConflictUtils.ts |  2 +
 src/libs/actions/User.ts                 | 10 +---
 9 files changed, 75 insertions(+), 59 deletions(-)

diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index 16405e45ff9d..62d53f395c06 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -1,6 +1,8 @@
 import type {OnyxUpdate} from 'react-native-onyx';
 import Onyx from 'react-native-onyx';
 import type {SetRequired} from 'type-fest';
+import {resolveDuplicationConflictAction, resolveEnableFeatureConflicts} from '@libs/actions/RequestConflictUtils';
+import type {EnablePolicyFeatureCommand, RequestMatcher} from '@libs/actions/RequestConflictUtils';
 import Log from '@libs/Log';
 import {handleDeletedAccount, HandleUnusedOptimisticID, Logging, Pagination, Reauthentication, RecheckConnection, SaveResponseInOnyx} from '@libs/Middleware';
 import {isOffline} from '@libs/Network/NetworkStore';
@@ -157,6 +159,60 @@ function write<TCommand extends WriteCommand>(
     return processRequest(request, CONST.API_REQUEST_TYPE.WRITE);
 }
 
+/**
+ * This function is used to write data to the API while ensuring that there are no duplicate requests in the queue.
+ * If a duplicate request is found, it resolves the conflict by replacing the duplicated request with the new one.
+ *
+ * @param command - Name of API command to call.
+ * @param apiCommandParameters - Parameters to send to the API.
+ * @param onyxData  - Object containing errors, loading states, and optimistic UI data that will be merged
+ *                             into Onyx before and after a request is made. Each nested object will be formatted in
+ *                             the same way as an API response.
+ * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
+ * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
+ * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
+ * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
+ * @param requestMatcher - Custom request matcher function for resolveDuplicationConflictAction.
+ */
+function writeWithNoDuplicatesConflictAction<TCommand extends WriteCommand>(
+    command: TCommand,
+    apiCommandParameters: ApiRequestCommandParameters[TCommand],
+    onyxData: OnyxData = {},
+    requestMatcher: RequestMatcher = (request) => request.command === command,
+): Promise<void | Response> {
+    const conflictResolver = {
+        checkAndFixConflictingRequest: (persistedRequests: OnyxRequest[]) => resolveDuplicationConflictAction(persistedRequests, requestMatcher),
+    };
+
+    return write(command, apiCommandParameters, onyxData, conflictResolver);
+}
+
+/**
+ * This function is used to write data to the API while ensuring that there are no conflicts with enabling policy features.
+ * If a conflict is found, it resolves the conflict by deleting the duplicated request.
+ *
+ * @param command - Name of API command to call.
+ * @param apiCommandParameters - Parameters to send to the API.
+ * @param onyxData  - Object containing errors, loading states, and optimistic UI data that will be merged
+ *                             into Onyx before and after a request is made. Each nested object will be formatted in
+ *                             the same way as an API response.
+ * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
+ * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
+ * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
+ * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
+ */
+function writeWithNoDuplicatesEnableFeatureConflicts<TCommand extends EnablePolicyFeatureCommand>(
+    command: TCommand,
+    apiCommandParameters: ApiRequestCommandParameters[TCommand],
+    onyxData: OnyxData = {},
+): Promise<void | Response> {
+    const conflictResolver = {
+        checkAndFixConflictingRequest: (persistedRequests: OnyxRequest[]) => resolveEnableFeatureConflicts(command, persistedRequests, apiCommandParameters),
+    };
+
+    return write(command, apiCommandParameters, onyxData, conflictResolver);
+}
+
 /**
  * For commands where the network response must be accessed directly or when there is functionality that can only
  * happen once the request is finished (eg. calling third-party services like Onfido and Plaid, redirecting a user
@@ -281,4 +337,4 @@ function paginate<TRequestType extends ApiRequestType, TCommand extends CommandO
     }
 }
 
-export {write, makeRequestWithSideEffects, read, paginate};
+export {write, makeRequestWithSideEffects, read, paginate, writeWithNoDuplicatesConflictAction, writeWithNoDuplicatesEnableFeatureConflicts};
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 017d10ded498..f2cacf0cc78f 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -33,7 +33,6 @@ import type {OnyxData} from '@src/types/onyx/Request';
 import {setShouldForceOffline} from './Network';
 import {getAll, rollbackOngoingRequest, save} from './PersistedRequests';
 import {createDraftInitialWorkspace, createWorkspace, generatePolicyID} from './Policy/Policy';
-import {resolveDuplicationConflictAction} from './RequestConflictUtils';
 import {isAnonymousUser} from './Session';
 import Timing from './Timing';
 
@@ -332,9 +331,7 @@ function getOnyxDataForOpenOrReconnect(isOpenApp = false, isFullReconnect = fals
 function openApp(shouldKeepPublicRooms = false) {
     return getPolicyParamsForOpenOrReconnect().then((policyParams: PolicyParamsForOpenOrReconnect) => {
         const params: OpenAppParams = {enablePriorityModeFilter: true, ...policyParams};
-        return API.write(WRITE_COMMANDS.OPEN_APP, params, getOnyxDataForOpenOrReconnect(true, undefined, shouldKeepPublicRooms), {
-            checkAndFixConflictingRequest: (persistedRequests) => resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.OPEN_APP),
-        });
+        return API.writeWithNoDuplicatesConflictAction(WRITE_COMMANDS.OPEN_APP, params, getOnyxDataForOpenOrReconnect(true, undefined, shouldKeepPublicRooms));
     });
 }
 
@@ -359,9 +356,7 @@ function reconnectApp(updateIDFrom: OnyxEntry<number> = 0) {
             }
 
             const isFullReconnect = !updateIDFrom;
-            API.write(WRITE_COMMANDS.RECONNECT_APP, params, getOnyxDataForOpenOrReconnect(false, isFullReconnect), {
-                checkAndFixConflictingRequest: (persistedRequests) => resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.RECONNECT_APP),
-            });
+            API.writeWithNoDuplicatesConflictAction(WRITE_COMMANDS.RECONNECT_APP, params, getOnyxDataForOpenOrReconnect(false, isFullReconnect, isSidebarLoaded));
         });
     });
 }
diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts
index 62f1f1c5ddbb..ea506ab923c3 100644
--- a/src/libs/actions/Policy/Category.ts
+++ b/src/libs/actions/Policy/Category.ts
@@ -30,7 +30,6 @@ import enhanceParameters from '@libs/Network/enhanceParameters';
 import {hasEnabledOptions} from '@libs/OptionsListUtils';
 import {getPolicy, goBackWhenEnableFeature} from '@libs/PolicyUtils';
 import {getAllPolicyReports, pushTransactionViolationsOnyxData} from '@libs/ReportUtils';
-import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils';
 import {getFinishOnboardingTaskOnyxData} from '@userActions/Task';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -1104,9 +1103,7 @@ function enablePolicyCategories(
 
     const parameters: EnablePolicyCategoriesParams = {policyID, enabled};
 
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, parameters, onyxData);
 
     if (enabled && getIsNarrowLayout() && shouldGoBack) {
         goBackWhenEnableFeature(policyID);
diff --git a/src/libs/actions/Policy/DistanceRate.ts b/src/libs/actions/Policy/DistanceRate.ts
index 0ef355c243b0..03c197d02ab2 100644
--- a/src/libs/actions/Policy/DistanceRate.ts
+++ b/src/libs/actions/Policy/DistanceRate.ts
@@ -15,7 +15,6 @@ import * as ErrorUtils from '@libs/ErrorUtils';
 import getIsNarrowLayout from '@libs/getIsNarrowLayout';
 import {getDistanceRateCustomUnit, goBackWhenEnableFeature, removePendingFieldsFromCustomUnit} from '@libs/PolicyUtils';
 import * as ReportUtils from '@libs/ReportUtils';
-import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx';
@@ -180,9 +179,7 @@ function enablePolicyDistanceRates(policyID: string, enabled: boolean) {
 
     const parameters: EnablePolicyDistanceRatesParams = {policyID, enabled};
 
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, parameters, onyxData);
 
     if (enabled && getIsNarrowLayout()) {
         goBackWhenEnableFeature(policyID);
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 1c61d7272fec..151328b9b06e 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -80,7 +80,6 @@ import * as ReportUtils from '@libs/ReportUtils';
 import type {PolicySelector} from '@pages/home/sidebar/FloatingActionButtonAndPopover';
 import * as PaymentMethods from '@userActions/PaymentMethods';
 import * as PersistedRequests from '@userActions/PersistedRequests';
-import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils';
 import {buildTaskData} from '@userActions/Task';
 import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow';
 import type {OnboardingCompanySize, OnboardingPurpose} from '@userActions/Welcome/OnboardingFlow';
@@ -3074,9 +3073,7 @@ function enablePolicyConnections(policyID: string, enabled: boolean) {
 
     const parameters: EnablePolicyConnectionsParams = {policyID, enabled};
 
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS, parameters, onyxData);
 
     if (enabled && getIsNarrowLayout()) {
         goBackWhenEnableFeature(policyID);
@@ -3133,9 +3130,7 @@ function enableExpensifyCard(policyID: string, enabled: boolean, shouldNavigateT
 
     const parameters: EnablePolicyExpensifyCardsParams = {authToken, policyID, enabled};
 
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS, parameters, onyxData);
 
     if (enabled && shouldNavigateToExpensifyCardPage) {
         navigateToExpensifyCardPage(policyID);
@@ -3190,9 +3185,7 @@ function enableCompanyCards(policyID: string, enabled: boolean, shouldGoBack = t
 
     const parameters: EnablePolicyCompanyCardsParams = {authToken, policyID, enabled};
 
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS, parameters, onyxData);
 
     if (enabled && getIsNarrowLayout() && shouldGoBack) {
         goBackWhenEnableFeature(policyID);
@@ -3240,9 +3233,7 @@ function enablePolicyReportFields(policyID: string, enabled: boolean) {
 
     const parameters: EnablePolicyReportFieldsParams = {policyID, enabled};
 
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, parameters, onyxData);
 }
 
 function enablePolicyTaxes(policyID: string, enabled: boolean) {
@@ -3360,9 +3351,7 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) {
     if (shouldAddDefaultTaxRatesData) {
         parameters.taxFields = JSON.stringify(defaultTaxRates);
     }
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_TAXES, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_TAXES, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_TAXES, parameters, onyxData);
 
     if (enabled && getIsNarrowLayout()) {
         goBackWhenEnableFeature(policyID);
@@ -3465,9 +3454,7 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) {
         setWorkspaceAutoReportingFrequency(policyID, CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT);
     }
 
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, parameters, onyxData);
 
     if (enabled && getIsNarrowLayout()) {
         goBackWhenEnableFeature(policyID);
@@ -3533,9 +3520,7 @@ function enablePolicyRules(policyID: string, enabled: boolean, shouldGoBack = tr
     };
 
     const parameters: SetPolicyRulesEnabledParams = {policyID, enabled};
-    API.write(WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, parameters, onyxData);
 
     if (enabled && getIsNarrowLayout() && shouldGoBack) {
         goBackWhenEnableFeature(policyID);
@@ -3648,9 +3633,7 @@ function enablePolicyInvoicing(policyID: string, enabled: boolean) {
 
     const parameters: EnablePolicyInvoicingParams = {policyID, enabled};
 
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_INVOICING, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_INVOICING, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_INVOICING, parameters, onyxData);
 
     if (enabled && getIsNarrowLayout()) {
         goBackWhenEnableFeature(policyID);
diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts
index afe9ef6365b8..2f98884b9cfd 100644
--- a/src/libs/actions/Policy/Tag.ts
+++ b/src/libs/actions/Policy/Tag.ts
@@ -28,7 +28,6 @@ import {goBackWhenEnableFeature} from '@libs/PolicyUtils';
 import * as ReportUtils from '@libs/ReportUtils';
 import {getTagArrayFromName} from '@libs/TransactionUtils';
 import type {PolicyTagList} from '@pages/workspace/tags/types';
-import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {ImportedSpreadsheet, Policy, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags, Report} from '@src/types/onyx';
@@ -765,9 +764,7 @@ function enablePolicyTags(policyID: string, enabled: boolean) {
 
     const parameters: EnablePolicyTagsParams = {policyID, enabled};
 
-    API.write(WRITE_COMMANDS.ENABLE_POLICY_TAGS, parameters, onyxData, {
-        checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_TAGS, persistedRequests, parameters),
-    });
+    API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_TAGS, parameters, onyxData);
 
     if (enabled && getIsNarrowLayout()) {
         goBackWhenEnableFeature(policyID);
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 599f2e117ae8..a3bf0d78c500 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -1595,14 +1595,11 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker
         lastReadTime,
     };
 
-    API.write(
+    API.writeWithNoDuplicatesConflictAction(
         WRITE_COMMANDS.READ_NEWEST_ACTION,
         parameters,
         {optimisticData},
-        {
-            checkAndFixConflictingRequest: (persistedRequests) =>
-                resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === parameters.reportID),
-        },
+        (request) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === parameters.reportID,
     );
 
     if (shouldResetUnreadMarker) {
diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts
index 1cbeb2eb82de..359db4ada266 100644
--- a/src/libs/actions/RequestConflictUtils.ts
+++ b/src/libs/actions/RequestConflictUtils.ts
@@ -228,3 +228,5 @@ export {
     resolveEnableFeatureConflicts,
     enablePolicyFeatureCommand,
 };
+
+export type {EnablePolicyFeatureCommand, RequestMatcher};
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 3697bc1b6242..010982d6057b 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -56,7 +56,6 @@ import {reconnectApp} from './App';
 import applyOnyxUpdatesReliably from './applyOnyxUpdatesReliably';
 import {openOldDotLink} from './Link';
 import {showReportActionNotification} from './Report';
-import {resolveDuplicationConflictAction} from './RequestConflictUtils';
 import {resendValidateCode as sessionResendValidateCode} from './Session';
 import Timing from './Timing';
 
@@ -814,14 +813,7 @@ function pingPusher() {
     lastPingSentTimestamp = pingTimestamp;
 
     const parameters: PusherPingParams = {pingID, pingTimestamp};
-    API.write(
-        WRITE_COMMANDS.PUSHER_PING,
-        parameters,
-        {},
-        {
-            checkAndFixConflictingRequest: (persistedRequests) => resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.PUSHER_PING),
-        },
-    );
+    API.writeWithNoDuplicatesConflictAction(WRITE_COMMANDS.PUSHER_PING, parameters);
     Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${pingTimestamp}`);
     Timing.start(CONST.TIMING.PUSHER_PING_PONG);
 }

From 03c0fcde3789455d9992167a65e66908659af137 Mon Sep 17 00:00:00 2001
From: daledah <daleda261@outlook.com>
Date: Wed, 2 Jul 2025 22:59:59 +0700
Subject: [PATCH 2/4] test: add more test cases

---
 tests/unit/APITest.ts | 174 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 174 insertions(+)

diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts
index 8a3d3b365516..c22f6b50fc5f 100644
--- a/tests/unit/APITest.ts
+++ b/tests/unit/APITest.ts
@@ -1,5 +1,6 @@
 import MockedOnyx from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
+import {EnablePolicyFeatureCommand} from '@libs/actions/RequestConflictUtils';
 import type {ApiRequestCommandParameters, ReadCommand, WriteCommand} from '@libs/API/types';
 import CONST from '@src/CONST';
 import * as PersistedRequests from '@src/libs/actions/PersistedRequests';
@@ -626,4 +627,177 @@ describe('APITests', () => {
         const [thirdCommand] = xhr.mock.calls.at(2) ?? [];
         expect(thirdCommand).toBe('MockReadCommand');
     });
+
+    test('duplicated write APIs with resolveDuplicationConflictAction conflict', () => {
+        const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS});
+        return Onyx.multiSet({
+            [ONYXKEYS.SESSION]: {authToken: 'anyToken'},
+            [ONYXKEYS.NETWORK]: {isOffline: true},
+            [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'},
+        })
+            .then(() => {
+                // When we queue 3 duplicate persistable commands
+                API.writeWithNoDuplicatesConflictAction('MockCommand' as WriteCommand, {content: 'value1'} as ApiRequestCommandParameters[WriteCommand]);
+                API.writeWithNoDuplicatesConflictAction('MockCommand' as WriteCommand, {content: 'value2'} as ApiRequestCommandParameters[WriteCommand]);
+                API.writeWithNoDuplicatesConflictAction('MockCommand' as WriteCommand, {content: 'value3'} as ApiRequestCommandParameters[WriteCommand]);
+                return waitForBatchedUpdates();
+            })
+            .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}))
+            .then(waitForBatchedUpdates)
+            .then(() => {
+                // Then expect only 1 call to have been made and for the Writes to be made at the last one that was made
+                expect(xhr.mock.calls.length).toBe(1);
+                expect(xhr.mock.calls.at(0)?.[1].content).toBe('value3');
+            });
+    });
+
+    test('different write APIs with resolveDuplicationConflictAction conflict', () => {
+        const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS});
+        return Onyx.multiSet({
+            [ONYXKEYS.SESSION]: {authToken: 'anyToken'},
+            [ONYXKEYS.NETWORK]: {isOffline: true},
+            [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'},
+        })
+            .then(() => {
+                // When we queue 3 different persistable commands
+                API.writeWithNoDuplicatesConflictAction('MockCommandOne' as WriteCommand, {content: 'value1'} as ApiRequestCommandParameters[WriteCommand]);
+                API.writeWithNoDuplicatesConflictAction('MockCommandTwo' as WriteCommand, {content: 'value2'} as ApiRequestCommandParameters[WriteCommand]);
+                API.writeWithNoDuplicatesConflictAction('MockCommandThree' as WriteCommand, {content: 'value3'} as ApiRequestCommandParameters[WriteCommand]);
+                return waitForBatchedUpdates();
+            })
+            .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}))
+            .then(waitForBatchedUpdates)
+            .then(() => {
+                // Then expect all 3 calls to have been made and for the Writes to be made in the order that we made them
+                expect(xhr.mock.calls.length).toBe(3);
+                expect(xhr.mock.calls.at(0)?.[1].content).toBe('value1');
+                expect(xhr.mock.calls.at(1)?.[1].content).toBe('value2');
+                expect(xhr.mock.calls.at(2)?.[1].content).toBe('value3');
+            });
+    });
+
+    test('duplicated write APIs with resolveEnableFeatureConflicts conflict and same policyID', () => {
+        const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS});
+        return Onyx.multiSet({
+            [ONYXKEYS.SESSION]: {authToken: 'anyToken'},
+            [ONYXKEYS.NETWORK]: {isOffline: true},
+            [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'},
+        })
+            .then(() => {
+                // When we queue 3 duplicate persistable commands with same policyID and different enabled values
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '1', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                return waitForBatchedUpdates();
+            })
+            .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}))
+            .then(waitForBatchedUpdates)
+            .then(() => {
+                // Then expect only 1 call to have been made and for the Writes to be made at the first one that was made
+                expect(xhr.mock.calls.length).toBe(1);
+                expect(xhr.mock.calls.at(0)?.[1].enabled).toBe(true);
+            });
+    });
+
+    test('consecutively enable and disable a feature with resolveEnableFeatureConflicts conflict', () => {
+        const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS});
+        return Onyx.multiSet({
+            [ONYXKEYS.SESSION]: {authToken: 'anyToken'},
+            [ONYXKEYS.NETWORK]: {isOffline: true},
+            [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'},
+        })
+            .then(() => {
+                // When we queue 2 duplicate persistable commands with same policyID and true and false enabled values
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '1', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                return waitForBatchedUpdates();
+            })
+            .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}))
+            .then(waitForBatchedUpdates)
+            .then(() => {
+                // Then expect no calls is made
+                expect(xhr.mock.calls.length).toBe(0);
+            });
+    });
+
+    test('multiple write APIs with resolveEnableFeatureConflicts conflict with different policyIDs', () => {
+        const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS});
+        return Onyx.multiSet({
+            [ONYXKEYS.SESSION]: {authToken: 'anyToken'},
+            [ONYXKEYS.NETWORK]: {isOffline: true},
+            [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'},
+        })
+            .then(() => {
+                // When we queue 3 different persistable commands with different policyIDs
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '2', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '3', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                return waitForBatchedUpdates();
+            })
+            .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}))
+            .then(waitForBatchedUpdates)
+            .then(() => {
+                // Then expect all 3 calls to have been made and for the Writes to be made in the order that we made them
+                expect(xhr.mock.calls.length).toBe(3);
+                expect(xhr.mock.calls.at(0)?.[1].policyID).toBe('1');
+                expect(xhr.mock.calls.at(1)?.[1].policyID).toBe('2');
+                expect(xhr.mock.calls.at(2)?.[1].policyID).toBe('3');
+            });
+    });
+
+    test('multiple write APIs with resolveEnableFeatureConflicts conflict with some different policyIDs', () => {
+        const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS});
+        return Onyx.multiSet({
+            [ONYXKEYS.SESSION]: {authToken: 'anyToken'},
+            [ONYXKEYS.NETWORK]: {isOffline: true},
+            [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'},
+        })
+            .then(() => {
+                // When we queue 3 different persistable commands with only 2 policyIDs
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '2', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                API.writeWithNoDuplicatesEnableFeatureConflicts(
+                    'MockCommand' as EnablePolicyFeatureCommand,
+                    {policyID: '1', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand],
+                );
+                return waitForBatchedUpdates();
+            })
+            .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}))
+            .then(waitForBatchedUpdates)
+            .then(() => {
+                // Then expect only 1 call to have been made and for the Writes that have unique policyID to be made
+                expect(xhr.mock.calls.length).toBe(1);
+                expect(xhr.mock.calls.at(0)?.[1].policyID).toBe('2');
+            });
+    });
 });

From 9e6e87deb46399303d7d254d4fd74d6c1e61b530 Mon Sep 17 00:00:00 2001
From: daledah <daleda261@outlook.com>
Date: Wed, 2 Jul 2025 23:45:23 +0700
Subject: [PATCH 3/4] fix: lint

---
 tests/unit/APITest.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts
index c22f6b50fc5f..4de2b79a862c 100644
--- a/tests/unit/APITest.ts
+++ b/tests/unit/APITest.ts
@@ -1,6 +1,6 @@
 import MockedOnyx from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
-import {EnablePolicyFeatureCommand} from '@libs/actions/RequestConflictUtils';
+import type {EnablePolicyFeatureCommand} from '@libs/actions/RequestConflictUtils';
 import type {ApiRequestCommandParameters, ReadCommand, WriteCommand} from '@libs/API/types';
 import CONST from '@src/CONST';
 import * as PersistedRequests from '@src/libs/actions/PersistedRequests';

From 57a09a365109702dc706ebd5cff2495a590289f8 Mon Sep 17 00:00:00 2001
From: daledah <daleda261@outlook.com>
Date: Fri, 4 Jul 2025 23:18:42 +0700
Subject: [PATCH 4/4] refactor: remove unused jsdocs

---
 src/libs/API/index.ts | 52 -------------------------------------------
 1 file changed, 52 deletions(-)

diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index 62d53f395c06..089c1452ccb4 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -136,16 +136,6 @@ function processRequest(request: OnyxRequest, type: ApiRequestType): Promise<voi
 /**
  * All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData (or finallyData, if included in place of the former two values).
  * This is so that if the network is unavailable or the app is closed, we can send the WRITE request later.
- *
- * @param command - Name of API command to call.
- * @param apiCommandParameters - Parameters to send to the API.
- * @param onyxData  - Object containing errors, loading states, and optimistic UI data that will be merged
- *                             into Onyx before and after a request is made. Each nested object will be formatted in
- *                             the same way as an API response.
- * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
- * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
- * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
- * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
  */
 
 function write<TCommand extends WriteCommand>(
@@ -162,17 +152,6 @@ function write<TCommand extends WriteCommand>(
 /**
  * This function is used to write data to the API while ensuring that there are no duplicate requests in the queue.
  * If a duplicate request is found, it resolves the conflict by replacing the duplicated request with the new one.
- *
- * @param command - Name of API command to call.
- * @param apiCommandParameters - Parameters to send to the API.
- * @param onyxData  - Object containing errors, loading states, and optimistic UI data that will be merged
- *                             into Onyx before and after a request is made. Each nested object will be formatted in
- *                             the same way as an API response.
- * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
- * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
- * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
- * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
- * @param requestMatcher - Custom request matcher function for resolveDuplicationConflictAction.
  */
 function writeWithNoDuplicatesConflictAction<TCommand extends WriteCommand>(
     command: TCommand,
@@ -190,16 +169,6 @@ function writeWithNoDuplicatesConflictAction<TCommand extends WriteCommand>(
 /**
  * This function is used to write data to the API while ensuring that there are no conflicts with enabling policy features.
  * If a conflict is found, it resolves the conflict by deleting the duplicated request.
- *
- * @param command - Name of API command to call.
- * @param apiCommandParameters - Parameters to send to the API.
- * @param onyxData  - Object containing errors, loading states, and optimistic UI data that will be merged
- *                             into Onyx before and after a request is made. Each nested object will be formatted in
- *                             the same way as an API response.
- * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
- * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
- * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
- * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
  */
 function writeWithNoDuplicatesEnableFeatureConflicts<TCommand extends EnablePolicyFeatureCommand>(
     command: TCommand,
@@ -220,17 +189,6 @@ function writeWithNoDuplicatesEnableFeatureConflicts<TCommand extends EnablePoli
  * It works just like API.read(), except that it will return a promise.
  * Using this method is discouraged and will throw an ESLint error. Use it sparingly and only when all other alternatives have been exhausted.
  * It is best to discuss it in Slack anytime you are tempted to use this method.
- *
- * @param command - Name of API command to call.
- * @param apiCommandParameters - Parameters to send to the API.
- * @param onyxData  - Object containing errors, loading states, and optimistic UI data that will be merged
- *                             into Onyx before and after a request is made. Each nested object will be formatted in
- *                             the same way as an API response.
- * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
- * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
- * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
- * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
- * @returns
  */
 function makeRequestWithSideEffects<TCommand extends SideEffectRequestCommand>(
     command: TCommand,
@@ -258,16 +216,6 @@ function waitForWrites<TCommand extends ReadCommand>(command: TCommand) {
 
 /**
  * Requests made with this method are not be persisted to disk. If there is no network connectivity, the request is ignored and discarded.
- *
- * @param command - Name of API command to call.
- * @param apiCommandParameters - Parameters to send to the API.
- * @param onyxData  - Object containing errors, loading states, and optimistic UI data that will be merged
- *                             into Onyx before and after a request is made. Each nested object will be formatted in
- *                             the same way as an API response.
- * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
- * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
- * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
- * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
  */
 function read<TCommand extends ReadCommand>(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void {
     Log.info('[API] Called API.read', false, {command, ...apiCommandParameters});
