Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/Expensify/App into feat/#Ex…
Browse files Browse the repository at this point in the history
  • Loading branch information
perunt committed Jan 19, 2024
2 parents 3ce1080 + 991a4e2 commit b0035bf
Show file tree
Hide file tree
Showing 44 changed files with 761 additions and 357 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s
### How This Works
1. On the day of your first card settlement, we'll create the Expensify Card Liability account in your QuickBooks Online general ledger. If you've opted for Daily Settlement, we'll also create an Expensify Clearing Account.
2. During your QuickBooks Online auto-sync on that same day, if there are unsettled transactions, we'll generate a journal entry totaling all posted transactions since the last settlement. This entry will credit the selected bank account and debit the new Expensify Clearing Account (for Daily Settlement) or the Expensify Liability Account (for Monthly Settlement).
3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual credit card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories.
3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories.

### Example
- We have card transactions for the day totaling $100, so we create the following journal entry upon sync:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ For an efficiency-focused company, we recommend setting up [Scheduled Submit](ht
4. You’ll notice *Scheduled Submit* is located directly under *Report Basics*
5. Choose *Daily*

Between Expensify's SmartScan technology, direct corporate card feed import, automatic categorization, and [DoubleCheck](https://community.expensify.com/discussion/5738/deep-dive-how-does-concierge-receipt-audit-work) features, your employees shouldn't need to do anything more than swipe their Expensify Card or scan their receipt.
Between Expensify's SmartScan technology, direct corporate card feed import, automatic categorization, and [DoubleCheck](https://community.expensify.com/discussion/5738/deep-dive-how-does-concierge-receipt-audit-work) features, your employees shouldn't need to do anything more than swipe their Expensify Visa® Commercial Card or scan their receipt.

Scheduled Submit will ensure all expenses are submitted automatically. Any expenses that do not fall within the rules you’ve set up for your policy will be escalated to you for manual review.

Expand Down Expand Up @@ -155,7 +155,7 @@ The Expensify Card has many benefits for your company. Two in particular are wor
### If you don't have a corporate card, use the Expensify Card
Expensify provides a corporate card with the following features:

- Up to 2% cash back (within the US)
- Up to 2% cash back (Applies to USD purchases only)
- [SmartLimits](https://community.expensify.com/discussion/4851/deep-dive-what-are-unapproved-expense-limits#latest)
- Unlimited Virtual Cards - single-purpose cards with a fixed or monthly limit for specific company purchases
- A stable, unbreakable connection (third-party bank feeds can run into connectivity issues)
Expand Down
8 changes: 7 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3069,7 +3069,8 @@ const CONST = {
},

/**
* Constants for maxToRenderPerBatch parameter that is used for FlatList or SectionList. This controls the amount of items rendered per batch, which is the next chunk of items rendered on every scroll.
* Constants for maxToRenderPerBatch parameter that is used for FlatList or SectionList. This controls the amount of items rendered per batch, which is the next chunk of items
* rendered on every scroll.
*/
MAX_TO_RENDER_PER_BATCH: {
DEFAULT: 5,
Expand All @@ -3081,6 +3082,11 @@ const CONST = {
RBR: 'RBR',
},

/**
* Constants for types of violations.
* Defined here because they need to be referenced by the type system to generate the
* ViolationNames type.
*/
VIOLATIONS: {
ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired',
AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense',
Expand Down
1 change: 1 addition & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ type OnyxValues = {
[ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags;
[ONYXKEYS.COLLECTION.SELECTED_TAB]: string;
[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations;
[ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string;
[ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep;

Expand Down
31 changes: 30 additions & 1 deletion src/components/LHNOptionsList/LHNOptionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import _ from 'underscore';
import participantPropTypes from '@components/participantPropTypes';
import transactionPropTypes from '@components/transactionPropTypes';
import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} from '@components/withCurrentReportID';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
import {transactionViolationsPropType} from '@libs/Violations/propTypes';
import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
import reportPropTypes from '@pages/reportPropTypes';
import stylePropTypes from '@styles/stylePropTypes';
Expand Down Expand Up @@ -63,8 +65,13 @@ const propTypes = {

/** The transaction from the parent report action */
transactions: PropTypes.objectOf(transactionPropTypes),

/** List of draft comments */
draftComments: PropTypes.objectOf(PropTypes.string),

/** The list of transaction violations */
transactionViolations: transactionViolationsPropType,

...withCurrentReportIDPropTypes,
};

Expand All @@ -78,6 +85,7 @@ const defaultProps = {
personalDetails: {},
transactions: {},
draftComments: {},
transactionViolations: {},
...withCurrentReportIDDefaultProps,
};

Expand All @@ -98,8 +106,10 @@ function LHNOptionsList({
transactions,
draftComments,
currentReportID,
transactionViolations,
}) {
const styles = useThemeStyles();
const {canUseViolations} = usePermissions();
/**
* Function which renders a row in the list
*
Expand Down Expand Up @@ -137,10 +147,26 @@ function LHNOptionsList({
onSelectRow={onSelectRow}
preferredLocale={preferredLocale}
comment={itemComment}
transactionViolations={transactionViolations}
canUseViolations={canUseViolations}
/>
);
},
[currentReportID, draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions],
[
currentReportID,
draftComments,
onSelectRow,
optionMode,
personalDetails,
policy,
preferredLocale,
reportActions,
reports,
shouldDisableFocusOptions,
transactions,
transactionViolations,
canUseViolations,
],
);

return (
Expand Down Expand Up @@ -189,5 +215,8 @@ export default compose(
draftComments: {
key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT,
},
transactionViolations: {
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
},
}),
)(LHNOptionsList);
21 changes: 19 additions & 2 deletions src/components/LHNOptionsList/OptionRowLHNData.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import _ from 'underscore';
import participantPropTypes from '@components/participantPropTypes';
import transactionPropTypes from '@components/transactionPropTypes';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import SidebarUtils from '@libs/SidebarUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import {transactionViolationsPropType} from '@libs/Violations/propTypes';
import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -42,6 +44,9 @@ const propTypes = {
/** The transaction from the parent report action */
transaction: transactionPropTypes,

/** Any violations associated with the transaction */
transactionViolations: transactionViolationsPropType,

...basePropTypes,
};

Expand Down Expand Up @@ -73,6 +78,8 @@ function OptionRowLHNData({
receiptTransactions,
parentReportAction,
transaction,
transactionViolations,
canUseViolations,
...propsToForward
}) {
const reportID = propsToForward.reportID;
Expand All @@ -85,9 +92,19 @@ function OptionRowLHNData({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fullReport.reportID, receiptTransactions, reportActions]);

const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction);

const optionItem = useMemo(() => {
// Note: ideally we'd have this as a dependent selector in onyx!
const item = SidebarUtils.getOptionData(fullReport, reportActions, personalDetails, preferredLocale, policy, parentReportAction);
const item = SidebarUtils.getOptionData({
report: fullReport,
reportActions,
personalDetails,
preferredLocale,
policy,
parentReportAction,
hasViolations,
});
if (deepEqual(item, optionItemRef.current)) {
return optionItemRef.current;
}
Expand All @@ -96,7 +113,7 @@ function OptionRowLHNData({
// Listen parentReportAction to update title of thread report when parentReportAction changed
// Listen to transaction to update title of transaction report when transaction changed
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]);
}, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction, transactionViolations, canUseViolations]);

useEffect(() => {
if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) {
Expand Down
6 changes: 3 additions & 3 deletions src/components/OfflineWithFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import shouldRenderOffscreen from '@libs/shouldRenderOffscreen';
import CONST from '@src/CONST';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import {isNotEmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import MessagesRow from './MessagesRow';

/**
Expand Down Expand Up @@ -82,10 +82,10 @@ function OfflineWithFeedback({
const StyleUtils = useStyleUtils();
const {isOffline} = useNetwork();

const hasErrors = isNotEmptyObject(errors ?? {});
const hasErrors = !isEmptyObject(errors ?? {});
// Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages.
const errorMessages = omitBy(errors, (e) => e === null);
const hasErrorMessages = isNotEmptyObject(errorMessages);
const hasErrorMessages = !isEmptyObject(errorMessages);
const isOfflinePendingAction = !!isOffline && !!pendingAction;
const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE);
const isAddError = hasErrors && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;
Expand Down
14 changes: 13 additions & 1 deletion src/components/ReportActionItem/ReportPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Text from '@components/Text';
import transactionPropTypes from '@components/transactionPropTypes';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
Expand All @@ -27,6 +28,7 @@ import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportActionUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import {transactionViolationsPropType} from '@libs/Violations/propTypes';
import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
import reportPropTypes from '@pages/reportPropTypes';
import * as IOU from '@userActions/IOU';
Expand Down Expand Up @@ -108,6 +110,9 @@ const propTypes = {
/** All the transactions, used to update ReportPreview label and status */
transactions: PropTypes.objectOf(transactionPropTypes),

/** All of the transaction violations */
transactionViolations: transactionViolationsPropType,

...withLocalizePropTypes,
};

Expand All @@ -121,6 +126,9 @@ const defaultProps = {
accountID: null,
},
isWhisper: false,
transactionViolations: {
violations: [],
},
policy: {
isHarvestingEnabled: false,
},
Expand All @@ -131,6 +139,7 @@ function ReportPreview(props) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const {canUseViolations} = usePermissions();

const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyDistanceRequests, hasNonReimbursableTransactions} = useMemo(
() => ({
Expand Down Expand Up @@ -162,7 +171,7 @@ function ReportPreview(props) {
const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length;
const hasReceipts = transactionsWithReceipts.length > 0;
const isScanning = hasReceipts && areAllRequestsBeingSmartScanned;
const hasErrors = hasReceipts && hasMissingSmartscanFields;
const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(props.iouReportID, props.transactionViolations));
const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3);
const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction));
let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null;
Expand Down Expand Up @@ -365,5 +374,8 @@ export default compose(
transactions: {
key: ONYXKEYS.COLLECTION.TRANSACTION,
},
transactionViolations: {
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
},
}),
)(ReportPreview);
12 changes: 6 additions & 6 deletions src/components/ReportActionItem/TaskPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';

type PolicyRole = {
/** The role of current user */
role: string;
role: Task.PolicyValue | undefined;
};

type TaskPreviewOnyxProps = {
Expand Down Expand Up @@ -94,7 +94,7 @@ function TaskPreview({
? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED
: action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED;
const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? ''));
const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport ?? {}) ?? action?.childManagerAccountID ?? '';
const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? '';
const assigneeLogin = personalDetails[taskAssigneeAccountID]?.login ?? '';
const assigneeDisplayName = personalDetails[taskAssigneeAccountID]?.displayName ?? '';
const taskAssignee = assigneeDisplayName || LocalePhoneNumber.formatPhoneNumber(assigneeLogin);
Expand Down Expand Up @@ -124,12 +124,12 @@ function TaskPreview({
style={[styles.mr2]}
containerStyle={[styles.taskCheckbox]}
isChecked={isTaskCompleted}
disabled={!Task.canModifyTask(taskReport ?? {}, currentUserPersonalDetails.accountID, rootParentReportpolicy?.role ?? '')}
disabled={!Task.canModifyTask(taskReport, currentUserPersonalDetails.accountID, rootParentReportpolicy?.role)}
onPress={Session.checkIfActionIsAllowed(() => {
if (isTaskCompleted) {
Task.reopenTask(taskReport ?? {});
Task.reopenTask(taskReport);
} else {
Task.completeTask(taskReport ?? {});
Task.completeTask(taskReport);
}
})}
accessibilityLabel={translate('task.task')}
Expand All @@ -154,7 +154,7 @@ export default withCurrentUserPersonalDetails(
},
rootParentReportpolicy: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '0'}`,
selector: (policy: Policy | null) => ({role: policy?.role ?? ''}),
selector: (policy: Policy | null) => ({role: policy?.role}),
},
})(TaskPreview),
);
2 changes: 1 addition & 1 deletion src/components/TaskHeaderActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function TaskHeaderActionButton({report, session, policy}: TaskHeaderActionButto
<View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd]}>
<Button
success
isDisabled={!Task.canModifyTask(report, session?.accountID ?? 0, policy?.role ?? '')}
isDisabled={!Task.canModifyTask(report, session?.accountID ?? 0, policy?.role)}
medium
text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')}
onPress={Session.checkIfActionIsAllowed(() => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ViolationMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, {useMemo} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import ViolationsUtils from '@libs/ViolationsUtils';
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
import type {TransactionViolation} from '@src/types/onyx';
import Text from './Text';

Expand Down
7 changes: 7 additions & 0 deletions src/hooks/__mocks__/usePermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @returns A mock of the usePermissions hook.
*/
const usePermissions = () => ({
canUseViolations: true,
});
export default usePermissions;
17 changes: 17 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ function getYearFromExpirationDateString(expirationDateString: string) {
return cardYear.length === 2 ? `20${cardYear}` : cardYear;
}

/**
* @returns string with a month in MM/YYYY format
*/
function formatCardExpiration(expirationDateString: string) {
// already matches MM/YYYY format
const dateFormat = /^\d{2}\/\d{4}$/;
if (dateFormat.test(expirationDateString)) {
return expirationDateString;
}

const expirationMonth = getMonthFromExpirationDateString(expirationDateString);
const expirationYear = getYearFromExpirationDateString(expirationDateString);

return `${expirationMonth}/${expirationYear}`;
}

/**
* @param cardList - collection of assigned cards
* @returns collection of assigned cards grouped by domain
Expand Down Expand Up @@ -120,6 +136,7 @@ export {
isExpensifyCard,
isCorporateCard,
getDomainCards,
formatCardExpiration,
getMonthFromExpirationDateString,
getYearFromExpirationDateString,
maskCard,
Expand Down
Loading

0 comments on commit b0035bf

Please sign in to comment.