Skip to content

Store money as integer cents - #348

Merged
eirvandelden merged 23 commits into
mainfrom
cents-instead-of-decimals
Oct 22, 2026
Merged

eirvandelden merged 23 commits into
mainfrom
cents-instead-of-decimals

Conversation

@eirvandelden

Copy link
Copy Markdown
Owner

Summary

Amounts live in decimal columns, so half cents can enter the books through arithmetic such as splits and receipt lines. A half cent changes a total that a user sees, and no bank statement has one.

Every money amount is stored as a whole number of cents: what is 1,5 today becomes 150. Users still type and read 1,50 in the interface. No amount that a user sees goes up or down because of the change.

Implementation

New:

  • app/models/cents.rb — Cents.parse, Cents.from_euros, Cents.to_euros.
  • app/models/cents/type.rb — Cents::Type.
  • app/models/concerns/monetary.rb — monetary macro and the unreadable-amount validation.
  • app/helpers/money_helper.rb — cents_to_currency, cents_field_value.
  • db/migrate/20261016120000_convert_money_to_cents.rb — the migration.
  • test/models/cents_test.rb, test/models/cents/type_test.rb, test/models/concerns/monetary_test.rb, test/models/money_fixtures_test.rb, test/helpers/money_helper_test.rb, test/migrations/convert_money_to_cents_test.rb, test/integration/transaction_amounts_test.rb, test/tools/assistant/budget_status_test.rb.
  • docs/changes/cents-instead-of-decimals/verify/dump_before.rb, verify/dump_after.rb, verify/dump_raw.sql — the real-data check scripts. They leave with the change folder at delivery.

Changed:

  • db/schema.rb — ten columns become integer.
  • Models: appropriation.rb, budget_category.rb, chattel.rb, receipt.rb, receipt_line.rb, transaction_split.rb, transaction.rb — include Monetary, monetary ..., numericality → comparison, search SQL, amount_was.to_i, return-type doc comments (@return [Integer] cents).
  • app/models/concerns/splittable.rb, linkable.rb — doc comments to Integer cents; Cents.parse in searchable_transfers.
  • app/models/budget.rb — suggested_amounts rounds to Integer cents.
  • app/models/dashboard.rb — budget_amounts and historical_averages in Integer cents; doc comments.
  • app/models/importing/ing/row.rb, app/models/importing/albert_heijn/packing_slip.rb — cents.
  • Helpers: products_helper.rb (to_d division), todos_helper.rb (cents_to_currency).
  • Views — every number_to_currency becomes cents_to_currency: appropriations/{index,show,_balance}, budgets/show, chattels/_table, dashboard/_balance, product_types/show, products/{show,_price_chart}, receipts/{index,show,_basket}, searches/_result_transaction, shared/_recent_transactions, transaction_links/{_balance_table,_search_results,_suggestions}, transaction_splits/_table, transactions/{_table,show}.
  • Form fields get value: cents_field_value(...): appropriations/_form, budgets/_budget_category_fields, chattels/_form, transaction_splits/_table, transactions/_form.
  • dashboard/index.html.erb — chart data converted to euros.
  • transactions/_transaction.json.jbuilder — amount in euros.
  • MCP tools: budget_status.rb, charge_to_appropriation.rb, find_uncategorized_transactions.rb, list_appropriations.rb, set_appropriation.rb, set_budget_amount.rb.
  • Locales en.yml, nl.yml, it.yml — activerecord.errors.messages.not_an_amount (en "is not an amount", nl "is geen bedrag", it "non è un importo"); then bundle exec i18n-tasks normalize.
  • test/fixtures/{appropriations,budget_categories,chattels,receipt_lines,receipts,transaction_splits,transactions}.yml — values in cents.
  • Existing tests that use euro literals for money, across test/models, test/integration, test/helpers, test/jobs, test/tools, test/views, test/system (56 files mention an amount; only those whose literals or expectations break change). test/system/budgets_test.rb formats the expected field value with format("%.2f", Cents.to_euros(amount)).

Proof

  • A transaction stored as 1,5 shows as 1,50 after the migration and as 150 in the database → test/migrations/convert_money_to_cents_test.rb "up stores a transaction of 1,5 as 150 cents" and test/integration/transaction_amounts_test.rb "a transaction of 150 cents shows as 1,50"
  • A user who types 1,50 in the transaction form gets 150 stored and sees 1,50 again → test/integration/transaction_amounts_test.rb "typing 1,50 stores 150 cents and the form shows 1,50 again"
  • A user who types 1,5 or 1.50 gets the same result → test/integration/transaction_amounts_test.rb "typing 1,5 or 1.50 stores 150 cents"
  • A transaction of 12,34 split into 10,00 and 2,34 still sums to 12,34 and stays valid → test/integration/transaction_splits_test.rb "splitting 12,34 into 10,00 and 2,34 leaves nothing unsplit"
  • A receipt with lines that add up to its total before the migration still adds up after it → test/migrations/convert_money_to_cents_test.rb "receipt lines that add up to the total still do after up"
  • Budget totals and dashboard balances show the same figures before and after the migration → test/integration/dashboard_test.rb and test/integration/budgets_test.rb existing figure assertions, green on cent fixtures; and check: the real-data comparison below, dashboard and budget section
  • The MCP tool for budget status still reports 12.50 for 1250 cents → test/tools/assistant/budget_status_test.rb "reports a plan of 1250 cents as 12.50"
  • Rolling the migration back restores the original decimal values → test/migrations/convert_money_to_cents_test.rb "down restores the original decimal values"; and check: after the real-data run, bin/rails db:rollback, then sqlite3 storage/db/development.sqlite3 < docs/changes/cents-instead-of-decimals/verify/dump_raw.sql > tmp/raw_after_rollback.txt, and diff tmp/raw_before.txt tmp/raw_after_rollback.txt prints nothing. This raw dump needs no app code, so the branch's cent-based code cannot distort it. Migrate up again afterwards.

@eirvandelden
eirvandelden marked this pull request as ready for review October 17, 2026 06:29
eirvandelden and others added 19 commits October 22, 2026 08:50
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The tools find_uncategorized_transactions and charge_to_appropriation convert cents to euros. No test proved it. A regression to raw cents would have passed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The partial shows the amount in euros. No test read it. The test stubs two account columns the partial still reads, so the existing bug there stays out of scope.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An unreadable amount also failed the presence and comparison checks. The form printed three messages. Now only 'is not an amount' stays.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
printf turned a NULL amount into 0.00, so a search for 0 found it. The match now needs an amount.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two tests get the Proof name. Three new tests cover the comma amount, the paid amount and the packing slip cents.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@eirvandelden
eirvandelden force-pushed the cents-instead-of-decimals branch from b50d22d to f14e8f0 Compare October 22, 2026 06:55
eirvandelden and others added 4 commits October 22, 2026 09:27
Main added appropriation tests in euros while this branch moved money to cents.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@eirvandelden
eirvandelden force-pushed the cents-instead-of-decimals branch from f14e8f0 to 9159722 Compare October 22, 2026 07:36
@eirvandelden
eirvandelden merged commit 7bc7052 into main Oct 22, 2026
7 checks passed
@eirvandelden
eirvandelden deleted the cents-instead-of-decimals branch October 22, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant