Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d944b76
fix: [GPS]Transactions - Always use iouRequestType to check for reque…
TaduJR Apr 19, 2026
12c3d5b
fix: remove remaining Partial<Transaction> caller in TrackExpense and…
TaduJR Apr 19, 2026
8fb5b4d
test: update tests to set iouRequestType after removing structural fa…
TaduJR Apr 19, 2026
3688075
test: set iouRequestType on ViolationUtilsTest tax-violation fixtures
TaduJR Apr 19, 2026
bd93a70
fix: set iouRequestType on optimistic split bill scan transactions
TaduJR Apr 19, 2026
f3a7663
test: verify startSplitBill sets iouRequestType on optimistic split t…
TaduJR Apr 19, 2026
6f81363
test: remove redundant startSplitBill iouRequestType tests
TaduJR Apr 19, 2026
18931b3
fix: propagate iouRequestType in merge transaction preview
TaduJR Apr 19, 2026
70301d8
Merge branch 'main' of https://github.com/TaduJR/App into fix-GPS-Tra…
TaduJR Apr 20, 2026
44ce9d1
test: cover iouRequestType propagation in buildMergedTransactionData
TaduJR Apr 20, 2026
3eff827
Merge branch 'main' of https://github.com/TaduJR/App into fix-GPS-Tra…
TaduJR Apr 28, 2026
d67f364
fix: resolve leftover merge conflict markers and drop unused isManual…
TaduJR Apr 28, 2026
aacb30c
Merge branch 'main' of https://github.com/TaduJR/App into fix-GPS-Tra…
TaduJR May 9, 2026
40d4fc6
fix: propagate iouRequestType to optimistic transactions for all requ…
TaduJR May 9, 2026
6af4347
fix: pass existingTransaction through createTransaction; use minimal …
TaduJR May 9, 2026
af222be
fix: pass existingTransaction in remaining scan submission paths
TaduJR May 9, 2026
b477c85
fix: avoid Transaction.comment type mismatch in duplicate shims
TaduJR May 9, 2026
6f41c46
fix: scrub existingTransaction from retry payloads to avoid bloating …
TaduJR May 9, 2026
4e80f6b
fix: preserve iouRequestType in retry payloads via minimal existingTr…
TaduJR May 9, 2026
270b2f9
test: fix lint and typecheck issues and set iouRequestType on distanc…
TaduJR May 9, 2026
c1d6235
refactor: drop redundant IOURequestType import in Duplicate.ts
TaduJR May 9, 2026
0173d32
Merge branch 'main' of https://github.com/TaduJR/App into fix-GPS-Tra…
TaduJR May 22, 2026
27fc427
fix: revert stray prettier reformatting in MoneyRequestView from merge
TaduJR May 22, 2026
8258c14
chore: CI restart
TaduJR May 22, 2026
cbbd526
Merge branch 'main' of https://github.com/TaduJR/App into fix-GPS-Tra…
TaduJR May 22, 2026
a82c0cd
test: set iouRequestType on distance fixtures in TransactionTest and …
TaduJR May 22, 2026
e2ee7a3
fix: use duplicate-safe request type and propagate existingTransactio…
TaduJR May 22, 2026
a5e69fd
test: set iouRequestType on per-diem fixtures in ReportSecondaryActio…
TaduJR May 22, 2026
402ae74
test: cover SCAN→MANUAL coercion across all duplicate paths
TaduJR May 22, 2026
e75a524
test: cover SCAN to MANUAL coercion across all duplicate paths
TaduJR May 22, 2026
4fead9e
Merge branch 'main' of https://github.com/TaduJR/App into fix-GPS-Tra…
TaduJR May 22, 2026
fbba0c5
Merge branch 'main' of https://github.com/TaduJR/App into fix-GPS-Tra…
TaduJR May 22, 2026
d86efe5
Merge branch 'main' of https://github.com/TaduJR/App into fix-GPS-Tra…
TaduJR May 25, 2026
fb05eca
fix: tighten duplicate/retry existingTransaction shim
TaduJR May 25, 2026
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: 1 addition & 1 deletion src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ function MoneyRequestView({
} = getTransactionDetails(transaction, undefined, undefined, allowNegativeAmount, false, currentUserPersonalDetails) ?? {};
const isEmptyMerchant = isInvalidMerchantValue(transactionMerchant);
const isDistanceRequest = isDistanceRequestTransactionUtils(transaction);
const isManualDistanceRequest = isManualDistanceRequestTransactionUtils(transaction, !!mergeTransactionID);
const isManualDistanceRequest = isManualDistanceRequestTransactionUtils(transaction);
const isGPSDistanceRequest = isGPSDistanceRequestTransactionUtils(transaction);
const isOdometerDistanceRequest = isOdometerDistanceRequestTransactionUtils(transaction);
const isMapDistanceRequest = isMapDistanceRequestTransactionUtils(transaction) || isDistanceTypeRequest(transaction);
Expand Down
1 change: 1 addition & 0 deletions src/libs/MergeTransactionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ function buildMergedTransactionData(targetTransaction: OnyxEntry<Transaction>, m
taxAmount: mergeTransaction.taxAmount,
taxCode: mergeTransaction.taxCode,
taxName: mergeTransaction.taxName,
...(mergeTransaction.iouRequestType && {iouRequestType: mergeTransaction.iouRequestType}),
};
}

