Skip to content

038: Store Currency per Account - #173

Merged
plainprogrammer merged 25 commits into
mainfrom
038-currency-per-account
Nov 6, 2026
Merged

plainprogrammer merged 25 commits into
mainfrom
038-currency-per-account

Conversation

@plainprogrammer

@plainprogrammer plainprogrammer commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Implements spec 038 (v0.2.0) per its plan: each account stores the currency its prices paid are shown in. COLLECTOR_CURRENCY becomes only the currency a new account starts with. A collector changes theirs on a Currency page linked from More and the account menu. The export writes the account's currency (format version 3 unchanged), and applying an import brings the file's currency in.

What each phase did

Phase 1, the data layer.

  • accounts.currency is NOT NULL, default USD, added without a DDL transaction so its rollback copies the table with foreign keys off.
  • A data migration carries the instance's COLLECTOR_CURRENCY to every account. It checks the value against an inline list of the 20 codes, writes it with one bound UPDATE, and raises on an unsupported code.
  • Account defaults the column to x.default_currency, validates it ("Choose one of the listed currencies.") and adds #currency_unit.
  • The export writes accounts.currency.
  • The format document marks accounts "Exported" in its Coverage table and updates its prose. No schema, example or version changed.

Phase 2, every reader moves.

  • 2a: lot_price (card page, collection table, and through lot_line the Move copies and location-refusal pages) and the lot form's label and prefix read Current.account.currency_unit. import_lot_line takes currency:: the account's for a removed lot, the file's for a problem row.
  • 2b, the import:
    • A well-formed code outside the 20 is refused as unsupported_currency, replacing other_currency.
    • The plan gains currency_change, which counts against "nothing to change". Its two codes join the fingerprint only when there is a change, so other plans keep their old fingerprint.
    • replan reloads the account inside the apply's transaction, and the apply writes the currency first in that transaction.
    • The notice ends "; prices now shown in EUR". The preview's Profile section says "Your prices would be shown in EUR instead of USD. Nothing is converted."
    • The round trip applies every valid example (versions 1–3), whatever currency it carries.
  • 2c: x.currency is removed. spec/currency_reads_spec.rb fails on any read of x.currency or x.default_currency outside Account and the initializer.

Phase 3, the Currency page.

  • /currency/edit has one native select of the 20 codes (EUR — €), with a hint saying that changing converts nothing and how many lots have a price.
  • PATCH /currency accepts only account[currency] via params.expect. It redirects to More (303) with "Prices are now shown in EUR.", or re-renders 422 with a field error.
  • The page is plain HTML with no Stimulus. More lists Currency after Locations; the desktop account menu lists it after Import collection.

Phase 4, upgrade, backup and documents.

  • script/upgrade/seeds/next.rb puts Grace in EUR (AC-4.7).
  • A backup restore keeps each account's currency (AC-7.1).
  • Updated: the collection-and-lots and data-portability area docs, the north star's breach note (closed), the roadmap's Store currency per account #64 line, the README, compose.yaml and config/deploy.yml comments.
  • The specs index records the supersessions in both directions.

