Skip to content

Commit

Permalink
Merge pull request #29064 from Expensify/youssef_completeSplitBill
Browse files Browse the repository at this point in the history
Allow split actions to be edited and implement IOU action completeSplitBill
  • Loading branch information
luacmartins authored Oct 12, 2023
2 parents 0aeeae4 + 69c0df9 commit 828b8b3
Show file tree
Hide file tree
Showing 14 changed files with 646 additions and 83 deletions.
1 change: 1 addition & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ const ONYXKEYS = {
REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_',
SECURITY_GROUP: 'securityGroup_',
TRANSACTION: 'transactions_',
SPLIT_TRANSACTION_DRAFT: 'splitTransactionDraft_',
PRIVATE_NOTES_DRAFT: 'privateNotesDraft_',

// Manual request tab selector
Expand Down
8 changes: 8 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ export default {
route: 'r/:reportID/split/:reportActionID',
getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}`,
},
EDIT_SPLIT_BILL: {
route: `r/:reportID/split/:reportActionID/edit/:field`,
getRoute: (reportID: string, reportActionID: string, field: ValueOf<typeof CONST.EDIT_REQUEST_FIELD>) => `r/${reportID}/split/${reportActionID}/edit/${field}`,
},
EDIT_SPLIT_BILL_CURRENCY: {
route: 'r/:reportID/split/:reportActionID/edit/currency',
getRoute: (reportID: string, reportActionID: string, currency: string, backTo: string) => `r/${reportID}/split/${reportActionID}/edit/currency?currency=${currency}&backTo=${backTo}`,
},
TASK_TITLE: {
route: 'r/:reportID/title',
getRoute: (reportID: string) => `r/${reportID}/title`,
Expand Down
151 changes: 120 additions & 31 deletions src/components/MoneyRequestConfirmationList.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ export default {
receiptMissingDetails: 'Receipt missing details',
receiptStatusTitle: 'Scanning…',
receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.",
receiptScanningFailed: 'Receipt scanning failed. Enter the details manually.',
requestCount: ({count, scanningReceipts = 0}: RequestCountParams) => `${count} requests${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}`,
deleteRequest: 'Delete request',
deleteConfirmation: 'Are you sure that you want to delete this request?',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ export default {
receiptMissingDetails: 'Recibo con campos vacíos',
receiptStatusTitle: 'Escaneando…',
receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.',
receiptScanningFailed: 'El escaneo de recibo ha fallado. Introduce los detalles manualmente.',
requestCount: ({count, scanningReceipts = 0}: RequestCountParams) => `${count} solicitudes${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}`,
deleteRequest: 'Eliminar pedido',
deleteConfirmation: '¿Estás seguro de que quieres eliminar este pedido?',
Expand Down
2 changes: 2 additions & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator({

const SplitDetailsModalStackNavigator = createModalStackNavigator({
SplitDetails_Root: () => require('../../../pages/iou/SplitBillDetailsPage').default,
SplitDetails_Edit_Request: () => require('../../../pages/EditSplitBillPage').default,
SplitDetails_Edit_Currency: () => require('../../../pages/iou/IOUCurrencySelection').default,
});

const DetailsModalStackNavigator = createModalStackNavigator({
Expand Down
2 changes: 2 additions & 0 deletions src/libs/Navigation/linkingConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ export default {
SplitDetails: {
screens: {
SplitDetails_Root: ROUTES.SPLIT_BILL_DETAILS.route,
SplitDetails_Edit_Request: ROUTES.EDIT_SPLIT_BILL.route,
SplitDetails_Edit_Currency: ROUTES.EDIT_SPLIT_BILL_CURRENCY.route,
},
},
Task_Details: {
Expand Down
28 changes: 20 additions & 8 deletions src/libs/TransactionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,25 @@ function hasReceipt(transaction: Transaction | undefined | null): boolean {
}

function areRequiredFieldsEmpty(transaction: Transaction): boolean {
return (
const isMerchantEmpty =
transaction.merchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction.merchant === '';

const isModifiedMerchantEmpty =
!transaction.modifiedMerchant ||
transaction.modifiedMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT ||
transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ||
(transaction.modifiedMerchant === '' &&
(transaction.merchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transaction.merchant === '' || transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT)) ||
(transaction.modifiedAmount === 0 && transaction.amount === 0) ||
(transaction.modifiedCreated === '' && transaction.created === '')
);
transaction.modifiedMerchant === '';

const isModifiedAmountEmpty = !transaction.modifiedAmount || transaction.modifiedAmount === 0;
const isModifiedCreatedEmpty = !transaction.modifiedCreated || transaction.modifiedCreated === '';

return (isModifiedMerchantEmpty && isMerchantEmpty) || (isModifiedAmountEmpty && transaction.amount === 0) || (isModifiedCreatedEmpty && transaction.created === '');
}

/**
* Given the edit made to the money request, return an updated transaction object.
*/
function getUpdatedTransaction(transaction: Transaction, transactionChanges: TransactionChanges, isFromExpenseReport: boolean): Transaction {
function getUpdatedTransaction(transaction: Transaction, transactionChanges: TransactionChanges, isFromExpenseReport: boolean, shouldUpdateReceiptState = true): Transaction {
// Only changing the first level fields so no need for deep clone now
const updatedTransaction = {...transaction};
let shouldStopSmartscan = false;
Expand Down Expand Up @@ -144,7 +149,13 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
updatedTransaction.tag = transactionChanges.tag;
}

if (shouldStopSmartscan && transaction?.receipt && Object.keys(transaction.receipt).length > 0 && transaction?.receipt?.state !== CONST.IOU.RECEIPT_STATE.OPEN) {
if (
shouldUpdateReceiptState &&
shouldStopSmartscan &&
transaction?.receipt &&
Object.keys(transaction.receipt).length > 0 &&
transaction?.receipt?.state !== CONST.IOU.RECEIPT_STATE.OPEN
) {
updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN;
}

Expand Down Expand Up @@ -438,6 +449,7 @@ export {
isPending,
isPosted,
getWaypoints,
areRequiredFieldsEmpty,
hasMissingSmartscanFields,
getWaypointIndex,
waypointHasValidAddress,
Expand Down
234 changes: 234 additions & 0 deletions src/libs/actions/IOU.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ Onyx.connect({
},
});

let allDraftSplitTransactions;
Onyx.connect({
key: ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT,
waitForCollectionCallback: true,
callback: (val) => {
allDraftSplitTransactions = val || {};
},
});

let allRecentlyUsedTags = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS,
Expand Down Expand Up @@ -1489,6 +1498,229 @@ function startSplitBill(participants, currentUserLogin, currentUserAccountID, co
Report.notifyNewAction(splitChatReport.chatReportID, currentUserAccountID);
}

/** Used for editing a split bill while it's still scanning or when SmartScan fails, it completes a split bill started by startSplitBill above.
*
* @param {number} chatReportID - The group chat or workspace reportID
* @param {Object} reportAction - The split action that lives in the chatReport above
* @param {Object} updatedTransaction - The updated **draft** split transaction
* @param {Number} sessionAccountID - accountID of the current user
* @param {String} sessionEmail - email of the current user
*/
function completeSplitBill(chatReportID, reportAction, updatedTransaction, sessionAccountID, sessionEmail) {
const currentUserEmailForIOUSplit = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail);
const {transactionID} = updatedTransaction;
const unmodifiedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];

// Save optimistic updated transaction and action
const optimisticData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {
...updatedTransaction,
receipt: {
state: CONST.IOU.RECEIPT_STATE.OPEN,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`,
value: {
[reportAction.reportActionID]: {
lastModified: DateUtils.getDBTime(),
whisperedToAccountIDs: [],
},
},
},
];