Expand Down
151 changes: 13 additions & 138 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,53 +141,13 @@ function isDeletedTransaction(transaction: {reportID?: string}): boolean {
return transaction.reportID === CONST.REPORT.TRASH_REPORT_ID;
}

function hasDistanceCustomUnit(transaction: OnyxEntry<Transaction> | Partial<Transaction>): boolean {
return transaction?.comment?.type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && transaction?.comment?.customUnit?.name === CONST.CUSTOM_UNITS.NAME_DISTANCE;
}

function isDistanceRequest(transaction: OnyxEntry<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (transaction && Object.hasOwn(transaction, 'iouRequestType')) {
return (
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE ||
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP ||
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER ||
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_GPS ||
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL
);
}

// This is the case for transaction objects once they have been saved to the server
return hasDistanceCustomUnit(transaction);
const requestType = transaction?.iouRequestType;
return requestType === CONST.IOU.REQUEST_TYPE.DISTANCE || isDistanceExpenseType(requestType);
Comment thread
TaduJR marked this conversation as resolved.
}

function isDistanceTypeRequest(transaction: OnyxEntry<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (transaction && Object.hasOwn(transaction, 'iouRequestType')) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE;
}

// This is the case for transaction objects once they have been saved to the server
return hasDistanceCustomUnit(transaction);
}

/**
* todo: Currently there is no way to tell server map transaction object from
* server GPS transaction object, this will be discussed and updated later.
* To fix this temporarily we set keyForList of GPS waypoints to 'gps_start' and 'gps_end'
* and use that to determine if it's a GPS or Map transaction. This should be changed before
* the first GPS release.
*/
function hasGPSWaypoints(transaction: OnyxEntry<Transaction>) {
const waypoints = transaction?.comment?.waypoints;

if (!waypoints) {
return false;
}

const waypoint = Object.values(waypoints).at(0);

return !!waypoint?.keyForList?.startsWith('gps');
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE;
}

