Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,261 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in new issue
Block a user