const successData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {pendingAction: null},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`,
value: null,
},
];

const failureData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {
...unmodifiedTransaction,
errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`,
value: {
[reportAction.reportActionID]: {
...reportAction,
errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
},
},
},
];

const splitParticipants = updatedTransaction.comment.splits;
const {modifiedAmount: amount, modifiedCurrency: currency} = updatedTransaction;

// Exclude the current user when calculating the split amount, `calculateAmount` takes it into account
const splitAmount = IOUUtils.calculateAmount(splitParticipants.length - 1, amount, currency, false);

const splits = [{email: currentUserEmailForIOUSplit}];
_.each(splitParticipants, (participant) => {
// Skip creating the transaction for the current user
if (participant.email === currentUserEmailForIOUSplit) {
return;
}
const isPolicyExpenseChat = !_.isEmpty(participant.policyID);

if (!isPolicyExpenseChat) {
// In case this is still the optimistic accountID saved in the splits array, return early as we cannot know
// if there is an existing chat between the split creator and this participant
// Instead, we will rely on Auth generating the report IDs and the user won't see any optimistic chats or reports created
const participantPersonalDetails = allPersonalDetails[participant.accountID] || {};
if (!participantPersonalDetails || participantPersonalDetails.isOptimisticPersonalDetail) {
splits.push({
email: participant.email,
});
return;
}
}

let oneOnOneChatReport;
let isNewOneOnOneChatReport = false;
if (isPolicyExpenseChat) {
// The workspace chat reportID is saved in the splits array when starting a split bill with a workspace
oneOnOneChatReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`];
} else {
const existingChatReport = ReportUtils.getChatByParticipants([participant.accountID]);
isNewOneOnOneChatReport = !existingChatReport;
oneOnOneChatReport = existingChatReport || ReportUtils.buildOptimisticChatReport([participant.accountID]);
}

let oneOnOneIOUReport = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`, undefined);
const shouldCreateNewOneOnOneIOUReport =
_.isUndefined(oneOnOneIOUReport) || (isPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport));

if (shouldCreateNewOneOnOneIOUReport) {
oneOnOneIOUReport = isPolicyExpenseChat
? ReportUtils.buildOptimisticExpenseReport(oneOnOneChatReport.reportID, participant.policyID, sessionAccountID, splitAmount, currency)
: ReportUtils.buildOptimisticIOUReport(sessionAccountID, participant.accountID, splitAmount, oneOnOneChatReport.reportID, currency);
} else if (isPolicyExpenseChat) {
// Because of the Expense reports are stored as negative values, we subtract the total from the amount
oneOnOneIOUReport.total -= splitAmount;
} else {
oneOnOneIOUReport = IOUUtils.updateIOUOwnerAndTotal(oneOnOneIOUReport, sessionAccountID, splitAmount, currency);
}

const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction(
isPolicyExpenseChat ? -splitAmount : splitAmount,
currency,
oneOnOneIOUReport.reportID,
updatedTransaction.comment.comment,
updatedTransaction.modifiedCreated,
CONST.IOU.MONEY_REQUEST_TYPE.SPLIT,
transactionID,
updatedTransaction.modifiedMerchant,
{...updatedTransaction.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN},
updatedTransaction.filename,
);

const oneOnOneCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit);
const oneOnOneCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit);
const oneOnOneIOUAction = ReportUtils.buildOptimisticIOUReportAction(
CONST.IOU.REPORT_ACTION_TYPE.CREATE,
splitAmount,
currency,
updatedTransaction.comment.comment,
[participant],
oneOnOneTransaction.transactionID,
'',
oneOnOneIOUReport.reportID,
);

