Skip to content

Commit

Permalink
Fix(Expense form): Account balance payout method fixes (#11065)
Browse files Browse the repository at this point in the history
* Fix(Expense form): Account balance payout method fixes

* update graphql

* Fix merge

* Remove unused code

* Set newPayoutMethodType options for non logged in user

---------

Co-authored-by: Henrique <[email protected]>
  • Loading branch information
gustavlrsn and hdiniz authored Mar 5, 2025
1 parent 46312ba commit ea06ae3
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 22 deletions.
24 changes: 17 additions & 7 deletions components/expenses/EditExpenseDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { z } from 'zod';

import { i18nGraphqlException } from '../../lib/errors';
import { API_V2_CONTEXT } from '../../lib/graphql/helpers';
import type { Currency, CurrencyExchangeRateInput } from '../../lib/graphql/types/v2/graphql';
import { type Currency, type CurrencyExchangeRateInput, PayoutMethodType } from '../../lib/graphql/types/v2/graphql';
import { cn } from '../../lib/utils';
import type { Expense } from '@/lib/graphql/types/v2/schema';

Expand Down Expand Up @@ -87,9 +87,14 @@ const EditPayee = ({ expense, onSubmit }) => {
payoutMethod:
!values.payoutMethodId || values.payoutMethodId === '__newPayoutMethod'
? { ...values.newPayoutMethod, isSaved: false }
: {
id: values.payoutMethodId,
},
: values.payoutMethodId === '__newAccountBalancePayoutMethod'
? {
type: PayoutMethodType.ACCOUNT_BALANCE,
data: {},
}
: {
id: values.payoutMethodId,
},
}),
};
return onSubmit(editValues);
Expand Down Expand Up @@ -167,9 +172,14 @@ const EditPayoutMethod = ({ expense, onSubmit }) => {
payoutMethod:
!values.payoutMethodId || values.payoutMethodId === '__newPayoutMethod'
? { ...values.newPayoutMethod, isSaved: false }
: {
id: values.payoutMethodId,
},
: values.payoutMethodId === '__newAccountBalancePayoutMethod'
? {
type: PayoutMethodType.ACCOUNT_BALANCE,
data: {},
}
: {
id: values.payoutMethodId,
},
payee: {
slug: formOptions.payee?.slug,
},
Expand Down
11 changes: 8 additions & 3 deletions components/submit-expense/SubmitExpenseFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,14 @@ export function SubmitExpenseFlow(props: SubmitExpenseFlowProps) {
payoutMethod:
!values.payoutMethodId || values.payoutMethodId === '__newPayoutMethod'
? { ...values.newPayoutMethod, isSaved: false }
: {
id: values.payoutMethodId,
},
: values.payoutMethodId === '__newAccountBalancePayoutMethod'
? {
type: PayoutMethodType.ACCOUNT_BALANCE,
data: {},
}
: {
id: values.payoutMethodId,
},
type: values.expenseTypeOption,
accountingCategory: values.accountingCategoryId
? {
Expand Down
17 changes: 9 additions & 8 deletions components/submit-expense/form/PayoutMethodSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function getFormProps(form: ExpenseForm) {
...pick(form.options, [
'payee',
'payoutMethods',
'newPayoutMethodTypes',
'recentlySubmittedExpenses',
'isAdminOfPayee',
'loggedInAccount',
Expand Down Expand Up @@ -198,7 +199,7 @@ export const PayoutMethodFormContent = memoWithGetFormProps(function PayoutMetho
</RadioGroupCard>
)}

{!(isLoading || isLoadingPayee) && !isVendor && (
{!(isLoading || isLoadingPayee) && !isVendor && props.newPayoutMethodTypes?.length > 0 && (
<RadioGroupCard
value="__newPayoutMethod"
checked={isNewPayoutMethodSelected}
Expand Down Expand Up @@ -250,8 +251,7 @@ function getNewPayoutMethodOptionFormProps(form: ExpenseForm) {
return {
...pick(form, ['setFieldValue', 'setFieldTouched', 'validateForm', 'refresh', 'isSubmitting']),
...pick(form.values, ['newPayoutMethod', 'payeeSlug']),
...pick(form.options, ['supportedPayoutMethods', 'host', 'loggedInAccount', 'payee']),
touchedNewPayoutMethodName: form.touched.newPayoutMethod?.name,
...pick(form.options, ['newPayoutMethodTypes', 'payoutMethods', 'host', 'loggedInAccount', 'payee']),
};
}

Expand Down Expand Up @@ -314,13 +314,13 @@ const NewPayoutMethodOption = memoWithGetFormProps(function NewPayoutMethodOptio
}
}, [createPayoutMethod, intl, props.newPayoutMethod, refresh, setFieldTouched, setFieldValue, toast, validateForm]);

const suportedPayoutMethodComboOptions = React.useMemo(
const newPayoutMethodComboOptions = React.useMemo(
() =>
props.supportedPayoutMethods.map(m => ({
props.newPayoutMethodTypes.map(m => ({
value: m,
label: intl.formatMessage(I18nPayoutMethodLabels[m]),
})),
[intl, props.supportedPayoutMethods],
[intl, props.newPayoutMethodTypes],
);

const onPayoutMethodTypeChange = React.useCallback(
Expand Down Expand Up @@ -358,7 +358,7 @@ const NewPayoutMethodOption = memoWithGetFormProps(function NewPayoutMethodOptio
<ComboSelect
{...field}
disabled={props.isSubmitting}
options={suportedPayoutMethodComboOptions}
options={newPayoutMethodComboOptions}
onChange={onPayoutMethodTypeChange}
/>
)}
Expand Down Expand Up @@ -459,6 +459,7 @@ export const PayoutMethodRadioGroupItem = function PayoutMethodRadioGroupItem(pr
const intl = useIntl();
const { toast } = useToast();

const isEditable = props.payoutMethod.type !== PayoutMethodType.ACCOUNT_BALANCE && props.isEditable;
const isMissingCurrency = isEmpty(props.payoutMethod.data?.currency);
const isLegalNameFuzzyMatched = React.useMemo(() => {
const accountHolderName: string = props.payoutMethod.data?.accountHolderName ?? '';
Expand Down Expand Up @@ -821,7 +822,7 @@ export const PayoutMethodRadioGroupItem = function PayoutMethodRadioGroupItem(pr
</Badge>
)}
</div>
{!isEditingPayoutMethod && props.isEditable && (
{!isEditingPayoutMethod && isEditable && (
<div className="flex gap-2">
{!props.archived && props.isChecked && (
<Button
Expand Down
33 changes: 32 additions & 1 deletion components/submit-expense/useExpenseForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ type ExpenseFormOptions = {
| ExpenseFormSchemaQuery['loggedInAccount']['payoutMethods'][number]
| ExpenseFormValues['newPayoutMethod'];
supportedPayoutMethods?: PayoutMethodType[];
newPayoutMethodTypes?: PayoutMethodType[];
expenseTags?: ExpenseFormSchemaHostFieldsFragment['expensesTags'];
isAccountingCategoryRequired?: boolean;
accountingCategories?: ExpenseFormSchemaHostFieldsFragment['accountingCategories']['nodes'];
Expand Down Expand Up @@ -649,7 +650,12 @@ function buildFormSchema(
if (['__invite', '__inviteSomeone', '__inviteExistingUser'].includes(values.payeeSlug)) {
return true;
}

if (
v === '__newAccountBalancePayoutMethod' &&
options.payoutMethods?.some(pm => pm.type === PayoutMethodType.ACCOUNT_BALANCE)
) {
return true;
}
if (v && v !== '__newPayoutMethod' && !options.payee?.payoutMethods?.some(pm => pm.id === v)) {
return false;
}
Expand Down Expand Up @@ -994,6 +1000,10 @@ function buildFormSchema(
return true;
}

if (values.payoutMethodId === '__newAccountBalancePayoutMethod') {
return true;
}

if (values.payoutMethodId === '__newPayoutMethod') {
return !!currency;
}
Expand Down Expand Up @@ -1336,10 +1346,30 @@ async function buildFormOptions(
}
return pm;
});

// Add ACCOUNT_BALANCE payout method if it's supported but not available for the payee
if (
options.supportedPayoutMethods?.includes(PayoutMethodType.ACCOUNT_BALANCE) &&
host &&
!options.payoutMethods?.some(pm => pm.type === PayoutMethodType.ACCOUNT_BALANCE)
) {
options.payoutMethods = [
...(options.payoutMethods || []),
{
id: '__newAccountBalancePayoutMethod',
type: PayoutMethodType.ACCOUNT_BALANCE,
data: { currency: host.currency },
isSaved: true,
},
];
}
} else if (payee && payee.type === CollectiveType.VENDOR) {
options.payoutMethods = payee.payoutMethods?.filter(p => options.supportedPayoutMethods.includes(p.type));
}

// Filter out ACCOUNT_BALANCE from the list of payout methods, since we add it manually to the default list
options.newPayoutMethodTypes = options.supportedPayoutMethods.filter(t => t !== PayoutMethodType.ACCOUNT_BALANCE);

if (values.payoutMethodId && values.payoutMethodId !== '__newPayoutMethod') {
options.payoutMethod = options.payoutMethods?.find(p => p.id === values.payoutMethodId);
} else if (values.payoutMethodId === '__newPayoutMethod') {
Expand All @@ -1353,6 +1383,7 @@ async function buildFormOptions(
values.payeeSlug === '__findAccountIAdminister';
} else {
options.payoutMethod = values.newPayoutMethod;
options.newPayoutMethodTypes = options.supportedPayoutMethods;
}

if (!startOptions.duplicateExpense && options.expense?.lockedFields?.length) {
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/types/v2/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ const documents = {
"\n query ApplyToHost($hostSlug: String!, $collectiveSlug: String!) {\n host(slug: $hostSlug) {\n id\n ...ApplyToHostFields\n }\n account(slug: $collectiveSlug) {\n id\n ...ApplyToHostAccountFields\n }\n }\n \n \n": types.ApplyToHostDocument,
"\n query ApplyToHostWithAccounts($hostSlug: String!) {\n host(slug: $hostSlug) {\n id\n ...ApplyToHostFields\n }\n loggedInAccount {\n id\n memberOf(role: ADMIN, accountType: [COLLECTIVE, FUND], isApproved: false, isArchived: false) {\n nodes {\n id\n account {\n id\n ...ApplyToHostAccountFields\n ... on AccountWithHost {\n host {\n id\n legacyId\n }\n }\n }\n }\n }\n }\n }\n \n \n": types.ApplyToHostWithAccountsDocument,
"\n mutation ApplyToNewHost(\n $collective: AccountReferenceInput!\n $host: AccountReferenceInput!\n $message: String\n $inviteMembers: [InviteMemberInput]\n ) {\n applyToHost(collective: $collective, host: $host, message: $message, inviteMembers: $inviteMembers) {\n id\n slug\n ... on AccountWithHost {\n isActive\n isApproved\n host {\n id\n ...ApplyToHostFields\n }\n }\n }\n }\n \n": types.ApplyToNewHostDocument,
"\n mutation RemovePaymentMethod($paymentMethod: PaymentMethodReferenceInput!) {\n removePaymentMethod(paymentMethod: $paymentMethod, cancelActiveSubscriptions: false) {\n id\n }\n }\n ": types.RemovePaymentMethodDocument,
"\n mutation RemovePaymentMethod($paymentMethod: PaymentMethodReferenceInput!) {\n removePaymentMethod(paymentMethod: $paymentMethod, cancelActiveSubscriptions: true) {\n id\n }\n }\n ": types.RemovePaymentMethodDocument,
"\n mutation ConfirmOrder($order: OrderReferenceInput!) {\n confirmOrder(order: $order) {\n order {\n id\n status\n transactions {\n id\n }\n fromAccount {\n id\n slug\n }\n }\n stripeError {\n message\n account\n response\n }\n }\n }\n ": types.ConfirmOrderDocument,
"\n mutation PaymentInfoRemovePayoutMethod($payoutMethodId: String!) {\n removePayoutMethod(payoutMethodId: $payoutMethodId) {\n id\n ...PayoutMethodFields\n }\n }\n \n ": types.PaymentInfoRemovePayoutMethodDocument,
"\n mutation PaymentInfoRestorePayoutMethod($payoutMethod: PayoutMethodReferenceInput!) {\n restorePayoutMethod(payoutMethod: $payoutMethod) {\n id\n ...PayoutMethodFields\n }\n }\n \n ": types.PaymentInfoRestorePayoutMethodDocument,
Expand Down Expand Up @@ -991,7 +991,7 @@ export function graphql(source: "\n mutation ApplyToNewHost(\n $collective:
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation RemovePaymentMethod($paymentMethod: PaymentMethodReferenceInput!) {\n removePaymentMethod(paymentMethod: $paymentMethod, cancelActiveSubscriptions: false) {\n id\n }\n }\n "): (typeof documents)["\n mutation RemovePaymentMethod($paymentMethod: PaymentMethodReferenceInput!) {\n removePaymentMethod(paymentMethod: $paymentMethod, cancelActiveSubscriptions: false) {\n id\n }\n }\n "];
export function graphql(source: "\n mutation RemovePaymentMethod($paymentMethod: PaymentMethodReferenceInput!) {\n removePaymentMethod(paymentMethod: $paymentMethod, cancelActiveSubscriptions: true) {\n id\n }\n }\n "): (typeof documents)["\n mutation RemovePaymentMethod($paymentMethod: PaymentMethodReferenceInput!) {\n removePaymentMethod(paymentMethod: $paymentMethod, cancelActiveSubscriptions: true) {\n id\n }\n }\n "];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading

0 comments on commit ea06ae3

Please sign in to comment.