summaryrefslogtreecommitdiffstats
path: root/firefly_cli/commands/piggy.py
blob: 9ade3c83686ad2c3df5ad9bd52d8d711de745cdb (plain)
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
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>  GPL-2.0-only
from decimal import Decimal, InvalidOperation
from firefly_cli import registry, output
from firefly_cli.errors import FireflyError


@registry.command("piggy list", help="list piggy banks with saved/target amounts and linked accounts")
def cmd_list(args, ctx):
    resp = ctx.client.request("GET", "/api/v1/piggy-banks", params={"limit": 1000})
    output.emit(output.unwrap(resp), human=ctx.human)
    return 0


def _move_args(p):
    p.add_argument("ref", help="piggy bank name or id")
    p.add_argument("amount", help="amount to move (positive)")
    p.add_argument("--account", default=None,
                   help="linked asset account name (required if the piggy bank has several)")


def _amount(raw):
    try:
        amt = Decimal(str(raw))
    except InvalidOperation:
        amt = None
    if amt is None or not amt.is_finite() or amt <= 0:
        raise FireflyError(f'amount must be a positive number, got "{raw}"')
    return amt


def _move(args, ctx, sign):
    amt = _amount(args.amount)
    pb = ctx.resolver.piggy(args.ref)
    linked = pb.get("accounts") or []
    if args.account is None:
        if len(linked) != 1:
            raise FireflyError(
                f'piggy bank "{pb.get("name")}" is linked to {len(linked)} accounts; '
                "pass --account: " + ", ".join(a.get("name", "") for a in linked))
        target = linked[0]
    else:
        hits = [a for a in linked if str(a.get("name", "")).lower() == args.account.lower()]
        if len(hits) != 1:
            raise FireflyError(
                f'account "{args.account}" is not linked to piggy bank "{pb.get("name")}". '
                "Linked: " + (", ".join(a.get("name", "") for a in linked) or "(none)"))
        target = hits[0]

    new = Decimal(str(target.get("current_amount") or "0")) + sign * amt
    if new < 0:
        raise FireflyError(
            f'cannot remove {amt}: only {target.get("current_amount")} saved from {target.get("name")}')
    new_s = f"{new:.2f}"
    # Firefly syncs the whole account list on PUT, so every linked account must
    # be resent or it gets unlinked. Omitting current_amount keeps its saved value.
    body = {"accounts": [
        {"account_id": a["account_id"], "current_amount": new_s} if a is target
        else {"account_id": a["account_id"]}
        for a in linked]}
    resp = ctx.client.request("PUT", f"/api/v1/piggy-banks/{pb['id']}", body=body)
    saved = output.unwrap(resp)
    # Validation normally 422s an unaffordable add, but PiggyBankFactory can
    # still skip it and answer 200. Read back and fail loudly instead.
    after = next((a for a in saved.get("accounts") or []
                  if str(a.get("account_id")) == str(target["account_id"])), {})
    if Decimal(str(after.get("current_amount") or "0")) != new:
        raise FireflyError(
            f'Firefly did not apply the change (saved {after.get("current_amount")}, wanted {new_s}): '
            "not enough unallocated money on the account, or the target would be exceeded")
    output.emit(saved, human=ctx.human)
    return 0


@registry.command("piggy add", help="put money into a piggy bank", args=_move_args)
def cmd_add(args, ctx):
    return _move(args, ctx, 1)


@registry.command("piggy remove", help="take money out of a piggy bank", args=_move_args)
def cmd_remove(args, ctx):
    return _move(args, ctx, -1)