722 lines
33 KiB
JavaScript
722 lines
33 KiB
JavaScript
// Generated, do not edit. sha256:e9ccb4b4784d24c1c464248bf37dc50c0e76a1e76874e1d0edd11a9b9edf6572
|
|||
|
|
import { describe, it, beforeEach, mock } from 'node:test';
|
||
|
|
import { toSearchName } from '../../shared/search-name.js';
|
||
|
|
import { toUtcIso } from '../../shared/time.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 { createAsset } = await import('../../server/data/assets.js');
|
||
|
|
const { dueCreditsForAccount, realizedInterestSub, scheduleForecast, bookDueCredit, dismissDueCredit } = await import('../../server/data/interest-credits.js');
|
||
|
|
const { createSchedule, createPeriod, listPeriods, updatePeriod, deletePeriod,
|
||
|
|
listSchedulesForAccount, listInterest, realizedForSchedule } = await import('../../server/data/interest-schedules.js');
|
||
|
|
|
||
|
|
const SATS = 8;
|
||
|
|
const EEN_BTC = 100_000_000;
|
||
|
|
|
||
|
|
let ctx;
|
||
|
|
beforeEach(() => {
|
||
|
|
harness.recreate();
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES (?,?,?,?,?,?,?,?)`, ['BTC', 'Bitcoin', toSearchName('Bitcoin'), 'crypto', 'BTC', 'sat', SATS, 'subunit']);
|
||
|
|
const assetId = harness.exports.lastId();
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES (?,?,?,?,?,1,NULL)`, ['Rentekening', toSearchName('Rentekening'), 'bank', 0, 0]);
|
||
|
|
const accountId = harness.exports.lastId();
|
||
|
|
ctx = { assetId, accountId };
|
||
|
|
});
|
||
|
|
|
||
|
|
const afspraak = (extra) => ({
|
||
|
|
accountId: ctx.accountId,
|
||
|
|
rateKind: 'fixed',
|
||
|
|
principalAssetId: ctx.assetId,
|
||
|
|
payoutAssetId: ctx.assetId,
|
||
|
|
principalAmount: EEN_BTC,
|
||
|
|
principal_decimals: SATS,
|
||
|
|
payout_decimals: SATS,
|
||
|
|
annualRate: 0.05,
|
||
|
|
creditWeekday: null,
|
||
|
|
creditDayOfMonth: null,
|
||
|
|
...extra,
|
||
|
|
});
|
||
|
|
|
||
|
|
function renteEvent(dagIso, bedragSub) {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, toAccountId, source, createdAt)
|
||
|
|
VALUES (?,?,?,?,?,?,?)`,
|
||
|
|
[ctx.assetId, 'interest', `${dagIso}T00:00:00`, bedragSub, ctx.accountId, 'manual', '2026-01-01T00:00:00']);
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('crediteringsmomenten per cadans', () => {
|
||
|
|
const aantalPerioden = (extra) => {
|
||
|
|
const s = afspraak(extra);
|
||
|
|
return scheduleForecast(s, s.startsOn).remainingCount;
|
||
|
|
};
|
||
|
|
|
||
|
|
it('term_end levert één periode over de hele looptijd', () => {
|
||
|
|
assert.equal(aantalPerioden({
|
||
|
|
creditCadence: 'term_end', startsOn: '2024-01-01', endsOn: '2026-12-31',
|
||
|
|
}), 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('annual rekent per jaarultimo (1 januari van het volgende jaar)', () => {
|
||
|
|
assert.equal(aantalPerioden({
|
||
|
|
creditCadence: 'annual', startsOn: '2020-03-01', endsOn: '2023-01-01',
|
||
|
|
}), 3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('annual sluit af op de einddatum en niet op een ultimo erna', () => {
|
||
|
|
assert.equal(aantalPerioden({
|
||
|
|
creditCadence: 'annual', startsOn: '2020-03-01', endsOn: '2022-12-31',
|
||
|
|
}), 3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('schrijft het laatste stuk van een deposito bij op de einddatum', async () => {
|
||
|
|
await createSchedule(afspraak({
|
||
|
|
creditCadence: 'annual', annualRate: 0.028, startsOn: '2015-06-15', endsOn: '2019-06-15',
|
||
|
|
}));
|
||
|
|
const dues = dueCreditsForAccount(ctx.accountId);
|
||
|
|
const laatste = dues.at(-1);
|
||
|
|
assert.deepEqual([laatste.periodStart, laatste.periodEnd], ['2019-01-01', '2019-06-15']);
|
||
|
|
assert.equal(laatste.projected_sub, Math.round(EEN_BTC * 0.028 * 165 / 365));
|
||
|
|
const s = listSchedulesForAccount(ctx.accountId)[0];
|
||
|
|
const totaal = scheduleForecast(s, '2015-06-15').termTotalSub;
|
||
|
|
assert.ok(Math.abs(totaal - EEN_BTC * 0.028 * 1461 / 365) <= dues.length, `termijntotaal ${totaal}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('quarterly rekent per KALENDERkwartaal en niet per drie maanden vanaf de start', () => {
|
||
|
|
assert.equal(aantalPerioden({
|
||
|
|
creditCadence: 'quarterly', startsOn: '2024-02-15', endsOn: '2025-01-01',
|
||
|
|
}), 4);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('quarterly sluit af op de einddatum en niet op een kwartaal erna', () => {
|
||
|
|
assert.equal(aantalPerioden({
|
||
|
|
creditCadence: 'quarterly', startsOn: '2024-02-15', endsOn: '2024-09-30',
|
||
|
|
}), 3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('quarterly begint óp een kwartaalgrens zonder een periode van nul dagen', () => {
|
||
|
|
assert.equal(aantalPerioden({
|
||
|
|
creditCadence: 'quarterly', startsOn: '2024-01-01', endsOn: '2025-01-01',
|
||
|
|
}), 4);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('monthly slaat de startdag over en neemt de einddatum wél mee', () => {
|
||
|
|
assert.equal(aantalPerioden({
|
||
|
|
creditCadence: 'monthly', creditDayOfMonth: 15,
|
||
|
|
startsOn: '2024-01-15', endsOn: '2024-04-15',
|
||
|
|
}), 3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('weekly begint bij het eerste voorkomen strikt ná de start', () => {
|
||
|
|
assert.equal(aantalPerioden({
|
||
|
|
creditCadence: 'weekly', creditWeekday: 1,
|
||
|
|
startsOn: '2024-01-01', endsOn: '2024-01-29',
|
||
|
|
}), 4);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('loopt zonder einddatum door tot vandaag', () => {
|
||
|
|
const s = afspraak({ creditCadence: 'monthly', creditDayOfMonth: 1, startsOn: '2024-01-01', endsOn: null });
|
||
|
|
const tot = scheduleForecast(s, '2024-06-15');
|
||
|
|
assert.equal(tot.termTotalSub, null, 'geen termijntotaal zonder einddatum');
|
||
|
|
assert.equal(tot.remainingCount, null);
|
||
|
|
assert.equal(dueCreditsForAccount(ctx.accountId).length, 0, 'deze afspraak staat nog niet in de database');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('geeft zonder einddatum wél te-boeken bijschrijvingen', async () => {
|
||
|
|
await createSchedule(afspraak({
|
||
|
|
creditCadence: 'monthly', creditDayOfMonth: 1, startsOn: '2024-01-01', endsOn: null,
|
||
|
|
}));
|
||
|
|
const due = dueCreditsForAccount(ctx.accountId);
|
||
|
|
assert.ok(due.length >= 12, `verwacht minstens een jaar aan maandmomenten, kreeg ${due.length}`);
|
||
|
|
assert.ok(due.every(d => d.periodEnd <= new Date().toISOString().slice(0, 10)),
|
||
|
|
'geen crediteringsmoment dat nog moet komen');
|
||
|
|
assert.ok(due.every(d => d.periodEnd.endsWith('-01')), 'alleen maandmomenten, geen stuk tot vandaag');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('remainingCount telt alleen perioden ná de peildatum', () => {
|
||
|
|
const s = afspraak({
|
||
|
|
creditCadence: 'monthly', creditDayOfMonth: 15,
|
||
|
|
startsOn: '2024-01-15', endsOn: '2024-04-15',
|
||
|
|
});
|
||
|
|
assert.equal(scheduleForecast(s, '2024-01-15').remainingCount, 3);
|
||
|
|
assert.equal(scheduleForecast(s, '2024-02-15').remainingCount, 2);
|
||
|
|
assert.equal(scheduleForecast(s, '2024-04-15').remainingCount, 0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('crediteringsdatums (via de due-lijst)', () => {
|
||
|
|
const eersteDues = async (extra, n) => {
|
||
|
|
await createSchedule(afspraak({ endsOn: '2099-12-31', ...extra }));
|
||
|
|
return dueCreditsForAccount(ctx.accountId).slice(0, n);
|
||
|
|
};
|
||
|
|
|
||
|
|
it('klemt dag 31 op de laatste dag van de maand, inclusief schrikkeljaar', async () => {
|
||
|
|
const dues = await eersteDues({
|
||
|
|
creditCadence: 'monthly', creditDayOfMonth: 31, startsOn: '2024-01-31',
|
||
|
|
}, 3);
|
||
|
|
assert.deepEqual(dues.map(d => d.periodEnd), ['2024-02-29', '2024-03-31', '2024-04-30']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('klemt februari op 28 in een niet-schrikkeljaar', async () => {
|
||
|
|
const dues = await eersteDues({
|
||
|
|
creditCadence: 'monthly', creditDayOfMonth: 31, startsOn: '2023-01-31',
|
||
|
|
}, 2);
|
||
|
|
assert.deepEqual(dues.map(d => d.periodEnd), ['2023-02-28', '2023-03-31']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('perioden sluiten op elkaar aan en beginnen bij de startdatum', async () => {
|
||
|
|
const dues = await eersteDues({
|
||
|
|
creditCadence: 'monthly', creditDayOfMonth: 10, startsOn: '2024-05-10',
|
||
|
|
}, 4);
|
||
|
|
assert.equal(dues[0].periodStart, '2024-05-10');
|
||
|
|
for (let i = 1; i < dues.length; i++) {
|
||
|
|
assert.equal(dues[i].periodStart, dues[i - 1].periodEnd,
|
||
|
|
'een periode moet beginnen waar de vorige eindigde, anders valt er rente tussen');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('wekelijkse momenten liggen zeven dagen uit elkaar op de gekozen weekdag', async () => {
|
||
|
|
const dues = await eersteDues({
|
||
|
|
creditCadence: 'weekly', creditWeekday: 3, startsOn: '2024-01-01',
|
||
|
|
}, 3);
|
||
|
|
assert.deepEqual(dues.map(d => d.periodEnd), ['2024-01-03', '2024-01-10', '2024-01-17']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("geeft geen due's voor een variabele afspraak", async () => {
|
||
|
|
await createSchedule({
|
||
|
|
accountId: ctx.accountId, rateKind: 'variable', payoutAssetId: ctx.assetId,
|
||
|
|
principalAssetId: null, principalAmount: null, annualRate: null,
|
||
|
|
startsOn: '2024-01-01', endsOn: '2099-12-31',
|
||
|
|
creditCadence: null, creditWeekday: null, creditDayOfMonth: null,
|
||
|
|
});
|
||
|
|
assert.deepEqual(dueCreditsForAccount(ctx.accountId), []);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("blijft de bijschrijvingen van een afgelopen afspraak aanbieden", async () => {
|
||
|
|
await createSchedule(afspraak({
|
||
|
|
creditCadence: 'monthly', creditDayOfMonth: 1,
|
||
|
|
startsOn: '2020-01-01', endsOn: '2020-12-31',
|
||
|
|
}));
|
||
|
|
const dues = dueCreditsForAccount(ctx.accountId);
|
||
|
|
assert.equal(dues.length, 12, 'elf maandmomenten en de afsluiting, allemaal nog te boeken');
|
||
|
|
assert.equal(dues.at(-1).periodEnd, '2020-12-31', 'en niets ná de einddatum');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('biedt de rente van een verstreken deposito precies één keer aan', async () => {
|
||
|
|
const eind = new Date(Date.now() - 5 * 24 * 3600 * 1000).toISOString().slice(0, 10);
|
||
|
|
const start = new Date(Date.now() - 370 * 24 * 3600 * 1000).toISOString().slice(0, 10);
|
||
|
|
await createSchedule(afspraak({ creditCadence: 'term_end', startsOn: start, endsOn: eind }));
|
||
|
|
const dues = dueCreditsForAccount(ctx.accountId);
|
||
|
|
assert.equal(dues.length, 1);
|
||
|
|
assert.equal(dues[0].periodEnd, eind);
|
||
|
|
assert.ok(dues[0].projected_sub > 0, 'met een bedrag erbij, anders is er niets te boeken');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('due-detectie en dubbeltelling', () => {
|
||
|
|
async function maandelijkseAfspraak() {
|
||
|
|
const id = await createSchedule(afspraak({
|
||
|
|
creditCadence: 'monthly', creditDayOfMonth: 15,
|
||
|
|
startsOn: '2024-01-15', endsOn: '2099-12-31',
|
||
|
|
}));
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
|
||
|
|
it('een geboekte credit haalt precies die periode van de lijst', async () => {
|
||
|
|
const scheduleId = await maandelijkseAfspraak();
|
||
|
|
const voor = dueCreditsForAccount(ctx.accountId);
|
||
|
|
const eerste = voor[0];
|
||
|
|
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO InterestCredit (scheduleId, periodStart, periodEnd, status, createdAt)
|
||
|
|
VALUES (?,?,?, 'booked', ?)`,
|
||
|
|
[scheduleId, eerste.periodStart, eerste.periodEnd, '2026-01-01T00:00:00']);
|
||
|
|
|
||
|
|
const na = dueCreditsForAccount(ctx.accountId);
|
||
|
|
assert.equal(na.length, voor.length - 1);
|
||
|
|
assert.ok(!na.some(d => d.periodEnd === eerste.periodEnd));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('een genegeerde credit haalt de periode ook van de lijst', async () => {
|
||
|
|
const scheduleId = await maandelijkseAfspraak();
|
||
|
|
const eerste = dueCreditsForAccount(ctx.accountId)[0];
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO InterestCredit (scheduleId, periodStart, periodEnd, status, createdAt)
|
||
|
|
VALUES (?,?,?, 'dismissed', ?)`,
|
||
|
|
[scheduleId, eerste.periodStart, eerste.periodEnd, '2026-01-01T00:00:00']);
|
||
|
|
assert.ok(!dueCreditsForAccount(ctx.accountId).some(d => d.periodEnd === eerste.periodEnd));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rente die al via import binnenkwam onderdrukt de due (geen dubbeltelling)', async () => {
|
||
|
|
await maandelijkseAfspraak();
|
||
|
|
const eerste = dueCreditsForAccount(ctx.accountId)[0];
|
||
|
|
renteEvent(eerste.periodEnd, 12_345);
|
||
|
|
assert.ok(!dueCreditsForAccount(ctx.accountId).some(d => d.periodEnd === eerste.periodEnd),
|
||
|
|
'een uitkering óp de crediteringsdag hoort bij de periode die dan eindigt');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('een uitkering op de startdag van een periode onderdrukt die periode NIET', async () => {
|
||
|
|
await maandelijkseAfspraak();
|
||
|
|
const eerste = dueCreditsForAccount(ctx.accountId)[0];
|
||
|
|
renteEvent(eerste.periodStart, 12_345);
|
||
|
|
assert.ok(dueCreditsForAccount(ctx.accountId).some(d => d.periodEnd === eerste.periodEnd));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('bedragen: de prognose is de som van de bijschrijvingen', () => {
|
||
|
|
it('rekent één vol jaar op 5% van 1 BTC als 0,05 BTC', () => {
|
||
|
|
const s = afspraak({
|
||
|
|
creditCadence: 'term_end', startsOn: '2024-01-01', endsOn: '2024-12-31',
|
||
|
|
});
|
||
|
|
assert.equal(scheduleForecast(s, '2024-01-01').termTotalSub, 5_000_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rekent enkelvoudig over meerdere jaren, net als de bijschrijving', () => {
|
||
|
|
const s = afspraak({
|
||
|
|
creditCadence: 'term_end', startsOn: '2024-01-01', endsOn: '2025-12-31',
|
||
|
|
});
|
||
|
|
assert.equal(scheduleForecast(s, '2024-01-01').termTotalSub, 10_000_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('telt op tot precies wat de te-boeken-lijst aanbiedt', async () => {
|
||
|
|
await createSchedule(afspraak({
|
||
|
|
creditCadence: 'annual', annualRate: 0.028, startsOn: '2020-03-01', endsOn: '2023-01-01',
|
||
|
|
}));
|
||
|
|
const dues = dueCreditsForAccount(ctx.accountId);
|
||
|
|
assert.equal(dues.length, 3);
|
||
|
|
const s = listSchedulesForAccount(ctx.accountId)[0];
|
||
|
|
assert.equal(scheduleForecast(s, '2020-03-01').termTotalSub, dues.reduce((n, d) => n + d.projected_sub, 0));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('trekt al ontvangen rente af van het resterende bedrag', () => {
|
||
|
|
const s = afspraak({
|
||
|
|
creditCadence: 'term_end', startsOn: '2024-01-01', endsOn: '2024-12-31',
|
||
|
|
});
|
||
|
|
renteEvent('2024-06-01', 2_000_000);
|
||
|
|
const f = scheduleForecast(s, '2024-12-31');
|
||
|
|
assert.equal(f.realizedSub, 2_000_000);
|
||
|
|
assert.equal(f.remainingSub, 3_000_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat het resterende bedrag niet onder nul zakken', () => {
|
||
|
|
const s = afspraak({
|
||
|
|
creditCadence: 'term_end', startsOn: '2024-01-01', endsOn: '2024-12-31',
|
||
|
|
});
|
||
|
|
renteEvent('2024-06-01', 9_999_999);
|
||
|
|
assert.equal(scheduleForecast(s, '2024-12-31').remainingSub, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('merkt op wanneer hoofdsom en uitkering hetzelfde asset zijn', () => {
|
||
|
|
const s = afspraak({
|
||
|
|
creditCadence: 'term_end', startsOn: '2024-01-01', endsOn: '2024-12-31',
|
||
|
|
});
|
||
|
|
assert.equal(scheduleForecast(s, '2024-01-01').sameAsset, true);
|
||
|
|
assert.equal(scheduleForecast({ ...s, principalAssetId: 999 }, '2024-01-01').sameAsset, false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('komende bijschrijvingen', () => {
|
||
|
|
it('noemt elke resterende periode met zijn bedrag, de lopende inbegrepen', () => {
|
||
|
|
const s = afspraak({ creditCadence: 'annual', startsOn: '2024-03-01', endsOn: '2027-01-01' });
|
||
|
|
const { upcoming } = scheduleForecast(s, '2025-06-15');
|
||
|
|
assert.deepEqual(upcoming, [
|
||
|
|
{ periodStart: '2025-01-01', periodEnd: '2026-01-01', projectedSub: 5_000_000 },
|
||
|
|
{ periodStart: '2026-01-01', periodEnd: '2027-01-01', projectedSub: 5_000_000 },
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat een periode die op de peildatum eindigt weg, want die staat bij te boeken', () => {
|
||
|
|
const s = afspraak({ creditCadence: 'annual', startsOn: '2024-03-01', endsOn: '2027-01-01' });
|
||
|
|
assert.deepEqual(scheduleForecast(s, '2026-01-01').upcoming.map(p => p.periodEnd), ['2027-01-01']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is leeg na de einddatum', () => {
|
||
|
|
const s = afspraak({ creditCadence: 'annual', startsOn: '2024-03-01', endsOn: '2027-01-01' });
|
||
|
|
assert.deepEqual(scheduleForecast(s, '2027-01-01').upcoming, []);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('geeft zonder einddatum alleen de volgende, en die ligt ná vandaag', () => {
|
||
|
|
const s = afspraak({ creditCadence: 'monthly', creditDayOfMonth: 1, startsOn: '2024-01-01', endsOn: null });
|
||
|
|
const f = scheduleForecast(s, '2024-06-15');
|
||
|
|
assert.deepEqual(f.upcoming, [
|
||
|
|
{ periodStart: '2024-06-01', periodEnd: '2024-07-01', projectedSub: Math.round(EEN_BTC * 0.05 * 30 / 365) },
|
||
|
|
]);
|
||
|
|
assert.equal(f.termTotalSub, null);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('vindt zonder einddatum ook de volgende jaarbijschrijving', () => {
|
||
|
|
const s = afspraak({ creditCadence: 'annual', startsOn: '2024-03-01', endsOn: null });
|
||
|
|
assert.deepEqual(scheduleForecast(s, '2024-03-02').upcoming.map(p => p.periodEnd), ['2025-01-01']);
|
||
|
|
assert.deepEqual(scheduleForecast(s, '2025-01-01').upcoming.map(p => p.periodEnd), ['2026-01-01']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('telt een rentewijziging mee in het bedrag van de komende periode', async () => {
|
||
|
|
const id = await createSchedule(afspraak({ creditCadence: 'annual', startsOn: '2024-03-01', endsOn: '2027-01-01' }));
|
||
|
|
await createPeriod(id, { startsOn: '2025-07-01', annualRate: 0.01 });
|
||
|
|
const s = listSchedulesForAccount(ctx.accountId)[0];
|
||
|
|
assert.equal(scheduleForecast(s, '2025-06-15').upcoming[0].projectedSub,
|
||
|
|
Math.round(EEN_BTC * (0.05 * 181 / 365 + 0.01 * 184 / 365)));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('rente op rente', () => {
|
||
|
|
const drieJaar = (extra) => afspraak({
|
||
|
|
creditCadence: 'annual', startsOn: '2021-01-01', endsOn: '2024-01-01', ...extra,
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rekent elke periode ook over de bijschrijvingen ervóór', () => {
|
||
|
|
const f = scheduleForecast(drieJaar({ compounding: 1 }), '2021-01-01');
|
||
|
|
assert.deepEqual(f.upcoming.map(p => p.projectedSub), [5_000_000, 5_250_000, 5_512_500]);
|
||
|
|
assert.equal(f.termTotalSub, 15_762_500);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rekent zonder de keuze enkelvoudig, zoals voorheen', () => {
|
||
|
|
const f = scheduleForecast(drieJaar({ compounding: 0 }), '2021-01-01');
|
||
|
|
assert.deepEqual(f.upcoming.map(p => p.projectedSub), [5_000_000, 5_000_000, 5_000_000]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('biedt op te boeken dezelfde bedragen aan als de prognose', async () => {
|
||
|
|
await createSchedule(drieJaar({ compounding: 1 }));
|
||
|
|
assert.deepEqual(dueCreditsForAccount(ctx.accountId).map(d => d.projected_sub), [5_000_000, 5_250_000, 5_512_500]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('telt een genegeerde bijschrijving gewoon mee, want de bank schrijft hem toch bij', async () => {
|
||
|
|
const id = await createSchedule(drieJaar({ compounding: 1 }));
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO InterestCredit (scheduleId, periodStart, periodEnd, status, createdAt)
|
||
|
|
VALUES (?, '2021-01-01', '2022-01-01', 'dismissed', '2026-01-01T00:00:00')`, [id]);
|
||
|
|
assert.deepEqual(dueCreditsForAccount(ctx.accountId).map(d => d.projected_sub), [5_250_000, 5_512_500]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('weigert rente op rente op een hoofdsom die het saldo volgt', async () => {
|
||
|
|
await assert.rejects(() => createSchedule(drieJaar({ compounding: 1, principalAmount: null })),
|
||
|
|
(e) => e.key === 'interest.compoundingNeedsPrincipal');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('weigert rente op rente als de rente in een ander asset uitkeert', async () => {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES (?,?,?,?,?,?,?,?)`,
|
||
|
|
['EUR', 'Euro', toSearchName('Euro'), 'fiat', 'EUR', 'cent', 2, 'unit']);
|
||
|
|
const eur = harness.exports.lastId();
|
||
|
|
await assert.rejects(() => createSchedule(drieJaar({ compounding: 1, payoutAssetId: eur })),
|
||
|
|
(e) => e.key === 'interest.compoundingNeedsSameAsset');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('bewaart de keuze en geeft hem terug', async () => {
|
||
|
|
await createSchedule(drieJaar({ compounding: 1 }));
|
||
|
|
assert.equal(listSchedulesForAccount(ctx.accountId)[0].compounding, 1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('realizedInterestSub', () => {
|
||
|
|
it('telt vanaf `from` tot en met de dag vóór `to`', () => {
|
||
|
|
renteEvent('2024-03-01', 100);
|
||
|
|
renteEvent('2024-03-31', 200);
|
||
|
|
const som = realizedInterestSub(ctx.accountId, ctx.assetId, '2024-03-01', '2024-03-31');
|
||
|
|
assert.equal(som, 100, 'de bovengrens hoort er niet bij te vallen');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is 0 zonder rente-events', () => {
|
||
|
|
assert.equal(realizedInterestSub(ctx.accountId, ctx.assetId, '2024-01-01', '2025-01-01'), 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('telt alleen rente van het gevraagde account en asset', () => {
|
||
|
|
renteEvent('2024-03-01', 100);
|
||
|
|
assert.equal(realizedInterestSub(ctx.accountId + 99, ctx.assetId, '2024-01-01', '2025-01-01'), 0);
|
||
|
|
assert.equal(realizedInterestSub(ctx.accountId, ctx.assetId + 99, '2024-01-01', '2025-01-01'), 0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('ontvangen per afspraak', () => {
|
||
|
|
const tweeAfspraken = async () => {
|
||
|
|
await createSchedule(afspraak({ creditCadence: 'term_end', startsOn: '2024-01-01', endsOn: '2024-12-31', note: 'deposito' }));
|
||
|
|
await createSchedule(afspraak({ creditCadence: 'annual', principalAmount: null, startsOn: '2024-01-01', endsOn: null, note: 'spaar' }));
|
||
|
|
const lijst = listSchedulesForAccount(ctx.accountId);
|
||
|
|
return { deposito: lijst.find(s => s.note === 'deposito'), spaar: lijst.find(s => s.note === 'spaar') };
|
||
|
|
};
|
||
|
|
|
||
|
|
it('telt één rente-boeking onder twee overlappende afspraken maar één keer', async () => {
|
||
|
|
const { deposito, spaar } = await tweeAfspraken();
|
||
|
|
renteEvent('2024-06-01', 2_000_000);
|
||
|
|
const som = scheduleForecast(deposito, '2024-12-31').realizedSub + scheduleForecast(spaar, '2024-12-31').realizedSub;
|
||
|
|
assert.equal(som, 2_000_000, 'opgeteld over de afspraken is het ontvangen bedrag er één keer');
|
||
|
|
assert.equal(realizedForSchedule(spaar, '2024-12-31'), 2_000_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rekent een geboekte bijschrijving toe aan de afspraak waaronder hij geboekt is', async () => {
|
||
|
|
const { deposito, spaar } = await tweeAfspraken();
|
||
|
|
renteEvent('2024-06-01', 2_000_000);
|
||
|
|
const eventId = harness.exports.lastId();
|
||
|
|
harness.exports.run(`INSERT INTO InterestCredit (scheduleId, periodStart, periodEnd, eventId, status, createdAt)
|
||
|
|
VALUES (?,?,?,?, 'booked', ?)`, [deposito.id, '2024-01-01', '2024-06-01', eventId, '2024-06-01T00:00:00']);
|
||
|
|
assert.equal(scheduleForecast(deposito, '2024-12-31').realizedSub, 2_000_000);
|
||
|
|
assert.equal(scheduleForecast(spaar, '2024-12-31').realizedSub, 0);
|
||
|
|
assert.equal(listInterest().find(r => r.id === eventId).scheduleId, deposito.id);
|
||
|
|
});
|
||
|
|
|
||
|
|
const tweeInHetVerleden = async () => {
|
||
|
|
await createSchedule(afspraak({ creditCadence: 'monthly', creditDayOfMonth: 1, startsOn: '2020-01-01', endsOn: '2020-12-31', note: 'deposito' }));
|
||
|
|
await createSchedule(afspraak({ creditCadence: 'annual', startsOn: '2020-03-01', endsOn: '2020-12-31', note: 'spaar' }));
|
||
|
|
const lijst = listSchedulesForAccount(ctx.accountId);
|
||
|
|
return { deposito: lijst.find(s => s.note === 'deposito'), spaar: lijst.find(s => s.note === 'spaar') };
|
||
|
|
};
|
||
|
|
const dueVan = (s) => dueCreditsForAccount(ctx.accountId).filter(d => d.scheduleId === s.id);
|
||
|
|
|
||
|
|
it('laat een geboekte bijschrijving van de ene afspraak de andere niet van Te boeken halen', async () => {
|
||
|
|
const { deposito, spaar } = await tweeInHetVerleden();
|
||
|
|
const julDue = dueVan(deposito).find(d => d.periodEnd === '2020-07-01');
|
||
|
|
for (const d of dueVan(deposito).filter(d => d.periodEnd < '2020-07-01')) await dismissDueCredit(d);
|
||
|
|
await bookDueCredit(julDue, julDue.projected_sub);
|
||
|
|
assert.deepEqual(dueVan(spaar).map(d => d.periodEnd), ['2020-12-31'], 'de spaarrekening houdt haar bijschrijving');
|
||
|
|
assert.ok(!dueVan(deposito).some(d => d.periodEnd === '2020-07-01'), 'de geboekte periode is weg');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat een losse rente-boeking alleen de afspraak raken waaronder hij valt', async () => {
|
||
|
|
const { deposito, spaar } = await tweeInHetVerleden();
|
||
|
|
renteEvent('2020-06-15', 1_000);
|
||
|
|
assert.deepEqual(dueVan(spaar), [], 'de import vult de spaarperiode');
|
||
|
|
assert.ok(dueVan(deposito).some(d => d.periodEnd === '2020-07-01'), 'en niet de periode van het deposito');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('createSchedule: validatie', () => {
|
||
|
|
it('weigert een einddatum vóór de startdatum', async () => {
|
||
|
|
await assert.rejects(() => createSchedule(afspraak({
|
||
|
|
creditCadence: 'term_end', startsOn: '2024-06-01', endsOn: '2024-01-01',
|
||
|
|
})), (e) => e.key === 'accounts.schEndBeforeStart');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('eist een weekdag bij een wekelijkse cadans', async () => {
|
||
|
|
await assert.rejects(() => createSchedule(afspraak({
|
||
|
|
creditCadence: 'weekly', startsOn: '2024-01-01', endsOn: '2024-12-31',
|
||
|
|
})), (e) => e.key === 'interest.weekdayRequired');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('staat geen dag-van-de-maand toe bij een wekelijkse cadans', async () => {
|
||
|
|
await assert.rejects(() => createSchedule(afspraak({
|
||
|
|
creditCadence: 'weekly', creditWeekday: 1, creditDayOfMonth: 15,
|
||
|
|
startsOn: '2024-01-01', endsOn: '2024-12-31',
|
||
|
|
})), /has no day of the month/);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat een vaste afspraak zónder einddatum toe (doorlopend)', async () => {
|
||
|
|
const id = await createSchedule(afspraak({
|
||
|
|
creditCadence: 'quarterly', startsOn: '2024-01-01', endsOn: null,
|
||
|
|
}));
|
||
|
|
assert.ok(id > 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat een vaste afspraak zónder hoofdsom toe (volgt het saldo)', async () => {
|
||
|
|
const id = await createSchedule(afspraak({
|
||
|
|
creditCadence: 'quarterly', startsOn: '2024-01-01', endsOn: null, principalAmount: null,
|
||
|
|
}));
|
||
|
|
assert.ok(id > 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('weigert een hoofdsom van nul, want dat is een vergissing en geen keuze', async () => {
|
||
|
|
await assert.rejects(() => createSchedule(afspraak({
|
||
|
|
creditCadence: 'quarterly', startsOn: '2024-01-01', endsOn: null, principalAmount: 0,
|
||
|
|
})), (e) => e.key === 'interest.principalPositive');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('weigert hoofdsom of cadans op een variabele afspraak', async () => {
|
||
|
|
await assert.rejects(() => createSchedule({
|
||
|
|
...afspraak({ creditCadence: null, startsOn: '2024-01-01', endsOn: '2024-12-31' }),
|
||
|
|
rateKind: 'variable',
|
||
|
|
}), /manual or import/);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('due-bijschrijvingen van nul', () => {
|
||
|
|
it('biedt een periode zonder bedrag niet aan', async () => {
|
||
|
|
await createSchedule(afspraak({
|
||
|
|
creditCadence: 'annual', annualRate: 0,
|
||
|
|
startsOn: '2020-01-01', endsOn: '2022-01-01',
|
||
|
|
}));
|
||
|
|
assert.deepEqual(dueCreditsForAccount(ctx.accountId), []);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('biedt de perioden mét een bedrag nog steeds aan', async () => {
|
||
|
|
await createSchedule(afspraak({
|
||
|
|
creditCadence: 'annual', annualRate: 0.05,
|
||
|
|
startsOn: '2020-01-01', endsOn: '2022-01-01',
|
||
|
|
}));
|
||
|
|
const dues = dueCreditsForAccount(ctx.accountId);
|
||
|
|
assert.equal(dues.length, 2);
|
||
|
|
assert.ok(dues.every(d => d.projected_sub > 0));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('renteperioden: een rente die per datum wijzigt', () => {
|
||
|
|
async function jaarAfspraak(extra) {
|
||
|
|
return createSchedule(afspraak({
|
||
|
|
creditCadence: 'term_end', startsOn: '2024-01-01', endsOn: '2024-12-31', ...extra,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
const dueBedrag = () => dueCreditsForAccount(ctx.accountId)[0].projected_sub;
|
||
|
|
|
||
|
|
it('rekent zonder perioden precies zoals voorheen', async () => {
|
||
|
|
await jaarAfspraak();
|
||
|
|
assert.equal(dueBedrag(), 5_000_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('knipt de bijschrijving op de ingangsdatum van een wijziging', async () => {
|
||
|
|
const id = await jaarAfspraak();
|
||
|
|
await createPeriod(id, { startsOn: '2024-07-01', annualRate: 0.01 });
|
||
|
|
assert.equal(dueBedrag(), 2_994_521);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('telt twee wijzigingen in één periode allebei mee', async () => {
|
||
|
|
const id = await jaarAfspraak();
|
||
|
|
await createPeriod(id, { startsOn: '2024-04-01', annualRate: 0.04 });
|
||
|
|
await createPeriod(id, { startsOn: '2024-07-01', annualRate: 0.03 });
|
||
|
|
const verwacht = Math.round(100_000_000 * (0.05 * 91 / 365 + 0.04 * 91 / 365 + 0.03 * 183 / 365));
|
||
|
|
assert.equal(dueBedrag(), verwacht);
|
||
|
|
assert.notEqual(dueBedrag(), Math.round(100_000_000 * (0.05 * 182 / 365 + 0.03 * 183 / 365)),
|
||
|
|
'de middelste periode mag niet overgeslagen worden');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('gebruikt de laatste wijziging vóór het venster als beginrente', async () => {
|
||
|
|
const id = await createSchedule(afspraak({
|
||
|
|
creditCadence: 'quarterly', startsOn: '2024-01-01', endsOn: '2024-12-31',
|
||
|
|
}));
|
||
|
|
await createPeriod(id, { startsOn: '2024-02-01', annualRate: 0.01 });
|
||
|
|
const derde = dueCreditsForAccount(ctx.accountId).find(d => d.periodEnd === '2024-10-01');
|
||
|
|
assert.equal(derde.projected_sub, Math.round(100_000_000 * 0.01 * 92 / 365));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('telt in de prognose elk tarief over zijn eigen dagen', async () => {
|
||
|
|
const id = await createSchedule(afspraak({
|
||
|
|
creditCadence: 'term_end', startsOn: '2024-01-01', endsOn: '2025-12-31',
|
||
|
|
}));
|
||
|
|
await createPeriod(id, { startsOn: '2024-12-31', annualRate: 0.03 });
|
||
|
|
const s = listSchedulesForAccount(ctx.accountId)[0];
|
||
|
|
assert.equal(scheduleForecast(s, '2024-01-01').termTotalSub, 8_000_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('weigert een ingangsdatum op of vóór de start van de afspraak', async () => {
|
||
|
|
const id = await jaarAfspraak();
|
||
|
|
await assert.rejects(() => createPeriod(id, { startsOn: '2024-01-01', annualRate: 0.01 }), (e) => e.key === 'interest.periodStartBeforeSchedule');
|
||
|
|
await assert.rejects(() => createPeriod(id, { startsOn: '2023-12-31', annualRate: 0.01 }), (e) => e.key === 'interest.periodStartBeforeSchedule');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('weigert een ingangsdatum ná het einde van de afspraak', async () => {
|
||
|
|
const id = await jaarAfspraak();
|
||
|
|
await assert.rejects(() => createPeriod(id, { startsOn: '2025-06-01', annualRate: 0.01 }), (e) => e.key === 'interest.periodStartAfterEnd');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('weigert een periode op een variabele afspraak', async () => {
|
||
|
|
const id = await createSchedule({
|
||
|
|
accountId: ctx.accountId, rateKind: 'variable', payoutAssetId: ctx.assetId,
|
||
|
|
principalAssetId: null, principalAmount: null, annualRate: null,
|
||
|
|
startsOn: '2024-01-01', endsOn: null, creditCadence: null, creditWeekday: null, creditDayOfMonth: null,
|
||
|
|
});
|
||
|
|
await assert.rejects(() => createPeriod(id, { startsOn: '2024-06-01', annualRate: 0.01 }), /manual or import/);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat een periode bijwerken en verwijderen, en dat verandert het bedrag terug', async () => {
|
||
|
|
const id = await jaarAfspraak();
|
||
|
|
const pid = await createPeriod(id, { startsOn: '2024-07-01', annualRate: 0.01 });
|
||
|
|
assert.equal(dueBedrag(), 2_994_521);
|
||
|
|
|
||
|
|
await updatePeriod(pid, { startsOn: '2024-07-01', annualRate: 0.05 });
|
||
|
|
assert.equal(dueBedrag(), 5_000_000, 'zelfde rente als de afspraak: hetzelfde bedrag als zonder periode');
|
||
|
|
|
||
|
|
await deletePeriod(pid);
|
||
|
|
assert.deepEqual(listPeriods(id), []);
|
||
|
|
assert.equal(dueBedrag(), 5_000_000);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('hoofdsom leeg: de rente volgt het saldo van de rekening', () => {
|
||
|
|
const dueVoor = (eind) => dueCreditsForAccount(ctx.accountId).find(d => d.periodEnd === eind);
|
||
|
|
|
||
|
|
function balansOp(dagIso, balanceSub) {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
||
|
|
VALUES (?,?,?,?,1.0,?, 'manual')`,
|
||
|
|
[ctx.assetId, dagIso, ctx.accountId, balanceSub, toUtcIso(`${dagIso}T00:00:00`)]);
|
||
|
|
}
|
||
|
|
|
||
|
|
function storting(dagIso, bedragSub) {
|
||
|
|
harness.exports.run(
|
||
|
|
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, toAccountId, source, createdAt)
|
||
|
|
VALUES (?, 'buy', ?, ?, 1, ?, 'manual', ?)`,
|
||
|
|
[ctx.assetId, `${dagIso}T00:00:00`, bedragSub, ctx.accountId, `${dagIso}T00:00:00`]);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function saldoAfspraak() {
|
||
|
|
return createSchedule(afspraak({
|
||
|
|
creditCadence: 'quarterly', startsOn: '2024-01-01', endsOn: null, principalAmount: null,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
it('rekent over de stand en niet over een vast bedrag', async () => {
|
||
|
|
storting('2024-01-01', EEN_BTC);
|
||
|
|
await saldoAfspraak();
|
||
|
|
assert.equal(dueVoor('2024-04-01').projected_sub, Math.round(EEN_BTC * 0.05 * 91 / 365));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat een storting halverwege vanaf díe dag meetellen', async () => {
|
||
|
|
storting('2024-01-01', EEN_BTC);
|
||
|
|
storting('2024-03-01', EEN_BTC);
|
||
|
|
await saldoAfspraak();
|
||
|
|
const verwacht = Math.round(EEN_BTC * 0.05 * 60 / 365 + 2 * EEN_BTC * 0.05 * 31 / 365);
|
||
|
|
assert.equal(dueVoor('2024-04-01').projected_sub, verwacht);
|
||
|
|
assert.notEqual(dueVoor('2024-04-01').projected_sub, Math.round(EEN_BTC * 0.05 * 91 / 365),
|
||
|
|
'de storting van maart hoort mee te tellen');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('knipt óók op een balans halverwege de periode', async () => {
|
||
|
|
balansOp('2024-01-01', EEN_BTC);
|
||
|
|
balansOp('2024-03-01', EEN_BTC / 4);
|
||
|
|
await saldoAfspraak();
|
||
|
|
const verwacht = Math.round(EEN_BTC * 0.05 * 60 / 365 + (EEN_BTC / 4) * 0.05 * 31 / 365);
|
||
|
|
assert.equal(dueVoor('2024-04-01').projected_sub, verwacht);
|
||
|
|
assert.notEqual(dueVoor('2024-04-01').projected_sub, Math.round(EEN_BTC * 0.05 * 91 / 365),
|
||
|
|
'de balans van maart hoort mee te tellen');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('laat een balans van middernacht niet op de dag ervóór meetellen', async () => {
|
||
|
|
balansOp('2024-09-02', 12_000_000);
|
||
|
|
balansOp('2024-09-03', 14_150_000);
|
||
|
|
await createSchedule(afspraak({
|
||
|
|
creditCadence: 'quarterly', startsOn: '2024-09-02', endsOn: null, principalAmount: null,
|
||
|
|
}));
|
||
|
|
const verwacht = Math.round(12_000_000 * 0.05 * 1 / 365 + 14_150_000 * 0.05 * 28 / 365);
|
||
|
|
assert.equal(dueVoor('2024-10-01').projected_sub, verwacht);
|
||
|
|
assert.notEqual(dueVoor('2024-10-01').projected_sub, Math.round(14_150_000 * 0.05 * 29 / 365),
|
||
|
|
'de balans van 3 september mag niet op 2 september al gelden');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('volgt ook een balans, want dat is dezelfde stand', async () => {
|
||
|
|
balansOp('2024-01-01', EEN_BTC);
|
||
|
|
await saldoAfspraak();
|
||
|
|
assert.equal(dueVoor('2024-04-01').projected_sub, Math.round(EEN_BTC * 0.05 * 91 / 365));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('combineert een saldowijziging met een rentewijziging', async () => {
|
||
|
|
storting('2024-01-01', EEN_BTC);
|
||
|
|
storting('2024-03-01', EEN_BTC);
|
||
|
|
const id = await saldoAfspraak();
|
||
|
|
await createPeriod(id, { startsOn: '2024-02-01', annualRate: 0.02 });
|
||
|
|
const verwacht = Math.round(
|
||
|
|
EEN_BTC * 0.05 * 31 / 365 + EEN_BTC * 0.02 * 29 / 365 + 2 * EEN_BTC * 0.02 * 31 / 365);
|
||
|
|
assert.equal(dueVoor('2024-04-01').projected_sub, verwacht);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('geeft geen prognose, want er is geen termijn om naar toe te rekenen', async () => {
|
||
|
|
storting('2024-01-01', EEN_BTC);
|
||
|
|
await saldoAfspraak();
|
||
|
|
const s = listSchedulesForAccount(ctx.accountId)[0];
|
||
|
|
const f = scheduleForecast(s, '2024-06-01');
|
||
|
|
assert.equal(f.termTotalSub, null, 'zonder hoofdsom én einddatum bestaat een termijntotaal niet');
|
||
|
|
assert.equal(f.remainingSub, null);
|
||
|
|
assert.equal(typeof f.realizedSub, 'number', 'wat er ontvangen is, is er wél');
|
||
|
|
});
|
||
|
|
});
|