148 lines
7.7 KiB
JavaScript
148 lines
7.7 KiB
JavaScript
// 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');
|
||
|
|
});
|
||
|
|
});
|