Rulings and deviations (each also in its commit body)

  • The Coverage rows landed with the column (phase 1's first commit) so every commit stays green.
  • 2b's refuse/plan/apply landed as one commit: removing other_currency alone would preview a EUR file as "nothing to change".
  • The Currency page's two sentences are the field's c-field__hint, tied by aria-describedby, as the import page's is. There is no new CSS or pattern doc; MorePage.md gains the Currency row.
  • Collector::Currency.fetch! takes setting:, so a bad stored code blames accounts.currency, not the environment variable (review m2).
  • After a refused code, the controller clears the record's errors once it has copied the messages. Rails' .field_with_errors wrapper narrowed the select. This was found in the screenshots and fixed tests first (lesson in docs/lessons/).
  • A one-commit bridge (68f9bee) set the round trip's account currency between phases 1 and 2b; 2b replaced it.
  • Commit trailers: the first seven commits (a684f56–68f9bee) carry Co-Authored-By: Claude Sonnet 5.5 by mistake. The session runs on Opus 5.5, and every later commit carries the Opus trailer. They are not rewritten (no force-push).

Tests

Implementation review

SPEC-ALIGNED WITH MINOR GAPS (sdd-review Mode B, read-only, Opus). Nothing was blocking. Every AC was met; AC-4.6 is met by CI's upgrade proof only, and AC-3.4 by the unit test plus reading.

  • Major: verification.md was missing. It is written.
  • Minors:
    • MorePage doc: fixed.
    • fetch!'s error naming the environment variable: fixed.
    • AC-6.7's rendered sentence: now asserted.
    • xyz: now tested.
    • AC-4.3's second clause: recorded as proved by structure.
    • The local Gemfile.lock arm64 noise: left uncommitted.

Details per AC are in verification.md.

Screenshots

Captured in headless Firefox against this worktree's dev server, seeded by a scratch script with one collector, four printings and three priced lots. The tiles have no card art.

Walkthrough: the table in $, More → Currency → EUR → Save, the notice, the table in €, the lot form (EUR) / €, and an import preview of a USD file ("Your prices would be shown in USD instead of EUR", removed lots in €).

walkthrough

Desktop, light Desktop, dark
Phone (390px), light Phone (390px), dark More on a phone

A refused code (422), and the import preview in full:

For the maintainer to check by hand

  • AC-4.6: CI's bin/upgrade-smoke, on both architectures, must stay green. spec/backup_upgrade_spec.rb rolls back STEP=1, which is now the empty data step, so it no longer exercises a schema rollback. The migration spec covers the column's rollback. Consider STEP=2 there.
  • Your copies on a card page: open one in a non-USD account and check a price in the select's symbol style (CHF — CHF, kr). Those repeats are accepted by decision 8.
  • Stored previews at deploy: one made before the deploy, whose file's currency differs from the account's, shows as stale once and needs previewing again (an accepted risk in the plan).

Closes #64

🤖 Generated with Claude Code

plainprogrammer and others added 23 commits November 5, 2026 23:25
Spec 038 FR-1, decision 1, AC-4.5: accounts.currency, NOT NULL, default USD,
added without a DDL transaction so a rollback copies the table with foreign
keys off. The format document's Coverage rows land with the column, since the
Coverage guard fails the moment accounts has an unlisted column.

Ruling: the Coverage rows move into this commit from the plan's
"docs(export-format)" commit, so every commit leaves the suite green; the prose
changes stay in their own commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spec 038 FR-3, decision 3, AC-4.1–4.5: a data step sets every account to
COLLECTOR_CURRENCY (strip, upcase, default USD), checked against an inline copy
of the 20 codes, in one bound UPDATE; an unsupported code raises naming the
variable and value and changes nothing; down leaves the data.

The spec stubs ENV with a Hash (stub_const), which reaches ENV.fetch and
ENV[] in the migration as the plan hoped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spec 038 FR-1, decision 2, AC-3.1–3.3, AC-3.5: the initializer keeps the boot
check and stores COLLECTOR_CURRENCY as x.default_currency; Account defaults
its currency to it, validates inclusion in the 20 codes ("Choose one of the
listed currencies.") and answers currency_unit. x.currency stays as a
temporary alias until phase 2 moves the last reader.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spec 038 FR-5, AC-5.1–5.3: the export writes accounts.currency, still at
format version 3; VERSIONS stays [1, 2, 3].

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spec 038 decision 6, AC-5.4: the Currency section says the code is the
collector's own, and the Versions section drops #64 from the expected
changes, saying it moved without a version. Schemas and examples untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The export now writes accounts.currency, while the import still compares with
the instance key; the round trip of each valid example sets both until phase
2b rewrites it (spec 038 AC-6.10). Found by the pre-push run (seed 60803).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spec 038 FR-4, decision 8, AC-1.3, AC-1.4: lot_price (card page, collection
table, and through lot_line the Move copies and location refusal pages) and
the lot form's label and prefix read Current.account.currency_unit;
import_lot_line takes a required currency: a held lot in the account's, a
file's row in the file's.

Ruling: an import_previews_helper spec is added beside the planned
lots_helper spec, so the new keyword has a direct test before 2b's request
examples (AC-6.8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 038 FR-6, decision 7, AC-6.1–6.9: a well-formed code outside the 20 is
refused as unsupported_currency ("Collector can't show prices in XYZ."),
replacing other_currency; the plan gains currency_change (from the account's,
to the file's), counts it against "nothing to change" and appends both codes
to the fingerprint only when set, so a plan without one keeps its old
fingerprint; replan reloads the account inside the apply's transaction; the
apply writes the currency first in its transaction; the notice ends
"; prices now shown in EUR"; the preview's Profile says "Your prices would be
shown in EUR instead of USD. Nothing is converted."

Ruling: the plan's three commits (refuse, plan, apply and notice) land as one,
because removing the other_currency refusal makes a file in another currency
previewable, which needs the plan's change to be right — splitting them would
leave a commit that previews a EUR file as "nothing to change".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 038 AC-6.10, AC-6.11: the round trip no longer stubs the instance
currency or sets the account's; each valid example (versions 1–3) applies to
an account at the default and exports the same collection, its currency
included, and then has nothing to change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t its return

Spec 038 decision 2, FR-4: x.currency is gone; spec/currency_reads_spec.rb
fails on any read of x.currency or x.default_currency in app/, lib/ or
config/ outside Account and the initializer. The source-check spec joins the
RSpec/DescribeClass exclusions, as spec/footer_core_spec.rb does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 038 FR-2, decisions 4–5, AC-1.2, AC-1.3, AC-1.5–1.8, AC-2.1–2.3:
/currency/edit shows one native select of the 20 codes ("EUR — €", in
Collector::Currency's order, the account's own selected) and Save; PATCH
/currency updates Current.account only (params.expect), redirects to More
(303) with "Prices are now shown in EUR.", or re-renders 422 with "Choose one
of the listed currencies." and the saved code selected. No Stimulus.

Ruling: the two sentences ("Changing the currency doesn't convert…" and the
priced-lot count) are the field's c-field__hint, tied to the select with
aria-describedby, as the import page's hint is, so the page needs no new CSS
or pattern doc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 038 decision 4, AC-1.1: More lists "Currency" after Locations, and the
desktop account menu after Import collection; the navigation examples that
pin positions move by one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 038 AC-4.7: next.rb puts Grace's account in EUR, so the next release's
upgrade proof carries a non-default currency across; the seeds spec checks
it. No seed_exceptions entry was needed (the coverage check passes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 038 AC-7.1: a backup of accounts in USD and EUR restores each with its
own currency, and the EUR collector's table writes €12.50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 038 "Documents that change": collection and lots (price paid in the
account's currency, the default, the Currency page, the guard), data
portability (the export's source, unsupported_currency, the plan, fingerprint,
preview, apply order and notice, More's rows), the north star's breach note
closed, the roadmap's #64 line built, README's COLLECTOR_CURRENCY row and
backup paragraph, compose.yaml and config/deploy.yml comments.
docs/backup-restore.md repeats no currency sentence, so it is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Superseded-by cells of specs 004, 006, 017, 022, 025, 028, 030 and 032
name 038, matching its Supersedes table; the shipped specs are not edited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found looking at the screenshots: after a refused code the account kept its
errors, so Rails wrapped the label and select in .field_with_errors and the
select shrank to its content width. The controller copies the messages for the
page and clears the record's errors; the request spec asserts no wrapper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 038 Mode B review (verification.md):
- m2: Collector::Currency.fetch! takes setting:, and Account#currency_unit
  names accounts.currency, so a bad stored code no longer blames the
  environment variable.
- m3: a request example renders the stale page after a currency change and
  finds "Your prices would be shown in EUR instead of GBP." (AC-6.7).
- m4: a lower-case `xyz` currency is a schema refusal (AC-6.6).
- m1: MorePage.md lists Currency after Locations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verification.md records each AC's evidence, the rulings, the AC-4.6
vacuity note on spec/backup_upgrade_spec.rb (STEP=1 now rolls back only the
empty data step), the gaps, and the Mode B review (SPEC-ALIGNED WITH MINOR
GAPS) with how each finding was handled. Screenshots and the walkthrough GIF
were captured against the worktree's dev server with a seeded collector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lesson from spec 038: a form that renders its own field errors must clear
the record's, or Rails' .field_with_errors wrapper breaks the c-field layout;
found only in a screenshot of the refused state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-portability

The upgrade that carried COLLECTOR_CURRENCY to every account (Story 4) and the
backup's restoring each account's currency (Story 7) join the statements
phase 4 wrote; both docs gain a Distillation log row and their Sources name
the stories. The index marks spec 038 Frozen, by both docs, with its plan's
retirement owed. CLAUDE.md states no currency rule, so it is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… portability area docs (spec 027 D4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@plainprogrammer

Copy link
Copy Markdown
Owner Author

Follow-ups filed by the coordinator: #179 (backup_upgrade_spec's STEP=1 now rolls back only the empty data step), #180 (field_with_errors wrappers on other forms' 422s), and #178 (harness tmpdir race hit by the pre-push run).

@plainprogrammer
plainprogrammer marked this pull request as ready for review November 6, 2026 05:46
@plainprogrammer plainprogrammer self-assigned this Nov 6, 2026
plainprogrammer and others added 2 commits November 6, 2026 01:13
Brings in specs 037 (location filter) and 039 (art index missing file).
Conflicts resolved by keeping both sides: db/schema.rb regenerated by
migrating (accounts.currency and bulk_selections' location filter, version
2026_11_06_100001); collection-and-lots Sources and Distillation log hold both
037 and 038 (#47 and #64 both closed); the specs index keeps 037 and 039
frozen from main and 038 frozen from this branch, and spec 030's Superseded-by
cell names both 039 and 038.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in spec 040 (scan a sitting into a location). Conflicts resolved by
keeping both sides: collection-and-lots drops both resolved open items (#56
and #64) and lists both specs in Sources and the Distillation log;
data-portability lists both specs; spec 032's Superseded-by cell names 040
and 038. db/schema.rb regenerated by migrating (accounts.currency and
scanner_sittings' location).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@plainprogrammer
plainprogrammer merged commit 407799d into main Nov 6, 2026
9 checks passed
@plainprogrammer
plainprogrammer deleted the 038-currency-per-account branch November 6, 2026 06:33
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.

Store currency per account

1 participant