fix: stabilize broker display models

This commit is contained in:
Sergey Krylov 2026-06-19 06:13:23 +03:00
parent 2462f2122c
commit 6d2df6a12b
4 changed files with 39 additions and 4 deletions

View File

@ -95,6 +95,22 @@ describe('buildBrokerAllocation', () => {
]);
});
it('ignores a tiny negative residual caused by decimal arithmetic', () => {
const result = buildBrokerAllocation(portfolio({ shares: 0.1, bonds: 0.2, portfolio: 0.3 }));
expect(result.sectors.map(({ key }) => key)).toEqual(['shares', 'bonds']);
expect(result.negative).toEqual([]);
});
it('ignores a tiny positive residual caused by decimal arithmetic', () => {
const result = buildBrokerAllocation(
portfolio({ shares: 0.3, portfolio: 0.30000000000000004 }),
);
expect(result.sectors.map(({ key }) => key)).toEqual(['shares']);
expect(result.negative).toEqual([]);
});
it('returns no allocation for missing or nonpositive portfolio totals', () => {
expect(buildBrokerAllocation(portfolio({ shares: 100, portfolio: null }))).toEqual({
total: 0,

View File

@ -32,12 +32,22 @@ export function buildBrokerAllocation(portfolio: BrokerPortfolio): {
const bonds = portfolio.totals.bonds?.value ?? 0;
const etf = portfolio.totals.etf?.value ?? 0;
const cash = portfolio.totals.currencies?.value ?? 0;
const mappedTotal = shares + bonds + etf + cash;
const residual = total - mappedTotal;
const residualTolerance =
Number.EPSILON *
Math.max(
1,
Math.abs(total),
Math.abs(shares) + Math.abs(bonds) + Math.abs(etf) + Math.abs(cash),
) *
8;
const values: Record<BrokerAllocationKey, number> = {
shares,
bonds,
etf,
cash,
other: total - shares - bonds - etf - cash,
other: Math.abs(residual) <= residualTolerance ? 0 : residual,
};
const sectors: BrokerAllocationItem[] = [];

View File

@ -138,6 +138,11 @@ describe('broker display helpers', () => {
expect(labels).toEqual([...labels].sort((left, right) => left.localeCompare(right, 'ru')));
});
it('keeps operation type options immutable at runtime', () => {
expect(Object.isFrozen(BROKER_OPERATION_TYPE_OPTIONS)).toBe(true);
expect(BROKER_OPERATION_TYPE_OPTIONS.every((option) => Object.isFrozen(option))).toBe(true);
});
it('validates only exact known operation type values', () => {
expect(isBrokerOperationType('OPERATION_TYPE_COUPON')).toBe(true);
expect(isBrokerOperationType('operation_type_coupon')).toBe(false);

View File

@ -94,9 +94,13 @@ const OPERATION_TYPE_LABELS: Record<string, string> = {
OPERATION_TYPE_OUTPUT_SECURITIES: 'Списание бумаг',
};
export const BROKER_OPERATION_TYPE_OPTIONS = Object.entries(OPERATION_TYPE_LABELS)
.map(([value, label]) => ({ value, label }))
.sort((left, right) => left.label.localeCompare(right.label, 'ru'));
export const BROKER_OPERATION_TYPE_OPTIONS: ReadonlyArray<
Readonly<{ value: string; label: string }>
> = Object.freeze(
Object.entries(OPERATION_TYPE_LABELS)
.map(([value, label]) => Object.freeze({ value, label }))
.sort((left, right) => left.label.localeCompare(right.label, 'ru')),
);
const BROKER_OPERATION_TYPES = new Set(BROKER_OPERATION_TYPE_OPTIONS.map(({ value }) => value));