Skip to content

Commit

Permalink
Merge pull request #11597 from Expensify/cmartins-createSplitBill
Browse files Browse the repository at this point in the history
Create splitBill in the client
  • Loading branch information
luacmartins authored Oct 31, 2022
2 parents f656f08 + d20ea60 commit 2174ef5
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 279 deletions.
1 change: 1 addition & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ const CONST = {
REPORT_ACTION_TYPE: {
PAY: 'pay',
CREATE: 'create',
SPLIT: 'split',
},
AMOUNT_MAX_LENGTH: 10,
},
Expand Down
68 changes: 7 additions & 61 deletions src/components/IOUConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Log from '../libs/Log';
import SettlementButton from './SettlementButton';
import ROUTES from '../ROUTES';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from './withCurrentUserPersonalDetails';
import * as IOUUtils from '../libs/IOUUtils';

const propTypes = {
/** Callback to inform parent modal of success */
Expand Down Expand Up @@ -143,7 +144,7 @@ class IOUConfirmationList extends Component {
getParticipantsWithAmount(participants) {
return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(
participants,
this.props.numberFormat(this.calculateAmount(participants) / 100, {
this.props.numberFormat(IOUUtils.calculateAmount(participants, this.props.iouAmount) / 100, {
style: 'currency',
currency: this.props.iou.selectedCurrencyCode,
}),
Expand Down Expand Up @@ -177,7 +178,7 @@ class IOUConfirmationList extends Component {

const formattedMyPersonalDetails = OptionsListUtils.getIOUConfirmationOptionsFromMyPersonalDetail(
this.props.currentUserPersonalDetails,
this.props.numberFormat(this.calculateAmount(selectedParticipants, true) / 100, {
this.props.numberFormat(IOUUtils.calculateAmount(selectedParticipants, this.props.iouAmount, true) / 100, {
style: 'currency',
currency: this.props.iou.selectedCurrencyCode,
}),
Expand Down Expand Up @@ -212,35 +213,6 @@ class IOUConfirmationList extends Component {
return sections;
}

/**
* Gets splits for the transaction
* @returns {Array|null}
*/
getSplits() {
// There can only be splits when there are multiple participants, so return early when there are not
// multiple participants
if (!this.props.hasMultipleParticipants) {
return null;
}
const selectedParticipants = this.getSelectedParticipants();
const splits = _.map(selectedParticipants, participant => ({
email: OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login),

// We should send in cents to API
// Cents is temporary and there must be support for other currencies in the future
amount: this.calculateAmount(selectedParticipants),
}));

splits.push({
email: OptionsListUtils.addSMSDomainIfPhoneNumber(this.props.currentUserPersonalDetails.login),

// The user is default and we should send in cents to API
// USD is temporary and there must be support for other currencies in the future
amount: this.calculateAmount(selectedParticipants, true),
});
return splits;
}

/**
* Returns selected options -- there is checkmark for every row in List for split flow
* @returns {Array}
Expand All @@ -256,30 +228,6 @@ class IOUConfirmationList extends Component {
];
}

/**
* Calculates the amount per user given a list of participants
* @param {Array} participants
* @param {Boolean} isDefaultUser
* @returns {Number}
*/
calculateAmount(participants, isDefaultUser = false) {
// Convert to cents before working with iouAmount to avoid
// javascript subtraction with decimal problem -- when dealing with decimals,
// because they are encoded as IEEE 754 floating point numbers, some of the decimal
// numbers cannot be represented with perfect accuracy.
// Cents is temporary and there must be support for other currencies in the future
const iouAmount = Math.round(parseFloat(this.props.iouAmount * 100));
const totalParticipants = participants.length + 1;
const amountPerPerson = Math.round(iouAmount / totalParticipants);

if (!isDefaultUser) { return amountPerPerson; }

const sumAmount = amountPerPerson * totalParticipants;
const difference = iouAmount - sumAmount;

return iouAmount !== sumAmount ? (amountPerPerson + difference) : amountPerPerson;
}

/**
* Toggle selected option's selected prop.
* @param {Object} option
Expand Down Expand Up @@ -318,7 +266,7 @@ class IOUConfirmationList extends Component {
Log.info(`[IOU] Sending money via: ${paymentMethod}`);
this.props.onSendMoney(paymentMethod);
} else {
this.props.onConfirm(this.getSplits());
this.props.onConfirm(selectedParticipants);
}
}

Expand All @@ -327,8 +275,8 @@ class IOUConfirmationList extends Component {
const shouldShowSettlementButton = this.props.iouType === CONST.IOU.IOU_TYPE.SEND;
const shouldDisableButton = selectedParticipants.length === 0;
const recipient = this.state.participants[0];
const canModifyParticipants = this.props.isIOUAttachedToExistingChatReport && this.props.hasMultipleParticipants;
const isLoading = this.props.iou.loading;
const canModifyParticipants = !this.props.isIOUAttachedToExistingChatReport && this.props.hasMultipleParticipants;

return (
<OptionsSelector
sections={this.getSections()}
Expand All @@ -339,7 +287,7 @@ class IOUConfirmationList extends Component {
textInputLabel={this.props.translate('iOUConfirmationList.whatsItFor')}
placeholderText={this.props.translate('common.optional')}
selectedOptions={this.getSelectedOptions()}
canSelectMultipleOptions={this.props.hasMultipleParticipants}
canSelectMultipleOptions={canModifyParticipants}
disableArrowKeysActions={!canModifyParticipants}
isDisabled={!canModifyParticipants}
hideAdditionalOptionStates
Expand All @@ -358,14 +306,12 @@ class IOUConfirmationList extends Component {
addBankAccountRoute={ROUTES.IOU_SEND_ADD_BANK_ACCOUNT}
addDebitCardRoute={ROUTES.IOU_SEND_ADD_DEBIT_CARD}
currency={this.props.iou.selectedCurrencyCode}
isLoading={isLoading}
/>
) : (
<ButtonWithMenu
isDisabled={shouldDisableButton}
onPress={(_event, value) => this.confirm(value)}
options={this.splitOrRequestOptions}
isLoading={isLoading}
/>
)}
/>
Expand Down
62 changes: 46 additions & 16 deletions src/libs/ReportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,23 +677,58 @@ function buildOptimisticIOUReport(ownerEmail, userEmail, total, chatReportID, cu
};
}

/**
* @param {String} type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split)
* @param {Number} total - IOU total in cents
* @param {Array} participants - List of logins for the IOU participants, excluding the current user login
* @param {String} comment - IOU comment
* @param {String} currency - IOU currency
* @returns {Array}
*/
function getIOUReportActionMessage(type, total, participants, comment, currency) {
const amount = NumberFormatUtils.format(preferredLocale, total / 100, {style: 'currency', currency});
const isMultipleParticipantReport = participants.length > 1;
const displayNames = _.map(participants, participant => getDisplayNameForParticipant(allPersonalDetails[participant.login], isMultipleParticipantReport) || participant.login);
const from = displayNames.length < 3
? displayNames.join(' and ')
: `${displayNames.slice(0, -1).join(', ')}, and ${_.last(displayNames)}`;

let iouMessage;
switch (type) {
case CONST.IOU.REPORT_ACTION_TYPE.CREATE:
iouMessage = `Requested ${amount} from ${from}${comment && ` for ${comment}`}`;
break;
case CONST.IOU.REPORT_ACTION_TYPE.SPLIT:
iouMessage = `Split ${amount} with ${from}${comment && ` for ${comment}`}`;
break;
default:
break;
}

return [{
html: iouMessage,
text: iouMessage,
isEdited: false,
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
}];
}

/**
* Builds an optimistic IOU reportAction object
*
* @param {Number} sequenceNumber - Caller is responsible for providing a best guess at what the next sequenceNumber will be.
* @param {String} type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay).
* @param {String} type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split).
* @param {Number} amount - IOU amount in cents.
* @param {String} currency - IOU currency.
* @param {String} comment - User comment for the IOU.
* @param {Array} participants - An array with participants details.
* @param {String} paymentType - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, payPal, Expensify).
* @param {String} iouTransactionID - Only required if the IOUReportAction type is oneOf(cancel, decline). Generates a randomID as default.
* @param {String} iouReportID - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default.
* @param {String} debtorEmail - Email of the user that has to pay
* @param {String} locale - Locale of the user
*
* @returns {Object}
*/
function buildOptimisticIOUReportAction(sequenceNumber, type, amount, currency, comment, paymentType = '', iouTransactionID = '', iouReportID = '', debtorEmail = '', locale = 'en') {
function buildOptimisticIOUReportAction(sequenceNumber, type, amount, currency, comment, participants, paymentType = '', iouTransactionID = '', iouReportID = '') {
const IOUTransactionID = iouTransactionID || NumberUtils.rand64();
const IOUReportID = iouReportID || generateReportID();
const originalMessage = {
Expand All @@ -704,17 +739,6 @@ function buildOptimisticIOUReportAction(sequenceNumber, type, amount, currency,
IOUReportID,
type,
};
const formattedTotal = NumberFormatUtils.format(locale,
amount / 100, {
style: 'currency',
currency,
});
const message = [{
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
isEdited: false,
html: comment ? `Requested ${formattedTotal} from ${debtorEmail} for ${comment}` : `Requested ${formattedTotal} from ${debtorEmail}`,
text: comment ? `Requested ${formattedTotal} from ${debtorEmail} for ${comment}` : `Requested ${formattedTotal} from ${debtorEmail}`,
}];

// We store amount, comment, currency in IOUDetails when type = pay
if (type === CONST.IOU.REPORT_ACTION_TYPE.PAY) {
Expand All @@ -725,6 +749,11 @@ function buildOptimisticIOUReportAction(sequenceNumber, type, amount, currency,
originalMessage.paymentType = paymentType;
}

// IOUs of type split only exist in group DMs and those don't have an iouReport so we need to delete the IOUReportID key
if (type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT) {
delete originalMessage.IOUReportID;
}

return {
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
actorAccountID: currentUserAccountID,
Expand All @@ -733,8 +762,8 @@ function buildOptimisticIOUReportAction(sequenceNumber, type, amount, currency,
avatar: lodashGet(currentUserPersonalDetails, 'avatar', getDefaultAvatar(currentUserEmail)),
clientID: NumberUtils.generateReportActionClientID(),
isAttachment: false,
message,
originalMessage,
message: getIOUReportActionMessage(type, amount, participants, comment, currency),
person: [{
style: 'strong',
text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserEmail),
Expand Down Expand Up @@ -1067,4 +1096,5 @@ export {
buildOptimisticReportAction,
shouldReportBeInOptionList,
getChatByParticipants,
getIOUReportActionMessage,
};
Loading

0 comments on commit 2174ef5

Please sign in to comment.