Skip to content

fix(import): use shared Peanut FX rates safely - #4

Merged
0xkkonrad merged 16 commits into
mainfrom
fix/import-fx-and-format-audit
Aug 7, 2026
Merged

fix(import): use shared Peanut FX rates safely#4
0xkkonrad merged 16 commits into
mainfrom
fix/import-fx-and-format-audit

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fetch Peanut's validated FX table for the room currency from GET /fx/rates?base=<room> on api.peanut.me (endpoint overridable via SPLIT_FX_ENDPOINT).
  • Use the returned direct row for each imported currency; never synthesize a live cross-rate from unrelated rows locally.
  • Cache by base in Postgres with single-flight refresh, bounded backoff/last-known behavior, and the existing 12-currency static table only as an outage/development fallback.
  • Fix the KUNC regression: the fixture's 82 PLN toll imports into the EUR room as €18.98, producing balances of ±€9.49.
  • Harden the complete import path: bounded byte-aware requests/CSV parsing, signed money limits, exact 500-row caps, source-stable idempotency, and explicit ambiguous-number handling.

FX transport and policy

Production egress is default-deny, so the rate fetch rides the pinned split-egress squid proxy via egressFetch, exactly like the model scan — credential-free Accept-only GET, redirects refused, byte-bounded streamed read, aborted on timeout (5dce9f7). Infra prerequisites before this does anything in prod: api.peanut.me on the squid CONNECT-443 allowlist and SPLIT_FX_PROXY_URL set. Without them every refresh fails quietly and the table stays on the twelve static rates.

