mirror of
https://github.com/we-promise/sure.git
synced 2026-09-03 13:51:29 +00:00
* feat(mcp): Add MCP budget update tool Adds an update_budget assistant/MCP function so AI assistants can write monthly budgets: total budgeted spending, expected income, and per-category allocations in one transactional call. - Month resolution and slug format mirror get_budget (YYYY-MM or MMM-YYYY, custom month start respected); targeting a valid month with no budget row bootstraps it via Budget.find_or_bootstrap, same as the budgets UI. - Category allocations accept an exact (case-insensitive) name or id and go through BudgetCategory#update_budgeted_spending!, so subcategory writes keep the parent total in sync. - All writes in one call share a transaction: an invalid category rolls back a totals change from the same call. - Family-scoped like the budgets UI; amounts validated non-negative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(mcp): harden update_budget per review feedback - Extract shared month resolution into Assistant::Function::MonthResolvable so get_budget and update_budget can't drift on custom month starts - Run budget bootstrap inside the update transaction so a failed entry no longer leaves a newly created budget behind - Apply explicit parent amounts after subcategory syncs so results don't depend on the caller's array order - Reject non-finite amounts (NaN/Infinity) - Explain the synthetic Uncategorized bucket instead of a generic category-not-found error Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
30 lines
1.1 KiB
Ruby
30 lines
1.1 KiB
Ruby
# Shared month handling for budget-facing assistant tools so month slugs
|
|
# round-trip between get_budget and update_budget, including families with a
|
|
# custom month start day.
|
|
module Assistant::Function::MonthResolvable
|
|
private
|
|
def resolve_month_start(raw)
|
|
base = parse_month(raw)
|
|
return (base || Date.current).beginning_of_month unless family.uses_custom_month_start?
|
|
|
|
# Match Budget.param_to_date for explicit slugs so the input round-trips with the response.
|
|
base ? Date.new(base.year, base.month, family.month_start_day) : family.custom_month_start_for(Date.current)
|
|
end
|
|
|
|
def parse_month(raw)
|
|
return nil if raw.blank?
|
|
|
|
# Date.strptime ignores trailing characters, so guard with strict anchors first.
|
|
fmt = case raw
|
|
when /\A\d{4}-\d{2}\z/ then "%Y-%m"
|
|
when /\A[A-Za-z]{3}-\d{4}\z/ then "%b-%Y"
|
|
end
|
|
|
|
raise Assistant::Error, "Invalid month: #{raw}. Use YYYY-MM or MMM-YYYY." if fmt.nil?
|
|
|
|
Date.strptime(raw, fmt)
|
|
rescue ArgumentError
|
|
raise Assistant::Error, "Invalid month: #{raw}. Use YYYY-MM or MMM-YYYY."
|
|
end
|
|
end
|