chore: strengthen backend eslint profile

This commit is contained in:
2026-09-28 16:37:01 +03:00
committed by ksv741
parent 53b2794963
commit a970eaaf7a
179 changed files with 4485 additions and 2716 deletions
@@ -1,14 +1,16 @@
import type { Request } from 'express';
import { Controller, Get, Param, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import type { Request } from 'express';
import { ApiProblemResponses } from '../../../common/decorators/api-safe-error-response.decorator';
import { AccessPayload, JwtAuthGuard } from '../../auth/auth.api';
import { InvestmentReadQueryDto } from '../investment-resource.dto';
import {
IncomeAnalyticsResourceDto,
IncomeAnalyticsQueryDto,
IncomeAnalyticsResourceDto,
IncomeSummaryResourceDto,
} from './income-analytics.dto';
import { InvestmentReadQueryDto } from '../investment-resource.dto';
import { IncomeAnalyticsService } from './income-analytics.service';
@ApiTags('brokerage-accounts')
@@ -1,13 +1,13 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, Matches } from 'class-validator';
import { MoneyDto } from '../../../common/money.dto';
import { BrokerageOperationDto } from '../operations/operations.api';
import { MoneyDto } from '../../../common/money.dto';
import {
InvestmentDependencyDto,
InvestmentReadQueryDto,
InvestmentResourceMetaDto,
} from '../investment-resource.dto';
import { BrokerageOperationDto } from '../operations/operations.api';
const dateOnly = /^\d{4}-\d{2}-\d{2}$/;
@@ -102,16 +102,16 @@ export class IncomeInstrumentDto {
declare key: string;
@ApiProperty({ type: String, nullable: true })
declare figi: string | null;
declare figi: null | string;
@ApiProperty({ type: String, nullable: true })
declare instrumentUid: string | null;
declare instrumentUid: null | string;
@ApiProperty({ type: String, nullable: true })
declare ticker: string | null;
declare ticker: null | string;
@ApiProperty({ type: String, nullable: true })
declare instrumentType: string | null;
declare instrumentType: null | string;
@ApiProperty()
declare displayName: string;
@@ -142,7 +142,7 @@ export class IncomeAnalyticsDto extends IncomeSummaryDto {
declare yearComparison: IncomeComparisonDto | null;
@ApiProperty({ enum: ['day', 'week', 'month'] })
declare bucketGranularity: 'day' | 'week' | 'month';
declare bucketGranularity: 'day' | 'month' | 'week';
@ApiProperty({ type: IncomeBucketDto, isArray: true })
declare buckets: IncomeBucketDto[];
@@ -157,6 +157,7 @@ export class IncomeAnalyticsDto extends IncomeSummaryDto {
export class IncomeSummaryResourceDto {
@ApiProperty({ type: IncomeSummaryDto, nullable: true })
declare data: IncomeSummaryDto | null;
@ApiProperty({ type: InvestmentResourceMetaDto })
declare meta: InvestmentResourceMetaDto;
}
@@ -169,6 +170,7 @@ export class IncomeDependencyDto extends InvestmentDependencyDto {
}
export class IncomeAnalyticsMetaDto extends InvestmentResourceMetaDto {
// eslint-disable-next-line @darraghor/nestjs-typed/api-property-matches-property-optionality -- Overrides optional dependencies inherited from InvestmentResourceMetaDto.
@ApiProperty({
type: IncomeDependencyDto,
isArray: true,
@@ -182,6 +184,7 @@ export class IncomeAnalyticsMetaDto extends InvestmentResourceMetaDto {
export class IncomeAnalyticsResourceDto {
@ApiProperty({ type: IncomeAnalyticsDto, nullable: true })
declare data: IncomeAnalyticsDto | null;
@ApiProperty({ type: IncomeAnalyticsMetaDto })
declare meta: IncomeAnalyticsMetaDto;
}
@@ -6,8 +6,9 @@ jest.mock('../operations/application/brokerage-operations.service', () => ({
}));
import assert from 'node:assert/strict';
import { BrokerageOperationDto } from '../operations/presentation/brokerage-operation.dto';
import { investmentResourceMeta } from '../investment-resource.mapper';
import { BrokerageOperationDto } from '../operations/presentation/brokerage-operation.dto';
import { IncomeAnalyticsService } from './income-analytics.service';
describe('IncomeAnalyticsService', () => {
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import { Injectable } from '@nestjs/common';
import assert from 'node:assert/strict';
import {
add,
compare,
@@ -7,24 +8,21 @@ import {
formatDecimal,
subtract,
} from '../../../common/decimal';
import { investmentResourceMeta } from '../investment-resource.mapper';
import {
IncomeAnalyticsResourceDto,
IncomeDependencyDto,
IncomeSummaryResourceDto,
} from './income-analytics.dto';
import { DateOnlyRange, moscowDate } from '../investment-calendar';
import { investmentResourceMeta } from '../investment-resource.mapper';
import {
BrokerageOperationDto,
BrokerageOperationsService,
} from '../operations/operations.api';
import {
IncomeAnalyticsEntryDto,
IncomeAnalyticsResourceDto,
IncomeComparisonDto,
IncomeCurrencySummaryDto,
IncomeDependencyDto,
IncomeInstrumentDto,
IncomeSummaryDto,
IncomeSummaryResourceDto,
} from './income-analytics.dto';
import {
IncomeBucketGranularity,
@@ -96,9 +94,9 @@ export class IncomeAnalyticsService {
{ ...current.meta, role: 'selected' },
{ ...prior.meta, role: 'previous' },
{
...(plan.yearAgo && year
? year.meta
: investmentResourceMeta({
...(plan.yearAgo && year ?
year.meta :
investmentResourceMeta({
resourceKey: 'income/yearAgo', // gitleaks:allow public resource identifier
dataRevision: 'unavailable',
availability: 'missing',
@@ -119,9 +117,9 @@ export class IncomeAnalyticsService {
resourceKey: `income/${accountId}/${plan.selected.from}/${plan.selected.to}`,
dataRevision: current.meta.dataRevision,
availability: current.meta.availability,
fetchedAt: current.meta.lastSuccessfulAt
? new Date(current.meta.lastSuccessfulAt)
: null,
fetchedAt: current.meta.lastSuccessfulAt ?
new Date(current.meta.lastSuccessfulAt) :
null,
freshForMs: current.meta.freshness === 'fresh' ? Infinity : 0,
work: work?.id ? { ...work, id: work.id } : null,
coverage: current.meta.coverage,
@@ -144,13 +142,13 @@ export class IncomeAnalyticsService {
current.meta.lastSuccessfulAt,
),
previous:
prior.items === null
? null
: comparison(selected, plan.previous, qualifying(prior.items)),
prior.items === null ?
null :
comparison(selected, plan.previous, qualifying(prior.items)),
yearComparison:
plan.yearAgo && year?.items !== null && year
? comparison(selected, plan.yearAgo, qualifying(year.items))
: null,
plan.yearAgo && year?.items !== null && year ?
comparison(selected, plan.yearAgo, qualifying(year.items)) :
null,
bucketGranularity: plan.granularity,
buckets: buckets(plan.selected, plan.granularity, selected),
instruments: instruments(selected),
@@ -184,9 +182,9 @@ function totals(entries: BrokerageOperationDto[]): IncomeCurrencySummaryDto[] {
if (!entry.payment) continue;
const total = values.get(entry.payment.currency) ?? zeroTotal();
const value = decimalFromString(entry.payment.value);
if (entry.type === 'OPERATION_TYPE_COUPON')
if (entry.type === 'OPERATION_TYPE_COUPON') {
total.coupon = add(total.coupon, value);
else total.dividend = add(total.dividend, value);
} else total.dividend = add(total.dividend, value);
values.set(entry.payment.currency, total);
}
return [...values]
@@ -207,6 +205,7 @@ function comparison(
);
const currencies = [
...new Set([...selected.keys(), ...compared.keys()]),
// eslint-disable-next-line sonarjs/no-alphabetical-sort -- Code-unit ordering is intentional for canonical keys and ISO dates; locale collation would change persisted bindings.
].sort();
return {
period: comparedPeriod,
@@ -279,8 +278,9 @@ function instruments(
function instrumentKey(entry: BrokerageOperationDto): string {
if (entry.instrumentUid) return `uid:${entry.instrumentUid}`;
if (entry.figi) return `figi:${entry.figi}`;
if (entry.ticker || entry.instrumentType)
if (entry.ticker || entry.instrumentType) {
return `ticker:${entry.ticker ?? ''}:${entry.instrumentType ?? ''}`;
}
return `operation:${entry.account ? `${entry.account.id}:` : ''}${entry.id}`;
}
@@ -1,4 +1,5 @@
import { BadRequestException } from '@nestjs/common';
import { dateOnlyBounds } from '../investment-calendar';
import { planIncomePeriod, rangeBuckets } from './income-period';
@@ -17,8 +18,9 @@ describe('income period planning', () => {
['2026-03-02', '2026-03-01'],
['2026-01-01', '2027-01-02'],
['2026-01-01', '2026-04-01'],
])
]) {
expect(() => planIncomePeriod(from, to)).toThrow(BadRequestException);
}
});
it('uses a full previous month, skips impossible year-ago dates, and keeps empty buckets', () => {
+32 -29
View File
@@ -1,5 +1,6 @@
import { MILLISECONDS_PER_DAY } from '../../../common/time';
import { BadRequestException } from '@nestjs/common';
import { MILLISECONDS_PER_DAY } from '../../../common/time';
import {
DateOnlyRange,
dateParts,
@@ -13,41 +14,41 @@ const DAYS_PER_WEEK = 7;
const ISO_MONTH_LENGTH = 7;
const ISO_YEAR_LENGTH = 4;
export type IncomeBucketGranularity = 'day' | 'week' | 'month';
export type IncomeBucketGranularity = 'day' | 'month' | 'week';
export function planIncomePeriod(
from: string | undefined,
to: string | undefined,
openedDate?: Date,
): {
selected: DateOnlyRange;
previous: DateOnlyRange;
yearAgo: DateOnlyRange | null;
granularity: IncomeBucketGranularity;
previous: DateOnlyRange;
selected: DateOnlyRange;
yearAgo: DateOnlyRange | null;
} {
if (Boolean(from) !== Boolean(to)) throw invalidPeriod();
const today = moscowDate(new Date());
const selected =
from && to ? { from, to } : { from: firstOfMonth(today), to: today };
assertRange(selected, today);
const previous = isFullMonth(selected)
? previousMonth(selected.from)
: shiftDays(selected, -rangeDays(selected));
const previous = isFullMonth(selected) ?
previousMonth(selected.from) :
shiftDays(selected, -rangeDays(selected));
const candidate = yearAgo(selected);
const yearAgoRange =
candidate && (!openedDate || candidate.from >= moscowDate(openedDate))
? candidate
: null;
candidate && (!openedDate || candidate.from >= moscowDate(openedDate)) ?
candidate :
null;
return {
selected,
previous,
yearAgo: yearAgoRange,
granularity:
rangeDays(selected) <= DAILY_BUCKET_MAX_DAYS
? 'day'
: rangeDays(selected) <= WEEKLY_BUCKET_MAX_DAYS
? 'week'
: 'month',
rangeDays(selected) <= DAILY_BUCKET_MAX_DAYS ?
'day' :
rangeDays(selected) <= WEEKLY_BUCKET_MAX_DAYS ?
'week' :
'month',
};
}
@@ -55,24 +56,25 @@ export function rangeBuckets(
range: DateOnlyRange,
granularity: IncomeBucketGranularity,
): DateOnlyRange[] {
if (granularity === 'day')
if (granularity === 'day') {
return dates(range).map((date) => ({ from: date, to: date }));
}
const buckets: DateOnlyRange[] = [];
let cursor =
granularity === 'week'
? addDays(
granularity === 'week' ?
addDays(
range.from,
-(
(utcDate(range.from).getUTCDay() + DAYS_PER_WEEK - 1) %
DAYS_PER_WEEK
),
)
: `${range.from.slice(0, ISO_MONTH_LENGTH)}-01`;
) :
`${range.from.slice(0, ISO_MONTH_LENGTH)}-01`;
while (cursor <= range.to) {
const end =
granularity === 'week'
? addDays(cursor, DAYS_PER_WEEK - 1)
: endOfMonth(cursor);
granularity === 'week' ?
addDays(cursor, DAYS_PER_WEEK - 1) :
endOfMonth(cursor);
buckets.push({
from: cursor < range.from ? range.from : cursor,
to: end > range.to ? range.to : end,
@@ -86,7 +88,7 @@ export function rangeDays(range: DateOnlyRange): number {
return (
Math.round(
(utcDate(range.to).valueOf() - utcDate(range.from).valueOf()) /
MILLISECONDS_PER_DAY,
MILLISECONDS_PER_DAY,
) + 1
);
}
@@ -109,8 +111,9 @@ function addDays(value: string, days: number): string {
function dates(range: DateOnlyRange): string[] {
const result: string[] = [];
for (let date = range.from; date <= range.to; date = addDays(date, 1))
for (let date = range.from; date <= range.to; date = addDays(date, 1)) {
result.push(date);
}
return result;
}
@@ -142,9 +145,9 @@ function yearAgo(range: DateOnlyRange): DateOnlyRange | null {
const shift = (value: string) => {
const [year, month, day] = dateParts(value);
const result = `${String(year - 1).padStart(ISO_YEAR_LENGTH, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
return utcDate(result).toISOString().slice(0, 10) === result
? result
: null;
return utcDate(result).toISOString().slice(0, 10) === result ?
result :
null;
};
const from = shift(range.from);
const to = shift(range.to);
@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../../auth/auth.module';
import { OperationsModule } from '../operations/operations.module';
import { IncomeAnalyticsController } from './income-analytics.controller';