feat: add budget folder schema and migration

This commit is contained in:
Lucas Winther committed 2026-09-29 20:25:47 +02:00
1 parent 67c77eb247
commit c34aa846b9
19 files changed
+227 -6

No files matched your search

+54 -2
View File
@@ -114,6 +114,57 @@ describe("validation", () => {
});
});
describe("import rejects invalid folder graph", () => {
const folder = (id: string, parentId: string | null, order: number) => ({ id, name: id, parentId, order });
const parse = (data: Dataset) => parseDatasetJson(JSON.stringify(data));
test("valid nested tree parses", () => {
const data = populated();
data.folders = [folder("home", null, 0), folder("bills", "home", 0)];
data.posts[0]!.folderId = "bills";
expect(parse(data).posts[0]!.folderId).toBe("bills");
});
test("rejects duplicate folder IDs", () => {
const data = populated();
data.folders = [folder("same", null, 0), folder("same", null, 1)];
expect(() => parse(data)).toThrow(ImportValidationError);
});
test("rejects missing parent", () => {
const data = populated();
data.folders = [folder("child", "missing", 0)];
expect(() => parse(data)).toThrow(ImportValidationError);
});
test("rejects missing item folders", () => {
const data = populated();
data.posts[0]!.folderId = "missing";
expect(() => parse(data)).toThrow(ImportValidationError);
data.posts[0]!.folderId = null;
data.recurring.push({
id: "rent", name: "Rent", order: 3, folderId: "missing", archived: false,
amount: { amount: 100, currency: "DKK" }, startDate: "2026-01",
recurrence: { kind: "everyNMonths", n: 1 }, anchoring: "calendar",
splitMode: "percent", splits: [{ postId: data.posts[0]!.id, value: 100, absorbsRemainder: true }],
});
expect(() => parse(data)).toThrow(ImportValidationError);
});
test("rejects cycles", () => {
const data = populated();
data.folders = [folder("a", "b", 0), folder("b", "a", 0)];
expect(() => parse(data)).toThrow(ImportValidationError);
});
test("rejects duplicate sibling order across item kinds", () => {
const data = populated();
data.folders = [folder("bills", null, 0)];
data.posts[0]!.order = 0;
expect(() => parse(data)).toThrow(ImportValidationError);
});
});
test("describeDataset counts what an import would replace", () => {
expect(describeDataset(populated())).toEqual({
posts: 3,
@@ -286,7 +337,8 @@ describe("recurring cost validation", () => {
const valid = {
id: "r1",
name: "Rent",
order: 0,
order: 3,
folderId: null,
archived: false,
amount: { amount: 8000, currency: "DKK" },
startDate: "2026-01",
@@ -530,7 +582,7 @@ describe("Purchase.source validation", () => {
test("a source with a malformed occurrence date is refused", () => {
const data: any = JSON.parse(withSource({ recurringId: "r1", occurrenceDate: "nope" }));
data.recurring = [{
id: "r1", name: "Rent", order: 0, archived: false,
id: "r1", name: "Rent", order: 3, folderId: null, archived: false,
amount: { amount: 8000, currency: "DKK" },
startDate: "2026-01",
recurrence: { kind: "everyNMonths", n: 1 },