1362 lines
71 KiB
JavaScript
1362 lines
71 KiB
JavaScript
// Generated, do not edit. sha256:992d8ca6dcf32528e84e3de9e67ad72a7550a9fbea3e6b15efe619915b129368
|
||||
|
|
import { describe, it, beforeEach, mock } from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
|
|||
|
|
import { createDbHarness } from '../helpers/db-node.mjs';
|
|||
|
|
import {
|
|||
|
|
VERMOGENSCATEGORIE, CATEGORIE_PER_ASSET_KIND, categorieGekozen, categorieVoor, categorieVoorAsset, reikwijdteVoor,
|
|||
|
|
} from '../../client/tax/constants.js';
|
|||
|
|
import { ENUMS } from '../../shared/config.js';
|
|||
|
|
|
|||
|
|
const harness = createDbHarness();
|
|||
|
|
mock.module('../../server/db.js', { namedExports: harness.exports });
|
|||
|
|
|
|||
|
|
let liveSpot = null;
|
|||
|
|
mock.module('../../client/price/providers.js', {
|
|||
|
|
namedExports: {
|
|||
|
|
ensurePriceHistoryFor: async () => {},
|
|||
|
|
ensurePointsAtFor: async () => {},
|
|||
|
|
liveSpotPriceFor: async () => liveSpot,
|
|||
|
|
lastSpotSource: () => (liveSpot != null ? 'Spotbron' : null),
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
for (const mod of ['accounts', 'assets', 'events', 'positions', 'preferences', 'price-history', 'providers', 'valuations']) {
|
|||
|
|
mock.module(`../../client/data/${mod}.js`, { namedExports: await import(`../../server/data/${mod}.js`) });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { setPref } = await import('../../server/data/preferences.js');
|
|||
|
|
|
|||
|
|
const wealth = await import('../../client/wealth/data.js');
|
|||
|
|
|
|||
|
|
const HUIDIG_JAAR = new Date().getFullYear();
|
|||
|
|
|
|||
|
|
let btc, acc;
|
|||
|
|
beforeEach(() => {
|
|||
|
|
harness.recreate();
|
|||
|
|
testProviderId = null;
|
|||
|
|
liveSpot = null;
|
|||
|
|
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();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES ('Kluis','kluis','vault',1,0,1,NULL)`);
|
|||
|
|
acc = harness.exports.lastId();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function koop({ sats, fiatPrice, at, fiatFee = null, priceOrigin = null }) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, fiatFee, priceOrigin, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', ?, ?, ?, ?, ?, ?, 'manual', ?)`,
|
|||
|
|
[btc, at, sats, fiatPrice, fiatFee, priceOrigin, acc, at]);
|
|||
|
|
}
|
|||
|
|
function verkoop({ sats, fiatPrice, at, fiatFee = null, priceOrigin = null }) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, fiatFee, priceOrigin, fromAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'sell', ?, ?, ?, ?, ?, ?, 'manual', ?)`,
|
|||
|
|
[btc, at, sats, fiatPrice, fiatFee, priceOrigin, acc, at]);
|
|||
|
|
}
|
|||
|
|
let testProviderId = null;
|
|||
|
|
function testProvider() {
|
|||
|
|
if (testProviderId === null) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired,
|
|||
|
|
isActive, isSeed, isSystem, createdAt)
|
|||
|
|
VALUES ('test','test','test','','none',0,1,1,0,'2024-01-01T00:00:00Z')`);
|
|||
|
|
testProviderId = harness.exports.lastId();
|
|||
|
|
for (const symbool of ['BTC', 'EUR', 'XAU'])
|
|||
|
|
harness.exports.run(`INSERT INTO ProviderCoverage (providerId, symbol) VALUES (?,?)`,
|
|||
|
|
[testProviderId, symbool]);
|
|||
|
|
}
|
|||
|
|
return testProviderId;
|
|||
|
|
}
|
|||
|
|
function koersPerBtc(datum, eur) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt)
|
|||
|
|
VALUES (?,?, 'day', ?, ?, ?)`, [btc, `${datum}T00:00:00Z`, eur, testProvider(), `${datum}T00:00:00Z`]);
|
|||
|
|
}
|
|||
|
|
const jaarRij = (model, jaar) => model.perJaar.find(b => b.year === jaar);
|
|||
|
|
const aanwasRij = (model, jaar) => model.aanwas.find(a => a.year === jaar);
|
|||
|
|
|
|||
|
|
describe('buildWealthModel: de herkomst van een cel', () => {
|
|||
|
|
function tweedeBron() {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired,
|
|||
|
|
isActive, isSeed, isSystem, createdAt)
|
|||
|
|
VALUES ('tweede','Tweede','tweede','','none',0,1,1,0,'2024-01-01T00:00:00Z')`);
|
|||
|
|
const id = harness.exports.lastId();
|
|||
|
|
harness.exports.run(`INSERT INTO ProviderCoverage (providerId, symbol) VALUES (?, 'BTC')`, [id]);
|
|||
|
|
return id;
|
|||
|
|
}
|
|||
|
|
function slotkoers(datum, eur, providerId = testProvider()) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, createdAt)
|
|||
|
|
VALUES (?,?, 'day', ?, ?, '2026-01-15T00:00:00Z')`, [btc, `${datum}T00:00:00Z`, eur, providerId]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
it('noemt de soort, de bron en de dag van de koers op een peildatum', async () => {
|
|||
|
|
koop({ sats: 1e8, fiatPrice: 40000, at: '2024-06-01T00:00:00Z' });
|
|||
|
|
slotkoers('2025-01-01', 90000);
|
|||
|
|
const cel = (await wealth.buildWealthModel()).rows[0].perYear[2025];
|
|||
|
|
assert.equal(cel.price, 90000);
|
|||
|
|
assert.deepEqual({ kind: cel.origin.kind, source: cel.origin.source, day: cel.origin.day, fiatPrice: cel.origin.fiatPrice },
|
|||
|
|
{ kind: 'close', source: 'test', day: '2025-01-01', fiatPrice: 90000 });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('noemt een doorgetrokken koers met zijn brondag en leeftijd', async () => {
|
|||
|
|
koop({ sats: 1e8, fiatPrice: 40000, at: '2024-06-01T00:00:00Z' });
|
|||
|
|
slotkoers('2024-12-29', 88000);
|
|||
|
|
const cel = (await wealth.buildWealthModel()).rows[0].perYear[2025];
|
|||
|
|
assert.equal(cel.origin.kind, 'carried');
|
|||
|
|
assert.equal(cel.origin.sourceDay, '2024-12-29');
|
|||
|
|
assert.equal(cel.origin.ageDays, 3);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('heeft geen herkomst zonder koers, en ook niet bij een positie van nul', async () => {
|
|||
|
|
koop({ sats: 1e8, fiatPrice: 40000, at: '2024-06-01T00:00:00Z' });
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows[0].perYear[2025].price, null);
|
|||
|
|
assert.equal(m.rows[0].perYear[2025].origin, null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft ook de stand van nu (het dashboard) een herkomst, met de bron van een live spot', async () => {
|
|||
|
|
koop({ sats: 1e8, fiatPrice: 40000, at: '2024-06-01T00:00:00Z' });
|
|||
|
|
slotkoers('2025-01-01', 90000);
|
|||
|
|
const zonderSpot = (await wealth.currentStanding()).rows[0].origin;
|
|||
|
|
assert.equal(zonderSpot.kind, 'carried', 'de laatste koers, doorgetrokken tot vandaag');
|
|||
|
|
assert.equal(zonderSpot.source, 'test');
|
|||
|
|
liveSpot = 95000;
|
|||
|
|
const metSpot = (await wealth.currentStanding()).rows[0].origin;
|
|||
|
|
assert.deepEqual({ kind: metSpot.kind, fiatPrice: metSpot.fiatPrice, source: metSpot.source }, { kind: 'live', fiatPrice: 95000, source: 'Spotbron' });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('beschrijft bij twee bronnen op één dag de koers die gebruikt is', async () => {
|
|||
|
|
koop({ sats: 1e8, fiatPrice: 40000, at: '2024-06-01T00:00:00Z' });
|
|||
|
|
slotkoers('2025-01-01', 90000);
|
|||
|
|
slotkoers('2025-01-01', 91000, tweedeBron());
|
|||
|
|
const cel = (await wealth.buildWealthModel()).rows[0].perYear[2025];
|
|||
|
|
assert.equal(cel.origin.fiatPrice, cel.price, 'de herkomst beschrijft hetzelfde getal als de cel');
|
|||
|
|
assert.equal(cel.origin.source, cel.price === 90000 ? 'test' : 'Tweede');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('buildWealthModel: zonder gegevens', () => {
|
|||
|
|
it('levert een leeg model zonder events', async () => {
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.deepEqual(m, { years: [], rows: [], totals: null });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('levert een leeg model als het asset verdwijnt', async () => {
|
|||
|
|
koop({ sats: 100, fiatPrice: 1, at: '2024-01-01T00:00:00' });
|
|||
|
|
harness.exports.run(`DELETE FROM Asset`);
|
|||
|
|
assert.equal(harness.exports.one(`SELECT COUNT(*) AS n FROM Event`).n, 0, 'CASCADE ruimde de events op');
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.deepEqual(m.years, []);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('peiljaren', () => {
|
|||
|
|
it('begint het jaar ná de eerste gebeurtenis en loopt t/m dit jaar', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2022-06-01T00:00:00' });
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.years[0], 2023);
|
|||
|
|
assert.equal(m.years.at(-1), HUIDIG_JAAR);
|
|||
|
|
assert.equal(m.years.length, HUIDIG_JAAR - 2022);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('peildata waarop er niets stond', () => {
|
|||
|
|
it('noemt een peildatum leeg als geen enkele rij dan iets had', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
verkoop({ sats: 100_000_000, fiatPrice: 25000, at: '2023-09-01T00:00:00' });
|
|||
|
|
koop({ sats: 50_000_000, fiatPrice: 60000, at: `${HUIDIG_JAAR - 1}-06-01T00:00:00` });
|
|||
|
|
koersPerBtc('2023-01-01', 20000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.ok(m.emptyYears.includes(2024), '1 januari 2024: alles was verkocht');
|
|||
|
|
assert.ok(m.emptyYears.includes(HUIDIG_JAAR - 1),
|
|||
|
|
'en op 1 januari vorig jaar ook nog niet: die aankoop kwam in juni');
|
|||
|
|
assert.ok(!m.emptyYears.includes(HUIDIG_JAAR), 'maar op 1 januari van dit jaar staat er wél iets');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('noemt een peildatum NIET leeg als er wel iets stond maar zonder koers', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR}-01-01`, 50000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows[0].perYear[2024].value, null, 'de waarde is onbekend');
|
|||
|
|
assert.ok(!m.emptyYears.includes(2024), 'maar de kolom is niet leeg: er staat een positie');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een post BUITEN box 3 de kolom niet bezet houden', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2023-01-01', 20000);
|
|||
|
|
harness.exports.run(`UPDATE Account SET taxScope = 'none' WHERE id = ?`, [acc]);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.ok(m.emptyYears.includes(2024), 'alleen buiten-box-3-bezit is voor deze pagina leeg');
|
|||
|
|
assert.equal(m.buitenBox3.length, 1, 'en het wordt wel genoemd, met zijn bedrag');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('stromen over de grens van het overzicht', () => {
|
|||
|
|
let pensioen;
|
|||
|
|
beforeEach(() => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt, taxScope)
|
|||
|
|
VALUES ('Pensioen','pensioen','vault',1,0,1,NULL,'none')`);
|
|||
|
|
pensioen = harness.exports.lastId();
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2024-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 20000);
|
|||
|
|
for (const d of ['2024-06-01', '2025-01-01', '2025-03-01', '2026-01-01']) koersPerBtc(d, 30000);
|
|||
|
|
});
|
|||
|
|
const resultaat2025 = m => m.aanwas.find(a => a.year === 2025);
|
|||
|
|
|
|||
|
|
it('telt een verkoop op een rekening buiten het overzicht niet als onttrekking', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', '2024-06-01T00:00:00', 50000000, 20000, ?, 'manual', '2024-06-01T00:00:00')`, [btc, pensioen]);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, fromAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'sell', '2025-03-01T00:00:00', 50000000, 30000, ?, 'manual', '2025-03-01T00:00:00')`, [btc, pensioen]);
|
|||
|
|
const a = resultaat2025(await wealth.buildWealthModel());
|
|||
|
|
assert.equal(a.withdrawals, 0, 'die verkoop ligt buiten deze pagina');
|
|||
|
|
assert.equal(Math.round(a.result), 0, 'en maakt het resultaat dus niet hoger');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt een verplaatsing naar een rekening buiten het overzicht als onttrekking en niet als verlies', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fromAccountId, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'transfer', '2025-03-01T00:00:00', 50000000, ?, ?, 'manual', '2025-03-01T00:00:00')`, [btc, acc, pensioen]);
|
|||
|
|
const a = resultaat2025(await wealth.buildWealthModel());
|
|||
|
|
assert.equal(Math.round(a.withdrawals), 15000, 'een halve BTC tegen de dagkoers verliet het overzicht');
|
|||
|
|
assert.equal(Math.round(a.result), 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('trekt dezelfde grens bij "alleen per categorie": tabel en resultaat zien die rekening niet', async () => {
|
|||
|
|
harness.exports.run(`UPDATE Account SET taxScope = 'category' WHERE id = ?`, [pensioen]);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fromAccountId, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'transfer', '2025-03-01T00:00:00', 50000000, ?, ?, 'manual', '2025-03-01T00:00:00')`, [btc, acc, pensioen]);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const a = resultaat2025(m);
|
|||
|
|
assert.equal(Math.round(a.withdrawals), 15000);
|
|||
|
|
assert.equal(Math.round(a.result), 0);
|
|||
|
|
assert.equal(m.rows.some(r => r.account.id === pensioen), false, 'niet in de tabel');
|
|||
|
|
assert.equal(m.categoryRows.some(r => r.account.id === pensioen), true, 'wel in de splitsing');
|
|||
|
|
assert.equal(m.buitenBox3.length, 0, 'en niet genoemd als weggelaten: hij staat in het kader per categorie');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('negeert met ignoreTaxSettings de reikwijdte en categorie van account en asset', async () => {
|
|||
|
|
harness.exports.run(`UPDATE Asset SET taxCategory = 'bank' WHERE id = ?`, [btc]);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fromAccountId, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'transfer', '2025-03-01T00:00:00', 50000000, ?, ?, 'manual', '2025-03-01T00:00:00')`, [btc, acc, pensioen]);
|
|||
|
|
const eigen = await wealth.buildWealthModel();
|
|||
|
|
const standaard = await wealth.buildWealthModel({ ignoreTaxSettings: true });
|
|||
|
|
assert.equal(eigen.rows.some(r => r.account.id === pensioen), false, 'met de instelling buiten de tabel');
|
|||
|
|
assert.equal(standaard.rows.some(r => r.account.id === pensioen), true, 'zonder de instelling erin');
|
|||
|
|
assert.equal(standaard.buitenBox3.length, 0, 'en dus niet als weggelaten genoemd');
|
|||
|
|
assert.equal(resultaat2025(standaard).withdrawals, 0, 'de verplaatsing blijft binnen de pagina');
|
|||
|
|
assert.equal(categorieGekozen(eigen.categoryRows), true);
|
|||
|
|
assert.equal(categorieGekozen(standaard.categoryRows), false, 'de gekozen categorie telt ook niet');
|
|||
|
|
assert.equal(standaard.rows.every(r => r.categorie === VERMOGENSCATEGORIE.OVERIG), true, 'BTC volgt zijn soort');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('meldt hasTaxSettings alleen bij een afwijking, ook in de standaardweergave', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2024-06-01T00:00:00' });
|
|||
|
|
harness.exports.run(`UPDATE Account SET taxScope = NULL WHERE id = ?`, [pensioen]);
|
|||
|
|
assert.equal((await wealth.buildWealthModel()).hasTaxSettings, false, 'alles op de standaard');
|
|||
|
|
harness.exports.run(`UPDATE Asset SET taxCategory = 'bank' WHERE id = ?`, [btc]);
|
|||
|
|
assert.equal((await wealth.buildWealthModel()).hasTaxSettings, true, 'een categorie op een asset');
|
|||
|
|
assert.equal((await wealth.buildWealthModel({ ignoreTaxSettings: true })).hasTaxSettings, true);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('sumReturn: een rekening met een asset uit het jaar zelf', () => {
|
|||
|
|
let xau;
|
|||
|
|
beforeEach(() => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES ('XAU','Goud','goud','metal','g',NULL,4,'unit')`);
|
|||
|
|
xau = harness.exports.lastId();
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2024-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-06-01', 20000);
|
|||
|
|
koersPerBtc('2025-01-01', 30000);
|
|||
|
|
koersPerBtc('2025-07-01', 33000);
|
|||
|
|
koersPerBtc('2026-01-01', 33000);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', '2025-05-26T23:00:00Z', 1000000, 50, ?, 'manual', '2025-05-26T23:00:00Z')`, [xau, acc]);
|
|||
|
|
for (const [d, eur] of [['2025-05-01', 50], ['2025-10-01', 60], ['2026-01-01', 60]]) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt)
|
|||
|
|
VALUES (?,?, 'day', ?, ?, ?)`, [xau, `${d}T00:00:00Z`, eur, testProvider(), `${d}T00:00:00Z`]);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
const cellen = (m, ids) => m.rows.filter(r => r.account.id === acc && ids.includes(r.asset.id))
|
|||
|
|
.map(r => ({ begin: r.perYear[2025], eind: r.perYear[2026] }));
|
|||
|
|
|
|||
|
|
it('telt het goud mee in resultaat én percentage van de rekening', async () => {
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const goud = m.rows.find(r => r.asset.id === xau);
|
|||
|
|
assert.equal(goud.perYear[2026].pct, null, 'het goud zelf heeft geen percentage: het begon bij nul');
|
|||
|
|
const res = wealth.sumReturn(cellen(m, [btc, xau]));
|
|||
|
|
assert.equal(Math.round(res.gain), 4000);
|
|||
|
|
assert.ok(Math.abs(res.pct - 4000 / 33000) < 1e-4, `percentage ${res.pct}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft wel het resultaat maar geen percentage als de rekening op 1 januari leeg was', async () => {
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const res = wealth.sumReturn(cellen(m, [xau]));
|
|||
|
|
assert.equal(Math.round(res.gain), 1000);
|
|||
|
|
assert.equal(res.pct, null);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('startjaar van de administratie', () => {
|
|||
|
|
it('begrenst de peiljaren zonder de stand aan te tasten', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 5000, at: '2018-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
await setPref('app', 'start_year', 2024);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.years[0], 2024, 'de reeks begint bij het gekozen jaar');
|
|||
|
|
assert.equal(m.years.length, HUIDIG_JAAR - 2023);
|
|||
|
|
assert.equal(m.ingekort, true);
|
|||
|
|
assert.equal(m.firstYear, 2018, 'het écht eerste jaar gaat mee terug, voor de melding op de pagina');
|
|||
|
|
assert.equal(jaarRij(m, 2024).value, 40000, '1 BTC uit 2018 × de koers van 1-1-2024');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('doet niets bij een startjaar vóór de eerste peildatum', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2022-06-01T00:00:00' });
|
|||
|
|
await setPref('app', 'start_year', 1990);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.years[0], 2023, 'de eerste positie blijft de ondergrens');
|
|||
|
|
assert.equal(m.ingekort, false, 'er is niets afgesneden, dus de pagina meldt niets');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('houdt minstens één kolom over bij een startjaar in de toekomst', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2022-06-01T00:00:00' });
|
|||
|
|
await setPref('app', 'start_year', HUIDIG_JAAR + 5);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.deepEqual(m.years, [HUIDIG_JAAR]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('gebruikt zonder keuze de eerste positie', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2022-06-01T00:00:00' });
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.years[0], 2023);
|
|||
|
|
assert.equal(m.ingekort, false);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('koerstoekenning per peildatum', () => {
|
|||
|
|
it('gebruikt de koers van de peildatum zelf', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(jaarRij(m, 2024).value, 40000, '1 BTC × 40.000');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft geen waarde vóór het begin van de historie', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(jaarRij(m, 2024).value, 0, 'geen koers → deze rij telt niet mee in het totaal');
|
|||
|
|
assert.equal(m.rows[0].perYear[2024].price, null);
|
|||
|
|
assert.equal(m.rows[0].perYear[2024].value, null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('trekt een koers vooruit tot de OUDERDOMSGRENS en daarna niet meer', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2022-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2023-12-28', 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(jaarRij(m, 2024).value, 30000, 'binnen de grens wordt hij wél doorgetrokken');
|
|||
|
|
const cel = m.rows[0].perYear[2025];
|
|||
|
|
assert.equal(cel.price, null, 'een jaar later geldt die koers niet meer, en dat is de grens');
|
|||
|
|
assert.equal(cel.value, null, 'dus geen waarde, en null betekent hier "wel bezit, niet te waarderen"');
|
|||
|
|
assert.equal(cel.sats, 100_000_000, 'de positie staat er nog wel');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft metaal een RUIMERE grens dan crypto, met dezelfde koersdatum', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
|
|||
|
|
VALUES ('XAU','Goud','goud','metal','g','mg',3,'unit')`);
|
|||
|
|
const goud = harness.exports.lastId();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', '2022-06-01T00:00:00', 1000, 80, ?, 'manual', '2022-06-01T00:00:00')`,
|
|||
|
|
[goud, acc]);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt)
|
|||
|
|
VALUES (?, '2023-12-20T00:00:00Z', 'day', 100, ?, '2023-12-20T00:00:00Z')`, [goud, testProvider()]);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2022-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2023-12-20', 30000);
|
|||
|
|
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const cel = (assetId) => m.rows.find(r => r.asset.id === assetId).perYear[2024];
|
|||
|
|
assert.equal(cel(goud).price, 100, 'twaalf dagen valt binnen de grens van metaal');
|
|||
|
|
assert.equal(cel(btc).price, null, 'en buiten die van crypto');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('kent fiat GEEN grens, want een vaste koers veroudert niet', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
|
|||
|
|
VALUES ('EUR','Euro','euro','fiat','EUR','cent',2,'unit')`);
|
|||
|
|
const eur = harness.exports.lastId();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'interest', '2022-06-01T00:00:00', 100000, NULL, ?, 'manual', '2022-06-01T00:00:00')`,
|
|||
|
|
[eur, acc]);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt)
|
|||
|
|
VALUES (?, '2022-06-01T00:00:00Z', 'day', 1, ?, '2022-06-01T00:00:00Z')`, [eur, testProvider()]);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const rij = m.rows.find(r => r.asset.id === eur);
|
|||
|
|
assert.equal(rij.perYear[2024].price, 1, 'anderhalf jaar later nog steeds 1,00');
|
|||
|
|
assert.equal(rij.perYear[2024].value, 1000, 'en dus 1.000 euro op de rekening');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('gebruikt een live spot voor de huidige stand als die er is', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
liveSpot = 90000;
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.totals.current, 90000, 'de verse spot gaat vóór het laatste gridpunt');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('valt zonder live spot terug op het laatste gridpunt', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
koersPerBtc('2024-06-01', 55000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.totals.current, 55000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft NUL en niet "geen waarde" als de positie nul is', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
|
|||
|
|
VALUES ('XAU','Goud','goud','metal','g','mg',3,'unit')`);
|
|||
|
|
const goud = harness.exports.lastId();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', ?, ?, ?, ?, 'manual', ?)`,
|
|||
|
|
[goud, `${HUIDIG_JAAR}-03-01T00:00:00`, 10_000, 80, acc, `${HUIDIG_JAAR}-03-01T00:00:00`]);
|
|||
|
|
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const goudRij = m.rows.find(r => r.asset.id === goud);
|
|||
|
|
assert.ok(goudRij, 'de rij bestaat, want er is later wél een positie');
|
|||
|
|
assert.equal(goudRij.perYear[2024].sats, 0);
|
|||
|
|
assert.equal(goudRij.perYear[2024].price, null, 'er is geen koers op die peildatum');
|
|||
|
|
assert.equal(goudRij.perYear[2024].value, 0, 'en nul eenheden is nul euro, ook zonder koers');
|
|||
|
|
assert.equal(goudRij.current.sats, 10_000);
|
|||
|
|
assert.equal(goudRij.current.value, null, 'wél een positie en géén koers blijft onbekend');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('rijen per account en asset', () => {
|
|||
|
|
it('laat een combinatie zonder enig saldo weg', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES ('Leeg','leeg','bank',0,0,1,NULL)`);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows.length, 1);
|
|||
|
|
assert.equal(m.rows[0].account.name, 'Kluis');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('noemt de rij naar het account bij één asset en met symbool bij meerdere', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
assert.equal((await wealth.buildWealthModel()).rows[0].name, 'Kluis');
|
|||
|
|
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES ('XAU','Goud','goud','metal','g',NULL,7,'unit')`);
|
|||
|
|
assert.equal((await wealth.buildWealthModel()).rows[0].name, 'Kluis · BTC');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('vermogensaanwas', () => {
|
|||
|
|
it('rekent eind − begin − stortingen + onttrekkingen', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.equal(a.begin, 30000);
|
|||
|
|
assert.equal(a.eind, 50000);
|
|||
|
|
assert.equal(a.deposits, 0);
|
|||
|
|
assert.equal(a.result, 20000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('trekt een storting binnen het jaar van het resultaat af', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 35000, at: '2024-07-01T00:00:00' });
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.equal(a.eind, 100000, '2 BTC × 50.000');
|
|||
|
|
assert.equal(a.deposits, 35000);
|
|||
|
|
assert.equal(a.result, 100000 - 30000 - 35000, 'alleen de waardegroei blijft over');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt de fee van een aankoop mee in de storting', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 35000, at: '2024-07-01T00:00:00', fiatFee: 250 });
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
assert.equal(aanwasRij(await wealth.buildWealthModel(), 2024).deposits, 35250,
|
|||
|
|
'de kosten van de aankoop horen bij het ingelegde bedrag');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt een verkoop als onttrekking, ná aftrek van de fee', async () => {
|
|||
|
|
koop({ sats: 200_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
verkoop({ sats: 100_000_000, fiatPrice: 40000, at: '2024-07-01T00:00:00', fiatFee: 100 });
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.equal(a.begin, 60000);
|
|||
|
|
assert.equal(a.eind, 50000, 'nog 1 BTC over × 50.000');
|
|||
|
|
assert.equal(a.withdrawals, 40000 - 100, 'netto ontvangen, dus na kosten');
|
|||
|
|
assert.equal(a.result, 50000 - 60000 + 39900);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt van een interne verplaatsing met "niets tegenover" alleen de euro-fee', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES ('Tweede','tweede','vault',1,0,1,NULL)`);
|
|||
|
|
const tweede = harness.exports.lastId();
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2024-07-01', 40000);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatFee, priceOrigin, fromAccountId, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'transfer', '2024-07-01T00:00:00', 50000000, 2, 'none', ?, ?, 'manual', '2024-07-01T00:00:00')`,
|
|||
|
|
[btc, acc, tweede]);
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.equal(a.deposits, 2, 'alleen de fee');
|
|||
|
|
assert.equal(a.giften, 0, 'een interne verplaatsing is geen gift');
|
|||
|
|
assert.equal(a.withdrawals, 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rekent een ONTVANGEN GIFT uit het rendement, op de dagkoers', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2024-07-01', 40000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 0, at: '2024-07-01T00:00:00', priceOrigin: 'none' });
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.equal(a.deposits, 0, 'er is niets betaald, dus dit is geen inleg');
|
|||
|
|
assert.equal(a.giften, 40000, 'wel waarde binnen, tegen de koers van die dag');
|
|||
|
|
assert.equal(a.result, 100000 - 30000 - 40000, 'en dus blijft alleen de waardegroei over');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('boekt een WEGGEGEVEN gift terug, anders leest hij als verlies', async () => {
|
|||
|
|
koop({ sats: 200_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2024-07-01', 40000);
|
|||
|
|
verkoop({ sats: 100_000_000, fiatPrice: 0, at: '2024-07-01T00:00:00', priceOrigin: 'none' });
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.equal(a.withdrawals, 0, 'er is niets ontvangen, dus geen onttrekking');
|
|||
|
|
assert.equal(a.giften, -40000);
|
|||
|
|
assert.equal(a.result, 50000 - 60000 + 40000, 'de waardegroei van wat je hield, en geen verlies');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een gift zonder koers op die dag in het rendement staan', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 0, at: '2024-07-01T00:00:00', priceOrigin: 'none' });
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.equal(a.giften, 0, 'niet te waarderen, dus geen correctie');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('levert het rendement en géén belastingbedrag', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.equal(a.result, 20000, 'het rendement zelf is wél een kengetal');
|
|||
|
|
for (const veld of ['vrijstelling', 'belastbaar', 'belasting']) {
|
|||
|
|
assert.equal(a[veld], undefined, `${veld} hoort geen onderdeel meer te zijn van het model`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een verlies als negatief rendement staan', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 60000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 60000);
|
|||
|
|
koersPerBtc('2025-01-01', 40000);
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.equal(a.result, -20000, 'het verlies zelf blijft zichtbaar, en dat is het kengetal');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('markeert het lopende jaar en meet dat tot nu in plaats van tot 1 januari', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 1}-06-01T00:00:00` });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR}-01-01`, 30000);
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR}-06-01`, 45000);
|
|||
|
|
const a = aanwasRij(await wealth.buildWealthModel(), HUIDIG_JAAR);
|
|||
|
|
assert.equal(a.running, true);
|
|||
|
|
assert.equal(a.begin, 30000);
|
|||
|
|
assert.equal(a.eind, 45000, 'het eind van een lopend jaar is de huidige stand');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('kengetallen per belastingjaar', () => {
|
|||
|
|
const scenario2024 = async (waarde2024, waarde2025 = waarde2024) => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', waarde2024);
|
|||
|
|
koersPerBtc('2025-01-01', waarde2025);
|
|||
|
|
return jaarRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
it('geeft de waarde op de peildatum en het rendement over dat jaar', async () => {
|
|||
|
|
const b = await scenario2024(100000, 101000);
|
|||
|
|
assert.equal(b.value, 100000);
|
|||
|
|
assert.equal(b.rendement, 1000, 'eind − begin, gecorrigeerd voor stortingen en onttrekkingen');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('splitst de waarde uit per vermogenscategorie', async () => {
|
|||
|
|
const b = await scenario2024(100000);
|
|||
|
|
assert.deepEqual(b.perCategorie, { [VERMOGENSCATEGORIE.OVERIG]: 100000 });
|
|||
|
|
assert.equal(b.categorieCompleet, true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt de categorieën op tot het totaal', async () => {
|
|||
|
|
const b = await scenario2024(100000);
|
|||
|
|
const som = Object.values(b.perCategorie).reduce((s, v) => s + v, 0);
|
|||
|
|
assert.equal(som, b.value);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rekent geen belasting meer, en dat hoort zo te blijven', async () => {
|
|||
|
|
const b = await scenario2024(100000);
|
|||
|
|
for (const veld of ['params', 'hvv', 'grondslag', 'voordeel', 'belasting', 'tegenbewijs']) {
|
|||
|
|
assert.equal(b[veld], undefined, `${veld} hoort geen onderdeel van het model te zijn`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft ook voor een jaar zonder bekend regime gewoon de waarde', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 5000, at: '2019-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2020-01-01', 6000);
|
|||
|
|
const b = jaarRij(await wealth.buildWealthModel(), 2020);
|
|||
|
|
assert.equal(b.value, 6000);
|
|||
|
|
assert.deepEqual(b.perCategorie, { [VERMOGENSCATEGORIE.OVERIG]: 6000 });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('vermogenscategorie per asset-soort', () => {
|
|||
|
|
it('kent elke soort uit de asset-enum een categorie toe', async () => {
|
|||
|
|
const { ENUMS } = await import('../../shared/config.js');
|
|||
|
|
for (const kind of ENUMS.ASSET_KIND) {
|
|||
|
|
assert.ok(CATEGORIE_PER_ASSET_KIND[kind],
|
|||
|
|
`asset-soort '${kind}' heeft geen vermogenscategorie; voeg hem toe in tax/constants.js`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('koppelt fiat aan banktegoeden en de rest aan overige bezittingen', () => {
|
|||
|
|
assert.equal(categorieVoorAsset({ kind: 'fiat' }), VERMOGENSCATEGORIE.BANK);
|
|||
|
|
assert.equal(categorieVoorAsset({ kind: 'crypto' }), VERMOGENSCATEGORIE.OVERIG);
|
|||
|
|
assert.equal(categorieVoorAsset({ kind: 'metal' }), VERMOGENSCATEGORIE.OVERIG);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('ziet een gekozen categorie alleen als iemand hem zette, op account of asset', () => {
|
|||
|
|
const rij = (acc, asset) => ({ account: { taxCategory: acc }, asset: { kind: 'fiat', taxCategory: asset } });
|
|||
|
|
assert.equal(categorieGekozen([rij(null, null), rij(null, null)]), false, 'alles volgt de soort');
|
|||
|
|
assert.equal(categorieGekozen([rij(null, null), rij('bank', null)]), true, 'op een account');
|
|||
|
|
assert.equal(categorieGekozen([rij(null, 'overig')]), true, 'op een asset');
|
|||
|
|
assert.equal(categorieGekozen([{ account: { id: null, taxCategory: null }, asset: { taxCategory: null } }]), false);
|
|||
|
|
assert.equal(categorieGekozen([]), false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft null voor een onbekende soort in plaats van te gokken', () => {
|
|||
|
|
assert.equal(categorieVoorAsset({ kind: 'onzin' }), null);
|
|||
|
|
assert.equal(categorieVoorAsset({ kind: '' }), null);
|
|||
|
|
assert.equal(categorieVoorAsset(null), null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('kan vandaag niet vóórkomen in de database, en dat is de tweede helft van de guard', () => {
|
|||
|
|
assert.throws(
|
|||
|
|
() => harness.exports.run(`UPDATE Asset SET kind = 'onzin' WHERE id = ?`, [btc]),
|
|||
|
|
/CHECK constraint failed/);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('legt een fiat-asset onder banktegoeden in het model', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
|
|||
|
|
VALUES ('EUR','Euro','euro','fiat','EUR','cent',2,'unit')`);
|
|||
|
|
const eur = harness.exports.lastId();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'interest', '2023-06-01T00:00:00', 100000, NULL, ?, 'manual', '2023-06-01T00:00:00')`,
|
|||
|
|
[eur, acc]);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt)
|
|||
|
|
VALUES (?, '2024-01-01T00:00:00Z', 'day', 1, ?, '2024-01-01T00:00:00Z')`, [eur, testProvider()]);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 100000);
|
|||
|
|
koersPerBtc('2025-01-01', 100000);
|
|||
|
|
|
|||
|
|
const b = jaarRij(await wealth.buildWealthModel(), 2024);
|
|||
|
|
assert.deepEqual(b.perCategorie, {
|
|||
|
|
[VERMOGENSCATEGORIE.OVERIG]: 100000,
|
|||
|
|
[VERMOGENSCATEGORIE.BANK]: 1000,
|
|||
|
|
});
|
|||
|
|
assert.equal(b.value, 101000, 'het totaal is de som van beide categorieën');
|
|||
|
|
assert.equal(b.categorieCompleet, true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('haalt een asset met reikwijdte "alleen per categorie" uit tabel en resultaat, niet uit de splitsing', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
|
|||
|
|
VALUES ('EUR','Euro','euro','fiat','EUR','cent',2,'unit')`);
|
|||
|
|
const eur = harness.exports.lastId();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'interest', '2023-06-01T00:00:00', 100000, NULL, ?, 'manual', '2023-06-01T00:00:00')`,
|
|||
|
|
[eur, acc]);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt)
|
|||
|
|
VALUES (?, '2024-01-01T00:00:00Z', 'day', 1, ?, '2024-01-01T00:00:00Z')`, [eur, testProvider()]);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 100000);
|
|||
|
|
koersPerBtc('2025-01-01', 100000);
|
|||
|
|
|
|||
|
|
const gewoon = await wealth.buildWealthModel();
|
|||
|
|
harness.exports.run(`UPDATE Asset SET taxScope = 'category' WHERE id = ?`, [eur]);
|
|||
|
|
const apart = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(gewoon.totals.perYear[2024], 101000);
|
|||
|
|
assert.deepEqual(gewoon.categorySplit.perYear[2024], gewoon.totals.perYearPerCategorie[2024],
|
|||
|
|
'zonder aparte reikwijdte is de splitsing die van de tabel');
|
|||
|
|
assert.equal(apart.totals.perYear[2024], 100000, 'het banktegoed staat niet meer in de totalen');
|
|||
|
|
assert.equal(apart.rows.some(r => r.asset.id === eur), false, 'en niet in de tabel');
|
|||
|
|
assert.deepEqual(apart.categorySplit.perYear[2024],
|
|||
|
|
{ [VERMOGENSCATEGORIE.OVERIG]: 100000, [VERMOGENSCATEGORIE.BANK]: 1000 }, 'maar wel in de splitsing');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('rendementspercentage', () => {
|
|||
|
|
it('is null voor de eerste kolom', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.pct.perYear[m.years[0]], null, 'er is geen vorige kolom om tegen te meten');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('corrigeert voor stortingen, zodat bijstorten geen rendement lijkt', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 30000, at: '2024-07-01T00:00:00' });
|
|||
|
|
koersPerBtc('2025-01-01', 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.pct.perYear[2025], 0,
|
|||
|
|
'waarde verdubbeld, maar volledig door de storting → 0% rendement');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('meet de huidige waarde tegen het gemiddeld werkzame vermogen', async (t) => {
|
|||
|
|
const start = new Date(HUIDIG_JAAR, 0, 1);
|
|||
|
|
const storting = new Date(HUIDIG_JAAR, 0, 1, 1);
|
|||
|
|
if (Date.now() - storting.getTime() < 24 * 3600 * 1000) return t.skip('te vroeg in het jaar');
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 1}-06-01T00:00:00` });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR}-01-01`, 30000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 30000, at: storting.toISOString() });
|
|||
|
|
koersPerBtc(new Date().toISOString().slice(0, 10), 33000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const a = aanwasRij(m, HUIDIG_JAAR);
|
|||
|
|
assert.equal(a.result, 6000);
|
|||
|
|
const nu = Date.now();
|
|||
|
|
const gewicht = (nu - storting.getTime()) / (nu - start.getTime());
|
|||
|
|
const verwacht = 6000 / (30000 + gewicht * 30000);
|
|||
|
|
assert.ok(Math.abs(m.pct.current - verwacht) < 1e-4, `verwacht ${verwacht}, kreeg ${m.pct.current}`);
|
|||
|
|
assert.ok(Math.abs(a.pct - verwacht) < 1e-4, 'het aangifteblok leest hetzelfde getal');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('volgt de keuze voor IRR ook op de totaalregel', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 2}-06-01T00:00:00` });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 1}-01-01`, 30000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 30000, at: `${HUIDIG_JAAR - 1}-07-01T00:00:00` });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR}-01-01`, 36000);
|
|||
|
|
await setPref('app', 'mwr_method', 'irr');
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const rij = m.rows[0].perYear[HUIDIG_JAAR];
|
|||
|
|
assert.ok(Math.abs(rij.pct - 0.2715) < 0.002, `de IRR van de rij: verwacht 27,15%, kreeg ${rij.pct}`);
|
|||
|
|
assert.ok(Math.abs(m.pct.perYear[HUIDIG_JAAR] - rij.pct) < 1e-6,
|
|||
|
|
`totaal ${m.pct.perYear[HUIDIG_JAAR]} tegen rij ${rij.pct}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function rekening(naam) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES (?,?,'vault',1,0,1,NULL)`,
|
|||
|
|
[naam, naam.toLowerCase()]);
|
|||
|
|
return harness.exports.lastId();
|
|||
|
|
}
|
|||
|
|
function balans({ op, sats, at }) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
|||
|
|
VALUES (?,?,?,?,1.0,?, 'manual')`, [btc, at.slice(0, 10), op, sats, at]);
|
|||
|
|
}
|
|||
|
|
function verplaats({ sats, at, naar }) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fromAccountId, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'transfer', ?, ?, ?, ?, 'manual', ?)`, [btc, at, sats, acc, naar, at]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
it('deelt door het gemiddeld werkzame vermogen en niet meer door de beginwaarde', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2024-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2025-01-01', 20000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 24000, at: '2025-07-01T00:00:00' });
|
|||
|
|
koersPerBtc('2026-01-01', 24000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const cel = m.rows[0].perYear[2026];
|
|||
|
|
assert.equal(Math.round(cel.gain), 4000, 'de teller blijft wat hij was');
|
|||
|
|
assert.equal(Math.round((4000 / 20000) * 1000) / 10, 20, 'de oude som, ter vergelijking');
|
|||
|
|
assert.ok(Math.abs(cel.pct - 0.1246) < 0.002, `verwacht ongeveer 12,5%, kreeg ${cel.pct}`);
|
|||
|
|
assert.ok(cel.pct < 0.2, 'en in elk geval fors onder de oude 20%');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('zet de noemer op de cel, zodat een regel die assets optelt hem kan sommeren', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2024-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2025-01-01', 20000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 24000, at: '2025-07-01T00:00:00' });
|
|||
|
|
koersPerBtc('2026-01-01', 24000);
|
|||
|
|
const cel = (await wealth.buildWealthModel()).rows[0].perYear[2026];
|
|||
|
|
assert.ok(Math.abs(cel.basis - 32100) < 100, `verwacht ongeveer 32.100, kreeg ${cel.basis}`);
|
|||
|
|
assert.equal(Math.round((cel.gain / cel.basis) * 1000) / 1000, Math.round(cel.pct * 1000) / 1000,
|
|||
|
|
'en het percentage op de cel is precies die deling');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft exact het oude getal zolang er niets stroomt', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2025-01-01', 20000);
|
|||
|
|
koersPerBtc('2026-01-01', 25000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows[0].perYear[2026].pct, 0.25, '5.000 op 20.000, precies zoals voorheen');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('staat per rij op de kolom waar dat jaar op eindigt', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2025-01-01', 60000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows[0].perYear[2025].pct, 1, 'koers verdubbeld, niets bijgekocht → +100%');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('is per rij leeg in de eerste kolom', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows[0].perYear[m.years[0]].pct, null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('corrigeert per rij voor een aankoop halverwege het jaar', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 30000, at: '2024-07-01T00:00:00' });
|
|||
|
|
koersPerBtc('2025-01-01', 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows[0].perYear[2025].pct, 0, 'de waarde verdubbelde, maar volledig door de aankoop');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('leest een verplaatsing tussen eigen rekeningen niet als verlies', async () => {
|
|||
|
|
const tweede = rekening('Beurs');
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2024-07-01', 30000);
|
|||
|
|
verplaats({ sats: 100_000_000, at: '2024-07-01T00:00:00', naar: tweede });
|
|||
|
|
koersPerBtc('2025-01-01', 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const vertrek = m.rows.find(r => r.account.id === acc);
|
|||
|
|
const aankomst = m.rows.find(r => r.account.id === tweede);
|
|||
|
|
assert.equal(vertrek.perYear[2025].pct, 0, 'de rekening die leegliep heeft niets verloren');
|
|||
|
|
assert.equal(aankomst.perYear[2025].pct, null, 'en zonder beginwaarde is er geen percentage te geven');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft een positie die vanaf nul is opgebouwd wél een resultaat en géén percentage', async () => {
|
|||
|
|
const tweede = rekening('Beurs');
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2024-07-01', 30000);
|
|||
|
|
verplaats({ sats: 100_000_000, at: '2024-07-01T00:00:00', naar: tweede });
|
|||
|
|
koersPerBtc('2025-01-01', 45000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const aankomst = m.rows.find(r => r.account.id === tweede).perYear[2025];
|
|||
|
|
assert.equal(aankomst.pct, null, 'er was geen beginwaarde om tegen af te meten');
|
|||
|
|
assert.equal(aankomst.gain, 15000, 'maar de koersstijging ná de verplaatsing is een echt resultaat');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('trekt per RIJ een ontvangen gift van het resultaat af', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2024-07-01', 40000);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 0, at: '2024-07-01T00:00:00', priceOrigin: 'none' });
|
|||
|
|
koersPerBtc('2025-01-01', 50000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const cel = m.rows.find(r => r.account.id === acc).perYear[2025];
|
|||
|
|
assert.equal(cel.gain, 100000 - 30000 - 40000, 'alleen de koersgroei, niet de gift zelf');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('trekt per rij af wat uit een balans komt in plaats van uit een boeking', async () => {
|
|||
|
|
const spaar = rekening('Spaar');
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2023-06-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 30000);
|
|||
|
|
koersPerBtc('2025-01-01', 30000);
|
|||
|
|
balans({ op: spaar, sats: 100_000_000, at: '2023-12-01T00:00:00' });
|
|||
|
|
balans({ op: spaar, sats: 200_000_000, at: '2024-07-01T00:00:00' });
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const rij = m.rows.find(r => r.account.id === spaar);
|
|||
|
|
assert.equal(rij.perYear[2024].value, 30000, 'de stand op de peildatum komt uit de eerste balans');
|
|||
|
|
assert.equal(rij.perYear[2025].value, 60000, 'en verdubbelt door de tweede');
|
|||
|
|
assert.equal(rij.perYear[2025].pct, 0, 'maar dat is een meting en geen rendement');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('currentStanding', () => {
|
|||
|
|
it('is leeg zonder assets', async () => {
|
|||
|
|
harness.exports.run(`DELETE FROM Asset`);
|
|||
|
|
assert.deepEqual(await wealth.currentStanding(), { rows: [], total: 0, totalSats: 0, today: null });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft het resultaat van vandaag, zonder wat er op een beheerd account gebeurt', async () => {
|
|||
|
|
const gisteren = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt, wealthScope)
|
|||
|
|
VALUES ('Spaarpot','spaarpot','bank',0,0,1,NULL,'managed')`);
|
|||
|
|
const spaarpot = harness.exports.lastId();
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2024-01-01T00:00:00' });
|
|||
|
|
const nu = new Date().toISOString().slice(0, 19);
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', ?, 50000000, 20000, ?, 'manual', ?)`, [btc, nu, spaarpot, nu]);
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
koersPerBtc(gisteren, 40000);
|
|||
|
|
liveSpot = 44000;
|
|||
|
|
const s = await wealth.currentStanding();
|
|||
|
|
assert.equal(Math.round(s.today.total), 4000);
|
|||
|
|
assert.equal(Math.round(s.today.perAsset.get(btc)), 4000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een account zonder saldo weg', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES ('Leeg','leeg','bank',0,0,1,NULL)`);
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2024-01-01T00:00:00' });
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
const s = await wealth.currentStanding();
|
|||
|
|
assert.equal(s.rows.length, 1);
|
|||
|
|
assert.equal(s.total, 40000);
|
|||
|
|
assert.equal(s.totalSats, 100_000_000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('sorteert op waarde, grootste eerst', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES ('Tweede','tweede','vault',1,0,1,NULL)`);
|
|||
|
|
const acc2 = harness.exports.lastId();
|
|||
|
|
koop({ sats: 50_000_000, fiatPrice: 20000, at: '2024-01-01T00:00:00' });
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', '2024-01-02T00:00:00', 300000000, 20000, ?, 'manual', '2024-01-02T00:00:00')`,
|
|||
|
|
[btc, acc2]);
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
|
|||
|
|
const s = await wealth.currentStanding();
|
|||
|
|
assert.deepEqual(s.rows.map(r => r.account.name), ['Tweede', 'Kluis']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft een rij zonder koers een waarde van null maar telt het saldo mee', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2024-01-01T00:00:00' });
|
|||
|
|
const s = await wealth.currentStanding();
|
|||
|
|
assert.equal(s.rows[0].value, null, 'geen koershistorie → geen waarde');
|
|||
|
|
assert.equal(s.total, 0);
|
|||
|
|
assert.equal(s.totalSats, 100_000_000, 'de hoeveelheid is er wel');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('slaat een beheerd account over, zonder dat het in de restpost belandt', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt, wealthScope)
|
|||
|
|
VALUES ('Spaarpot','spaarpot','bank',0,0,1,NULL,'managed')`);
|
|||
|
|
const spaarpot = harness.exports.lastId();
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2024-01-01T00:00:00' });
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', '2024-01-02T00:00:00', 50000000, 20000, ?, 'manual', '2024-01-02T00:00:00')`,
|
|||
|
|
[btc, spaarpot]);
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
const s = await wealth.currentStanding();
|
|||
|
|
assert.deepEqual(s.rows.map(r => r.account.name), ['Kluis']);
|
|||
|
|
assert.equal(s.total, 40000);
|
|||
|
|
assert.equal(s.totalSats, 100_000_000);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('peilmoment: de jaargrens ligt op lokale middernacht, niet op UTC-middernacht', () => {
|
|||
|
|
const NET_VOOR = '2023-12-31T22:30:00Z';
|
|||
|
|
const NET_NA = '2023-12-31T23:30:00Z';
|
|||
|
|
|
|||
|
|
it('telt een aankoop van vlak vóór lokale middernacht nog mee in het oude jaar', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2022-06-01T00:00:00Z' });
|
|||
|
|
koop({ sats: 50_000_000, fiatPrice: 30000, at: NET_VOOR });
|
|||
|
|
koersPerBtc('2022-06-01', 20000);
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const rij = m.rows[0];
|
|||
|
|
assert.equal(rij.perYear[2024].sats, 150_000_000,
|
|||
|
|
'een aankoop om 23:30 lokaal hoort nog bij het jaar dat dan afloopt');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een aankoop van vlak ná lokale middernacht buiten het oude jaar', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: '2022-06-01T00:00:00Z' });
|
|||
|
|
koop({ sats: 50_000_000, fiatPrice: 30000, at: NET_NA });
|
|||
|
|
koersPerBtc('2022-06-01', 20000);
|
|||
|
|
koersPerBtc('2024-01-01', 40000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const rij = m.rows[0];
|
|||
|
|
assert.equal(rij.perYear[2024].sats, 100_000_000,
|
|||
|
|
'een aankoop om 00:30 lokaal valt in het nieuwe jaar; zou de grens op UTC-middernacht ' +
|
|||
|
|
'liggen, dan telde hij hier ten onrechte mee');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('buildWealthModel: met een balans als anker', () => {
|
|||
|
|
function balans({ sats, at }) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
|||
|
|
VALUES (?,?,?,?,1.0,?, 'manual')`, [btc, at.slice(0, 10), acc, sats, at]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
it('zet een rekening met alléén een balans in het model', async () => {
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 1}-01-01`, 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows.length, 1, 'er is een rij, zonder één gebeurtenis');
|
|||
|
|
assert.equal(m.years[0], HUIDIG_JAAR - 1, 'de peiljaren beginnen ná het jaar van de balans');
|
|||
|
|
assert.equal(m.rows[0].perYear[HUIDIG_JAAR - 1].sats, 100_000_000);
|
|||
|
|
assert.equal(m.rows[0].perYear[HUIDIG_JAAR - 1].value, 30000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat het anker de boekingen van vóór dat moment overrulen', async () => {
|
|||
|
|
koop({ sats: 40_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 2}-01-01T00:00:00` });
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 1}-01-01`, 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows[0].perYear[HUIDIG_JAAR - 1].sats, 100_000_000, 'de balans, niet de 0,4 BTC uit het grootboek');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt een boeking ná het anker er wél bij op', async () => {
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
koop({ sats: 50_000_000, fiatPrice: 25000, at: `${HUIDIG_JAAR - 2}-08-01T00:00:00` });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 1}-01-01`, 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows[0].perYear[HUIDIG_JAAR - 1].sats, 150_000_000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('gebruikt per peiljaar het anker dat op dát moment gold', async () => {
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 3}-06-30T00:00:00.000Z` });
|
|||
|
|
balans({ sats: 200_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 2}-01-01`, 30000);
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 1}-01-01`, 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows[0].perYear[HUIDIG_JAAR - 2].sats, 100_000_000, 'het oudste anker in het eerste jaar');
|
|||
|
|
assert.equal(m.rows[0].perYear[HUIDIG_JAAR - 1].sats, 200_000_000, 'en het nieuwste daarna');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een portefeuille-brede balans het model niet raken', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
|||
|
|
VALUES (?,?,NULL,?,1.0,?, 'manual')`,
|
|||
|
|
[btc, `${HUIDIG_JAAR - 2}-06-30`, 100_000_000, `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z`]);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.deepEqual(m, { years: [], rows: [], totals: null });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft de huidige stand van een rekening met alleen een balans', async () => {
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR}-01-02T00:00:00.000Z` });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR}-01-02`, 30000);
|
|||
|
|
const stand = await wealth.currentStanding();
|
|||
|
|
assert.equal(stand.totalSats, 100_000_000);
|
|||
|
|
assert.equal(stand.rows.length, 1);
|
|||
|
|
assert.equal(stand.rows[0].value, 30000);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('buildWealthModel: wat de boekingen niet verklaren', () => {
|
|||
|
|
function balans({ sats, at }) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
|||
|
|
VALUES (?,?,?,?,1.0,?, 'manual')`, [btc, at.slice(0, 10), acc, sats, at]);
|
|||
|
|
}
|
|||
|
|
function koersen() {
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 3}-01-01`, 30000);
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 2}-01-01`, 30000);
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 1}-01-01`, 40000);
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR}-01-01`, 40000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
it('houdt het op nul zolang er geen balans is', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 3}-06-01T00:00:00` });
|
|||
|
|
koersen();
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
for (const a of m.aanwas) assert.equal(a.unexplained, 0, `jaar ${a.year}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('zet de sprong van een balans náást het rendement in plaats van erin', async () => {
|
|||
|
|
koop({ sats: 40_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 3}-06-01T00:00:00` });
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
koersen();
|
|||
|
|
const jaar = aanwasRij(await wealth.buildWealthModel(), HUIDIG_JAAR - 2);
|
|||
|
|
assert.equal(jaar.unexplained, 24000, '0,6 BTC tegen de 40.000 van de peildatum aan het eind');
|
|||
|
|
assert.equal(jaar.deposits, 0, 'en het is géén storting geworden');
|
|||
|
|
assert.equal(jaar.result, 28000, 'het rendement staat er onveranderd naast, mét de sprong erin');
|
|||
|
|
assert.equal(jaar.begin, 12000, '0,4 BTC tegen 30.000');
|
|||
|
|
assert.equal(jaar.eind, 40000, '1 BTC tegen 40.000');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('meldt niets als de balans het grootboek bevestigt', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 3}-06-01T00:00:00` });
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
koersen();
|
|||
|
|
assert.equal(aanwasRij(await wealth.buildWealthModel(), HUIDIG_JAAR - 2).unexplained, 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rekent de sprong aan één jaar toe en niet ook aan het jaar erna', async () => {
|
|||
|
|
koop({ sats: 40_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 3}-06-01T00:00:00` });
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
koersen();
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(aanwasRij(m, HUIDIG_JAAR - 2).unexplained, 24000);
|
|||
|
|
assert.equal(aanwasRij(m, HUIDIG_JAAR - 1).unexplained, 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('noemt per rekening wie het veroorzaakt, met en zonder grootboek eronder', async () => {
|
|||
|
|
koop({ sats: 40_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 3}-06-01T00:00:00` });
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt)
|
|||
|
|
VALUES ('Node','node','lightning',1,0,1,NULL)`);
|
|||
|
|
const node = harness.exports.lastId();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
|||
|
|
VALUES (?,?,?,?,1.0,?, 'manual')`,
|
|||
|
|
[btc, `${HUIDIG_JAAR - 2}-06-30`, node, 5_000_000, `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z`]);
|
|||
|
|
koersen();
|
|||
|
|
const jaar = aanwasRij(await wealth.buildWealthModel(), HUIDIG_JAAR - 2);
|
|||
|
|
assert.equal(jaar.unexplained, 24000, '0,6 BTC uit de kluis, tegen de koers van 40.000');
|
|||
|
|
assert.equal(jaar.uitBalansen, 2000, '0,05 BTC uit de beginbalans van de node');
|
|||
|
|
assert.deepEqual(jaar.unexplainedRows, [
|
|||
|
|
{ name: 'Kluis', units: 60_000_000, value: 24000 },
|
|||
|
|
], 'alleen de rijen waar nog iets te doen valt');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een beginbalans niet als rendement gelden', async () => {
|
|||
|
|
koop({ sats: 40_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 3}-06-01T00:00:00` });
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt)
|
|||
|
|
VALUES ('Node','node','lightning',1,0,1,NULL)`);
|
|||
|
|
const node = harness.exports.lastId();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
|||
|
|
VALUES (?,?,?,?,1.0,?, 'manual')`,
|
|||
|
|
[btc, `${HUIDIG_JAAR - 2}-06-30`, node, 5_000_000, `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z`]);
|
|||
|
|
koersen();
|
|||
|
|
const jaar = aanwasRij(await wealth.buildWealthModel(), HUIDIG_JAAR - 2);
|
|||
|
|
assert.equal(jaar.begin, 12000, '0,4 BTC tegen 30.000');
|
|||
|
|
assert.equal(jaar.eind, 18000, '0,45 BTC tegen 40.000');
|
|||
|
|
assert.equal(jaar.uitBalansen, 2000, 'de beginbalans van de node');
|
|||
|
|
assert.equal(jaar.result, 4000, 'alleen de koerswinst op wat er al stond');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt een aanvaard verschil net zo goed uit het rendement', async () => {
|
|||
|
|
koop({ sats: 40_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 3}-06-01T00:00:00` });
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
|||
|
|
VALUES (?,?,?,?,1.0,?, 'manual')`,
|
|||
|
|
[btc, `${HUIDIG_JAAR - 2}-06-30`, acc, 45_000_000, `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z`]);
|
|||
|
|
koersen();
|
|||
|
|
const voor = aanwasRij(await wealth.buildWealthModel(), HUIDIG_JAAR - 2);
|
|||
|
|
assert.equal(voor.unexplained, 2000, 'vooraf: 0,05 BTC die nergens vandaan komt');
|
|||
|
|
assert.equal(voor.result, 6000, 'en die zit in het rendement');
|
|||
|
|
|
|||
|
|
harness.exports.run(`UPDATE Valuation SET gapAcceptedAt = ? WHERE accountId = ?`,
|
|||
|
|
[`${HUIDIG_JAAR - 2}-07-01T00:00:00.000Z`, acc]);
|
|||
|
|
const na = aanwasRij(await wealth.buildWealthModel(), HUIDIG_JAAR - 2);
|
|||
|
|
assert.equal(na.unexplained, 0, 'erna: niets meer onverklaard');
|
|||
|
|
assert.equal(na.uitBalansen, 2000, 'het staat nu bij de stromen');
|
|||
|
|
assert.equal(na.result, 4000, 'en het rendement is weer alleen de koerswinst');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('houdt de uitsplitsing leeg als er niets onverklaard is', async () => {
|
|||
|
|
koop({ sats: 100_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 3}-06-01T00:00:00` });
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
koersen();
|
|||
|
|
for (const a of (await wealth.buildWealthModel()).aanwas) assert.deepEqual(a.unexplainedRows, [], `jaar ${a.year}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een rekening buiten box 3 ook buiten de uitsplitsing', async () => {
|
|||
|
|
koop({ sats: 40_000_000, fiatPrice: 20000, at: `${HUIDIG_JAAR - 3}-06-01T00:00:00` });
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt, taxScope)
|
|||
|
|
VALUES ('Pensioenpotje','pensioenpotje','bank',1,0,1,NULL,'none')`);
|
|||
|
|
const pensioen = harness.exports.lastId();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
|||
|
|
VALUES (?,?,?,?,1.0,?, 'manual')`,
|
|||
|
|
[btc, `${HUIDIG_JAAR - 2}-06-30`, pensioen, 5_000_000, `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z`]);
|
|||
|
|
koersen();
|
|||
|
|
const jaar = aanwasRij(await wealth.buildWealthModel(), HUIDIG_JAAR - 2);
|
|||
|
|
assert.deepEqual(jaar.unexplainedRows, []);
|
|||
|
|
assert.equal(jaar.unexplained, 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rapporteert een sprong van vóór de eerste peildatum niet, en dat hoort zo', async () => {
|
|||
|
|
balans({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-30T00:00:00.000Z` });
|
|||
|
|
koersen();
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.years[0], HUIDIG_JAAR - 1, 'de reeks begint ná het jaar van de balans');
|
|||
|
|
for (const a of m.aanwas) assert.equal(a.unexplained, 0, `jaar ${a.year}`);
|
|||
|
|
assert.equal(m.rows[0].perYear[HUIDIG_JAAR - 1].sats, 100_000_000, 'de stand klopt wél vanaf dag één');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('een rekening buiten het belaste vermogen', () => {
|
|||
|
|
function pensioenrekening() {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt, taxScope)
|
|||
|
|
VALUES ('Pensioenpotje','pensioenpotje','bank',0,0,1,NULL,'none')`);
|
|||
|
|
return harness.exports.lastId();
|
|||
|
|
}
|
|||
|
|
function koop({ sats, at, acc }) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', ?, ?, 20000, ?, 'manual', ?)`, [btc, at, sats, acc, at]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
it('laat de rekening uit de cijfers, en noemt hem bij naam', async () => {
|
|||
|
|
const pensioen = pensioenrekening();
|
|||
|
|
koop({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-01T00:00:00`, acc });
|
|||
|
|
koop({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-01T00:00:00`, acc: pensioen });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 1}-01-01`, 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.deepEqual(m.rows.map(r => r.account.name), ['Kluis'], 'de pensioenrij staat niet in de tabel');
|
|||
|
|
assert.equal(m.totals.perYear[HUIDIG_JAAR - 1], 30000, 'en telt dus niet in het totaal mee');
|
|||
|
|
assert.deepEqual(m.buitenBox3.map(x => x.name), ['Pensioenpotje'],
|
|||
|
|
'maar hij verdwijnt niet stil: de pagina kan hem noemen');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('houdt de uitsplitsing per categorie compleet', async () => {
|
|||
|
|
const pensioen = pensioenrekening();
|
|||
|
|
koop({ sats: 100_000_000, at: `${HUIDIG_JAAR - 2}-06-01T00:00:00`, acc });
|
|||
|
|
koop({ sats: 50_000_000, at: `${HUIDIG_JAAR - 2}-06-01T00:00:00`, acc: pensioen });
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 1}-01-01`, 30000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(jaarRij(m, HUIDIG_JAAR - 1).categorieCompleet, true);
|
|||
|
|
assert.deepEqual(Object.keys(jaarRij(m, HUIDIG_JAAR - 1).perCategorie), ['overig']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een categorie op het asset zelf ook werken, en de rekening gaat vóór', async () => {
|
|||
|
|
assert.equal(categorieVoor({ taxCategory: 'bank' }, { kind: 'crypto' }), 'bank', 'rekening');
|
|||
|
|
assert.equal(categorieVoor({}, { kind: 'crypto', taxCategory: 'bank' }), 'bank', 'asset');
|
|||
|
|
assert.equal(categorieVoor({ taxCategory: 'overig' }, { kind: 'fiat', taxCategory: 'bank' }), 'overig',
|
|||
|
|
'de rekening gaat vóór het asset');
|
|||
|
|
assert.equal(categorieVoor({}, { kind: 'fiat' }), 'bank', 'en zonder override de asset-soort');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('volgt voor de reikwijdte dezelfde volgorde: rekening, asset, dan overal', async () => {
|
|||
|
|
assert.equal(reikwijdteVoor({ taxScope: 'none' }, { taxScope: 'category' }), 'none', 'de rekening gaat vóór');
|
|||
|
|
assert.equal(reikwijdteVoor({ taxScope: null }, { taxScope: 'category' }), 'category', 'dan het asset');
|
|||
|
|
assert.equal(reikwijdteVoor({}, {}), 'all', 'en zonder keuze telt het overal');
|
|||
|
|
assert.equal(reikwijdteVoor(null, null), 'all');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt een banktegoed gewoon mee in de jaarcijfers', async () => {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
|
|||
|
|
VALUES ('EUR','Euro','euro','fiat','EUR','cent',2,'unit')`);
|
|||
|
|
const eur = harness.exports.lastId();
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
|||
|
|
VALUES (?, 'interest', ?, 500000, NULL, ?, 'manual', ?)`,
|
|||
|
|
[eur, `${HUIDIG_JAAR - 3}-06-01T00:00:00`, acc, `${HUIDIG_JAAR - 3}-06-01T00:00:00`]);
|
|||
|
|
for (const jaar of [HUIDIG_JAAR - 2, HUIDIG_JAAR - 1, HUIDIG_JAAR]) {
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO PriceHistory (assetId, observedAt, resolution, fiatPrice, providerId, updatedAt)
|
|||
|
|
VALUES (?, ?, 'day', 1, ?, ?)`,
|
|||
|
|
[eur, `${jaar}-01-01T00:00:00Z`, testProvider(), `${jaar}-01-01T00:00:00Z`]);
|
|||
|
|
}
|
|||
|
|
const jaar = aanwasRij(await wealth.buildWealthModel(), HUIDIG_JAAR - 2);
|
|||
|
|
assert.equal(jaar.begin, 5000, 'de beginwaarde kent het banktegoed');
|
|||
|
|
assert.equal(jaar.eind, 5000, 'en de eindwaarde ook');
|
|||
|
|
assert.equal(jaar.result, 0, 'dus geen rendement, en geen gat');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('heeft dezelfde categorieën als de registry in shared/config.js', () => {
|
|||
|
|
assert.deepEqual([...Object.values(VERMOGENSCATEGORIE)].sort(), [...ENUMS.TAX_CATEGORY].sort());
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('de stand van nu, langs twee wegen', () => {
|
|||
|
|
const somPerAsset = async () => (await import('../../server/data/positions.js'))
|
|||
|
|
.positionsByAsset().reduce((s, r) => s + r.net, 0);
|
|||
|
|
const koopZonderRekening = (sats, at) => harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, source, createdAt)
|
|||
|
|
VALUES (?, 'buy', ?, ?, 30000, 'manual', ?)`, [btc, at, sats, at]);
|
|||
|
|
|
|||
|
|
it('geeft hetzelfde getal als elke boeking een rekening noemt', async () => {
|
|||
|
|
koop({ sats: 100_000, fiatPrice: 30_000, at: `${HUIDIG_JAAR - 1}-03-01T00:00:00` });
|
|||
|
|
koop({ sats: 40_000, fiatPrice: 40_000, at: `${HUIDIG_JAAR}-01-15T00:00:00` });
|
|||
|
|
verkoop({ sats: 25_000, fiatPrice: 50_000, at: `${HUIDIG_JAAR}-02-01T00:00:00` });
|
|||
|
|
const stand = await wealth.currentStanding();
|
|||
|
|
assert.equal(stand.totalSats, 115_000);
|
|||
|
|
assert.equal(stand.totalSats, await somPerAsset(), 'beide pagina\'s tellen dezelfde eenheden');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft ook hetzelfde getal als een boeking GEEN rekening noemt', async () => {
|
|||
|
|
koop({ sats: 100_000, fiatPrice: 30_000, at: `${HUIDIG_JAAR}-01-15T00:00:00` });
|
|||
|
|
koopZonderRekening(50_000, `${HUIDIG_JAAR}-02-01T00:00:00`);
|
|||
|
|
const stand = await wealth.currentStanding();
|
|||
|
|
assert.equal(await somPerAsset(), 150_000);
|
|||
|
|
assert.equal(stand.totalSats, 150_000, 'de rest telt mee in plaats van weg te vallen');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('zet die rest op een eigen rij, herkenbaar en zonder rekening-id', async () => {
|
|||
|
|
koop({ sats: 100_000, fiatPrice: 30_000, at: `${HUIDIG_JAAR}-01-15T00:00:00` });
|
|||
|
|
koopZonderRekening(50_000, `${HUIDIG_JAAR}-02-01T00:00:00`);
|
|||
|
|
const rest = (await wealth.currentStanding()).rows.find(r => r.account.isUnassigned);
|
|||
|
|
assert.ok(rest, 'er is een rij zonder rekening');
|
|||
|
|
assert.equal(rest.sats, 50_000);
|
|||
|
|
assert.equal(rest.account.id, null, 'geen id, zodat een opzoeking hard faalt in plaats van stil');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt de rest ook mee in het jaarmodel, in de kolom van zijn eigen jaar', async () => {
|
|||
|
|
koop({ sats: 100_000, fiatPrice: 30_000, at: `${HUIDIG_JAAR - 2}-03-01T00:00:00` });
|
|||
|
|
koopZonderRekening(50_000, `${HUIDIG_JAAR}-06-01T00:00:00`);
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR - 2}-01-01`, 30_000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
const restRij = m.rows.find(r => r.account.isUnassigned);
|
|||
|
|
assert.ok(restRij, 'de restrij staat in het model');
|
|||
|
|
assert.equal(restRij.perYear[HUIDIG_JAAR].sats, 0, 'op 1 januari bestond hij nog niet');
|
|||
|
|
assert.equal(restRij.current.sats, 50_000, 'en nu wel');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('telt ook een NEGATIEVE rest mee, want die kant bestaat net zo goed', async () => {
|
|||
|
|
koop({ sats: 100_000, fiatPrice: 30_000, at: `${HUIDIG_JAAR}-01-15T00:00:00` });
|
|||
|
|
harness.exports.run(
|
|||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, source, createdAt)
|
|||
|
|
VALUES (?, 'sell', ?, 40000, 30000, 'manual', ?)`,
|
|||
|
|
[btc, `${HUIDIG_JAAR}-02-01T00:00:00`, `${HUIDIG_JAAR}-02-01T00:00:00`]);
|
|||
|
|
|
|||
|
|
const stand = await wealth.currentStanding();
|
|||
|
|
assert.equal(await somPerAsset(), 60_000, 'per asset is de verkoop eraf');
|
|||
|
|
assert.equal(stand.totalSats, 60_000, 'en de stand volgt, in plaats van op 100.000 te blijven staan');
|
|||
|
|
assert.equal(stand.rows.find(r => r.account.isUnassigned).sats, -40_000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat de rest de categorie van het ASSET volgen, want er is geen rekening', async () => {
|
|||
|
|
koopZonderRekening(50_000, `${HUIDIG_JAAR}-06-01T00:00:00`);
|
|||
|
|
const restRij = (await wealth.buildWealthModel()).rows.find(r => r.account.isUnassigned);
|
|||
|
|
assert.equal(restRij.categorie, VERMOGENSCATEGORIE.OVERIG, 'bitcoin is een overige bezitting');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('houdt de rest BUITEN box 3 als het asset daar staat, net als bij een echte rekening', async () => {
|
|||
|
|
harness.exports.run(`UPDATE Asset SET taxScope = 'none' WHERE id = ?`, [btc]);
|
|||
|
|
koopZonderRekening(50_000, `${HUIDIG_JAAR}-06-01T00:00:00`);
|
|||
|
|
koersPerBtc(`${HUIDIG_JAAR}-01-01`, 30_000);
|
|||
|
|
const m = await wealth.buildWealthModel();
|
|||
|
|
assert.equal(m.rows.find(r => r.account.isUnassigned), undefined,
|
|||
|
|
'de restrij zit niet in de fiscale rijen');
|
|||
|
|
assert.equal(m.buitenBox3.length, 1, 'maar hij wordt wél bij naam genoemd');
|
|||
|
|
assert.match(m.buitenBox3[0].name, /Zonder account/);
|
|||
|
|
});
|
|||
|
|
});
|