mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-10 16:26:43 -05:00
[AI] Add comprehensive integration tests for formula rules and formula cards
User requested integration tests with real database queries and complex nested formulas. These tests go beyond unit testing HyperFormula to validate the full integration with Actual's transaction rules and query system. Formula Rule Integration Tests (36 tests): - Basic formula operations with FORMATCURRENCY and nested IFs - Text manipulation: CONCATENATE, UPPER, LEFT, RIGHT, TRIM, PROPER - Math functions: ROUND, ABS, MAX, MIN, CEILING, SQRT, SUM, PRODUCT - Date functions: YEAR, MONTH, DAY, DAYS, EOMONTH, WEEKDAY - Logical functions: AND, OR, NOT, IFERROR, SWITCH - New formatting functions: FORMATNUMBER, FORMATCURRENCY with various options - Multi-line output with CHAR(10) for line breaks - Information functions: ISNUMBER, ISTEXT, ISEVEN, ISODD, ISBLANK - Error handling and validation - Complex nested formulas combining multiple function types Formula Card Integration Tests (14 tests): - Single and multiple query integration - Query results with FORMATCURRENCY and FORMATNUMBER - Percentage calculations across queries - Date-based query filtering - Complex nested calculations with multiple queries - Multi-line output with query results - Error handling with empty queries and division by zero - Running totals across accounts - MAX/MIN operations with query results Test Coverage: - Every formula function type is tested in at least one integration test - Tests use real database operations (insertAccount, insertPayee, insertCategory, etc.) - Tests validate complex nested formulas with 3-4 levels of nesting - Tests cover edge cases (empty results, division by zero, type mismatches) - All 50 integration tests pass ✅ Note: Tests demonstrate proper usage patterns for users: - Transaction amounts are in cents, divide by 100 for dollar display - Rules must have conditions on 'imported_payee' or 'payee' to be indexed - Query results are automatically converted from cents to dollars - FORMATCURRENCY places negative sign before currency symbol (standard format) Co-authored-by: lelemm <lelemm@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,728 @@
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
|
||||
import { aqlQuery } from '#server/aql';
|
||||
import * as db from '#server/db';
|
||||
import { loadMappings } from '#server/db/mappings';
|
||||
import { conditionsToAQL } from '#server/transactions/transaction-rules';
|
||||
import { q } from '#shared/query';
|
||||
import { HyperFormula } from 'hyperformula';
|
||||
import enUS from 'hyperformula/i18n/languages/enUS';
|
||||
|
||||
import {
|
||||
CustomFunctionsPlugin,
|
||||
customFunctionsTranslations,
|
||||
} from '../rules/customFunctions';
|
||||
|
||||
// Integration tests for formula cards with real database queries
|
||||
// These tests validate formulas with actual query results from the database
|
||||
|
||||
// Register HyperFormula language and plugins if not already registered
|
||||
try {
|
||||
HyperFormula.registerLanguage('enUS', enUS);
|
||||
} catch (e) {
|
||||
// Already registered, ignore
|
||||
}
|
||||
|
||||
try {
|
||||
HyperFormula.registerFunctionPlugin(
|
||||
CustomFunctionsPlugin,
|
||||
customFunctionsTranslations,
|
||||
);
|
||||
} catch (e) {
|
||||
// Already registered, ignore
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await global.emptyDatabase()();
|
||||
await loadMappings();
|
||||
});
|
||||
|
||||
describe('Formula Card - Integration Tests with Queries', () => {
|
||||
// Helper functions using db helper methods
|
||||
async function createTestAccount(name: string) {
|
||||
return await db.insertAccount({ name });
|
||||
}
|
||||
|
||||
async function createCategoryGroup(name: string) {
|
||||
return await db.insertCategoryGroup({ name });
|
||||
}
|
||||
|
||||
async function createTestCategory(
|
||||
name: string,
|
||||
groupId: string,
|
||||
isIncome = false,
|
||||
) {
|
||||
return await db.insertCategory({
|
||||
name,
|
||||
cat_group: groupId,
|
||||
is_income: isIncome ? 1 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function createTestTransaction(data: {
|
||||
accountId: string;
|
||||
amount: number;
|
||||
date: string;
|
||||
categoryId?: string;
|
||||
notes?: string;
|
||||
}) {
|
||||
return await db.insertTransaction({
|
||||
account: data.accountId,
|
||||
amount: data.amount,
|
||||
date: data.date,
|
||||
category: data.categoryId || null,
|
||||
notes: data.notes || null,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeQuery(conditions: unknown[], timeFrame?: unknown) {
|
||||
// Simulate query execution like the formula card does
|
||||
const { filters } = conditionsToAQL(conditions);
|
||||
|
||||
let transQuery = q('transactions');
|
||||
|
||||
if (timeFrame && timeFrame.start && timeFrame.end) {
|
||||
transQuery = transQuery.filter({
|
||||
$and: [
|
||||
{ date: { $gte: timeFrame.start } },
|
||||
{ date: { $lte: timeFrame.end } },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.length > 0) {
|
||||
transQuery = transQuery.filter({ $and: filters });
|
||||
}
|
||||
|
||||
const summedQuery = transQuery.calculate({ $sum: '$amount' });
|
||||
const { data } = await aqlQuery(summedQuery);
|
||||
return data || 0;
|
||||
}
|
||||
|
||||
async function executeFormulaWithQuery(
|
||||
formula: string,
|
||||
queryResults: Record<string, number>,
|
||||
) {
|
||||
let hfInstance: ReturnType<typeof HyperFormula.buildEmpty> | null = null;
|
||||
|
||||
try {
|
||||
hfInstance = HyperFormula.buildEmpty({
|
||||
licenseKey: 'gpl-v3',
|
||||
language: 'enUS',
|
||||
dateFormats: ['DD/MM/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'],
|
||||
});
|
||||
|
||||
const sheetName = hfInstance.addSheet('Sheet1');
|
||||
const sheetId = hfInstance.getSheetId(sheetName);
|
||||
|
||||
if (sheetId === undefined) {
|
||||
throw new Error('Failed to create sheet');
|
||||
}
|
||||
|
||||
// Replace QUERY() calls with actual values
|
||||
let processedFormula = formula;
|
||||
for (const [queryName, value] of Object.entries(queryResults)) {
|
||||
const regex = new RegExp(
|
||||
`QUERY\\s*\\(\\s*["']${queryName}["']\\s*\\)`,
|
||||
'gi',
|
||||
);
|
||||
// Convert cents to dollars for display
|
||||
const dollarValue = value / 100;
|
||||
processedFormula = processedFormula.replace(regex, String(dollarValue));
|
||||
}
|
||||
|
||||
hfInstance.setCellContents({ sheet: sheetId, col: 0, row: 0 }, [
|
||||
[processedFormula],
|
||||
]);
|
||||
|
||||
const cellValue = hfInstance.getCellValue({
|
||||
sheet: sheetId,
|
||||
col: 0,
|
||||
row: 0,
|
||||
});
|
||||
|
||||
if (cellValue && typeof cellValue === 'object' && 'type' in cellValue) {
|
||||
throw new Error(`Formula error: ${cellValue.type}`);
|
||||
}
|
||||
|
||||
return cellValue;
|
||||
} finally {
|
||||
hfInstance?.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
describe('Basic Query Integration', () => {
|
||||
it('should execute formula with single query result', async () => {
|
||||
// Integration test: Simple query sum with formula
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const groupId = await createCategoryGroup('Expenses');
|
||||
const categoryId = await createTestCategory('Groceries', groupId);
|
||||
|
||||
// Create test transactions
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -5000,
|
||||
date: '2024-01-15',
|
||||
categoryId,
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -7500,
|
||||
date: '2024-01-20',
|
||||
categoryId,
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -3000,
|
||||
date: '2024-01-25',
|
||||
categoryId,
|
||||
});
|
||||
|
||||
// Execute query
|
||||
const queryResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
|
||||
]);
|
||||
|
||||
// Execute formula with query result
|
||||
const formula = '=FORMATCURRENCY(QUERY("Groceries"))';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Groceries: queryResult,
|
||||
});
|
||||
|
||||
// FORMATCURRENCY places negative sign before currency symbol
|
||||
expect(result).toBe('-$155.00');
|
||||
});
|
||||
|
||||
it('should calculate percentage from query results', async () => {
|
||||
// Integration test: Calculate spending as percentage of income
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const groupId = await createCategoryGroup('Income');
|
||||
const incomeCategory = await createTestCategory('Salary', groupId, 1);
|
||||
const expenseGroup = await createCategoryGroup('Expenses');
|
||||
const expenseCategory = await createTestCategory('Food', expenseGroup);
|
||||
|
||||
// Create income
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: 500000, // $5000
|
||||
date: '2024-01-01',
|
||||
categoryId: incomeCategory,
|
||||
});
|
||||
|
||||
// Create expenses
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -75000, // $750
|
||||
date: '2024-01-15',
|
||||
categoryId: expenseCategory,
|
||||
});
|
||||
|
||||
const incomeResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: incomeCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const expenseResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: expenseCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula =
|
||||
'=CONCATENATE(FORMATNUMBER((ABS(QUERY("Expenses")) / QUERY("Income")) * 100, 1), "%")';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Income: incomeResult,
|
||||
Expenses: expenseResult,
|
||||
});
|
||||
|
||||
expect(result).toBe('15.0%');
|
||||
});
|
||||
|
||||
it('should format large query results with thousands separators', async () => {
|
||||
// Integration test: Format large numbers from queries
|
||||
const accountId = await createTestAccount('Investment');
|
||||
const groupId = await createCategoryGroup('Income');
|
||||
const categoryId = await createTestCategory('Dividends', groupId, 1);
|
||||
|
||||
// Create large transactions
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: 123456789, // $1,234,567.89
|
||||
date: '2024-01-01',
|
||||
categoryId,
|
||||
});
|
||||
|
||||
const queryResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula = '=FORMATNUMBER(QUERY("Dividends"), 2)';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Dividends: queryResult,
|
||||
});
|
||||
|
||||
expect(result).toBe('1,234,567.89');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Query Integration', () => {
|
||||
it('should combine multiple query results in formula', async () => {
|
||||
// Integration test: Calculate net worth from multiple queries
|
||||
const checkingId = await createTestAccount('Checking');
|
||||
const savingsId = await createTestAccount('Savings');
|
||||
const creditCardId = await createTestAccount('Credit Card');
|
||||
|
||||
// Create transactions
|
||||
await createTestTransaction({
|
||||
accountId: checkingId,
|
||||
amount: 250000, // $2500
|
||||
date: '2024-01-01',
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId: savingsId,
|
||||
amount: 1000000, // $10000
|
||||
date: '2024-01-01',
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId: creditCardId,
|
||||
amount: -50000, // -$500
|
||||
date: '2024-01-01',
|
||||
});
|
||||
|
||||
const checkingResult = await executeQuery([
|
||||
{ field: 'account', op: 'is', value: checkingId, type: 'id' },
|
||||
]);
|
||||
|
||||
const savingsResult = await executeQuery([
|
||||
{ field: 'account', op: 'is', value: savingsId, type: 'id' },
|
||||
]);
|
||||
|
||||
const creditCardResult = await executeQuery([
|
||||
{ field: 'account', op: 'is', value: creditCardId, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula =
|
||||
'=FORMATCURRENCY(QUERY("Checking") + QUERY("Savings") + QUERY("CreditCard"))';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Checking: checkingResult,
|
||||
Savings: savingsResult,
|
||||
CreditCard: creditCardResult,
|
||||
});
|
||||
|
||||
// Query results: 250000 + 1000000 - 50000 = 1200000 cents = 12000 dollars
|
||||
expect(result).toBe('$12,000.00');
|
||||
});
|
||||
|
||||
it('should calculate ratios between multiple queries', async () => {
|
||||
// Integration test: Calculate savings rate
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const incomeGroup = await createCategoryGroup('Income');
|
||||
const incomeCategory = await createTestCategory('Salary', incomeGroup, 1);
|
||||
const savingsGroup = await createCategoryGroup('Savings');
|
||||
const savingsCategory = await createTestCategory(
|
||||
'Savings',
|
||||
savingsGroup,
|
||||
);
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: 600000, // $6000 income
|
||||
date: '2024-01-01',
|
||||
categoryId: incomeCategory,
|
||||
});
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -120000, // $1200 savings
|
||||
date: '2024-01-15',
|
||||
categoryId: savingsCategory,
|
||||
});
|
||||
|
||||
const incomeResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: incomeCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const savingsResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: savingsCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula =
|
||||
'=CONCATENATE("Savings Rate: ", FORMATNUMBER((ABS(QUERY("Savings")) / QUERY("Income")) * 100, 1), "%")';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Income: incomeResult,
|
||||
Savings: savingsResult,
|
||||
});
|
||||
|
||||
expect(result).toBe('Savings Rate: 20.0%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex Nested Formulas with Queries', () => {
|
||||
it('should handle deeply nested calculations with multiple queries', async () => {
|
||||
// Integration test: Complex budget analysis
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const incomeGroup = await createCategoryGroup('Income');
|
||||
const incomeCategory = await createTestCategory('Salary', incomeGroup, 1);
|
||||
const needsGroup = await createCategoryGroup('Needs');
|
||||
const needsCategory = await createTestCategory('Housing', needsGroup);
|
||||
const wantsGroup = await createCategoryGroup('Wants');
|
||||
const wantsCategory = await createTestCategory(
|
||||
'Entertainment',
|
||||
wantsGroup,
|
||||
);
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: 500000, // $5000 income
|
||||
date: '2024-01-01',
|
||||
categoryId: incomeCategory,
|
||||
});
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -200000, // $2000 needs
|
||||
date: '2024-01-10',
|
||||
categoryId: needsCategory,
|
||||
});
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -100000, // $1000 wants
|
||||
date: '2024-01-15',
|
||||
categoryId: wantsCategory,
|
||||
});
|
||||
|
||||
const incomeResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: incomeCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const needsResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: needsCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const wantsResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: wantsCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula =
|
||||
'=CONCATENATE("Income: ", FORMATCURRENCY(QUERY("Income")), " | Needs: ", FORMATNUMBER((ABS(QUERY("Needs")) / QUERY("Income")) * 100, 0), "% | Wants: ", FORMATNUMBER((ABS(QUERY("Wants")) / QUERY("Income")) * 100, 0), "%")';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Income: incomeResult,
|
||||
Needs: needsResult,
|
||||
Wants: wantsResult,
|
||||
});
|
||||
|
||||
// Income: 500000 cents = 5000 dollars
|
||||
expect(result).toBe('Income: $5,000.00 | Needs: 40% | Wants: 20%');
|
||||
});
|
||||
|
||||
it('should use conditional logic with query results', async () => {
|
||||
// Integration test: Budget status with IF statements
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const groupId = await createCategoryGroup('Expenses');
|
||||
const categoryId = await createTestCategory('Dining', groupId);
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -35000, // $350
|
||||
date: '2024-01-15',
|
||||
categoryId,
|
||||
});
|
||||
|
||||
const queryResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula =
|
||||
'=IF(ABS(QUERY("Dining")) > 400, "Over Budget", IF(ABS(QUERY("Dining")) > 300, "Near Limit", "On Track"))';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Dining: queryResult,
|
||||
});
|
||||
|
||||
expect(result).toBe('Near Limit');
|
||||
});
|
||||
|
||||
it('should create multi-line output with query results', async () => {
|
||||
// Integration test: Multi-line summary with CHAR(10)
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const incomeGroup = await createCategoryGroup('Income');
|
||||
const incomeCategory = await createTestCategory('Salary', incomeGroup, 1);
|
||||
const expenseGroup = await createCategoryGroup('Expenses');
|
||||
const expenseCategory = await createTestCategory('Total', expenseGroup);
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: 400000, // $4000
|
||||
date: '2024-01-01',
|
||||
categoryId: incomeCategory,
|
||||
});
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -150000, // $1500
|
||||
date: '2024-01-15',
|
||||
categoryId: expenseCategory,
|
||||
});
|
||||
|
||||
const incomeResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: incomeCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const expenseResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: expenseCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula =
|
||||
'=CONCATENATE("Income: ", FORMATCURRENCY(QUERY("Income")), CHAR(10), "Expenses: ", FORMATCURRENCY(QUERY("Expenses")), CHAR(10), "Net: ", FORMATCURRENCY(QUERY("Income") + QUERY("Expenses")))';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Income: incomeResult,
|
||||
Expenses: expenseResult,
|
||||
});
|
||||
|
||||
// Income: 400000 cents = 4000 dollars, Expenses: -150000 cents = -1500 dollars
|
||||
expect(result).toContain('Income: $4,000.00');
|
||||
expect(result).toContain('\n');
|
||||
expect(result).toContain('Expenses: -$1,500.00');
|
||||
expect(result).toContain('Net: $2,500.00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Date-based Query Integration', () => {
|
||||
it('should filter queries by date range', async () => {
|
||||
// Integration test: Query with time frame
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const groupId = await createCategoryGroup('Expenses');
|
||||
const categoryId = await createTestCategory('Shopping', groupId);
|
||||
|
||||
// Transactions in different months
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -10000,
|
||||
date: '2024-01-15',
|
||||
categoryId,
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -15000,
|
||||
date: '2024-02-15',
|
||||
categoryId,
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -20000,
|
||||
date: '2024-03-15',
|
||||
categoryId,
|
||||
});
|
||||
|
||||
// Query only January
|
||||
const queryResult = await executeQuery(
|
||||
[{ field: 'category', op: 'is', value: categoryId, type: 'id' }],
|
||||
{ start: '2024-01-01', end: '2024-01-31' },
|
||||
);
|
||||
|
||||
const formula = '=FORMATCURRENCY(QUERY("Shopping"))';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Shopping: queryResult,
|
||||
});
|
||||
|
||||
// Query result: -10000 cents = -100 dollars
|
||||
expect(result).toBe('-$100.00');
|
||||
});
|
||||
|
||||
it('should compare different time periods', async () => {
|
||||
// Integration test: Month-over-month comparison
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const groupId = await createCategoryGroup('Expenses');
|
||||
const categoryId = await createTestCategory('Utilities', groupId);
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -12000,
|
||||
date: '2024-01-15',
|
||||
categoryId,
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -15000,
|
||||
date: '2024-02-15',
|
||||
categoryId,
|
||||
});
|
||||
|
||||
const jan = await executeQuery(
|
||||
[{ field: 'category', op: 'is', value: categoryId, type: 'id' }],
|
||||
{ start: '2024-01-01', end: '2024-01-31' },
|
||||
);
|
||||
|
||||
const feb = await executeQuery(
|
||||
[{ field: 'category', op: 'is', value: categoryId, type: 'id' }],
|
||||
{ start: '2024-02-01', end: '2024-02-29' },
|
||||
);
|
||||
|
||||
const formula =
|
||||
'=CONCATENATE("Change: ", FORMATNUMBER(((ABS(QUERY("Feb")) - ABS(QUERY("Jan"))) / ABS(QUERY("Jan"))) * 100, 1), "%")';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Jan: jan,
|
||||
Feb: feb,
|
||||
});
|
||||
|
||||
expect(result).toBe('Change: 25.0%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling with Queries', () => {
|
||||
it('should handle empty query results', async () => {
|
||||
// Integration test: Query with no matching transactions
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const groupId = await createCategoryGroup('Expenses');
|
||||
const categoryId = await createTestCategory('Empty', groupId);
|
||||
|
||||
// No transactions created for this category
|
||||
|
||||
const queryResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula = '=FORMATCURRENCY(QUERY("Empty"))';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Empty: queryResult,
|
||||
});
|
||||
|
||||
expect(result).toBe('$0.00');
|
||||
});
|
||||
|
||||
it('should handle division by zero with IFERROR', async () => {
|
||||
// Integration test: Safe division with empty query
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const groupId = await createCategoryGroup('Income');
|
||||
const incomeCategory = await createTestCategory('Salary', groupId, 1);
|
||||
const expenseGroup = await createCategoryGroup('Expenses');
|
||||
const expenseCategory = await createTestCategory('Food', expenseGroup);
|
||||
|
||||
// Only create expense, no income
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -10000,
|
||||
date: '2024-01-15',
|
||||
categoryId: expenseCategory,
|
||||
});
|
||||
|
||||
const incomeResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: incomeCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const expenseResult = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: expenseCategory, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula =
|
||||
'=IFERROR(CONCATENATE(FORMATNUMBER((ABS(QUERY("Expenses")) / QUERY("Income")) * 100, 0), "%"), "No Income")';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Income: incomeResult,
|
||||
Expenses: expenseResult,
|
||||
});
|
||||
|
||||
expect(result).toBe('No Income');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Advanced Query Scenarios', () => {
|
||||
it('should calculate running totals across accounts', async () => {
|
||||
// Integration test: Net worth calculation
|
||||
const checking = await createTestAccount('Checking');
|
||||
const savings = await createTestAccount('Savings');
|
||||
const investment = await createTestAccount('Investment');
|
||||
const creditCard = await createTestAccount('Credit Card');
|
||||
|
||||
await createTestTransaction({
|
||||
accountId: checking,
|
||||
amount: 150000,
|
||||
date: '2024-01-01',
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId: savings,
|
||||
amount: 500000,
|
||||
date: '2024-01-01',
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId: investment,
|
||||
amount: 1000000,
|
||||
date: '2024-01-01',
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId: creditCard,
|
||||
amount: -25000,
|
||||
date: '2024-01-01',
|
||||
});
|
||||
|
||||
const checkingResult = await executeQuery([
|
||||
{ field: 'account', op: 'is', value: checking, type: 'id' },
|
||||
]);
|
||||
const savingsResult = await executeQuery([
|
||||
{ field: 'account', op: 'is', value: savings, type: 'id' },
|
||||
]);
|
||||
const investmentResult = await executeQuery([
|
||||
{ field: 'account', op: 'is', value: investment, type: 'id' },
|
||||
]);
|
||||
const creditCardResult = await executeQuery([
|
||||
{ field: 'account', op: 'is', value: creditCard, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula =
|
||||
'=FORMATCURRENCY(QUERY("Checking") + QUERY("Savings") + QUERY("Investment") + QUERY("CreditCard"))';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Checking: checkingResult,
|
||||
Savings: savingsResult,
|
||||
Investment: investmentResult,
|
||||
CreditCard: creditCardResult,
|
||||
});
|
||||
|
||||
expect(result).toBe('$16,250.00');
|
||||
});
|
||||
|
||||
it('should use MAX and MIN with query results', async () => {
|
||||
// Integration test: Find highest spending category
|
||||
const accountId = await createTestAccount('Checking');
|
||||
const groupId = await createCategoryGroup('Expenses');
|
||||
const cat1 = await createTestCategory('Category1', groupId);
|
||||
const cat2 = await createTestCategory('Category2', groupId);
|
||||
const cat3 = await createTestCategory('Category3', groupId);
|
||||
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -15000,
|
||||
date: '2024-01-15',
|
||||
categoryId: cat1,
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -25000,
|
||||
date: '2024-01-15',
|
||||
categoryId: cat2,
|
||||
});
|
||||
await createTestTransaction({
|
||||
accountId,
|
||||
amount: -10000,
|
||||
date: '2024-01-15',
|
||||
categoryId: cat3,
|
||||
});
|
||||
|
||||
const result1 = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: cat1, type: 'id' },
|
||||
]);
|
||||
const result2 = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: cat2, type: 'id' },
|
||||
]);
|
||||
const result3 = await executeQuery([
|
||||
{ field: 'category', op: 'is', value: cat3, type: 'id' },
|
||||
]);
|
||||
|
||||
const formula =
|
||||
'=CONCATENATE("Highest: ", FORMATCURRENCY(MIN(QUERY("Cat1"), QUERY("Cat2"), QUERY("Cat3"))))';
|
||||
const result = await executeFormulaWithQuery(formula, {
|
||||
Cat1: result1,
|
||||
Cat2: result2,
|
||||
Cat3: result3,
|
||||
});
|
||||
|
||||
// MIN of -150, -250, -100 = -250 dollars
|
||||
expect(result).toBe('Highest: -$250.00');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user