diff --git a/scripts/demo-data.ts b/scripts/demo-data.ts index 1792d4d..995557d 100755 --- a/scripts/demo-data.ts +++ b/scripts/demo-data.ts @@ -47,6 +47,7 @@ actions.setRuleOverride(d, "2026-09", social.id, { kind: "fixed", amount: { amou // every combination at once. const wowSub = actions.addRecurringCost(d, { name: "World of Warcraft", + folderId: null, archived: false, amount: { amount: 13, currency: "USD" }, startDate: "2026-06-05", @@ -57,6 +58,7 @@ const wowSub = actions.addRecurringCost(d, { }); actions.addRecurringCost(d, { name: "Rent", + folderId: null, archived: false, amount: { amount: 8200, currency: "DKK" }, startDate: "2026-06", @@ -67,6 +69,7 @@ actions.addRecurringCost(d, { }); actions.addRecurringCost(d, { name: "Veg box", + folderId: null, archived: false, amount: { amount: 175, currency: "DKK" }, startDate: "2026-06-06", @@ -79,6 +82,7 @@ actions.addRecurringCost(d, { // off-schedule payment moves the whole series — the phone-bill behaviour. actions.addRecurringCost(d, { name: "Car insurance", + folderId: null, archived: false, amount: { amount: 349, currency: "DKK" }, startDate: "2026-08-14", diff --git a/src/domain/allocation.test.ts b/src/domain/allocation.test.ts index f18fa24..8e16fc5 100644 --- a/src/domain/allocation.test.ts +++ b/src/domain/allocation.test.ts @@ -36,6 +36,7 @@ function post(overrides: Partial = {}): Post { id: "p1", name: "Food", order: 0, + folderId: null, archived: false, currency: "DKK", rules: [{ from: "2026-01", rule: fixed(400) }], @@ -48,6 +49,7 @@ function dataset(overrides: Partial = {}): Dataset { settings: { baseCurrency: "DKK", foldStartMonth: "2026-01", schemaVersion: 2, digits: DIGITS }, currencies: TEST_CURRENCIES, fxRates: rates, + folders: [], posts: [post()], months: [ { id: "2026-01", income: { amount: 20000, currency: "DKK" }, ruleOverrides: {} }, @@ -191,6 +193,7 @@ describe("the fold across a rule change", () => { settings: { baseCurrency: "DKK", foldStartMonth: "2026-05", schemaVersion: 2, digits: DIGITS }, currencies: TEST_CURRENCIES, fxRates: [], + folders: [], posts: [ post({ rules: [ diff --git a/src/domain/charges.test.ts b/src/domain/charges.test.ts index c7e91c0..b3eba82 100644 --- a/src/domain/charges.test.ts +++ b/src/domain/charges.test.ts @@ -214,6 +214,7 @@ describe("chargesForMonth", () => { settings: { baseCurrency: "DKK", foldStartMonth: "2026-01", schemaVersion: 1, digits: DIGITS }, currencies: TEST_CURRENCIES, fxRates: rates, + folders: [], posts: [], months: [], purchases: [ @@ -233,6 +234,7 @@ describe("chargesForMonth", () => { settings: { baseCurrency: "DKK", foldStartMonth: "2026-01", schemaVersion: 1, digits: DIGITS }, currencies: TEST_CURRENCIES, fxRates: rates, + folders: [], posts: [], months: [], purchases: [], @@ -251,6 +253,7 @@ describe("expectedForMonth", () => { { code: "USD", symbol: "$", name: "US dollar" }, ], fxRates: [{ currency: "USD", baseUnitsPerOne: 7, updatedAt: "2026-01-01", source: "manual" }], + folders: [], posts: [], months: [], purchases, @@ -262,6 +265,7 @@ describe("expectedForMonth", () => { id: "r1", name: "Rent", order: 0, + folderId: null, archived: false, amount: { amount: 8000, currency: "DKK" }, startDate: "2026-01", diff --git a/src/domain/currencyDigits.test.ts b/src/domain/currencyDigits.test.ts index bb58637..469488b 100644 --- a/src/domain/currencyDigits.test.ts +++ b/src/domain/currencyDigits.test.ts @@ -96,11 +96,13 @@ describe("the fold at zero decimals", () => { fxRates: [ { currency: "USD", baseUnitsPerOne: 151.234567, updatedAt: "2026-01-01", source: "manual" }, ], + folders: [], posts: [ { id: "food", name: "Food", order: 0, + folderId: null, archived: false, currency: "JPY", rules: [{ from: "2026-01", rule: { kind: "percentOfIncome", percent: 33.333 } }], @@ -113,6 +115,7 @@ describe("the fold at zero decimals", () => { id: "fun", name: "Fun", order: 1, + folderId: null, archived: false, currency: "JPY", rules: [], @@ -134,6 +137,7 @@ describe("the fold at zero decimals", () => { id: "gym", name: "Gym", order: 0, + folderId: null, archived: false, amount: { amount: 7, currency: "USD" }, startDate: "2026-01", diff --git a/src/domain/fold.test.ts b/src/domain/fold.test.ts index d3f11fb..2ea0298 100644 --- a/src/domain/fold.test.ts +++ b/src/domain/fold.test.ts @@ -18,6 +18,7 @@ function post(id: string, fixed: number, archived = false): Post { id, name: id, order: 0, + folderId: null, archived, currency: "DKK", rules: [{ from: "2026-01", rule: { kind: "fixed", amount: { amount: fixed, currency: "DKK" } } }], @@ -41,6 +42,7 @@ function dataset(posts: Post[], purchases: Purchase[]): Dataset { settings: { baseCurrency: "DKK", foldStartMonth: "2026-01", schemaVersion: 1, digits: DIGITS }, currencies: TEST_CURRENCIES, fxRates: [], + folders: [], posts, months: [ { id: "2026-01", income: { amount: 10000, currency: "DKK" }, ruleOverrides: {} }, @@ -131,6 +133,7 @@ test("percentage allocations follow the month's income", () => { id: "food", name: "Food", order: 0, + folderId: null, archived: false, currency: "DKK", rules: [{ from: "2026-01", rule: { kind: "percentOfIncome", percent: 10 } }], @@ -191,11 +194,13 @@ function datasetWithRent(): Dataset { settings: { baseCurrency: "DKK", foldStartMonth: "2026-01", schemaVersion: 7, digits: 2 }, currencies: [{ code: "DKK", symbol: "kr", name: "Danish krone" }], fxRates: [], + folders: [], posts: [ { id: "housing", name: "Housing", order: 0, + folderId: null, archived: false, currency: "DKK", rules: [{ from: "2026-01", rule: { kind: "fixed", amount: { amount: 10000, currency: "DKK" } } }], @@ -208,6 +213,7 @@ function datasetWithRent(): Dataset { id: "rent", name: "Rent", order: 0, + folderId: null, archived: false, amount: { amount: 8000, currency: "DKK" }, startDate: "2026-01", diff --git a/src/domain/occurrences.test.ts b/src/domain/occurrences.test.ts index 235ebd4..2660298 100644 --- a/src/domain/occurrences.test.ts +++ b/src/domain/occurrences.test.ts @@ -7,6 +7,7 @@ function cost(overrides: Partial = {}): RecurringCost { id: "r1", name: "Rent", order: 0, + folderId: null, archived: false, amount: { amount: 8000, currency: "DKK" }, startDate: "2026-01", @@ -248,6 +249,7 @@ describe("occurrencesByMonth", () => { settings: { baseCurrency: "DKK", foldStartMonth: "2026-01", schemaVersion: 7, digits: 2 }, currencies: [{ code: "DKK", symbol: "kr", name: "Danish krone" }], fxRates: [], + folders: [], posts: [], months: [], purchases, diff --git a/src/domain/seed.test.ts b/src/domain/seed.test.ts index 03bacfd..f692571 100644 --- a/src/domain/seed.test.ts +++ b/src/domain/seed.test.ts @@ -43,3 +43,9 @@ test("the seed dataset renders a valid month view", () => { test("newId returns distinct ids", () => { expect(newId()).not.toBe(newId()); }); + +test("fresh dataset starts Unfiled", () => { + const data = createSeedDataset("2026-09"); + expect(data.folders).toEqual([]); + expect(data.posts.every((post) => post.folderId === null)).toBe(true); +}); diff --git a/src/domain/seed.ts b/src/domain/seed.ts index 77b99f7..9546075 100644 --- a/src/domain/seed.ts +++ b/src/domain/seed.ts @@ -1,7 +1,7 @@ import { DEFAULT_DIGITS, SEED_CURRENCIES } from "./types.ts"; import type { CurrencyDef, Dataset, FxRate, MonthId, Post } from "./types.ts"; -export const SCHEMA_VERSION = 7; +export const SCHEMA_VERSION = 8; const SEED_POST_NAMES = ["Video Games", "Food", "Events and Social"] as const; @@ -24,6 +24,7 @@ export function createSeedDataset( id: newId(), name, order, + folderId: null, archived: false, currency: "DKK", // A new user has genuinely not budgeted anything yet. This is a different @@ -42,6 +43,7 @@ export function createSeedDataset( // out from under it the first time the user changes a rate or a currency. currencies: currencies.map((currency) => ({ ...currency })), fxRates: fxRates.map((rate) => ({ ...rate })), + folders: [], posts, months: [ { id: startMonth, income: { amount: 0, currency: "DKK" }, ruleOverrides: {} }, diff --git a/src/domain/types.ts b/src/domain/types.ts index 193c5e7..d52171e 100755 --- a/src/domain/types.ts +++ b/src/domain/types.ts @@ -54,6 +54,14 @@ export type MonthId = string; export type IsoDate = string; export type PostId = string; export type PurchaseId = string; +export type FolderId = string; + +export interface BudgetFolder { + id: FolderId; + name: string; + parentId: FolderId | null; + order: number; +} export type Rule = | { kind: "fixed"; amount: Money } @@ -69,6 +77,7 @@ export interface Post { id: PostId; name: string; order: number; + folderId: FolderId | null; archived: boolean; /** Display currency for this post's own views only. All totals use base. */ currency: Currency; @@ -138,6 +147,7 @@ export interface RecurringCost { id: RecurringCostId; name: string; order: number; + folderId: FolderId | null; /** * A UI flag ONLY. Projection is governed by `startDate` and `endedFrom`, * exactly as an archived post still folds — archiving must not retroactively @@ -221,6 +231,7 @@ export interface Dataset { */ currencies: CurrencyDef[]; fxRates: FxRate[]; + folders: BudgetFolder[]; posts: Post[]; months: Month[]; purchases: Purchase[]; diff --git a/src/domain/views.test.ts b/src/domain/views.test.ts index f5e855e..243c1b8 100644 --- a/src/domain/views.test.ts +++ b/src/domain/views.test.ts @@ -17,6 +17,7 @@ function post(id: string, order: number, percent: number, archived = false): Pos id, name: id, order, + folderId: null, archived, currency: "DKK", rules: [{ from: "2026-01", rule: { kind: "percentOfIncome", percent } }], @@ -39,6 +40,7 @@ const data: Dataset = { settings: { baseCurrency: "DKK", foldStartMonth: "2026-01", schemaVersion: 1, digits: DIGITS }, currencies: TEST_CURRENCIES, fxRates: [], + folders: [], posts: [post("food", 0, 20), post("games", 1, 10)], months: [ { id: "2026-01", income: { amount: 10000, currency: "DKK" }, ruleOverrides: {} }, @@ -137,6 +139,7 @@ describe("monthView", () => { id: "r1", name: "Storage unit", order: 0, + folderId: null, archived: false, amount: { amount: 400, currency: "DKK" }, startDate: "2026-01", diff --git a/src/export/json.test.ts b/src/export/json.test.ts index 7a75f19..47fe198 100644 --- a/src/export/json.test.ts +++ b/src/export/json.test.ts @@ -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 }, diff --git a/src/export/json.ts b/src/export/json.ts index 4829bdc..59ff5dd 100644 --- a/src/export/json.ts +++ b/src/export/json.ts @@ -1,4 +1,4 @@ -import type { Currency, CurrencyDef, Dataset, MonthId } from "../domain/types.ts"; +import type { Currency, CurrencyDef, Dataset, FolderId, MonthId } from "../domain/types.ts"; import { migrate } from "../store/migrations.ts"; import { monthOf } from "../domain/months.ts"; import { toDayOrdinal } from "../domain/days.ts"; @@ -132,6 +132,71 @@ function requireArray(data: Record, key: string): unknown[] { return value; } +function requireFolderTree(data: Dataset): void { + const folders = requireArray(data as unknown as Record, "folders"); + const parents = new Map(); + const siblingOrders = new Map>(); + const requireOrder = (order: unknown, label: string) => { + if (typeof order !== "number" || !Number.isFinite(order) || !Number.isInteger(order)) { + throw new ImportValidationError(`${label} has an invalid order`); + } + }; + const requireLocation = (folderId: unknown, order: unknown, label: string) => { + if (folderId !== null && (typeof folderId !== "string" || !parents.has(folderId))) { + throw new ImportValidationError(`${label} references unknown folder "${String(folderId)}"`); + } + requireOrder(order, label); + const key = folderId as FolderId | null; + const orders = siblingOrders.get(key) ?? new Set(); + if (orders.has(order as number)) { + throw new ImportValidationError(`${label} duplicates sibling order ${String(order)}`); + } + orders.add(order as number); + siblingOrders.set(key, orders); + }; + + for (const entry of folders) { + const folder = entry as Record | null; + if (!folder || typeof folder.id !== "string" || !folder.id.trim()) { + throw new ImportValidationError("Folder has an invalid id"); + } + if (parents.has(folder.id)) { + throw new ImportValidationError(`Duplicate folder id "${folder.id}"`); + } + if (typeof folder.name !== "string" || !folder.name.trim()) { + throw new ImportValidationError(`Folder "${folder.id}" has an empty name`); + } + if (folder.parentId !== null && typeof folder.parentId !== "string") { + throw new ImportValidationError(`Folder "${folder.id}" has an invalid parent`); + } + parents.set(folder.id, folder.parentId as FolderId | null); + } + + for (const [id, parentId] of parents) { + if (parentId !== null && !parents.has(parentId)) { + throw new ImportValidationError(`Folder "${id}" references unknown parent "${parentId}"`); + } + const visited = new Set(); + let cursor: FolderId | null = id; + while (cursor !== null) { + if (visited.has(cursor)) throw new ImportValidationError(`Folder cycle at "${cursor}"`); + visited.add(cursor); + cursor = parents.get(cursor) ?? null; + } + } + + for (const entry of folders) { + const folder = entry as Record; + requireLocation(folder.parentId, folder.order, `Folder "${folder.id}"`); + } + for (const post of data.posts) { + requireLocation(post?.folderId, post?.order, `Post "${post?.name}"`); + } + for (const cost of data.recurring) { + requireLocation(cost?.folderId, cost?.order, `Recurring cost "${cost?.name}"`); + } +} + /** * `n` is checked hard rather than clamped. The projection walk in * `domain/occurrences.ts` terminates only if every step strictly advances, and @@ -331,6 +396,7 @@ export function parseDatasetJson(text: string): Dataset { const recurring = requireArray(data, "recurring") as any[]; dataset.recurring = recurring; + requireFolderTree(dataset); // Optional chaining, not `.id` directly: a null/non-object entry must fall // through to the loop below and fail there with an ImportValidationError, // not throw a raw TypeError here while merely collecting ids. diff --git a/src/store/actions.test.ts b/src/store/actions.test.ts index aef132a..090959d 100644 --- a/src/store/actions.test.ts +++ b/src/store/actions.test.ts @@ -464,6 +464,7 @@ describe("recurring costs", () => { const rentInput = { name: "Rent", + folderId: null, archived: false, amount: { amount: 8000, currency: "DKK" }, startDate: "2026-01", @@ -764,6 +765,7 @@ describe("confirmOccurrence", () => { data.posts[0]!.id = "housing"; actions.addRecurringCost(data, { name: "Rent", + folderId: null, archived: false, amount: { amount: 8000, currency: "DKK" }, startDate: "2026-01", @@ -836,6 +838,7 @@ describe("editing a confirmation's date", () => { data.posts[0]!.id = "housing"; actions.addRecurringCost(data, { name: "Car insurance", + folderId: null, archived: false, amount: { amount: 900, currency: "DKK" }, startDate: "2026-08-14", diff --git a/src/store/actions.ts b/src/store/actions.ts index 59093ac..d830c85 100755 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -134,6 +134,7 @@ export function addPost(draft: Dataset, name: string, currency: Currency): Post id: newId(), name, order: draft.posts.length, + folderId: null, archived: false, currency, rules: [], diff --git a/src/store/migrations.test.ts b/src/store/migrations.test.ts index d1f27ce..69b5f3a 100644 --- a/src/store/migrations.test.ts +++ b/src/store/migrations.test.ts @@ -193,7 +193,7 @@ describe("v2 -> v3: currencies become data", () => { test("nothing else about the dataset is touched", () => { const before = v2(); const migrated = migrate(structuredClone(before)); - expect(migrated.posts).toEqual(before.posts); + expect(migrated.posts.map(({ folderId, ...post }) => post)).toEqual(before.posts); expect(migrated.months).toEqual(before.months); // Sterling's rate arrives later, in 4 -> 5; nothing this step does touches // the rate table. @@ -466,7 +466,7 @@ describe("6 -> 7: recurring costs join the dataset", () => { const v6: any = { ...createSeedDataset("2026-09"), recurring: existing }; v6.settings = { ...v6.settings, schemaVersion: 6 }; - expect(migrate(v6).recurring).toEqual(existing); + expect(migrate(v6).recurring).toEqual([{ ...existing[0], order: 3, folderId: null }]); }); test("NO figure changes: every post in every month folds identically", () => { @@ -485,3 +485,33 @@ describe("6 -> 7: recurring costs join the dataset", () => { } }); }); + +test("v7 migration preserves records and assigns mixed order", () => { + const data: any = createSeedDataset("2026-09"); + data.settings.schemaVersion = 7; + delete data.folders; + data.posts = [ + { ...data.posts[0], id: "later", order: 9, rules: [{ from: "2026-09", rule: { kind: "fixed", amount: { amount: 20, currency: "DKK" } } }] }, + { ...data.posts[1], id: "first", order: 2 }, + ].map(({ folderId, ...post }: any) => post); + data.recurring = [ + { id: "bill-later", name: "Later", order: 8, splits: [{ postId: "first", value: 100, absorbsRemainder: true }] }, + { id: "bill-first", name: "First", order: 1, splits: [{ postId: "later", value: 100, absorbsRemainder: true }] }, + ]; + data.purchases = [{ id: "purchase", splits: [{ postId: "later", value: 100, absorbsRemainder: true }] }]; + const before = structuredClone(data); + + const migrated = migrate(data); + + expect(migrated.folders).toEqual([]); + expect(migrated.posts.map((post) => [post.id, post.order, post.folderId])).toEqual([ + ["later", 1, null], ["first", 0, null], + ]); + expect(migrated.recurring.map((bill) => [bill.id, bill.order, bill.folderId])).toEqual([ + ["bill-later", 3, null], ["bill-first", 2, null], + ]); + expect(new Set([...migrated.posts, ...migrated.recurring].map((item) => item.order)).size).toBe(4); + expect(migrated.posts[0]!.rules).toEqual(before.posts[0].rules); + expect(migrated.recurring.map((bill) => bill.splits)).toEqual(before.recurring.map((bill: any) => bill.splits)); + expect(migrated.purchases).toEqual(before.purchases); +}); diff --git a/src/store/migrations.ts b/src/store/migrations.ts index de9ba06..f58f54a 100644 --- a/src/store/migrations.ts +++ b/src/store/migrations.ts @@ -193,6 +193,27 @@ const MIGRATIONS: Array<(data: any) => any> = [ settings: { ...data.settings, schemaVersion: 7 }, recurring: Array.isArray(data.recurring) ? data.recurring : [], }), + + // 7 -> 8: posts and recurring bills gain independent locations in a shared + // folder tree. Preserve each type's former display order, with posts first. + // The migration writes no financial data and reads no live default. + (data: any) => { + const posts = Array.isArray(data.posts) ? data.posts : []; + const recurring = Array.isArray(data.recurring) ? data.recurring : []; + const postOrders = new Map( + [...posts].sort((a: any, b: any) => a.order - b.order).map((post: any, index) => [post, index]), + ); + const billOrders = new Map( + [...recurring].sort((a: any, b: any) => a.order - b.order).map((bill: any, index) => [bill, posts.length + index]), + ); + return { + ...data, + settings: { ...data.settings, schemaVersion: 8 }, + folders: [], + posts: posts.map((post: any) => ({ ...post, order: postOrders.get(post), folderId: null })), + recurring: recurring.map((bill: any) => ({ ...bill, order: billOrders.get(bill), folderId: null })), + }; + }, ]; export function migrate(raw: unknown): Dataset { diff --git a/src/store/snapshot.test.ts b/src/store/snapshot.test.ts index 34a1fab..02ed8e7 100644 --- a/src/store/snapshot.test.ts +++ b/src/store/snapshot.test.ts @@ -181,6 +181,7 @@ describe("reset", () => { id: "extra", name: "Extra", order: 9, + folderId: null, archived: false, currency: "DKK", rules: [], diff --git a/src/ui/components/ExpectedBand.test.ts b/src/ui/components/ExpectedBand.test.ts index 7f9ec59..7de253b 100644 --- a/src/ui/components/ExpectedBand.test.ts +++ b/src/ui/components/ExpectedBand.test.ts @@ -65,6 +65,7 @@ function cost(overrides: Partial = {}): RecurringCost { id: "r1", name: "Rent", order: 0, + folderId: null, archived: false, amount: { amount: 8000, currency: "DKK" }, startDate: "2026-01", diff --git a/src/ui/routes/settings/RecurringSection.tsx b/src/ui/routes/settings/RecurringSection.tsx index 13dbe6a..b4f8072 100644 --- a/src/ui/routes/settings/RecurringSection.tsx +++ b/src/ui/routes/settings/RecurringSection.tsx @@ -373,6 +373,7 @@ export function RecurringSection() { void mutate((draft) => addRecurringCost(draft, { name, + folderId: null, archived: false, amount: { amount: 0, currency: base }, startDate: currentMonth,