"""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", " "), CommandSpec("budgets", "How you're tracking against budgets"), CommandSpec("unbudget", "Remove a budget", ""), 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. /budget food_dining 8000.\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 {render.escape(parts[0])}.\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. /budget groceries 12000.") 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 = ["Categories"] lines.extend( f"{name} — {render.escape(category_label(name))}" for name in CATEGORIES ) lines.append("") lines.append("Tag an entry to override: 250 lunch #transport") return Reply("\n".join(lines))