// Generated, do not edit. sha256:1f3b140e78bb63390abe586ae0f82dd70eb59c31c977547efd2e278b927a1a2f import * as db from '../db.js'; import { localDate } from '../../shared/time.js'; import { ENUMS } from '../../shared/config.js'; import { fail } from '../../shared/app-error.js'; import { assertEnum, nowIso } from './_shared.js'; import { EVENT_JOIN } from './events.js'; import { trackDelete, trackUpdate } from './change-log.js'; import { emitChange } from './_events.js'; export function listInterestForAccount(accountId) { return db.all(`${EVENT_JOIN} WHERE e.type = 'interest' AND e.toAccountId = ? ORDER BY e.occurredAt DESC, e.id DESC`, [accountId]); } export function listInterest() { const rows = db.all(`${EVENT_JOIN} WHERE e.type = 'interest' ORDER BY e.occurredAt DESC, e.id DESC`); const schedules = listAllSchedules(); const links = creditLinks(); for (const r of rows) { const s = attributeSchedule(r, schedules, links); r.scheduleId = s ? s.id : null; r.schedule_label = s ? scheduleLabel(s) : null; } return rows; } export function listAllSchedules() { return db.all(` SELECT s.*, ac.name AS account_name, pa.symbol AS principal_symbol, pa.decimals AS principal_decimals, pa.unit AS principal_unit, pa.subunit AS principal_subunit, pa.displayUnit AS principal_display_unit, po.symbol AS payout_symbol, po.decimals AS payout_decimals, po.unit AS payout_unit, po.subunit AS payout_subunit, po.displayUnit AS payout_display_unit FROM InterestSchedule s JOIN Account ac ON ac.id = s.accountId LEFT JOIN Asset pa ON pa.id = s.principalAssetId JOIN Asset po ON po.id = s.payoutAssetId ORDER BY s.startsOn DESC, s.id DESC`); } export function scheduleLabel(s) { const period = `${s.startsOn}–${s.endsOn || 'heden'}`; const kind = s.rateKind === 'variable' ? 'variabel' : `${+(s.annualRate * 100).toFixed(4)}%`; return `${s.account_name} · ${s.payout_symbol} · ${kind} · ${period}`; } export function realizedForSchedule(s, asofIso, sinceIso = null) { const stored = listSchedulesForAccount(s.accountId); const schedules = stored.some(o => o.id === s.id) ? stored.map(o => (o.id === s.id ? s : o)) : [...stored, s]; const links = creditLinks(); return db.all(`SELECT id, occurredAt, toAccountId, assetId, amount FROM Event WHERE type = 'interest' AND toAccountId = ? AND assetId = ? AND occurredAt >= ? AND occurredAt <= ?`, [s.accountId, s.payoutAssetId, sinceIso || '', `${asofIso}T23:59:59`]) .filter(ev => attributeSchedule(ev, schedules, links) === s) .reduce((n, ev) => n + ev.amount, 0); } function creditLinks() { return new Map(db.all(`SELECT eventId, scheduleId FROM InterestCredit WHERE eventId IS NOT NULL`) .map(c => [c.eventId, c.scheduleId])); } function attributeSchedule(ev, schedules, links) { const linkedId = links.get(ev.id); const linked = linkedId != null ? schedules.find(s => s.id === linkedId) : null; if (linked) return linked; const date = localDate(ev.occurredAt); let best = null; for (const s of schedules) { if (s.accountId !== ev.toAccountId || s.payoutAssetId !== ev.assetId) continue; if (date < s.startsOn || (s.endsOn && date > s.endsOn)) continue; if (!best || s.startsOn > best.startsOn) best = s; } return best; } export function listSchedulesForAccount(accountId) { return db.all(` SELECT s.*, pa.symbol AS principal_symbol, pa.decimals AS principal_decimals, pa.unit AS principal_unit, pa.subunit AS principal_subunit, pa.displayUnit AS principal_display_unit, po.symbol AS payout_symbol, po.decimals AS payout_decimals, po.unit AS payout_unit, po.subunit AS payout_subunit, po.displayUnit AS payout_display_unit FROM InterestSchedule s LEFT JOIN Asset pa ON pa.id = s.principalAssetId JOIN Asset po ON po.id = s.payoutAssetId WHERE s.accountId = ? ORDER BY s.startsOn DESC, s.id DESC`, [accountId]); } function assertSchedule(s) { assertEnum('rateKind', ENUMS.INTEREST_RATE_KIND, s.rateKind); if (!s.payoutAssetId) fail('accounts.schPayoutRequired', null, 'A schedule needs a payout asset.'); if (!s.startsOn) fail('accounts.schStartRequired', null, 'A schedule needs a start date.'); if (s.endsOn && s.endsOn < s.startsOn) fail('accounts.schEndBeforeStart', null, 'The end date precedes the start date.'); if (s.rateKind === 'variable') { if (s.principalAssetId != null || s.principalAmount != null || s.annualRate != null) throw new Error('A schedule on "manual or import" has no principal or rate: the app computes nothing there.'); if (s.creditCadence != null || s.creditWeekday != null || s.creditDayOfMonth != null) throw new Error('A schedule on "manual or import" has no credit cadence: the interest comes from outside.'); if (s.compounding) throw new Error('A schedule on "manual or import" has no interest on interest: the app computes nothing there.'); return; } if (s.compounding) { if (s.principalAmount == null) fail('interest.compoundingNeedsPrincipal', null, 'Interest on interest needs a fixed principal; a principal that follows the balance already grows by every credit.'); if (s.principalAssetId !== s.payoutAssetId) fail('interest.compoundingNeedsSameAsset', null, 'Interest on interest needs the payout in the principal asset.'); } if (!s.principalAssetId) fail('accounts.schAssetRequired', null, 'A schedule needs a principal asset.'); if (s.principalAmount != null && !(s.principalAmount > 0)) { fail('interest.principalPositive', null, 'The principal has to be greater than 0; leave it empty to follow the account balance.'); } if (!(s.annualRate >= 0)) fail('interest.rateNotNegative', null, 'The annual rate may not be negative.'); assertEnum('creditCadence', ENUMS.INTEREST_CADENCE, s.creditCadence); if (s.creditCadence === 'weekly') { if (!Number.isInteger(s.creditWeekday) || s.creditWeekday < 0 || s.creditWeekday > 6) fail('interest.weekdayRequired', null, 'Pick a weekday for the weekly credit.'); if (s.creditDayOfMonth != null) throw new Error('A weekly schedule has no day of the month.'); } else if (s.creditCadence === 'monthly') { if (!Number.isInteger(s.creditDayOfMonth) || s.creditDayOfMonth < 1 || s.creditDayOfMonth > 31) fail('interest.dayOfMonthRequired', null, 'Pick a day of the month for the monthly credit.'); if (s.creditWeekday != null) throw new Error('A monthly schedule has no weekday.'); } else if (s.creditWeekday != null || s.creditDayOfMonth != null) { throw new Error('This cadence uses neither a weekday nor a day of the month.'); } } export async function createSchedule(s) { assertSchedule(s); db.run(`INSERT INTO InterestSchedule (accountId, rateKind, principalAssetId, payoutAssetId, principalAmount, annualRate, startsOn, endsOn, creditCadence, creditWeekday, creditDayOfMonth, compounding, note, createdAt) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [s.accountId, s.rateKind, s.principalAssetId ?? null, s.payoutAssetId, s.principalAmount ?? null, s.annualRate ?? null, s.startsOn, s.endsOn ?? null, s.creditCadence ?? null, s.creditWeekday ?? null, s.creditDayOfMonth ?? null, s.compounding ? 1 : 0, s.note ?? null, nowIso()]); const id = db.lastId(); await db.persist(); emitChange('ledger-changed'); return id; } export async function updateSchedule(id, s) { assertSchedule(s); if (!s.accountId) fail('interest.schAccountRequired', null, 'A schedule needs an account.'); trackUpdate('InterestSchedule', id, () => db.run(`UPDATE InterestSchedule SET accountId = ?, rateKind = ?, principalAssetId = ?, payoutAssetId = ?, principalAmount = ?, annualRate = ?, startsOn = ?, endsOn = ?, creditCadence = ?, creditWeekday = ?, creditDayOfMonth = ?, compounding = ?, note = ? WHERE id = ?`, [s.accountId, s.rateKind, s.principalAssetId ?? null, s.payoutAssetId, s.principalAmount ?? null, s.annualRate ?? null, s.startsOn, s.endsOn ?? null, s.creditCadence ?? null, s.creditWeekday ?? null, s.creditDayOfMonth ?? null, s.compounding ? 1 : 0, s.note ?? null, id])); await db.persist(); emitChange('ledger-changed'); } export async function deleteSchedule(id) { trackDelete('InterestSchedule', id, () => db.run(`DELETE FROM InterestSchedule WHERE id = ?`, [id])); await db.persist(); emitChange('ledger-changed'); } export function listPeriods(scheduleId) { return db.all(`SELECT * FROM InterestPeriod WHERE scheduleId = ? ORDER BY startsOn ASC`, [scheduleId]); } function assertPeriod(scheduleId, p) { const s = db.one(`SELECT startsOn, endsOn, rateKind FROM InterestSchedule WHERE id = ?`, [scheduleId]); if (!s) fail('interest.unknownSchedule', null, 'Unknown interest schedule.'); if (s.rateKind !== 'fixed') throw new Error('A schedule on "manual or import" has no rate, and therefore no interest periods.'); if (!p.startsOn) fail('interest.periodStartRequired', null, 'A period needs a start date.'); if (p.startsOn <= s.startsOn) { fail('interest.periodStartBeforeSchedule', { start: s.startsOn }, `The start date has to be after the start of the schedule (${s.startsOn}); the rate from that day on is on the schedule itself.`); } if (s.endsOn && p.startsOn > s.endsOn) { fail('interest.periodStartAfterEnd', { eind: s.endsOn }, `The start date is after the end of the schedule (${s.endsOn}).`); } if (!(p.annualRate >= 0)) fail('interest.rateNotNegative', null, 'The annual rate may not be negative.'); } export async function createPeriod(scheduleId, p) { assertPeriod(scheduleId, p); db.run(`INSERT INTO InterestPeriod (scheduleId, startsOn, annualRate) VALUES (?,?,?)`, [scheduleId, p.startsOn, p.annualRate]); const id = db.lastId(); await db.persist(); return id; } export async function updatePeriod(id, p) { const bestaand = db.one(`SELECT scheduleId FROM InterestPeriod WHERE id = ?`, [id]); if (!bestaand) fail('interest.unknownPeriod', null, 'Unknown interest period.'); assertPeriod(bestaand.scheduleId, p); trackUpdate('InterestPeriod', id, () => db.run(`UPDATE InterestPeriod SET startsOn = ?, annualRate = ? WHERE id = ?`, [p.startsOn, p.annualRate, id])); await db.persist(); } export async function deletePeriod(id) { trackDelete('InterestPeriod', id, () => db.run(`DELETE FROM InterestPeriod WHERE id = ?`, [id])); await db.persist(); }