File size: 3,116 Bytes
bfd6783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
"""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))