From ad6d18a423f42858af95958c48f69f04bde2c8a1 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 28 Feb 2026 09:04:43 +0100 Subject: [PATCH 1/2] Wrap accountServiceRef in toString to prevent tedious Invalid string (#3297) fast-xml-parser parses purely numeric AcctSvcrRef values (e.g. 20110649015) as JavaScript numbers. Passing a number to a varchar(256) column causes tedious to throw "Invalid string". Wrap both AcctSvcrRef lookups in toString() to ensure the value is always a string. --- .../bank-tx/bank-tx/services/sepa-parser.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/subdomains/supporting/bank-tx/bank-tx/services/sepa-parser.service.ts b/src/subdomains/supporting/bank-tx/bank-tx/services/sepa-parser.service.ts index 1b3c00962c..dbf48536df 100644 --- a/src/subdomains/supporting/bank-tx/bank-tx/services/sepa-parser.service.ts +++ b/src/subdomains/supporting/bank-tx/bank-tx/services/sepa-parser.service.ts @@ -72,8 +72,8 @@ export class SepaParser { return Util.asyncMap(entries, async (entry) => { const accountServiceRef = - entry?.NtryDtls?.TxDtls?.Refs?.AcctSvcrRef ?? - entry?.AcctSvcrRef ?? + this.toString(entry?.NtryDtls?.TxDtls?.Refs?.AcctSvcrRef) ?? + this.toString(entry?.AcctSvcrRef) ?? `CUSTOM/${file.BkToCstmrStmt.Stmt?.Acct?.Id?.IBAN}/${entry.BookgDt.Dt}/${entry.AddtlNtryInf}`; const creditDebitIndicator = this.toString(entry?.NtryDtls?.TxDtls?.CdtDbtInd); From dfcb501889568ee6f1551d54af67211d067c16fd Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 28 Feb 2026 09:05:17 +0100 Subject: [PATCH 2/2] fix: custody history chart missing assets (#3292) * fix: include all custody assets in portfolio history chart The portfolio history chart was showing incorrect values (e.g. 0.01 CHF instead of 25'933 CHF) because getUserCustodyHistory() only considered assets from completed orders when fetching price history. Assets acquired through receives or other mechanisms without matching orders were missing. Additionally, order balance changes were only applied inside the price reduce loop, so volumes from order days without price records were lost. Changes: - Include assets from custody_balance table (with balance > 0) in the price query, not just assets from orders - Decouple order volume processing from price iteration by applying all cumulative order changes up to each price day before calculating value * fix: use order updated date --------- Co-authored-by: David May --- .../core/custody/services/custody.service.ts | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/subdomains/core/custody/services/custody.service.ts b/src/subdomains/core/custody/services/custody.service.ts index 7feda31aad..e375233e68 100644 --- a/src/subdomains/core/custody/services/custody.service.ts +++ b/src/subdomains/core/custody/services/custody.service.ts @@ -151,13 +151,13 @@ export class CustodyService { const custodyUserIds = account.users.filter((u) => u.role === UserRole.CUSTODY).map((u) => u.id); const custodyOrders = await this.custodyOrderRepo.find({ where: { user: { id: In(custodyUserIds) }, status: CustodyOrderStatus.COMPLETED }, - order: { id: 'ASC' }, + order: { updated: 'ASC' }, }); if (!custodyOrders.length) return { totalValue: [] }; const orderMap = custodyOrders.reduce((map, order) => { - const key = Util.isoDate(order.created); + const key = Util.isoDate(order.updated); const dayMap = map.get(key) ?? new Map(); [ @@ -178,33 +178,38 @@ export class CustodyService { const assets = Array.from(new Map(allAssets.map((a) => [a.id, a])).values()); // get all prices (by date) - const startDate = new Date(custodyOrders[0].created); + const startDate = new Date(custodyOrders[0].updated); startDate.setHours(0, 0, 0, 0); const prices = await this.assetPricesService.getAssetPrices(assets, startDate); const priceMap = Util.groupByAccessor(prices, (p) => Util.isoDate(p.created)); - // process by day + // process by day: apply order volumes before calculating value const assetBalancesMap = new Map(); const totalValue: CustodyHistoryEntryDto[] = []; + const sortedOrderDays = [...orderMap.keys()].sort(); + let orderDayIndex = 0; + + for (const [day, dayPrices] of priceMap.entries()) { + // apply all order balance changes up to and including this day + while (orderDayIndex < sortedOrderDays.length && sortedOrderDays[orderDayIndex] <= day) { + const dayOrders = orderMap.get(sortedOrderDays[orderDayIndex]); + if (dayOrders) { + for (const [assetId, orders] of dayOrders.entries()) { + const currentBalance = assetBalancesMap.get(assetId) ?? 0; + assetBalancesMap.set(assetId, currentBalance + Util.sum(orders.map((o) => o.amount))); + } + } + orderDayIndex++; + } - for (const [day, prices] of priceMap.entries()) { - const dailyValue = prices.reduce( + // calculate daily portfolio value from current balances and available prices + const dailyValue = dayPrices.reduce( (value, price) => { - // update asset balance - const currentBalance = assetBalancesMap.get(price.asset.id) ?? 0; - - const ordersToday = orderMap.get(day)?.get(price.asset.id) ?? []; - const dayVolume = Util.sum(ordersToday.map((o) => o.amount)); - - const newBalance = currentBalance + dayVolume; - assetBalancesMap.set(price.asset.id, newBalance); - - // update value - value.chf += newBalance * price.priceChf; - value.eur += newBalance * price.priceEur; - value.usd += newBalance * price.priceUsd; - + const balance = assetBalancesMap.get(price.asset.id) ?? 0; + value.chf += balance * price.priceChf; + value.eur += balance * price.priceEur; + value.usd += balance * price.priceUsd; return value; }, { chf: 0, eur: 0, usd: 0 },