Skip to content

Multi-currency: currency rules, aliases, precision, and internal FX service - #54

Merged
ItsThompson merged 17 commits into
mainfrom
mc/02-currency-rules-fx-service
Aug 19, 2026
Merged

Multi-currency: currency rules, aliases, precision, and internal FX service#54
ItsThompson merged 17 commits into
mainfrom
mc/02-currency-rules-fx-service

Conversation

@ItsThompson

@ItsThompson ItsThompson commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

Implements currency rules, aliases, precision helpers, and the internal FX service. Makes period reporting currency immutable after creation. Gates expense writes behind budget period context. Adds rollout-safe transactionCurrency/currency alias resolution. Replaces fixed two-decimal frontend money behavior with catalog-driven precision. Introduces the FX Service as a new internal-only Go module with gRPC conversion contracts, Open Exchange Rates provider, one-hour caching, and decimal math.

Base branch: mc/01-currency-catalog-and-period-currency

What changed

Backend: Finance Service

  • Added GetPeriodContext gRPC method: returns period ID, user ID, year, month, reportingCurrency, and lock state.
  • Added immutable reporting-currency detection in period update REST handler. Returns REPORTING_CURRENCY_IMMUTABLE for raw JSON reportingCurrency or reporting_currency fields.
  • Default settings currency now validates against the shared catalog. Returns field-level error for unsupported values.
  • CreatePeriodWithProRata uses the current default settings currency for each missed period.
  • Pro-rata endpoint accepts transactionCurrency with legacy currency alias resolution and conflict detection.

Backend: Expense Service

  • Finance period-context client injected via FINANCE_SERVICE_ADDR. Calls GetPeriodContext before create-expense writes.
  • Missing period returns PERIOD_NOT_FOUND with year and month in the response. Blocks repository writes.
  • transactionCurrency is the canonical field. Legacy currency accepted as alias with legacy_currency_alias_used telemetry.
  • Both fields equal: canonicalizes to transactionCurrency. Both fields different: returns CURRENCY_FIELD_CONFLICT.
  • Neither field: defaults to the period's reportingCurrency. Unsupported currency returns UNSUPPORTED_CURRENCY.

Backend: FX Service (new module services/fx)

  • gRPC FxService with three RPCs: CaptureRateSnapshot, ConvertAmount, ConvertWithSnapshot.
  • Same-currency conversion returns identity rate 1 without calling the provider.
  • Foreign-currency conversion uses USD-based cross-rate formula with math/big.Rat and half-away-from-zero rounding.
  • Open Exchange Rates provider with configurable timeout, retry count, and capped exponential backoff with jitter.
  • One-hour cache with stale entry rejection. Provider failure falls back to fresh cache if available.
  • Error-to-gRPC status mapping for unsupported currency, invalid amount, conversion unavailable, provider failures, missing rates, and snapshot integrity.
  • Prometheus metrics and structured logging for conversions, provider calls, cache hits/misses, and latency.
  • No browser-facing routes. Gateway and access layer reject /api/fx with 403.

Frontend

  • frontend/packages/core/src/currency.ts: catalog-backed parseInput, formatAmount, getMinorUnitDigits, validateInputPrecision.
  • Currency selectors in NewExpenseFeature, CorrectionForm, BudgetSettingsEditor, and DefaultBudgetSection.
  • Amount inputs use currency-specific step, min, and placeholder values. JPY rejects decimals.
  • Expense and pro-rata payloads send transactionCurrency. Legacy currency field removed from new submissions.
  • CreatePeriodPrompt shows immutable reporting-currency warning. DefaultBudgetSection shows future-scoped default currency warning.

Deployment

  • docker-compose.yml and docker-compose.dev.yml: added fx-service on compute and monitoring networks.
  • monitoring/prometheus/prometheus.yml: added FX scrape target.
  • justfile: added FX to backend tests and proto generation.

Tests

Backend

  • services/fx: go test ./... passes. Converter tests cover USD identity, EUR to USD, USD to JPY, EUR to GBP, rounding halves, unsupported currencies, missing rates, cache hit, cache miss, provider failure with fresh cache, provider failure without cache, and captured snapshot conversion.
  • services/finance: go test ./... passes. Covers period-context gRPC, immutable update rejection, default-currency validation, missed-period currency, and pro-rata alias resolution.
  • services/expense: go test ./... passes. Covers missing-period gate, all alias compatibility rows (canonical only, legacy only, both equal, both different, neither, unsupported).
  • services/access and services/gateway: route-guard tests confirm no /api/fx browser route or proxy prefix.

