Versie 0.3.3
This commit is contained in:
1 parent
1cf6eeed29
commit
6e741a51fe
57 files changed
+1362
-2135
No files matched your search
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:38c4b07d1566cda74ef6cac4d031f49fd52004b6938fdee8f13e3e7678fc12c7
|
||||
// Generated, do not edit. sha256:13b6b32cbbf6b8c4a6a6069af37a7be33560a8abfc2af81ae2b4009e9692d7c6
|
||||
import { describe, it, beforeEach, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createDbHarness } from '../helpers/db-node.mjs';
|
||||
@@ -22,6 +22,7 @@ mock.module('../../server/license.mjs', {
|
||||
|
||||
const { createAsset, updateAsset, deleteAsset, listAssets } = await import('../../server/data/assets.js');
|
||||
const { createAccount, listAccounts } = await import('../../server/data/accounts.js');
|
||||
const { mergeAccounts } = await import('../../server/data/accounts-merge.js');
|
||||
const { createValuation, listValuations } = await import('../../server/data/valuations.js');
|
||||
const { saveConnection, listConnections } = await import('../../server/data/connections.js');
|
||||
const { setPriceSourceRank } = await import('../../server/data/price-source.js');
|
||||
@@ -148,23 +149,10 @@ describe('de gratis stand op balansen', () => {
|
||||
rateAt: `${dag}T00:00:00Z`, source: 'manual',
|
||||
});
|
||||
|
||||
it('laat ÉÉN balans toe en weigert de tweede', async () => {
|
||||
it('kent geen grens: ook de gratis stand legt balans na balans vast', async () => {
|
||||
const asset = await createAsset(EEN);
|
||||
await createValuation(balans(asset, '2026-01-01'));
|
||||
await assert.rejects(() => createValuation(balans(asset, '2026-02-01')), (fout) => {
|
||||
assert.equal(fout.key, 'valuations.maxFree');
|
||||
assert.equal(fout.params.max, 1);
|
||||
return true;
|
||||
});
|
||||
assert.equal(listValuations().length, 1);
|
||||
});
|
||||
|
||||
it('laat de tweede WEL toe in de betaalde stand', async () => {
|
||||
const asset = await createAsset(EEN);
|
||||
await createValuation(balans(asset, '2026-01-01'));
|
||||
stand = { level: 'full', enforced: true, why: 'licensed' };
|
||||
await createValuation(balans(asset, '2026-02-01'));
|
||||
assert.equal(listValuations().length, 2);
|
||||
for (const dag of ['2026-01-01', '2026-02-01', '2026-03-01']) await createValuation(balans(asset, dag));
|
||||
assert.equal(listValuations().length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,4 +200,15 @@ describe('de features van de gratis stand', () => {
|
||||
await assert.rejects(() => setPriceSourceRank(asset, 'spot', 1),
|
||||
(f) => f.key === 'integrations.matrixPaid');
|
||||
});
|
||||
|
||||
it('weigert accounts samenvoegen en laat beide accounts staan; betaald gaat het door', async () => {
|
||||
const wallet = (name) => ({ name, kind: 'wallet', isOwnCustody: true, scanEnabled: false, taxCategory: null });
|
||||
const a = await createAccount(wallet('A'));
|
||||
const b = await createAccount(wallet('B'));
|
||||
await assert.rejects(() => mergeAccounts(a, b), (f) => f.key === 'accounts.mergePaid');
|
||||
assert.equal(listAccounts().length, 2);
|
||||
stand = { level: 'full', enforced: true, why: 'licensed' };
|
||||
await mergeAccounts(a, b);
|
||||
assert.deepEqual(listAccounts().map(r => r.id), [a]);
|
||||
});
|
||||
});
|
||||
@@ -1,302 +0,0 @@
|
||||
// Generated, do not edit. sha256:ba6482c6f8221f445c935cfd65e76c09e943da662ff5dab04e50d8a76e004316
|
||||
import { describe, it, beforeEach, mock } from 'node:test';
|
||||
import { toSearchName } from '../../shared/search-name.js';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { createDbHarness } from '../helpers/db-node.mjs';
|
||||
import { ACCOUNT_SOURCE_KINDS } from '../../shared/config.js';
|
||||
|
||||
const harness = createDbHarness();
|
||||
mock.module('../../server/db.js', { namedExports: harness.exports });
|
||||
|
||||
const { mergeAccounts, mergeCheck } = await import('../../server/data/accounts-merge.js');
|
||||
const { eventsNetByAccount } = await import('../../server/data/events.js');
|
||||
|
||||
let btc;
|
||||
beforeEach(() => {
|
||||
harness.recreate();
|
||||
harness.exports.run(
|
||||
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES ('BTC','Bitcoin','bitcoin','crypto','BTC','sat',8,'unit')`);
|
||||
btc = harness.exports.lastId();
|
||||
});
|
||||
|
||||
function account(naam, kind = 'wallet') {
|
||||
const eigen = ACCOUNT_SOURCE_KINDS[kind].length > 0 ? 1 : 0;
|
||||
harness.exports.run(
|
||||
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES (?,?,?,?,?,1,NULL)`, [naam, toSearchName(naam), kind, eigen, 0]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
function event({ type = 'buy', from = null, to = null, amount = 1000, at = '2024-01-01T00:00:00', unitFee = null }) {
|
||||
harness.exports.run(
|
||||
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, unitFee, fromAccountId, toAccountId, source, createdAt)
|
||||
VALUES (?,?,?,?,?,?,?,?,'manual',?)`,
|
||||
[btc, type, at, amount, type === 'buy' || type === 'sell' ? 1 : null, unitFee, from, to, at]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
function waardering({ acc, onDate, balance, rateAt = null }) {
|
||||
harness.exports.run(
|
||||
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
||||
VALUES (?,?,?,?,?,?, 'manual')`,
|
||||
[btc, onDate, acc, balance, 50000, rateAt ?? `${onDate}T00:00:00`]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
const tel = (tabel, kolom, id) =>
|
||||
harness.exports.one(`SELECT COUNT(*) AS n FROM ${tabel} WHERE ${kolom} = ?`, [id]).n;
|
||||
const bestaatAccount = id => harness.exports.one(`SELECT 1 AS x FROM Account WHERE id=?`, [id]) != null;
|
||||
|
||||
describe('mergeCheck: compatibiliteit', () => {
|
||||
it('vraagt om twee accounts', () => {
|
||||
const a = account('A');
|
||||
assert.equal(mergeCheck(a, null).reason, 'pickTwo');
|
||||
assert.equal(mergeCheck(null, a).reason, 'pickTwo');
|
||||
assert.equal(mergeCheck(a, null).counts, null);
|
||||
});
|
||||
|
||||
it('weigert een account met zichzelf', () => {
|
||||
const a = account('A');
|
||||
assert.equal(mergeCheck(a, a).reason, 'sameAccount');
|
||||
assert.equal(mergeCheck(String(a), a).reason, 'sameAccount',
|
||||
'ook als het id als string binnenkomt (uit een <select>)');
|
||||
});
|
||||
|
||||
it('weigert een onbekend account', () => {
|
||||
const a = account('A');
|
||||
assert.equal(mergeCheck(a, 9999).reason, 'notFound');
|
||||
});
|
||||
|
||||
it('weigert custodial × trackable', () => {
|
||||
const beurs = account('Beurs', 'exchange');
|
||||
const wallet = account('Wallet', 'wallet');
|
||||
assert.equal(mergeCheck(beurs, wallet).reason, 'sourceMismatch');
|
||||
assert.equal(mergeCheck(wallet, beurs).reason, 'sourceMismatch',
|
||||
'de regel geldt in beide richtingen');
|
||||
});
|
||||
|
||||
it('weigert een overlever die de bron-soorten van de opheffer niet kan dragen', () => {
|
||||
const wallet = account('Wallet', 'wallet');
|
||||
const node = account('Node', 'lightning');
|
||||
assert.equal(mergeCheck(wallet, node).reason, 'kindCannotCarry');
|
||||
});
|
||||
|
||||
it('staat de andere richting wél toe', () => {
|
||||
const wallet = account('Wallet', 'wallet');
|
||||
const node = account('Node', 'lightning');
|
||||
assert.equal(mergeCheck(node, wallet).ok, true,
|
||||
'lightning kan alles dragen wat wallet kan');
|
||||
});
|
||||
|
||||
it('staat twee custodial soorten toe', () => {
|
||||
const bank = account('Bank', 'bank');
|
||||
const kluis = account('Kluis', 'vault');
|
||||
assert.equal(mergeCheck(bank, kluis).ok, true);
|
||||
});
|
||||
|
||||
it('telt wat er zou verhuizen, en kijkt naar beide event-kanten', () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v });
|
||||
event({ type: 'sell', from: v });
|
||||
event({ type: 'buy', to: s });
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, kind, value, createdAt)
|
||||
VALUES (?, 'address', 'bc1qtest', '2024-01-01T00:00:00')`, [v]);
|
||||
waardering({ acc: v, onDate: '2024-02-01', balance: 1000 });
|
||||
|
||||
const chk = mergeCheck(s, v);
|
||||
assert.equal(chk.ok, true);
|
||||
assert.deepEqual(chk.counts, { sources: 1, events: 2, schedules: 0, valuations: 1 });
|
||||
assert.equal(chk.survivorName, 'S');
|
||||
assert.equal(chk.victimName, 'V');
|
||||
});
|
||||
|
||||
it('verandert niets aan de database', () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v });
|
||||
mergeCheck(s, v);
|
||||
assert.equal(tel('Event', 'toAccountId', v), 1, 'een preview hoort niets te muteren');
|
||||
assert.ok(bestaatAccount(v));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeAccounts: verhuizing van kinddata', () => {
|
||||
it('weigert een incompatibele merge en laat alles staan', async () => {
|
||||
const beurs = account('Beurs', 'exchange');
|
||||
const wallet = account('Wallet', 'wallet');
|
||||
event({ type: 'buy', to: wallet });
|
||||
|
||||
await assert.rejects(() => mergeAccounts(beurs, wallet), (e) => e.key === 'accounts.mergeReason_sourceMismatch');
|
||||
assert.ok(bestaatAccount(wallet), 'de opheffer bestaat nog');
|
||||
assert.equal(tel('Event', 'toAccountId', wallet), 1);
|
||||
});
|
||||
|
||||
it('verhuist alle acht soorten kinddata en verwijdert de opheffer', async () => {
|
||||
const s = account('Overlever');
|
||||
const v = account('Opheffer');
|
||||
|
||||
event({ type: 'buy', to: v });
|
||||
event({ type: 'sell', from: v });
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, kind, value, createdAt)
|
||||
VALUES (?, 'address', 'bc1qbron', '2024-01-01T00:00:00')`, [v]);
|
||||
harness.exports.run(`INSERT INTO WalletAddress (accountId, address) VALUES (?, 'bc1qadres')`, [v]);
|
||||
harness.exports.run(`INSERT INTO WalletTransaction (accountId, layer, txid, direction, valueSats, createdAt)
|
||||
VALUES (?, 'onchain', 'tx1', 'in', 100, '2024-01-01T00:00:00')`, [v]);
|
||||
harness.exports.run(`INSERT INTO ScanLog (accountId, scannedAt, status)
|
||||
VALUES (?, '2024-01-01T00:00:00', 'success')`, [v]);
|
||||
harness.exports.run(`INSERT INTO ReconcileProposal
|
||||
(accountId, assetId, amount, direction, suggestedType, status, detectedAt)
|
||||
VALUES (?,?, 100, 'in', 'transfer', 'pending', '2024-01-01T00:00:00')`, [v, btc]);
|
||||
harness.exports.run(`INSERT INTO ImportBatch (accountId, filename, importedAt, rowCount)
|
||||
VALUES (?, 'x.csv', '2024-01-01T00:00:00', 3)`, [v]);
|
||||
harness.exports.run(`INSERT INTO InterestSchedule
|
||||
(accountId, rateKind, payoutAssetId, startsOn, compounding, createdAt)
|
||||
VALUES (?, 'variable', ?, '2024-01-01', 0, '2024-01-01T00:00:00')`, [v, btc]);
|
||||
|
||||
const counts = await mergeAccounts(s, v);
|
||||
|
||||
assert.equal(tel('Event', 'toAccountId', s), 1);
|
||||
assert.equal(tel('Event', 'fromAccountId', s), 1);
|
||||
assert.equal(tel('WalletSource', 'accountId', s), 1);
|
||||
assert.equal(tel('WalletAddress', 'accountId', s), 1);
|
||||
assert.equal(tel('WalletTransaction', 'accountId', s), 1);
|
||||
assert.equal(tel('ScanLog', 'accountId', s), 1);
|
||||
assert.equal(tel('ReconcileProposal', 'accountId', s), 1);
|
||||
assert.equal(tel('ImportBatch', 'accountId', s), 1);
|
||||
assert.equal(tel('InterestSchedule', 'accountId', s), 1);
|
||||
|
||||
for (const [tabel, kolom] of [['Event', 'toAccountId'], ['Event', 'fromAccountId'],
|
||||
['WalletSource', 'accountId'], ['WalletAddress', 'accountId'], ['WalletTransaction', 'accountId'],
|
||||
['ScanLog', 'accountId'], ['ReconcileProposal', 'accountId'], ['ImportBatch', 'accountId'],
|
||||
['InterestSchedule', 'accountId']]) {
|
||||
assert.equal(tel(tabel, kolom, v), 0, `${tabel}.${kolom} verwijst nog naar de opheffer`);
|
||||
}
|
||||
|
||||
assert.equal(bestaatAccount(v), false, 'de opheffer hoort verdwenen te zijn');
|
||||
assert.ok(bestaatAccount(s));
|
||||
assert.equal(counts.events, 2, 'de teruggegeven counts komen uit de voorbeschouwing');
|
||||
});
|
||||
|
||||
it("laat de bron-id's ongemoeid, zodat NWC-secrets geldig blijven", async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, kind, value, createdAt)
|
||||
VALUES (?, 'address', 'bc1qbron', '2024-01-01T00:00:00')`, [v]);
|
||||
const bronId = harness.exports.lastId();
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
const bron = harness.exports.one(`SELECT id, accountId FROM WalletSource`);
|
||||
assert.equal(bron.id, bronId, 'het bron-id mag niet wijzigen');
|
||||
assert.equal(bron.accountId, s);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeAccounts: waarderingen', () => {
|
||||
const verschil = (valId) => {
|
||||
const v = harness.exports.one(`SELECT accountId, assetId, rateAt, balance FROM Valuation WHERE id=?`, [valId]);
|
||||
return v.balance - eventsNetByAccount(v.accountId, v.assetId, v.rateAt);
|
||||
};
|
||||
|
||||
it('houdt het verschil van elke waardering gelijk', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v, amount: 1000, at: '2024-01-01T00:00:00' });
|
||||
event({ type: 'buy', to: s, amount: 500, at: '2024-01-01T00:00:00' });
|
||||
|
||||
const valV = waardering({ acc: v, onDate: '2024-02-01', balance: 1200 });
|
||||
const valS = waardering({ acc: s, onDate: '2024-03-01', balance: 500 });
|
||||
|
||||
assert.equal(verschil(valV), 200);
|
||||
assert.equal(verschil(valS), 0);
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
|
||||
assert.equal(verschil(valV), 200, 'een bestaand verschil moet behouden blijven');
|
||||
assert.equal(verschil(valS), 0, 'reconciled (0) moet 0 blijven');
|
||||
});
|
||||
|
||||
it('corrigeert de balance daarvoor, ook van de overlever zelf', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v, amount: 1000, at: '2024-01-01T00:00:00' });
|
||||
event({ type: 'buy', to: s, amount: 500, at: '2024-01-01T00:00:00' });
|
||||
const valV = waardering({ acc: v, onDate: '2024-02-01', balance: 1200 });
|
||||
const valS = waardering({ acc: s, onDate: '2024-03-01', balance: 500 });
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
|
||||
const balans = id => harness.exports.one(`SELECT balance FROM Valuation WHERE id=?`, [id]).balance;
|
||||
assert.equal(balans(valV), 1700, '1500 grootboek + 200 verschil');
|
||||
assert.equal(balans(valS), 1500, '1500 grootboek + 0 verschil, dus herschreven van 500');
|
||||
});
|
||||
|
||||
it('verhuist een niet-botsende waardering naar de overlever', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
const val = waardering({ acc: v, onDate: '2024-02-01', balance: 10 });
|
||||
await mergeAccounts(s, v);
|
||||
assert.equal(harness.exports.one(`SELECT accountId FROM Valuation WHERE id=?`, [val]).accountId, s);
|
||||
});
|
||||
|
||||
it('houdt bij een botsing precies één waardering over', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
waardering({ acc: s, onDate: '2024-02-01', balance: 500 });
|
||||
waardering({ acc: v, onDate: '2024-02-01', balance: 1200 });
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
|
||||
const over = harness.exports.all(`SELECT accountId, balance FROM Valuation`);
|
||||
assert.equal(over.length, 1, 'geen dubbele rij, geen UNIQUE-fout');
|
||||
assert.equal(over[0].accountId, s, 'de overgebleven rij hangt aan de overlever');
|
||||
});
|
||||
|
||||
it('wist bij een botsing de rij van de opheffer, niet die van de overlever', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
const valS = waardering({ acc: s, onDate: '2024-02-01', balance: 500 });
|
||||
const valV = waardering({ acc: v, onDate: '2024-02-01', balance: 1200 });
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
|
||||
assert.ok(harness.exports.one(`SELECT 1 AS x FROM Valuation WHERE id=?`, [valS]),
|
||||
'de waardering van de overlever blijft');
|
||||
assert.equal(harness.exports.one(`SELECT 1 AS x FROM Valuation WHERE id=?`, [valV]), null,
|
||||
'de botsende rij van de opheffer wijkt');
|
||||
assert.equal(harness.exports.one(`SELECT balance FROM Valuation`).balance, 500,
|
||||
'en dus blijft de gemeten balans van het behouden account staan');
|
||||
});
|
||||
|
||||
it('botst alleen op dezelfde datum, niet op hetzelfde asset alleen', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
waardering({ acc: s, onDate: '2024-02-01', balance: 500 });
|
||||
waardering({ acc: v, onDate: '2024-02-02', balance: 1200 });
|
||||
await mergeAccounts(s, v);
|
||||
assert.equal(harness.exports.one(`SELECT COUNT(*) AS n FROM Valuation`).n, 2);
|
||||
});
|
||||
|
||||
it('rekent het verschil met transfers en fees mee', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v, amount: 1000, at: '2024-01-01T00:00:00' });
|
||||
event({ type: 'transfer', from: v, to: null, amount: 300, unitFee: 20, at: '2024-01-15T00:00:00' });
|
||||
const val = waardering({ acc: v, onDate: '2024-02-01', balance: 700 });
|
||||
|
||||
assert.equal(verschil(val), 20);
|
||||
await mergeAccounts(s, v);
|
||||
assert.equal(verschil(val), 20);
|
||||
});
|
||||
|
||||
it('negeert waarderingen ná het rateAt-moment bij de correctie', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v, amount: 1000, at: '2024-01-01T00:00:00' });
|
||||
event({ type: 'buy', to: s, amount: 9999, at: '2024-06-01T00:00:00' });
|
||||
const val = waardering({ acc: v, onDate: '2024-02-01', balance: 1000 });
|
||||
|
||||
assert.equal(verschil(val), 0);
|
||||
await mergeAccounts(s, v);
|
||||
assert.equal(verschil(val), 0, 'de late aankoop hoort buiten dit venster te vallen');
|
||||
assert.equal(harness.exports.one(`SELECT balance FROM Valuation WHERE id=?`, [val]).balance, 1000,
|
||||
'balance blijft 1000: het grootboek t/m die datum is niet veranderd');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user