The merged API contract (API #1277) preserves the atomic selection policy — both provider legs when both are covered, otherwise both reference legs, never a hybrid — with one post-review tightening: live Manteca rails (ARS, BRL) now fail closed instead of substituting a reference leg, while reference-only currencies (COP, BOB, …) keep their fallback so Split's bulk table stays complete. Split requires its 12-currency core set in every accepted table and degrades to cached (≤7 days) then static rows when the feed 503s or comes back incomplete, so the tightened contract needs no client change — there is an API-side regression test pinning the core-set guarantee.

Import compatibility boundary

  • Splitwise canonical group CSV: supported and covered.
  • Split Pro friend CSV and account JSON: supported and covered. Friend CSV loses group attribution; percentage/share rows are deliberately frozen as exact final amounts, including equal-looking 50/50 rows. Account JSON is balances-only.
  • Settle Up: CSV export exists, but Split does not claim or implement an adapter without a real redacted canonical fixture/schema.

Migration / branch integration

  • Includes one additive Prisma migration for optional source-stable import fingerprints and room-scoped uniqueness. Rolling clients remain compatible.
  • Merged with current main (post QA-sesh triage): resolved conflicts in api.ts (kept main's invalid-member-token notification plus this branch's error details passthrough), money.test.ts (import union), and expenses.test.ts (kept the room-base RateTable shape, added main's former-member helper).

Rollout

  1. Deploy the merged API (#1277 is in dev) to production and smoke the PLN→EUR pair plus an EUR-base bulk table.
  2. Infra: add api.peanut.me to the squid CONNECT-443 allowlist and set SPLIT_FX_PROXY_URL (ask Hugo — server-side, not shippable from this repo).
  3. Apply 20260805120000_stable_import_source_fingerprint before or with the Split deploy.
  4. Deploy Split, then smoke the KUNC import (82 PLN row → €18.98).
  5. Deploy the UI consumer (UI #2607, in dev) only after the API is live — it drops the Frankfurter CSP entry, so backend must go first.

Related PRs: API #1277 (merged to dev), UI #2607 (merged to dev), content #67.

QA

  • Full suite on the current merge (main@0a390a7 in ancestry): web Vitest 143 files / 2,138 tests, API Jest 4 suites / 80 tests, both typechecks, formatting clean.
  • Focused FX/import/KUNC regression: 68 tests; FX DB/cache: 44; pure import/FX: 45.
  • Multibyte and JSON-escaped maximum-body regressions cover both new-room and existing-room imports.
  • git diff --check clean, no conflict markers.

Screenshots: N/A (behavioral import/backend change; rendered layout is unchanged).

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@0xkkonrad, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: b8099d6c-2df4-4bbd-8add-187a4ab124a0

📥 Commits

Reviewing files that changed from the base of the PR and between 44dd26b and 9ca6d30.

📒 Files selected for processing (1)
  • README.md

Walkthrough

This PR changes FX data sources and base-specific rate tables, updates money and import parsing, adds immutable import source fingerprints, extends import error payloads, revises import UI checks, and aligns tests and copy with 162 catalog currencies and 156 automatic-conversion currencies.

Changes

FX and import flow

Layer / File(s) Summary
Currency catalog and product contracts
apps/web/docs/SPEC.md, apps/web/scripts/gen-currency-catalog.mjs, apps/web/src/lib/currency-*, apps/web/src/content/..., apps/web/src/components/marketing/*, apps/web/src/tools/*
The catalog and product text now describe 162 recognized currencies and 156 automatic-conversion currencies. Rate flags and unsupported-code expectations use the updated snapshot.
Base-specific FX tables and expense wiring
apps/web/src/server/fx.ts, apps/web/src/server/fx*.test.ts, apps/web/src/server/expenses*.ts, apps/web/src/app/api/rooms/[slug]/expenses*.ts
FX tables now load by destination base. Cache state, persistence, validation, conversion helpers, and expense paths use destination-specific tables.
Money and import parser updates
apps/web/src/lib/money*.ts, apps/web/src/lib/splitwise-csv*.ts, apps/web/src/lib/splitpro-import*.ts, apps/web/src/lib/manual-fx-rate.ts
Amount parsing now enforces bounded minor-unit limits. Import parsers add deterministic date fallback, malformed CSV errors, Split Pro warnings, stable source keys, and revised history capping.
Import identity and API contracts
apps/web/prisma/*, apps/web/src/lib/import-source-fingerprint.*, apps/web/src/lib/api*.ts, apps/web/src/server/{http,validation,splitwiseImport}.ts
Import requests can carry immutable source fingerprints. API errors can include details. Server imports persist fingerprints and replay by immutable or legacy identity.
Import UI checks and mapping
apps/web/src/components/import/*, apps/web/src/lib/use-rate.ts, apps/web/e2e/import.spec.ts
The UI fingerprints parsed imports, probes rate availability, tracks conversion failures, passes importer identity into mapping, and blocks submission while currency checks fail.

Estimated code review effort: 5 (Critical) | ~110 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login. If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands.

0xkkonrad and others added 15 commits August 5, 2026 09:55
…mat-audit

# Conflicts:
#	apps/web/src/lib/splash.test.ts
Production egress is default-deny — every runtime fetch rides the pinned squid
proxy — and this used the bare global fetch. In prod every refresh would have
failed, been swallowed by the existing catch, and left the table on the twelve
static rates. The feature would have looked shipped while doing nothing.

Uses the same egressFetch path as the model scan. egressFetch now takes a
plain RequestInit and returns a full Response so a streamed, byte-bounded read
works through it; Response satisfies EgressResponse, so model.ts is unchanged.

SPLIT_FX_ENDPOINT makes the API host overridable so staging can be pointed at
a staging API instead of prod.

STILL BLOCKED ON INFRA: api.peanut.me has to go on the squid CONNECT-443
allowlist and SPLIT_FX_PROXY_URL has to be set, or this stays on static rates.
…mat-audit

# Conflicts:
#	apps/web/src/lib/api.ts
#	apps/web/src/lib/money.test.ts
#	apps/web/src/server/expenses.test.ts
The FX refresh rides the same squid pinhole as push and scan, but its
failure mode is quieter — no 502 surfaces anywhere; the rate table just
stays on the static twelve. Worth naming next to the consumers whose
failures are visible.
@0xkkonrad
0xkkonrad marked this pull request as ready for review August 7, 2026 19:11
@0xkkonrad
0xkkonrad merged commit d64fbd8 into main Aug 7, 2026
3 checks passed
@0xkkonrad
0xkkonrad deleted the fix/import-fx-and-format-audit branch August 7, 2026 19:11
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.

2 participants