fix(investments): align roles and date contracts
Backend security / security (push) Successful in 9s
Backend verify / verify (push) Successful in 10m23s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Successful in 10m39s
Conventional commits / commits (pull_request) Successful in 29s
Backend security / security (push) Successful in 9s
Backend verify / verify (push) Successful in 10m23s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Successful in 10m39s
Conventional commits / commits (pull_request) Successful in 29s
Identify income dependencies by role and document screen-specific dates. Convert inclusive calendar days into half-open source ranges while preserving operation timestamps and existing stored coverage.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
@@ -22,6 +23,109 @@ describe('OpenAPI contract (integration)', () => {
|
||||
|
||||
afterAll(() => app.close());
|
||||
|
||||
it('distinguishes calendar screen dates from operation instants in requests and responses', () => {
|
||||
const document = createOpenApiDocument(app);
|
||||
const schemas = document.components?.schemas;
|
||||
const screenSchema = {
|
||||
oneOf: [
|
||||
{ $ref: '#/components/schemas/IncomeInvestmentScreenDto' },
|
||||
{ $ref: '#/components/schemas/EventsInvestmentScreenDto' },
|
||||
{ $ref: '#/components/schemas/TimestampInvestmentScreenDto' },
|
||||
],
|
||||
discriminator: {
|
||||
propertyName: 'screen',
|
||||
mapping: {
|
||||
income: '#/components/schemas/IncomeInvestmentScreenDto',
|
||||
events: '#/components/schemas/EventsInvestmentScreenDto',
|
||||
operations: '#/components/schemas/TimestampInvestmentScreenDto',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(
|
||||
document.paths['/api/v1/tbank-sync'].post?.requestBody,
|
||||
).toMatchObject({
|
||||
content: { 'application/json': { schema: screenSchema } },
|
||||
});
|
||||
expect(schemas?.TbankSyncAcceptedDto).toMatchObject({
|
||||
properties: { scope: screenSchema },
|
||||
});
|
||||
expect(schemas?.TbankSyncStatusDto).toMatchObject({
|
||||
properties: { scope: { ...screenSchema, nullable: true } },
|
||||
});
|
||||
for (const [model, screen] of [
|
||||
['IncomeInvestmentScreenDto', 'income'],
|
||||
['EventsInvestmentScreenDto', 'events'],
|
||||
]) {
|
||||
const schema = schemas?.[model];
|
||||
assert(schema && !('$ref' in schema));
|
||||
expect(schema.required).toContain('screen');
|
||||
expect(schema.required).toContain('accountId');
|
||||
expect(schema).toMatchObject({
|
||||
properties: {
|
||||
screen: { enum: [screen] },
|
||||
from: { type: 'string', format: 'date' },
|
||||
to: { type: 'string', format: 'date' },
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(schemas?.TimestampInvestmentScreenDto).toMatchObject({
|
||||
properties: {
|
||||
from: { type: 'string', format: 'date-time' },
|
||||
to: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
});
|
||||
expect(schemas).not.toHaveProperty('InvestmentScreenDto');
|
||||
for (const scope of ['all', '{accountId}']) {
|
||||
for (const [resource, format] of [
|
||||
['income-analytics', 'date'],
|
||||
['events', 'date'],
|
||||
['operations', 'date-time'],
|
||||
]) {
|
||||
const parameters =
|
||||
document.paths[`/api/v1/brokerage-accounts/${scope}/${resource}`].get
|
||||
?.parameters;
|
||||
for (const name of ['from', 'to'])
|
||||
expect(
|
||||
parameters?.find(
|
||||
(parameter) => 'name' in parameter && parameter.name === name,
|
||||
),
|
||||
).toMatchObject({
|
||||
name,
|
||||
in: 'query',
|
||||
schema: { format },
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('exposes required semantic income dependency roles', () => {
|
||||
const schemas = createOpenApiDocument(app).components?.schemas;
|
||||
expect(schemas?.IncomeAnalyticsResourceDto).toMatchObject({
|
||||
properties: {
|
||||
meta: { $ref: '#/components/schemas/IncomeAnalyticsMetaDto' },
|
||||
},
|
||||
});
|
||||
const meta = schemas?.IncomeAnalyticsMetaDto;
|
||||
assert(meta && !('$ref' in meta));
|
||||
expect(meta.required).toContain('dependencies');
|
||||
expect(meta).toMatchObject({
|
||||
properties: {
|
||||
dependencies: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/IncomeDependencyDto' },
|
||||
},
|
||||
},
|
||||
});
|
||||
const dependency = schemas?.IncomeDependencyDto;
|
||||
assert(dependency && !('$ref' in dependency));
|
||||
expect(dependency.required).toContain('role');
|
||||
expect(dependency).toMatchObject({
|
||||
properties: {
|
||||
role: { type: 'string', enum: ['selected', 'previous', 'year'] },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('generates the runtime contract without server startup or deployment configuration', async () => {
|
||||
const directory = await mkdtemp(resolve(tmpdir(), 'wealthledger-openapi-'));
|
||||
try {
|
||||
|
||||
@@ -138,7 +138,15 @@ describe('local income completeness and exact money (SQLite)', () => {
|
||||
).toEqual(['a', 'b']);
|
||||
expect(result.data.previous).toBeNull();
|
||||
expect(result.data.yearComparison).toBeNull();
|
||||
expect(result.meta.dependencies?.[1].availability).toBe('missing');
|
||||
expect(result.meta.dependencies.map(({ role }) => role)).toEqual([
|
||||
'selected',
|
||||
'previous',
|
||||
'year',
|
||||
]);
|
||||
expect(
|
||||
result.meta.dependencies.find(({ role }) => role === 'previous')
|
||||
?.availability,
|
||||
).toBe('missing');
|
||||
expect(result.data.calculatedAt).toBe('2025-02-01T00:00:00.000Z');
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
@@ -264,9 +272,10 @@ describe('local income completeness and exact money (SQLite)', () => {
|
||||
});
|
||||
it('distinguishes unavailable calendar comparison without scheduling it', async () => {
|
||||
const result = await read('a', 'from=2024-02-29&to=2024-02-29');
|
||||
expect(result.meta.dependencies?.at(-1)?.unavailableReason).toBe(
|
||||
'CALENDAR_OR_ACCOUNT_HISTORY',
|
||||
);
|
||||
expect(
|
||||
result.meta.dependencies.find(({ role }) => role === 'year')
|
||||
?.unavailableReason,
|
||||
).toBe('CALENDAR_OR_ACCOUNT_HISTORY');
|
||||
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,6 +122,25 @@ describe('screen sync HTTP and SQLite', () => {
|
||||
from: '2026-02-31',
|
||||
to: '2026-03-01',
|
||||
},
|
||||
...(['income', 'events'] as const).flatMap((screen) => [
|
||||
{ screen, accountId: 'all', from: '2025-02-29', to: '2025-03-01' },
|
||||
{ screen, accountId: 'all', from: '2024-03-01', to: '2024-02-29' },
|
||||
{
|
||||
screen,
|
||||
accountId: 'all',
|
||||
from: '2024-02-29T00:00:00Z',
|
||||
to: '2024-02-29',
|
||||
},
|
||||
{
|
||||
screen,
|
||||
accountId: 'all',
|
||||
from: '2024-02-29',
|
||||
to: '2024-02-29T23:59:59Z',
|
||||
},
|
||||
]),
|
||||
{ screen: 'income', accountId: 'all', from: '2024-02-29' },
|
||||
{ screen: 'income', accountId: 'all', to: '2024-02-29' },
|
||||
{ screen: 'events', accountId: 'all', to: '2000-01-01' },
|
||||
{
|
||||
screen: 'stock',
|
||||
instrumentUid: '00000000-0000-4000-8000-000000000001',
|
||||
@@ -145,6 +164,38 @@ describe('screen sync HTTP and SQLite', () => {
|
||||
expect(await prisma.tbankSyncWork.count()).toBe(0);
|
||||
expect(upstream.getOpenAccounts).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each([
|
||||
{
|
||||
screen: 'income',
|
||||
accountId: 'all',
|
||||
from: '2024-02-29',
|
||||
to: '2024-02-29',
|
||||
},
|
||||
{
|
||||
screen: 'events',
|
||||
accountId: 'all',
|
||||
from: '2096-02-29',
|
||||
to: '2096-02-29',
|
||||
},
|
||||
{ screen: 'events', accountId: 'all', from: '2096-02-29' },
|
||||
{ screen: 'events', accountId: 'all', to: '2096-02-29' },
|
||||
{
|
||||
screen: 'operations',
|
||||
accountId: 'all',
|
||||
from: '2024-02-29T00:00:00Z',
|
||||
to: '2024-03-01T00:00:00Z',
|
||||
},
|
||||
])(
|
||||
'preserves valid calendar/instant scope in accepted and status responses: %j',
|
||||
async (scope) => {
|
||||
const response = await post(scope).expect(202);
|
||||
const accepted = response.body as { id: string; scope: object };
|
||||
expect(accepted.scope).toEqual(scope);
|
||||
const result = await settled(accepted.id);
|
||||
expect(result.scope).toEqual(scope);
|
||||
expect(result.state).toBe('succeeded');
|
||||
},
|
||||
);
|
||||
it('keeps failed work terminal, cancels abandoned work without restart, and hides old connection handles', async () => {
|
||||
upstream.getOpenAccounts.mockRejectedValue(
|
||||
new Error('upstream details never exposed'),
|
||||
|
||||
@@ -497,4 +497,38 @@ describe('saved payout calendars and complete forecasts (SQLite HTTP)', () => {
|
||||
await get(`brokerage-accounts/account/events?${query}`).expect(400);
|
||||
expect(upstream.getDividends).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses UTC today for an end-only range and includes the entire leap day', async () => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
await get('brokerage-accounts/account/events?to=2096-02-29').expect(200);
|
||||
await settled();
|
||||
expect(upstream.getDividends).toHaveBeenCalledWith(
|
||||
'synthetic-owner',
|
||||
shareUid,
|
||||
today,
|
||||
'2096-02-29',
|
||||
);
|
||||
upstream.getDividends.mockResolvedValue([
|
||||
{
|
||||
recordDate: {
|
||||
seconds: String(Date.parse('2096-02-29T23:59:59Z') / 1000),
|
||||
},
|
||||
dividendNet: { units: '7', currency: 'rub' },
|
||||
},
|
||||
{
|
||||
recordDate: time('2096-03-01'),
|
||||
dividendNet: { units: '99', currency: 'rub' },
|
||||
},
|
||||
]);
|
||||
const range = { from: '2096-02-29', to: '2096-02-29' };
|
||||
await events(false, 'account', 'owner', range).expect(200);
|
||||
await settled();
|
||||
const result = (await events(true, 'account', 'owner', range).expect(200))
|
||||
.body as UpcomingEventsResourceDto;
|
||||
expect(result.data?.events).toHaveLength(1);
|
||||
expect(result.data?.events[0]).toMatchObject({
|
||||
date: '2096-02-29',
|
||||
projectedAmount: { currency: 'RUB', value: '14' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user