// Generated, do not edit. sha256:68008a6906cce53854e3533a4a9207b146e9943ef96ac00c6c0dd8b1e47f76f3 import * as db from '../db.js'; import { ENUMS } from '../../shared/config.js'; import { assertEnum, assertTimestamp, nowIso } from './_shared.js'; import { fail } from '../../shared/app-error.js'; import { trackDelete, trackUpdate } from './change-log.js'; import { emitChange } from './_events.js'; export function listEvents() { return db.all(`SELECT * FROM Event WHERE type <> 'reservation' ORDER BY occurredAt DESC, id DESC`); } export const EVENT_JOIN = ` SELECT e.*, 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, lf.name AS from_name, lt.name AS to_name, mp.name AS product_name, mp.form AS product_form FROM Event e JOIN Asset a ON a.id = e.assetId LEFT JOIN Account lf ON lf.id = e.fromAccountId LEFT JOIN Account lt ON lt.id = e.toAccountId LEFT JOIN MetalProduct mp ON mp.id = e.productId`; export function listTrades() { return db.all(`${EVENT_JOIN} WHERE e.type IN ('buy','sell') ORDER BY e.occurredAt DESC, e.id DESC`); } export function listTransfers() { return db.all(`${EVENT_JOIN} WHERE e.type = 'transfer' ORDER BY e.occurredAt DESC, e.id DESC`); } export function listAssetTrades(assetId) { return db.all(`SELECT e.id, e.type, e.amount, e.fiatPrice, e.fiatFee, e.occurredAt, e.fromAccountId, e.toAccountId, lf.name AS from_name, lt.name AS to_name, (SELECT GROUP_CONCAT(DISTINCT l.name) FROM WalletTransactionEventLink wl JOIN WalletTransaction wt ON wt.id = wl.walletTransactionId JOIN Label l ON l.id = wt.labelId WHERE wl.eventId = e.id) AS label_names FROM Event e LEFT JOIN Account lf ON lf.id = e.fromAccountId LEFT JOIN Account lt ON lt.id = e.toAccountId WHERE e.assetId = ? AND e.type IN ('buy','sell') ORDER BY e.occurredAt ASC, e.id ASC`, [assetId]); } function assertEventPayload(e) { assertEnum('type', ENUMS.EVENT_TYPE, e.type); assertEnum('source', ENUMS.EVENT_SOURCE, e.source); if ((e.type === 'buy' || e.type === 'sell') && (e.fiatPrice === undefined || e.fiatPrice === null)) { throw new Error(`fiatPrice is required for type '${e.type}'.`); } if (e.type === 'interest' && (Number(e.fiatFee) > 0 || Number(e.unitFee) > 0)) { fail('trades.feeOnInterest', null, 'A fee on interest counts nowhere; it is refused.'); } if (e.type === 'buy' && Number(e.unitFee) > 0) { fail('trades.unitFeeOnBuy', null, 'A fee in units on a purchase counts nowhere; lower the amount instead.'); } if (e.type === 'reservation') { if ((e.fromAccountId == null) === (e.toAccountId == null)) throw new Error('A reservation sits on exactly one account.'); if (e.fiatPrice != null || e.fiatFee != null || e.unitFee != null || e.productId != null || e.priceOrigin != null || e.importReference != null || e.externalAddress != null) { throw new Error('A reservation carries no price, fee, product, origin, import reference or address.'); } } assertTimestamp('occurredAt', e.occurredAt); assertProductAmount(e); } function priceOriginOf(value) { if (value == null || value === '') return null; assertEnum('priceOrigin', ENUMS.EVENT_PRICE_ORIGIN, value); return value; } function assertProductAmount(e) { if (e.productId == null) return; const p = db.one(`SELECT fineGrams FROM MetalProduct WHERE id = ?`, [e.productId]); if (!p) throw new Error(`Metal product ${e.productId} does not exist.`); const verwacht = e.productCount * p.fineGrams; if (e.amount !== verwacht) { throw new Error(`amount (${e.amount}) should be ${e.productCount} × ${p.fineGrams} = ${verwacht} ` + `for this product; adjust the count or the amount.`); } } export async function createEvent(e) { assertEventPayload(e); db.run(`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, fiatFee, unitFee, productId, productCount, fromAccountId, toAccountId, source, note, importReference, externalAddress, priceOrigin, createdAt) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [e.assetId, e.type, e.occurredAt, e.amount, e.fiatPrice ?? null, e.fiatFee ?? null, e.unitFee ?? null, e.productId ?? null, e.productCount ?? null, e.fromAccountId ?? null, e.toAccountId ?? null, e.source, e.note ?? null, e.importReference ?? null, e.externalAddress ?? null, priceOriginOf(e.priceOrigin), nowIso()]); const id = db.lastId(); await db.persist(); emitChange('ledger-changed'); return id; } export function findEventByRef(ref) { if (!ref) return null; return db.one(`SELECT * FROM Event WHERE importReference = ?`, [ref]) || null; } export function eventsWithoutImportRef() { return db.all(`SELECT * FROM Event WHERE importReference IS NULL AND type <> 'reservation'`); } export async function updateEvent(id, e) { assertEventPayload(e); const origin = Object.hasOwn(e, 'priceOrigin') ? priceOriginOf(e.priceOrigin) : (db.one(`SELECT priceOrigin AS o FROM Event WHERE id = ?`, [id]) || {}).o ?? null; trackUpdate('Event', id, () => db.run(`UPDATE Event SET assetId = ?, type = ?, occurredAt = ?, amount = ?, fiatPrice = ?, fiatFee = ?, unitFee = ?, productId = ?, productCount = ?, fromAccountId = ?, toAccountId = ?, source = ?, note = ?, externalAddress = ?, priceOrigin = ? WHERE id = ?`, [e.assetId, e.type, e.occurredAt, e.amount, e.fiatPrice ?? null, e.fiatFee ?? null, e.unitFee ?? null, e.productId ?? null, e.productCount ?? null, e.fromAccountId ?? null, e.toAccountId ?? null, e.source, e.note ?? null, e.externalAddress ?? null, origin, id])); await db.persist(); emitChange('ledger-changed'); } export async function deleteEvent(id) { trackDelete('Event', id, () => db.run(`DELETE FROM Event WHERE id = ?`, [id])); await db.persist(); emitChange('ledger-changed'); } export function eventsNetByAccount(accountId, assetId, untilIso) { const r = db.one(` SELECT COALESCE(SUM( CASE WHEN type = 'buy' AND toAccountId = ?1 THEN amount WHEN type = 'sell' AND fromAccountId = ?1 THEN -(amount + COALESCE(unitFee,0)) WHEN type = 'transfer' AND toAccountId = ?1 THEN amount WHEN type = 'transfer' AND fromAccountId = ?1 THEN -(amount + COALESCE(unitFee,0)) WHEN type = 'interest' AND toAccountId = ?1 THEN amount ELSE 0 END - (CASE WHEN type = 'transfer' AND fromAccountId = ?1 AND toAccountId = ?1 THEN amount + COALESCE(unitFee,0) ELSE 0 END) ), 0) AS net FROM Event WHERE assetId = ?2${untilIso ? ' AND occurredAt <= ?3' : ''}`, untilIso ? [accountId, assetId, untilIso] : [accountId, assetId]); return r.net; } export function eventsCountByAccount(accountId, assetId, untilIso) { const r = db.one( `SELECT COUNT(*) AS n FROM Event WHERE assetId = ?2 AND (fromAccountId = ?1 OR toAccountId = ?1) AND type <> 'reservation' ${untilIso ? 'AND occurredAt <= ?3' : ''}`, untilIso ? [accountId, assetId, untilIso] : [accountId, assetId]); return r.n; } export function eventAssetIdsByAccount(accountId) { return db.all( `SELECT DISTINCT assetId FROM Event WHERE (fromAccountId = ?1 OR toAccountId = ?1) AND type <> 'reservation' ORDER BY assetId`, [accountId]).map(r => r.assetId); } export function eventAccountIdsByAsset(assetId) { return db.all( `SELECT fromAccountId AS accountId FROM Event WHERE assetId = ?1 AND fromAccountId IS NOT NULL AND type <> 'reservation' UNION SELECT toAccountId AS accountId FROM Event WHERE assetId = ?1 AND toAccountId IS NOT NULL AND type <> 'reservation' ORDER BY accountId`, [assetId]).map(r => r.accountId); } export function eventDaysByAccount(accountId, assetId, vanIso, totIso) { return db.all( `SELECT DISTINCT substr(occurredAt, 1, 10) AS dag FROM Event WHERE assetId = ?2 AND (fromAccountId = ?1 OR toAccountId = ?1) AND type <> 'reservation' AND substr(occurredAt, 1, 10) > ?3 AND substr(occurredAt, 1, 10) < ?4 ORDER BY dag`, [accountId, assetId, vanIso, totIso]).map(r => r.dag); } export function assetPositions(untilIso, { ownOnly = false } = {}) { return db.all(` SELECT a.id, a.symbol, a.name, a.kind, a.decimals, a.unit, a.subunit, a.displayUnit, COALESCE(SUM(CASE WHEN e.type = 'buy' AND e.toIn THEN e.amount WHEN e.type = 'sell' AND e.fromIn THEN -(e.amount + COALESCE(e.unitFee,0)) WHEN e.type = 'interest' AND e.toIn THEN e.amount WHEN e.type = 'transfer' THEN (CASE WHEN e.toAccountId IS NOT NULL AND e.toIn THEN e.amount ELSE 0 END) - (CASE WHEN e.fromAccountId IS NOT NULL AND e.fromIn THEN e.amount + COALESCE(e.unitFee,0) ELSE 0 END) ELSE 0 END), 0) AS net, COALESCE(SUM(CASE WHEN e.type = 'buy' AND e.toIn AND e.priceOrigin IS NOT 'none' THEN 1 ELSE 0 END), 0) AS buy_events, COALESCE(SUM(CASE WHEN e.type = 'buy' AND e.toIn AND e.priceOrigin IS NOT 'none' THEN e.amount ELSE 0 END), 0) AS buy_amount, COALESCE(SUM(CASE WHEN e.type = 'buy' AND e.toIn AND e.priceOrigin IS NOT 'none' THEN e.amount * e.fiatPrice ELSE 0 END), 0) AS buy_spot_raw, COALESCE(SUM(CASE WHEN e.type = 'buy' AND e.toIn AND e.priceOrigin IS NOT 'none' THEN COALESCE(e.fiatFee,0) ELSE 0 END), 0) AS buy_fee_total, COALESCE(SUM(CASE WHEN e.priceOrigin IS 'none' THEN (CASE WHEN e.type = 'buy' AND e.toIn THEN e.amount WHEN e.type = 'sell' AND e.fromIn THEN -e.amount -- A ONE-SIDED TRANSFER without consideration is a gift too, -- as listTradeEvents already treats it; without this its units took the average -- purchase price as cost. Two sides is internal and stays out. WHEN e.type = 'transfer' AND NOT (e.fromAccountId IS NOT NULL AND e.fromIn) AND e.toAccountId IS NOT NULL AND e.toIn THEN e.amount WHEN e.type = 'transfer' AND NOT (e.toAccountId IS NOT NULL AND e.toIn) AND e.fromAccountId IS NOT NULL AND e.fromIn THEN -e.amount ELSE 0 END) ELSE 0 END), 0) AS gift_amount, COALESCE(SUM(CASE WHEN e.type = 'sell' AND e.fromIn THEN 1 ELSE 0 END), 0) AS sell_events FROM Asset a LEFT JOIN ( SELECT Event.*, (?2 = 0 OR toAccount.wealthScope IS NULL) AS toIn, (?2 = 0 OR fromAccount.wealthScope IS NULL) AS fromIn FROM Event LEFT JOIN Account toAccount ON toAccount.id = Event.toAccountId LEFT JOIN Account fromAccount ON fromAccount.id = Event.fromAccountId WHERE ?1 IS NULL OR Event.occurredAt <= ?1 ) e ON e.assetId = a.id GROUP BY a.id ORDER BY a.symbol`, [untilIso ?? null, ownOnly ? 1 : 0]); } export function listTradeEvents(fromIso, untilIso) { return db.all(`SELECT e.type, e.assetId, e.amount, e.fiatPrice, e.fiatFee, e.priceOrigin, e.occurredAt, e.fromAccountId, e.toAccountId, a.decimals FROM Event e JOIN Asset a ON a.id = e.assetId WHERE (e.type IN ('buy','sell') -- EVERY one-sided transfer: value entering or leaving the books. With 'none' a -- gift, without it a deposit or withdrawal at the day price. OR (e.type = 'transfer' AND (e.fromAccountId IS NULL) <> (e.toAccountId IS NULL)) -- A EURO FEE ON A TRANSFER is money in that is gone, as with a purchase (plan -- Bewijzen B2): the caller counts the fee and not the units. OR (e.type = 'transfer' AND e.fromAccountId IS NOT NULL AND e.fiatFee > 0)) AND (?1 IS NULL OR e.occurredAt > ?1) AND e.occurredAt <= ?2`, [fromIso, untilIso]); } export function listFlowLegs(fromIso, untilIso) { const venster = `(?1 IS NULL OR Event.occurredAt > ?1) AND Event.occurredAt <= ?2`; const been = (type, kolom, richting) => ` SELECT Event.${kolom} AS accountId, Event.assetId, Asset.decimals, Event.type, '${richting}' AS direction, Event.amount AS units, Event.fiatPrice, Event.fiatFee, Event.priceOrigin, Event.occurredAt FROM Event JOIN Asset ON Asset.id = Event.assetId WHERE Event.type = '${type}' AND Event.${kolom} IS NOT NULL AND ${venster}`; return db.all([ been('buy', 'toAccountId', 'in'), been('sell', 'fromAccountId', 'out'), been('transfer', 'toAccountId', 'in'), been('transfer', 'fromAccountId', 'out'), ].join(' UNION ALL '), [fromIso, untilIso]); } export function firstTaxEventDate() { const r = db.one(`SELECT MIN(occurredAt) AS first FROM Event WHERE type <> 'reservation'`); return r && r.first ? r.first : null; }