Files
budget2.0/src/domain/types.ts
T
Lucas WintherandClaude Opus 5 31f030fdea feat(ui): add settings for currency, rates, posts, and data
Rate fetching is optional and fails soft: any network or parse error
leaves cached rates untouched and shows a message, never throwing into
render. A cleared or non-positive FX rate input now calls the new
removeFxRate action instead of storing 0, since toBase() silently
converts a stored 0 to a 0-value conversion while an absent row
correctly throws MissingRateError. Posts are archived, never deleted.
Import downloads a backup of the current data before replacing it and
requires confirmation showing before/after counts.

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

97 lines
2.2 KiB
TypeScript

export const CURRENCIES = ["DKK", "USD", "EUR"] as const;
export type Currency = (typeof CURRENCIES)[number];
export const CURRENCY_DIGITS: Record<Currency, number> = {
DKK: 2,
USD: 2,
EUR: 2,
};
/** A float amount paired with its currency. See CLAUDE.md for rounding rules. */
export interface Money {
amount: number;
currency: Currency;
}
/** "YYYY-MM". Never a Date. */
export type MonthId = string;
/** "YYYY-MM-DD". Never a Date. */
export type IsoDate = string;
export type PostId = string;
export type PurchaseId = string;
export type Rule =
| { kind: "fixed"; amount: Money }
| { kind: "percentOfIncome"; percent: number };
export interface Post {
id: PostId;
name: string;
order: number;
archived: boolean;
/** Display currency for this post's own views only. All totals use base. */
currency: Currency;
standingRule: Rule;
}
export interface Month {
id: MonthId;
/** Always in Settings.baseCurrency. */
income: Money;
ruleOverrides: Record<PostId, Rule>;
}
export interface Split {
postId: PostId;
/** A percentage, or an amount in the purchase's currency, per Purchase.splitMode. */
value: number;
absorbsRemainder: boolean;
}
export interface ScheduleSlice {
month: MonthId;
amount: Money;
}
export interface Schedule {
slices: ScheduleSlice[];
/** Slices in this month AND every month after it are ignored by the fold. */
cancelledFromMonth?: MonthId;
}
export interface Purchase {
id: PurchaseId;
date: IsoDate;
description: string;
/** In the purchase's own currency. */
total: Money;
splitMode: "percent" | "fixed";
splits: Split[];
/** null means the whole total is charged in the month of `date`. */
schedule: Schedule | null;
}
export interface FxRate {
currency: Currency;
/** How many base units one unit of `currency` buys. Base has no row. */
baseUnitsPerOne: number;
updatedAt: string;
source: "manual" | "api";
}
export interface Settings {
baseCurrency: Currency;
foldStartMonth: MonthId;
schemaVersion: number;
/** Optional: no migration needed, existing datasets leave it undefined. */
fxApiUrl?: string;
}
export interface Dataset {
settings: Settings;
fxRates: FxRate[];
posts: Post[];
months: Month[];
purchases: Purchase[];
}