Frontend

  • @gofin/core: 4 test files, 78 tests pass. Covers parse, format, precision validation for USD, EUR, JPY, negative values, unsupported currencies.
  • @gofin/api: 11 test files, 179 tests pass.
  • @gofin/finance: 50 test files, 479 tests pass. Covers precision-aware forms, new-expense, pro-rata, correction, settings, and budget editor.
  • All workspaces pass tsc --noEmit and eslint.

Notes

  • just test-backend fails in the shared worktree due to unrelated uncommitted Expense/Finance changes referencing stale generated protobuf types (financepb.GetPeriodContext). Per-service GOWORK=off go test ./... passes for each individual service.
  • Docker image build for fx-service requires Compose buildx 0.17.0+. Local environment has an older version.
  • Legacy currency response field mirrors transactionCurrency during rollout for old-client compatibility.

References

Tickets:

  • Immutable period currency and future-scoped defaults (Ticket 2)
  • Expense period gate and rollout currency aliases (Ticket 3)
  • Currency precision helpers and frontend controls (Ticket 5)
  • Internal FX Service and deployment wiring (Ticket 6)

Changesets:

  • /Users/thompsontong/Desktop/multi-currency-support-Spec/changesets/changeset-2.md
  • /Users/thompsontong/Desktop/multi-currency-support-Spec/changesets/changeset-3.md
  • /Users/thompsontong/Desktop/multi-currency-support-Spec/changesets/changeset-5.md
  • /Users/thompsontong/Desktop/multi-currency-support-Spec/changesets/changeset-6.md

@ItsThompson ItsThompson changed the title mc/02 currency rules fx service Multi-currency: currency rules, aliases, precision, and internal FX service Aug 15, 2026
@ItsThompson
ItsThompson force-pushed the mc/02-currency-rules-fx-service branch 7 times, most recently from 8ae5465 to ce43c39 Compare August 18, 2026 22:38
Base automatically changed from mc/01-currency-catalog-and-period-currency to main August 18, 2026 23:45
@ItsThompson
ItsThompson force-pushed the mc/02-currency-rules-fx-service branch 2 times, most recently from e89ede9 to d8e43fa Compare August 18, 2026 23:51
Comment thread frontend/apps/finance/src/features/new-expense/NewExpenseFeature.tsx Outdated
- add read-only GetPeriodContext gRPC contract for period lookup

- return reporting currency and lock state from Finance service

- cover success and not-found status mapping in gRPC tests
- configure Finance period-context client before public expense writes

- accept transactionCurrency and rollout currency aliases

- add missing-period and alias compatibility coverage
- add transactionCurrency to CreateProRataRequest and proto, relax currency binding

- resolve alias in service: transactionCurrency canonical, currency as legacy fallback

- send only transactionCurrency from internal expense client to avoid telemetry pollution

- add service and REST handler tests for transactionCurrency-only pro-rata
…month

- add handler test verifying codes.NotFound and year/month in message

- covers should-fix 4 from review: gRPC field detail for missing-period path
…ution

- reject both-different with CURRENCY_FIELD_CONFLICT, no schedule or expense written

- accept both-equal, canonicalize to transactionCurrency

- add ErrCurrencyConflict to finance model errors

- add service tests for both-equal and both-different cases
Commit 777d05f accidentally included FX service modifications from
parallel agent work. This reverts those files to their pre-777d05f
state. The FX changes are preserved in git history at 777d05f and
can be cherry-picked by the FX ticket agent.
- move fetchPeriodContext into usePeriodContext with a discriminated union
- replace loading/missing/error/active flags with one status to drop impossible states
- add usePeriodContext tests for active, missing, error, and refetch
- extract loading, missing/error, and form JSX into components/
- slim NewExpenseFeature to an orchestrator that routes on period status
- add feature test for the unexpected period load error
@ItsThompson
ItsThompson force-pushed the mc/02-currency-rules-fx-service branch from 6e462ac to c594e61 Compare August 19, 2026 06:30
@ItsThompson
ItsThompson merged commit 8ced688 into main Aug 19, 2026
6 checks passed
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