feat: unify income payment cards and drop period comparisons (WealthLedger/main#33) (#83)
feat: unify income payment cards and drop period comparisons (WealthLedger/main#33)
This commit was merged in pull request #83.
This commit is contained in:
@@ -77,7 +77,7 @@ describe('OpenAPI contract (integration)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('exposes required semantic income dependency roles', () => {
|
||||
it('exposes only the selected income dependency role and no comparison data', () => {
|
||||
const schemas = createOpenApiDocument(app).components?.schemas;
|
||||
expect(schemas?.IncomeAnalyticsResourceDto).toMatchObject({
|
||||
properties: {
|
||||
@@ -100,9 +100,25 @@ describe('OpenAPI contract (integration)', () => {
|
||||
expect(dependency.required).toContain('role');
|
||||
expect(dependency).toMatchObject({
|
||||
properties: {
|
||||
role: { type: 'string', enum: ['selected', 'previous', 'year'] },
|
||||
role: { type: 'string', enum: ['selected'] },
|
||||
},
|
||||
});
|
||||
const analytics = schemas?.IncomeAnalyticsDto;
|
||||
assert(analytics && !('$ref' in analytics));
|
||||
expect(analytics.required).toEqual(
|
||||
expect.arrayContaining([
|
||||
'accountId',
|
||||
'period',
|
||||
'totalsByCurrency',
|
||||
'calculatedAt',
|
||||
'bucketGranularity',
|
||||
'buckets',
|
||||
'instruments',
|
||||
'entries',
|
||||
]),
|
||||
);
|
||||
expect(analytics.properties).not.toHaveProperty('previous');
|
||||
expect(analytics.properties).not.toHaveProperty('yearComparison');
|
||||
});
|
||||
|
||||
it('generates the runtime contract without server startup or deployment configuration', async () => {
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('local income completeness and exact money (SQLite)', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
it('returns exact selected sums without a false zero comparison; reopen and observe need no RPC', async () => {
|
||||
it('returns exact selected sums; reopen and observe need no RPC', async () => {
|
||||
for (let index = 0; index < 2; index++) {
|
||||
const result = await read();
|
||||
assert(result.data);
|
||||
@@ -140,22 +140,55 @@ describe('local income completeness and exact money (SQLite)', () => {
|
||||
.filter((row) => row.id === 'same')
|
||||
.map((row) => row.account?.id),
|
||||
).toEqual(['a', 'b']);
|
||||
expect(result.data.previous).toBeNull();
|
||||
expect(result.data.yearComparison).toBeNull();
|
||||
expect(result.meta.dependencies.map(({ role }) => role)).toEqual([
|
||||
'selected',
|
||||
'previous',
|
||||
'year',
|
||||
]);
|
||||
const previous = result.meta.dependencies.find(({ role }) => role === 'previous');
|
||||
expect(previous?.availability).toBe('temporarily-unavailable');
|
||||
expect(previous?.completeness).toBe('incomplete');
|
||||
expect(result.data.calculatedAt).toBe('2025-02-01T00:00:00.000Z');
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
expect(upstream.getOpenAccounts).not.toHaveBeenCalled();
|
||||
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
it('uses saved instrument logos for income entries without changing their financial data', async () => {
|
||||
const before = await read();
|
||||
const base = before.data;
|
||||
assert(base);
|
||||
const fetchedAt = new Date();
|
||||
await prisma.tbankOperation.update({
|
||||
where: {
|
||||
userId_revision_accountId_brokerOperationId: {
|
||||
userId: 'income-owner', revision, accountId: 'a', brokerOperationId: 'same',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
payload: {
|
||||
id: 'same', figi: 'income-figi', type: 'OPERATION_TYPE_COUPON',
|
||||
date: { seconds: String(Date.parse('2025-01-15Z') / 1000) },
|
||||
payment: { units: '10', nano: 1, currency: 'rub' },
|
||||
},
|
||||
},
|
||||
});
|
||||
await prisma.tbankInstrument.create({
|
||||
data: {
|
||||
provider: 'tbank', uid: 'income-uid', figi: 'income-figi',
|
||||
instrumentType: 'bond', fetchedAt,
|
||||
metadata: { uid: 'income-uid', figi: 'income-figi', name: 'Bond', brand: { logoName: 'bond.png' } },
|
||||
},
|
||||
});
|
||||
await prisma.tbankInstrumentLookup.create({
|
||||
data: {
|
||||
provider: 'tbank', identifierType: 'figi', identifier: 'income-figi',
|
||||
matchedUids: ['income-uid'], fetchedAt,
|
||||
},
|
||||
});
|
||||
const after = await read();
|
||||
assert(after.data);
|
||||
expect(after.data.entries.find((entry) => entry.account?.id === 'a' && entry.id === 'same')?.logoName).toBe('bond.png');
|
||||
expect(after.data.entries.find((entry) => entry.account?.id === 'b' && entry.id === 'same')?.logoName).toBeNull();
|
||||
expect(after.data.totalsByCurrency).toEqual(base.totalsByCurrency);
|
||||
expect(after.data.calculatedAt).toBe(base.calculatedAt);
|
||||
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
it('requires every account and separates missing, partial and confirmed empty periods', async () => {
|
||||
await prisma.tbankOperationCoverage.deleteMany({
|
||||
where: { accountId: 'b' },
|
||||
@@ -176,15 +209,7 @@ describe('local income completeness and exact money (SQLite)', () => {
|
||||
).toEqual([]);
|
||||
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
it('completes previous/yearAgo independently and counts corrected identities once', async () => {
|
||||
for (const account of ['a', 'b']) {
|
||||
await coverage(account, new Date('2024-11-30T21:00:00Z'), from);
|
||||
await coverage(
|
||||
account,
|
||||
new Date('2023-12-31T21:00:00Z'),
|
||||
new Date('2024-01-31T21:00:00Z'),
|
||||
);
|
||||
}
|
||||
it('counts corrected identities once for the selected period', async () => {
|
||||
await prisma.tbankOperation.updateMany({
|
||||
where: { accountId: 'a', brokerOperationId: 'same' },
|
||||
data: {
|
||||
@@ -197,14 +222,12 @@ describe('local income completeness and exact money (SQLite)', () => {
|
||||
},
|
||||
});
|
||||
const result = await read();
|
||||
assert(result.data?.previous && result.data.yearComparison);
|
||||
assert(result.data);
|
||||
expect(result.data.entries).toHaveLength(6);
|
||||
expect(
|
||||
result.data.previous.totalsByCurrency[0].difference.total.value,
|
||||
).toBe('25.000000002');
|
||||
expect(
|
||||
result.data.yearComparison.totalsByCurrency[0].compared.total.value,
|
||||
).toBe('0');
|
||||
result.data.totalsByCurrency.find(({ currency }) => currency === 'RUB')
|
||||
?.total,
|
||||
).toEqual({ currency: 'RUB', value: '25.000000002' });
|
||||
});
|
||||
it('preserves the complete financial result and timestamp through failed atomic reconciliation', async () => {
|
||||
const before = await read();
|
||||
@@ -276,12 +299,13 @@ describe('local income completeness and exact money (SQLite)', () => {
|
||||
const response = await get('all', undefined, false).expect(200);
|
||||
expect((response.body as IncomeAnalyticsResourceDto).data).not.toBeNull();
|
||||
});
|
||||
it('distinguishes unavailable calendar comparison without scheduling it', async () => {
|
||||
it('plans only the selected period for a leap day without scheduling it', async () => {
|
||||
const result = await read('a', 'from=2024-02-29&to=2024-02-29');
|
||||
expect(
|
||||
result.meta.dependencies.find(({ role }) => role === 'year')
|
||||
?.unavailableReason,
|
||||
).toBe('CALENDAR_OR_ACCOUNT_HISTORY');
|
||||
expect(result.meta.dependencies.map(({ role }) => role)).toEqual([
|
||||
'selected',
|
||||
]);
|
||||
expect(result.meta.dependencies[0].availability).toBe('temporarily-unavailable');
|
||||
expect(result.data).toBeNull();
|
||||
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user