Files
AtreidesAssetsFree/tests/repo/events.test.mjs
T
2026-10-29 11:57:29 +01:00

706 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});
});