Update the Create Linked Account workflow to prompt for Starting Date and Balance (#6629)

* feat: Add optional starting date and balance for bank sync accounts

Adds the ability to specify a custom starting date and balance when
linking new bank sync accounts in the Select Linked Accounts modal.

Addresses: https://discord.com/channels/937901803608096828/1402270361625563186

Changes:
- Frontend: Added inline date and amount input fields in the account
  linking table for new accounts
- Redux: Extended link account actions to accept startingDate and
  startingBalance parameters
- Backend: Updated account linking handlers to pass custom values to
  sync logic
- Sync: Modified syncAccount and processBankSyncDownload to use custom
  starting date/balance for initial sync transactions

Features:
- Only displays starting options when creating new accounts (not upgrades)
- AmountInput with smart sign detection based on account balance
  (negative for credit cards/loans)
- Defaults to 90 days ago for date and 0 for balance
- Mobile-responsive with separate AccountCard layout
- Works across all sync providers: GoCardless, SimpleFIN, Pluggy.ai

The custom starting balance is used directly for the starting balance
transaction, and the custom starting date determines both the sync
start date and the transaction date for the starting balance entry.

* refactor: Extract shared types and components for starting balance inputs

- Create CustomStartingSettings type to replace repeated inline type definitions
- Extract StartingOptionsInput component to consolidate duplicate UI between mobile/desktop views
- Create LinkAccountBasePayload type shared across GoCardless, SimpleFIN, and PluggyAI link functions
- Apply same base type pattern to server-side link account handlers

This simplifies the code introduced for custom starting date/balance when linking bank accounts.

[autofix.ci] apply automated fixes

* allow explicit zero values

* refactor: add type guard for BankSyncError to remove oxlint-disable

- Create isBankSyncError() type guard function with proper type narrowing
- Remove oxlint-disable-next-line comment that suppressed the no-explicit-any rule
- Add JSDoc comments for both isBankSyncError and handleSyncError functions
- Remove redundant type assertion now that type guard narrows correctly

* refactor: address code review nitpicks for SelectLinkedAccountsModal

- Use locale-aware date formatting instead of toISOString()
- Extract isNewAccountOption helper to reduce duplication
- Align AccountCardProps type definition pattern with TableRowProps

* Add placeholder date/balance for already linked accounts

* [autofix.ci] apply automated fixes

* Use StartingBalanceInfo only, and add mobile view

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Brown II
2026-02-06 15:44:30 -06:00
committed by GitHub
parent e72f18c5db
commit 1a26253457
5 changed files with 497 additions and 64 deletions

View File

@@ -245,17 +245,30 @@ export const unlinkAccount = createAppAsyncThunk(
},
);
type LinkAccountPayload = {
// Shared base type for link account payloads
type LinkAccountBasePayload = {
upgradingId?: AccountEntity['id'];
offBudget?: boolean;
startingDate?: string;
startingBalance?: number;
};
type LinkAccountPayload = LinkAccountBasePayload & {
requisitionId: string;
account: SyncServerGoCardlessAccount;
upgradingId?: AccountEntity['id'] | undefined;
offBudget?: boolean | undefined;
};
export const linkAccount = createAppAsyncThunk(
`${sliceName}/linkAccount`,
async (
{ requisitionId, account, upgradingId, offBudget }: LinkAccountPayload,
{
requisitionId,
account,
upgradingId,
offBudget,
startingDate,
startingBalance,
}: LinkAccountPayload,
{ dispatch },
) => {
await send('gocardless-accounts-link', {
@@ -263,50 +276,64 @@ export const linkAccount = createAppAsyncThunk(
account,
upgradingId,
offBudget,
startingDate,
startingBalance,
});
dispatch(markPayeesDirty());
dispatch(markAccountsDirty());
},
);
type LinkAccountSimpleFinPayload = {
type LinkAccountSimpleFinPayload = LinkAccountBasePayload & {
externalAccount: SyncServerSimpleFinAccount;
upgradingId?: AccountEntity['id'] | undefined;
offBudget?: boolean | undefined;
};
export const linkAccountSimpleFin = createAppAsyncThunk(
`${sliceName}/linkAccountSimpleFin`,
async (
{ externalAccount, upgradingId, offBudget }: LinkAccountSimpleFinPayload,
{
externalAccount,
upgradingId,
offBudget,
startingDate,
startingBalance,
}: LinkAccountSimpleFinPayload,
{ dispatch },
) => {
await send('simplefin-accounts-link', {
externalAccount,
upgradingId,
offBudget,
startingDate,
startingBalance,
});
dispatch(markPayeesDirty());
dispatch(markAccountsDirty());
},
);
type LinkAccountPluggyAiPayload = {
type LinkAccountPluggyAiPayload = LinkAccountBasePayload & {
externalAccount: SyncServerPluggyAiAccount;
upgradingId?: AccountEntity['id'];
offBudget?: boolean;
};
export const linkAccountPluggyAi = createAppAsyncThunk(
`${sliceName}/linkAccountPluggyAi`,
async (
{ externalAccount, upgradingId, offBudget }: LinkAccountPluggyAiPayload,
{
externalAccount,
upgradingId,
offBudget,
startingDate,
startingBalance,
}: LinkAccountPluggyAiPayload,
{ dispatch },
) => {
await send('pluggyai-accounts-link', {
externalAccount,
upgradingId,
offBudget,
startingDate,
startingBalance,
});
dispatch(markPayeesDirty());
dispatch(markAccountsDirty());

View File

@@ -1,15 +1,18 @@
import React, { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
import { Input } from '@actual-app/components/input';
import { SpaceBetween } from '@actual-app/components/space-between';
import { styles } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { Tooltip } from '@actual-app/components/tooltip';
import { View } from '@actual-app/components/view';
import { format as formatDate, parseISO } from 'date-fns';
import { currentDay, subDays } from 'loot-core/shared/months';
import {
type AccountEntity,
type SyncServerGoCardlessAccount,
@@ -41,9 +44,13 @@ import {
Table,
TableHeader,
} from '@desktop-client/components/table';
import { AmountInput } from '@desktop-client/components/util/AmountInput';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { closeModal } from '@desktop-client/modals/modalsSlice';
import { transactions } from '@desktop-client/queries';
import { liveQuery } from '@desktop-client/queries/liveQuery';
import { useDispatch } from '@desktop-client/redux';
function useAddBudgetAccountOptions() {
@@ -61,6 +68,20 @@ function useAddBudgetAccountOptions() {
return { addOnBudgetAccountOption, addOffBudgetAccountOption };
}
/**
* Helper to determine if the chosen account option represents creating a new account.
*/
function isNewAccountOption(
chosenAccountId: string | undefined,
addOnBudgetOptionId: string,
addOffBudgetOptionId: string,
): boolean {
return (
chosenAccountId === addOnBudgetOptionId ||
chosenAccountId === addOffBudgetOptionId
);
}
export type SelectLinkedAccountsModalProps =
| {
requisitionId: string;
@@ -129,6 +150,9 @@ export function SelectLinkedAccountsModal({
);
},
);
const [customStartingDates, setCustomStartingDates] = useState<
Record<string, StartingBalanceInfo>
>({});
const { addOnBudgetAccountOption, addOffBudgetAccountOption } =
useAddBudgetAccountOptions();
@@ -158,6 +182,14 @@ export function SelectLinkedAccountsModal({
}
// Finally link the matched account
const customSettings = customStartingDates[chosenExternalAccountId];
const startingDate =
customSettings?.date && customSettings.date.trim() !== ''
? customSettings.date
: undefined;
const startingBalance =
customSettings?.amount != null ? customSettings.amount : undefined;
if (propsWithSortedExternalAccounts.syncSource === 'simpleFin') {
dispatch(
linkAccountSimpleFin({
@@ -171,6 +203,8 @@ export function SelectLinkedAccountsModal({
? chosenLocalAccountId
: undefined,
offBudget,
startingDate,
startingBalance,
}),
);
} else if (propsWithSortedExternalAccounts.syncSource === 'pluggyai') {
@@ -186,6 +220,8 @@ export function SelectLinkedAccountsModal({
? chosenLocalAccountId
: undefined,
offBudget,
startingDate,
startingBalance,
}),
);
} else {
@@ -202,6 +238,8 @@ export function SelectLinkedAccountsModal({
? chosenLocalAccountId
: undefined,
offBudget,
startingDate,
startingBalance,
}),
);
}
@@ -255,6 +293,33 @@ export function SelectLinkedAccountsModal({
return localAccounts.find(acc => acc.id === chosenId);
};
// Memoize default starting settings to avoid repeated calculations
const defaultStartingSettings = useMemo<StartingBalanceInfo>(
() => ({
date: subDays(currentDay(), 90),
amount: 0,
}),
[],
);
const getCustomStartingDate = (accountId: string) => {
if (customStartingDates[accountId]) {
return customStartingDates[accountId];
}
// Default to 90 days ago (matches server default)
return defaultStartingSettings;
};
const setCustomStartingDate = (
accountId: string,
settings: StartingBalanceInfo,
) => {
setCustomStartingDates(prev => ({
...prev,
[accountId]: settings,
}));
};
const label = useMemo(() => {
const s = new Set(draftLinkAccounts.values());
if (s.has('linking') && s.has('unlinking')) {
@@ -325,6 +390,8 @@ export function SelectLinkedAccountsModal({
chosenAccount={getChosenAccount(account.account_id)}
unlinkedAccounts={unlinkedAccounts}
onSetLinkedAccount={onSetLinkedAccount}
customStartingDate={getCustomStartingDate(account.account_id)}
onSetCustomStartingDate={setCustomStartingDate}
/>
))}
</View>
@@ -333,35 +400,44 @@ export function SelectLinkedAccountsModal({
style={{ ...styles.tableContainer, height: 300, flex: 'unset' }}
>
<TableHeader>
<Cell value={t('Institution to Sync')} width={175} />
<Cell value={t('Bank Account To Sync')} width={175} />
<Cell value={t('Balance')} width={80} />
<Cell value={t('Institution to Sync')} width={150} />
<Cell value={t('Bank Account To Sync')} width={150} />
<Cell value={t('Balance')} width={120} />
<Cell value={t('Account in Actual')} width="flex" />
<Cell value={t('Actions')} width={150} />
<Cell value={t('Starting Date')} width={120} />
<Cell value={t('Starting Balance')} width={120} />
<Cell value={t('Actions')} width={150} textAlign="center" />
</TableHeader>
<Table<
SelectLinkedAccountsModalProps['externalAccounts'][number] & {
id: string;
}
>
<Table<ExternalAccount & { id: string }>
items={propsWithSortedExternalAccounts.externalAccounts.map(
account => ({
...account,
id: account.account_id,
}),
acc => ({ ...acc, id: acc.account_id }),
)}
style={{ backgroundColor: theme.tableHeaderBackground }}
renderItem={({ item }) => (
<View key={item.id}>
renderItem={({ item }) => {
const chosenAccount = getChosenAccount(item.account_id);
// Only show starting options for new accounts being created
const shouldShowStartingOptions = isNewAccountOption(
chosenAccount?.id,
addOnBudgetAccountOption.id,
addOffBudgetAccountOption.id,
);
return (
<TableRow
key={item.id}
externalAccount={item}
chosenAccount={getChosenAccount(item.account_id)}
chosenAccount={chosenAccount}
unlinkedAccounts={unlinkedAccounts}
onSetLinkedAccount={onSetLinkedAccount}
customStartingDate={getCustomStartingDate(
item.account_id,
)}
onSetCustomStartingDate={setCustomStartingDate}
showStartingOptions={shouldShowStartingOptions}
/>
</View>
)}
);
}}
/>
</View>
)}
@@ -407,6 +483,11 @@ type ExternalAccount =
| SyncServerSimpleFinAccount
| SyncServerPluggyAiAccount;
type StartingBalanceInfo = {
date: string;
amount: number;
};
type SharedAccountRowProps = {
externalAccount: ExternalAccount;
chosenAccount: { id: string; name: string } | undefined;
@@ -435,19 +516,64 @@ function getAvailableAccountOptions(
return options;
}
type TableRowProps = SharedAccountRowProps;
type TableRowProps = SharedAccountRowProps & {
customStartingDate: StartingBalanceInfo;
onSetCustomStartingDate: (
accountId: string,
settings: StartingBalanceInfo,
) => void;
showStartingOptions: boolean;
};
function useStartingBalanceInfo(accountId: string | undefined) {
const [info, setInfo] = useState<StartingBalanceInfo | null>(null);
useEffect(() => {
if (!accountId) {
setInfo(null);
return;
}
const query = transactions(accountId)
.filter({ starting_balance_flag: true })
.select(['date', 'amount'])
.limit(1);
const live = liveQuery<StartingBalanceInfo>(query, {
onData: data => {
setInfo(data?.[0] ?? null);
},
onError: () => {
setInfo(null);
},
});
return () => {
live?.unsubscribe();
};
}, [accountId]);
return info;
}
function TableRow({
externalAccount,
chosenAccount,
unlinkedAccounts,
onSetLinkedAccount,
customStartingDate,
onSetCustomStartingDate,
showStartingOptions,
}: TableRowProps) {
const [focusedField, setFocusedField] = useState<string | null>(null);
const { addOnBudgetAccountOption, addOffBudgetAccountOption } =
useAddBudgetAccountOptions();
const format = useFormat();
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
const { t } = useTranslation();
const startingBalanceInfo = useStartingBalanceInfo(
showStartingOptions ? undefined : chosenAccount?.id,
);
const availableAccountOptions = getAvailableAccountOptions(
unlinkedAccounts,
@@ -458,7 +584,8 @@ function TableRow({
return (
<Row style={{ backgroundColor: theme.tableBackground }}>
<Field width={175}>
{/* Institution to Sync */}
<Field width={150}>
<Tooltip content={getInstitutionName(externalAccount)}>
<View
style={{
@@ -471,7 +598,8 @@ function TableRow({
</View>
</Tooltip>
</Field>
<Field width={175}>
{/* Bank Account To Sync */}
<Field width={150}>
<Tooltip content={externalAccount.name}>
<View
style={{
@@ -484,7 +612,8 @@ function TableRow({
</View>
</Tooltip>
</Field>
<Field width={80}>
{/* Balance */}
<Field width={120} style={{ textAlign: 'right' }}>
<PrivacyFilter>
{externalAccount.balance != null ? (
<FinancialText>
@@ -495,6 +624,7 @@ function TableRow({
)}
</PrivacyFilter>
</Field>
{/* Account in Actual */}
<Field
width="flex"
truncate={focusedField !== 'account'}
@@ -518,6 +648,47 @@ function TableRow({
chosenAccount?.name
)}
</Field>
{showStartingOptions ? (
<StartingOptionsFields
accountId={externalAccount.account_id}
externalBalance={externalAccount.balance}
customStartingDate={customStartingDate}
onSetCustomStartingDate={onSetCustomStartingDate}
layout="inline"
/>
) : (
<>
{/* Starting Date */}
<Field width={120} truncate={false} style={{ textAlign: 'right' }}>
{startingBalanceInfo ? (
<Text
style={{
color: theme.pageTextSubdued,
fontStyle: 'italic',
}}
>
{formatDate(parseISO(startingBalanceInfo.date), dateFormat)}
</Text>
) : null}
</Field>
{/* Starting Balance */}
<Field width={120} truncate={false} style={{ textAlign: 'right' }}>
{startingBalanceInfo ? (
<PrivacyFilter>
<FinancialText
style={{
color: theme.pageTextSubdued,
fontStyle: 'italic',
}}
>
{format(startingBalanceInfo.amount, 'financial')}
</FinancialText>
</PrivacyFilter>
) : null}
</Field>
</>
)}
{/* Actions */}
<Field width={150}>
{chosenAccount ? (
<Button
@@ -558,18 +729,141 @@ function getInstitutionName(
return '';
}
type AccountCardProps = SharedAccountRowProps;
type StartingOptionsFieldsProps = {
accountId: string;
externalBalance: number | null | undefined;
customStartingDate: StartingBalanceInfo;
onSetCustomStartingDate: (
accountId: string,
settings: StartingBalanceInfo,
) => void;
layout: 'inline' | 'stacked';
};
function StartingOptionsFields({
accountId,
externalBalance,
customStartingDate,
onSetCustomStartingDate,
layout,
}: StartingOptionsFieldsProps) {
const zeroSign = externalBalance != null && externalBalance < 0 ? '-' : '+';
if (layout === 'inline') {
return (
<>
{/* Starting Date */}
<Field width={120} truncate={false}>
<Input
type="date"
value={customStartingDate.date}
onChange={e =>
onSetCustomStartingDate(accountId, {
...customStartingDate,
date: e.target.value,
})
}
style={{ width: '100%' }}
/>
</Field>
{/* Starting Balance */}
<Field width={120} truncate={false} style={{ textAlign: 'right' }}>
<AmountInput
value={customStartingDate.amount}
zeroSign={zeroSign}
onUpdate={amount =>
onSetCustomStartingDate(accountId, {
...customStartingDate,
amount,
})
}
style={{ width: '100%' }}
/>
</Field>
</>
);
}
return (
<View
style={{
marginTop: 8,
padding: '12px',
backgroundColor: theme.tableHeaderBackground,
borderRadius: 4,
}}
>
<View style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<View>
<Text
style={{
marginBottom: 4,
fontSize: 13,
color: theme.pageTextSubdued,
}}
>
<Trans>Starting date:</Trans>
</Text>
<Input
type="date"
value={customStartingDate.date}
onChange={e =>
onSetCustomStartingDate(accountId, {
...customStartingDate,
date: e.target.value,
})
}
style={{ width: '100%' }}
/>
</View>
<View>
<Text
style={{
marginBottom: 4,
fontSize: 13,
color: theme.pageTextSubdued,
}}
>
<Trans>Balance on that date:</Trans>
</Text>
<AmountInput
value={customStartingDate.amount}
zeroSign={zeroSign}
onUpdate={amount =>
onSetCustomStartingDate(accountId, {
...customStartingDate,
amount,
})
}
style={{ width: '100%' }}
/>
</View>
</View>
</View>
);
}
type AccountCardProps = SharedAccountRowProps & {
customStartingDate: StartingBalanceInfo;
onSetCustomStartingDate: (
accountId: string,
settings: StartingBalanceInfo,
) => void;
};
function AccountCard({
externalAccount,
chosenAccount,
unlinkedAccounts,
onSetLinkedAccount,
customStartingDate,
onSetCustomStartingDate,
}: AccountCardProps) {
const [focusedField, setFocusedField] = useState<string | null>(null);
const { addOnBudgetAccountOption, addOffBudgetAccountOption } =
useAddBudgetAccountOptions();
const format = useFormat();
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
const { t } = useTranslation();
const availableAccountOptions = getAvailableAccountOptions(
@@ -579,6 +873,16 @@ function AccountCard({
addOffBudgetAccountOption,
);
// Only show starting date options for new accounts being created
const shouldShowStartingOptions = isNewAccountOption(
chosenAccount?.id,
addOnBudgetAccountOption.id,
addOffBudgetAccountOption.id,
);
const startingBalanceInfo = useStartingBalanceInfo(
shouldShowStartingOptions ? undefined : chosenAccount?.id,
);
return (
<SpaceBetween
direction="vertical"
@@ -655,6 +959,47 @@ function AccountCard({
)}
</SpaceBetween>
{!shouldShowStartingOptions && startingBalanceInfo && (
<View
style={{
fontSize: '0.9em',
color: theme.pageTextSubdued,
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<View style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
<Text style={{ color: theme.pageTextSubdued }}>
<Trans>Starting date:</Trans>
</Text>
<Text
style={{
color: theme.pageTextSubdued,
fontStyle: 'italic',
}}
>
{formatDate(parseISO(startingBalanceInfo.date), dateFormat)}
</Text>
</View>
<View style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
<Text style={{ color: theme.pageTextSubdued }}>
<Trans>Starting balance:</Trans>
</Text>
<PrivacyFilter>
<FinancialText
style={{
color: theme.pageTextSubdued,
fontStyle: 'italic',
}}
>
{format(startingBalanceInfo.amount, 'financial')}
</FinancialText>
</PrivacyFilter>
</View>
</View>
)}
{focusedField === 'account' && (
<View style={{ marginBottom: 12 }}>
<Autocomplete
@@ -675,6 +1020,16 @@ function AccountCard({
</View>
)}
{shouldShowStartingOptions && (
<StartingOptionsFields
accountId={externalAccount.account_id}
externalBalance={externalAccount.balance}
customStartingDate={customStartingDate}
onSetCustomStartingDate={onSetCustomStartingDate}
layout="stacked"
/>
)}
{chosenAccount ? (
<Button
onPress={() => {

View File

@@ -38,6 +38,14 @@ import * as link from './link';
import { getStartingBalancePayee } from './payees';
import * as bankSync from './sync';
// Shared base type for link account parameters
type LinkAccountBaseParams = {
upgradingId?: AccountEntity['id'];
offBudget?: boolean;
startingDate?: string;
startingBalance?: number;
};
export type AccountHandlers = {
'account-update': typeof updateAccount;
'accounts-get': typeof getAccounts;
@@ -120,11 +128,11 @@ async function linkGoCardlessAccount({
account,
upgradingId,
offBudget = false,
}: {
startingDate,
startingBalance,
}: LinkAccountBaseParams & {
requisitionId: string;
account: SyncServerGoCardlessAccount;
upgradingId?: AccountEntity['id'] | undefined;
offBudget?: boolean | undefined;
}) {
let id;
const bank = await link.findOrCreateBank(account.institution, requisitionId);
@@ -170,6 +178,8 @@ async function linkGoCardlessAccount({
id,
account.account_id,
bank.bank_id,
startingDate,
startingBalance,
);
connection.send('sync-event', {
@@ -184,10 +194,10 @@ async function linkSimpleFinAccount({
externalAccount,
upgradingId,
offBudget = false,
}: {
startingDate,
startingBalance,
}: LinkAccountBaseParams & {
externalAccount: SyncServerSimpleFinAccount;
upgradingId?: AccountEntity['id'] | undefined;
offBudget?: boolean | undefined;
}) {
let id;
@@ -240,6 +250,8 @@ async function linkSimpleFinAccount({
id,
externalAccount.account_id,
bank.bank_id,
startingDate,
startingBalance,
);
await connection.send('sync-event', {
@@ -254,10 +266,10 @@ async function linkPluggyAiAccount({
externalAccount,
upgradingId,
offBudget = false,
}: {
startingDate,
startingBalance,
}: LinkAccountBaseParams & {
externalAccount: SyncServerPluggyAiAccount;
upgradingId?: AccountEntity['id'] | undefined;
offBudget?: boolean | undefined;
}) {
let id;
@@ -310,6 +322,8 @@ async function linkPluggyAiAccount({
id,
externalAccount.account_id,
bank.bank_id,
startingDate,
startingBalance,
);
await connection.send('sync-event', {
@@ -845,24 +859,37 @@ type SyncError =
internal?: string;
};
/**
* Type guard to check if an error is a BankSyncError.
* Handles both class instances and plain objects with the BankSyncError shape.
*/
function isBankSyncError(err: unknown): err is BankSyncError {
return (
err instanceof BankSyncError ||
(typeof err === 'object' &&
err !== null &&
'type' in err &&
err.type === 'BankSyncError')
);
}
/**
* Converts a sync error into a standardized SyncError response object.
*/
function handleSyncError(
err: Error | PostError | BankSyncError,
acct: db.DbAccount,
): SyncError {
// TODO: refactor bank sync logic to use BankSyncError properly
// oxlint-disable-next-line typescript/no-explicit-any
if (err instanceof BankSyncError || (err as any)?.type === 'BankSyncError') {
const error = err as BankSyncError;
if (isBankSyncError(err)) {
const syncError = {
type: 'SyncError',
accountId: acct.id,
message: 'Failed syncing account "' + acct.name + '."',
category: error.category,
code: error.code,
category: err.category,
code: err.code,
};
if (error.category === 'RATE_LIMIT_EXCEEDED') {
if (err.category === 'RATE_LIMIT_EXCEEDED') {
return {
...syncError,
message: `Failed syncing account ${acct.name}. Rate limit exceeded. Please try again later.`,

View File

@@ -902,6 +902,8 @@ async function processBankSyncDownload(
id,
acctRow,
initialSync = false,
customStartingBalance?: number,
customStartingDate?: string,
) {
// If syncing an account from sync source it must not use strictIdChecking. This allows
// the fuzzy search to match transactions where the import IDs are different. It is a known quirk
@@ -930,16 +932,17 @@ async function processBankSyncDownload(
const { transactions } = download;
let balanceToUse = currentBalance;
if (acctRow.account_sync_source === 'simpleFin') {
// Use custom starting balance if provided, otherwise calculate it
if (customStartingBalance !== undefined) {
balanceToUse = customStartingBalance;
} else if (acctRow.account_sync_source === 'simpleFin') {
const previousBalance = transactions.reduce((total, trans) => {
return (
total - parseInt(trans.transactionAmount.amount.replace('.', ''))
);
}, currentBalance);
balanceToUse = previousBalance;
}
if (acctRow.account_sync_source === 'pluggyai') {
} else if (acctRow.account_sync_source === 'pluggyai') {
const currentBalance = download.startingBalance;
const previousBalance = transactions.reduce(
(total, trans) => total - trans.transactionAmount.amount * 100,
@@ -950,10 +953,15 @@ async function processBankSyncDownload(
const oldestTransaction = transactions[transactions.length - 1];
const oldestDate =
transactions.length > 0
? oldestTransaction.date
: monthUtils.currentDay();
// Use custom starting date if provided, otherwise use oldest transaction date or current day
let startingBalanceDate: string;
if (customStartingDate) {
startingBalanceDate = customStartingDate;
} else if (transactions.length > 0) {
startingBalanceDate = oldestTransaction.date;
} else {
startingBalanceDate = monthUtils.currentDay();
}
const payee = await getStartingBalancePayee();
@@ -963,7 +971,7 @@ async function processBankSyncDownload(
amount: balanceToUse,
category: acctRow.offbudget === 0 ? payee.category : null,
payee: payee.id,
date: oldestDate,
date: startingBalanceDate,
cleared: true,
starting_balance_flag: true,
});
@@ -1014,10 +1022,13 @@ export async function syncAccount(
id: string,
acctId: string,
bankId: string,
customStartingDate?: string,
customStartingBalance?: number,
) {
const acctRow = await db.select('accounts', id);
const syncStartDate = await getAccountSyncStartDate(id);
const syncStartDate =
customStartingDate ?? (await getAccountSyncStartDate(id));
const oldestTransaction = await getAccountOldestTransaction(id);
const newAccount = oldestTransaction == null;
@@ -1041,7 +1052,14 @@ export async function syncAccount(
);
}
return processBankSyncDownload(download, id, acctRow, newAccount);
return processBankSyncDownload(
download,
id,
acctRow,
newAccount,
customStartingBalance,
customStartingDate,
);
}
export async function simpleFinBatchSync(