buzzromainandClaude Opus 5 613964529a feat(goals): let a goal be spent without looking like it fell behind (#3176)
* feat(goals): let a goal be spent without looking like it fell behind

Coming home from the holiday a goal paid for dropped it from 100% to 20%. The
money went where it was meant to go, and the app read that as failure. The only
way back was to edit the target — falsifying what the user had actually set out
to save.

`consumed_amount` records what was spent ON the thing the goal was for, and
progress reads `(backing + consumed) / target`. Spending the money is no longer
indistinguishable from losing it.

**The part that is easy to miss.** `consume!` also shrinks the earmark on the
account by the same amount. Without that, money the user has already spent
stays reserved and keeps its share away from every sibling goal — the exact
double-counting the exclusivity rules exist to prevent, arriving through the
back door. A test pins it through the pro-rata haircut, where the effect is
visible: a sibling's backing grows as the spent share is released.

**Kept separate from `completed_amount`.** That one freezes the BACKING at
closure; folding consumption into it would count the same money twice on a goal
partly spent and then closed. A test asserts each side is counted once.

**A reserve refuses consumption outright.** It is drawn down and refilled, not
spent, and recording a withdrawal as consumption would erase the shortfall the
reserve exists to report.

`account:` may be omitted only when the goal has one link — with several,
guessing would silently pick a side. The controller refuses an account id that
resolves to nothing rather than falling back to nil, which on a single-link
goal would have recorded the spend against an account the user never named.

The write is its own action rather than a verb branch inside `consume`: HEAD
routes like GET but `request.get?` is false for it, so a branch would send a
HEAD request down the write path. Brakeman caught that.

bin/rails test: 7088 runs, 28510 assertions, 0 failures. RuboCop, erb_lint and
Brakeman clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye

* fix(goals): make a spend either happen entirely or not at all

Review on #3176 found the recorded spend and the released earmark could
drift apart, and that nothing stopped the two figures being edited out of
agreement afterwards.

The lock was on the link, not the goal. `consumed_amount` lives on the
goal, so two concurrent requests locking only their own links both read
the same old value, both passed the target check, and both added to it.
The whole check-and-write now runs under `with_lock` on the goal.

Consuming more than the chosen link held was silently clamped: the link
released what it had while `consumed_amount` took the full figure, so
money counted as spent stayed reserved against every sibling goal. It is
refused now — `:exceeds_earmark` — rather than half-applied.

A dialog left open in another tab could still post to a goal that had
since been completed or archived; `:not_active` closes that.

Two validations stop the pair being separated after the fact: a goal that
has recorded a spend cannot become a reserve (reserves refuse consumption,
so the figure would count toward progress on an object whose model treats
spending as a shortfall), and the target cannot be lowered below what was
already spent.

Consuming cleared the columns and left the memos standing, so an instance
that had already read its backing kept reporting the pre-spend figure.
Progress holding steady is the feature — the earmark shrinks by what
consumption grows by — which is exactly what hid the stale backing.

Also from review: `accessible_accounts` rather than the whole family for
the account picker, `DS::Select` rather than a bare `select_tag`, the
flash amount through `Money#format`, and the French label for the menu
entry, which I had left untranslated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye

* refactor(goals): reuse the cache reset the class already owns

Review pointed out `reset_state_dependent_caches!` exists for exactly this,
and that hand-rolling a second ivar list was the wrong shape. It was also
wrong in substance: mine omitted `@pooled_allocations`, and consuming
shrinks a link's allocation, which is precisely what the pool is computed
from. One list, kept in one place, stays right when a memo is added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye

* docs(goals): put four comments back on the methods they describe

Rebases had stranded them: a paragraph about clearing memos on an AASM
transition, two about what reopening does to a frozen figure, and one
about `reload` leaving memos standing had all piled up in front of
`consumption_link_for`, which does none of those things.

The last is dropped rather than moved — its explanation now sits at the
call site in `consume!`, where the reset actually happens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye

* fix(goals): keep a private backing account out of the consume dialog

A goal can be backed by an account the viewer is not allowed to see, and
the first pass only guarded the named-account path. Two ways round it
remained.

The dialog listed every link, so it named private accounts outright. And
with `account_id` left blank the model picked the sole link on its own,
without anyone having checked the viewer could reach it — so a direct POST
reduced a private account's earmark, the figures moving afterwards saying
roughly how much was in it.

The controller now derives the eligible links from
`Current.user.accessible_accounts`, the dialog renders those, and a blank
id resolves only to a sole *eligible* account. With none the request is
refused; with several it stays nil and the model asks, as before.

Naming the account explicitly matters even when the goal has several
links: the one the viewer can reach is not necessarily the one the model
would have picked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:52:25 +02:00
2024-02-02 09:05:04 -06:00
2024-02-02 09:05:04 -06:00
2024-02-02 09:05:04 -06:00
2025-05-20 13:31:05 -05:00
2024-02-02 09:05:04 -06:00
2024-08-23 10:06:24 -04:00
2026-06-15 23:29:36 +02:00
2024-02-02 09:05:04 -06:00
2026-06-15 23:29:36 +02:00
2024-02-02 09:05:04 -06:00
2025-09-24 00:19:51 +02:00
2026-04-13 13:44:37 +02:00
2024-02-02 09:05:04 -06:00
2026-06-05 15:16:41 +02:00

Ask DeepWiki View performance data on Skylight Dosu Pipelock Security Scan

sure_shot

Deutsch | Español | Français | 日本語 | 한국어 | Português | Русский | 中文

Sure: The personal finance app for everyone

Get involved: DiscordWebsiteIssues

Important

This repository is a community fork of the now-abandoned Maybe Finance project.
Learn more in their final release doc.

Backstory

The Maybe Finance (archived/abandoned repo) team spent most of 20212022 building a full-featured personal finance and wealth management app. It even included an “Ask an Advisor” feature that connected users with a real CFP/CFA — all included with your subscription.

The business end of things didn't work out, and so they stopped developing the app in mid-2023.

After spending nearly $1 million on development (employees, contractors, data providers, infra, etc.), the team open-sourced the app. Their goal was to let users self-host it for free — and eventually launch a hosted version for a small fee.

They actually did launch that hosted version … briefly.

That also didnt work out — at least not as a sustainable B2C business — so now here we are: hosting a community-maintained fork to keep the codebase alive and see where this can go next.

Join us!

Hosting Sure

Sure is a fully working personal finance app that can be self hosted with Docker. Sure can be accessed from a browser, the macOS desktop app, the mobile app, API clients, and LLM agents. See Sure Clients for an overview.

Forking and Attribution

This repo is a community fork of the archived Maybe Finance repo. Youre free to fork it under the AGPLv3 license — but wed love it if you stuck around and contributed here instead.

To stay compliant and avoid trademark issues:

  • Be sure to include the original AGPLv3 license and clearly state in your README that your fork is based on Maybe Finance but is not affiliated with or endorsed by Maybe Finance Inc.
  • "Maybe" is a trademark of Maybe Finance Inc. and therefore, use of it is NOT allowed in forked repositories (or the logo)

Performance Issues

With data-heavy apps, inevitably, there are performance issues. We've set up a public dashboard showing the problematic requests seen on the demo site, along with the stacktraces to help debug them.

https://www.skylight.io/app/applications/s6PEZSKwcklL/recent/6h/endpoints

Any contributions that help improve performance are very much welcome.

Local Development Setup

If you are trying to self-host the app, read this guide to get started.

The instructions below are for developers to get started with contributing to the app.

Requirements

  • See .ruby-version file for required Ruby version
  • PostgreSQL >9.3 (latest stable version recommended)
  • Redis > 5.4 (latest stable version recommended)

Getting Started

cd sure
cp .env.local.example .env.local
bin/setup
bin/dev

# Optionally, load demo data
rake demo_data:default

Visit http://localhost:3000 to view the app.

If you loaded the optional demo data, log in with these credentials:

  • Email: user@example.com
  • Password: Password1!

For further instructions, see guides below.

Setup Guides

One-click Install

Run on PikaPods

Deploy on Railway

Managed OpenClaw for Sure Finances

Managed OpenClaw for Sure Finances

License and Trademarks

Maybe and Sure are both distributed under an AGPLv3 license.

  • "Maybe" is a trademark of Maybe Finance, Inc.
  • "Sure" is not, and refers to this community fork.

Alt

S
Description
No description provided
Readme AGPL-3.0
146 MiB
Languages
Ruby 77.5%
HTML 12.9%
Dart 5.7%
JavaScript 3.1%
Rust 0.3%
Other 0.2%