refactor!: support RUB only (WealthLedger/main#55) (#204)
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 1m32s
Backend verify / verify (push) Failing after 9m25s

BREAKING CHANGE: per-currency API fields replaced with single RUB Money.
This commit was merged in pull request #204.
This commit is contained in:
2026-11-11 10:07:11 +03:00
parent 85d65f8997
commit 809936e62d
61 changed files with 617 additions and 735 deletions
+55
View File
@@ -340,4 +340,59 @@ describe('MoexClient', () => {
expect(starts).toEqual(['0', '1']);
},
);
it('filters non-RUB coupons and maturity out of bond events', async () => {
const pages: Record<string, unknown> = {
coupons: Response.json({
'coupons': {
columns: ['coupondate', 'value', 'faceunit'],
data: [
['2099-02-01', '2', 'SUR'],
['2099-02-02', '3', 'USD'],
],
},
'coupons.cursor': {
columns: ['INDEX', 'TOTAL', 'PAGESIZE'],
data: [[0, 2, 100]],
},
}),
offers: Response.json({
'offers': {
columns: ['offerdate'],
data: [['2099-02-28']],
},
'offers.cursor': {
columns: ['INDEX', 'TOTAL', 'PAGESIZE'],
data: [[0, 1, 100]],
},
}),
description: Response.json({
description: {
columns: ['name', 'value'],
data: [
['MATDATE', '2099-02-28'],
['FACEVALUE', '500'],
['FACEUNIT', 'USD'],
],
},
}),
};
const fetchMock = jest
.spyOn(global, 'fetch')
.mockImplementation((input) => {
const url = new URL(input instanceof Request ? input.url : String(input));
const block = url.searchParams.get('iss.only')?.split(',')[0] ?? 'description';
// eslint-disable-next-line security/detect-object-injection -- Keys are fixed MOEX block names of this test's page map.
const page = pages[block] ?? pages.description;
return Promise.resolve(page as Response);
});
try {
await expect(new MoexClient().bondEvents('BOND')).resolves.toEqual([
{ type: 'coupon', date: '2099-02-01', value: '2', currency: 'RUB' },
{ type: 'offer', date: '2099-02-28' },
]);
} finally {
fetchMock.mockRestore();
}
});
});
+5 -6
View File
@@ -157,9 +157,9 @@ export class MoexClient {
for (const row of coupons) {
const date = text(row.coupondate);
const value = numberText(row.value);
const currency = currencyText(row.faceunit);
if (date && value && currency) {
events.push({ type: 'coupon', date, value, currency });
// RUB-only: coupons in other currencies are filtered out at this ingest boundary.
if (date && value && currencyText(row.faceunit) === 'RUB') {
events.push({ type: 'coupon', date, value, currency: 'RUB' });
}
}
for (const row of offers) {
@@ -171,9 +171,8 @@ export class MoexClient {
);
const date = values.get('MATDATE');
const value = numberText(values.get('FACEVALUE'));
const currency = currencyText(values.get('FACEUNIT'));
if (date && value && currency) {
events.push({ type: 'maturity', date, value, currency });
if (date && value && currencyText(values.get('FACEUNIT')) === 'RUB') {
events.push({ type: 'maturity', date, value, currency: 'RUB' });
}
return events;
}
+9 -1
View File
@@ -1,4 +1,6 @@
import { Decimal } from '../../common/decimal';
import type { Money } from '../../common/money.types';
import { Decimal, formatDecimal } from '../../common/decimal';
const MISSING_PROTO_COMPONENT = 0;
@@ -6,6 +8,12 @@ const NANO = 1_000_000_000n;
export type ProtoDecimal = { nano?: number | string; units?: number | string };
export function rubMoney(value?: ProtoDecimal & { currency?: string }): Money | null {
return value?.currency?.trim().toUpperCase() === 'RUB'
? { currency: 'RUB', value: formatDecimal(decimal(value)) }
: null;
}
export function decimal(value: ProtoDecimal): Decimal {
return {
value: BigInt(value.units ?? MISSING_PROTO_COMPONENT) * NANO + BigInt(value.nano ?? MISSING_PROTO_COMPONENT),