Browse Source

Task/prepare tool to update account balance in MCP (#8177)

* Preparations

* Update changelog
pull/8180/head
Thomas Kaul 23 hours ago
committed by GitHub
parent
commit
a057f1bdc5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 7
      apps/api/src/app/account-balance/account-balance.service.ts
  3. 4
      apps/api/src/app/account/account.service.ts
  4. 6
      apps/api/src/app/activities/activities.service.ts
  5. 31
      apps/api/src/helper/account.helper.spec.ts
  6. 11
      apps/api/src/helper/account.helper.ts
  7. 5
      libs/common/src/lib/dtos/create-account-balance.dto.ts
  8. 69
      libs/common/src/lib/helper.spec.ts
  9. 16
      libs/common/src/lib/helper.ts
  10. 11
      libs/common/src/lib/validator-constraints/is-valid-account-balance-date.spec.ts
  11. 22
      libs/common/src/lib/validator-constraints/is-valid-account-balance-date.ts

1
CHANGELOG.md

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the colors of the muted texts and borders by enabling the dark mode of `bootstrap`
- Reduced the size of the global stylesheet by removing the unused utilities of `bootstrap`
- Hardened the validation of the date in the endpoint to create an account balance
- Upgraded `bootstrap` from version `4.6.2` to `5.3.8`
### Fixed

7
apps/api/src/app/account-balance/account-balance.service.ts

@ -1,8 +1,5 @@
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import {
isAccountBalanceInFuture,
WHERE_ACCOUNT_NOT_EXCLUDED
} from '@ghostfolio/api/helper/account.helper';
import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -11,6 +8,7 @@ import {
DATE_FORMAT,
getStartOfUtcDateOfTomorrow,
getSum,
isAccountBalanceInFuture,
resetHours
} from '@ghostfolio/common/helper';
import {
@ -70,6 +68,7 @@ export class AccountBalanceService {
value: balance
},
where: {
userId,
accountId_date: {
accountId,
date: resetHours(parseISO(date))

4
apps/api/src/app/account/account.service.ts

@ -2,7 +2,6 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import {
getWhereAccountBalanceNotInFuture,
isAccountBalanceInFuture,
WHERE_ACCOUNT_NOT_EXCLUDED
} from '@ghostfolio/api/helper/account.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -10,7 +9,8 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import {
DATE_FORMAT,
getStartOfUtcDateOfTomorrow
getStartOfUtcDateOfTomorrow,
isAccountBalanceInFuture
} from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces';
import { AccountWithBalance } from '@ghostfolio/common/types';

6
apps/api/src/app/activities/activities.service.ts

@ -3,10 +3,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
import { AssetProfileChangedEvent } from '@ghostfolio/api/events/asset-profile-changed.event';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import {
isAccountBalanceInFuture,
WHERE_ACCOUNT_NOT_EXCLUDED
} from '@ghostfolio/api/helper/account.helper';
import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper';
import {
getTagsWithDraftTag,
isActivityInFuture,
@ -38,6 +35,7 @@ import {
getAssetProfileIdentifier,
getStartOfUtcDateOfTomorrow,
hasGhostfolioPrefix,
isAccountBalanceInFuture,
isDraftActivity
} from '@ghostfolio/common/helper';
import {

31
apps/api/src/helper/account.helper.spec.ts

@ -1,7 +1,4 @@
import {
getWhereAccountBalanceNotInFuture,
isAccountBalanceInFuture
} from './account.helper';
import { getWhereAccountBalanceNotInFuture } from './account.helper';
// A user in a time zone ahead of the instance is already on the next day at
// this moment. The expected limit is a moment in UTC, hence this suite must
@ -26,30 +23,4 @@ describe('account.helper', () => {
});
});
});
describe('isAccountBalanceInFuture', () => {
it('should accept the account balance of the current day', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-03T00:00:00.000Z') })
).toBe(false);
});
it('should accept the account balance of a user in a time zone ahead of the instance', () => {
expect(
isAccountBalanceInFuture({ date: START_OF_UTC_DATE_OF_TOMORROW })
).toBe(false);
});
it('should reject an account balance after the start of the UTC date of tomorrow', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-04T00:00:00.001Z') })
).toBe(true);
});
it('should reject an account balance of the day after tomorrow', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-05T00:00:00.000Z') })
).toBe(true);
});
});
});

11
apps/api/src/helper/account.helper.ts

@ -2,7 +2,6 @@ import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config';
import { getStartOfUtcDateOfTomorrow } from '@ghostfolio/common/helper';
import { Prisma } from '@prisma/client';
import { isAfter } from 'date-fns';
export const WHERE_ACCOUNT_NOT_EXCLUDED: Prisma.AccountWhereInput = {
tags: {
@ -31,13 +30,3 @@ export function getWhereAccountBalanceNotInFuture(): Prisma.AccountBalanceWhereI
date: { lte: getStartOfUtcDateOfTomorrow() }
};
}
export function isAccountBalanceInFuture({
date,
startOfUtcDateOfTomorrow = getStartOfUtcDateOfTomorrow()
}: {
date: Date;
startOfUtcDateOfTomorrow?: Date;
}) {
return isAfter(date, startOfUtcDateOfTomorrow);
}

5
libs/common/src/lib/dtos/create-account-balance.dto.ts

