// Generated, do not edit. sha256:3f3763318dbbed19b2856eb5ad9116f8f3871486bcca510f894e39694929a1e0 import { describe, it, beforeEach, mock } from 'node:test'; import { toSearchName } from '../../shared/search-name.js'; import assert from 'node:assert/strict'; import { createDbHarness } from '../helpers/db-node.mjs'; const harness = createDbHarness(); mock.module('../../server/db.js', { namedExports: harness.exports }); const { createEvent, eventsCountByAccount, eventsNetByAccount, eventsWithoutImportRef, findEventByRef, firstTaxEventDate, listAssetTrades, listEvents, listFlowLegs, listTradeEvents, listTrades, listTransfers, updateEvent } = await import('../../server/data/events.js'); const { listInterest, listInterestForAccount, scheduleLabel } = await import('../../server/data/interest-schedules.js'); const { anchorBefore, listOpenValuationDiffs, listValuations, valuationDiff, valuationDiffs, valuationGap, valuationReconcilable } = await import('../../server/data/valuations.js'); let btc, eth, accA, accB; beforeEach(() => { harness.recreate(); btc = asset('BTC'); eth = asset('ETH'); accA = account('A'); accB = account('B'); }); function asset(symbol) { harness.exports.run( `INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES (?,?,?,'crypto',?,NULL,8,'unit')`, [symbol, symbol, toSearchName(symbol), symbol]); return harness.exports.lastId(); } function account(naam) { harness.exports.run( `INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES (?,?,'wallet',1,0,1,NULL)`, [naam, toSearchName(naam)]); return harness.exports.lastId(); } function event({ type = 'buy', assetId = null, from = null, to = null, amount = 1000, at = '2024-01-01T00:00:00', fiatPrice = null, fiatFee = null, unitFee = null, ref = null, source = 'manual' } = {}) { harness.exports.run( `INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, fiatFee, unitFee, fromAccountId, toAccountId, source, importReference, createdAt) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, [assetId ?? btc, type, at, amount, fiatPrice ?? (type === 'buy' || type === 'sell' ? 30000 : null), fiatFee, unitFee, from, to, source, ref, at]); return harness.exports.lastId(); } function waardering({ acc, onDate, balance, assetId = null, rateAt = null }) { harness.exports.run( `INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source) VALUES (?,?,?,?,?,?, 'manual')`, [assetId ?? btc, onDate, acc, balance, 50000, rateAt ?? `${onDate}T00:00:00`]); return harness.exports.lastId(); } describe('eventsNetByAccount', () => { const net = (acc = accA, assetId = btc, until = '2030-01-01T00:00:00') => eventsNetByAccount(acc, assetId, until); it('is 0 zonder events', () => { assert.equal(net(), 0); }); it('telt een aankoop op het ontvangende account op', () => { event({ type: 'buy', to: accA, amount: 1000 }); assert.equal(net(), 1000); }); it('trekt een verkoop van het afgevende account af', () => { event({ type: 'sell', from: accA, amount: 400 }); assert.equal(net(), -400); }); it('trekt bij een verkoop ook de fee af', () => { event({ type: 'sell', from: accA, amount: 400, unitFee: 3 }); assert.equal(net(), -403); }); it('telt rente op het ontvangende account op', () => { event({ type: 'interest', to: accA, amount: 25 }); assert.equal(net(), 25); }); it('telt een inkomende transfer vól mee, zonder fee-aftrek', () => { event({ type: 'transfer', from: accB, to: accA, amount: 1000, unitFee: 20 }); assert.equal(net(), 1000); }); it('trekt bij een uitgaande transfer het bedrag én de fee af', () => { event({ type: 'transfer', from: accA, to: accB, amount: 1000, unitFee: 20 }); assert.equal(net(), -1020); }); it('rekent een transfer naar hetzelfde account als alleen de fee', () => { event({ type: 'transfer', from: accA, to: accA, amount: 1000, unitFee: 20 }); assert.equal(net(), -20); }); it('behandelt een ontbrekende fee als 0', () => { event({ type: 'transfer', from: accA, to: accB, amount: 1000, unitFee: null }); assert.equal(net(), -1000); }); it('negeert events van een ander asset', () => { event({ type: 'buy', to: accA, amount: 1000, assetId: eth }); assert.equal(net(accA, btc), 0); assert.equal(net(accA, eth), 1000); }); it('negeert events van een ander account', () => { event({ type: 'buy', to: accB, amount: 1000 }); assert.equal(net(accA), 0); }); it('neemt het peilmoment zelf mee (t/m, niet tot)', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-03-01T12:00:00' }); assert.equal(net(accA, btc, '2024-03-01T12:00:00'), 1000, 'gelijk aan het peilmoment telt mee'); assert.equal(net(accA, btc, '2024-03-01T11:59:59'), 0, 'daarvóór nog niet'); }); it('telt zonder peilmoment alles mee', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2030-06-01T00:00:00' }); assert.equal(eventsNetByAccount(accA, btc, null), 1000); }); it('somt een reeks correct op', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); event({ type: 'interest', to: accA, amount: 50, at: '2024-02-01T00:00:00' }); event({ type: 'transfer', from: accA, to: accB, amount: 300, unitFee: 10, at: '2024-03-01T00:00:00' }); event({ type: 'sell', from: accA, amount: 200, at: '2024-04-01T00:00:00' }); assert.equal(net(), 1000 + 50 - 310 - 200); }); }); describe('listTradeEvents', () => { beforeEach(() => { event({ type: 'buy', to: accA, at: '2023-12-31T23:59:59', amount: 1 }); event({ type: 'buy', to: accA, at: '2024-01-01T00:00:00', amount: 2 }); event({ type: 'sell', from: accA, at: '2024-06-01T00:00:00', amount: 3 }); event({ type: 'buy', to: accA, at: '2024-12-31T23:59:59', amount: 4 }); event({ type: 'buy', to: accA, at: '2025-01-01T00:00:00', amount: 5 }); event({ type: 'transfer', from: accA, to: accB, at: '2024-05-01T00:00:00', amount: 99 }); event({ type: 'interest', to: accA, at: '2024-05-02T00:00:00', amount: 98 }); }); const bedragen = (van, tot) => listTradeEvents(van, tot).map(e => e.amount).sort((a, b) => a - b); it('neemt alleen buy en sell mee', () => { assert.deepEqual(bedragen(null, '2030-01-01T00:00:00'), [1, 2, 3, 4, 5]); }); it('sluit de ondergrens uit en de bovengrens in', () => { assert.deepEqual(bedragen('2024-01-01T00:00:00', '2024-12-31T23:59:59'), [3, 4], 'de aankoop exact op de ondergrens hoort bij het vorige jaar'); }); it('betekent van=null: vanaf het begin', () => { assert.deepEqual(bedragen(null, '2024-01-01T00:00:00'), [1, 2]); }); it('geeft niets terug voor een leeg venster', () => { assert.deepEqual(bedragen('2024-07-01T00:00:00', '2024-07-31T00:00:00'), []); }); it('neemt een verplaatsing tussen twee eigen accounts alleen mee als hij een euro-fee draagt', () => { event({ type: 'transfer', from: accA, to: accB, at: '2024-08-01T00:00:00', amount: 97, fiatFee: 2 }); event({ type: 'transfer', from: accA, to: accB, at: '2024-08-02T00:00:00', amount: 96, fiatFee: 0 }); assert.deepEqual(bedragen('2024-07-31T00:00:00', '2024-08-31T00:00:00'), [97]); }); it('neemt elke eenzijdige verplaatsing mee, naar buiten en van buiten', () => { event({ type: 'transfer', from: accA, to: null, at: '2024-08-03T00:00:00', amount: 95 }); event({ type: 'transfer', from: null, to: accB, at: '2024-08-04T00:00:00', amount: 94 }); assert.deepEqual(bedragen('2024-07-31T00:00:00', '2024-08-31T00:00:00'), [94, 95]); }); it('levert de velden die de aanwasberekening nodig heeft', () => { const e = listTradeEvents('2024-05-31T00:00:00', '2024-06-02T00:00:00')[0]; assert.deepEqual(Object.keys(e).sort(), ['amount', 'assetId', 'decimals', 'fiatFee', 'fiatPrice', 'fromAccountId', 'occurredAt', 'priceOrigin', 'toAccountId', 'type']); assert.equal(e.decimals, 8, 'de decimals van het asset komen mee voor de omrekening'); }); }); describe('listFlowLegs', () => { const benen = (van, tot) => listFlowLegs(van, tot) .map(l => `${l.accountId}:${l.type}:${l.direction}:${l.units}`).sort(); it('geeft een aankoop als één been op de ontvangende rekening', () => { event({ type: 'buy', to: accA, amount: 100, fiatPrice: 20000, at: '2024-02-01T00:00:00' }); assert.deepEqual(benen(null, '2025-01-01T00:00:00'), [`${accA}:buy:in:100`]); }); it('geeft een verkoop als één been op de leverende rekening', () => { event({ type: 'sell', from: accA, amount: 100, fiatPrice: 20000, at: '2024-02-01T00:00:00' }); assert.deepEqual(benen(null, '2025-01-01T00:00:00'), [`${accA}:sell:out:100`]); }); it('geeft een verplaatsing als TWEE benen, één per rekening', () => { event({ type: 'transfer', from: accA, to: accB, amount: 300, at: '2024-03-01T00:00:00' }); assert.deepEqual(benen(null, '2025-01-01T00:00:00'), [`${accA}:transfer:out:300`, `${accB}:transfer:in:300`].sort()); }); it('laat de unitFee van een verplaatsing BUITEN de stroom', () => { event({ type: 'transfer', from: accA, to: accB, amount: 300, unitFee: 10, at: '2024-03-01T00:00:00' }); const uit = listFlowLegs(null, '2025-01-01T00:00:00').find(l => l.direction === 'out'); assert.equal(uit.units, 300, 'de fee zit niet in de stroom'); }); it('laat rente helemaal weg', () => { event({ type: 'interest', to: accA, amount: 50, at: '2024-02-01T00:00:00' }); assert.deepEqual(benen(null, '2025-01-01T00:00:00'), []); }); it('geeft bij een verplaatsing naar zichzelf twee benen die elkaar opheffen', () => { event({ type: 'transfer', from: accA, to: accA, amount: 300, at: '2024-03-01T00:00:00' }); assert.deepEqual(benen(null, '2025-01-01T00:00:00'), [`${accA}:transfer:in:300`, `${accA}:transfer:out:300`].sort()); }); it('houdt één been over bij een verplaatsing van of naar buiten', () => { event({ type: 'transfer', from: accA, to: null, amount: 300, at: '2024-03-01T00:00:00' }); assert.deepEqual(benen(null, '2025-01-01T00:00:00'), [`${accA}:transfer:out:300`]); }); it('scheidt de assets', () => { event({ type: 'buy', to: accA, assetId: eth, amount: 7, fiatPrice: 3000, at: '2024-02-01T00:00:00' }); const l = listFlowLegs(null, '2025-01-01T00:00:00')[0]; assert.equal(l.assetId, eth); }); it('sluit de ondergrens uit en de bovengrens in', () => { event({ type: 'buy', to: accA, amount: 1, fiatPrice: 1, at: '2024-01-01T00:00:00' }); event({ type: 'buy', to: accA, amount: 2, fiatPrice: 1, at: '2024-12-31T23:59:59' }); assert.deepEqual(benen('2024-01-01T00:00:00', '2024-12-31T23:59:59'), [`${accA}:buy:in:2`]); }); it('levert de velden die de waardering nodig heeft', () => { event({ type: 'buy', to: accA, amount: 100, fiatPrice: 20000, fiatFee: 5, at: '2024-02-01T00:00:00' }); const l = listFlowLegs(null, '2025-01-01T00:00:00')[0]; assert.deepEqual(Object.keys(l).sort(), ['accountId', 'assetId', 'decimals', 'direction', 'fiatFee', 'fiatPrice', 'occurredAt', 'priceOrigin', 'type', 'units']); assert.equal(l.decimals, 8, 'de decimals komen mee voor de omrekening'); assert.equal(l.occurredAt, '2024-02-01T00:00:00', 'de dag komt mee, want een verplaatsing wordt op de koers van die dag gewaardeerd'); }); }); describe('firstTaxEventDate', () => { it('geeft null zonder events', () => { assert.equal(firstTaxEventDate(), null); }); it('geeft de vroegste gebeurtenis, niet de eerst ingevoerde', () => { event({ at: '2024-06-01T00:00:00' }); event({ at: '2022-03-15T00:00:00' }); event({ at: '2023-01-01T00:00:00' }); assert.equal(firstTaxEventDate(), '2022-03-15T00:00:00'); }); }); describe('event-lijsten', () => { it('sorteert listEvents nieuwste eerst', () => { const oud = event({ at: '2024-01-01T00:00:00' }); const nieuw = event({ at: '2024-06-01T00:00:00' }); assert.deepEqual(listEvents().map(e => e.id), [nieuw, oud]); }); it('splitst trades en transfers', () => { const koop = event({ type: 'buy', to: accA }); const verkoop = event({ type: 'sell', from: accA }); const verplaatsing = event({ type: 'transfer', from: accA, to: accB }); event({ type: 'interest', to: accA }); assert.deepEqual(listTrades().map(e => e.id).sort((a, b) => a - b), [koop, verkoop]); assert.deepEqual(listTransfers().map(e => e.id), [verplaatsing]); }); it('geeft alleen de aan- en verkopen van het gevraagde asset', () => { const koop = event({ type: 'buy', to: accA, at: '2024-03-01T00:00:00' }); const verkoop = event({ type: 'sell', from: accA, at: '2024-05-01T00:00:00' }); event({ type: 'transfer', from: accA, to: accB, at: '2024-04-01T00:00:00' }); event({ type: 'interest', to: accA, at: '2024-04-15T00:00:00' }); event({ type: 'buy', assetId: eth, to: accA, at: '2024-02-01T00:00:00' }); assert.deepEqual(listAssetTrades(btc).map(e => e.id), [koop, verkoop]); }); it('sorteert oplopend, want hij staat naast een grafiek die van links naar rechts loopt', () => { const laat = event({ type: 'buy', to: accA, at: '2024-06-01T00:00:00' }); const vroeg = event({ type: 'buy', to: accA, at: '2024-01-01T00:00:00' }); assert.deepEqual(listAssetTrades(btc).map(e => e.id), [vroeg, laat]); }); it('geeft niets terug voor een asset zonder handel', () => { event({ type: 'buy', to: accA }); assert.deepEqual(listAssetTrades(eth), []); }); it('draagt de rekening als id én als naam, want de dialoog groepeert op het id', () => { event({ type: 'buy', to: accA }); event({ type: 'sell', from: accB }); const [koop, verkoop] = listAssetTrades(btc); assert.equal(koop.toAccountId, accA); assert.equal(koop.fromAccountId, null, 'een aankoop komt ergens BINNEN, dus alleen de to-kant'); assert.equal(koop.to_name, 'A'); assert.equal(verkoop.fromAccountId, accB); assert.equal(verkoop.toAccountId, null); assert.equal(verkoop.from_name, 'B'); }); function label(naam) { harness.exports.run(`INSERT INTO Label (name, searchName, createdAt) VALUES (?,?,?)`, [naam, toSearchName(naam), '2024-01-01T00:00:00']); return harness.exports.lastId(); } function beweging({ labelId = null, txid = 'tx-1' } = {}) { harness.exports.run( `INSERT INTO WalletTransaction (accountId, layer, txid, direction, valueSats, labelId, blockHeight, blockTime, createdAt) VALUES (?,'onchain',?,'in',?,?,?,?,?)`, [accA, txid, 100_000, labelId, 840_000, '2024-03-01T00:00:00', '2024-03-02T00:00:00']); return harness.exports.lastId(); } const koppel = (txId, eventId) => harness.exports.run( `INSERT INTO WalletTransactionEventLink (walletTransactionId, eventId, method, createdAt) VALUES (?,?, 'manual', '2024-03-02T00:00:00')`, [txId, eventId]); it('draagt het label van de gekoppelde beweging mee', () => { const ev = event({ type: 'buy', to: accA, at: '2024-03-01T00:00:00' }); koppel(beweging({ labelId: label('Van Jan') }), ev); assert.equal(listAssetTrades(btc)[0].label_names, 'Van Jan'); }); it('laat het label leeg als er geen beweging of geen label is', () => { const zonderKoppeling = event({ type: 'buy', to: accA, at: '2024-03-01T00:00:00' }); const zonderLabel = event({ type: 'buy', to: accA, at: '2024-04-01T00:00:00' }); koppel(beweging({ txid: 'tx-2' }), zonderLabel); const rijen = listAssetTrades(btc); assert.equal(rijen.length, 2, 'beide aankopen staan er, met en zonder koppeling'); assert.equal(rijen[0].label_names, null, 'geen koppeling, geen label'); assert.equal(rijen[1].label_names, null, 'wel een beweging, maar die draagt geen label'); assert.equal(rijen[0].id, zonderKoppeling); }); it('HOUDT ÉÉN RIJ bij twee gekoppelde bewegingen, met beide labels', () => { const ev = event({ type: 'buy', to: accA, at: '2024-03-01T00:00:00' }); koppel(beweging({ txid: 'tx-1', labelId: label('Van Jan') }), ev); koppel(beweging({ txid: 'tx-2', labelId: label('Van Piet') }), ev); const rijen = listAssetTrades(btc); assert.equal(rijen.length, 1, 'één aankoop blijft één rij, wat er ook aan hangt'); assert.deepEqual(rijen[0].label_names.split(',').sort(), ['Van Jan', 'Van Piet']); }); it('verrijkt met asset- en accountnamen', () => { event({ type: 'transfer', from: accA, to: accB }); const t = listTransfers()[0]; assert.equal(t.asset_symbol, 'BTC'); assert.equal(t.from_name, 'A'); assert.equal(t.to_name, 'B'); assert.equal(t.asset_decimals, 8); }); it('geeft rente per account op basis van de bestemming', () => { event({ type: 'interest', to: accA, amount: 10 }); event({ type: 'interest', to: accB, amount: 20 }); assert.deepEqual(listInterestForAccount(accA).map(e => e.amount), [10]); }); it('vindt een event op import-referentie', () => { const id = event({ ref: 'bitvavo-42' }); assert.equal(findEventByRef('bitvavo-42').id, id); assert.equal(findEventByRef('bestaat-niet'), null); assert.equal(findEventByRef(null), null, 'een lege referentie matcht nooit'); }); it('levert de pool zonder import-referentie voor de fuzzy match', () => { const handmatig = event({ ref: null }); event({ ref: 'uit-import' }); assert.deepEqual(eventsWithoutImportRef().map(e => e.id), [handmatig]); }); }); describe('listInterest: attributie aan een afspraak', () => { function afspraak({ acc = accA, assetId = null, van, tot = null, rate = 0.05, type = 'fixed' }) { harness.exports.run( `INSERT INTO InterestSchedule (accountId, rateKind, principalAssetId, payoutAssetId, principalAmount, annualRate, startsOn, endsOn, creditCadence, compounding, createdAt) VALUES (?,?,?,?,?,?,?,?,?,0,?)`, [acc, type, type === 'fixed' ? (assetId ?? btc) : null, assetId ?? btc, type === 'fixed' ? 100000 : null, type === 'fixed' ? rate : null, van, tot, type === 'fixed' ? 'term_end' : null, '2024-01-01T00:00:00']); return harness.exports.lastId(); } it('koppelt een rente-event aan de afspraak van dat account en asset', () => { const s = afspraak({ van: '2024-01-01', tot: '2024-12-31' }); event({ type: 'interest', to: accA, at: '2024-06-01T00:00:00' }); assert.equal(listInterest()[0].scheduleId, s); }); it('koppelt niet buiten het datumvenster', () => { afspraak({ van: '2024-01-01', tot: '2024-05-31' }); event({ type: 'interest', to: accA, at: '2024-06-01T00:00:00' }); assert.equal(listInterest()[0].scheduleId, null); }); it('koppelt niet aan een afspraak van een ander account of asset', () => { afspraak({ acc: accB, van: '2024-01-01', tot: '2024-12-31' }); afspraak({ acc: accA, assetId: eth, van: '2024-01-01', tot: '2024-12-31' }); event({ type: 'interest', to: accA, at: '2024-06-01T00:00:00' }); assert.equal(listInterest()[0].scheduleId, null); }); it('laat bij overlap de afspraak met de laatste startdatum winnen', () => { afspraak({ van: '2024-01-01', tot: '2024-12-31' }); const nieuwer = afspraak({ van: '2024-03-01', tot: '2024-12-31' }); event({ type: 'interest', to: accA, at: '2024-06-01T00:00:00' }); assert.equal(listInterest()[0].scheduleId, nieuwer, 'de meest recente afspraak overschrijft de oudere'); }); it('koppelt aan een lopende afspraak zonder einddatum', () => { const s = afspraak({ van: '2024-01-01', tot: null, type: 'variable' }); event({ type: 'interest', to: accA, at: '2029-06-01T00:00:00' }); assert.equal(listInterest()[0].scheduleId, s); }); }); describe('scheduleLabel', () => { const basis = { account_name: 'Spaarrekening', payout_symbol: 'BTC', rateKind: 'fixed', annualRate: 0.05, startsOn: '2024-01-01', endsOn: '2024-12-31' }; it('zet accountnaam, asset, rente en periode achter elkaar', () => { assert.equal(scheduleLabel(basis), 'Spaarrekening · BTC · 5% · 2024-01-01–2024-12-31'); }); it('schrijft een variabele afspraak als "variabel"', () => { assert.equal(scheduleLabel({ ...basis, rateKind: 'variable' }), 'Spaarrekening · BTC · variabel · 2024-01-01–2024-12-31'); }); it('schrijft een lopende afspraak als "heden"', () => { assert.ok(scheduleLabel({ ...basis, endsOn: null }).endsWith('2024-01-01–heden')); }); it('haalt de nullen van een rond percentage weg', () => { assert.equal(scheduleLabel({ ...basis, annualRate: 0.0725 }).split(' · ')[2], '7.25%'); }); }); describe('Valuation', () => { it('sorteert listValuations op peildatum, nieuwste eerst', () => { const oud = waardering({ acc: accA, onDate: '2024-01-01', balance: 1 }); const nieuw = waardering({ acc: accA, onDate: '2024-06-01', balance: 2 }); assert.deepEqual(listValuations().map(v => v.id), [nieuw, oud]); }); it('verrijkt met account- en assetnaam', () => { waardering({ acc: accA, onDate: '2024-01-01', balance: 1 }); const v = listValuations()[0]; assert.equal(v.account_name, 'A'); assert.equal(v.asset_symbol, 'BTC'); }); it('berekent het verschil als saldo min grootboek', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); waardering({ acc: accA, onDate: '2024-02-01', balance: 1200 }); assert.equal(valuationDiff(listValuations()[0]), 200); }); it('geeft null als de waardering geen account heeft', () => { waardering({ acc: null, onDate: '2024-02-01', balance: 1200 }); assert.equal(valuationDiff(listValuations()[0]), null); }); it('kijkt alleen naar events t/m het rateAt-moment', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); event({ type: 'buy', to: accA, amount: 5000, at: '2024-09-01T00:00:00' }); waardering({ acc: accA, onDate: '2024-02-01', balance: 1000 }); assert.equal(valuationDiff(listValuations()[0]), 0, 'de latere aankoop valt buiten het venster van deze waardering'); }); it('listOpenValuationDiffs laat reconciled waarderingen weg', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); waardering({ acc: accA, onDate: '2024-02-01', balance: 1000 }); waardering({ acc: accA, onDate: '2024-03-01', balance: 1500 }); const open = listOpenValuationDiffs(); assert.equal(open.length, 1); assert.equal(open[0].diff, 500); }); it('listOpenValuationDiffs laat waarderingen zonder account weg', () => { waardering({ acc: null, onDate: '2024-02-01', balance: 1200 }); assert.deepEqual(listOpenValuationDiffs(), [], 'diff is null en null is geen openstaand verschil'); }); it('ziet een negatief verschil ook als openstaand', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); waardering({ acc: accA, onDate: '2024-02-01', balance: 800 }); assert.equal(listOpenValuationDiffs()[0].diff, -200); }); it('is niet verzoenbaar zonder boekingen vóór het peilmoment', () => { waardering({ acc: accA, onDate: '2024-02-01', balance: 1200 }); assert.equal(valuationReconcilable(listValuations()[0]), false); assert.equal(valuationDiff(listValuations()[0]), 1200, 'het verschil bestaat nog wél als getal'); assert.deepEqual(listOpenValuationDiffs(), [], 'maar het is geen werk'); }); it('is wél verzoenbaar met een boeking vóór het peilmoment', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); waardering({ acc: accA, onDate: '2024-02-01', balance: 1200 }); assert.equal(valuationReconcilable(listValuations()[0]), true); assert.equal(listOpenValuationDiffs().length, 1); }); it('kijkt voor de verzoenbaarheid naar hetzelfde venster als het verschil', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-09-01T00:00:00' }); waardering({ acc: accA, onDate: '2024-02-01', balance: 1200 }); assert.equal(valuationReconcilable(listValuations()[0]), false); }); it('is niet verzoenbaar zonder account', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); waardering({ acc: null, onDate: '2024-02-01', balance: 1200 }); assert.equal(valuationReconcilable(listValuations()[0]), false); }); it('telt een grootboek dat op nul uitkomt tóch als grootboek', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); event({ type: 'sell', from: accA, amount: 1000, at: '2024-01-15T00:00:00' }); waardering({ acc: accA, onDate: '2024-02-01', balance: 1200 }); assert.equal(eventsNetByAccount(accA, btc, '2024-02-01T00:00:00'), 0, 'de som is nul'); assert.equal(valuationReconcilable(listValuations()[0]), true, 'en er is tóch een grootboek'); assert.equal(listOpenValuationDiffs().length, 1); }); it('geeft per waardering het verschil én de verzoenbaarheid in één lijst', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); const met = waardering({ acc: accA, onDate: '2024-02-01', balance: 1200 }); const zonder = waardering({ acc: accA, onDate: '2023-06-01', balance: 500 }); const perId = new Map(valuationDiffs().map(s => [s.id, s])); assert.deepEqual(perId.get(met), { id: met, diff: 200, gap: -300, reconcilable: true, accepted: false, future: false }); assert.deepEqual(perId.get(zonder), { id: zonder, diff: 500, gap: 500, reconcilable: false, accepted: false, future: false }); }); it('haalt een AANVAARD verschil uit de werklijst en zegt het in de kolom', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); const id = waardering({ acc: accA, onDate: '2024-02-01', balance: 1200 }); assert.equal(listOpenValuationDiffs().length, 1, 'vooraf staat hij op de werklijst'); harness.exports.run(`UPDATE Valuation SET gapAcceptedAt = '2024-03-01T00:00:00Z' WHERE id = ?`, [id]); assert.deepEqual(listOpenValuationDiffs(), [], 'erna niet meer'); assert.equal(valuationDiffs().find(s => s.id === id).accepted, true, 'maar de kolom weet het wel'); assert.equal(valuationDiffs().find(s => s.id === id).gap, 200, 'en het bedrag blijft berekend'); }); }); describe('eventsCountByAccount', () => { it('telt de boekingen van dit account voor dit asset', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); event({ type: 'transfer', from: accA, to: accB, amount: 400, at: '2024-02-01T00:00:00' }); assert.equal(eventsCountByAccount(accA, btc, null), 2, 'in- én uitgaand tellen mee'); assert.equal(eventsCountByAccount(accB, btc, null), 1); }); it('houdt zich aan het peilmoment', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); event({ type: 'buy', to: accA, amount: 1000, at: '2024-09-01T00:00:00' }); assert.equal(eventsCountByAccount(accA, btc, '2024-06-01T00:00:00'), 1); }); it('telt niets voor een ander asset of een boeking zonder account', () => { harness.exports.run( `INSERT INTO Asset (symbol, name, searchName, kind, unit, decimals, displayUnit) VALUES ('XMR','Monero','monero','crypto','XMR',8,'unit')`); const ander = harness.exports.lastId(); event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); assert.equal(eventsCountByAccount(accA, ander, null), 0, 'ander asset'); harness.exports.run( `INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, source, createdAt) VALUES (?, 'buy', '2024-03-01T00:00:00', 500, 50000, 'manual', '2024-03-01T00:00:00')`, [btc]); assert.equal(eventsCountByAccount(accA, btc, null), 1, 'een boeking zonder rekening hangt hier niet aan'); }); }); describe('waarderingen met een peildatum die nog moet komen', () => { const morgen = new Date(Date.now() + 36 * 3600 * 1000).toISOString(); it('meldt géén openstaand verschil, ook al wijkt het van het grootboek af', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); waardering({ acc: accA, onDate: morgen.slice(0, 10), balance: 5000, rateAt: morgen }); assert.deepEqual(listOpenValuationDiffs(), []); }); it('markeert hem als toekomstig en rekent het verschil wél uit', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); const id = waardering({ acc: accA, onDate: morgen.slice(0, 10), balance: 5000, rateAt: morgen }); const s = valuationDiffs().find(x => x.id === id); assert.deepEqual(s, { id, diff: 4000, gap: 4000, reconcilable: true, accepted: false, future: true }); }); it('gaat wél als werk gelden zodra het peilmoment voorbij is', () => { const gisteren = new Date(Date.now() - 36 * 3600 * 1000).toISOString(); event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); waardering({ acc: accA, onDate: gisteren.slice(0, 10), balance: 5000, rateAt: gisteren }); assert.equal(listOpenValuationDiffs().length, 1); }); }); describe('valuationGap', () => { it('is gelijk aan valuationDiff zolang er geen eerdere balans is', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); waardering({ acc: accA, onDate: '2024-02-01', balance: 1200 }); const v = listValuations()[0]; assert.equal(valuationGap(v), valuationDiff(v)); assert.equal(valuationGap(v), 200); }); it('meet vanaf de vorige balans en niet vanaf het begin van het grootboek', () => { waardering({ acc: accA, onDate: '2024-01-01', balance: 1000 }); event({ type: 'interest', to: accA, amount: 50, at: '2024-02-01T00:00:00' }); const nieuw = waardering({ acc: accA, onDate: '2024-03-01', balance: 800 }); const v = listValuations().find(x => x.id === nieuw); assert.equal(valuationGap(v), -250, 'wat er sinds de vorige meting ongeboekt uit is gegaan'); assert.equal(valuationDiff(v), 750, 'het oude getal, en dat is hier onbruikbaar'); }); it('laat een ongeboekt verschil niet bij élke volgende balans terugkomen', () => { event({ type: 'buy', to: accA, amount: 1000, at: '2024-01-01T00:00:00' }); waardering({ acc: accA, onDate: '2024-02-01', balance: 1100 }); event({ type: 'buy', to: accA, amount: 500, at: '2024-03-01T00:00:00' }); const tweede = waardering({ acc: accA, onDate: '2024-04-01', balance: 1600 }); const v = listValuations().find(x => x.id === tweede); assert.equal(valuationGap(v), 0, 'de tweede meting klopt met de boeking ertussen'); assert.equal(valuationDiff(v), 100, 'terwijl het oude getal die 100 blijft herhalen'); }); it('kijkt naar de vorige balans van dezelfde rekening en hetzelfde asset', () => { waardering({ acc: accA, onDate: '2024-01-01', balance: 1000 }); waardering({ acc: accB, onDate: '2024-02-01', balance: 7777 }); waardering({ acc: accA, onDate: '2024-02-01', balance: 1000, assetId: eth }); const v = listValuations().find(x => x.accountId === accA && x.assetId === btc); assert.equal(anchorBefore(accA, btc, v.rateAt), null, 'er is er geen eerdere voor dit paar'); assert.equal(valuationGap(v), 1000); }); it('geeft null zonder account', () => { waardering({ acc: null, onDate: '2024-02-01', balance: 1200 }); assert.equal(valuationGap(listValuations()[0]), null); }); }); describe('Event.priceOrigin', () => { const payload = (extra = {}) => ({ assetId: btc, type: 'buy', occurredAt: '2026-05-01T10:00:00.000Z', amount: 100000, fiatPrice: 50000, source: 'manual', toAccountId: accA, ...extra, }); const rij = id => listEvents().find(e => e.id === id); it('bewaart de herkomst en geeft hem terug op de leesweg', async () => { const id = await createEvent(payload({ priceOrigin: 'market_day' })); assert.equal(rij(id).priceOrigin, 'market_day'); assert.equal(listTrades().find(e => e.id === id).priceOrigin, 'market_day', 'ook via de join, want daar leest de pagina hem'); }); it('laat de herkomst NULL als niemand hem meegeeft, en dat betekent onbekend', async () => { const id = await createEvent(payload()); assert.equal(rij(id).priceOrigin, null); }); it('maakt van een leeg veld NULL en niet van een lege string', async () => { const id = await createEvent(payload({ priceOrigin: '' })); assert.equal(rij(id).priceOrigin, null); }); it('weigert een herkomst die de enum niet kent', async () => { await assert.rejects(() => createEvent(payload({ priceOrigin: 'guessed' })), /Invalid priceOrigin/); assert.deepEqual(listEvents(), [], 'en er staat niets, dus de guard zit voor de INSERT'); }); it('LAAT DE HERKOMST STAAN bij een bewerking die er niet over gaat', async () => { const id = await createEvent(payload({ priceOrigin: 'market_day' })); await updateEvent(id, payload({ fiatFee: 12 })); assert.equal(rij(id).priceOrigin, 'market_day', 'een bewerking elders mag dit niet wissen'); assert.equal(rij(id).fiatFee, 12, 'en de bewerking zelf is wel doorgevoerd'); }); it('wist hem WEL als de aanroeper dat expliciet zegt', async () => { const id = await createEvent(payload({ priceOrigin: 'paid' })); await updateEvent(id, payload({ priceOrigin: null })); assert.equal(rij(id).priceOrigin, null); }); });