Skip to content

Commit

Permalink
Merge pull request #55607 from callstack-internal/feat/55158-settle-u…
Browse files Browse the repository at this point in the history
…p-feature

[InternalQA] Bring in Settle Up feature for workspace feeds on monthly settlement frequency
  • Loading branch information
mountiny authored Jan 31, 2025
2 parents 80f1e44 + e6b0937 commit 12254ea
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 36 deletions.
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,9 @@ const ONYXKEYS = {
/** Expensify cards settings */
PRIVATE_EXPENSIFY_CARD_SETTINGS: 'private_expensifyCardSettings_',

/** Expensify cards manual billing setting */
PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING: 'private_expensifyCardManualBilling_',

/** Stores which connection is set up to use Continuous Reconciliation */
EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION: 'expensifyCard_continuousReconciliationConnection_',

Expand Down Expand Up @@ -898,6 +901,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod;
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER]: OnyxTypes.CardFeeds;
[ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings;
[ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING]: boolean;
[ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList;
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName;
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean;
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ import type {
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
SettleExpensifyCardParams,
SettlementDateParams,
ShareParams,
SignUpNewFaceCodeParams,
SizeExceededParams,
Expand Down Expand Up @@ -3506,6 +3507,8 @@ const translations = {
limit: 'Limit',
currentBalance: 'Current balance',
currentBalanceDescription: 'Current balance is the sum of all posted Expensify Card transactions that have occurred since the last settlement date.',
balanceWillBeSettledOn: ({settlementDate}: SettlementDateParams) => `Balance will be settled on ${settlementDate}`,
settleBalance: 'Settle balance',
cardLimit: 'Card limit',
remainingLimit: 'Remaining limit',
requestLimitIncrease: 'Request limit increase',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ import type {
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
SettleExpensifyCardParams,
SettlementDateParams,
ShareParams,
SignUpNewFaceCodeParams,
SizeExceededParams,
Expand Down Expand Up @@ -3547,6 +3548,8 @@ const translations = {
currentBalance: 'Saldo actual',
currentBalanceDescription:
'El saldo actual es la suma de todas las transacciones contabilizadas con la Tarjeta Expensify que se han producido desde la última fecha de liquidación.',
balanceWillBeSettledOn: ({settlementDate}: SettlementDateParams) => `El saldo se liquidará el ${settlementDate}.`,
settleBalance: 'Liquidar saldo',
cardLimit: 'Límite de la tarjeta',
remainingLimit: 'Límite restante',
requestLimitIncrease: 'Solicitar aumento de límite',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,10 @@ type FlightLayoverParams = {
layover: string;
};

type SettlementDateParams = {
settlementDate: string;
};

export type {
AuthenticationErrorParams,
ImportMembersSuccessfullDescriptionParams,
Expand Down Expand Up @@ -816,4 +820,5 @@ export type {
ChatWithAccountManagerParams,
EditDestinationSubtitleParams,
FlightLayoverParams,
SettlementDateParams,
};
6 changes: 6 additions & 0 deletions src/libs/API/parameters/QueueExpensifyCardForBillingParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type QueueExpensifyCardForBillingParams = {
feedCountry: string;
domainAccountID: number;
};

export default QueueExpensifyCardForBillingParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ export type {default as DismissProductTrainingParams} from './DismissProductTrai
export type {default as OpenWorkspacePlanPageParams} from './OpenWorkspacePlanPage';
export type {default as ResetSMSDeliveryFailureStatusParams} from './ResetSMSDeliveryFailureStatusParams';
export type {default as CreatePerDiemRequestParams} from './CreatePerDiemRequestParams';
export type {default as QueueExpensifyCardForBillingParams} from './QueueExpensifyCardForBillingParams';
export type {default as GetCorpayOnboardingFieldsParams} from './GetCorpayOnboardingFieldsParams';
export type {SaveCorpayOnboardingCompanyDetailsParams} from './SaveCorpayOnboardingCompanyDetailsParams';
export type {default as AcceptSpotnanaTermsParams} from './AcceptSpotnanaTermsParams';
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ const WRITE_COMMANDS = {
CONFIGURE_EXPENSIFY_CARDS_FOR_POLICY: 'ConfigureExpensifyCardsForPolicy',
CREATE_EXPENSIFY_CARD: 'CreateExpensifyCard',
CREATE_ADMIN_ISSUED_VIRTUAL_CARD: 'CreateAdminIssuedVirtualCard',
QUEUE_EXPENSIFY_CARD_FOR_BILLING: 'Domain_QueueExpensifyCardForBilling',
ADD_DELEGATE: 'AddDelegate',
REMOVE_DELEGATE: 'RemoveDelegate',
UPDATE_DELEGATE_ROLE: 'UpdateDelegateRole',
Expand Down Expand Up @@ -875,6 +876,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CONFIGURE_EXPENSIFY_CARDS_FOR_POLICY]: Parameters.ConfigureExpensifyCardsForPolicyParams;
[WRITE_COMMANDS.CREATE_EXPENSIFY_CARD]: Omit<Parameters.CreateExpensifyCardParams, 'domainAccountID'>;
[WRITE_COMMANDS.CREATE_ADMIN_ISSUED_VIRTUAL_CARD]: Omit<Parameters.CreateExpensifyCardParams, 'feedCountry'>;
[WRITE_COMMANDS.QUEUE_EXPENSIFY_CARD_FOR_BILLING]: Parameters.QueueExpensifyCardForBillingParams;
[WRITE_COMMANDS.ADD_DELEGATE]: Parameters.AddDelegateParams;
[WRITE_COMMANDS.UPDATE_DELEGATE_ROLE]: Parameters.UpdateDelegateRoleParams;
[WRITE_COMMANDS.REMOVE_DELEGATE]: Parameters.RemoveDelegateParams;
Expand Down
10 changes: 10 additions & 0 deletions src/libs/actions/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,15 @@ function updateSelectedFeed(feed: CompanyCardFeed, policyID: string | undefined)
]);
}

function queueExpensifyCardForBilling(feedCountry: string, domainAccountID: number) {
const parameters = {
feedCountry,
domainAccountID,
};

API.write(WRITE_COMMANDS.QUEUE_EXPENSIFY_CARD_FOR_BILLING, parameters);
}

export {
requestReplacementExpensifyCard,
activatePhysicalExpensifyCard,
Expand All @@ -920,5 +929,6 @@ export {
updateSelectedFeed,
deactivateCard,
getCardDefaultName,
queueExpensifyCardForBilling,
};
export type {ReplacementReason};
71 changes: 63 additions & 8 deletions src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import FormHelpMessage from '@components/FormHelpMessage';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import * as PolicyUtils from '@libs/PolicyUtils';
import {getLatestErrorMessage} from '@libs/ErrorUtils';
import {getWorkspaceAccountID} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import WorkspaceCardsListLabel from './WorkspaceCardsListLabel';
Expand All @@ -21,15 +23,55 @@ function WorkspaceCardListHeader({policyID}: WorkspaceCardListHeaderProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID);
const workspaceAccountID = getWorkspaceAccountID(policyID);
const isLessThanMediumScreen = isMediumScreenWidth || isSmallScreenWidth;

const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`);
const [cardManualBilling] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING}${workspaceAccountID}`);

return (
<View style={styles.appBG}>
<View style={[isLessThanMediumScreen ? styles.flexColumn : styles.flexRow, isLessThanMediumScreen ? [styles.mt5, styles.mb3] : styles.mv5, styles.mh5, styles.ph4]}>
<View style={[styles.flexRow, styles.flex1, isLessThanMediumScreen && styles.mb5]}>
const errorMessage = getLatestErrorMessage(cardSettings) ?? '';

const shouldShowSettlementButtonOrDate = !!cardSettings?.isMonthlySettlementAllowed || cardManualBilling;

const getLabelsLayout = () => {
if (!isLessThanMediumScreen) {
return (
<>
<WorkspaceCardsListLabel
type={CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CURRENT_BALANCE}
value={cardSettings?.[CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CURRENT_BALANCE] ?? 0}
/>
<WorkspaceCardsListLabel
type={CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.REMAINING_LIMIT}
value={cardSettings?.[CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.REMAINING_LIMIT] ?? 0}
/>
<WorkspaceCardsListLabel
type={CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CASH_BACK}
value={cardSettings?.[CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CASH_BACK] ?? 0}
/>
</>
);
}
return shouldShowSettlementButtonOrDate ? (
<>
<WorkspaceCardsListLabel
type={CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CURRENT_BALANCE}
value={cardSettings?.[CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CURRENT_BALANCE] ?? 0}
/>
<View style={[styles.flexRow, !isLessThanMediumScreen && styles.flex2, isLessThanMediumScreen && styles.mt5]}>
<WorkspaceCardsListLabel
type={CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.REMAINING_LIMIT}
value={cardSettings?.[CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.REMAINING_LIMIT] ?? 0}
/>
<WorkspaceCardsListLabel
type={CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CASH_BACK}
value={cardSettings?.[CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CASH_BACK] ?? 0}
/>
</View>
</>
) : (
<>
<View style={[styles.flexRow, isLessThanMediumScreen && styles.mb5]}>
<WorkspaceCardsListLabel
type={CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CURRENT_BALANCE}
value={cardSettings?.[CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CURRENT_BALANCE] ?? 0}
Expand All @@ -43,9 +85,22 @@ function WorkspaceCardListHeader({policyID}: WorkspaceCardListHeaderProps) {
type={CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CASH_BACK}
value={cardSettings?.[CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CASH_BACK] ?? 0}
/>
</View>
</>
);
};

<View style={[styles.flexRow, styles.mh5, styles.gap2, styles.p4]}>
return (
<View style={styles.appBG}>
<View style={[isLessThanMediumScreen ? styles.flexColumn : styles.flexRow, styles.mt5, styles.mh5, styles.ph4]}>{getLabelsLayout()}</View>
{!!errorMessage && (
<View style={[styles.mh5, styles.ph4, styles.mt2]}>
<FormHelpMessage
isError
message={errorMessage}
/>
</View>
)}
<View style={[styles.flexRow, styles.mh5, styles.gap2, styles.p4, isLessThanMediumScreen ? styles.mt3 : styles.mt5]}>
<View style={[styles.flexRow, styles.flex4, styles.gap2, styles.alignItemsCenter]}>
<Text
numberOfLines={1}
Expand Down
85 changes: 57 additions & 28 deletions src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useRoute} from '@react-navigation/native';
import {addDays, format} from 'date-fns';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
Expand All @@ -16,14 +17,15 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import getClickedTargetLocation from '@libs/getClickedTargetLocation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import * as PolicyUtils from '@libs/PolicyUtils';
import {getWorkspaceAccountID} from '@libs/PolicyUtils';
import type {FullScreenNavigatorParamList} from '@navigation/types';
import variables from '@styles/variables';
import * as Policy from '@userActions/Policy/Policy';
import * as Report from '@userActions/Report';
import {queueExpensifyCardForBilling} from '@userActions/Card';
import {requestExpensifyCardLimitIncrease} from '@userActions/Policy/Policy';
import {navigateToConciergeChat} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
Expand All @@ -44,21 +46,25 @@ function WorkspaceCardsListLabel({type, value, style}: WorkspaceCardsListLabelPr
const policy = usePolicy(route.params.policyID);
const styles = useThemeStyles();
const {windowWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
const theme = useTheme();
const {translate} = useLocalize();
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
const [isVisible, setVisible] = useState(false);
const [anchorPosition, setAnchorPosition] = useState({top: 0, left: 0});
const anchorRef = useRef(null);

const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(route.params.policyID);
const workspaceAccountID = getWorkspaceAccountID(route.params.policyID);

const policyCurrency = useMemo(() => policy?.outputCurrency ?? CONST.CURRENCY.USD, [policy]);
const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`);
const [cardManualBilling] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING}${workspaceAccountID}`);
const paymentBankAccountID = cardSettings?.paymentBankAccountID;

const isLessThanMediumScreen = isMediumScreenWidth || shouldUseNarrowLayout;

const isConnectedWithPlaid = useMemo(() => {
const bankAccountData = bankAccountList?.[paymentBankAccountID ?? 0]?.accountData;
const bankAccountData = bankAccountList?.[paymentBankAccountID ?? CONST.DEFAULT_NUMBER_ID]?.accountData;

// TODO: remove the extra check when plaidAccountID storing is aligned in https://github.com/Expensify/App/issues/47944
// Right after adding a bank account plaidAccountID is stored inside the accountData and not in the additionalData
Expand All @@ -80,34 +86,57 @@ function WorkspaceCardsListLabel({type, value, style}: WorkspaceCardsListLabelPr
}, [isVisible, windowWidth]);

const requestLimitIncrease = () => {
Policy.requestExpensifyCardLimitIncrease(cardSettings?.paymentBankAccountID);
requestExpensifyCardLimitIncrease(cardSettings?.paymentBankAccountID);
setVisible(false);
Report.navigateToConciergeChat();
navigateToConciergeChat();
};

const isCurrentBalanceType = type === CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CURRENT_BALANCE;
const isSettleBalanceButtonDisplayed = !!cardSettings?.isMonthlySettlementAllowed && !cardManualBilling && isCurrentBalanceType;
const isSettleDateTextDisplayed = !!cardManualBilling && isCurrentBalanceType;

const settlementDate = isSettleDateTextDisplayed ? format(addDays(new Date(), 1), CONST.DATE.FNS_FORMAT_STRING) : '';

const handleSettleBalanceButtonClick = () => {
queueExpensifyCardForBilling(CONST.COUNTRY.US, workspaceAccountID);
};

return (
<View style={styles.flex1}>
<View
ref={anchorRef}
style={[styles.flexRow, styles.alignItemsCenter, styles.mb1, style]}
>
<Text style={[styles.mutedNormalTextLabel, styles.mr1]}>{translate(`workspace.expensifyCard.${type}`)}</Text>
<PressableWithFeedback
accessibilityLabel={translate(`workspace.expensifyCard.${type}`)}
accessibilityRole={CONST.ROLE.BUTTON}
onPress={() => setVisible(true)}
<View style={styles.flex1}>
<View
ref={anchorRef}
style={[styles.flexRow, styles.alignItemsCenter, styles.mb1, style]}
>
<Icon
src={Expensicons.Info}
width={variables.iconSizeExtraSmall}
height={variables.iconSizeExtraSmall}
fill={theme.icon}
/>
</PressableWithFeedback>
<Text style={[styles.mutedNormalTextLabel, styles.mr1]}>{translate(`workspace.expensifyCard.${type}`)}</Text>
<PressableWithFeedback
accessibilityLabel={translate(`workspace.expensifyCard.${type}`)}
accessibilityRole={CONST.ROLE.BUTTON}
onPress={() => setVisible(true)}
>
<Icon
src={Expensicons.Info}
width={variables.iconSizeExtraSmall}
height={variables.iconSizeExtraSmall}
fill={theme.icon}
/>
</PressableWithFeedback>
</View>
<View style={[styles.flexRow, styles.flexWrap]}>
<Text style={[styles.shortTermsHeadline, isSettleBalanceButtonDisplayed && [styles.mb2, styles.mr3]]}>{convertToDisplayString(value, policyCurrency)}</Text>
{isSettleBalanceButtonDisplayed && (
<View style={[styles.mr2, isLessThanMediumScreen && styles.mb3]}>
<Button
onPress={handleSettleBalanceButtonClick}
text={translate('workspace.expensifyCard.settleBalance')}
innerStyles={[styles.buttonSmall]}
textStyles={[styles.buttonSmallText]}
/>
</View>
)}
</View>
</View>

<Text style={styles.shortTermsHeadline}>{CurrencyUtils.convertToDisplayString(value, policyCurrency)}</Text>

{isSettleDateTextDisplayed && <Text style={[styles.mutedNormalTextLabel, styles.mt1]}>{translate('workspace.expensifyCard.balanceWillBeSettledOn', {settlementDate})}</Text>}
<Popover
onClose={() => setVisible(false)}
isVisible={isVisible}
Expand Down

0 comments on commit 12254ea

Please sign in to comment.