262 lines
13 KiB
JavaScript
262 lines
13 KiB
JavaScript
// 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;
|
||
}
|