/**
Expand All @@ -202,90 +162,31 @@ function haveWaypointAddressesChanged(oldWaypoints: WaypointCollection | undefin
}

function isMapDistanceRequest(transaction: OnyxEntry<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (transaction && Object.hasOwn(transaction, 'iouRequestType')) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP;
}

// This is the case for transaction objects once they have been saved to the server
return hasDistanceCustomUnit(transaction) && !hasGPSWaypoints(transaction);
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP;
}

function isGPSDistanceRequest(transaction: OnyxEntry<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (transaction && Object.hasOwn(transaction, 'iouRequestType')) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_GPS;
}

// This is the case for transaction objects once they have been saved to the server
return hasGPSWaypoints(transaction);
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_GPS;
}

function isManualDistanceRequest(transaction: OnyxEntry<Transaction>, isUpdatedMergeTransaction = false): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (transaction && Object.hasOwn(transaction, 'iouRequestType') && !isUpdatedMergeTransaction) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL;
}

// This is the case for transaction objects once they have been saved to the server
// Exclude odometer requests which also have no waypoints but have odometer readings
return (
hasDistanceCustomUnit(transaction) &&
isEmptyObject(transaction?.comment?.waypoints) &&
transaction?.comment?.odometerStart === undefined &&
transaction?.comment?.odometerEnd === undefined
);
function isManualDistanceRequest(transaction: OnyxEntry<Transaction>): boolean {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL;
}

function isOdometerDistanceRequest(transaction: OnyxEntry<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (transaction && Object.hasOwn(transaction, 'iouRequestType')) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER;
}

// This is the case for transaction objects once they have been saved to the server
// Odometer requests have odometerStart and odometerEnd in comment, and no waypoints
return (
hasDistanceCustomUnit(transaction) &&
isEmptyObject(transaction?.comment?.waypoints) &&
(transaction?.comment?.odometerStart !== undefined || transaction?.comment?.odometerEnd !== undefined)
);
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER;
}

function isScanRequest(transaction: OnyxEntry<Transaction> | Partial<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (transaction && Object.hasOwn(transaction, 'iouRequestType')) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN;
}

// Distance requests can have a receipt source (for the map), so we need to exclude them
if (hasDistanceCustomUnit(transaction)) {
return false;
}

return !!transaction?.receipt?.source && transaction?.amount === 0;
function isScanRequest(transaction: OnyxEntry<Transaction>): boolean {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN;
Comment thread
TaduJR marked this conversation as resolved.
}

function isPerDiemRequest(transaction: OnyxEntry<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (transaction && Object.hasOwn(transaction, 'iouRequestType')) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.PER_DIEM;
}

// This is the case for transaction objects once they have been saved to the server
const type = transaction?.comment?.type;
const customUnitName = transaction?.comment?.customUnit?.name;
return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL;
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.PER_DIEM;
}

function isTimeRequest(transaction: OnyxEntry<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (transaction && Object.hasOwn(transaction, 'iouRequestType')) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.TIME;
}

// This is the case for transaction objects once they have been saved to the server
return transaction?.comment?.type === CONST.TRANSACTION.TYPE.TIME;
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.TIME;
}

function isDistanceExpenseType(requestType: IOURequestType | undefined): requestType is DistanceExpenseType {
Expand All @@ -302,32 +203,7 @@ function isCorporateCardTransaction(transaction: OnyxEntry<Transaction>): boolea
}

function getRequestType(transaction: OnyxEntry<Transaction>): IOURequestType {
if (isOdometerDistanceRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER;
}
if (isManualDistanceRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL;
}
if (isMapDistanceRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.DISTANCE_MAP;
}
if (isDistanceTypeRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.DISTANCE;
}
if (isScanRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.SCAN;
}
if (isPerDiemRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.PER_DIEM;
}
if (isTimeRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.TIME;
}
if (isGPSDistanceRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.DISTANCE_GPS;
}

return CONST.IOU.REQUEST_TYPE.MANUAL;
return transaction?.iouRequestType ?? CONST.IOU.REQUEST_TYPE.MANUAL;
Comment thread
TaduJR marked this conversation as resolved.
Comment thread
TaduJR marked this conversation as resolved.
Comment thread
TaduJR marked this conversation as resolved.
}

/**
Expand Down Expand Up @@ -552,7 +428,6 @@ function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): T
cardID: existingTransaction?.cardID,
cardName: existingTransaction?.cardName,
cardNumber: existingTransaction?.cardNumber,
// Use conditional spread to avoid creating the key if it's undefined, which would break lodashHas checks.
...(existingTransaction?.iouRequestType ? {iouRequestType: existingTransaction.iouRequestType} : {}),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimistic transactions won’t get iouRequestType unless the caller seeded it ? Can you confirm my assumption please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildOptimisticTransaction only copies iouRequestType from the existingTransaction it's handed; it doesn't derive it.

But the caller reliably seeds it: the draft created by startMoneyRequest always carries iouRequestType (MoneyRequest.ts:987), and that draft is threaded through as existingTransaction then getMoneyRequestInformation then buildOptimisticTransaction. Making that propagation reliable across every creation path (submit/track/duplicate) was one of the core fixes in this PR.

routes,
};
Expand Down
51 changes: 41 additions & 10 deletions src/libs/actions/IOU/Duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,15 @@ function buildDuplicateTransactionParams(transaction: OnyxTypes.Transaction, tra
return {transactionParams, waypoints};
}

/**
* Returns the request type the duplicate should be created with. SCAN sources become MANUAL because
* `buildDuplicateTransactionParams` strips the receipt — without one, the duplicate cannot be a scan request.
*/
function getDuplicateRequestType(transaction: OnyxTypes.Transaction) {
const sourceRequestType = getRequestType(transaction);
return sourceRequestType === CONST.IOU.REQUEST_TYPE.SCAN ? CONST.IOU.REQUEST_TYPE.MANUAL : sourceRequestType;
}

