Versie 0.2.0

This commit is contained in:
Atreides Assets committed 2026-10-29 11:57:29 +01:00
commit e77d8fd373
495 files changed
+81026

No files matched your search

+302
View File
@@ -0,0 +1,302 @@
// 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');
});
});