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>
97 lines
2.2 KiB
TypeScript
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[];
|
|
}
|