Skip to content

Commit b568fd0

Browse files
authored
Fix/missing tables on export (#295)
* Update version to 2.9.1 and add tests for SaveButton component * Update @luca-financial/luca-schema to version 3.3.2 in package.json and pnpm-lock.yaml * Update changelog for version 2.9.1: include schema update and fix for Save Accounts export
1 parent 3daac85 commit b568fd0

5 files changed

Lines changed: 246 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.9.1] - 2026-04-05
11+
12+
### Changed
13+
14+
- Updated `@luca-financial/luca-schema` from `^3.3.1` to `^3.3.2`.
15+
- Bumped application version to `2.9.1`. (#295)
16+
17+
### Fixed
18+
19+
- Fixed the `Save Accounts` export so recurring transaction links and transaction links are included in the downloaded data payload, preserving linked-record relationships on round-trip import/export.
20+
- Added regression coverage for the `Save Accounts` export payload so schema-backed collections do not drift out of the full export again.
21+
1022
## [2.9.0] - 2026-04-05
1123

1224
### Added

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "luca-ledger",
3-
"version": "2.9.0",
3+
"version": "2.9.1",
44
"homepage": "https://lucaledger.app/",
55
"type": "module",
66
"license": "MIT",
@@ -19,7 +19,7 @@
1919
"dependencies": {
2020
"@emotion/react": "^11.14.0",
2121
"@emotion/styled": "^11.14.1",
22-
"@luca-financial/luca-schema": "^3.3.1",
22+
"@luca-financial/luca-schema": "^3.3.2",
2323
"@mui/icons-material": "^7.3.9",
2424
"@mui/material": "^7.3.9",
2525
"@mui/system": "^7.3.9",

pnpm-lock.yaml

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { act } from 'react';
2+
import { createRoot } from 'react-dom/client';
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { Provider } from 'react-redux';
5+
import { configureStore } from '@reduxjs/toolkit';
6+
import { SCHEMA_VERSION } from '@luca-financial/luca-schema';
7+
import rootReducer from '@/store/rootReducer';
8+
import SaveButton from '@/views/Accounts/SaveButton';
9+
10+
describe('SaveButton', () => {
11+
let container = null;
12+
let root = null;
13+
let originalCreateObjectURL;
14+
let originalRevokeObjectURL;
15+
let originalStringify;
16+
let createElementSpy;
17+
18+
beforeEach(() => {
19+
originalCreateObjectURL = global.URL.createObjectURL;
20+
originalRevokeObjectURL = global.URL.revokeObjectURL;
21+
originalStringify = global.JSON.stringify;
22+
23+
global.URL.createObjectURL = vi.fn(() => 'mock-url');
24+
global.URL.revokeObjectURL = vi.fn();
25+
});
26+
27+
afterEach(async () => {
28+
if (createElementSpy) {
29+
createElementSpy.mockRestore();
30+
createElementSpy = null;
31+
}
32+
33+
global.URL.createObjectURL = originalCreateObjectURL;
34+
global.URL.revokeObjectURL = originalRevokeObjectURL;
35+
global.JSON.stringify = originalStringify;
36+
37+
if (root) {
38+
await act(async () => {
39+
root.unmount();
40+
});
41+
root = null;
42+
}
43+
44+
if (container) {
45+
container.remove();
46+
container = null;
47+
}
48+
});
49+
50+
it('includes link collections in the Save Accounts export payload', async () => {
51+
const store = configureStore({
52+
reducer: rootReducer,
53+
preloadedState: {
54+
accounts: {
55+
data: [
56+
{
57+
id: 'acc1',
58+
name: 'Test Account',
59+
type: 'checking',
60+
createdAt: '2026-01-01T00:00:00.000Z',
61+
updatedAt: null,
62+
},
63+
],
64+
loading: false,
65+
error: null,
66+
loadingAccountIds: [],
67+
},
68+
transactions: [
69+
{
70+
id: 'txn1',
71+
accountId: 'acc1',
72+
amount: 1000,
73+
date: '2026-01-15',
74+
createdAt: '2026-01-01T00:00:00.000Z',
75+
updatedAt: null,
76+
},
77+
],
78+
categories: [
79+
{
80+
id: 'cat1',
81+
name: 'Test Category',
82+
slug: 'test-category',
83+
createdAt: '2026-01-01T00:00:00.000Z',
84+
updatedAt: null,
85+
},
86+
],
87+
statements: [
88+
{
89+
id: 'stmt1',
90+
accountId: 'acc1',
91+
createdAt: '2026-01-01T00:00:00.000Z',
92+
updatedAt: null,
93+
},
94+
],
95+
recurringTransactions: [
96+
{
97+
id: 'rt1',
98+
accountId: 'acc1',
99+
amount: 500,
100+
description: 'Recurring',
101+
categoryId: null,
102+
frequency: 'MONTH',
103+
interval: 1,
104+
startOn: '2026-01-01',
105+
endOn: null,
106+
recurringTransactionState: 'ACTIVE',
107+
createdAt: '2026-01-01T00:00:00.000Z',
108+
updatedAt: null,
109+
},
110+
],
111+
recurringTransactionEvents: [
112+
{
113+
id: 'rte1',
114+
recurringTransactionId: 'rt1',
115+
expectedDate: '2026-12-01',
116+
eventState: 'MODIFIED',
117+
transactionId: 'txn1',
118+
createdAt: '2026-01-01T00:00:00.000Z',
119+
updatedAt: null,
120+
},
121+
],
122+
recurringTransactionLinks: [
123+
{
124+
id: 'rtl1',
125+
sourceRecurringTransactionId: 'rt1',
126+
destinationRecurringTransactionId: 'rt2',
127+
isSameSign: false,
128+
createdAt: '2026-01-01T00:00:00.000Z',
129+
updatedAt: null,
130+
},
131+
],
132+
transactionSplits: [
133+
{
134+
id: 'split1',
135+
transactionId: 'txn1',
136+
amount: 500,
137+
createdAt: '2026-01-01T00:00:00.000Z',
138+
updatedAt: null,
139+
},
140+
],
141+
transactionLinks: [
142+
{
143+
id: 'tl1',
144+
sourceTransactionId: 'txn1',
145+
destinationTransactionId: 'txn2',
146+
isSameSign: false,
147+
createdAt: '2026-01-01T00:00:00.000Z',
148+
updatedAt: null,
149+
},
150+
],
151+
settings: {},
152+
encryption: { status: 'uninitialized' },
153+
},
154+
});
155+
156+
let capturedData = null;
157+
const anchor = { click: vi.fn() };
158+
const originalCreateElement = document.createElement;
159+
160+
global.JSON.stringify = vi.fn((data, ...args) => {
161+
if (data?.schemaVersion) {
162+
capturedData = data;
163+
}
164+
return originalStringify(data, ...args);
165+
});
166+
167+
container = document.createElement('div');
168+
document.body.appendChild(container);
169+
root = createRoot(container);
170+
171+
await act(async () => {
172+
root.render(
173+
<Provider store={store}>
174+
<SaveButton />
175+
</Provider>,
176+
);
177+
});
178+
179+
createElementSpy = vi
180+
.spyOn(document, 'createElement')
181+
.mockImplementation((tagName, options) => {
182+
if (tagName === 'a') {
183+
return anchor;
184+
}
185+
return originalCreateElement.call(document, tagName, options);
186+
});
187+
188+
const button = container.querySelector('button');
189+
expect(button).not.toBeNull();
190+
191+
await act(async () => {
192+
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
193+
});
194+
195+
expect(capturedData).toBeDefined();
196+
expect(capturedData.schemaVersion).toBe(SCHEMA_VERSION);
197+
expect(capturedData.recurringTransactionLinks).toEqual([
198+
expect.objectContaining({
199+
id: 'rtl1',
200+
sourceRecurringTransactionId: 'rt1',
201+
}),
202+
]);
203+
expect(capturedData.transactionLinks).toEqual([
204+
expect.objectContaining({
205+
id: 'tl1',
206+
sourceTransactionId: 'txn1',
207+
}),
208+
]);
209+
expect(anchor.click).toHaveBeenCalledTimes(1);
210+
});
211+
});

src/views/Accounts/SaveButton.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import { selectors as categorySelectors } from '@/store/categories';
99
import { selectors as statementSelectors } from '@/store/statements';
1010
import { selectors as recurringTransactionSelectors } from '@/store/recurringTransactions';
1111
import { selectors as recurringTransactionEventSelectors } from '@/store/recurringTransactionEvents';
12+
import { selectors as recurringTransactionLinkSelectors } from '@/store/recurringTransactionLinks';
1213
import { selectors as transactionSplitSelectors } from '@/store/transactionSplits';
14+
import { selectors as transactionLinkSelectors } from '@/store/transactionLinks';
1315

1416
export default function SaveButton() {
1517
const accounts = useSelector(accountSelectors.selectAccounts);
@@ -22,9 +24,15 @@ export default function SaveButton() {
2224
const recurringTransactionEvents = useSelector(
2325
recurringTransactionEventSelectors.selectRecurringTransactionEvents,
2426
);
27+
const recurringTransactionLinks = useSelector(
28+
recurringTransactionLinkSelectors.selectRecurringTransactionLinks,
29+
);
2530
const transactionSplits = useSelector(
2631
transactionSplitSelectors.selectTransactionSplits,
2732
);
33+
const transactionLinks = useSelector(
34+
transactionLinkSelectors.selectTransactionLinks,
35+
);
2836
const loading = useSelector(accountSelectors.selectAccountsLoading);
2937

3038
const handleSave = () => {
@@ -36,7 +44,9 @@ export default function SaveButton() {
3644
statements,
3745
recurringTransactions,
3846
recurringTransactionEvents,
47+
recurringTransactionLinks,
3948
transactionSplits,
49+
transactionLinks,
4050
};
4151

4252
const saveString = JSON.stringify(data, null, 2);

0 commit comments

Comments
 (0)