/**
* Routes a duplicate expense to the correct creation function based on transaction type.
* Shared between duplicateExpenseTransaction and duplicateReport.
Expand Down Expand Up @@ -654,18 +663,20 @@ function createExpenseByType({
currentUserLogin: params.currentUserEmailParam,
currentUserAccountID: params.currentUserAccountIDParam,
existingTransaction: {
...(params.transactionParams ?? {}),
iouRequestType: getDuplicateRequestType(transaction),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see this being duplicated quite a few times.

amount: 0,
currency: '',
created: '',
merchant: '',
reportID: '1',
transactionID: '1',
comment: {
...transaction.comment,
hold: undefined,
originalTransactionID: undefined,
source: undefined,
waypoints,
},
iouRequestType: getRequestType(transaction),
modifiedCreated: '',
reportID: '1',
transactionID: '1',
},
transactionParams: {
...(params.transactionParams ?? {}),
Expand Down Expand Up @@ -787,6 +798,15 @@ function duplicateExpenseTransaction({
policyRecentlyUsedCurrencies,
quickAction,
existingTransactionDraft,
existingTransaction: {
iouRequestType: getDuplicateRequestType(transaction),
amount: 0,
currency: '',
created: '',
merchant: '',
reportID: '1',
transactionID: '1',
},
draftTransactionIDs,
isSelfTourViewed,
betas,
Expand All @@ -803,18 +823,20 @@ function duplicateExpenseTransaction({
participant: {accountID: currentUserAccountID, selected: true},
},
existingTransaction: {
...(params.transactionParams ?? {}),
iouRequestType: getDuplicateRequestType(transaction),
amount: 0,
currency: '',
created: '',
merchant: '',
reportID: '1',
transactionID: '1',
comment: {
...transaction.comment,
hold: undefined,
originalTransactionID: undefined,
source: undefined,
waypoints,
},
iouRequestType: getRequestType(transaction),
modifiedCreated: '',
reportID: '1',
transactionID: '1',
},
transactionParams: {
...(params.transactionParams ?? {}),
Expand Down Expand Up @@ -981,6 +1003,15 @@ function duplicateReport({
quickAction,
policyRecentlyUsedCurrencies,
existingTransactionDraft: undefined,
existingTransaction: {
iouRequestType: getDuplicateRequestType(transaction),
amount: 0,
currency: '',
created: '',
merchant: '',
reportID: '1',
transactionID: '1',
},
draftTransactionIDs,
isSelfTourViewed,
betas,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/actions/IOU/MoneyRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ function createTransaction({
taxCode,
taxAmount,
},
existingTransaction: transaction,
...(policyParams ?? {}),
shouldHandleNavigation: shouldHandleNav,
shouldDeferForSearch,
Expand Down Expand Up @@ -307,6 +308,7 @@ function createTransaction({
quickAction,
policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [],
existingTransactionDraft,
existingTransaction: transaction,
draftTransactionIDs,
isSelfTourViewed,
personalDetails,
Expand Down Expand Up @@ -734,6 +736,7 @@ function handleMoneyRequestStepDistanceNavigation({
taxCode: distanceTaxCode,
taxAmount: distanceTaxAmount,
},
existingTransaction: transaction,
shouldHandleNavigation: overrides.shouldHandleNavigation,
shouldDeferForSearch: overrides.shouldDeferForSearch,
isASAPSubmitBetaEnabled,
Expand Down
8 changes: 1 addition & 7 deletions src/libs/actions/IOU/MoneyRequestBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,13 +1231,7 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma
iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`] ?? null;
}

const isScanRequest = isScanRequestTransactionUtils({
amount,
receipt,
...(existingTransaction && {
iouRequestType: existingTransaction.iouRequestType,
}),
});
const isScanRequest = isScanRequestTransactionUtils(existingTransaction);

const shouldCreateNewMoneyRequestReport = isSplitExpense
? false
Expand Down
2 changes: 2 additions & 0 deletions src/libs/actions/IOU/Split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,8 @@ function startSplitBill({
},
});

splitTransaction.iouRequestType = receipt?.source ? CONST.IOU.REQUEST_TYPE.SCAN : CONST.IOU.REQUEST_TYPE.MANUAL;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're building splitTransaction just before this line. Any reason why this isn't added there?


const filename = splitTransaction.receipt?.filename;

// Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat
Expand Down
Loading
Loading