Files

148 lines
7.7 KiB
JavaScript
Raw Permalink Normal View History

2026-10-29 11:57:29 +01:00
// Generated, do not edit. sha256:605d840e94e42af14d150c5b1095887b6d46b752b6e0dc0407b3f4a209ee7b66
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert/strict';
import { createDbHarness } from '../helpers/db-node.mjs';
import { toSearchName } from '../../shared/search-name.js';
import { isOwnWealth } from '../../shared/wealth-scope.js';
const harness = createDbHarness();
mock.module('../../server/db.js', { namedExports: harness.exports });
const { assetPositions } = await import('../../server/data/events.js');
const { positionsByAsset } = await import('../../server/data/positions.js');
const { latestAnchors } = await import('../../server/data/valuations.js');
const { metalHoldings } = await import('../../server/data/metals.js');
const { run, lastId } = harness.exports;
const PEIL = '2026-12-31T00:00:00.000Z';
const rijVan = (rijen, id) => rijen.find(r => r.id === id);
const eigen = (assetId) => rijVan(positionsByAsset(PEIL, { ownOnly: true }), assetId);
const alles = (assetId) => rijVan(positionsByAsset(PEIL), assetId);
let btc, xau, munt, kluis, spaarpot, pensioen;
beforeEach(() => {
harness.recreate();
btc = asset('BTC', 'Bitcoin', 'crypto', 8);
xau = asset('XAU', 'Goud', 'metal', 4);
run(`INSERT INTO MetalProduct (assetId, name, searchName, form, fineGrams) VALUES (?, 'Krugerrand', 'krugerrand', 'coin', 311035)`, [xau]);
munt = lastId();
kluis = account('Kluis', null);
spaarpot = account('Spaarpot kind', 'managed');
pensioen = account('Pensioen', 'none');
});
function asset(symbol, naam, kind, decimals) {
run(`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
VALUES (?,?,?,?,?,NULL,?,'unit')`, [symbol, naam, toSearchName(naam), kind, symbol, decimals]);
return lastId();
}
function account(naam, wealthScope) {
run(`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt, wealthScope)
VALUES (?,?,'bank',1,0,1,NULL,?)`, [naam, toSearchName(naam), wealthScope]);
return lastId();
}
function event({ assetId = null, type = 'buy', to = null, from = null, amount = 1000, unitFee = null,
priceOrigin = null, productId = null, productCount = null, at = '2026-03-01T12:00:00.000Z' } = {}) {
const prijs = type === 'buy' || type === 'sell' ? 50000 : null;
run(`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, fiatFee, unitFee, productId, productCount,
fromAccountId, toAccountId, source, priceOrigin, createdAt)
VALUES (?,?,?,?,?,?,?,?,?,?,?,'manual',?,?)`,
[assetId ?? btc, type, at, amount, prijs, type === 'buy' ? 10 : null, unitFee, productId, productCount,
from, to, priceOrigin, at]);
return lastId();
}
function balans({ assetId = null, acc, balance, at = '2026-06-30T00:00:00.000Z' }) {
run(`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
VALUES (?,?,?,?,1.0,?, 'manual')`, [assetId ?? btc, at.slice(0, 10), acc, balance, at]);
return lastId();
}
describe('isOwnWealth', () => {
it('is waar zonder account en zonder keuze, onwaar bij beheerd en bij telt niet mee', () => {
assert.equal(isOwnWealth(null), true, 'wat aan geen account hangt is van niemand anders');
assert.equal(isOwnWealth({ wealthScope: null }), true);
assert.equal(isOwnWealth({ wealthScope: 'managed' }), false);
assert.equal(isOwnWealth({ wealthScope: 'none' }), false);
});
});
describe('ownOnly zonder afwijking', () => {
it('geeft dezelfde rijen als zonder de optie, ook met een gift, een fee en een zelf-transfer', () => {
event({ to: kluis, amount: 100_000 });
event({ to: null, amount: 20_000, at: '2026-03-02T00:00:00.000Z' });
event({ type: 'transfer', from: kluis, to: null, amount: 5_000, unitFee: 300, at: '2026-04-01T00:00:00.000Z' });
event({ type: 'transfer', from: null, to: kluis, amount: 7_000, priceOrigin: 'none', at: '2026-04-02T00:00:00.000Z' });
event({ type: 'transfer', from: kluis, to: kluis, amount: 1_000, unitFee: 50, at: '2026-04-03T00:00:00.000Z' });
event({ type: 'sell', from: kluis, amount: 10_000, at: '2026-05-01T00:00:00.000Z' });
event({ assetId: xau, to: kluis, amount: 622_070, productId: munt, productCount: 2 });
balans({ acc: kluis, balance: 90_000 });
assert.deepEqual(assetPositions(PEIL, { ownOnly: true }), assetPositions(PEIL));
assert.deepEqual(positionsByAsset(PEIL, { ownOnly: true }), positionsByAsset(PEIL));
assert.deepEqual(latestAnchors(PEIL, { ownOnly: true }), latestAnchors(PEIL));
assert.deepEqual(metalHoldings({ ownOnly: true }), metalHoldings());
});
});
describe('ownOnly over de grens van het eigen vermogen', () => {
it('telt een aankoop op een beheerd account niet, ook niet in de aankoopcijfers', () => {
event({ to: kluis, amount: 100_000 });
event({ to: spaarpot, amount: 40_000 });
assert.equal(alles(btc).net, 140_000);
assert.equal(alles(btc).buy_events, 2);
const r = eigen(btc);
assert.equal(r.net, 100_000);
assert.equal(r.buy_events, 1);
assert.equal(r.buy_amount, 100_000);
assert.equal(r.buy_fee_total, 10, 'alleen de fee van de eigen aankoop');
});
it('telt een overboeking naar een spaarpot als onttrekking, met de fee', () => {
event({ to: kluis, amount: 100_000 });
event({ type: 'transfer', from: kluis, to: spaarpot, amount: 30_000, unitFee: 200, at: '2026-04-01T00:00:00.000Z' });
assert.equal(alles(btc).net, 99_800, 'in het geheel valt alleen de fee weg');
assert.equal(eigen(btc).net, 69_800, 'het eigen vermogen ziet het geld vertrekken');
assert.equal(eigen(btc).gift_amount, 0, 'een overboeking met een prijsherkomst is geen gift');
});
it('telt een overboeking terug als storting', () => {
event({ to: spaarpot, amount: 50_000 });
event({ type: 'transfer', from: spaarpot, to: kluis, amount: 20_000, at: '2026-04-01T00:00:00.000Z' });
assert.equal(eigen(btc).net, 20_000);
assert.equal(alles(btc).net, 50_000);
});
it('negeert een balans op een beheerd account, en houdt die op een eigen account', () => {
event({ to: kluis, amount: 100_000 });
event({ to: spaarpot, amount: 40_000 });
balans({ acc: spaarpot, balance: 45_000 });
balans({ acc: kluis, balance: 101_000 });
assert.equal(alles(btc).net, 146_000);
assert.equal(eigen(btc).net, 101_000, 'de spaarpot verschuift het eigen vermogen niet');
assert.deepEqual(latestAnchors(PEIL, { ownOnly: true }).map(a => a.accountId), [kluis]);
});
it('houdt een boeking zonder account bij het eigen vermogen', () => {
event({ to: null, amount: 25_000 });
assert.equal(eigen(btc).net, 25_000);
});
it('behandelt telt niet mee net als beheerd', () => {
event({ to: kluis, amount: 100_000 });
event({ to: pensioen, amount: 60_000 });
event({ type: 'transfer', from: kluis, to: pensioen, amount: 10_000, at: '2026-04-01T00:00:00.000Z' });
assert.equal(eigen(btc).net, 90_000);
assert.equal(alles(btc).net, 160_000);
});
it('telt munten die naar een spaarpot gaan niet meer bij het eigen vermogen', () => {
event({ assetId: xau, to: kluis, amount: 933_105, productId: munt, productCount: 3 });
event({ assetId: xau, to: spaarpot, amount: 311_035, productId: munt, productCount: 1 });
event({ assetId: xau, type: 'transfer', from: kluis, to: spaarpot, amount: 311_035, productId: munt,
productCount: 1, at: '2026-04-01T00:00:00.000Z' });
assert.equal(metalHoldings()[0].net_count, 4, 'het geheel: vier gekocht, een verplaatsing telt niet');
const eigenMunt = metalHoldings({ ownOnly: true })[0];
assert.equal(eigenMunt.net_count, 2, 'drie gekocht, één naar de spaarpot');
assert.equal(eigenMunt.buy_events, 1, 'de aankoop op de spaarpot telt niet mee');
});
});