@ -1,4 +1,6 @@
import { IsISO8601, IsNumber, IsUUID } from 'class-validator';
import { IsValidAccountBalanceDateConstraint } from '@ghostfolio/common/validator-constraints/is-valid-account-balance-date';
import { IsISO8601, IsNumber, IsUUID, Validate } from 'class-validator';
export class CreateAccountBalanceDto {
@IsUUID()
@ -8,5 +10,6 @@ export class CreateAccountBalanceDto {
balance: number;
@IsISO8601()
@Validate(IsValidAccountBalanceDateConstraint)
date: string;
}

69
libs/common/src/lib/helper.spec.ts

@ -12,11 +12,13 @@ import {
getNumberFormatGroup,
getStringOrNull,
getStringOrUndefined,
isAccountBalanceInFuture,
isAccountExcluded,
isCurrency,
isCurrencySymbol,
isDarkColorScheme,
isSplitRatio,
isValidAccountBalanceDate,
isValidCurrencyCode,
isValidCustomAssetProfileSymbol,
isValidDateAfter1970,
@ -362,6 +364,43 @@ describe('Helper', () => {
});
});
// A user in a time zone ahead of the instance is already on the next day at
// this moment. The expected limit is a moment in UTC, hence this suite must
// give the same result with the instance in any time zone.
describe('Is account balance in future', () => {
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('2026-09-03T23:30:00.000Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('should accept the account balance of the current day', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-03T00:00:00.000Z') })
).toBe(false);
});
it('should accept the account balance of a user in a time zone ahead of the instance', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-04T00:00:00.000Z') })
).toBe(false);
});
it('should reject an account balance after the start of the UTC date of tomorrow', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-04T00:00:00.001Z') })
).toBe(true);
});
it('should reject an account balance of the day after tomorrow', () => {
expect(
isAccountBalanceInFuture({ date: new Date('2026-09-05T00:00:00.000Z') })
).toBe(true);
});
});
describe('Is account excluded', () => {
it('Account with Exclude from Analysis tag', () => {
expect(
@ -520,6 +559,36 @@ describe('Helper', () => {
});
});
describe('Is valid account balance date', () => {
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('2026-09-03T23:30:00.000Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('Accepts a date in the past', () => {
expect(isValidAccountBalanceDate('2026-09-02')).toBe(true);
});
it('Accepts the current date', () => {
expect(isValidAccountBalanceDate('2026-09-03')).toBe(true);
});
it('Accepts the current date of a user in a time zone ahead of the instance', () => {
expect(isValidAccountBalanceDate('2026-09-04')).toBe(true);
});
it('Refuses a date in the future', () => {
expect(isValidAccountBalanceDate('2026-09-05')).toBe(false);
});
it('Refuses a date which is not valid', () => {
expect(isValidAccountBalanceDate('2026-02-30')).toBe(false);
});
});
describe('Is valid currency code', () => {
it('Currency code in lower case', () => {
expect(isValidCurrencyCode('usd')).toEqual(false);

16
libs/common/src/lib/helper.ts

@ -610,6 +610,16 @@ export function interpolate(template: string, context: any) {
});
}
export function isAccountBalanceInFuture({
date,
startOfUtcDateOfTomorrow = getStartOfUtcDateOfTomorrow()
}: {
date: Date;
startOfUtcDateOfTomorrow?: Date;
}) {
return isAfter(date, startOfUtcDateOfTomorrow);
}
export function isAccountExcluded(account?: { tags?: { id: string }[] }) {
return (
account?.tags?.some(({ id }) => {
@ -725,6 +735,12 @@ export function isUserSettingOfAuthenticatedUser(aKey: string) {
);
}
export function isValidAccountBalanceDate(aDate: string) {
const date = parseISO(aDate);
return isValid(date) && !isAccountBalanceInFuture({ date: resetHours(date) });
}
export function isValidCurrencyCode(aCurrency: string) {
if (!aCurrency) {
return false;

11
libs/common/src/lib/validator-constraints/is-valid-account-balance-date.spec.ts

@ -0,0 +1,11 @@
import { IsValidAccountBalanceDateConstraint } from './is-valid-account-balance-date';
describe('IsValidAccountBalanceDateConstraint', () => {
const constraint = new IsValidAccountBalanceDateConstraint();
it('Leaves a value which is not a string to IsISO8601', () => {
expect(constraint.validate(undefined)).toBe(true);
expect(constraint.validate(null)).toBe(true);
expect(constraint.validate(123)).toBe(true);
});
});

22
libs/common/src/lib/validator-constraints/is-valid-account-balance-date.ts

@ -0,0 +1,22 @@
import { isValidAccountBalanceDate } from '@ghostfolio/common/helper';
import {
ValidatorConstraint,
ValidatorConstraintInterface
} from 'class-validator';
@ValidatorConstraint({ name: 'isValidAccountBalanceDate' })
export class IsValidAccountBalanceDateConstraint implements ValidatorConstraintInterface {
public defaultMessage() {
return '$property must be a valid date which is not in the future';
}
public validate(aDate: unknown) {
// IsISO8601 refuses a value which is not a string
if (typeof aDate !== 'string') {
return true;
}
return isValidAccountBalanceDate(aDate);
}
}
Loading…
Cancel
Save