import * as monthUtils from '../../shared/months'; import { safeNumber } from '../../shared/util'; import * as db from '../db'; import * as prefs from '../prefs'; import * as sheet from '../sheet'; import { batchMessages } from '../sync'; export async function getSheetValue(sheetName, cell) { const node = await sheet.getCell(sheetName, cell); return safeNumber(typeof node.value === 'number' ? node.value : 0); } // We want to only allow the positive movement of money back and // forth. buffered should never be allowed to go into the negative, // and you shouldn't be allowed to pull non-existant money from // leftover. function calcBufferedAmount(toBudget, buffered, amount) { amount = Math.min(Math.max(amount, -buffered), Math.max(toBudget, 0)); return buffered + amount; } function getBudgetTable() { let { budgetType } = prefs.getPrefs() || {}; return budgetType === 'report' ? 'reflect_budgets' : 'zero_budgets'; } export function isReflectBudget() { let { budgetType } = prefs.getPrefs(); return budgetType === 'report'; } function dbMonth(month) { return parseInt(month.replace('-', '')); } function getBudgetData(table, month) { return db.all( ` SELECT b.*, c.is_income FROM v_categories c LEFT JOIN ${table} b ON b.category = c.id WHERE c.tombstone = 0 AND b.month = ? `, [month], ); } function getAllMonths(startMonth) { let { createdMonths } = sheet.get().meta(); let latest = null; for (let month of createdMonths) { if (latest == null || month > latest) { latest = month; } } return monthUtils.rangeInclusive(startMonth, latest); } // TODO: Valid month format in all the functions below export function getBudget({ category, month }) { let table = getBudgetTable(); let existing = db.firstSync( `SELECT * FROM ${table} WHERE month = ? AND category = ?`, [dbMonth(month), category], ); return existing ? existing.amount || 0 : 0; } export function setBudget({ category, month, amount }) { amount = safeNumber(typeof amount === 'number' ? amount : 0); const table = getBudgetTable(); let existing = db.firstSync( `SELECT id FROM ${table} WHERE month = ? AND category = ?`, [dbMonth(month), category], ); if (existing) { return db.update(table, { id: existing.id, amount }); } return db.insert(table, { id: `${dbMonth(month)}-${category}`, month: dbMonth(month), category, amount, }); } export function setBuffer(month, amount) { let existing = db.firstSync( `SELECT id FROM zero_budget_months WHERE id = ?`, [month], ); if (existing) { return db.update('zero_budget_months', { id: existing.id, buffered: amount, }); } return db.insert('zero_budget_months', { id: month, buffered: amount }); } function setCarryover(table, category, month, flag) { let existing = db.firstSync( `SELECT id FROM ${table} WHERE month = ? AND category = ?`, [month, category], ); if (existing) { return db.update(table, { id: existing.id, carryover: flag ? 1 : 0 }); } return db.insert(table, { id: `${month}-${category}`, month, category, carryover: flag ? 1 : 0, }); } // Actions export async function copyPreviousMonth({ month }) { let prevMonth = dbMonth(monthUtils.prevMonth(month)); let table = getBudgetTable(); let budgetData = await getBudgetData(table, prevMonth); await batchMessages(async () => { budgetData.forEach(prevBudget => { if (prevBudget.is_income === 1 && !isReflectBudget()) { return; } setBudget({ category: prevBudget.category, month, amount: prevBudget.amount, }); }); }); } export async function copySinglePreviousMonth({ month, category }) { let prevMonth = monthUtils.prevMonth(month); let newAmount = await getSheetValue( monthUtils.sheetForMonth(prevMonth), 'budget-' + category, ); await batchMessages(async () => { setBudget({ category: category, month, amount: newAmount }); }); } export async function setZero({ month }) { let categories = await db.all( 'SELECT * FROM v_categories WHERE tombstone = 0', ); await batchMessages(async () => { categories.forEach(cat => { if (cat.is_income === 1 && !isReflectBudget()) { return; } setBudget({ category: cat.id, month, amount: 0 }); }); }); } export async function set3MonthAvg({ month }) { let categories = await db.all( 'SELECT * FROM v_categories WHERE tombstone = 0', ); let prevMonth1 = monthUtils.prevMonth(month); let prevMonth2 = monthUtils.prevMonth(prevMonth1); let prevMonth3 = monthUtils.prevMonth(prevMonth2); await batchMessages(async () => { for (let cat of categories) { if (cat.is_income === 1 && !isReflectBudget()) { continue; } let spent1 = await getSheetValue( monthUtils.sheetForMonth(prevMonth1), 'sum-amount-' + cat.id, ); let spent2 = await getSheetValue( monthUtils.sheetForMonth(prevMonth2), 'sum-amount-' + cat.id, ); let spent3 = await getSheetValue( monthUtils.sheetForMonth(prevMonth3), 'sum-amount-' + cat.id, ); const avg = Math.round((spent1 + spent2 + spent3) / 3); setBudget({ category: cat.id, month, amount: -avg }); } }); } export async function setNMonthAvg({ month, N, category }) { let prevMonth = monthUtils.prevMonth(month); let sumAmount = 0; for (let l = 0; l < N; l++) { sumAmount += await getSheetValue( monthUtils.sheetForMonth(prevMonth), 'sum-amount-' + category, ); prevMonth = monthUtils.prevMonth(prevMonth); } await batchMessages(async () => { const avg = Math.round(sumAmount / N); setBudget({ category: category, month, amount: -avg }); }); } export async function holdForNextMonth({ month, amount }) { let row = await db.first( 'SELECT buffered FROM zero_budget_months WHERE id = ?', [month], ); let sheetName = monthUtils.sheetForMonth(month); let toBudget = await getSheetValue(sheetName, 'to-budget'); if (toBudget > 0) { let bufferedAmount = calcBufferedAmount( toBudget, (row && row.buffered) || 0, amount, ); await setBuffer(month, bufferedAmount); return true; } return false; } export async function resetHold({ month }) { await setBuffer(month, 0); } export async function coverOverspending({ month, to, from }) { let sheetName = monthUtils.sheetForMonth(month); let toBudgeted = await getSheetValue(sheetName, 'budget-' + to); let leftover = await getSheetValue(sheetName, 'leftover-' + to); let leftoverFrom = await getSheetValue( sheetName, from === 'to-be-budgeted' ? 'to-budget' : 'leftover-' + from, ); if (leftover >= 0 || leftoverFrom <= 0) { return; } let amountCovered = Math.min(-leftover, leftoverFrom); // If we are covering it from the to be budgeted amount, ignore this if (from !== 'to-be-budgeted') { const fromBudgeted = await getSheetValue(sheetName, 'budget-' + from); await setBudget({ category: from, month, amount: fromBudgeted - amountCovered, }); } await setBudget({ category: to, month, amount: toBudgeted + amountCovered }); } export async function transferAvailable({ month, amount, category }) { let sheetName = monthUtils.sheetForMonth(month); let leftover = await getSheetValue(sheetName, 'to-budget'); amount = Math.max(Math.min(amount, leftover), 0); let budgeted = await getSheetValue(sheetName, 'budget-' + category); await setBudget({ category, month, amount: budgeted + amount }); } export async function transferCategory({ month, amount, from, to }) { const sheetName = monthUtils.sheetForMonth(month); const fromBudgeted = await getSheetValue(sheetName, 'budget-' + from); await setBudget({ category: from, month, amount: fromBudgeted - amount }); // If we are simply moving it back into available cash to budget, // don't do anything else if (to !== 'to-be-budgeted') { const toBudgeted = await getSheetValue(sheetName, 'budget-' + to); await setBudget({ category: to, month, amount: toBudgeted + amount }); } } export async function setCategoryCarryover({ startMonth, category, flag }) { let table = getBudgetTable(); let months = getAllMonths(startMonth); await batchMessages(async () => { for (let month of months) { setCarryover(table, category, dbMonth(month), flag); } }); }