let oneOnOneReportPreviewAction = ReportActionsUtils.getReportPreviewAction(oneOnOneChatReport.reportID, oneOnOneIOUReport.reportID);
if (oneOnOneReportPreviewAction) {
oneOnOneReportPreviewAction = ReportUtils.updateReportPreview(oneOnOneIOUReport, oneOnOneReportPreviewAction);
} else {
oneOnOneReportPreviewAction = ReportUtils.buildOptimisticReportPreview(oneOnOneChatReport, oneOnOneIOUReport, '', oneOnOneTransaction);
}

const [oneOnOneOptimisticData, oneOnOneSuccessData, oneOnOneFailureData] = buildOnyxDataForMoneyRequest(
oneOnOneChatReport,
oneOnOneIOUReport,
oneOnOneTransaction,
oneOnOneCreatedActionForChat,
oneOnOneCreatedActionForIOU,
oneOnOneIOUAction,
{},
oneOnOneReportPreviewAction,
{},
{},
isNewOneOnOneChatReport,
shouldCreateNewOneOnOneIOUReport,
);

splits.push({
email: participant.email,
accountID: participant.accountID,
policyID: participant.policyID,
iouReportID: oneOnOneIOUReport.reportID,
chatReportID: oneOnOneChatReport.reportID,
transactionID: oneOnOneTransaction.transactionID,
reportActionID: oneOnOneIOUAction.reportActionID,
createdChatReportActionID: oneOnOneCreatedActionForChat.reportActionID,
createdIOUReportActionID: oneOnOneCreatedActionForIOU.reportActionID,
reportPreviewReportActionID: oneOnOneReportPreviewAction.reportActionID,
});

optimisticData.push(...oneOnOneOptimisticData);
successData.push(...oneOnOneSuccessData);
failureData.push(...oneOnOneFailureData);
});

API.write(
'CompleteSplitBill',
{
transactionID,
amount: updatedTransaction.modifiedAmount,
currency: updatedTransaction.modifiedCurrency,
created: updatedTransaction.modifiedCreated,
merchant: updatedTransaction.modifiedMerchant,
comment: updatedTransaction.comment.comment,
splits: JSON.stringify(splits),
},
{optimisticData, successData, failureData},
);
Navigation.dismissModal(chatReportID);
Report.notifyNewAction(chatReportID, sessionAccountID);
}

/**
* @param {String} transactionID
* @param {Object} transactionChanges
*/
function setDraftSplitTransaction(transactionID, transactionChanges = {}) {
let draftSplitTransaction = allDraftSplitTransactions[`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`];

if (!draftSplitTransaction) {
draftSplitTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
}

const updatedTransaction = TransactionUtils.getUpdatedTransaction(draftSplitTransaction, transactionChanges, false, false);

Onyx.merge(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, updatedTransaction);
}

/**
* @param {String} transactionID
* @param {Number} transactionThreadReportID
Expand Down Expand Up @@ -2660,7 +2892,9 @@ export {
deleteMoneyRequest,
splitBill,
splitBillAndOpenReport,
setDraftSplitTransaction,
startSplitBill,
completeSplitBill,
requestMoney,
sendMoneyElsewhere,
approveMoneyRequest,
Expand Down
Loading

0 comments on commit 828b8b3

Please sign in to comment.