// 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/); }); });