Files
AtreidesAssetsFree/server/data/valuations.js
T

138 lines
5.5 KiB
JavaScript
Raw Normal View History

2026-10-29 11:57:29 +01:00
// Generated, do not edit. sha256:297c7a1d9ed86e15053cd37bb7126f5fb5b54d94b67e8da901fb66f370ca9301
import * as db from '../db.js';
import { ENUMS } from '../../shared/config.js';
import { nowUtcIso } from '../../shared/time.js';
import { assertEnum } from './_shared.js';
import { capacityReachedFor } from './_limits.js';
import { fail } from '../../shared/app-error.js';
import { eventsCountByAccount, eventsNetByAccount } from './events.js';
import { trackDelete, trackUpdate } from './change-log.js';
import { emitChange } from './_events.js';
export function listValuations() {
return db.all(`SELECT v.*, ac.name AS account_name, a.symbol AS asset_symbol, a.name AS asset_name,
a.decimals AS asset_decimals, a.unit AS asset_unit, a.subunit AS asset_subunit,
a.displayUnit AS asset_display_unit, a.kind AS asset_kind
FROM Valuation v
LEFT JOIN Account ac ON ac.id = v.accountId
JOIN Asset a ON a.id = v.assetId
ORDER BY v.valuedOn DESC, v.rateAt DESC, v.id DESC`);
}
export function anchorAt(accountId, assetId, atIso) {
return db.one(
`SELECT * FROM Valuation
WHERE accountId = ?1 AND assetId = ?2 ${atIso ? 'AND rateAt <= ?3' : ''}
ORDER BY rateAt DESC, id DESC LIMIT 1`,
atIso ? [accountId, assetId, atIso] : [accountId, assetId]) || null;
}
export function latestAnchors(atIso, { ownOnly = false } = {}) {
return db.all(
`SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY accountId, assetId ORDER BY rateAt DESC, id DESC) AS rn
FROM Valuation WHERE accountId IS NOT NULL AND (?1 IS NULL OR rateAt <= ?1)
AND (?2 = 0 OR accountId IN (SELECT id FROM Account WHERE wealthScope IS NULL))
) WHERE rn = 1`, [atIso ?? null, ownOnly ? 1 : 0]);
}
export function anchorsBetween(accountId, assetId, fromIso, untilIso) {
return db.all(
`SELECT * FROM Valuation
WHERE accountId = ?1 AND assetId = ?2 AND rateAt > ?3 AND rateAt <= ?4
ORDER BY rateAt ASC, id ASC`, [accountId, assetId, fromIso, untilIso]);
}
export function anchorDaysBetween(accountId, assetId, vanDag, totDag) {
return db.all(
`SELECT DISTINCT valuedOn FROM Valuation
WHERE accountId = ?1 AND assetId = ?2 AND valuedOn > ?3 AND valuedOn < ?4
ORDER BY valuedOn`, [accountId, assetId, vanDag, totDag]).map(r => r.valuedOn);
}
export function firstAnchorDate() {
const r = db.one(`SELECT MIN(rateAt) AS first FROM Valuation WHERE accountId IS NOT NULL`);
return r && r.first ? r.first : null;
}
export function valuationDiff(v) {
if (!v.accountId) return null;
return v.balance - eventsNetByAccount(v.accountId, v.assetId, v.rateAt);
}
export function anchorBefore(accountId, assetId, atIso) {
return db.one(
`SELECT * FROM Valuation
WHERE accountId = ?1 AND assetId = ?2 AND rateAt < ?3
ORDER BY rateAt DESC, id DESC LIMIT 1`, [accountId, assetId, atIso]) || null;
}
export function valuationGap(v) {
if (!v.accountId) return null;
const vorige = anchorBefore(v.accountId, v.assetId, v.rateAt);
if (!vorige) return valuationDiff(v);
const sindsVorige = eventsNetByAccount(v.accountId, v.assetId, v.rateAt)
- eventsNetByAccount(v.accountId, v.assetId, vorige.rateAt);
return v.balance - (vorige.balance + sindsVorige);
}
export function valuationReconcilable(v) {
if (!v.accountId) return false;
return eventsCountByAccount(v.accountId, v.assetId, v.rateAt) > 0;
}
function verrijkt() {
const nu = nowUtcIso();
return listValuations().map(v => ({
...v,
diff: valuationDiff(v),
gap: valuationGap(v),
reconcilable: valuationReconcilable(v),
accepted: !!v.gapAcceptedAt,
future: v.rateAt > nu,
}));
}
export function valuationDiffs() {
return verrijkt().map(({ id, diff, gap, reconcilable, accepted, future }) =>
({ id, diff, gap, reconcilable, accepted, future }));
}
export function listOpenValuationDiffs() {
return verrijkt().filter(v => !v.future && !v.accepted && v.reconcilable && v.gap != null && v.gap !== 0);
}
export async function updateValuation(id, v) {
assertEnum('source', ENUMS.VALUATION_SOURCE, v.source);
trackUpdate('Valuation', id, () =>
db.run(`UPDATE Valuation SET assetId=?, valuedOn=?, accountId =?, balance=?, fiatRate=?, rateAt=?, source=? WHERE id=?`,
[v.assetId, v.valuedOn, v.accountId ?? null, v.balance, v.fiatRate, v.rateAt, v.source, id]));
await db.persist();
emitChange('ledger-changed');
}
export async function acceptValuationGap(id, aan = true) {
trackUpdate('Valuation', id, () =>
db.run(`UPDATE Valuation SET gapAcceptedAt = ? WHERE id = ?`, [aan ? nowUtcIso() : null, id]));
await db.persist();
emitChange('ledger-changed');
}
export async function deleteValuation(id) {
trackDelete('Valuation', id, () => db.run(`DELETE FROM Valuation WHERE id=?`, [id]));
await db.persist();
emitChange('ledger-changed');
}
export async function createValuation(v) {
assertEnum('source', ENUMS.VALUATION_SOURCE, v.source);
const max = capacityReachedFor('valuations');
if (max !== null) {
fail('valuations.maxFree', { max },
`The free version holds ${max} balance(s). A licence lifts that; nothing is removed in the meantime.`);
}
db.run(`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
VALUES (?,?,?,?,?,?,?)`,
[v.assetId, v.valuedOn, v.accountId ?? null, v.balance, v.fiatRate, v.rateAt, v.source]);
const id = db.lastId();
await db.persist();
emitChange('ledger-changed');
return id;
}