Oracle / finbot /features /budgets.py
spacedout-bits's picture
Deploy coco-finbot
bfd6783 verified
Raw History Blame Contribute Delete
3.12 kB
"""Monthly budgets per category."""
from __future__ import annotations
from typing import Sequence
from .. import render
from ..analytics import budget_status, month_bounds
from ..categories import CATEGORIES, coerce
from ..categories import label as category_label
from ..models import Budget
from ..money import parse_amount
from .base import BotContext, CommandSpec, Reply, SimpleFeature
class BudgetsFeature(SimpleFeature):
name = "budgets"
description = "🎯 Budgets"
priority = 20
def commands(self) -> Sequence[CommandSpec]:
return (
CommandSpec("budget", "Set a monthly budget", "<category> <amount>"),
CommandSpec("budgets", "How you're tracking against budgets"),
CommandSpec("unbudget", "Remove a budget", "<category>"),
CommandSpec("categories", "List the category names"),
)
def cmd_budget(self, ctx: BotContext) -> Reply:
args = ctx.args.strip()
if not args:
return Reply(
"Set a monthly cap, e.g. <code>/budget food_dining 8000</code>.\n"
"/categories lists the names."
)
parts = args.split()
category = coerce(parts[0])
if category == "other" and parts[0].lower() not in ("other",):
return Reply(
f"I don't recognise the category <code>{render.escape(parts[0])}</code>.\n"
"/categories lists what I understand."
)
amount = parse_amount(" ".join(parts[1:]), ctx.settings.default_currency)
if amount is None or amount.minor <= 0:
return Reply("I need an amount, e.g. <code>/budget groceries 12000</code>.")
budget = Budget(
user_id=ctx.user_id,
category=category,
amount_minor=amount.minor,
currency=amount.currency or ctx.settings.default_currency,
)
ctx.store.set_budget(budget)
return Reply(
f"🎯 Budget set: {render.escape(category_label(category))} "
f"{render.money(budget.amount_minor, budget.currency)}/month."
)
def cmd_budgets(self, ctx: BotContext) -> Reply:
budgets = ctx.store.get_budgets(ctx.user_id)
start, _ = month_bounds(ctx.today)
expenses = ctx.store.list_expenses(ctx.user_id, start, ctx.today)
return Reply(render.budget_list(budget_status(budgets, expenses)))
def cmd_unbudget(self, ctx: BotContext) -> Reply:
category = coerce(ctx.args.strip())
if ctx.store.delete_budget(ctx.user_id, category):
return Reply(f"Removed the {render.escape(category_label(category))} budget.")
return Reply("No budget set for that category.")
def cmd_categories(self, ctx: BotContext) -> Reply:
lines = ["<b>Categories</b>"]
lines.extend(
f"<code>{name}</code> — {render.escape(category_label(name))}"
for name in CATEGORIES
)
lines.append("")
lines.append("Tag an entry to override: <code>250 lunch #transport</code>")
return Reply("\n".join(lines))