Files
budget2.0/src/store/index.ts
T
Lucas WintherandClaude Opus 5 b4e1933761 feat: bake FX rates in at build time so a fresh install can convert
TODO section 2, designed and approved, now built. A brand-new dataset arrives
with DKK/USD/EUR rates instead of erroring on the first EUR purchase.

build.ts fetches at build time and embeds the result via `define`. A network
failure does NOT fail the build — it bakes committed constants instead, and an
incomplete response is treated as a failure too, since one unconvertible
currency is the problem this solves. Fetched rates carry source "api" and the
BUILD date, which is why parseFxResponse now takes an explicit updatedAt; the
constants carry source "manual" and their own date, so Settings never claims a
hardcoded number came from a rate service. SKIP_FX_FETCH=1 and FX_API_URL are
there for offline, CI and reproducible builds.

They seed a NEW dataset and nothing else. They are NOT a fallback for a missing
rate: clearing a rate removes its row so MissingRateError surfaces, rather than
silently converting money at a number the user never chose. A test asserts a
cleared rate still throws, and loading an existing dataset never touches its
rates.

Domain purity forced the shape. createSeedDataset takes the rates as an
argument (and copies the rows, so shared constants cannot be edited out from
under the caller); the env read lives in the new src/store/bakedRates.ts behind
the try/catch accessor, because an unset BUN_PUBLIC_* leaves a bare process.env
in the browser bundle; and createSnapshotStore takes seed rates as a parameter
so it stays free of the ambient environment, with src/store/index.ts wiring it.

Verified all three build paths: live fetch bakes api-sourced rates with today's
date, an unreachable endpoint and SKIP_FX_FETCH both bake manual constants and
still build, and the payload is present in the bundle in each case. Verified at
runtime that a fresh install converts EUR 100 -> 747.50 DKK and USD 100 ->
644.95 DKK with the variable both set and unset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 14:37:39 +02:00

19 lines
721 B
TypeScript

import { monthOf } from "../domain/months.ts";
import { readDataset, writeDataset } from "./db.ts";
import { createSnapshotStore } from "./snapshot.ts";
import { BAKED_FX_RATES } from "./bakedRates.ts";
/** The current month, read at module load. The UI owns the clock; the domain does not. */
export const currentMonth = monthOf(new Date().toISOString().slice(0, 10));
export const store = createSnapshotStore(
{ read: readDataset, write: writeDataset },
currentMonth,
// First run only, so a fresh install can convert a EUR purchase on day one.
// Never a backstop for a rate the user has cleared.
BAKED_FX_RATES,
);
export * from "./snapshot.ts";
export { UnsupportedSchemaError } from "./migrations.ts";