Skip to content

Commit

Permalink
Merge branch 'master' into mbank_api
Browse files Browse the repository at this point in the history
  • Loading branch information
szymon-romanko authored Mar 2, 2025
2 parents f4b090a + e10b105 commit 4b506ee
Show file tree
Hide file tree
Showing 30 changed files with 263 additions and 133 deletions.
8 changes: 4 additions & 4 deletions packages/loot-core/src/mocks/budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,14 +461,14 @@ async function fillOther(handlers, account, payees, groups) {
async function createBudget(accounts, payees, groups) {
const primaryAccount = accounts.find(a => (a.name = 'Bank of America'));
const earliestDate = (
await db.first(
`SELECT * FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
await db.first<Pick<db.DbViewTransaction, 'date'>>(
`SELECT t.date FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
WHERE a.offbudget = 0 AND t.is_child = 0 ORDER BY date ASC LIMIT 1`,
)
).date;
const earliestPrimaryDate = (
await db.first(
`SELECT * FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
await db.first<Pick<db.DbViewTransaction, 'date'>>(
`SELECT t.date FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
WHERE a.id = ? AND a.offbudget = 0 AND t.is_child = 0 ORDER BY date ASC LIMIT 1`,
[primaryAccount.id],
)
Expand Down
80 changes: 55 additions & 25 deletions packages/loot-core/src/server/accounts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import {
AccountEntity,
CategoryEntity,
SyncServerGoCardlessAccount,
PayeeEntity,
TransactionEntity,
SyncServerSimpleFinAccount,
} from '../../types/models';
import { BankEntity } from '../../types/models/bank';
import { createApp } from '../app';
import * as db from '../db';
import {
Expand Down Expand Up @@ -77,24 +75,27 @@ async function getAccountBalance({
id: string;
cutoff: string | Date;
}) {
const { balance }: { balance: number } = await db.first(
const result = await db.first<{ balance: number }>(
'SELECT sum(amount) as balance FROM transactions WHERE acct = ? AND isParent = 0 AND tombstone = 0 AND date <= ?',
[id, db.toDateRepr(dayFromDate(cutoff))],
);
return balance ? balance : 0;
return result?.balance ? result.balance : 0;
}

async function getAccountProperties({ id }: { id: AccountEntity['id'] }) {
const { balance }: { balance: number } = await db.first(
const balanceResult = await db.first<{ balance: number }>(
'SELECT sum(amount) as balance FROM transactions WHERE acct = ? AND isParent = 0 AND tombstone = 0',
[id],
);
const { count }: { count: number } = await db.first(
const countResult = await db.first<{ count: number }>(
'SELECT count(id) as count FROM transactions WHERE acct = ? AND tombstone = 0',
[id],
);

return { balance: balance || 0, numTransactions: count };
return {
balance: balanceResult?.balance || 0,
numTransactions: countResult?.count || 0,
};
}

async function linkGoCardlessAccount({
Expand All @@ -112,10 +113,15 @@ async function linkGoCardlessAccount({
const bank = await link.findOrCreateBank(account.institution, requisitionId);

if (upgradingId) {
const accRow: AccountEntity = await db.first(
const accRow = await db.first<db.DbAccount>(
'SELECT * FROM accounts WHERE id = ?',
[upgradingId],
);

if (!accRow) {
throw new Error(`Account with ID ${upgradingId} not found.`);
}

id = accRow.id;
await db.update('accounts', {
id,
Expand Down Expand Up @@ -178,10 +184,15 @@ async function linkSimpleFinAccount({
);

if (upgradingId) {
const accRow: AccountEntity = await db.first(
const accRow = await db.first<db.DbAccount>(
'SELECT * FROM accounts WHERE id = ?',
[upgradingId],
);

if (!accRow) {
throw new Error(`Account with ID ${upgradingId} not found.`);
}

id = accRow.id;
await db.update('accounts', {
id,
Expand Down Expand Up @@ -278,7 +289,7 @@ async function closeAccount({
await unlinkAccount({ id });

return withUndo(async () => {
const account: AccountEntity = await db.first(
const account = await db.first<db.DbAccount>(
'SELECT * FROM accounts WHERE id = ? AND tombstone = 0',
[id],
);
Expand All @@ -303,11 +314,15 @@ async function closeAccount({
true,
);

const { id: payeeId }: Pick<PayeeEntity, 'id'> = await db.first(
const transferPayee = await db.first<Pick<db.DbPayee, 'id'>>(
'SELECT id FROM payees WHERE transfer_acct = ?',
[id],
);

if (!transferPayee) {
throw new Error(`Transfer payee with account ID ${id} not found.`);
}

await batchMessages(async () => {
// TODO: what this should really do is send a special message that
// automatically marks the tombstone value for all transactions
Expand All @@ -328,7 +343,7 @@ async function closeAccount({
});

db.deleteAccount({ id });
db.deleteTransferPayee({ id: payeeId });
db.deleteTransferPayee({ id: transferPayee.id });
});
} else {
if (balance !== 0 && transferAccountId == null) {
Expand All @@ -340,14 +355,20 @@ async function closeAccount({
// If there is a balance we need to transfer it to the specified
// account (and possibly categorize it)
if (balance !== 0 && transferAccountId) {
const { id: payeeId }: Pick<PayeeEntity, 'id'> = await db.first(
const transferPayee = await db.first<Pick<db.DbPayee, 'id'>>(
'SELECT id FROM payees WHERE transfer_acct = ?',
[transferAccountId],
);

if (!transferPayee) {
throw new Error(
`Transfer payee with account ID ${transferAccountId} not found.`,
);
}

await mainApp.handlers['transaction-add']({
id: uuidv4(),
payee: payeeId,
payee: transferPayee.id,
amount: -balance,
account: id,
date: monthUtils.currentDay(),
Expand Down Expand Up @@ -948,20 +969,21 @@ async function importTransactions({
}

async function unlinkAccount({ id }: { id: AccountEntity['id'] }) {
const { bank: bankId }: Pick<AccountEntity, 'bank'> = await db.first(
'SELECT bank FROM accounts WHERE id = ?',
const accRow = await db.first<db.DbAccount>(
'SELECT * FROM accounts WHERE id = ?',
[id],
);

if (!accRow) {
throw new Error(`Account with ID ${id} not found.`);
}

const bankId = accRow.bank;

if (!bankId) {
return 'ok';
}

const accRow: AccountEntity = await db.first(
'SELECT * FROM accounts WHERE id = ?',
[id],
);

const isGoCardless = accRow.account_sync_source === 'goCardless';

await db.updateAccount({
Expand All @@ -978,7 +1000,7 @@ async function unlinkAccount({ id }: { id: AccountEntity['id'] }) {
return;
}

const { count }: { count: number } = await db.first(
const accountWithBankResult = await db.first<{ count: number }>(
'SELECT COUNT(*) as count FROM accounts WHERE bank = ?',
[bankId],
);
Expand All @@ -990,15 +1012,23 @@ async function unlinkAccount({ id }: { id: AccountEntity['id'] }) {
return 'ok';
}

if (count === 0) {
const { bank_id: requisitionId }: Pick<BankEntity, 'bank_id'> =
await db.first('SELECT bank_id FROM banks WHERE id = ?', [bankId]);
if (!accountWithBankResult || accountWithBankResult.count === 0) {
const bank = await db.first<Pick<db.DbBank, 'bank_id'>>(
'SELECT bank_id FROM banks WHERE id = ?',
[bankId],
);

if (!bank) {
throw new Error(`Bank with ID ${bankId} not found.`);
}

const serverConfig = getServer();
if (!serverConfig) {
throw new Error('Failed to get server config.');
}

const requisitionId = bank.bank_id;

try {
await post(
serverConfig.GOCARDLESS_SERVER + '/remove-account',
Expand Down
4 changes: 2 additions & 2 deletions packages/loot-core/src/server/accounts/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { v4 as uuidv4 } from 'uuid';
import * as db from '../db';

export async function findOrCreateBank(institution, requisitionId) {
const bank = await db.first(
'SELECT id, bank_id, name FROM banks WHERE bank_id = ?',
const bank = await db.first<Pick<db.DbBank, 'id' | 'bank_id'>>(
'SELECT id, bank_id FROM banks WHERE bank_id = ?',
[requisitionId],
);

Expand Down
9 changes: 4 additions & 5 deletions packages/loot-core/src/server/accounts/payees.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
// @ts-strict-ignore
import { CategoryEntity, PayeeEntity } from '../../types/models';
import * as db from '../db';

export async function createPayee(description) {
// Check to make sure no payee already exists with exactly the same
// name
const row: Pick<PayeeEntity, 'id'> = await db.first(
const row = await db.first<Pick<db.DbPayee, 'id'>>(
`SELECT id FROM payees WHERE UNICODE_LOWER(name) = ? AND tombstone = 0`,
[description.toLowerCase()],
);

if (row) {
return row.id;
} else {
return (await db.insertPayee({ name: description })) as PayeeEntity['id'];
return (await db.insertPayee({ name: description })) as db.DbPayee['id'];
}
}

export async function getStartingBalancePayee() {
let category: CategoryEntity = await db.first(`
let category = await db.first<db.DbCategory>(`
SELECT * FROM categories
WHERE is_income = 1 AND
LOWER(name) = 'starting balances' AND
tombstone = 0
`);
if (category === null) {
category = await db.first(
category = await db.first<db.DbCategory>(
'SELECT * FROM categories WHERE is_income = 1 AND tombstone = 0',
);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/loot-core/src/server/accounts/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ export async function matchTransactions(
);

// The first pass runs the rules, and preps data for fuzzy matching
const accounts: AccountEntity[] = await db.getAccounts();
const accounts: db.DbAccount[] = await db.getAccounts();
const accountsMap = new Map(accounts.map(account => [account.id, account]));

const transactionsStep1 = [];
Expand All @@ -603,7 +603,7 @@ export async function matchTransactions(
// is the highest fidelity match and should always be attempted
// first.
if (trans.imported_id) {
match = await db.first(
match = await db.first<db.DbViewTransaction>(
'SELECT * FROM v_transactions WHERE imported_id = ? AND account = ?',
[trans.imported_id, acctId],
);
Expand Down Expand Up @@ -737,7 +737,7 @@ export async function addTransactions(
{ rawPayeeName: true },
);

const accounts: AccountEntity[] = await db.getAccounts();
const accounts: db.DbAccount[] = await db.getAccounts();
const accountsMap = new Map(accounts.map(account => [account.id, account]));

for (const { trans: originalTrans, subtransactions } of normalized) {
Expand Down
7 changes: 4 additions & 3 deletions packages/loot-core/src/server/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ async function validateExpenseCategory(debug, id) {
throw APIError(`${debug}: category id is required`);
}

const row = await db.first('SELECT is_income FROM categories WHERE id = ?', [
id,
]);
const row = await db.first<Pick<db.DbCategory, 'is_income'>>(
'SELECT is_income FROM categories WHERE id = ?',
[id],
);

if (!row) {
throw APIError(`${debug}: category “${id}” does not exist`);
Expand Down
4 changes: 2 additions & 2 deletions packages/loot-core/src/server/budget/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export async function setNMonthAvg({
N: number;
category: string;
}): Promise<void> {
const categoryFromDb = await db.first(
const categoryFromDb = await db.first<Pick<db.DbViewCategory, 'is_income'>>(
'SELECT is_income FROM v_categories WHERE id = ?',
[category],
);
Expand Down Expand Up @@ -361,7 +361,7 @@ export async function holdForNextMonth({
month: string;
amount: number;
}): Promise<boolean> {
const row = await db.first(
const row = await db.first<Pick<db.DbZeroBudgetMonth, 'buffered'>>(
'SELECT buffered FROM zero_budget_months WHERE id = ?',
[month],
);
Expand Down
2 changes: 1 addition & 1 deletion packages/loot-core/src/server/budget/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ export async function createBudget(months) {
}

export async function createAllBudgets() {
const earliestTransaction = await db.first(
const earliestTransaction = await db.first<db.DbTransaction>(
'SELECT * FROM transactions WHERE isChild=0 AND date IS NOT NULL ORDER BY date ASC LIMIT 1',
);
const earliestDate =
Expand Down
6 changes: 3 additions & 3 deletions packages/loot-core/src/server/budget/cleanup-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async function applyGroupCleanups(
);
const to_budget = budgeted + Math.abs(balance);
const categoryId = generalGroup[ii].category;
let carryover = await db.first(
let carryover = await db.first<Pick<db.DbZeroBudget, 'carryover'>>(
`SELECT carryover FROM zero_budgets WHERE month = ? and category = ?`,
[db_month, categoryId],
);
Expand Down Expand Up @@ -220,7 +220,7 @@ async function processCleanup(month: string): Promise<Notification> {
} else {
warnings.push(category.name + ' does not have available funds.');
}
const carryover = await db.first(
const carryover = await db.first<Pick<db.DbZeroBudget, 'carryover'>>(
`SELECT carryover FROM zero_budgets WHERE month = ? and category = ?`,
[db_month, category.id],
);
Expand Down Expand Up @@ -249,7 +249,7 @@ async function processCleanup(month: string): Promise<Notification> {
const budgeted = await getSheetValue(sheetName, `budget-${category.id}`);
const to_budget = budgeted + Math.abs(balance);
const categoryId = category.id;
let carryover = await db.first(
let carryover = await db.first<Pick<db.DbZeroBudget, 'carryover'>>(
`SELECT carryover FROM zero_budgets WHERE month = ? and category = ?`,
[db_month, categoryId],
);
Expand Down
6 changes: 4 additions & 2 deletions packages/loot-core/src/server/budget/goalsSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ async function createScheduleList(
const errors = [];

for (let ll = 0; ll < template.length; ll++) {
const { id: sid, completed: complete } = await db.first(
'SELECT * FROM schedules WHERE TRIM(name) = ? AND tombstone = 0',
const { id: sid, completed: complete } = await db.first<
Pick<db.DbSchedule, 'id' | 'completed'>
>(
'SELECT id, completed FROM schedules WHERE TRIM(name) = ? AND tombstone = 0',
[template[ll].name.trim()],
);
const rule = await getRuleForSchedule(sid);
Expand Down
4 changes: 3 additions & 1 deletion packages/loot-core/src/server/dashboard/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ async function addDashboardWidget(
// If no x & y was provided - calculate it dynamically
// The new widget should be the very last one in the list of all widgets
if (!('x' in widget) && !('y' in widget)) {
const data = await db.first(
const data = await db.first<
Pick<db.DbDashboard, 'x' | 'y' | 'width' | 'height'>
>(
'SELECT x, y, width, height FROM dashboard WHERE tombstone = 0 ORDER BY y DESC, x DESC',
);

Expand Down
Loading

0 comments on commit 4b506ee

Please sign in to comment.