// Generated, do not edit. sha256:c2a988d6416e302a0ad7224bc98a3cd265bc5cc9cadb0dce809af179af082299 import * as db from '../db.js'; import { nowIso, todayIso, daysBetween, isoToDate, dateToIso, addDaysIso, weekdayOf, clampDom } from './_shared.js'; import { createEvent, eventDaysByAccount } from './events.js'; import { listSelectableAccounts } from './accounts.js'; import { positionAt } from './positions.js'; import { listSchedulesForAccount, listPeriods, realizedForSchedule } from './interest-schedules.js'; import { anchorDaysBetween } from './valuations.js'; import { toUtcIso } from '../../shared/time.js'; import { fail } from '../../shared/app-error.js'; export function realizedInterestSub(accountId, assetId, fromIso, toIso) { const r = db.one(`SELECT COALESCE(SUM(amount),0) AS s FROM Event WHERE type = 'interest' AND toAccountId = ? AND assetId = ? AND occurredAt >= ? AND occurredAt < ?`, [accountId, assetId, fromIso, toIso]); return r ? r.s : 0; } function weeklyDates(startIso, endIso, weekday) { const out = []; let cur = addDaysIso(startIso, 1), guard = 0; while (weekdayOf(cur) !== weekday && guard++ < 7) cur = addDaysIso(cur, 1); for (; cur <= endIso; cur = addDaysIso(cur, 7)) out.push(cur); return out; } function monthlyDates(startIso, endIso, dom) { const out = []; const s = isoToDate(startIso); let y = s.getUTCFullYear(), m = s.getUTCMonth(); for (;;) { const iso = dateToIso(new Date(Date.UTC(y, m, clampDom(y, m, dom)))); if (iso > endIso) break; if (iso > startIso) out.push(iso); if (++m > 11) { m = 0; y++; } } return out; } function buildPeriods(dates, startIso) { const periods = []; let prev = startIso; for (const d of dates) { periods.push({ periodStart: prev, periodEnd: d }); prev = d; } return periods; } function allCreditingPeriods(s, horizonIso = todayIso()) { const periods = cadencePeriods(s, horizonIso); if (s.endsOn && s.creditCadence !== 'term_end') { const last = periods.length ? periods[periods.length - 1].periodEnd : s.startsOn; if (last < s.endsOn) periods.push({ periodStart: last, periodEnd: s.endsOn }); } return periods; } function cadencePeriods(s, horizonIso) { const eind = s.endsOn || horizonIso; if (s.creditCadence === 'term_end') return s.endsOn ? [{ periodStart: s.startsOn, periodEnd: s.endsOn }] : []; if (s.creditCadence === 'annual') { const periods = []; const startYear = Number(s.startsOn.slice(0, 4)); for (let y = startYear; ; y++) { const periodEnd = `${y + 1}-01-01`; if (periodEnd > eind) break; periods.push({ periodStart: y === startYear ? s.startsOn : `${y}-01-01`, periodEnd: periodEnd }); } return periods; } if (s.creditCadence === 'quarterly') { const periods = []; const s0 = isoToDate(s.startsOn); let y = s0.getUTCFullYear(); let m = Math.floor(s0.getUTCMonth() / 3) * 3; let prev = s.startsOn; for (;;) { m += 3; if (m > 11) { m -= 12; y++; } const periodEnd = dateToIso(new Date(Date.UTC(y, m, 1))); if (periodEnd > eind) break; periods.push({ periodStart: prev, periodEnd }); prev = periodEnd; } return periods; } const dates = s.creditCadence === 'weekly' ? weeklyDates(s.startsOn, eind, s.creditWeekday) : monthlyDates(s.startsOn, eind, s.creditDayOfMonth); return buildPeriods(dates, s.startsOn); } export function scheduleForecast(s, asofIso = todayIso()) { const realizedSub = realizedForSchedule(s, asofIso); const sameAsset = s.principalAssetId === s.payoutAssetId; const periods = allCreditingPeriods(s, addDaysIso(asofIso, 400)); const amount = periodAmounts(s, periods); const later = periods.filter(p => p.periodEnd > asofIso); const upcoming = (s.endsOn ? later : later.slice(0, 1)).map(p => ({ ...p, projectedSub: amount(p) })); if (!s.endsOn || s.principalAmount == null) { return { termTotalSub: null, realizedSub, remainingSub: null, remainingCount: null, sameAsset, upcoming }; } const termTotalSub = periods.reduce((n, p) => n + amount(p), 0); const remainingSub = Math.max(0, termTotalSub - realizedSub); return { termTotalSub, realizedSub, remainingSub, remainingCount: upcoming.length, sameAsset, upcoming }; } function periodAmounts(s, periods) { if (!s.compounding) return p => projectedSubForPeriod(s, p); const byEnd = new Map(); let accrued = 0; for (const p of periods) { const sub = projectedSubForPeriod(s, p, accrued); byEnd.set(p.periodEnd, sub); accrued += sub; } return p => byEnd.get(p.periodEnd); } function creditingPeriods(s, asofIso) { return allCreditingPeriods(s).filter(p => p.periodEnd <= asofIso); } function rateSlices(s, vanIso, totIso) { const perioden = listPeriods(s.id); const wissels = perioden.filter(p => p.startsOn > vanIso && p.startsOn < totIso); const eerder = perioden.filter(p => p.startsOn <= vanIso); let rate = eerder.length ? eerder[eerder.length - 1].annualRate : s.annualRate; const uit = []; let van = vanIso; for (const w of wissels) { uit.push({ van, tot: w.startsOn, rate }); van = w.startsOn; rate = w.annualRate; } uit.push({ van, tot: totIso, rate }); return uit; } function principalMainVoor(s, opDag) { if (s.principalAmount != null) return s.principalAmount / Math.pow(10, s.principal_decimals); const moment = toUtcIso(`${opDag}T23:59:59`); return positionAt(s.accountId, s.principalAssetId, moment) / Math.pow(10, s.principal_decimals); } function saldoDagen(s, vanDag, totDag) { const dagen = new Set([ ...eventDaysByAccount(s.accountId, s.principalAssetId, vanDag, totDag), ...anchorDaysBetween(s.accountId, s.principalAssetId, vanDag, totDag), ]); return [...dagen].sort(); } function projectedSubForPeriod(s, p, accruedSub = 0) { const stukken = []; for (const deel of rateSlices(s, p.periodStart, p.periodEnd)) { if (s.principalAmount != null) { stukken.push(deel); continue; } let van = deel.van; for (const dag of saldoDagen(s, deel.van, deel.tot)) { stukken.push({ van, tot: dag, rate: deel.rate }); van = dag; } stukken.push({ van, tot: deel.tot, rate: deel.rate }); } const accrued = accruedSub / Math.pow(10, s.payout_decimals); const som = stukken.reduce((n, deel) => n + (principalMainVoor(s, deel.van) + accrued) * deel.rate * Math.max(0, daysBetween(deel.van, deel.tot) / 365), 0); return Math.round(som * Math.pow(10, s.payout_decimals)); } function getCredit(scheduleId, periodEnd) { return db.one(`SELECT * FROM InterestCredit WHERE scheduleId = ? AND periodEnd = ?`, [scheduleId, periodEnd]) || null; } export function dueCreditsForAccount(accountId) { const today = todayIso(); const out = []; for (const s of listSchedulesForAccount(accountId)) { if (s.rateKind !== 'fixed') continue; const periods = creditingPeriods(s, today); const amount = periodAmounts(s, periods); for (const p of periods) { if (getCredit(s.id, p.periodEnd)) continue; if (realizedForSchedule(s, p.periodEnd, addDaysIso(p.periodStart, 1)) > 0) continue; const projected = amount(p); if (projected === 0) continue; out.push({ scheduleId: s.id, accountId: accountId, payoutAssetId: s.payoutAssetId, payout_symbol: s.payout_symbol, payout_decimals: s.payout_decimals, payout_unit: s.payout_unit, payout_subunit: s.payout_subunit, payout_display_unit: s.payout_display_unit, periodStart: p.periodStart, periodEnd: p.periodEnd, projected_sub: projected, }); } } return out; } export function dueCreditsAll() { const out = []; for (const a of listSelectableAccounts()) { for (const d of dueCreditsForAccount(a.id)) out.push({ ...d, account_name: a.name }); } return out; } function olderOpenDue(due) { return dueCreditsForAccount(due.accountId) .filter(d => d.periodEnd < due.periodEnd) .sort((a, b) => a.periodEnd.localeCompare(b.periodEnd))[0] || null; } export async function bookDueCredit(due, amountSub) { const ouder = olderOpenDue(due); if (ouder) { fail('interest.bookOlderFirst', { start: ouder.periodStart, eind: ouder.periodEnd }, `Book the credit of ${ouder.periodStart} to ${ouder.periodEnd} first: each credit raises the ` + 'balance the next one is computed on.'); } const eventId = await createEvent({ assetId: due.payoutAssetId, type: 'interest', occurredAt: toUtcIso(`${due.periodEnd}T00:00:00`), amount: amountSub, fiatPrice: null, fiatFee: null, unitFee: null, fromAccountId: null, toAccountId: due.accountId, source: 'manual', importReference: null, externalAddress: null, note: null, }); db.run(`INSERT INTO InterestCredit (scheduleId, periodStart, periodEnd, eventId, status, createdAt) VALUES (?,?,?,?, 'booked', ?)`, [due.scheduleId, due.periodStart, due.periodEnd, eventId, nowIso()]); await db.persist(); return eventId; } export async function dismissDueCredit(due) { db.run(`INSERT INTO InterestCredit (scheduleId, periodStart, periodEnd, eventId, status, createdAt) VALUES (?,?,?, NULL, 'dismissed', ?)`, [due.scheduleId, due.periodStart, due.periodEnd, nowIso()]); await db.persist(); }