215 lines
10 KiB
JavaScript
215 lines
10 KiB
JavaScript
// Generated, do not edit. sha256:a2c33f19cc145348f36ba31f41736c0be16d371654b28747c636f79229822fe6
|
|||
|
|
import { describe, it, beforeEach, mock } from 'node:test';
|
||
|
|
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 {
|
||
|
|
listAccounts, listSelectableAccounts, deactivateCheck, deactivateAccount, reactivateAccount,
|
||
|
|
} = await import('../../server/data/accounts.js');
|
||
|
|
const { eventsNetByAccount } = await import('../../server/data/events.js');
|
||
|
|
|
||
|
|
let btc, kluis, tijdelijk;
|
||
|
|
beforeEach(() => {
|
||
|
|
harness.recreate();
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit)
|
||
|
|
VALUES ('BTC','Bitcoin','bitcoin','crypto','BTC','sat',8,'subunit')`);
|
||
|
|
btc = harness.exports.lastId();
|
||
|
|
kluis = account('Kluis');
|
||
|
|
tijdelijk = account('Tijdelijk');
|
||
|
|
});
|
||
|
|
|
||
|
|
function account(naam, { scanEnabled = 0 } = {}) {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt)
|
||
|
|
VALUES (?,?, 'wallet', 1, ?, 1, NULL)`, [naam, naam.toLowerCase(), scanEnabled]);
|
||
|
|
return harness.exports.lastId();
|
||
|
|
}
|
||
|
|
function koop(accountId, sats, at = '2024-05-01T10:00:00Z') {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
||
|
|
VALUES (?, 'buy', ?, ?, 50000, ?, 'manual', ?)`, [btc, at, sats, accountId, at]);
|
||
|
|
}
|
||
|
|
const rij = id => harness.exports.one(`SELECT * FROM Account WHERE id = ?`, [id]);
|
||
|
|
|
||
|
|
describe('non-actief: uit de keuzelijst, maar niet uit de stand', () => {
|
||
|
|
it('blijft volledig meetellen in het grootboek', async () => {
|
||
|
|
koop(tijdelijk, 100_000);
|
||
|
|
const voor = eventsNetByAccount(tijdelijk, btc, '2030-01-01T00:00:00Z');
|
||
|
|
await deactivateAccount(tijdelijk, '2024-06-01T00:00:00Z');
|
||
|
|
assert.equal(eventsNetByAccount(tijdelijk, btc, '2030-01-01T00:00:00Z'), voor,
|
||
|
|
'deactiveren is géén verwijderen: het saldo verandert niet');
|
||
|
|
assert.equal(voor, 100_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('blijft in listAccounts staan, want die voedt de totalen', () => {
|
||
|
|
harness.exports.run(`UPDATE Account SET isActive = 0, closedAt = '2024-06-01T00:00:00Z' WHERE id = ?`, [tijdelijk]);
|
||
|
|
assert.deepEqual(listAccounts().map(a => a.name).sort(), ['Kluis', 'Tijdelijk']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('verdwijnt wél uit de keuzelijst', () => {
|
||
|
|
harness.exports.run(`UPDATE Account SET isActive = 0, closedAt = '2024-06-01T00:00:00Z' WHERE id = ?`, [tijdelijk]);
|
||
|
|
assert.deepEqual(listSelectableAccounts().map(a => a.name), ['Kluis']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('komt met de expliciete opt-in tóch mee, actieve eerst', () => {
|
||
|
|
harness.exports.run(`UPDATE Account SET isActive = 0, closedAt = '2024-06-01T00:00:00Z' WHERE id = ?`, [tijdelijk]);
|
||
|
|
assert.deepEqual(listSelectableAccounts({ metGesloten: true }).map(a => a.name), ['Kluis', 'Tijdelijk']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('houdt de huidige waarde van een bewerkformulier in de lijst', () => {
|
||
|
|
harness.exports.run(`UPDATE Account SET isActive = 0, closedAt = '2024-06-01T00:00:00Z' WHERE id = ?`, [tijdelijk]);
|
||
|
|
assert.deepEqual(listSelectableAccounts({ ookId: tijdelijk }).map(a => a.name), ['Kluis', 'Tijdelijk']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat de andere non-actieve accounts wél weg bij ookId', () => {
|
||
|
|
const derde = account('Derde');
|
||
|
|
harness.exports.run(`UPDATE Account SET isActive = 0, closedAt = '2024-06-01T00:00:00Z' WHERE id IN (?,?)`, [tijdelijk, derde]);
|
||
|
|
assert.deepEqual(listSelectableAccounts({ ookId: tijdelijk }).map(a => a.name), ['Kluis', 'Tijdelijk']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('verandert niets als ookId leeg of onbekend is', () => {
|
||
|
|
harness.exports.run(`UPDATE Account SET isActive = 0, closedAt = '2024-06-01T00:00:00Z' WHERE id = ?`, [tijdelijk]);
|
||
|
|
for (const opt of [{}, { ookId: null }, { ookId: undefined }, { ookId: 0 }, { ookId: 9999 }]) {
|
||
|
|
assert.deepEqual(listSelectableAccounts(opt).map(a => a.name), ['Kluis'], JSON.stringify(opt));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('deactivateCheck', () => {
|
||
|
|
const soorten = chk => chk.findings.map(f => f.kind).sort();
|
||
|
|
|
||
|
|
it('meldt niets bij een leeg, ongebruikt account', () => {
|
||
|
|
const chk = deactivateCheck(kluis);
|
||
|
|
assert.equal(chk.found, true);
|
||
|
|
assert.deepEqual(chk.findings, []);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('geeft found:false voor een onbekend account', () => {
|
||
|
|
assert.deepEqual(deactivateCheck(9999), { found: false, findings: [] });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('meldt dat het scannen aanstaat', () => {
|
||
|
|
const a = account('Scanner', { scanEnabled: 1 });
|
||
|
|
assert.deepEqual(soorten(deactivateCheck(a)), ['scanEnabled']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('meldt wallet-bronnen', () => {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO WalletSource (accountId, kind, value, name, createdAt)
|
||
|
|
VALUES (?, 'xpub', 'zpub', 'Ledger', '2024-01-01T00:00:00Z')`, [kluis]);
|
||
|
|
assert.deepEqual(soorten(deactivateCheck(kluis)), ['walletSources']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('meldt een rente-afspraak zonder einddatum', () => {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO InterestSchedule (accountId, payoutAssetId, rateKind, startsOn, compounding, createdAt)
|
||
|
|
VALUES (?,?, 'variable', '2024-01-01', 0, '2024-01-01T00:00:00Z')`, [kluis, btc]);
|
||
|
|
assert.deepEqual(soorten(deactivateCheck(kluis)), ['openSchedules']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('meldt een afspraak met een einddatum in de toekomst, maar niet één die al afliep', () => {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO InterestSchedule (accountId, payoutAssetId, rateKind, startsOn, endsOn, compounding, createdAt)
|
||
|
|
VALUES (?,?, 'variable', '2020-01-01', '2020-12-31', 0, '2020-01-01T00:00:00Z')`, [kluis, btc]);
|
||
|
|
assert.deepEqual(soorten(deactivateCheck(kluis)), [], 'een afgelopen afspraak is geen gevolg');
|
||
|
|
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO InterestSchedule (accountId, payoutAssetId, rateKind, startsOn, endsOn, compounding, createdAt)
|
||
|
|
VALUES (?,?, 'variable', '2024-01-01', '2099-12-31', 0, '2024-01-01T00:00:00Z')`, [kluis, btc]);
|
||
|
|
assert.deepEqual(soorten(deactivateCheck(kluis)), ['openSchedules']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('meldt openstaande reconcile-voorstellen, maar niet afgehandelde', () => {
|
||
|
|
const voorstel = status => harness.exports.run(
|
||
|
|
`INSERT INTO ReconcileProposal (accountId, assetId, amount, direction, suggestedType, status, detectedAt)
|
||
|
|
VALUES (?,?, 1000, 'in', 'transfer', ?, '2024-05-01T00:00:00Z')`, [kluis, btc, status]);
|
||
|
|
voorstel('ignored');
|
||
|
|
assert.deepEqual(soorten(deactivateCheck(kluis)), [], 'afgehandeld is geen gevolg');
|
||
|
|
voorstel('pending');
|
||
|
|
assert.deepEqual(soorten(deactivateCheck(kluis)), ['pendingProposals']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('meldt een saldo dat niet nul is, met het bedrag per asset', () => {
|
||
|
|
koop(kluis, 250_000);
|
||
|
|
const chk = deactivateCheck(kluis);
|
||
|
|
const saldo = chk.findings.find(f => f.kind === 'balance');
|
||
|
|
assert.ok(saldo, 'een saldo hoort een bevinding te zijn');
|
||
|
|
assert.equal(saldo.balances[0].saldo, 250_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('meldt een saldo van nul juist NIET', () => {
|
||
|
|
koop(kluis, 100_000);
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, fromAccountId, source, createdAt)
|
||
|
|
VALUES (?, 'sell', '2024-06-01T10:00:00Z', 100000, 50000, ?, 'manual', '2024-06-01T10:00:00Z')`,
|
||
|
|
[btc, kluis]);
|
||
|
|
const chk = deactivateCheck(kluis);
|
||
|
|
assert.equal(chk.findings.some(f => f.kind === 'balance'), false, 'leeg account, geen saldo-bevinding');
|
||
|
|
assert.equal(chk.findings.some(f => f.kind === 'events'), true, 'de historie wordt wél gemeld');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('meldt een saldo dat alléén uit een balans komt', () => {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
||
|
|
VALUES (?, '2026-06-30', ?, 123456, 1.0, '2026-06-30T00:00:00.000Z', 'manual')`, [btc, kluis]);
|
||
|
|
const chk = deactivateCheck(kluis);
|
||
|
|
const saldo = chk.findings.find(f => f.kind === 'balance');
|
||
|
|
assert.ok(saldo, 'een gemeten balans is net zo goed een saldo');
|
||
|
|
assert.deepEqual(saldo.balances, [{ assetId: btc, saldo: 123456 }]);
|
||
|
|
assert.equal(chk.findings.some(f => f.kind === 'events'), false, 'en er is geen historie');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('meldt de fee van een verplaatsing naar dezelfde rekening', () => {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, unitFee, fromAccountId, toAccountId, source, createdAt)
|
||
|
|
VALUES (?, 'transfer', '2024-06-01T10:00:00Z', 100000, 250, ?, ?, 'manual', '2024-06-01T10:00:00Z')`,
|
||
|
|
[btc, kluis, kluis]);
|
||
|
|
const saldo = deactivateCheck(kluis).findings.find(f => f.kind === 'balance');
|
||
|
|
assert.ok(saldo, 'de fee is een saldo');
|
||
|
|
assert.deepEqual(saldo.balances, [{ assetId: btc, saldo: -250 }]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('merkt de events-bevinding aan als informatief', () => {
|
||
|
|
koop(kluis, 100_000);
|
||
|
|
const ev = deactivateCheck(kluis).findings.find(f => f.kind === 'events');
|
||
|
|
assert.equal(ev.informational, true, 'de historie blijft en blijft meetellen: dat is geen waarschuwing');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('verandert niets aan de database', () => {
|
||
|
|
koop(kluis, 100_000);
|
||
|
|
const voor = rij(kluis);
|
||
|
|
deactivateCheck(kluis);
|
||
|
|
assert.deepEqual({ ...rij(kluis) }, { ...voor });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('deactiveren en heractiveren', () => {
|
||
|
|
it('zet de vlag, de datum én het scannen uit', async () => {
|
||
|
|
const a = account('Scanner', { scanEnabled: 1 });
|
||
|
|
await deactivateAccount(a, '2024-06-01T00:00:00Z');
|
||
|
|
const r = rij(a);
|
||
|
|
assert.equal(r.isActive, 0);
|
||
|
|
assert.equal(r.closedAt, '2024-06-01T00:00:00Z');
|
||
|
|
assert.equal(r.scanEnabled, 0, 'dat mag stil, want deactivateCheck heeft het gemeld');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('zet bij heractiveren het scannen NIET terug aan', async () => {
|
||
|
|
const a = account('Scanner', { scanEnabled: 1 });
|
||
|
|
await deactivateAccount(a, '2024-06-01T00:00:00Z');
|
||
|
|
await reactivateAccount(a);
|
||
|
|
const r = rij(a);
|
||
|
|
assert.equal(r.isActive, 1);
|
||
|
|
assert.equal(r.closedAt, null, 'de CHECK eist dat een actief account geen afsluitdatum heeft');
|
||
|
|
assert.equal(r.scanEnabled, 0, 'scannen aanzetten is een eigen, expliciete handeling');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('weigert een onbekend account in beide richtingen', async () => {
|
||
|
|
await assert.rejects(() => deactivateAccount(9999), (e) => e.key === 'accounts.unknownAccount');
|
||
|
|
await assert.rejects(() => reactivateAccount(9999), (e) => e.key === 'accounts.unknownAccount');
|
||
|
|
});
|
||
|
|
});
|