feat: Wave 10 — expense tracker pack (voice-driven)
Some checks are pending
Rust CI / cargo test (jarvis-core) (push) Waiting to run
Rust CI / cargo clippy (push) Waiting to run
Rust CI / cargo check (workspace) (push) Waiting to run

resources/commands/expenses/ (4 commands):
- expenses.log     "потратил 500 на еду"     → append entry
- expenses.today   "сколько потратил сегодня" → today's total
- expenses.week    "сколько на неделе"       → 7-day rolling total
- expenses.breakdown "куда ушли деньги"      → top-5 categories

Storage: `jarvis.state` (auto-namespaced per-pack JSON). Wire format is
compact "ts1,amount1,cat1|ts2,amount2,cat2" so we don't need a JSON parser
inside Lua.

Phrase parsing:
- Trigger stripped via jarvis.text.strip_trigger.
- First number captured; "12,50" comma decimals accepted.
- Category extracted from "на <слово>" (RU) or "for/on <word>" (EN);
  defaults to "разное" / "misc".

Time math:
- Day boundary: local-calendar midnight via os.time({year,month,day,...}).
- Week boundary: rolling 7×86400 seconds.

Pack count: 98 → 99 functional.
This commit is contained in:
Bossiara13 2026-05-16 14:57:58 +03:00
parent bb3b4f5942
commit 87bb186e94
5 changed files with 293 additions and 0 deletions

View file

@ -0,0 +1,55 @@
-- Per-category breakdown for the last 7 days, sorted by amount desc.
local lang = jarvis.context.language
local raw_state = jarvis.state.get("entries") or ""
if raw_state == "" then
return jarvis.cmd.not_found(lang == "ru"
and "Нечего разбивать, сэр."
or "Nothing to break down, sir.")
end
local now = jarvis.context.time
local now_ts = os.time({
year = now.year, month = now.month, day = now.day,
hour = now.hour, min = now.minute, sec = now.second or 0,
})
local cutoff = now_ts - 7 * 86400
local by_cat = {}
for chunk in raw_state:gmatch("([^|]+)") do
local ts, am, ca = chunk:match("^(%d+),([%-%d%.]+),(.+)$")
ts = tonumber(ts); am = tonumber(am)
if ts and am and ts >= cutoff then
by_cat[ca] = (by_cat[ca] or 0) + am
end
end
local rows = {}
for ca, total in pairs(by_cat) do
table.insert(rows, { ca = ca, total = total })
end
table.sort(rows, function(a, b) return a.total > b.total end)
if #rows == 0 then
return jarvis.cmd.ok(lang == "ru"
and "На этой неделе тишина по тратам, сэр."
or "Quiet week on spending, sir.")
end
-- Top 5 categories spoken; rest collapses into "и другое"
local top = math.min(5, #rows)
local parts = {}
for i = 1, top do
table.insert(parts, string.format("%s %.0f", rows[i].ca, rows[i].total))
end
local tail = ""
if #rows > top then
local rest_total = 0
for i = top + 1, #rows do rest_total = rest_total + rows[i].total end
tail = string.format(lang == "ru"
and " и другое %.0f" or " and other %.0f", rest_total)
end
return jarvis.cmd.ok(string.format(lang == "ru"
and "По категориям: %s%s."
or "By category: %s%s.", table.concat(parts, ", "), tail))